npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vdoninja/sdk

v1.6.1

Published

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

Readme

VDO.Ninja SDK

SDK Tests npm version npm downloads License: MPL-2.0

JavaScript SDK for VDO.Ninja-compatible audio, video, and data in browsers and Node.js. Build custom broadcast tools, remote camera viewers, P2P messaging, and agent integrations using hosted signaling and WebRTC. Connections use TURN relay servers when a direct route is unavailable.

Choose Your Starting Point

Browse the guide chooser for a comparison of SDK, iframe, page API, MCP, and WHIP/WHEP options. The website offers the same guides as readable static pages.

| What you want to build | Start here | | --- | --- | | Publish or view VDO.Ninja audio/video | Audio/video quick start and API reference | | Record incoming media | Recording guide | | Show OBS camera/screen tally in VRChat | Tally guide and runnable samples | | Add P2P messaging to an application | Basic data channel example | | Transfer files or large results | File transfer guide | | Build status dashboards or shared interfaces | Data messaging guide | | Build a remote control panel | Remote control options | | Publish to or watch a media service | WHIP/WHEP guide | | Let AI agents communicate in rooms | Agent network guide | | Understand reconnect and ICE recovery | Reliability and recovery | | Upgrade without breaking an integration | Compatibility contract |

The SDK deliberately supports several layers. Media publishing/viewing and generic data use the same VDO.Ninja-compatible WebRTC transport. The optional MCP package presents that data transport as practical tools for agent messaging, file transfer, and shared state. Recording is performed by browser or Node recording APIs after the SDK delivers media.

⚠️ IMPORTANT: Usage Guidelines

Direct handshake-server WebSocket access is NOT APPROVED. Use this SDK for VDO.Ninja signaling. The documented &api remote-control endpoint is separate and is supported by the tally sample.

  • SDK Required: Direct handshake-server WebSocket connections will be blocked
  • API Stability: The WebSocket API may change without notice - the SDK handles these updates
  • Rate Limits: Excessive requests are throttled/blocked. Higher limits available on request
  • Serverless Philosophy: No state management or data relay through the signaling server
  • Connection Limits: ~80 connections per room, viewer limits may apply
  • Data Policy: Only WebRTC handshake data allowed through WebSocket - all other data must use P2P channels

Use of VDO.Ninja services operated by Steve Seguin is subject to applicable Terms of Service, operational policies, rate limits, and access controls.

Installation

NPM Package

npm install @vdoninja/sdk

# For Node.js, also install ONE of these WebRTC implementations:
npm install @roamhq/wrtc  # Recommended for full media support
# OR
npm install node-datachannel  # For data channels only

TypeScript declarations are included for the browser and Node entry points; no separate @types package is needed:

import VDONinja, { PeerQuality, FileTransferResult } from '@vdoninja/sdk';
import VDONinjaNode, { WebRTCInfo } from '@vdoninja/sdk/node';

Browser (CDN)

<script src="https://unpkg.com/@vdoninja/sdk/vdoninja-sdk.js"></script>
<!-- OR minified version -->
<script src="https://unpkg.com/@vdoninja/sdk/vdoninja-sdk.min.js"></script>
<!-- OR from GitHub CDN -->
<script src="https://cdn.jsdelivr.net/gh/steveseguin/ninjasdk@latest/vdoninja-sdk.min.js"></script>

Quick Start

Browser

const vdo = new VDONinjaSDK();

Node.js - Simple Setup

// Auto-detect an installed WebRTC implementation (uses included adapter)
const VDONinjaSDK = require('@vdoninja/sdk/node');
const vdo = new VDONinjaSDK();

Install a supported WebRTC implementation such as @roamhq/wrtc first. The package root also selects the Node adapter under Node's package export conditions. Use @vdoninja/sdk/browser only when intentionally providing your own runtime globals.

See README-NODE.md for detailed Node.js setup with full adapter support.

Basic Data Channel Example

// Create instance
const vdo = new VDONinjaSDK();

// Handle incoming messages
vdo.addEventListener('dataReceived', (event) => {
    console.log(`Received from ${event.detail.uuid}:`, event.detail.data);
});

// Choose a unique room name and use this same value in both peers.
const roomId = 'replace_with_your_shared_unique_room';

// In both peers, use the SAME roomId. Each peer gets its own stream ID.
// Register before autoConnect: sends require an open data channel.
vdo.addEventListener('dataChannelOpen', () => {
    vdo.sendData({ message: "Hello P2P!" });
});
await vdo.autoConnect({ room: roomId });

Note: Stream and room IDs accept alphanumeric and underscore. Any hyphens or non‑word characters are automatically sanitized to _.

Audio/Video Example

const vdo = new VDONinjaSDK({ salt: "vdo.ninja", host: "wss://apibackup.vdo.ninja" });

// Handle incoming tracks
vdo.addEventListener('track', (event) => {
    const video = document.getElementById('video');
    if (!video.srcObject) {
        video.srcObject = new MediaStream();
    }
    video.srcObject.addTrack(event.detail.track);
});

// Get user media
const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true
});

// Connect, join room, and publish
await vdo.connect();
await vdo.joinRoom({ room: "videoroom" });
await vdo.publish(stream, { streamID: "my_camera" });

// Open in another browser (same room, salt, password, and signaling host):
// https://vdo.ninja/alpha/?view=my_camera&room=videoroom&scene&wss2=apibackup.vdo.ninja

Use this VDO.Ninja viewer link, with a unique room and stream ID for your application. A room-scoped publisher needs a room-scoped viewer. Pass original password text to SDK options; the connection guide shows how to encode passwords for VDO.Ninja viewer URLs, including literal percent characters. Browser capture needs HTTPS or localhost and user permission.

Features

  • 🤖 AI-Friendly: No accounts, no CAPTCHA, perfect for bots
  • 🔒 Private: End-to-end encrypted P2P connections
  • 💸 Free: No server costs, minimal infrastructure
  • 🚀 Easy: Simple API, works everywhere
  • 📡 Flexible: Audio, video, and data channels
  • 🌐 Resilient: NAT traversal and firewall bypassing
  • 📤 WHIP/WHEP Support: Publish to Twitch, Meshcast, Cloudflare and more
  • 🧩 Optional MCP Add-on: @vdoninja/mcp for AI-agent rooms and private bot-to-bot workflows

Additional transport features:

  • Bulk Data: Raw binary, additional channels, backpressure, and partial reliability
  • Native Interop: VDO.Ninja-compatible file transfer and resource channels
  • Typed: Browser and Node TypeScript declarations included

Optional MCP Add-on (Minor Feature)

If you want AI tools to coordinate over VDO.Ninja data channels, use the optional MCP bridge:

  • npm: https://www.npmjs.com/package/@vdoninja/mcp
  • repo: https://github.com/steveseguin/ninjamcp
  • Works with local stdio MCP clients (Codex CLI, Claude Code, Cursor, and other MCP-compatible CLIs).
  • First call after connecting: {"name":"vdo_capabilities","arguments":{}}

Examples:

  • Claude Code session asking a Codex CLI session for help over a private P2P room
  • OpenClaw-to-OpenClaw bot collaboration in real time
  • Small encrypted LLM group chats (swarm brainstorming, review loops, triage rooms)

WHIP/WHEP Support

The SDK includes standalone WHIP and WHEP clients for publishing to and consuming from standard WebRTC-HTTP endpoints.

What are WHIP and WHEP?

  • WHIP (WebRTC-HTTP Ingestion Protocol): Publish media to servers like Twitch, Meshcast, Cloudflare Stream
  • WHEP (WebRTC-HTTP Egress Protocol): Consume media from WHEP-compatible servers

WHIP Client - Publish to Streaming Services

// Include the WHIP client
// Browser: <script src="whip-client.js"></script>
// Node.js: const WHIPClient = require('./whip-client.js');

// Publish to Meshcast.io
const client = new WHIPClient('https://cae1.meshcast.io/whip/mystream', {
    videoCodec: 'h264',
    videoBitrate: 2500,
    debug: true
});

// Get camera/screen
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });

// Publish
await client.publish(stream);
console.log('Publishing! View at: https://meshcast.io/mystream');

// Stop when done
await client.stop();

WHEP Client - Watch Streams

// Include the WHEP client
// Browser: <script src="whep-client.js"></script>
// Node.js: const WHEPClient = require('./whep-client.js');

// Connect to a WHEP endpoint
const client = new WHEPClient('https://cae1.meshcast.io/whep/mystream', {
    debug: true
});

// Handle incoming tracks
client.addEventListener('track', (e) => {
    document.getElementById('video').srcObject = e.detail.streams[0];
});

// Start viewing
await client.view();

// Stop when done
await client.stop();

Supported WHIP/WHEP Services

| Service | WHIP (Publish) | WHEP (View) | |---------|---------------|-------------| | Meshcast.io | https://cae1.meshcast.io/whip/{streamId} | https://cae1.meshcast.io/whep/{streamId} | | Cloudflare Stream | Yes | Yes | | Twitch | https://g.webrtc.live-video.net:4443/v2/offer | N/A | | Dolby.io | Yes | Yes |

WHIP/WHEP Demos

Constructor Options

const vdo = new VDONinjaSDK({
    host: 'wss://wss.vdo.ninja',     // WebSocket server URL
    room: "myroom",                   // Initial room name (optional)
    password: "roomPassword",         // Room password (optional, default: "someEncryptionKey123")
    salt: "vdo.ninja",                // IMPORTANT: Set to "vdo.ninja" for playback on https://vdo.ninja
                                      // The salt affects streamID hashing. Without this, streams may not be 
                                      // viewable on vdo.ninja when running from a different domain
    debug: false,                     // Enable debug logging
    turnServers: null,                // null=auto-fetch, false=disable, or array of custom servers
    forceTURN: false,                 // Force relay mode through TURN servers
    turnCacheTTL: 5,                  // TURN server cache time-to-live in minutes
    stunServers: [{                   // STUN servers (default: Google & Cloudflare)
        urls: 'stun:stun.l.google.com:19302'
    }],
    maxReconnectAttempts: 5,          // Maximum reconnection attempts
    reconnectDelay: 1000,             // Initial reconnection delay in ms
    videoElement: null,               // DOM element to auto-attach streams (optional)
    autoPingViewer: false,            // Optional: auto ping from viewer side only
    autoPingInterval: 10000,          // Optional: viewer auto-ping interval (ms)
    autoRecover: true,                // Recover failed peer directions automatically
    autoRelay: true,                  // Temporarily try TURN after direct recovery fails
    disconnectGracePeriod: 5000,      // Grace period for temporary ICE disconnects
    connectionTimeout: 20000,         // Initial peer connection timeout
    recoveryTimeout: 12000,           // Wait between bounded recovery phases
    relayRestoreDelay: 45000          // Restore direct-first ICE policy after recovery
});

Core Methods

// Connection flow
await vdo.connect();                  // Connect to signaling server
await vdo.joinRoom({ room: "myroom", password: "optional" });
await vdo.leaveRoom();               // Leave current room
await vdo.disconnect();              // Disconnect after teardown genuinely finishes

// Publishing
await vdo.publish(mediaStream, {     // Publish media stream
    room: "myroom",                  // Optional if already in room
    streamID: "custom-id",           // Optional custom stream ID
    label: "Main Camera",            // Optional label; sent to viewers on DC open
    meta: "Desk Cam",                // Optional metadata string
    order: "1",                      // Optional ordering hint
    broadcast: false,                 // Optional flags (example set)
    allowdrawing: false,
    iframe: false,
    widget: false,
    allowmidi: false,
    allowresources: false,
    allowchunked: true                // true/false or 1/2 per your needs
});
await vdo.announce({ streamID: "myStreamID" }); // Data-only publisher
await vdo.stopPublishing();          // Stop publishing

// Viewing
await vdo.view("streamID", {         // View a specific stream
    audio: true,                     // Request audio
    video: true,                     // Request video
    label: "Viewer Label",           // Optional label
    downloads: true,                 // Advertise willingness to receive file offers
    allowresources: false            // Opt in to VDO.Ninja resource channels
});
await vdo.stopViewing("streamID");  // Stop viewing

// Data communication
vdo.sendData(data);                  // Send to all peers
vdo.sendData(data, "targetUUID");    // Send to specific peer
vdo.sendData(data, {                 // Advanced targeting
    uuid: "targetUUID",
    type: "viewer",                  // Target viewers only
    streamID: "streamID",            // Target stream connections
    allowFallback: true              // Allow WebSocket fallback
});

// Raw bytes and additional channels never use the JSON control channel
await vdo.sendBinary(bytes, "targetUUID");
const channel = await vdo.openChannel("targetUUID", "bulk", {
    ordered: false,
    maxRetransmits: 0
});

P2P Connection Patterns

Pattern 1: Publisher-Viewer (Recommended for Data Channels)

One peer announces (publisher), the other views (viewer). Creates a single bidirectional data channel.

// Peer A - Publisher
const publisher = new VDONinjaSDK();
await publisher.connect();
await publisher.joinRoom({ room: "myroom" });
await publisher.announce({ streamID: "peer-a" });

// Peer B - Viewer  
const viewer = new VDONinjaSDK();
await viewer.connect();
await viewer.joinRoom({ room: "myroom" });
await viewer.view("peer-a");

// BOTH can send/receive on the SAME data channel (verified!)
publisher.sendData("Hello from publisher");  // Viewer receives this
viewer.sendData("Hello from viewer");        // Publisher receives this

✅ Verified behavior:

  • Creates ONE P2P connection with bidirectional data channel
  • Both peers can send and receive messages
  • Most efficient for data-only applications

Pattern 2: Dual Connection (Required for Media Exchange)

Both peers announce AND view each other. Creates two separate P2P connections.

// Peer A
const peerA = new VDONinjaSDK();
await peerA.connect();
await peerA.joinRoom({ room: "myroom" });
await peerA.announce({ streamID: "peer-a" });
await peerA.view("peer-b");  // View peer B

// Peer B
const peerB = new VDONinjaSDK();
await peerB.connect();
await peerB.joinRoom({ room: "myroom" });
await peerB.announce({ streamID: "peer-b" });
await peerB.view("peer-a");  // View peer A

// Smart routing prevents duplicates by default
peerA.sendData("Hello");  // peerB receives ONCE (via publisher channel preferred)

// To intentionally use both channels:
peerA.sendData("Hello", { preference: 'all' });  // peerB receives TWICE

Data Routing Control

The SDK intelligently routes messages to prevent duplicates when dual connections exist:

// Default behavior (no target): publisher data channel preferred (no duplicates)
peer.sendData(data);  // Tries publisher data channel first, then viewer data channel

// Explicit channel selection (optional)
peer.sendData(data, { preference: 'publisher' });  // ONLY use publisher channel
peer.sendData(data, { preference: 'viewer' });     // ONLY use viewer channel  
peer.sendData(data, { preference: 'any' });        // Publisher data channel, then viewer (default)
peer.sendData(data, { preference: 'all' });        // Use ALL channels (duplicates!)

// Target specific peer
peer.sendData(data, "uuid");                       // Send to specific UUID (uses 'any')
peer.sendData(data, { uuid: "...", preference: 'viewer' }); // Force viewer channel

Preference options:

  • 'any' (default): Try the publisher data channel first, then the viewer data channel
  • 'publisher': Use only the publisher data channel
  • 'viewer': Use only the viewer data channel
  • 'all': Send via ALL available connections (intentional duplicates)

Note: The default 'any' preference tries one available data channel per peer while preventing duplicates. It tries the publisher channel first (as that's typically the announcing peer's primary channel), then uses the viewer channel if the publisher channel isn't available.

These preferences select between WebRTC data channels. WebSocket signaling fallback is separate and disabled by default; opt in with { allowFallback: true }.

Salt Configuration (Important!)

The salt parameter is crucial when you want streams to be viewable on https://vdo.ninja:

  • For Video/Audio Streams: Set salt: "vdo.ninja" to ensure your published streams can be viewed on vdo.ninja
  • For Data-Only Applications: The salt can be omitted or set to any consistent value between peers
  • Default Behavior: Without explicit salt, the SDK uses your current domain as salt, which may cause incompatibility
// For vdo.ninja compatibility (video/audio streams)
const vdo = new VDONinjaSDK({
    salt: "vdo.ninja"  // Required for vdo.ninja playback
    // Use default password for simplest URLs
});

// Published streams will be viewable at:
// https://vdo.ninja/?view=YOUR_STREAM_ID

Password & Encryption

  • Default password: if password is undefined, null, or "", the SDK uses "someEncryptionKey123".
  • Disable encryption explicitly with password: false.
  • Empty string ("") is not the same as disabled. An empty string uses the default password.
  • When effective password is set, the SDK:
    • Encrypts SDP and ICE (WebSocket and DataChannel) with AES‑CBC and includes a vector.
    • Appends a 6‑char hex suffix to the streamID for hashing. Offers include this hashed streamID so viewers can match allow‑lists.
  • Set salt: "vdo.ninja" when you want streams to be viewable on https://vdo.ninja (affects hash compatibility).
  • Generating vdo.ninja viewer links: When using password: false, include &password=false in the viewer URL. Omitting the parameter does NOT disable encryption on the viewer side.

Data Channel Signaling

  • The publisher creates a data channel named sendChannel (matching VDO.Ninja core).
  • Before the data channel opens, signaling uses WebSocket. After it opens, ICE is sent via DataChannel only (no duplication).
  • Pings are DC‑only: use sendPing(uuid) manually or enable viewer auto‑ping with autoPingViewer: true.

Event Listeners

// The SDK extends EventTarget, use addEventListener
vdo.addEventListener('connected', (event) => {
    console.log('Connected to signaling server');
});

vdo.addEventListener('track', (event) => {
    const { track, streams, uuid, streamID } = event.detail;
    console.log('Track received:', track.kind, 'from:', uuid);
});

vdo.addEventListener('dataReceived', (event) => {
    const { data, uuid, streamID } = event.detail;
    console.log('Data received:', data, 'from:', uuid);
});

// Publisher and viewer info (e.g., labels)
vdo.addEventListener('peerInfo', (event) => {
    const { uuid, streamID, info } = event.detail;
    console.log('Peer info updated:', uuid, streamID, info); // info.label available
});

// An OBS browser source viewing this publisher changed tally-related state.
vdo.addEventListener('obsState', (event) => {
    const { uuid, state, update } = event.detail;
    console.log('OBS state changed:', uuid, update, 'merged state:', state);
});

Discovering participants and their names

Use listing for existing room streams and videoaddedtoroom for newly announced streams. Listings may not include labels. View the discovered stream with await vdo.view(streamID, { audio: false, video: false }) to establish a control connection without requesting media, then listen for peerInfo to receive its label. Register listeners before joining/viewing. Use stream IDs as selection keys and labels as display text; labels can change or be shared by multiple people.

peerConnected reports a transport connection, not a complete participant record. connection.type is the SDK's local role: on a viewer connection, connection.streamID identifies the remote stream; on a publisher connection, it identifies the SDK's own stream being sent to that viewer. connection.info contains received remote metadata and starts empty. Outgoing publisher metadata is stored separately in connection.localInfo. Wait for peerInfo rather than expecting a label at peerConnected.

Viewing a camera stream does not expose the OBS tally that other viewers send to that camera's publisher. An SDK publisher receives obsState when OBS views its stream. Register the listener before establishing connections; the browser peer supplies initial state asynchronously, and an empty connection.obsState means unknown, not off air. There is no SDK snapshot-request method or automatic room-wide tally relay.

Publisher Info (Data Channel)

When a publisher’s data channel (label sendChannel) opens, the SDK sends a publisher info payload to the viewer:

{ info: {
    label: "Main Camera",   // optional string
    meta: "Desk Cam",       // optional string
    order: "1",             // optional string/number (stringified)
    broadcast: false,        // optional boolean
    allowdrawing: false,     // optional boolean
    iframe: false,           // optional boolean
    widget: false,           // optional boolean
    allowmidi: false,        // optional boolean
    allowresources: false,   // optional boolean
    allowchunked: true       // optional boolean/number
}}

Provide these in publish()/announce() options. You can either:

  • Pass fields at the top level: { label, meta, order, ... }
  • Or pass an info object: { info: { label, meta, order, ... } }

The viewer receives peerInfo with the merged info object.

vdo.addEventListener('peerConnected', (event) => {
    const { uuid, connection } = event.detail;
    console.log('Peer connected:', uuid);
});

vdo.addEventListener('disconnected', (event) => {
    console.log('Disconnected from server');
});

vdo.addEventListener('error', (event) => {
    console.error('Error:', event.detail.error);
});

Core Methods

Connection Management

  • async connect() - Connect to signaling server
  • async disconnect() - Disconnect and resolve after teardown genuinely finishes
  • async joinRoom(options) - Join a room
  • leaveRoom() - Leave current room

Publishing

  • async publish(stream, options) - Publish media stream
  • async announce(options) - Announce as data-only publisher
  • stopPublishing() - Stop publishing

Viewing

  • async view(streamID, options) - View a stream
  • stopViewing(streamID) - Stop viewing a stream

Data Communication

  • sendData(data, target) - Send data with flexible targeting
  • async sendBinary(data, uuid, options) - Send raw bytes on a dedicated reserved channel
  • async openChannel(uuid, label, options) - Open an additional SDK-to-SDK channel
  • getChannel(uuid, label) - Get an existing additional channel
  • getBufferedAmount(uuid, label?) - Inspect queued channel bytes for backpressure
  • getMaxMessageSize(uuid) - Read the negotiated SCTP message limit
  • sendPing(uuid) - Send ping (manual; either role; DC-only)

Native File and Resource Transfer

  • hostFile(source, options) / unhostFile(id) - Offer or withdraw a file
  • requestFile(uuid, fileId, options) - Download a VDO.Ninja-compatible file
  • getHostedFiles() - List locally hosted files
  • sendResource(uuid, metadata, data) - Send a VDO.Ninja resource/template asset

Track Management

  • async addTrack(track, stream) - Add track to publishers
  • async removeTrack(track) - Remove track from publishers
  • async replaceTrack(oldTrack, newTrack) - Replace track

Statistics

  • async getStats(uuid) - Get connection statistics
  • async getPeerQuality(uuid) - Get normalized RTT, loss, route, relay, and byte metrics

Utility Methods

  • async quickPublish(options) - Connect, join, and publish
  • async quickView(options) - Connect, join, and view
  • async autoConnect(roomOrOptions, [filter]) - Connect, join, announce, and auto‑view peers (mesh helper)

Auto‑Connect Mesh

Use autoConnect to make room meshes effortless. It connects, joins a room, announces a streamID, and automatically views other peers. Two modes:

  • Half mesh (default): One P2P connection per pair (best for data‑only). Each pair connects exactly once using a deterministic rule to avoid duplicates.
  • Full mesh: Two P2P connections per pair (needed for audio/video exchange).

Examples:

// Minimal: half mesh, data-only
const sdk = new VDONinjaSDK({ debug: true });
await sdk.autoConnect('myroom');

// Full mesh with AV viewing defaults
await sdk.autoConnect({ room: 'stage', mode: 'full' });

// Custom filters
await sdk.autoConnect('sensors', /^sensor_/);                  // regex on streamID
await sdk.autoConnect('chat', (item) => item.label === 'chat'); // function filter
await sdk.autoConnect({ room: 'lab', filter: { prefix: 'node_' }});

// Controller to stop auto-connect
const ctl = await sdk.autoConnect('team');
// ... later
ctl.stop();

// Send and receive after connection
sdk.addEventListener('dataChannelOpen', () => sdk.sendData({ hello: 'world' }));
sdk.addEventListener('dataReceived', (e) => console.log('Got', e.detail.data));

See the minimal demo at demos/auto-connect.html.

Data Channel Patterns

Pub/Sub Messaging (SDK helpers)

const sdk = new VDONinjaSDK({ room: 'lobby', debug: true });
await sdk.connect();
await sdk.joinRoom({ room: 'lobby' });

// Viewer/subscriber side
sdk.addEventListener('dataChannelOpen', () => {
  // Subscribe to one or many channels
  sdk.subscribe(['general', 'alerts']);
});

// Receive channel messages (emitted only if locally subscribed)
sdk.addEventListener('channelMessage', (e) => {
  const { channel, data, timestamp, uuid } = e.detail;
  console.log(`[${channel}]`, data, 'from', uuid, 'at', new Date(timestamp));
});

// Publisher side: send to a channel; only subscribed peers will receive
sdk.publishToChannel('general', { message: 'Hello subscribers!' });

// Optional: monitor peer subscription changes (publisher UI)
sdk.addEventListener('peerSubscribed', (e) => {
  console.log('peerSubscribed', e.detail.uuid, e.detail.channels, e.detail.allChannels);
});
sdk.addEventListener('peerUnsubscribed', (e) => {
  console.log('peerUnsubscribed', e.detail.uuid, e.detail.channels, e.detail.allChannels);
});

// Query local subscriptions and per-peer subscriptions
sdk.getSubscriptions();                // ['general','alerts']
sdk.getPeerSubscriptions('peer-uuid'); // ['general']

Note: The SDK reserves pipe messages with type: 'subscribe'|'unsubscribe'|'channelMessage' for its pub/sub system and emits channelMessage events accordingly. For custom app-level protocols, prefer distinct type names to avoid collisions (e.g., topicSubscribe).

Binary Data

// Raw bytes use a dedicated x-bin channel, never the JSON control channel.
await vdo.sendBinary(new Uint8Array([1, 2, 3]), peerUUID);

vdo.addEventListener('binaryReceived', (event) => {
    const { bytes, uuid } = event.detail;
    processBinaryData(bytes, uuid);
});

For custom bulk protocols, use openChannel(). Labels are automatically placed in the reserved x- namespace, which VDO.Ninja safely ignores:

const bulk = await vdo.openChannel(peerUUID, 'bulk', {
    ordered: false,
    maxRetransmits: 0
});
bulk.send(chunk);

Native File Transfer

// Publisher: advertise a file using VDO.Ninja's native transfer protocol
const offered = vdo.hostFile(fileBytes, { name: 'report.pdf' });

// Viewer: request an advertised file
vdo.addEventListener('fileList', async (event) => {
    const file = event.detail.files[0];
    const result = await vdo.requestFile(event.detail.uuid, file.id);
    saveBytes(result.bytes, result.name);
});

File offers are capability-gated: viewers opt in with view(streamID, { downloads: true }), which is the default. Resource/template transfers similarly require view(streamID, { allowresources: true }).

Request/Response

// Request pattern
const requests = new Map();

vdo.sendData({
    type: 'request',
    id: 'req-123',
    method: 'getStatus'
}, peerId);

vdo.addEventListener('dataReceived', (event) => {
    const { data, uuid } = event.detail;
    if (data.type === 'response' && data.id) {
        const handler = requests.get(data.id);
        if (handler) handler(data.result);
    }
});

API Reference

For a full list of methods, helpers, aliases, and events, see:

  • docs/api-reference.md (Markdown)
  • docs/api-reference.html (HTML)

Quick highlights:

  • Core: connect, disconnect, joinRoom, leaveRoom, publish, announce, view, stopViewing, stopPublishing
  • Quick: quickPublish, quickView, autoConnect, quickSubscribe
  • Data: sendData, sendPing, request, respond, onRequest
  • Pub/Sub: subscribe, unsubscribe, publishToChannel, getSubscriptions, getPeerSubscriptions with events channelMessage, peerSubscribed, peerUnsubscribed
  • Tip: avoid reserved types ('subscribe'|'unsubscribe'|'channelMessage') for custom protocols

Examples

OBS tally and VRChat OSC

Browser camera operators can keep publishing normally while a Node script discovers streams and labels, reads tally through the alpha &api, and sends program/preview booleans to OSC.

The guide covers room discovery, duplicate labels, screen IDs, initial state, reconnects, OBS setup, and OSC parameters. Tested with real OBS against deployed alpha, including browser camera and screen publishing. Merely viewing another publisher with the SDK does not subscribe to that publisher's received tally.

Other examples

Use Cases

AI Bot Integration

// AI bot that joins rooms without human intervention
const bot = new VDONinjaSDK();

bot.addEventListener('dataReceived', async (event) => {
    const { data, uuid } = event.detail;
    if (data.type === 'question') {
        const response = await processWithAI(data.text);
        bot.sendData({
            type: 'answer',
            text: response
        }, uuid);
    }
});

// Connect, join room, and announce as data-only publisher
await bot.connect();
await bot.joinRoom({ room: 'ai-support' });
await bot.announce({ streamID: 'ai_bot_1' });

Collaborative Canvas

// Shared drawing application
const vdo = new VDONinjaSDK();
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// Connect and publish canvas stream
await vdo.connect();
await vdo.joinRoom({ room: 'drawing-room' });

// Share canvas as video stream
const stream = canvas.captureStream(30);
await vdo.publish(stream, { room: 'drawing-room' });

// Share drawing commands
canvas.addEventListener('mousemove', (e) => {
    if (drawing) {
        vdo.sendData({
            type: 'draw',
            x: e.offsetX,
            y: e.offsetY,
            color: currentColor
        });
    }
});

// Receive drawing commands
vdo.addEventListener('dataReceived', (event) => {
    const { data } = event.detail;
    if (data.type === 'draw') {
        ctx.fillStyle = data.color;
        ctx.fillRect(data.x, data.y, 2, 2);
    }
});

IoT Sensor Network

// Sensor node
const sensor = new VDONinjaSDK();

// Connect and announce as data-only publisher
await sensor.connect();
await sensor.joinRoom({ room: 'sensor_network' });
await sensor.announce({ streamID: 'sensor_node_1' });

setInterval(() => {
    sensor.sendData({
        type: 'telemetry',
        temperature: readTemperature(),
        humidity: readHumidity(),
        timestamp: Date.now()
    });
}, 5000);

// Monitoring station
const monitor = new VDONinjaSDK();

monitor.addEventListener('dataReceived', (event) => {
    const { data, uuid } = event.detail;
    if (data.type === 'telemetry') {
        updateDashboard(uuid, data);
    }
});

await monitor.connect();
await monitor.joinRoom({ room: 'sensor_network' });

Platform Support

Browser Compatibility

  • Chrome/Edge 80+
  • Firefox 75+
  • Safari 14+
  • Opera 67+
  • Mobile browsers with WebRTC support

Node.js Support

Full Node.js support is available with WebRTC implementations. See README-NODE.md for setup.

Other Platforms

  • Python: Use Raspberry.Ninja for VDO.Ninja Python SDK support
  • Mobile (Flutter): See vdon_flutter for a sample mobile app with VDO.Ninja support

Social Stream Ninja Integration

Social Stream Ninja consolidates live chat from multiple platforms into a unified stream. The SDK provides seamless two-way integration:

Supported Platforms

  • Twitch
  • YouTube
  • TikTok
  • Kick
  • X (Twitter)
  • Facebook
  • Discord
  • Instagram
  • And many more...

Quick Integration

const sdk = new VDONinjaSDK({
    host: 'wss://wss.socialstream.ninja',
    room: 'your-session-id',  // Note: SSN calls this "session ID" not "room ID"
    password: false
});

// Listen for live chat messages
sdk.on('dataReceived', (event) => {
    const data = event.detail?.data || event.data;
    if (data?.overlayNinja) {
        console.log(`${data.overlayNinja.chatname}: ${data.overlayNinja.chatmessage}`);
        console.log(`Platform: ${data.overlayNinja.type}`);
    }
});

// Connect as a "dock" client to receive messages
await sdk.connect();
await sdk.joinRoom();
await sdk.view('your-session-id', { 
    audio: false, 
    video: false, 
    label: "dock" 
});

See demos/socialstreamninja-listener.js for a complete example.

Security

  • End-to-end encryption by default
  • No data stored on servers
  • DTLS for data channels
  • SRTP for media streams
  • Optional TURN server for firewall traversal

Performance Tips

  1. Data Channels: Use announce() (publisher) and view() (viewer) for data-only applications; datamode is not used
  2. Bitrate: Adjust based on network conditions
  3. Codec: H264 for compatibility, VP8/VP9 for quality
  4. Broadcast: Use broadcast mode for one-to-many
  5. Binary: Use sendBinary() or openChannel() for raw bytes; do not send them on the control channel

Troubleshooting

Connection Issues

vdo.addEventListener('error', (event) => {
    console.error('Connection error:', event.detail.error);
    if (event.detail.error.includes('TURN')) {
        // Firewall blocking P2P
    }
});

vdo.addEventListener('connectionFailed', (event) => {
    const { uuid, streamID, reason } = event.detail;
    console.error('Connection to', uuid, 'failed:', reason);
});

Media Permissions

try {
    const stream = await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: true
    });
    await vdo.publish(stream);
} catch (error) {
    if (error.name === 'NotAllowedError') {
        // User denied permissions
    }
}

Ecosystem

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

By submitting a pull request, you agree to the Contributor License Agreement (CLA), which assigns your contribution rights to Steve Seguin so the project can be enforced, relicensed, and commercially licensed consistently.

License

SDK Core

vdoninja-sdk.js, vdoninja-sdk.min.js, and vdoninja-sdk.d.ts are licensed under the Mozilla Public License 2.0 (MPL-2.0). You may use, bundle, minify, and distribute the SDK as part of an MIT, AGPL, or proprietary larger work without relicensing the rest of that work. Distribution must preserve the applicable notices and make the MPL-covered source available as required by MPL-2.0. If you distribute modifications to an MPL-covered SDK file, that file and its source remain subject to MPL-2.0.

The SDK is not marked "Incompatible With Secondary Licenses," preserving compatibility with VDO.Ninja's AGPL-3.0 code.

SDK Extras

vdoninja-sdk-node.js, vdoninja-sdk-node.d.ts, webrtc-adapter.js, whip-client.js, and whep-client.js are MIT licensed. See LICENSE-MIT. The Node wrapper loads and extends the MPL-2.0 SDK core; its MIT license does not relicense the core.

MCP Wrapper

The separately published @vdoninja/mcp wrapper is MIT licensed. It depends on @vdoninja/sdk, whose core remains MPL-2.0.

Demos and Examples

Demo files (the demos/ folder) are MIT licensed. See LICENSE-DEMOS.

Note: demo files are excluded from the npm package via .npmignore.

See LICENSING.md for the complete file map and the separate trademark and hosted-service terms.

Support


Built by Steve Seguin with appreciation for the broader WebRTC ecosystem.