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

316 lines (285 loc) 10.6 kB
// Copyright (c) 2025-2026 Steve Seguin // SPDX-License-Identifier: MIT // See LICENSE-MIT for the full terms. /** * VDO.Ninja SDK - Node.js Version with WebRTC Adapter * * This version extends the main SDK to support multiple WebRTC implementations * in Node.js environments. It automatically detects and uses: * - @roamhq/wrtc (recommended, full media support) * - node-datachannel (data channels only, no media) * * Usage: * const VDONinjaSDK = require('./vdoninja-sdk-node.js'); * const sdk = new VDONinjaSDK({ room: 'myroom' }); * * // Check which implementation is being used * console.log('Using WebRTC:', sdk.webrtcAdapter.getImplementation()); * console.log('Media support:', sdk.webrtcAdapter.hasMediaSupport()); */ // Check for required dependencies try { require.resolve('ws'); } catch (e) { console.error('ERROR: WebSocket module not found. Please install it with: npm install ws'); process.exit(1); } // Load the WebRTC adapter const WebRTCAdapter = require('./webrtc-adapter.js'); // Load the original SDK const originalSDKCode = require('fs').readFileSync(__dirname + '/vdoninja-sdk.js', 'utf8'); // Import TextEncoder/TextDecoder const { TextEncoder, TextDecoder } = require('util'); // AbortController - use native if available (Node 15+), otherwise polyfill let AbortControllerPolyfill; try { AbortControllerPolyfill = AbortController; } catch (e) { AbortControllerPolyfill = require('abort-controller').AbortController; } // Create EventTarget polyfill for older Node versions let EventTargetPolyfill; try { // Try to use native EventTarget (Node 15+) EventTargetPolyfill = EventTarget; } catch (e) { // Fallback to events.EventEmitter wrapped as EventTarget const { EventEmitter } = require('events'); EventTargetPolyfill = class EventTarget extends EventEmitter { constructor() { super(); this._emitter = this; } addEventListener(type, listener) { this.on(type, listener); } removeEventListener(type, listener) { this.off(type, listener); } dispatchEvent(event) { this.emit(event.type, event); return true; } }; } // Create CustomEvent using native Event if available let CustomEventPolyfill; try { // Try to use native CustomEvent (Node 19+) CustomEventPolyfill = CustomEvent; } catch (e) { // Fallback for older Node versions CustomEventPolyfill = class extends Event { constructor(type, options = {}) { super(type, options); this.detail = options.detail || null; } }; } // Initialize WebRTC adapter BEFORE creating context const webrtcAdapter = new WebRTCAdapter(); // Create a module context and evaluate the SDK const sdkModule = { exports: {} }; const sdkContext = { module: sdkModule, exports: sdkModule.exports, require: require, __dirname: __dirname, __filename: __filename, global: global, console: console, process: process, Buffer: Buffer, setTimeout: setTimeout, clearTimeout: clearTimeout, setInterval: setInterval, clearInterval: clearInterval, setImmediate: setImmediate, clearImmediate: clearImmediate, EventTarget: EventTargetPolyfill, WebSocket: require('ws'), crypto: require('crypto'), document: { createElement: () => ({ innerText: '', textContent: '' }) }, window: { crypto: require('crypto').webcrypto || require('crypto'), EventTarget: EventTargetPolyfill }, TextEncoder: TextEncoder, TextDecoder: TextDecoder, // Share the host realm's binary types. Without these, the vm context builds typed // arrays with its own constructors, so anything the SDK hands back — file transfer // bytes, sendBinary payloads, received resources — fails `instanceof Uint8Array` in // the caller's code even though the data is correct. Injecting them makes the SDK // construct host-realm objects, so instanceof behaves the way a caller expects. ArrayBuffer: ArrayBuffer, SharedArrayBuffer: typeof SharedArrayBuffer !== 'undefined' ? SharedArrayBuffer : undefined, Uint8Array: Uint8Array, Uint16Array: Uint16Array, Uint32Array: Uint32Array, Int8Array: Int8Array, Int16Array: Int16Array, Int32Array: Int32Array, Float32Array: Float32Array, Float64Array: Float64Array, DataView: DataView, Blob: typeof Blob !== 'undefined' ? Blob : undefined, CustomEvent: CustomEventPolyfill, AbortController: AbortControllerPolyfill, RTCPeerConnection: webrtcAdapter.RTCPeerConnection, RTCSessionDescription: webrtcAdapter.RTCSessionDescription, RTCIceCandidate: webrtcAdapter.RTCIceCandidate, MediaStream: webrtcAdapter.MediaStream, MediaStreamTrack: webrtcAdapter.MediaStreamTrack, RTCAudioSource: webrtcAdapter.nonstandard?.RTCAudioSource, RTCAudioSink: webrtcAdapter.nonstandard?.RTCAudioSink, fetch: fetch }; // Evaluate SDK in isolated context const vm = require('vm'); vm.runInNewContext(originalSDKCode, sdkContext); // Get the original SDK class - it should be in module.exports const OriginalVDONinjaSDK = sdkModule.exports; // Create extended SDK class with WebRTC adapter class VDONinjaSDKNode extends OriginalVDONinjaSDK { constructor(options = {}) { // Call parent constructor super(options); // Store adapter reference (use the global one we created) this.webrtcAdapter = webrtcAdapter; // Log implementation details if (this.debug) { console.log(`[VDO.Ninja SDK] Using WebRTC implementation: ${webrtcAdapter.getImplementation()}`); console.log(`[VDO.Ninja SDK] Media support available: ${webrtcAdapter.hasMediaSupport()}`); } // Add event emitter style methods for Node.js compatibility this.on = this.addEventListener; this.off = this.removeEventListener; this.emit = (event, data) => { if (typeof event === 'string') { this._emit(event, data); } else { this.dispatchEvent(event); } }; } /** * Override _emit to handle Node.js environment * @private */ _emit(eventName, detail = {}) { // For Node.js, we need to handle events differently const event = new CustomEventPolyfill(eventName, { detail }); // Get all listeners for this event const listeners = this.listeners ? this.listeners(eventName) : []; // If using EventEmitter-style (Node.js) if (this._events && this._events[eventName]) { // Emit using EventEmitter style super.emit(eventName, event); } else { // Try native dispatchEvent if available try { super._emit(eventName, detail); } catch (e) { // Fallback to manual listener invocation if (this._eventListeners && this._eventListeners[eventName]) { this._eventListeners[eventName].forEach(listener => { listener.call(this, event); }); } } } } /** * Override _createConnection to use adapter - REMOVED * The parent class will handle this properly * @private */ /** * Override publish to check media support */ async publish(streamOrOptions, maybeOptions = {}) { const MediaStreamCtor = this.webrtcAdapter.MediaStream || global.MediaStream; let stream = streamOrOptions; let options = maybeOptions; // Support legacy style: publish({ stream, ...options }) if ( streamOrOptions && typeof streamOrOptions === 'object' && !(MediaStreamCtor && streamOrOptions instanceof MediaStreamCtor) && !(typeof streamOrOptions.getTracks === 'function') && streamOrOptions.stream ) { stream = streamOrOptions.stream; options = { ...streamOrOptions }; delete options.stream; } const isMediaStream = MediaStreamCtor && stream instanceof MediaStreamCtor; const isMediaLike = isMediaStream || (stream && typeof stream.getTracks === 'function'); const wantsMedia = isMediaLike || options.audio || options.video; if (wantsMedia && !this.webrtcAdapter.hasMediaSupport()) { throw new Error( `Media publishing not supported with ${this.webrtcAdapter.getImplementation()}. ` + `Please install @roamhq/wrtc for audio/video support.` ); } return super.publish(stream, options); } /** * Get WebRTC implementation details */ getWebRTCInfo() { return { implementation: this.webrtcAdapter.getImplementation(), hasMediaSupport: this.webrtcAdapter.hasMediaSupport(), adapter: this.webrtcAdapter }; } /** * Static method to check available WebRTC implementations */ static checkWebRTCSupport() { const implementations = []; // Check @roamhq/wrtc try { require.resolve('@roamhq/wrtc'); implementations.push({ name: '@roamhq/wrtc', available: true, mediaSupport: true, recommended: true }); } catch (e) { implementations.push({ name: '@roamhq/wrtc', available: false, mediaSupport: true, recommended: true }); } // Check node-datachannel try { require.resolve('node-datachannel'); implementations.push({ name: 'node-datachannel', available: true, mediaSupport: false, recommended: false }); } catch (e) { implementations.push({ name: 'node-datachannel', available: false, mediaSupport: false, recommended: false }); } return implementations; } } // Export the Node.js SDK module.exports = VDONinjaSDKNode; module.exports.default = VDONinjaSDKNode; module.exports.VDONinja = VDONinjaSDKNode; module.exports.VDONinjaSDK = VDONinjaSDKNode;