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
JavaScript
/**
* 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