@vdoninja/sdk
Version:
AI-friendly P2P communication SDK for audio, video, and data. Includes WHIP/WHEP clients for publishing to Twitch, Meshcast, Cloudflare Stream
455 lines (398 loc) • 18.1 kB
TypeScript
/*
* Copyright (c) 2025-2026 Steve Seguin. All rights reserved.
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/// <reference lib="dom" />
// Type definitions for @vdoninja/sdk
// Project: https://github.com/steveseguin/ninjasdk
//
// These describe the SDK's public surface. Internal members (prefixed with _) are
// deliberately omitted: they are not part of the compatibility contract and change
// between releases. See docs/compatibility.md.
export as namespace VDONinjaSDK;
// ---------------------------------------------------------------------------
// Options
// ---------------------------------------------------------------------------
/**
* `false` disables encryption entirely. `undefined`, `null`, and `""` all mean
* "use the default password" — an empty string is not the same as `false`.
*/
export type Password = string | false | null;
export interface VDONinjaOptions {
/** Signaling WebSocket URL. Default: wss://wss.vdo.ninja */
host?: string;
/** Room to join on connect. */
room?: string;
password?: Password;
/** Hash salt. Must match the target deployment; "vdo.ninja" for vdo.ninja itself. */
salt?: string;
/** Human-readable label advertised to peers. */
label?: string;
/** Publisher metadata. Must be an object for VDO.Ninja to accept it. */
meta?: Record<string, unknown> | string;
debug?: boolean;
/** null auto-fetches, false disables, or supply your own ICE servers. */
turnServers?: RTCIceServer[] | null | false;
stunServers?: RTCIceServer[] | null | false;
forceTURN?: boolean;
configuration?: RTCConfiguration;
/** Recover failed peer directions automatically. Default: true */
autoRecover?: boolean;
/** Temporarily escalate failed direct paths to TURN. Default: true */
autoRelay?: boolean;
/** Grace period for temporary ICE disconnects, ms. Default: 5000 */
disconnectGracePeriod?: number;
/** Initial peer connection timeout, ms. Default: 20000 */
connectionTimeout?: number;
/** Wait between bounded recovery phases, ms. Default: 12000 */
recoveryTimeout?: number;
/** Restore direct-first ICE policy after recovery, ms. Default: 45000 */
relayRestoreDelay?: number;
/** Advertise willingness to receive the resources channel. */
allowresources?: boolean;
/** Advertise chunked media support. The SDK does not implement chunked media. */
allowchunked?: boolean | number;
[key: string]: unknown;
}
export interface MediaPreferences {
video?: {
codec?: string;
maxBitrate?: number;
resolution?: { width?: number; height?: number };
frameRate?: number;
};
audio?: { codec?: string; maxBitrate?: number };
}
export interface PublishOptions {
streamID?: string;
room?: string;
label?: string;
password?: Password;
media?: MediaPreferences;
[key: string]: unknown;
}
export interface ViewOptions {
audio?: boolean;
video?: boolean;
label?: string;
/** Shorthand for { audio: false, video: false }. */
dataOnly?: boolean;
/**
* Advertise willingness to receive file offers. Default true. VDO.Ninja publishers
* only send their file list to a viewer that asked for it.
*/
downloads?: boolean;
/**
* Advertise willingness to receive the `resources` channel. Default false, matching
* VDO.Ninja, where it requires the &resources URL flag.
*/
allowresources?: boolean;
[key: string]: unknown;
}
/** Where to send data. Omit to broadcast to every connected peer. */
export type DataTarget =
| string
| {
uuid?: string;
type?: 'viewer' | 'publisher';
streamID?: string;
/** Data-channel routing. Default 'any': publisher first, then viewer. */
preference?: 'any' | 'viewer' | 'publisher' | 'all';
/** Use WebSocket signaling when no matching data channel is available. Default false. */
allowFallback?: boolean;
};
// ---------------------------------------------------------------------------
// File transfer and resources
// ---------------------------------------------------------------------------
export type FileSource = Blob | ArrayBuffer | ArrayBufferView;
export interface HostFileOptions {
/** Required unless the source is a File. */
name?: string;
id?: string;
/** A peer UUID to restrict the offer to, or false to offer it to everyone. */
restricted?: string | false;
}
export interface HostedFile {
id: string;
name: string;
size: number;
restricted: string | false;
}
export interface AdvertisedFile {
id: string;
name: string;
size: number;
}
export interface RequestFileOptions {
/** Emit `fileChunk` events instead of buffering the whole file. */
stream?: boolean;
/** Milliseconds to wait for the peer to start sending. Default 30000. */
timeout?: number;
}
export interface FileTransferResult {
id: string;
name: string;
size: number;
uuid: string;
streamID: string | null;
/** Absent in streaming mode. */
bytes?: Uint8Array;
/** Present only where Blob exists. */
blob?: Blob;
}
export interface ChannelOptions {
/** false allows out-of-order delivery. Default true. */
ordered?: boolean;
/** Partial reliability by retry count. Mutually exclusive with maxPacketLifeTime. */
maxRetransmits?: number;
/** Partial reliability by time in ms. Mutually exclusive with maxRetransmits. */
maxPacketLifeTime?: number;
protocol?: string;
/** Milliseconds to wait for a newly created channel to open. Default 15000. */
timeout?: number;
}
export interface ResourceMetadata {
/** Key the receiver stores this resource under. */
templateName: string;
/** MIME type used to build the receiver's object URL. Defaults to image/png. */
type?: string;
[key: string]: unknown;
}
// ---------------------------------------------------------------------------
// Diagnostics
// ---------------------------------------------------------------------------
export interface PeerQuality {
/** Round-trip time in milliseconds, or null if ICE has not reported one. */
rttMs: number | null;
/** 0..1 across inbound RTP. Null on a data-only peer, which carries no RTP. */
lossRate: number | null;
/** e.g. "host/srflx", "relay/host". Null if the candidate pair is unknown. */
candidatePairType: string | null;
relayed: boolean | null;
availableOutgoingBitrate: number | null;
bytesSent: number;
bytesReceived: number;
}
export interface SDKState {
connected: boolean;
room: string | null;
streamID: string | null;
uuid: string | null;
roomJoined: boolean;
publishing: boolean;
}
// ---------------------------------------------------------------------------
// Events
// ---------------------------------------------------------------------------
export interface DisconnectedDetail {
/** True when the local side asked to disconnect. */
intentional: boolean;
reason: 'local-disconnect' | 'socket-closed' | 'teardown-complete' | string;
willReconnect: boolean;
/** 'socket' fires when the socket closes; 'teardown' when cleanup is finished. */
phase: 'socket' | 'teardown';
}
export interface FileTransferProgressDetail {
uuid: string;
streamID: string | null;
id: string;
name: string;
direction: 'inbound' | 'outbound';
bytes: number;
size: number;
progress: number;
}
/**
* Event payloads by name. Not exhaustive — see docs/api-reference.md for the full list.
*/
export interface VDONinjaEventMap {
connected: undefined;
disconnected: DisconnectedDetail;
/** Emitted exactly once, when teardown genuinely finishes. */
teardownComplete: { reason: string };
reconnecting: unknown;
reconnected: unknown;
reconnectFailed: unknown;
roomJoined: { room: string };
roomLeft: { room: string };
listing: { list: unknown[]; raw: unknown };
peerConnected: { uuid: string; connection: unknown };
peerDisconnected: { uuid: string };
dataChannelOpen: { uuid: string; type: string; streamID: string | null };
dataChannelClose: { uuid: string; type: string; streamID: string | null };
peerInfo: { uuid: string; streamID: string | null; info: Record<string, unknown> };
peerLatency: { uuid: string; latency: number; streamID: string | null };
dataReceived: { data: unknown; uuid: string; streamID?: string | null; fallback?: boolean };
/** Long-standing misspelling, still emitted alongside dataReceived. */
dataRecieved: { data: unknown; uuid: string; streamID?: string | null; fallback?: boolean };
track: { track: MediaStreamTrack; streams?: MediaStream[]; uuid: string; streamID: string | null };
fileList: { uuid: string; streamID: string | null; files: AdvertisedFile[] };
fileTransferStart: {
uuid: string; streamID: string | null; id: string; name: string;
size: number; direction: 'inbound' | 'outbound'; requested: boolean;
};
fileTransferProgress: FileTransferProgressDetail;
fileChunk: {
uuid: string; streamID: string | null; id: string; name: string;
chunk: Uint8Array; bytes: number; size: number;
};
fileTransferComplete: {
uuid: string; id: string; name: string; size: number;
direction: 'inbound' | 'outbound';
};
fileTransferCancelled: {
uuid: string; id: string; name: string; direction: 'inbound' | 'outbound';
};
fileTransferError: {
uuid: string; id: string; name?: string;
direction: 'inbound' | 'outbound'; error: Error;
};
resourceReceived: {
uuid: string; streamID: string | null;
metadata: ResourceMetadata; bytes: Uint8Array;
};
/**
* A peer opened a reserved `x-*` channel. The raw channel is handed over; the
* application owns whatever protocol runs on it.
*/
channelOpen: {
uuid: string; streamID: string | null;
label: string; channel: RTCDataChannel;
};
/** Raw bytes from a peer's sendBinary(). */
binaryReceived: {
uuid: string; streamID: string | null;
bytes: Uint8Array | null; data: unknown;
};
/** A channel's send buffer drained below its low-water mark. */
bufferedAmountLow: {
uuid: string; streamID: string | null;
label: string; bufferedAmount: number;
};
/** A peer opened an auxiliary channel this build does not speak. */
unsupportedChannel: { uuid: string; streamID: string | null; label: string };
}
export interface VDONinjaEvent<K extends keyof VDONinjaEventMap> extends CustomEvent {
detail: VDONinjaEventMap[K];
}
// ---------------------------------------------------------------------------
// SDK
// ---------------------------------------------------------------------------
export declare class VDONinja extends EventTarget {
constructor(options?: VDONinjaOptions);
readonly state: SDKState;
readonly connections: Map<string, Record<string, unknown>>;
readonly streams: Map<string, unknown>;
// -- Connection ---------------------------------------------------------
connect(options?: Record<string, unknown>): Promise<void>;
/** Resolves once teardown genuinely completes. Safe to call more than once. */
disconnect(): Promise<void>;
joinRoom(options: { room: string; password?: Password }): Promise<void>;
leaveRoom(): void;
autoConnect(
roomOrOptions: string | Record<string, unknown>,
filter?: (peer: unknown) => boolean
): Promise<{ stop: () => void; streamID: string }>;
// -- Publishing ---------------------------------------------------------
publish(stream: MediaStream, options?: PublishOptions): Promise<string>;
announce(options?: PublishOptions): Promise<string>;
stopPublishing(): void;
updatePublisherMedia(options: {
media?: MediaPreferences;
videoBitrate?: number;
videoCodec?: string;
clear?: boolean;
}): Promise<Record<string, unknown> | null>;
addTrack(track: MediaStreamTrack, stream?: MediaStream): Promise<void>;
removeTrack(track: MediaStreamTrack): Promise<void>;
replaceTrack(oldTrack: MediaStreamTrack, newTrack: MediaStreamTrack): Promise<void>;
// -- Viewing ------------------------------------------------------------
view(streamID: string, options?: ViewOptions): Promise<RTCPeerConnection>;
stopViewing(streamID: string): void;
// -- Quick helpers ------------------------------------------------------
quickPublish(options: PublishOptions & { stream?: MediaStream }): Promise<string>;
quickView(options: ViewOptions & { streamID: string; room?: string }): Promise<RTCPeerConnection>;
quickSubscribe(options?: ViewOptions & { streamID?: string; room?: string }): Promise<RTCPeerConnection>;
// -- Data ---------------------------------------------------------------
sendData(data: unknown, target?: DataTarget): boolean;
sendPing(uuid?: string): boolean;
request(requestType: string, data: unknown, targetUUID: string, timeout?: number): Promise<unknown>;
respond(requestId: string, data: unknown, targetUUID: string): boolean;
onRequest(requestType: string, handler: (data: unknown, uuid: string) => unknown): void;
// -- Pub/sub ------------------------------------------------------------
subscribe(channels: string | string[]): void;
unsubscribe(channels: string | string[]): void;
getSubscriptions(): string[];
publishToChannel(channel: string, data: unknown, target?: DataTarget | 'all'): boolean;
getPeerSubscriptions(uuid: string): string[];
// -- File transfer ------------------------------------------------------
hostFile(source: FileSource, options?: HostFileOptions): AdvertisedFile;
unhostFile(id: string): boolean;
getHostedFiles(): HostedFile[];
requestFile(uuid: string, fileId: string, options?: RequestFileOptions): Promise<FileTransferResult>;
// -- Resources ----------------------------------------------------------
sendResource(uuid: string, metadata: ResourceMetadata, data: ArrayBuffer | ArrayBufferView): Promise<void>;
// -- Binary and additional channels -------------------------------------
/**
* Send raw bytes over a dedicated reserved channel. Never uses the control channel:
* VDO.Ninja renders binary there as a WebP image.
*/
sendBinary(
data: ArrayBuffer | ArrayBufferView,
uuid: string,
options?: ChannelOptions & { waitForDrain?: boolean }
): Promise<boolean>;
/** Open an additional channel. The label is forced into the reserved `x-` namespace. */
openChannel(uuid: string, label: string, options?: ChannelOptions): Promise<RTCDataChannel>;
getChannel(uuid: string, label: string): RTCDataChannel | null;
/** Omit `label` for the control channel. Null if the peer or channel is unknown. */
getBufferedAmount(uuid: string, label?: string): number | null;
/** Negotiated SCTP limit, or null if the transport has not reported one. */
getMaxMessageSize(uuid: string): number | null;
// -- Diagnostics --------------------------------------------------------
getStats(uuid?: string): Promise<Record<string, unknown>>;
/** Digested per-peer quality. Null if the peer is unknown. */
getPeerQuality(uuid: string): Promise<PeerQuality | null>;
getStreams(): unknown[];
getStreamInfo(streamID: string): unknown;
clearTURNCache(): void;
// -- Typed event helpers ------------------------------------------------
on<K extends keyof VDONinjaEventMap>(event: K, handler: (e: VDONinjaEvent<K>) => void): this;
on(event: string, handler: EventListenerOrEventListenerObject): this;
off<K extends keyof VDONinjaEventMap>(event: K, handler: (e: VDONinjaEvent<K>) => void): this;
off(event: string, handler: EventListenerOrEventListenerObject): this;
once<K extends keyof VDONinjaEventMap>(event: K, handler: (e: VDONinjaEvent<K>) => void): this;
once(event: string, handler: EventListenerOrEventListenerObject): this;
// -- Aliases ------------------------------------------------------------
play(streamID: string, options?: ViewOptions): Promise<RTCPeerConnection>;
watch(streamID: string, options?: ViewOptions): Promise<RTCPeerConnection>;
startViewing(streamID: string, options?: ViewOptions): Promise<RTCPeerConnection>;
stop(streamID: string): void;
stopPlaying(streamID: string): void;
stopWatching(streamID: string): void;
stream(stream: MediaStream, options?: PublishOptions): Promise<string>;
broadcast(stream: MediaStream, options?: PublishOptions): Promise<string>;
startPublishing(stream: MediaStream, options?: PublishOptions): Promise<string>;
share(stream: MediaStream, options?: PublishOptions): Promise<string>;
stopStreaming(): void;
stopBroadcasting(): void;
stopSharing(): void;
unpublish(): void;
quickStream(options: PublishOptions & { stream?: MediaStream }): Promise<string>;
quickBroadcast(options: PublishOptions & { stream?: MediaStream }): Promise<string>;
quickShare(options: PublishOptions & { stream?: MediaStream }): Promise<string>;
quickPlay(options: ViewOptions & { streamID: string }): Promise<RTCPeerConnection>;
quickWatch(options: ViewOptions & { streamID: string }): Promise<RTCPeerConnection>;
join(options: { room: string; password?: Password }): Promise<void>;
enterRoom(options: { room: string; password?: Password }): Promise<void>;
enter(options: { room: string; password?: Password }): Promise<void>;
leave(): void;
exitRoom(): void;
exit(): void;
}
export default VDONinja;
export { VDONinja as VDONinjaSDK };