UNPKG

@vdoninja/sdk

Version:

AI-friendly P2P communication SDK for audio, video, and data. Includes WHIP/WHEP clients for publishing to Twitch, Meshcast, Cloudflare Stream

276 lines (203 loc) 12.8 kB
# SDK Native-Protocol Support — Implementation Plan Status: phases 0–3 complete; phase 4 deferred Owner: Steve Seguin Last updated: 2026-07-26 ## Goal Teach `@vdoninja/sdk` to speak VDO.Ninja's existing data-channel protocols, and add a high-throughput binary path for SDK-to-SDK use. Close the items in `ninja-p2p/docs/sdk-wishlist.md` without regressing VDO.Ninja interoperability. ## Ground rules 1. **SDK changes only, by default.** All work happens in this repository. 2. **VDO.Ninja changes are gated.** Where the plan needs a change to VDO.Ninja, work stops at a numbered gate. The pending change is written up and reviewed before any VDO.Ninja file is touched. Gates are marked **GATE** below. 3. **Additive only in VDO.Ninja.** No existing VDO.Ninja behavior changes. This is viable because VDO.Ninja's data-channel handlers are a chain of `if ("key" in msg)` checks, so unknown keys are ignored by peers that predate them. `allowchunked` and `chunkprotocols` already work this way. 4. **Mixed versions are normal.** Vendored SDK copies exist across several consumers at v1.3.18 and v1.4.0. Every change must tolerate an old peer in the same room. 5. **No new signaling-server message types.** Per `docs/compatibility.md`. ## Verified starting state Facts below were read from source, not assumed. ### VDO.Ninja's channel set (per peer) | Label | Options | Purpose | | --- | --- | --- | | `sendChannel` | default ordered | JSON control — the only one the SDK knows today | | `chunked` | `{ordered:true}`, arraybuffer, `bufferedAmountLowThreshold` 64KB/512KB | Chunked media transfer | | `resources` | `{ordered:true, maxRetransmits:30}` | Resource/template sync | | `<fileid>` | arraybuffer, 16384-byte chunks | File transfer | Routing is by label in `ondatachannel` (`vdoninja/webrtc.js:20597`): `chunked``recieveChunkedStream`, `resources``recieveResourcesChannel`, any other non-`sendChannel` label → `recieveFile`. ### File transfer protocol 1. Publisher advertises `{fileList:[{id,name,size}]}` over `sendChannel` (`vdoninja/webrtc.js:12824`) 2. Viewer requests `{requestFile:<id>}` (`vdoninja/webrtc.js:12264`, `:23069`) 3. Publisher opens a channel labeled `<fileid>`, renamed `sendChannel_<rand5>` on collision (`vdoninja/lib.js:20315`) 4. Header frame: `{type:"filetransfer",size,filename,id}` 5. Body: 16384-byte `ArrayBuffer` chunks 6. Terminator: `"EOF1"` success, `"EOF2"` cancelled Note: `sendFile` (`vdoninja/lib.js:20365-20395`) is a `FileReader` ping-pong loop with no `bufferedAmount` check. It is correct for browser file sharing and unsuitable as a bulk transport. This is why Phase 2 exists. ### Resources protocol Channel `resources`. JSON metadata carrying `templateName` and `size`, then 16KB binary chunks with 50ms inter-chunk delays; receiver accumulates until `receivedSize >= metadata.size` (`vdoninja/lib.js:69150-69230`). Gated by `allowResources` on both ends. ### Chunked media protocol Negotiated via `allowchunked` plus `chunkprotocols:["indexed-v1"]`, with `positional-v1` and `indexed-v1` variants (`vdoninja/webrtc.js:1674-1702`). Carries encoded media from `chunkedRecorder` with keyframe and audio-header sequencing. Not fully mapped; scoped last and deliberately estimated loosely. ### The SDK-side defect this all rests on `vdoninja-sdk.js:1916`: ```js connection.pc.ondatachannel = (event) => { connection.dataChannel = event.channel; // any label, no filter this._setupDataChannel(connection, event.channel); }; ``` `_setupDataChannel` (`vdoninja-sdk.js:2446`) is a *control-channel* routine. Applied to an auxiliary channel it will overwrite `connection.dataChannel`, re-send publisher info, start a second ping monitor, emit a spurious `dataChannelOpen`, and `JSON.parse` binary frames. Reachable today: a caller setting `allowchunked` or `allowresources` (`vdoninja-sdk.js:556-557`) puts those keys in the outbound info payload (`:2466`), which VDO.Ninja reads (`webrtc.js:12364`, `:12401`) and uses to decide whether to open those channels. Not reachable by default, since both flags default off. --- ## Phase 0 — Prerequisites — **DONE** Every native protocol arrives as a non-`sendChannel` channel, so this was completed before the native transfer work. **0.1 Done.** Split control-channel setup from auxiliary-channel routing. `_setupDataChannel` keeps its current behavior for `sendChannel` only. Introduce a per-connection channel registry (`connection.channels` keyed by label) so `connection.dataChannel` always means the control channel. **0.2 Done.** Route `ondatachannel` by label, matching VDO.Ninja's rule: `sendChannel` → control, `chunked` / `resources` → their handlers once they exist, anything else → file transfer. Unhandled labels are registered and ignored, never routed into control setup. **0.3 Done.** Rewrote `docs/compatibility.md` to document all channel labels, routing, and capability negotiation. **0.4 Done.** Added `tests/interop/harness.html` and exercised real VDO.Ninja peers against the local SDK. **Acceptance met:** auxiliary channels no longer replace the control channel, and existing SDK-to-SDK and SDK-to-VDO.Ninja behavior remains intact. --- ## Phase 1 — Tier 1: native protocol interop — **DONE** SDK only. No VDO.Ninja changes. **1.1 Done.** Send and receive `{fileList:[...]}` over the control channel. Public surface for hosting files and for enumerating a peer's offered files. **1.2 Done.** Send and handle `{requestFile:<id>}`. **1.3 Done.** Open a channel labeled `<fileid>` with the collision rename, send the `{type:"filetransfer",...}` header, then 16384-byte chunks, then `EOF1`. Honor the `restricted`-to-one-UUID semantics. Deviation to decide at implementation time: VDO.Ninja's sender has no backpressure. The SDK's sender should check `bufferedAmount` — it stays wire-compatible because the receiver cannot tell the difference. Flag it in the PR rather than silently matching the naive loop. **1.4 Done.** Header, chunk accumulation, `EOF1`/`EOF2`, progress events, cancellation. **1.5 Done.** Send and receive resources, honoring `allowResources` in both directions. Keep the metadata/size framing exactly. **Acceptance met:** an SDK peer and real VDO.Ninja tab exchanged files in both directions through the Phase 0 harness. Old SDK peers remain unaffected. --- ## **GATE 1** — before Phase 2 **Resolved: Phase 2 no longer requires a VDO.Ninja change.** The proposal is written up in `docs/vdoninja-wishlist.md`. The gate was raised on the assumption that a capability handshake would need a VDO.Ninja diff. It does not: the data-channel message handler is a chain of independent `if ("key" in msg)` blocks with no schema or whitelist, so unrecognised keys are ignored for free. The SDK can negotiate on its own and only open a namespaced channel toward a peer that advertised support, which means a VDO.Ninja peer never sees one. The optional hardening later landed in VDO.Ninja `3098fa0`: the `x-` namespace is reserved and ignored by its native handlers. The SDK honours the same reservation. The gate discipline still stands: no file in `~/Code/vdoninja` is edited without a reviewed and approved proposal. --- ## Phase 2 — Tier 2: bulk binary transport — **DONE** All SDK-only. VDO.Ninja's `x-` reservation landed separately in `3098fa0`. **2.1/2.2 Done** on the VDO.Ninja side; the SDK honours the same reservation. **2.3 Done.** `sendBinary()` + `binaryReceived`. Bytes pass through untouched. **2.4 Done.** `openChannel(uuid, label, opts)` with ordered / maxRetransmits / maxPacketLifeTime pass-through, plus `getChannel()`. **2.5 Done.** `getBufferedAmount()` and a `bufferedAmountLow` event. **2.6 Done.** `getMaxMessageSize()`. Verified live: 4MB flooded at a VDO.Ninja tab with zero errors and the control channel intact; browser backpressure throttled correctly at a 1.1MB peak across 6 drain events. Two findings recorded rather than worked around: - **Binary must never touch the control channel.** VDO.Ninja renders any object payload there as a WebP image (`webrtc.js:21219`), so bytes would corrupt a viewer rather than be ignored. `sendBinary` uses a dedicated `x-bin` lane. - **`@roamhq/wrtc` backpressure varies by platform.** The Windows build stays at 0 after 2.4MB queued; Linux CI reports queued bytes but omits the native low-buffer event. The SDK polls when bytes are observable and documents the unavoidable zero-reporting case in README-NODE.md. <details> <summary>Original plan</summary> For SDK-to-SDK use. This is what removes ninja-p2p's ~33% base64 overhead and the head-of-line blocking between bulk and control traffic. Wishlist items 1, 3, 4, 5, 6. **2.1** Reserved label namespace — *VDO.Ninja change, gated above* **2.2** Capability advertisement — *VDO.Ninja change, gated above* **2.3** `sendBinary(bytes, target)` and a `binaryReceived` event. `ArrayBuffer` / `ArrayBufferView` pass through untouched. `sendData` is not changed. **2.4** `openChannel(uuid, label, options)` with `ordered`, `maxRetransmits`, and `maxPacketLifeTime` passed through. **2.5** `getBufferedAmount(uuid)` and a `bufferedAmountLow` / `drain` event, using the same threshold pattern VDO.Ninja already uses on its chunked channel. **2.6** `getMaxMessageSize(uuid)` from the negotiated SCTP value. **Acceptance:** ninja-p2p can drop its base64 encoding and its fixed in-flight caps. Measured throughput improvement on a real transfer. VDO.Ninja peers see no new channels they do not understand. </details> --- ## Phase 3 — SDK-local surface — **DONE** Zero wire risk, no VDO.Ninja involvement. Wishlist items 2, 7, 8, 9. **3.1 Done.** `disconnect()` returns a promise resolving on genuine teardown completion, and is idempotent. Added a `teardownComplete` event emitted exactly once. **3.2 Done.** `disconnected` now carries `{intentional, reason, willReconnect, phase}`. `phase` is `'socket'` or `'teardown'`, which distinguishes the two emit sites that previously looked identical. **3.3 Done.** `getPeerQuality(uuid)` returns `{rttMs, lossRate, candidatePairType, relayed, availableOutgoingBitrate, bytesSent, bytesReceived}`. `lossRate` is null on a data-only peer rather than a misleading zero. **3.4 Done.** `vdoninja-sdk.d.ts` ships, with `types` in package.json and per-export `types` conditions. `npm run test:types` typechecks a consumer under `--strict` so the definitions cannot drift unnoticed — the drift was the actual complaint in the wishlist. **3.5 Dropped.** Documenting the password/salt derivation as a stable contract is not being done as part of this work; password logic is owned elsewhere. **3.6 Done.** `@roamhq/wrtc` exit-segfault documented in README-NODE.md with the minimal reproduction, plus the shutdown ordering that avoids it. Covered by the `Lifecycle and Peer Quality` test (17 assertions against live peers). --- ## Phase 4 — Chunked media interop Deferred. Phase 1 and the harness are proven, but this is a media codec transport rather than a data protocol and should have a separate implementation plan. --- ## Deferred, tracked separately Real work, independent of the above: - README restructure — 911 lines, `## Core Methods` appears twice (`:274`, `:502`) - Consolidate the five overlapping entry docs (`README.md`, `AI-INTEGRATION.md`, `llms.txt`, `README-NODE.md`, `docs/*`) - `sdk.vdo.ninja` landing page — leads with "Option 1: Simple IFRAME API", which is not this SDK; claims "Python & Mobile" which the package does not ship - Right-size the MCP material now that `@vdoninja/mcp` is a separate package - Vendored-copy policy and refresh: `video_capture_extension` (v1.4.0, 237 lines behind), `social_stream/thirdparty` (v1.4.0), `vst/js_sdk` (v1.4.0), `vdocable` (v1.3.18), plus several unversioned copies ## Open questions - **`&password=false` did not interoperate in the harness.** An SDK peer with `password: false` plus a VDO.Ninja viewer with `&password=false` never matched at the signaling layer; the default-password path works. `&password=false` is understood to disable all passwords including the default, so both ends should have agreed. Recorded as an observation only — password logic is not to be modified as part of this work. Worth a look by someone who owns that code before the derivation is documented as a stable contract. - `auth-client.js` in the private repo — token heartbeat and handle resolution via api.vdo.ninja. Headed for the SDK, or app-only? Worth settling before the authentication surface expands. - Does Tier 1 file transfer need a public API that mirrors VDO.Ninja's hosted-file model, or a simpler send/receive pair with the hosting semantics internal?