# 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&lt;boolean&gt;
  - 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&lt;RTCDataChannel&gt;
  - 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.
