@vdoninja/sdk
Version:
AI-friendly P2P communication SDK for audio, video, and data. Includes WHIP/WHEP clients for publishing to Twitch, Meshcast, Cloudflare Stream
425 lines (340 loc) • 19.8 kB
Markdown
# VDO.Ninja SDK — API Reference
This page lists the primary methods, helpers, aliases, and events exposed by the SDK. It complements the short examples in README.
Tip: For custom app protocols, avoid using reserved types 'subscribe' | 'unsubscribe' | 'channelMessage' on the pipe payload — those are used by the SDK pub/sub system.
## Constructor
```js
const vdo = new VDONinjaSDK(options)
```
- host: WebSocket URL (default: `wss://wss.vdo.ninja`)
- room: Initial room to join (optional)
- password: Room password or false to disable encryption (default: autogenerated key)
- label: Optional human-readable label for your stream/view
- debug: Enable SDK logs (boolean)
- turnServers: null=auto-fetch, false=disable, or custom ICE servers
- forceTURN: Force relay mode (boolean)
- stunServers, configuration, salt, plus advanced info flags (see README for details)
- autoRecover: Recover failed peer directions automatically (default: true)
- autoRelay: Temporarily escalate failed direct paths to TURN (default: true)
- disconnectGracePeriod: Grace period for temporary ICE disconnects (default: 5000 ms)
- connectionTimeout: Initial peer connection timeout (default: 20000 ms)
- recoveryTimeout: Wait between bounded recovery phases (default: 12000 ms)
- relayRestoreDelay: Restore direct-first ICE policy after recovery (default: 45000 ms)
## Connection
- connect(): Promise<void>
- disconnect(): Promise<void>
- Resolves once teardown genuinely completes: bye messages flushed, peers closed, timers
cleared, socket shut. Exiting the process before it resolves can crash the native
WebRTC module mid-teardown.
- Safe to call more than once; repeat calls return the same promise and do not tear down
twice.
- The returned promise is new in v1.5. Callers that ignore it behave exactly as before.
- `disconnected` is **not** a completion signal — it also fires when the socket closes,
partway through. Await the promise, or listen for `teardownComplete`.
- joinRoom({ room, password }): Promise<void>
- leaveRoom(): void
- autoConnect(roomOrOptions, filter?): Promise<{ stop: Function, streamID: string }>
- Shorthand to connect, join, announce, and view peers in a mesh.
- Options: { room, mode: 'half' | 'full', view: { audio, video }, label, password, streamID?, filter }
## Publishing (Sender)
- publish(stream, { streamID, room?, label?, password?, media? }): Promise<string>
- media: { video?: { codec?, maxBitrate?, resolution?, frameRate? }, audio?: { codec?, maxBitrate? } }
- announce({ streamID?, room?, label?, password? }): Promise<string>
- Data-only publishing; generates a streamID if not provided.
- stopPublishing(): void
- updatePublisherMedia({ media?, videoBitrate?, videoCodec?, clear? }): Promise<Object|null>
- Re-applies bitrate/codec/resolution preferences to the active publisher; use `clear:true` to reset.
## Viewing (Receiver)
- view(streamID, { audio=true, video=true, label?, downloads=true, allowresources=false }): Promise<RTCPeerConnection>
- downloads: advertise willingness to receive file offers. A VDO.Ninja publisher only
sends its file list to a viewer that asked for it, so turning this off means no
`fileList` event arrives at connect time.
- allowresources: advertise willingness to receive the `resources` channel. Off by
default, matching VDO.Ninja, where it requires the `&resources` URL flag.
- stopViewing(streamID): void
## Quick Helpers
- quickPublish({ stream, room?, streamID?, label?, password? }): Promise<string>
- quickView({ streamID, room?, audio?, video?, label?, password?, dataOnly? }): Promise<RTCPeerConnection>
- dataOnly: true maps to { audio:false, video:false }
- quickSubscribe({ streamID, room?, ... } = {}): Promise<RTCPeerConnection>
- Convenience wrapper around quickView that defaults to dataOnly unless explicitly overridden.
## Data Communication
- sendData(data, target?): boolean
- target: undefined → all connected peers; 'uuid' → a specific peer; { uuid?, type?: 'viewer'|'publisher', streamID?, preference?: 'any'|'viewer'|'publisher'|'all', allowFallback? }
- preference controls data-channel routing. 'any' (default) tries publisher first, then viewer; an explicit role uses only that role; 'all' may duplicate messages.
- allowFallback controls the separate WebSocket signaling fallback. It defaults to false; set it to true to opt in.
- sendPing(uuid?): boolean
- request(requestType, data, targetUUID, timeout=5000): Promise<any>
- respond(requestId, data, targetUUID): boolean
- onRequest(requestType, handler): void
## Binary and Additional Channels
Turns the SDK from a messaging transport into a bulk transport: raw bytes, a second
channel so bulk traffic stops head-of-line blocking control messages, partial reliability,
and a real backpressure signal.
Everything here lives in the reserved `x-` namespace, which VDO.Ninja ignores by contract.
A reserved channel is therefore safe to open toward any peer — verified with 4MB flooded
at a live VDO.Ninja tab, which saw zero errors and kept its control channel.
- sendBinary(data, uuid, { ordered?, maxRetransmits?, maxPacketLifeTime?, waitForDrain?, timeout? }): Promise<boolean>
- Bytes go out untouched — no JSON, no base64.
- **Never uses the control channel.** VDO.Ninja renders any binary payload there as a
WebP image, so raw bytes would visibly corrupt a viewer. `sendBinary` uses a dedicated
`x-bin` lane instead. A VDO.Ninja peer ignores that lane, so nothing breaks; it simply
will not receive the bytes, having no generic binary sink.
- `waitForDrain` defaults true, applying backpressure before each send.
- openChannel(uuid, label, { ordered?, maxRetransmits?, maxPacketLifeTime?, protocol?, timeout? }): Promise<RTCDataChannel>
- The label is forced into the `x-` namespace; `'bulk'` becomes `'x-bulk'`.
- Resolves once the channel is open. Idempotent — reopening returns the existing channel.
- `ordered: false` plus `maxRetransmits` or `maxPacketLifeTime` gives partially-reliable
delivery, which suits chunked transfer that already indexes and hashes its own chunks.
- `maxRetransmits` and `maxPacketLifeTime` are mutually exclusive; supplying both throws.
- getChannel(uuid, label): RTCDataChannel | null
- getBufferedAmount(uuid, label?): number | null
- Omit `label` for the control channel. Null if the peer or channel is unknown.
- getMaxMessageSize(uuid): number | null
- The negotiated SCTP limit. Null when the transport has not reported one — see the
implementation note below. On null, 65536 is the conventional safe assumption;
VDO.Ninja's own file transfer uses 16384.
Events:
- binaryReceived: { uuid, streamID, bytes: Uint8Array, data }
- channelOpen: { uuid, streamID, label, channel } — a peer opened a reserved channel other
than the SDK's own `x-bin` lane; the raw channel is handed over
- bufferedAmountLow: { uuid, streamID, label, bufferedAmount }
- Emitted after an observed queue above the SDK's 256 KiB low-water mark drains to or
below it. A positive value already below the mark does not imply a pending event.
```js
// Bulk on its own channel, unreliable and unordered, with backpressure
const bulk = await vdo.openChannel(uuid, 'bulk', { ordered: false, maxRetransmits: 0 });
for (const chunk of chunks) {
while (vdo.getBufferedAmount(uuid, 'bulk') > 1_000_000) {
await new Promise(r => vdo.once('bufferedAmountLow', r));
}
bulk.send(chunk);
}
```
### Implementation note: backpressure needs a transport that reports it
Some `@roamhq/wrtc` builds report `bufferedAmount: 0` no matter how much is queued —
2.4MB in Windows testing. Other builds report queued bytes but omit the native
`bufferedamountlow` event; the SDK polls as a fallback for those builds. If a build always
reports zero, `getBufferedAmount` remains zero, `bufferedAmountLow` cannot observe a
high-to-low transition, and `waitForDrain` is a no-op. Browsers report it correctly.
This is a limitation of the WebRTC implementation, not the SDK. If you need flow control
in Node today, keep an application-level cap on outstanding sends rather than relying on
the drain signal.
## File Transfer
Implements VDO.Ninja's native file transfer, so an SDK peer and a VDO.Ninja browser tab
can exchange files in either direction. Files move over their own data channel, never the
control channel. See `docs/compatibility.md` for the wire format.
Hosting:
- hostFile(source, { name?, id?, restricted=false }): { id, name, size }
- source: Blob, File, ArrayBuffer, or any typed array. `name` is required unless the
source is a File.
- restricted: a peer UUID to offer the file to only that peer; `false` offers it to all.
- Advertises the new file immediately to eligible publisher-side peers that sent
`downloads: true`.
- unhostFile(id): boolean
- Stops serving the file and cancels transfers in flight. Note that VDO.Ninja's protocol
has no un-advertise message, so a peer that already saw the offer keeps displaying it;
requesting it afterwards is refused.
- getHostedFiles(): Array<{ id, name, size, restricted }>
Receiving:
- requestFile(uuid, fileId, { stream=false, timeout=30000 }): Promise<Result>
- Result: `{ id, name, size, uuid, streamID, bytes: Uint8Array, blob?: Blob }`
- stream: true emits `fileChunk` events instead of buffering the whole file in memory;
`bytes` is then omitted.
- Rejects if the peer never starts the transfer, if the channel closes early, or if the
delivered byte count does not match the announced size.
Events:
- fileList: { uuid, streamID, files: [{ id, name, size }] } — a peer advertised files
- fileTransferStart: { uuid, id, name, size, direction, requested }
- fileTransferProgress: { uuid, id, name, direction, bytes, size, progress }
- fileChunk: { uuid, id, name, chunk: Uint8Array, bytes, size } — streaming mode only
- fileTransferComplete: { uuid, id, name, size, direction }
- fileTransferCancelled: { uuid, id, name, direction }
- fileTransferError: { uuid, id, name, direction, error }
`direction` is `'inbound'` or `'outbound'` on every transfer event.
```js
// Host a file and let a VDO.Ninja viewer download it from its chat feed
const vdo = new VDONinjaSDK();
await vdo.connect();
await vdo.joinRoom({ room: 'myroom' });
await vdo.announce({ streamID: 'mystream' });
const offered = vdo.hostFile(bytes, { name: 'report.pdf' });
// Or download what a peer is offering
vdo.addEventListener('fileList', async (e) => {
const file = e.detail.files[0];
const { bytes } = await vdo.requestFile(e.detail.uuid, file.id);
});
```
## Resources
VDO.Ninja's `resources` channel carries images keyed by meta template name. The receiver
turns each into an object URL and stores it under `meta[templateName].value`.
- sendResource(uuid, metadata, data): Promise<void>
- metadata: must include `templateName`; `type` sets the MIME type (default image/png);
`size` is filled in for you.
- data: ArrayBuffer or typed array.
- Throws unless the peer advertised `allowresources`, which VDO.Ninja viewers do via the
`&resources` URL flag and SDK viewers via `view(id, { allowresources: true })`.
- The publisher's `meta` must be an **object** keyed by template name for a VDO.Ninja
receiver to store anything; it rejects a string.
Event:
- resourceReceived: { uuid, streamID, metadata, bytes: Uint8Array }
## Pub/Sub
- subscribe(channels: string | string[]): void
- unsubscribe(channels: string | string[]): void
- getSubscriptions(): string[]
- publishToChannel(channel: string, data: any, target = 'all'): boolean
- getPeerSubscriptions(uuid: string): string[]
Events:
- channelMessage: { channel, data, timestamp, uuid } — only emitted if locally subscribed
- peerSubscribed: { uuid, channels, allChannels }
- peerUnsubscribed: { uuid, channels, allChannels }
## Utilities
- getStats(uuid?): Promise<RTCStatsReport | any>
- getPeerQuality(uuid): Promise<PeerQuality | null>
- Digested per-peer link quality, so a peer can be ranked as soon as ICE settles instead
of after the application has measured RTT itself.
- `{ rttMs, lossRate, candidatePairType, relayed, availableOutgoingBitrate, bytesSent, bytesReceived }`
- `rttMs` is milliseconds (the underlying stat is in seconds). `candidatePairType` looks
like `"host/srflx"` or `"relay/host"`.
- `lossRate` is `null` on a data-only peer rather than a misleading zero — data channels
carry no RTP, so there is nothing to measure loss against.
- Returns `null` for an unknown peer or when no statistics are available.
- on/off/once(eventName, handler): chaining shorthands for add/removeEventListener
## TypeScript
Type definitions ship with the package (`vdoninja-sdk.d.ts`); no `@types` install needed.
```ts
import VDONinja, { PeerQuality, FileTransferResult } from '@vdoninja/sdk';
```
`on`/`off`/`once` are typed against the event map, so `e.detail` is inferred per event
name. `npm run test:types` typechecks a consumer against the shipped declarations under
`--strict`, so the definitions cannot silently drift from the implementation.
## Aliases (Common Names)
- Viewing: play(), watch(), startViewing() → view()
- Publishing: stream(), broadcast(), startPublishing(), share() → publish()
- Quick: quickStream(), quickBroadcast(), quickShare() → quickPublish()
- Stop viewing: stop(), stopPlaying(), stopWatching() → stopViewing()
- Stop publishing: stopStreaming(), stopBroadcasting(), stopSharing(), unpublish() → stopPublishing()
- Connection: join(), enterRoom(), enter() → joinRoom(); leave(), exitRoom() → leaveRoom()
Note: The viewing alias `unsubscribe(streamID)` that conflicted with pub/sub has been removed.
## Events (Selected)
Connection & Room
- connected
- disconnected { intentional, reason, willReconnect, phase }
- Fires twice on a deliberate disconnect: once with `phase: 'socket'` when the socket
closes, once with `phase: 'teardown'` when cleanup finishes. `intentional`
distinguishes a local `disconnect()` from a dropped connection, so callers no longer
have to keep their own flag to avoid announcing a reconnect that will not happen.
- teardownComplete { reason } — emitted exactly once, only when cleanup genuinely finishes
- reconnecting, reconnected, reconnectFailed
- connectionRecovering, connectionRecovered, connectionFailed, relayEscalated, relayRestored
- iframe-friendly aliases: hss-connection, room-peer-listing, push-connection, view-connection
- roomJoined { room }, roomLeft { room }
- listing { list, raw }, peerListing (raw VDO.Ninja listings)
Peer & Channel
- peerConnected { uuid, connection }
- peerDisconnected { uuid }
- dataChannelOpen { uuid, type, streamID }, dataChannelClose { uuid, type, streamID }
- peerInfo { uuid, streamID, info }
- peerLatency { uuid, latency, streamID }
Data
- dataReceived { data, uuid, streamID?, fallback? }
- data (legacy, original WS/DC format)
- Typo alias also emitted: dataRecieved
File Transfer & Resources (see the sections above for payloads)
- fileList, fileTransferStart, fileTransferProgress, fileChunk
- fileTransferComplete, fileTransferCancelled, fileTransferError
- resourceReceived
- channelOpen { uuid, streamID, label, channel } — a peer opened a reserved `x-*` channel.
The raw `RTCDataChannel` is handed over; the application owns whatever protocol runs on
it. VDO.Ninja ignores these labels by contract, so they are safe to open toward any peer.
- unsupportedChannel { uuid, streamID, label } — a peer opened an auxiliary channel this
SDK build does not speak (currently `chunked`); it is accepted and ignored rather than
mis-routed
Media
- track { track, streams?, uuid, streamID }
- trackAdded, trackRemoved, trackReplaced
State & Errors
- publishing { streamID, hashedStreamID }
- publishingStopped
- viewingStopped { streamID }
- connectionFailed { reason, ... }
- iceRestart { uuid, streamID, reason }
- approved, rejected, bye, hangup, transferred, alert { message }, error { error, details? }
## Compatibility Notes
- Reserved pipe types: 'subscribe' | 'unsubscribe' | 'channelMessage' are used by the SDK pub/sub system and do not emit dataReceived; use the pub/sub helpers and events.
- Viewer preferences (audio/video) are sent viewer → publisher via the viewer data channel, not the reverse.
- Legacy support: quickView and autoConnect can infer data-only; you can also pass { dataOnly:true }.
- WebSocket fallback: sendData can deliver via signaling when allowFallback is true (default false); dataReceived includes { fallback:true }.
---
## WHIP/WHEP Clients
Standalone clients for standard WebRTC-HTTP streaming protocols. These work independently of the VDO.Ninja P2P system.
### WHIPClient (whip-client.js)
Publish media streams to WHIP-compatible endpoints (Twitch, Meshcast, Cloudflare, etc.)
```js
const client = new WHIPClient(endpoint, options)
```
Options:
- endpoint: WHIP endpoint URL (required)
- authToken: Bearer token for authentication
- videoCodec: Preferred codec ('h264', 'vp8', 'vp9', 'av1')
- videoBitrate: Target video bitrate in kbps
- audioBitrate: Target audio bitrate in kbps
- trickleIce: Enable trickle ICE (default: true)
- iceServers: Custom ICE servers array
- headers: Additional HTTP headers
- debug: Enable debug logging
Methods:
- publish(stream): Promise<void> — Publish a MediaStream
- replaceTrack(oldTrack, newTrack): Promise<void> — Replace a track mid-session
- stop(): Promise<void> — Stop publishing and cleanup
- getStats(): Promise<RTCStatsReport> — Get connection statistics
- restartIce(): Promise<void> — Restart ICE connection
Events: connecting, connected, icestate, connectionstate, error, disconnected, stopped
### WHEPClient (whep-client.js)
Consume media streams from WHEP-compatible endpoints.
```js
const client = new WHEPClient(endpoint, options)
```
Options:
- endpoint: WHEP endpoint URL (required)
- authToken: Bearer token for authentication
- audio: Request audio track (default: true)
- video: Request video track (default: true)
- trickleIce: Enable trickle ICE (default: true)
- iceServers: Custom ICE servers array
- headers: Additional HTTP headers
- debug: Enable debug logging
Methods:
- view(): Promise<MediaStream> — Start viewing
- getStream(): MediaStream | null — Get the received MediaStream
- muteAudio(muted): void — Mute/unmute audio locally
- muteVideo(muted): void — Mute/unmute video locally
- stop(): Promise<void> — Stop viewing and cleanup
- getStats(): Promise<RTCStatsReport> — Get connection statistics
- restartIce(): Promise<void> — Restart ICE connection
Events: connecting, connected, track, icestate, connectionstate, error, disconnected, stopped
### Supported WHIP/WHEP Services
| Service | WHIP URL | WHEP URL |
|---------|----------|----------|
| Meshcast.io | `https://cae1.meshcast.io/whip/{streamId}` | `https://cae1.meshcast.io/whep/{streamId}` |
| Twitch | `https://g.webrtc.live-video.net:4443/v2/offer` | N/A |
| Cloudflare Stream | Your Stream endpoint | Your Stream endpoint |
| Dolby.io | Your Dolby endpoint | Your Dolby endpoint |
### WHIP/WHEP Example
```js
// Publish to Meshcast
const whip = new WHIPClient('https://cae1.meshcast.io/whip/mystream', {
videoCodec: 'h264',
videoBitrate: 2500
});
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
await whip.publish(stream);
// View at: https://meshcast.io/mystream
// Watch from Meshcast
const whep = new WHEPClient('https://cae1.meshcast.io/whep/mystream');
whep.addEventListener('track', (e) => {
document.getElementById('video').srcObject = e.detail.streams[0];
});
await whep.view();
```
---
See README for end-to-end examples and the demos folder for runnable samples.