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: Avoid reserved application message types subscribe, unsubscribe, channelMessage, request, and response; the SDK uses them for pub/sub and RPC. Start with the guide chooser for complete workflows.
Constructor
const vdo = new VDONinjaSDK(options)
- host: WebSocket URL (default:
wss://wss.vdo.ninja) - room: Initial room to join (optional)
- password: Original password text, or
falseto disable password-based signaling encryption. Constructor default is the sharedsomeEncryptionKey123, not an autogenerated private key. WebRTC media/data transport remains encrypted. - 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.
- Local disconnect emits
disconnectedonce after cleanup. Unexpected socket loss emits it immediately; await the promise orteardownCompletefor local cleanup.
- 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:trueto reset.
- Re-applies bitrate/codec/resolution preferences to the active publisher; use
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
fileListevent arrives at connect time. - allowresources: advertise willingness to receive the
resourceschannel. Off by default, matching VDO.Ninja, where it requires the&resourcesURL flag.
- 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
- 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.
sendBinaryuses a dedicatedx-binlane instead. A VDO.Ninja peer ignores that lane, so nothing breaks; it simply will not receive the bytes, having no generic binary sink. waitForDraindefaults 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: falseplusmaxRetransmitsormaxPacketLifeTimegives partially-reliable delivery, which suits chunked transfer that already indexes and hashes its own chunks.maxRetransmitsandmaxPacketLifeTimeare mutually exclusive; supplying both throws.
- The label is forced into the
- getChannel(uuid, label): RTCDataChannel | null
- getBufferedAmount(uuid, label?): number | null
- Omit
labelfor the control channel. Null if the peer or channel is unknown.
- Omit
- 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-binlane; 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.
// 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.addEventListener('bufferedAmountLow', r, { once: true }));
}
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.
nameis required unless the source is a File. - restricted: a peer UUID to offer the file to only that peer;
falseoffers it to all. - Advertises the new file immediately to eligible publisher-side peers that sent
downloads: true.
- source: Blob, File, ArrayBuffer, or any typed array.
- 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
fileChunkevents instead of buffering the whole file in memory;bytesis 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.
- Result:
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.
// 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;typesets the MIME type (default image/png);sizeis filled in for you. - data: ArrayBuffer or typed array.
- Throws unless the peer advertised
allowresources, which VDO.Ninja viewers do via the&resourcesURL flag and SDK viewers viaview(id, { allowresources: true }). - The publisher's
metamust be an object keyed by template name for a VDO.Ninja receiver to store anything; it rejects a string.
- metadata: must include
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<Record<string, Array<Object>>>
- Keyed by peer UUID. Each array includes both publisher/viewer reports, with
connectionType: 'publisher' | 'viewer'on each record to distinguish directions. An unknown UUID returns{}. A failed report does not prevent other directions from being returned.
- Keyed by peer UUID. Each array includes both publisher/viewer reports, with
- 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 }rttMsis milliseconds (the underlying stat is in seconds).candidatePairTypelooks like"host/srflx"or"relay/host".lossRateisnullon a data-only peer rather than a misleading zero — data channels carry no RTP, so there is nothing to measure loss against.- Returns
nullfor 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.
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 once on a deliberate disconnect with
phase: 'teardown'after cleanup. Unexpected socket loss emitsphase: 'socket'immediately.intentionaldistinguishes local disconnect from a dropped connection. Repeated local calls return the same promise and do not emit extra events.
- Fires once on a deliberate disconnect with
- 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 }
- Received remote metadata, including
info.label; processed even when bundled with audio/video preferences. It can arrive afterpeerConnected. connection.infostarts empty and contains remote metadata only;connection.localInfoholds outgoing publisher metadata.- Connection roles are local:
publisherconnections carry your stream ID;viewerconnections carry the remote publisher's stream ID. Use room discovery plus viewer-sidepeerInfoto build a stream/name list.
- Received remote metadata, including
- peerLatency { uuid, latency, streamID }
- obsState { uuid, streamID, state, update } — OBS browser-source state received by an SDK publisher.
updatecontains the sparse fields from the latest message;stateis the merged state for that OBS peer.
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 rawRTCDataChannelis 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 }.
- OBS state is reported per connected OBS viewer. Applications combining multiple OBS browser sources should aggregate by
uuid; common fields arevisibility,sourceActive,recording, andstreaming.
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.)
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.
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
// 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.