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

455 lines (398 loc) 18.1 kB
/* * 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 };