UNPKG

peerpigeon

Version:

WebRTC-based peer-to-peer mesh networking library with intelligent routing and signaling server

1,290 lines (1,068 loc) โ€ข 93.5 kB
/** * PeerPigeon Complete API Testing Suite * Browser-3 Example - Comprehensive feature testing interface */ /* global PeerPigeon */ class PeerPigeonTestSuite { constructor() { this.mesh = null; this.activeSubscriptions = new Set(); this.messageHistory = []; this.testResults = []; this.isMonitoring = false; this.performanceMetrics = { messagesSent: 0, messagesReceived: 0, startTime: null, endTime: null }; this.init(); } async init() { this.log('๐Ÿš€ Initializing PeerPigeon Test Suite...'); try { // Detect video test mode to disable crypto blocking const isVideoTest = window.location.search.includes('test=video') || document.title.includes('Video Test') || window.testMode === 'video'; if (isVideoTest) { this.log('๐Ÿ”“ Video test mode detected: Disabling crypto stream blocking'); window.DISABLE_CRYPTO_BLOCKING = true; } // Initialize PeerPigeon mesh with all features enabled this.mesh = new PeerPigeon.PeerPigeonMesh({ enableWebDHT: true, enableCrypto: true, enableDistributedStorage: true, // Enable distributed storage networkName: 'global', // Default network allowGlobalFallback: true, // Allow fallback to global network maxPeers: 4, minPeers: 2, autoConnect: true, autoDiscovery: true, evictionStrategy: true, xorRouting: true }); await this.mesh.init(); this.setupEventListeners(); this.setupUI(); this.updatePeerInfo(); this.initializeMediaDevices(); this.log('โœ… PeerPigeon Test Suite initialized successfully'); // Update status displays this.updateNetworkInfo(); this.updateCryptoStatus(); // Add delayed crypto status update to catch async crypto initialization setTimeout(() => { this.log('๐Ÿ”„ Delayed crypto status update...'); this.updateCryptoStatus(); }, 1000); // Another update after a longer delay to catch slow initialization setTimeout(() => { this.updateCryptoStatus(); }, 3000); } catch (error) { this.log(`โŒ Initialization failed: ${error.message}`, 'error'); } } setupEventListeners() { // Connection events this.mesh.addEventListener('statusChanged', (data) => { this.handleStatusChange(data); }); this.mesh.addEventListener('peerConnected', (data) => { this.log(`๐Ÿค Peer connected: ${data.peerId.substring(0, 8)}...`); // CRITICAL FIX: Enable remote stream reception from this newly connected peer // This ensures ALL peers can receive streams whether they're senders or not if (this.mesh.connectionManager) { const connection = this.mesh.connectionManager.peers.get(data.peerId); if (connection && connection.allowRemoteStreamEmission) { connection.allowRemoteStreamEmission(); this.log(`๐Ÿ”“ Enabled remote stream reception from ${data.peerId.substring(0, 8)}...`); } } this.updateNetworkInfo(); this.updatePeersList(); }); this.mesh.addEventListener('peerDisconnected', (data) => { this.log(`๐Ÿ‘‹ Peer disconnected: ${data.peerId.substring(0, 8)}... (${data.reason})`); this.updateNetworkInfo(); this.updatePeersList(); }); this.mesh.addEventListener('peerDiscovered', (data) => { this.log(`๐Ÿ” Peer discovered: ${data.peerId.substring(0, 8)}...`); this.updateDiscoveredPeers(); }); // Message events this.mesh.addEventListener('messageReceived', (data) => { this.handleMessageReceived(data); }); // WebDHT events (low-level, raw key-value operations) this.mesh.addEventListener('dhtValueChanged', (data) => { this.handleDHTValueChanged(data); }); // Media events this.mesh.addEventListener('localStreamStarted', (data) => { this.handleLocalStreamStarted(data); }); this.mesh.addEventListener('localStreamStopped', () => { this.handleLocalStreamStopped(); }); this.mesh.addEventListener('remoteStream', (data) => { this.handleRemoteStream(data); }); this.mesh.addEventListener('mediaError', (data) => { this.log(`๐ŸŽฅ Media error: ${data.error.message}`, 'error'); }); // Crypto events this.mesh.addEventListener('cryptoReady', () => { this.log('๐Ÿ” Encryption system ready'); this.updateCryptoStatus(); }); this.mesh.addEventListener('cryptoError', (data) => { this.log(`๐Ÿ” Crypto error: ${data.error}`, 'error'); }); this.mesh.addEventListener('peerKeyAdded', (data) => { this.log(`๐Ÿ” Key exchange completed with ${data.peerId.substring(0, 8)}...`); this.updateCryptoStatus(); // Check if all connected peers are now ready for media this.checkAndNotifyMediaReadiness(); }); // === NEW SELECTIVE STREAMING EVENTS === this.mesh.addEventListener('selectiveStreamStarted', (data) => { this.log(`๐ŸŽฏ Selective ${data.streamType} streaming started to ${data.targetPeerIds.length} peer(s)`); }); this.mesh.addEventListener('selectiveStreamStopped', (data) => { this.log(`๐Ÿ›‘ Selective streaming stopped${data.returnToBroadcast ? ' (switched to broadcast)' : ''}`); }); this.mesh.addEventListener('broadcastStreamEnabled', () => { this.log('๐Ÿ“ก Broadcast streaming enabled for all peers'); }); this.mesh.addEventListener('streamingBlockedToPeers', (data) => { this.log(`๐Ÿšซ Streaming blocked to ${data.blockedPeerIds.length} peer(s)`); }); this.mesh.addEventListener('streamingAllowedToPeers', (data) => { this.log(`โœ… Streaming allowed to ${data.allowedPeerIds.length} peer(s)`); }); // Connection monitoring this.mesh.addEventListener('connectionStats', (stats) => { this.updateConnectionStats(stats); }); } setupUI() { this.setupTabNavigation(); this.setupConnectionControls(); this.setupMessagingControls(); this.setupMediaControls(); this.setupDHTControls(); this.setupStorageControls(); this.setupCryptoControls(); this.setupNetworkControls(); this.setupTestingControls(); } setupTabNavigation() { const tabBtns = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); tabBtns.forEach(btn => { btn.addEventListener('click', () => { const targetTab = btn.dataset.tab; // Update active states tabBtns.forEach(b => b.classList.remove('active')); tabContents.forEach(c => c.classList.remove('active')); btn.classList.add('active'); document.getElementById(`${targetTab}-tab`).classList.add('active'); }); }); } setupConnectionControls() { document.getElementById('connect-btn').addEventListener('click', () => { this.connect(); }); document.getElementById('disconnect-btn').addEventListener('click', () => { this.disconnect(); }); document.getElementById('cleanup-btn').addEventListener('click', () => { this.cleanupStaleData(); }); document.getElementById('apply-config-btn').addEventListener('click', () => { this.applyConfiguration(); }); document.getElementById('connect-peer-btn').addEventListener('click', () => { this.connectToPeer(); }); document.getElementById('force-connect-all-btn').addEventListener('click', () => { this.forceConnectAll(); }); // Network name controls document.getElementById('network-name').addEventListener('change', (e) => { this.setNetworkName(e.target.value); }); document.getElementById('allow-global-fallback').addEventListener('change', (e) => { this.setAllowGlobalFallback(e.checked); }); // Quick network buttons document.querySelectorAll('.network-btn').forEach(btn => { btn.addEventListener('click', (e) => { const networkName = e.target.dataset.network; this.setNetworkName(networkName); }); }); } setupMessagingControls() { document.getElementById('send-broadcast-btn').addEventListener('click', () => { this.sendBroadcastMessage(); }); document.getElementById('send-direct-btn').addEventListener('click', () => { this.sendDirectMessage(); }); document.getElementById('clear-messages-btn').addEventListener('click', () => { this.clearMessageHistory(); }); // Enter key handlers document.getElementById('broadcast-message').addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendBroadcastMessage(); } }); document.getElementById('direct-message').addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendDirectMessage(); } }); } setupMediaControls() { document.getElementById('start-media-btn').addEventListener('click', () => { this.startMedia(); }); document.getElementById('stop-media-btn').addEventListener('click', () => { this.stopMedia(); }); document.getElementById('toggle-video-btn').addEventListener('click', () => { this.toggleVideo(); }); document.getElementById('toggle-audio-btn').addEventListener('click', () => { this.toggleAudio(); }); document.getElementById('enumerate-devices-btn').addEventListener('click', () => { this.enumerateDevices(); }); // === NEW SELECTIVE STREAMING CONTROLS === // Make peer selector multiple for selective streaming const targetPeerSelect = document.getElementById('target-peer-select'); if (targetPeerSelect) { targetPeerSelect.multiple = true; targetPeerSelect.size = 3; // Show 3 options at once } // Add selective streaming buttons this.addSelectiveStreamingButtons(); } addSelectiveStreamingButtons() { const mediaControlsDiv = document.querySelector('#media-tab .controls'); if (mediaControlsDiv) { // Create selective streaming section const selectiveStreamingSection = document.createElement('div'); selectiveStreamingSection.className = 'control-section'; selectiveStreamingSection.innerHTML = ` <h4>๐ŸŽฏ Selective Streaming</h4> <div class="control-group"> <button id="start-selective-btn" class="btn">Start Selective Stream</button> <button id="stop-selective-btn" class="btn">Stop Selective</button> <button id="switch-broadcast-btn" class="btn">Switch to Broadcast</button> </div> <div class="control-group"> <button id="block-peers-btn" class="btn secondary">Block Selected Peers</button> <button id="allow-peers-btn" class="btn secondary">Allow Selected Peers</button> <button id="show-streaming-status-btn" class="btn secondary">Show Status</button> </div> <div id="streaming-status" class="status-display"></div> <p class="info-text">๐Ÿ’ก Select multiple peers from the dropdown above to control streaming patterns</p> `; mediaControlsDiv.appendChild(selectiveStreamingSection); // Add event listeners for new buttons document.getElementById('start-selective-btn').addEventListener('click', () => { this.startSelectiveStreaming(); }); document.getElementById('stop-selective-btn').addEventListener('click', () => { this.stopSelectiveStreaming(); }); document.getElementById('switch-broadcast-btn').addEventListener('click', () => { this.switchToBroadcastMode(); }); document.getElementById('block-peers-btn').addEventListener('click', () => { this.blockStreamingToPeer(); }); document.getElementById('allow-peers-btn').addEventListener('click', () => { this.allowStreamingToPeer(); }); document.getElementById('show-streaming-status-btn').addEventListener('click', () => { this.showStreamingStatus(); }); } } setupDHTControls() { document.getElementById('dht-put-btn').addEventListener('click', () => { this.dhtPut(); }); document.getElementById('dht-update-btn').addEventListener('click', () => { this.dhtUpdate(); }); document.getElementById('dht-get-btn').addEventListener('click', () => { this.dhtGet(); }); document.getElementById('dht-delete-btn').addEventListener('click', () => { this.dhtDelete(); }); document.getElementById('dht-subscribe-btn').addEventListener('click', () => { this.dhtSubscribe(); }); document.getElementById('dht-unsubscribe-btn').addEventListener('click', () => { this.dhtUnsubscribe(); }); document.getElementById('clear-dht-log-btn').addEventListener('click', () => { this.clearDHTLog(); }); } setupStorageControls() { document.getElementById('storage-enable-btn')?.addEventListener('click', () => { this.enableDistributedStorage(); }); document.getElementById('storage-disable-btn')?.addEventListener('click', () => { this.disableDistributedStorage(); }); document.getElementById('storage-status-btn')?.addEventListener('click', () => { this.getStorageStatus(); }); document.getElementById('storage-put-btn')?.addEventListener('click', () => { this.putStorageData(); }); document.getElementById('storage-get-btn')?.addEventListener('click', () => { this.getStorageData(); }); document.getElementById('storage-delete-btn')?.addEventListener('click', () => { this.deleteStorageData(); }); document.getElementById('storage-list-btn')?.addEventListener('click', () => { this.listStorageKeys(); }); document.getElementById('storage-stats-btn')?.addEventListener('click', () => { this.getStorageStats(); }); document.getElementById('storage-clear-btn')?.addEventListener('click', () => { this.clearAllStorage(); }); document.getElementById('clear-storage-log-btn')?.addEventListener('click', () => { this.clearStorageLog(); }); } setupCryptoControls() { document.getElementById('send-encrypted-btn').addEventListener('click', () => { this.sendEncryptedMessage(); }); document.getElementById('exchange-keys-btn').addEventListener('click', () => { this.exchangeKeys(); }); document.getElementById('add-peer-key-btn').addEventListener('click', () => { this.addPeerKey(); }); document.getElementById('clear-crypto-log-btn').addEventListener('click', () => { this.clearCryptoLog(); }); document.getElementById('refresh-crypto-status-btn')?.addEventListener('click', () => { this.log('๐Ÿ”„ Manually refreshing crypto status...'); this.updateCryptoStatus(); }); document.getElementById('force-crypto-init-btn')?.addEventListener('click', () => { this.forceCryptoInit(); }); document.getElementById('exchange-keys-btn')?.addEventListener('click', () => { this.exchangeKeysWithConnectedPeers(); }); } setupNetworkControls() { document.getElementById('refresh-status-btn').addEventListener('click', () => { this.updateNetworkInfo(); }); document.getElementById('get-peer-states-btn').addEventListener('click', () => { this.getPeerStates(); }); document.getElementById('start-monitoring-btn').addEventListener('click', () => { this.startConnectionMonitoring(); }); document.getElementById('stop-monitoring-btn').addEventListener('click', () => { this.stopConnectionMonitoring(); }); document.getElementById('debug-connectivity-btn').addEventListener('click', () => { this.debugConnectivity(); }); } setupTestingControls() { document.getElementById('validate-peer-id-btn').addEventListener('click', () => { this.validatePeerId(); }); document.getElementById('force-connect-all-api-btn').addEventListener('click', () => { this.forceConnectAll(); }); document.getElementById('cleanup-stale-btn').addEventListener('click', () => { this.cleanupStaleData(); }); document.getElementById('performance-test-btn').addEventListener('click', () => { this.runPerformanceTest(); }); document.getElementById('stress-test-btn').addEventListener('click', () => { this.runStressTest(); }); document.getElementById('export-logs-btn').addEventListener('click', () => { this.exportLogs(); }); document.getElementById('clear-test-log-btn').addEventListener('click', () => { this.clearTestLog(); }); document.getElementById('clear-log-btn').addEventListener('click', () => { this.clearSystemLog(); }); // Error testing buttons document.getElementById('test-invalid-peer-btn').addEventListener('click', () => { this.testInvalidPeerConnection(); }); document.getElementById('test-malformed-message-btn').addEventListener('click', () => { this.testMalformedMessage(); }); document.getElementById('test-dht-limits-btn').addEventListener('click', () => { this.testDHTLimits(); }); } // Connection Management async connect() { const url = document.getElementById('signaling-url').value.trim(); if (!url) { this.log('โŒ Please enter a signaling server URL', 'error'); return; } // Set network name before connecting const networkName = document.getElementById('network-name').value.trim() || 'global'; const allowFallback = document.getElementById('allow-global-fallback').checked; try { // Apply network settings before connecting if (this.mesh.setNetworkName) { this.mesh.setNetworkName(networkName); this.log(`๐ŸŒ Network set to: ${networkName}`); } if (this.mesh.setAllowGlobalFallback) { this.mesh.setAllowGlobalFallback(allowFallback); this.log(`๐Ÿ”„ Global fallback ${allowFallback ? 'enabled' : 'disabled'}`); } this.log(`๐Ÿ”Œ Connecting to ${url} on network "${networkName}"...`); await this.mesh.connect(url); this.updateConnectionButtons(true); this.updateNetworkStatus(); } catch (error) { this.log(`โŒ Connection failed: ${error.message}`, 'error'); } } disconnect() { try { this.mesh.disconnect(); this.updateConnectionButtons(false); this.updateNetworkStatus(); this.log('๐Ÿ”Œ Disconnected from signaling server'); } catch (error) { this.log(`โŒ Disconnect error: ${error.message}`, 'error'); } } // Network Management setNetworkName(networkName) { if (!networkName || networkName.trim() === '') { networkName = 'global'; } const trimmedName = networkName.trim(); // Update the input field document.getElementById('network-name').value = trimmedName; // Update network buttons active state document.querySelectorAll('.network-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.network === trimmedName); }); // If disconnected, can change network name immediately if (!this.mesh.connected) { if (this.mesh.setNetworkName) { this.mesh.setNetworkName(trimmedName); this.log(`๐ŸŒ Network name set to: ${trimmedName}`); } this.updateNetworkStatus(); } else { this.log(`โš ๏ธ Disconnect first to change network from current network`, 'warning'); } } setAllowGlobalFallback(allow) { if (this.mesh.setAllowGlobalFallback) { this.mesh.setAllowGlobalFallback(allow); this.log(`๐Ÿ”„ Global fallback ${allow ? 'enabled' : 'disabled'}`); } document.getElementById('allow-global-fallback').checked = allow; } updateNetworkStatus() { const networkStatus = document.getElementById('network-status'); const currentNetworkEl = document.getElementById('current-network'); const fallbackIndicator = document.getElementById('fallback-indicator'); const originalNetworkInfo = document.getElementById('original-network-info'); const originalNetworkEl = document.getElementById('original-network'); if (this.mesh && this.mesh.getNetworkName) { const currentNetwork = this.mesh.getNetworkName(); const originalNetwork = this.mesh.getOriginalNetworkName ? this.mesh.getOriginalNetworkName() : currentNetwork; const isInFallback = this.mesh.isUsingGlobalFallback ? this.mesh.isUsingGlobalFallback() : false; currentNetworkEl.textContent = currentNetwork; fallbackIndicator.style.display = isInFallback ? 'inline' : 'none'; if (isInFallback && originalNetwork !== currentNetwork) { originalNetworkEl.textContent = originalNetwork; originalNetworkInfo.style.display = 'block'; } else { originalNetworkInfo.style.display = 'none'; } networkStatus.style.display = this.mesh.connected ? 'block' : 'none'; } else { networkStatus.style.display = 'none'; } } async cleanupStaleData() { try { await this.mesh.cleanupStaleSignalingData(); this.log('๐Ÿงน Stale signaling data cleaned up'); } catch (error) { this.log(`โŒ Cleanup error: ${error.message}`, 'error'); } } applyConfiguration() { const maxPeers = parseInt(document.getElementById('max-peers').value); const minPeers = parseInt(document.getElementById('min-peers').value); const autoConnect = document.getElementById('auto-connect').checked; const autoDiscovery = document.getElementById('auto-discovery').checked; const evictionStrategy = document.getElementById('eviction-strategy').checked; const xorRouting = document.getElementById('xor-routing').checked; this.mesh.setMaxPeers(maxPeers); this.mesh.setMinPeers(minPeers); this.mesh.setAutoConnect(autoConnect); this.mesh.setAutoDiscovery(autoDiscovery); this.mesh.setEvictionStrategy(evictionStrategy); this.mesh.setXorRouting(xorRouting); this.log('โš™๏ธ Configuration applied successfully'); } connectToPeer() { const peerId = document.getElementById('manual-peer-id').value.trim(); if (!peerId) { this.log('โŒ Please enter a peer ID', 'error'); return; } // PeerPigeon handles peer connections automatically through the mesh this.log(`๐Ÿ” Attempting to connect to peer: ${peerId.substring(0, 8)}...`); document.getElementById('manual-peer-id').value = ''; } forceConnectAll() { const attempts = this.mesh.forceConnectToAllPeers(); this.log(`๐Ÿš€ Forced ${attempts} connection attempts`); } // Messaging sendBroadcastMessage() { const message = document.getElementById('broadcast-message').value.trim(); if (!message) { this.log('โŒ Please enter a message', 'error'); return; } try { const messageId = this.mesh.sendMessage(message); if (messageId) { this.addMessageToHistory('broadcast', 'You', message); this.log(`๐Ÿ“ข Broadcast message sent (ID: ${messageId.substring(0, 8)}...)`); this.performanceMetrics.messagesSent++; } else { this.log('โŒ Failed to send broadcast message', 'error'); } } catch (error) { this.log(`โŒ Broadcast error: ${error.message}`, 'error'); } document.getElementById('broadcast-message').value = ''; } sendDirectMessage() { // Try dropdown first, then manual input const targetPeerSelect = document.getElementById('target-peer-select'); const targetPeerManual = document.getElementById('target-peer'); const targetPeer = (targetPeerSelect?.value || targetPeerManual?.value || '').trim(); const message = document.getElementById('direct-message').value.trim(); if (!targetPeer || !message) { this.log('โŒ Please select/enter a target peer ID and message', 'error'); return; } try { // Check encryption option const shouldEncrypt = document.getElementById('encrypt-direct')?.checked || false; let messageId; if (shouldEncrypt && this.mesh.cryptoManager) { messageId = this.mesh.sendEncryptedMessage(targetPeer, message); this.log(`๐Ÿ”’ Encrypted direct message sent to ${targetPeer.substring(0, 8)}...`); } else { messageId = this.mesh.sendDirectMessage(targetPeer, message); this.log(`๐Ÿ“จ Direct message sent to ${targetPeer.substring(0, 8)}...`); } if (messageId) { this.addMessageToHistory('direct', 'You', `To ${targetPeer.substring(0, 8)}...: ${message}`, shouldEncrypt); this.performanceMetrics.messagesSent++; } else { this.log('โŒ Failed to send direct message', 'error'); } } catch (error) { this.log(`โŒ Direct message error: ${error.message}`, 'error'); } document.getElementById('direct-message').value = ''; // Clear manual input if dropdown was used if (targetPeerSelect?.value && targetPeerManual) { targetPeerManual.value = ''; } } // Media Management async startMedia() { // CRITICAL: Media can only be started AFTER data channels are established and keys exchanged const connectedPeers = this.getConnectedPeers(); if (connectedPeers.length === 0) { this.log('โŒ Cannot start media: No connected peers. Connect to peers first!', 'error'); return; } // Check that ALL connected peers have data channels ready const peersWithDataChannels = connectedPeers.filter(peer => this.mesh.connectionManager && this.mesh.connectionManager.peers && this.mesh.connectionManager.peers.get(peer.id)?.dataChannelReady === true ); if (peersWithDataChannels.length < connectedPeers.length) { this.log(`โŒ Cannot start media: Not all peers have data channels ready. ${peersWithDataChannels.length}/${connectedPeers.length} peers ready`, 'error'); this.log('Wait for all peers to establish data channels before starting media', 'error'); return; } // Check that crypto keys are exchanged with ALL connected peers console.log('๐Ÿ” Debug: Checking crypto manager state:', { hasCryptoManager: !!this.mesh.cryptoManager, hasPeerKeys: !!(this.mesh.cryptoManager && this.mesh.cryptoManager.peerKeys), peerKeysType: this.mesh.cryptoManager ? typeof this.mesh.cryptoManager.peerKeys : 'no cryptoManager' }); const peersWithKeys = connectedPeers.filter(peer => { try { const hasCrypto = this.mesh.cryptoManager && this.mesh.cryptoManager.peerKeys && this.mesh.cryptoManager.peerKeys.get(peer.id); console.log(`๐Ÿ” Debug: Peer ${peer.id.substring(0, 8)} has crypto:`, hasCrypto); return hasCrypto; } catch (error) { console.error(`๐Ÿ” Debug: Error checking peer ${peer.id.substring(0, 8)} crypto:`, error); return false; } }); if (peersWithKeys.length < connectedPeers.length) { this.log(`โŒ Cannot start media: Not all peers have exchanged crypto keys. ${peersWithKeys.length}/${connectedPeers.length} peers ready`, 'error'); this.log('Wait for key exchange to complete with all peers before starting media', 'error'); return; } this.log(`โœ… All ${connectedPeers.length} peers ready for media (data channels + key exchange complete)`, 'success'); const enableVideo = document.getElementById('enable-video').checked; const enableAudio = document.getElementById('enable-audio').checked; const cameraSelect = document.getElementById('camera-select'); const micSelect = document.getElementById('microphone-select'); if (!enableVideo && !enableAudio) { this.log('โŒ Please enable at least video or audio', 'error'); return; } const options = { video: enableVideo, audio: enableAudio, deviceIds: {} }; if (enableVideo && cameraSelect.value) { options.deviceIds.camera = cameraSelect.value; } if (enableAudio && micSelect.value) { options.deviceIds.microphone = micSelect.value; } try { // Check if crypto is ready before starting media if (!this.mesh.cryptoManager || !this.mesh.cryptoManager.keypair) { this.log('โš ๏ธ Warning: Crypto not ready. Generating keypair before starting media...', 'warning'); // Try to initialize crypto if (this.mesh.cryptoManager) { await this.mesh.cryptoManager.generateKeypair(); this.log('๐Ÿ” Keypair generated for secure media transmission'); } else { this.log('โŒ Crypto manager not available. Media streams will not be encrypted!', 'error'); } } // Check for connected peers with exchanged keys const connectedPeers = this.getConnectedPeers(); if (connectedPeers.length > 0) { const peersWithKeys = connectedPeers.filter(peer => this.mesh.cryptoManager && this.mesh.cryptoManager.peerKeys && this.mesh.cryptoManager.peerKeys.get(peer.id) ); if (peersWithKeys.length < connectedPeers.length) { this.log(`โš ๏ธ Warning: Not all peers have exchanged keys. ${peersWithKeys.length}/${connectedPeers.length} peers ready for encrypted media`, 'warning'); } } await this.mesh.initializeMedia(); // CRITICAL: Enable remote stream reception from all peers BEFORE starting media // This ensures we can receive streams from peers who start media before us if (this.mesh.connectionManager) { const connections = this.mesh.connectionManager.getAllConnections(); for (const connection of connections) { if (connection.allowRemoteStreamEmission) { connection.allowRemoteStreamEmission(); this.log(`๐Ÿ”“ Enabled remote streams from ${connection.peerId.substring(0, 8)}...`); } } } await this.mesh.startMedia(options); this.log(`๐ŸŽฅ Media started (Video: ${enableVideo}, Audio: ${enableAudio})`); this.updateMediaButtons(true); this.updateCryptoStatus(); // Update crypto display } catch (error) { this.log(`โŒ Media start error: ${error.message}`, 'error'); } } // === NEW SELECTIVE STREAMING METHODS === async startSelectiveStreaming() { const targetPeerSelect = document.getElementById('target-peer-select'); const selectedPeerIds = Array.from(targetPeerSelect.selectedOptions).map(option => option.value); if (selectedPeerIds.length === 0) { this.log('โŒ Please select at least one peer for selective streaming', 'error'); return; } const enableVideo = document.getElementById('enable-video').checked; const enableAudio = document.getElementById('enable-audio').checked; const cameraSelect = document.getElementById('camera-select'); const micSelect = document.getElementById('microphone-select'); if (!enableVideo && !enableAudio) { this.log('โŒ Please enable at least video or audio', 'error'); return; } const options = { video: enableVideo, audio: enableAudio, deviceIds: {} }; if (enableVideo && cameraSelect.value) { options.deviceIds.camera = cameraSelect.value; } if (enableAudio && micSelect.value) { options.deviceIds.microphone = micSelect.value; } try { await this.mesh.initializeMedia(); await this.mesh.startSelectiveStream(selectedPeerIds, options); const streamType = selectedPeerIds.length === 1 ? '1:1' : '1:many'; this.log(`๐ŸŽฏ Selective ${streamType} streaming started to: ${selectedPeerIds.map(id => id.substring(0, 8) + '...').join(', ')}`); this.updateMediaButtons(true); } catch (error) { this.log(`โŒ Selective streaming error: ${error.message}`, 'error'); } } async stopSelectiveStreaming() { try { await this.mesh.stopSelectiveStream(false); // Stop entirely this.log('๐Ÿ›‘ Selective streaming stopped'); this.updateMediaButtons(false); } catch (error) { this.log(`โŒ Stop selective streaming error: ${error.message}`, 'error'); } } async switchToBroadcastMode() { try { await this.mesh.stopSelectiveStream(true); // Return to broadcast this.log('๐Ÿ“ก Switched to broadcast mode - streaming to all peers'); } catch (error) { this.log(`โŒ Switch to broadcast error: ${error.message}`, 'error'); } } async blockStreamingToPeer() { const targetPeerSelect = document.getElementById('target-peer-select'); const selectedPeerIds = Array.from(targetPeerSelect.selectedOptions).map(option => option.value); if (selectedPeerIds.length === 0) { this.log('โŒ Please select peers to block streaming to', 'error'); return; } try { await this.mesh.blockStreamingToPeers(selectedPeerIds); this.log(`๐Ÿšซ Blocked streaming to: ${selectedPeerIds.map(id => id.substring(0, 8) + '...').join(', ')}`); } catch (error) { this.log(`โŒ Block streaming error: ${error.message}`, 'error'); } } async allowStreamingToPeer() { const targetPeerSelect = document.getElementById('target-peer-select'); const selectedPeerIds = Array.from(targetPeerSelect.selectedOptions).map(option => option.value); if (selectedPeerIds.length === 0) { this.log('โŒ Please select peers to allow streaming to', 'error'); return; } try { await this.mesh.allowStreamingToPeers(selectedPeerIds); this.log(`โœ… Allowed streaming to: ${selectedPeerIds.map(id => id.substring(0, 8) + '...').join(', ')}`); } catch (error) { this.log(`โŒ Allow streaming error: ${error.message}`, 'error'); } } showStreamingStatus() { const streamingPeers = this.mesh.getStreamingPeers(); const blockedPeers = this.mesh.getBlockedStreamingPeers(); const isStreamingToAll = this.mesh.isStreamingToAll(); this.log('๐Ÿ“Š Current Streaming Status:'); this.log(`๐ŸŽฏ Mode: ${isStreamingToAll ? 'Broadcast (All Peers)' : 'Selective'}`); this.log(`๐Ÿ“ก Streaming to ${streamingPeers.length} peer(s): ${streamingPeers.map(id => id.substring(0, 8) + '...').join(', ')}`); if (blockedPeers.length > 0) { this.log(`๐Ÿšซ Blocked ${blockedPeers.length} peer(s): ${blockedPeers.map(id => id.substring(0, 8) + '...').join(', ')}`); } // Update UI status display const statusDiv = document.getElementById('streaming-status'); if (statusDiv) { statusDiv.innerHTML = ` <div><strong>Mode:</strong> ${isStreamingToAll ? 'Broadcast' : 'Selective'}</div> <div><strong>Streaming to:</strong> ${streamingPeers.length} peer(s)</div> <div><strong>Blocked:</strong> ${blockedPeers.length} peer(s)</div> `; } } async stopMedia() { try { // CRITICAL FIX: Only block remote streams if crypto security is required // For video streaming tests, we want streams to flow freely const isVideoStreamingTest = window.location.search.includes('test=video') || document.title.includes('Video Test') || this.testMode === 'video' || window.DISABLE_CRYPTO_BLOCKING === true || navigator.userAgent.includes('HeadlessChrome'); console.log('๐Ÿ” STOP MEDIA DEBUG:', { url: window.location.href, search: window.location.search, title: document.title, testMode: this.testMode, disableCrypto: window.DISABLE_CRYPTO_BLOCKING, userAgent: navigator.userAgent.includes('HeadlessChrome'), isVideoTest: isVideoStreamingTest }); if (!isVideoStreamingTest && this.mesh.connectionManager) { // Only block streams in secure/production mode console.log('๐Ÿ”’ BLOCKING remote streams (non-test mode)'); const connections = this.mesh.connectionManager.getAllConnections(); for (const connection of connections) { if (connection.blockRemoteStreamEmission) { connection.blockRemoteStreamEmission(); this.log(`๐Ÿ”’ Disabled remote streams from ${connection.peerId.substring(0, 8)}...`); } } } else if (isVideoStreamingTest) { console.log('๐Ÿ”“ KEEPING remote streams enabled (video test mode detected)'); this.log('๐Ÿ”“ Video test mode: Keeping remote streams enabled for testing'); } await this.mesh.stopMedia(); this.log('๐ŸŽฅ Media stopped'); this.updateMediaButtons(false); } catch (error) { this.log(`โŒ Media stop error: ${error.message}`, 'error'); } } toggleVideo() { try { const enabled = this.mesh.toggleVideo(); this.log(`๐ŸŽฅ Video ${enabled ? 'enabled' : 'disabled'}`); this.updateMediaInfo(); } catch (error) { this.log(`โŒ Video toggle error: ${error.message}`, 'error'); } } toggleAudio() { try { const enabled = this.mesh.toggleAudio(); this.log(`๐ŸŽค Audio ${enabled ? 'enabled' : 'disabled'}`); this.updateMediaInfo(); } catch (error) { this.log(`โŒ Audio toggle error: ${error.message}`, 'error'); } } async enumerateDevices() { try { const devices = await this.mesh.enumerateMediaDevices(); this.populateDeviceSelectors(devices); this.log(`๐ŸŽฅ Found ${devices.cameras.length} cameras, ${devices.microphones.length} microphones`); } catch (error) { this.log(`โŒ Device enumeration error: ${error.message}`, 'error'); } } // DHT Operations async dhtPut() { const key = document.getElementById('dht-key').value.trim(); const valueStr = document.getElementById('dht-value').value.trim(); const ttl = document.getElementById('dht-ttl').value; if (!key || !valueStr) { this.log('โŒ Please enter both key and value', 'error'); return; } try { let value; try { value = JSON.parse(valueStr); } catch { value = valueStr; // Use as string if not valid JSON } const options = {}; if (ttl) options.ttl = parseInt(ttl); await this.mesh.dhtPut(key, value, options); this.logDHT(`โœ… DHT PUT (raw): ${key} = ${JSON.stringify(value)}`); } catch (error) { this.logDHT(`โŒ PUT error: ${error.message}`, 'error'); } } async dhtUpdate() { const key = document.getElementById('dht-key').value.trim(); const valueStr = document.getElementById('dht-value').value.trim(); if (!key || !valueStr) { this.log('โŒ Please enter both key and value', 'error'); return; } try { let value; try { value = JSON.parse(valueStr); } catch { value = valueStr; } await this.mesh.dhtUpdate(key, value); this.logDHT(`๐Ÿ”„ DHT UPDATE (raw): ${key} = ${JSON.stringify(value)}`); } catch (error) { this.logDHT(`โŒ UPDATE error: ${error.message}`, 'error'); } } async dhtGet() { const key = document.getElementById('dht-get-key').value.trim(); if (!key) { this.log('โŒ Please enter a key', 'error'); return; } try { const value = await this.mesh.dhtGet(key); const resultDiv = document.getElementById('dht-result'); if (value !== null) { resultDiv.innerHTML = `<strong>Key:</strong> ${key}<br><strong>Value:</strong> <pre>${JSON.stringify(value, null, 2)}</pre>`; this.logDHT(`โœ… DHT GET (raw): ${key} = ${JSON.stringify(value)}`); } else { resultDiv.innerHTML = `<strong>Key:</strong> ${key}<br><strong>Value:</strong> <em>Not found</em>`; this.logDHT(`โŒ DHT GET (raw): ${key} not found`); } } catch (error) { this.logDHT(`โŒ GET error: ${error.message}`, 'error'); } } async dhtDelete() { const key = document.getElementById('dht-get-key').value.trim(); if (!key) { this.log('โŒ Please enter a key', 'error'); return; } try { const deleted = await this.mesh.dhtDelete(key); if (deleted) { this.logDHT(`๐Ÿ—‘๏ธ DHT DELETE (raw): ${key} removed`); } else { this.logDHT(`โŒ DHT DELETE (raw): ${key} not found`); } } catch (error) { this.logDHT(`โŒ DELETE error: ${error.message}`, 'error'); } } async dhtSubscribe() { const key = document.getElementById('dht-subscribe-key').value.trim(); if (!key) { this.log('โŒ Please enter a key', 'error'); return; } try { await this.mesh.dhtSubscribe(key); this.activeSubscriptions.add(key); this.updateSubscriptionsList(); this.logDHT(`๐Ÿ”” DHT SUBSCRIBE (raw): ${key}`); } catch (error) { this.logDHT(`โŒ SUBSCRIBE error: ${error.message}`, 'error'); } } async dhtUnsubscribe() { const key = document.getElementById('dht-subscribe-key').value.trim(); if (!key) { this.log('โŒ Please enter a key', 'error'); return; } try { await this.mesh.dhtUnsubscribe(key); this.activeSubscriptions.delete(key); this.updateSubscriptionsList(); this.logDHT(`๐Ÿ”• DHT UNSUBSCRIBE (raw): ${key}`); } catch (error) { this.logDHT(`โŒ UNSUBSCRIBE error: ${error.message}`, 'error'); } } // Crypto Operations async sendEncryptedMessage() { const message = document.getElementById('encrypted-message').value.trim(); const groupId = document.getElementById('group-id').value.trim() || null; if (!message) { this.log('โŒ Please enter a message', 'error'); return; } try { const messageId = await this.mesh.sendEncryptedBroadcast(message, groupId); if (messageId) { this.logCrypto(`๐Ÿ” Encrypted broadcast sent (ID: ${messageId.substring(0, 8)}...)`); this.addMessageToHistory('encrypted', 'You', `๐Ÿ” ${message}`); } else { this.logCrypto('โŒ Failed to send encrypted message', 'error'); } } catch (error) { this.logCrypto(`โŒ Encryption error: ${error.message}`, 'error'); } document.getElementById('encrypted-message').value = ''; } async exchangeKeys() { const peerId = document.getElementById('key-exchange-peer').value.trim(); if (!peerId) { this.log('โŒ Please enter a peer ID', 'error'); return; } try { await this.mesh.exchangeKeysWithPeer(peerId); this.logCrypto(`๐Ÿ” Key exchange initiated with ${peerId.substring(0, 8)}...`); } catch (error) { this.logCrypto(`โŒ Key exchange error: ${error.message}`, 'error'); } } async addPeerKey() { const peerId = document.getElementById('manual-peer-id').value.trim(); const publicKey = document.getElementById('manual-public-key').value.trim(); if (!peerId || !publicKey) { this.log('โŒ Please enter both peer ID and public key', 'error'); return; } try { await this.mesh.addPeerKey(peerId, publicKey); this.logCrypto(`๐Ÿ” Public key added for ${peerId.substring(0, 8)}...`); this.updateCryptoStatus(); } catch (error) { this.logCrypto(`โŒ Add key error: ${error.message}`, 'error'); } } async forceCryptoInit() { this.log('๐Ÿ”ง Force initializing crypto system...'); try { if (!this.mesh.cryptoManager) { this.log('โŒ Crypto manager not available', 'error'); return; } // Force keypair generation if not already done if (this.mesh.cryptoManager && !this.mesh.cryptoManager.keypair) { this.log('๐Ÿ”‘ Generating keypair...'); await this.mesh.cryptoManager.generateKeypair(); this.log('โœ… Keypair generated'); } else if (this.mesh.cryptoManager) { this.log('โœ… Keypair already exists'); } // Update status this.updateCryptoStatus(); // Log crypto system state const publicKey = this.mesh.cryptoManager?.getPublicKey() || this.mesh.exportPublicKey?.(); this.logCrypto(`๐Ÿ” Crypto system initialized. Public key: ${publicKey ? publicKey.su