@vdoninja/sdk
Version:
AI-friendly P2P communication SDK for audio, video, and data
367 lines (320 loc) • 12.9 kB
JavaScript
/**
* WebRTC Adapter for VDO.Ninja SDK
*
* This adapter provides a unified interface for different WebRTC implementations
* in Node.js environments. It supports:
* - Browser native WebRTC (default)
* - @roamhq/wrtc (Node.js)
* - node-datachannel (Node.js)
*
* The adapter automatically detects available implementations and provides
* a consistent API regardless of which library is being used.
*/
class WebRTCAdapter {
constructor() {
this.implementation = null;
this.RTCPeerConnection = null;
this.RTCSessionDescription = null;
this.RTCIceCandidate = null;
this.mediaDevices = null;
this._detectImplementation();
}
/**
* Detect and load available WebRTC implementation
* @private
*/
_detectImplementation() {
// 1. Check if we're in a browser environment
if (typeof window !== 'undefined' && window.RTCPeerConnection) {
this.implementation = 'browser';
this.RTCPeerConnection = window.RTCPeerConnection;
this.RTCSessionDescription = window.RTCSessionDescription;
this.RTCIceCandidate = window.RTCIceCandidate;
this.mediaDevices = navigator.mediaDevices;
return;
}
// 2. Try @roamhq/wrtc first (most compatible)
try {
const wrtc = require('@roamhq/wrtc');
this.implementation = '@roamhq/wrtc';
this.RTCPeerConnection = wrtc.RTCPeerConnection;
this.RTCSessionDescription = wrtc.RTCSessionDescription;
this.RTCIceCandidate = wrtc.RTCIceCandidate;
this.mediaDevices = wrtc.mediaDevices || this._createMediaDevicesShim(wrtc);
return;
} catch (e) {
// @roamhq/wrtc not available
}
// 3. Try legacy wrtc
try {
const wrtc = require('wrtc');
this.implementation = 'wrtc';
this.RTCPeerConnection = wrtc.RTCPeerConnection;
this.RTCSessionDescription = wrtc.RTCSessionDescription;
this.RTCIceCandidate = wrtc.RTCIceCandidate;
this.mediaDevices = wrtc.mediaDevices || this._createMediaDevicesShim(wrtc);
return;
} catch (e) {
// wrtc not available
}
// 4. Try node-datachannel (requires wrapper)
try {
const nodeDataChannel = require('node-datachannel');
this.implementation = 'node-datachannel';
this._setupNodeDataChannel(nodeDataChannel);
return;
} catch (e) {
// node-datachannel not available
}
// 5. No WebRTC implementation found
throw new Error(
'No WebRTC implementation found. Please install one of:\n' +
'- @roamhq/wrtc (recommended)\n' +
'- node-datachannel\n' +
'Or use this SDK in a browser environment.'
);
}
/**
* Create a media devices shim for wrtc implementations
* @private
*/
_createMediaDevicesShim(wrtc) {
return {
getUserMedia: async (constraints) => {
// Basic implementation - can be extended
if (wrtc.getUserMedia) {
return new Promise((resolve, reject) => {
wrtc.getUserMedia(constraints, resolve, reject);
});
}
throw new Error('getUserMedia not supported in this wrtc implementation');
},
enumerateDevices: async () => {
// Return empty array if not supported
return [];
}
};
}
/**
* Setup node-datachannel with WebRTC-compatible wrapper
* @private
*/
_setupNodeDataChannel(nodeDataChannel) {
const self = this;
// Create RTCPeerConnection wrapper
this.RTCPeerConnection = class RTCPeerConnectionWrapper {
constructor(config) {
this._pc = new nodeDataChannel.PeerConnection(
config?.iceServers?.[0]?.urls?.[0] || '',
{
iceServers: config?.iceServers || [],
iceTransportPolicy: config?.iceTransportPolicy || 'all'
}
);
this.localDescription = null;
this.remoteDescription = null;
this.signalingState = 'stable';
this.iceConnectionState = 'new';
this.iceGatheringState = 'new';
this.connectionState = 'new';
this._dataChannels = new Map();
this._tracks = [];
this._streams = [];
// Event handlers
this.onicecandidate = null;
this.oniceconnectionstatechange = null;
this.onicegatheringstatechange = null;
this.onsignalingstatechange = null;
this.ondatachannel = null;
this.ontrack = null;
// Setup internal event handlers
this._setupEventHandlers();
}
_setupEventHandlers() {
this._pc.onLocalDescription((sdp, type) => {
this.localDescription = { sdp, type };
this.signalingState = 'have-local-offer';
});
this._pc.onLocalCandidate((candidate, mid) => {
if (this.onicecandidate) {
this.onicecandidate({
candidate: candidate ? {
candidate: candidate,
sdpMid: mid,
sdpMLineIndex: 0
} : null
});
}
});
this._pc.onStateChange((state) => {
this.iceConnectionState = state;
if (this.oniceconnectionstatechange) {
this.oniceconnectionstatechange();
}
});
this._pc.onDataChannel((dc) => {
const channel = this._wrapDataChannel(dc);
if (this.ondatachannel) {
this.ondatachannel({ channel });
}
});
}
async createOffer(options) {
const sdp = await this._pc.createOffer();
return { sdp, type: 'offer' };
}
async createAnswer(options) {
const sdp = await this._pc.createAnswer();
return { sdp, type: 'answer' };
}
async setLocalDescription(desc) {
this._pc.setLocalDescription(desc.type);
this.localDescription = desc;
this.signalingState = desc.type === 'offer' ? 'have-local-offer' : 'stable';
}
async setRemoteDescription(desc) {
this._pc.setRemoteDescription(desc.sdp, desc.type);
this.remoteDescription = desc;
this.signalingState = desc.type === 'offer' ? 'have-remote-offer' : 'stable';
}
async addIceCandidate(candidate) {
if (candidate && candidate.candidate) {
this._pc.addRemoteCandidate(candidate.candidate, candidate.sdpMid || '0');
}
}
createDataChannel(label, options) {
const dc = this._pc.createDataChannel(label, options);
return this._wrapDataChannel(dc);
}
_wrapDataChannel(dc) {
const wrapper = {
label: dc.label,
id: dc.id,
readyState: 'connecting',
bufferedAmount: 0,
bufferedAmountLowThreshold: 0,
binaryType: 'arraybuffer',
// Event handlers
onopen: null,
onclose: null,
onmessage: null,
onerror: null,
send: (data) => {
if (typeof data === 'string') {
dc.sendMessage(data);
} else {
dc.sendMessageBinary(new Uint8Array(data));
}
},
close: () => {
dc.close();
}
};
dc.onOpen(() => {
wrapper.readyState = 'open';
if (wrapper.onopen) wrapper.onopen();
});
dc.onClosed(() => {
wrapper.readyState = 'closed';
if (wrapper.onclose) wrapper.onclose();
});
dc.onMessage((msg) => {
if (wrapper.onmessage) {
wrapper.onmessage({ data: msg });
}
});
dc.onError((error) => {
if (wrapper.onerror) wrapper.onerror(error);
});
return wrapper;
}
// Media-related methods (limited support in node-datachannel)
addTrack(track, stream) {
console.warn('Media tracks not fully supported in node-datachannel. Use data channels for media transport.');
this._tracks.push({ track, stream });
return { track, streams: [stream] };
}
removeTrack(sender) {
const index = this._tracks.findIndex(t => t.track === sender.track);
if (index !== -1) {
this._tracks.splice(index, 1);
}
}
getSenders() {
return this._tracks.map(t => ({ track: t.track }));
}
getReceivers() {
return [];
}
getTransceivers() {
return [];
}
close() {
this._pc.close();
this.signalingState = 'closed';
this.iceConnectionState = 'closed';
this.connectionState = 'closed';
}
};
// RTCSessionDescription wrapper
this.RTCSessionDescription = class {
constructor(init) {
this.type = init.type;
this.sdp = init.sdp;
}
};
// RTCIceCandidate wrapper
this.RTCIceCandidate = class {
constructor(init) {
this.candidate = init.candidate;
this.sdpMid = init.sdpMid;
this.sdpMLineIndex = init.sdpMLineIndex;
}
};
// Media devices stub (no camera/mic support in node-datachannel)
this.mediaDevices = {
getUserMedia: async (constraints) => {
throw new Error('getUserMedia not supported with node-datachannel. Use @roamhq/wrtc for media capture.');
},
enumerateDevices: async () => []
};
}
/**
* Get the detected implementation name
*/
getImplementation() {
return this.implementation;
}
/**
* Check if media (audio/video) is supported
*/
hasMediaSupport() {
return this.implementation !== 'node-datachannel';
}
/**
* Create a new RTCPeerConnection with the detected implementation
*/
createPeerConnection(config) {
if (!this.RTCPeerConnection) {
throw new Error('No WebRTC implementation available');
}
return new this.RTCPeerConnection(config);
}
/**
* Get user media (camera/microphone)
*/
async getUserMedia(constraints) {
if (!this.mediaDevices || !this.mediaDevices.getUserMedia) {
throw new Error('getUserMedia not supported in current implementation');
}
return this.mediaDevices.getUserMedia(constraints);
}
}
// Export for different module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = WebRTCAdapter;
} else if (typeof define === 'function' && define.amd) {
define([], () => WebRTCAdapter);
} else {
window.WebRTCAdapter = WebRTCAdapter;
}