/** * WebRTC Test Harness * * A utility for testing WebRTC DataChannel and MediaStream connections * between two twins using direct PhyHub connection (bypassing phyos). * * Prerequisites: * 1. Get device credentials from CLI: * phy dev list * phy dev select # Select a device * phy dev develop --export # Get PHYGRID_DEVICE_ID and PHYGRID_DEVICE_KEY * * 2. You need TWO devices for testing WebRTC between them * * Usage (two terminals): * * Terminal 1 (Device A - initiator): * ``` * PHYHUB_DIRECT=true \ * DEVICE_ID= \ * ACCESS_KEY= \ * PEER_TWIN_ID= \ * ROLE=initiator \ * bunx ts-node src/test/webrtc-test-harness.ts * ``` * * Terminal 2 (Device B - responder): * ``` * PHYHUB_DIRECT=true \ * DEVICE_ID= \ * ACCESS_KEY= \ * PEER_TWIN_ID= \ * ROLE=responder \ * bunx ts-node src/test/webrtc-test-harness.ts * ``` * * Environment variables: * PHYHUB_DIRECT - Must be 'true' to enable direct connection * DEVICE_ID - Your device ID (also used as twin ID) * ACCESS_KEY - Your device access key * PEER_TWIN_ID - The twin ID of the peer to connect to * ROLE - 'initiator' or 'responder' * PHYHUB_URL - PhyHub URL (optional, defaults to EU region) * PHYHUB_REGION - 'eu', 'us', or 'local' (optional) */ import { PhyHubClient } from '../index'; import { PhygridDataChannel } from '../services/webrtc/types'; async function runInitiator(peerTwinId: string): Promise { console.log('\n=== Running as INITIATOR ===\n'); // Connect to PhyHub console.log('Connecting to PhyHub...'); const client = await PhyHubClient.connect(); console.log('Connected!\n'); // Get WebRTC manager with verbose mode const manager = await client.getWebRTCManager({ verbose: true }); // Setup event listeners manager.on('connected', (data) => { console.log(`[EVENT] Connected to ${data.targetTwinId} (${data.connectionType})`); }); manager.on('disconnected', (data) => { console.log(`[EVENT] Disconnected from ${data.targetTwinId}`); }); manager.on('error', (data) => { console.error(`[EVENT] Error:`, data.error); }); manager.on('reconnecting', (data) => { console.log(`[EVENT] Reconnecting to ${data.targetTwinId} (attempt ${data.attempt})`); }); console.log(`Creating DataChannel to peer: ${peerTwinId}`); console.log('Waiting for responder to be ready...\n'); try { const channel = await client.getDataChannel(peerTwinId); console.log('DataChannel created successfully!\n'); // Setup message handler channel.onMessage((data) => { console.log('[RECEIVED]', data); }); // Send test messages console.log('Sending test messages...\n'); for (let i = 1; i <= 5; i++) { const msg = { type: 'test', count: i, timestamp: Date.now() }; console.log('[SENDING]', msg); channel.send(msg); await sleep(1000); } // Wait for responses console.log('\nWaiting for responses (10 seconds)...'); await sleep(10000); console.log('\nTest complete. Closing channel...'); channel.close(); } catch (error) { console.error('Failed to create DataChannel:', error); throw error; } } async function runResponder(peerTwinId: string): Promise { console.log('\n=== Running as RESPONDER ===\n'); // Connect to PhyHub console.log('Connecting to PhyHub...'); const client = await PhyHubClient.connect(); console.log('Connected!\n'); // Get WebRTC manager with verbose mode const manager = await client.getWebRTCManager({ verbose: true }); // Setup event listeners manager.on('connected', (data) => { console.log(`[EVENT] Connected to ${data.targetTwinId} (${data.connectionType})`); }); manager.on('disconnected', (data) => { console.log(`[EVENT] Disconnected from ${data.targetTwinId}`); }); manager.on('error', (data) => { console.error(`[EVENT] Error:`, data.error); }); console.log(`Waiting for DataChannel from peer: ${peerTwinId}`); console.log('Ready to accept connection...\n'); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Timeout waiting for incoming connection (60s)')); }, 60000); client .onDataChannel((channel) => { clearTimeout(timeout); console.log('DataChannel received!\n'); let messageCount = 0; channel.onMessage((data) => { console.log('[RECEIVED]', data); messageCount++; // Echo back with modification const response = { type: 'echo', original: data, respondedAt: Date.now(), }; console.log('[SENDING]', response); channel.send(response); }); channel.onClose(() => { console.log(`\nChannel closed. Received ${messageCount} messages.`); resolve(); }); }) .catch(reject); }); } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } async function main(): Promise { console.log('='.repeat(60)); console.log('WebRTC Test Harness'); console.log('='.repeat(60)); // Validate environment if (process.env.PHYHUB_DIRECT !== 'true') { console.error(` ERROR: Direct connection mode not enabled. Set PHYHUB_DIRECT=true to enable direct PhyHub connection. `); process.exit(1); } const deviceId = process.env.DEVICE_ID || process.env.PHYGRID_DEVICE_ID; const accessKey = process.env.ACCESS_KEY || process.env.PHYGRID_DEVICE_KEY; const peerTwinId = process.env.PEER_TWIN_ID; const role = process.env.ROLE?.toLowerCase(); if (!deviceId || !accessKey) { console.error(` ERROR: Missing device credentials. Required environment variables: DEVICE_ID (or PHYGRID_DEVICE_ID) - Your device ID ACCESS_KEY (or PHYGRID_DEVICE_KEY) - Your device access key To get these, run: phy dev list phy dev select phy dev develop --export `); process.exit(1); } if (!peerTwinId) { console.error(` ERROR: Missing peer twin ID. Set PEER_TWIN_ID to the twin ID of the device you want to connect to. `); process.exit(1); } if (role !== 'initiator' && role !== 'responder') { console.error(` ERROR: Invalid or missing ROLE. Set ROLE to either: - 'initiator' - The side that creates the DataChannel - 'responder' - The side that waits for the DataChannel `); process.exit(1); } console.log('\nConfiguration:'); console.log(` Device ID: ${deviceId}`); console.log(` Peer Twin ID: ${peerTwinId}`); console.log(` Role: ${role}`); console.log(` PhyHub URL: ${process.env.PHYHUB_URL || '(default EU)'}`); console.log(''); try { if (role === 'initiator') { await runInitiator(peerTwinId); } else { await runResponder(peerTwinId); } console.log('\n[SUCCESS] Test completed successfully!'); process.exit(0); } catch (error) { console.error('\n[FAILED] Test failed:', error); process.exit(1); } } // Run if executed directly if (require.main === module) { main(); } export { runInitiator, runResponder };