SDK Compatibility Contract

The SDK is intended to remain compatible with earlier SDK releases and with VDO.Ninja peers. Compatibility has three separate boundaries.

1. WebSocket wire compatibility

The signaling server sees the same messages used by VDO.Ninja, including joinroom, listing, seed, play, offerSDP, videoaddedtoroom, SDP descriptions, ICE candidates, bye, and hangup.

Reliability changes must not introduce SDK-specific WebSocket request types. Reconnection restores application intent by replaying the existing room, seed, and play operations.

2. WebRTC and data-channel compatibility

Password correction: constructor passwords containing special characters previously became encoded again on room join and reconnect. The SDK now preserves the sanitized value internally and refreshes the stream hash when the room password changes. Integrations relying on the old double encoding should use the original password on both peers. No wire fields or cryptographic algorithms changed.

When password encryption is enabled, an encryption failure stops that SDP/ICE message instead of silently sending it as plaintext. Applications that explicitly disable password encryption keep their existing behavior.

Channel labels

VDO.Ninja opens up to four kinds of data channel per peer and dispatches incoming channels by label. The SDK matches that dispatch exactly.

Label Options Carries
sendChannel ordered JSON control protocol
chunked {ordered:true}, arraybuffer Chunked media transport
resources {ordered:true, maxRetransmits:30} Resource/template sync
x-* caller's choice Reserved. Third-party SDK channels; never interpreted
anything else arraybuffer File transfer; the label is the file ID

A falsy label is treated as the control channel, matching VDO.Ninja.

The reserved x- namespace

VDO.Ninja ignores x- labels in both ondatachannel handlers (session.isReservedChannelLabel) rather than passing them to its file-transfer receiver. The SDK honours the same reservation: an x- channel is surfaced to the application via channelOpen and is never treated as a file transfer, and hostFile() refuses a file ID using the prefix.

The prefix is collision-proof by construction: every label either side opens is drawn from an alphanumeric charset or is a known literal, so no existing label can begin with x-. If either generateStreamID is ever changed to emit punctuation, this guarantee breaks.

Without the reservation, a binary frame on an unrecognised label costs VDO.Ninja two errors per frame — a JSON.parse SyntaxError on the header retry plus a TypeError from transferList[false] — which is a flood on a bulk transfer.

Verified: 4MB flooded onto an x-bulk channel at a live VDO.Ninja tab produced zero console errors, left the control channel open and ICE connected, created no transfer entries, and did not touch the image path.

Binary never goes on the control channel

VDO.Ninja's control-channel handler treats any object payload as a WebP image frame: it builds an <img>, sets src to a blob URL of the bytes, and swaps it into the view (webrtc.js:21219). Raw bytes sent there would visibly corrupt a viewer rather than being ignored.

So binary belongs only on reserved channels. sendBinary() uses a dedicated x-bin lane for exactly this reason. A VDO.Ninja peer ignores that lane — it receives nothing, which is correct, since it has no generic binary sink.

Only the control channel carries the JSON control protocol: SDP, ICE, ping/pong, publisher info, viewer media preferences, generic pipe data, fileList, requestFile, and iceRestartRequest. The other labels are binary side-channels.

An auxiliary channel must never be adopted as the connection's control channel. Doing so re-sends publisher info, starts a duplicate ping monitor, emits a spurious dataChannelOpen, and attempts to JSON.parse binary frames. connection.dataChannel always means the control channel; connection.channels is the label-keyed registry of all channels on that peer.

File transfer

  1. Host advertises {fileList:[{id,name,size}]} on the control channel
  2. Peer requests {requestFile:<id>} on the control channel
  3. Host opens a data channel labelled <id>
  4. Host sends {type:"filetransfer",size,filename,id}
  5. Host sends 16384-byte binary chunks
  6. Host sends EOF1 (complete) or EOF2 (cancelled)

The SDK's sender waits for bufferedAmount to drain between chunks and before EOF1, allows the native adapter a transport turn, then treats the receiver's post-EOF1 channel close as the delivery acknowledgement. VDO.Ninja's sender does none of these. This is deliberate and invisible to the receiver: framing and chunk size are unchanged, so it stays wire-compatible while avoiding SCTP overrun, terminator races, and premature close on native adapters.

Resources

Channel resources, opened only toward a peer that advertised allowresources. A JSON metadata frame carrying at least templateName and size, then 16384-byte binary chunks until size bytes have arrived. The SDK refuses an unsolicited resources channel, as VDO.Ninja does.

Capability negotiation

Capabilities travel viewer to publisher, in the viewer's preferences message — not in the publisher's info payload. The publisher then decides what to open (webrtc.js:12889-12894):

Viewer advertises Publisher does
downloads: true sends its fileList
allowresources: true opens the resources channel
allowchunked + chunkprotocols opens the chunked channel

Getting the direction wrong means the capability is simply never acted on. Peers that predate a key ignore it, because VDO.Ninja's handlers are independent if ("key" in msg) checks rather than an exhaustive switch. New capability keys must preserve that property.

Publisher meta must stay a non-null object

info.meta is accepted only when it is truthy and typeof === "object"; anything else makes the receiver set meta = false. Resources are stored into meta[templateName].value, so a string-sanitized meta silently disables the entire resources path. Sanitize the values inside meta, never the container.

meta: null is the trap: typeof null === "object", so a naive type check accepts it and then throws on first use. VDO.Ninja now guards for truthiness and warns on a truthy non-object; the SDK drops the key entirely rather than putting meta: null on the wire. Both halves are needed — the guard protects against old SDK builds, and dropping the key protects against receivers that predate the guard.

Not yet implemented

The SDK does not speak the chunked media protocol (indexed-v1 / positional-v1). It accepts and ignores a chunked channel and emits unsupportedChannel rather than mis-routing it.

The MCP package may use its own versioned envelopes inside generic application data. Those envelopes are payloads between MCP peers; they are not signaling-server protocol additions.

3. SDK-local API compatibility

Public methods, options, aliases, event names, and event payloads should remain additive across minor versions. Existing compatibility aliases, including dataRecieved, remain available even when a preferred spelling exists.

Additive includes giving a previously void method a return value, and adding a detail to an event that had none — callers ignoring either are unaffected. disconnect() now returns a promise and disconnected now carries { intentional, reason, willReconnect, phase } on that basis.

Duplicate-event correction: a deliberate disconnect() now emits disconnected once, with phase: 'teardown'. It no longer emits the intermediate socket notification. Unexpected socket loss still emits phase: 'socket' immediately; teardownComplete remains the separate cleanup signal. Consumers relying on two local notifications should use the single teardown notification instead.

Type definitions in vdoninja-sdk.d.ts are part of this surface. npm run test:types typechecks a consumer against them under --strict so they cannot drift from the implementation unnoticed.

SDK events do not need to have the same names as VDO.Ninja internals. Similar naming is useful for applications migrating away from an iframe, but it is not a wire-level requirement.

Important SDK-local events include:

Known consumers

Compatibility checks should cover at least:

An embedded SDK consumer may not receive fixes until its vendored file is refreshed. Changes should therefore tolerate mixed SDK versions in the same room.