@vdoninja/sdk
Version:
AI-friendly P2P communication SDK for audio, video, and data. Includes WHIP/WHEP clients for publishing to Twitch, Meshcast, Cloudflare Stream
172 lines (125 loc) • 8.93 kB
Markdown
# 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
- The publisher creates the `sendChannel` data channel and owns SDP offers.
- The viewer answers offers.
- ICE candidate `type` routing remains compatible with VDO.Ninja's publisher/viewer directions.
- Password hashing, salt behavior, and encrypted SDP/ICE formats remain unchanged.
### 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.
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:
- Signaling: `connected`, `disconnected`, `teardownComplete`, `reconnecting`, `reconnected`, `reconnectFailed`
- Room/discovery: `roomJoined`, `roomLeft`, `listing`, `streamAdded`, `videoaddedtoroom`
- Peer health: `peerConnected`, `peerDisconnected`, `connectionRecovering`, `connectionRecovered`, `connectionFailed`, `iceRestart`
- Data: `dataChannelOpen`, `dataChannelClose`, `dataReceived`, `dataRecieved`, `data`, `peerInfo`
- Media: `track`, `publishing`, `publishingStopped`, `viewingStopped`
- File transfer: `fileList`, `fileTransferStart`, `fileTransferProgress`, `fileChunk`, `fileTransferComplete`, `fileTransferCancelled`, `fileTransferError`
- Resources: `resourceReceived`
- Diagnostics: `unsupportedChannel`
## Known consumers
Compatibility checks should cover at least:
- Browser CDN/global usage
- The Node adapter
- Social Stream's P2P data bridge
- The VDO.Ninja Video Capture extension's embedded SDK build
- The MCP bridge
- VDO.Ninja browser peers
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.