@vdoninja/sdk
Version:
AI-friendly P2P communication SDK for audio, video, and data
485 lines (424 loc) • 17.4 kB
JavaScript
#!/usr/bin/env node
/**
* Comprehensive test suite for VDO.Ninja SDK
* Run all tests locally to verify SDK functionality
*
* Usage: node run-all-tests.js
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
// Check if WebRTC is available
let webrtcLib = null;
try {
require.resolve('@roamhq/wrtc');
webrtcLib = '@roamhq/wrtc';
console.log('✅ Using @roamhq/wrtc for WebRTC');
} catch (e) {
try {
require.resolve('node-datachannel');
webrtcLib = 'node-datachannel';
console.log('✅ Using node-datachannel for WebRTC');
} catch (e2) {
console.error('❌ No WebRTC implementation found!');
console.error('Please install one of:');
console.error(' npm install @roamhq/wrtc');
console.error(' npm install node-datachannel');
process.exit(1);
}
}
const tests = [
{
name: 'Basic Connectivity',
file: 'test-connectivity.js',
code: `
const wrtc = require('${webrtcLib}');
const WebSocket = require('ws');
const crypto = require('crypto');
const VDONinjaSDK = require('./vdoninja-sdk-node.js');
// Polyfills for Node.js
global.WebSocket = WebSocket;
global.crypto = crypto.webcrypto || crypto;
if (wrtc.RTCPeerConnection) {
global.RTCPeerConnection = wrtc.RTCPeerConnection;
global.RTCIceCandidate = wrtc.RTCIceCandidate;
global.RTCSessionDescription = wrtc.RTCSessionDescription;
}
global.document = { createElement: () => ({ innerText: '', textContent: '' }) };
global.CustomEvent = class CustomEvent extends Event {
constructor(type, options) {
super(type, options);
this.detail = options?.detail;
}
};
global.btoa = (str) => Buffer.from(str).toString('base64');
global.atob = (str) => Buffer.from(str, 'base64').toString();
const WSS = process.env.WSS_URL || 'wss://apibackup.vdo.ninja';
async function test() {
const vdo = new VDONinjaSDK({ host: WSS });
try {
await vdo.connect();
console.log(' ✓ Connected to signaling server');
await vdo.joinRoom({ room: "test-room-" + Date.now() });
console.log(' ✓ Joined room');
await vdo.announce({ streamID: "test-stream" });
console.log(' ✓ Announced stream');
vdo.disconnect();
console.log(' ✓ Disconnected cleanly');
process.exit(0);
} catch (error) {
console.error(' ✗ Test failed:', error.message);
process.exit(1);
}
}
test();`
},
{
name: 'P2P Data Channel',
file: 'test-p2p-datachannel.js',
code: `
const wrtc = require('${webrtcLib}');
const WebSocket = require('ws');
const crypto = require('crypto');
const VDONinjaSDK = require('./vdoninja-sdk-node.js');
// Polyfills for Node.js
global.WebSocket = WebSocket;
global.crypto = crypto.webcrypto || crypto;
if (wrtc.RTCPeerConnection) {
global.RTCPeerConnection = wrtc.RTCPeerConnection;
global.RTCIceCandidate = wrtc.RTCIceCandidate;
global.RTCSessionDescription = wrtc.RTCSessionDescription;
}
global.document = { createElement: () => ({ innerText: '', textContent: '' }) };
global.CustomEvent = class CustomEvent extends Event {
constructor(type, options) {
super(type, options);
this.detail = options?.detail;
}
};
global.btoa = (str) => Buffer.from(str).toString('base64');
global.atob = (str) => Buffer.from(str, 'base64').toString();
const WSS = process.env.WSS_URL || 'wss://apibackup.vdo.ninja';
const TEST_ROOM = 'local-p2p-' + Math.random().toString(36).substr(2, 9) + '-' + Date.now();
const PUBLISHER_STREAM = 'pub-' + Math.random().toString(36).substr(2, 9);
let messageReceived = false;
async function createPublisher() {
const publisher = new VDONinjaSDK({ host: WSS });
publisher.addEventListener('peerConnected', async (event) => {
console.log(' ✓ Publisher detected peer connection');
setTimeout(() => {
publisher.sendData({ test: 'message', timestamp: Date.now() });
console.log(' ✓ Publisher sent message');
}, 2000);
});
await publisher.connect();
await publisher.joinRoom({ room: TEST_ROOM });
await publisher.announce({ streamID: PUBLISHER_STREAM });
return publisher;
}
async function createViewer() {
const viewer = new VDONinjaSDK({ host: WSS });
viewer.addEventListener('dataReceived', (event) => {
console.log(' ✓ Viewer received:', event.detail.data);
messageReceived = true;
});
await viewer.connect();
await viewer.joinRoom({ room: TEST_ROOM });
await viewer.view(PUBLISHER_STREAM);
return viewer;
}
async function test() {
try {
const publisher = await createPublisher();
await new Promise(r => setTimeout(r, 5000));
const viewer = await createViewer();
// Wait longer for connection establishment and message
await new Promise(r => setTimeout(r, 8000));
if (messageReceived) {
console.log(' ✓ P2P data channel works');
publisher.disconnect();
viewer.disconnect();
process.exit(0);
} else {
throw new Error('No message received');
}
} catch (error) {
console.error(' ✗ P2P test failed:', error.message);
process.exit(1);
}
}
test();`
},
{
name: 'Bidirectional Communication',
file: 'test-bidirectional.js',
code: `
const wrtc = require('${webrtcLib}');
const WebSocket = require('ws');
const crypto = require('crypto');
const VDONinjaSDK = require('./vdoninja-sdk-node.js');
// Polyfills for Node.js
global.WebSocket = WebSocket;
global.crypto = crypto.webcrypto || crypto;
if (wrtc.RTCPeerConnection) {
global.RTCPeerConnection = wrtc.RTCPeerConnection;
global.RTCIceCandidate = wrtc.RTCIceCandidate;
global.RTCSessionDescription = wrtc.RTCSessionDescription;
}
global.document = { createElement: () => ({ innerText: '', textContent: '' }) };
global.CustomEvent = class CustomEvent extends Event {
constructor(type, options) {
super(type, options);
this.detail = options?.detail;
}
};
global.btoa = (str) => Buffer.from(str).toString('base64');
global.atob = (str) => Buffer.from(str, 'base64').toString();
const WSS = process.env.WSS_URL || 'wss://apibackup.vdo.ninja';
const TEST_ROOM = 'local-bidir-' + Math.random().toString(36).substr(2, 9) + '-' + Date.now();
const PUB_STREAM = 'pub-' + Math.random().toString(36).substr(2, 9);
let pubRecv = false, viewRecv = false;
async function test() {
try {
const publisher = new VDONinjaSDK({ host: WSS });
const viewer = new VDONinjaSDK({ host: WSS });
publisher.addEventListener('dataReceived', (e) => {
console.log(' ✓ Publisher received:', e.detail.data);
pubRecv = true;
});
viewer.addEventListener('dataReceived', (e) => {
console.log(' ✓ Viewer received:', e.detail.data);
viewRecv = true;
});
await publisher.connect();
await publisher.joinRoom({ room: TEST_ROOM });
await publisher.announce({ streamID: PUB_STREAM });
await viewer.connect();
await viewer.joinRoom({ room: TEST_ROOM });
await viewer.view(PUB_STREAM);
await new Promise(r => setTimeout(r, 3000));
publisher.sendData({ from: 'publisher', test: 1 });
viewer.sendData({ from: 'viewer', test: 2 });
await new Promise(r => setTimeout(r, 5000));
if (pubRecv && viewRecv) {
console.log(' ✓ Bidirectional communication works');
publisher.disconnect();
viewer.disconnect();
process.exit(0);
} else {
throw new Error(\`Bidirectional failed: pub=\${pubRecv}, view=\${viewRecv}\`);
}
} catch (error) {
console.error(' ✗ Bidirectional test failed:', error.message);
process.exit(1);
}
}
test();`
},
{
name: 'Duplicate Prevention',
file: 'test-no-duplicates.js',
code: `
const wrtc = require('${webrtcLib}');
const WebSocket = require('ws');
const crypto = require('crypto');
const VDONinjaSDK = require('./vdoninja-sdk.js');
// Polyfills for Node.js
global.WebSocket = WebSocket;
global.crypto = crypto.webcrypto || crypto;
if (wrtc.RTCPeerConnection) {
global.RTCPeerConnection = wrtc.RTCPeerConnection;
global.RTCIceCandidate = wrtc.RTCIceCandidate;
global.RTCSessionDescription = wrtc.RTCSessionDescription;
}
global.document = { createElement: () => ({ innerText: '', textContent: '' }) };
global.CustomEvent = class CustomEvent extends Event {
constructor(type, options) {
super(type, options);
this.detail = options?.detail;
}
};
global.btoa = (str) => Buffer.from(str).toString('base64');
global.atob = (str) => Buffer.from(str, 'base64').toString();
const WSS = process.env.WSS_URL || 'wss://apibackup.vdo.ninja';
const TEST_ROOM = 'local-dup-' + Math.random().toString(36).substr(2, 9) + '-' + Date.now();
const PEER1_STREAM = 'p1-' + Math.random().toString(36).substr(2, 9);
const PEER2_STREAM = 'p2-' + Math.random().toString(36).substr(2, 9);
let messagesReceived = [];
let PEER2_UUID = null;
const DP_DEBUG = !!process.env.DP_DEBUG && process.env.DP_DEBUG !== '0';
const dp = (...args) => { if (DP_DEBUG) console.log(...args); };
async function test() {
try {
const peer1 = new VDONinjaSDK({ host: WSS });
const peer2 = new VDONinjaSDK({ host: WSS });
// Capture peer2's UUID from peer1's perspective
peer1.addEventListener('peerConnected', (e) => {
dp('[DP] peer1 peerConnected:', e.detail?.uuid, e.detail?.connection?.type);
if (!PEER2_UUID && e?.detail?.uuid) {
PEER2_UUID = e.detail.uuid;
}
});
peer2.addEventListener('peerConnected', (e) => {
dp('[DP] peer2 peerConnected:', e.detail?.uuid, e.detail?.connection?.type);
});
for (const sdk of [peer1, peer2]) {
sdk.addEventListener('dataChannelOpen', (e) => dp('[DP] dataChannelOpen', sdk===peer1?'peer1':'peer2', e.detail));
sdk.addEventListener('listing', (e) => dp('[DP] listing', sdk===peer1?'peer1':'peer2', e.detail?.streamID || '(list)'));
sdk.addEventListener('videoaddedtoroom', (e) => dp('[DP] videoadded', sdk===peer1?'peer1':'peer2', e.detail.streamID, e.detail.uuid));
sdk.addEventListener('streamAdded', (e) => dp('[DP] streamAdded', sdk===peer1?'peer1':'peer2', e.detail.streamID, e.detail.uuid));
sdk.addEventListener('error', (e) => dp('[DP] error', sdk===peer1?'peer1':'peer2', e.detail));
sdk.addEventListener('alert', (e) => dp('[DP] alert', sdk===peer1?'peer1':'peer2', e.detail));
}
peer2.addEventListener('dataReceived', (e) => {
const d = e.detail?.data;
if (d && d.test === 'no-duplicate' && d.id === 1) {
messagesReceived.push(d);
dp('[DP] peer2 counted dataReceived. total=', messagesReceived.length);
} else {
dp('[DP] peer2 ignored dataReceived:', d);
}
});
// Both announce and view each other (dual connection)
dp('[DP] peer1.connect');
await peer1.connect();
dp('[DP] peer1.joinRoom');
await peer1.joinRoom({ room: TEST_ROOM });
dp('[DP] peer1.announce');
await peer1.announce({ streamID: PEER1_STREAM });
dp('[DP] peer2.connect');
await peer2.connect();
dp('[DP] peer2.joinRoom');
await peer2.joinRoom({ room: TEST_ROOM });
dp('[DP] peer2.announce');
await peer2.announce({ streamID: PEER2_STREAM });
await new Promise(r => setTimeout(r, 1000));
dp('[DP] peer1.view(PEER2_STREAM)');
await peer1.view(PEER2_STREAM);
dp('[DP] peer2.view(PEER1_STREAM)');
await peer2.view(PEER1_STREAM);
// Retry sending until the SDK confirms sent (up to 12s)
const payload = { test: 'no-duplicate', id: 1 };
let sent = false;
let attempts = 0;
const deadline = Date.now() + 12000;
while (!sent && Date.now() < deadline) {
attempts++;
if (PEER2_UUID) {
sent = peer1.sendData(payload, PEER2_UUID);
dp('[DP] send attempt', attempts, '(uuid)', PEER2_UUID, 'sent=', sent);
} else {
sent = peer1.sendData(payload, { streamID: PEER2_STREAM, type: 'viewer' });
dp('[DP] send attempt', attempts, '(streamID viewer)', PEER2_STREAM, 'sent=', sent);
}
if (!sent) {
try {
const list1 = [];
for (const [uuid, conns] of peer1.connections || []) {
for (const t of ['publisher','viewer']) {
const c = conns[t];
if (c) list1.push({ uuid: c.uuid, type: c.type, streamID: c.streamID, dc: c.dataChannel?.readyState, ice: c.pc?.iceConnectionState });
}
}
dp('[DP] peer1 connections:', list1);
const list2 = [];
for (const [uuid, conns] of peer2.connections || []) {
for (const t of ['publisher','viewer']) {
const c = conns[t];
if (c) list2.push({ uuid: c.uuid, type: c.type, streamID: c.streamID, dc: c.dataChannel?.readyState, ice: c.pc?.iceConnectionState });
}
}
dp('[DP] peer2 connections:', list2);
try {
const v1 = peer1._getConnections ? peer1._getConnections({ streamID: PEER2_STREAM, type: 'viewer' }) : [];
const p1 = peer1._getConnections ? peer1._getConnections({ streamID: PEER2_STREAM, type: 'publisher' }) : [];
dp('[DP] peer1 _getConnections viewer count:', v1.length, 'publisher count:', p1.length);
} catch (e2) {}
} catch (e) {}
await new Promise(r => setTimeout(r, 300));
}
}
dp('[DP] after send attempts, sent=', sent);
if (!sent) {
dp('[DP] state: p1.connected=', peer1.state.connected, 'p2.connected=', peer2.state.connected);
throw new Error('Failed to send test message after retries');
}
await new Promise(r => setTimeout(r, 5000));
console.log("6");
if (messagesReceived.length === 1) {
console.log(' ✓ No duplicates with dual connections');
peer1.disconnect();
peer2.disconnect();
process.exit(0);
} else {
throw new Error(\`Expected 1 message, got \${messagesReceived.length}\`);
}
} catch (error) {
console.error(' ✗ Duplicate prevention test failed:', error.message);
process.exit(1);
}
}
test();`
}
];
async function runTest(test) {
return new Promise((resolve) => {
console.log(`\n📝 Testing: ${test.name}`);
console.log('─'.repeat(40));
// Write test file
fs.writeFileSync(test.file, test.code);
// Run test with timeout
const child = spawn('node', [test.file], {
stdio: 'inherit',
timeout: 30000
});
child.on('exit', (code) => {
// Clean up test file
try {
fs.unlinkSync(test.file);
} catch (e) {}
if (code === 0) {
console.log(`✅ ${test.name} passed!`);
resolve(true);
} else {
console.log(`❌ ${test.name} failed!`);
resolve(false);
}
});
child.on('error', (error) => {
console.error(`❌ ${test.name} error:`, error.message);
resolve(false);
});
});
}
async function runAllTests() {
console.log('🚀 VDO.Ninja SDK Test Suite');
console.log('═'.repeat(40));
const results = [];
for (const test of tests) {
const passed = await runTest(test);
results.push({ name: test.name, passed });
}
// Summary
console.log('\n' + '═'.repeat(40));
console.log('📊 Test Summary');
console.log('─'.repeat(40));
const passed = results.filter(r => r.passed).length;
const failed = results.filter(r => !r.passed).length;
results.forEach(r => {
console.log(` ${r.passed ? '✅' : '❌'} ${r.name}`);
});
console.log('─'.repeat(40));
if (failed === 0) {
console.log(`\n🎉 All tests passed! (${passed}/${results.length})`);
process.exit(0);
} else {
console.log(`\n⚠️ ${failed} test(s) failed (${passed}/${results.length} passed)`);
process.exit(1);
}
}
// Run tests
runAllTests().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});