peerpigeon
Version:
WebRTC-based peer-to-peer mesh networking library with intelligent routing and signaling server
1,369 lines (1,158 loc) • 70.3 kB
JavaScript
// Updated: 2025-07-04 - Fixed getConnectionStatus method name
export class PeerPigeonUI {
constructor(mesh) {
this.mesh = mesh;
this.lastCleanupTime = 0; // Track when we last did cleanup
this.setupEventListeners();
this.bindDOMEvents();
this.initializeMedia();
}
setupEventListeners() {
this.mesh.addEventListener('statusChanged', (data) => {
this.handleStatusChange(data);
});
this.mesh.addEventListener('peerDiscovered', (_data) => {
this.updateDiscoveredPeers();
});
this.mesh.addEventListener('peerConnected', (_data) => {
this.updateUI();
this.updateDiscoveredPeers();
});
this.mesh.addEventListener('peerDisconnected', (data) => {
this.addMessage('System', `Peer ${data.peerId.substring(0, 8)}... disconnected (${data.reason})`);
this.updateUI();
this.updateDiscoveredPeers();
});
this.mesh.addEventListener('messageReceived', (data) => {
// Only show messages from other peers, not self
if (data.from !== this.mesh.peerId) {
if (data.direct) {
this.addMessage(`${data.from.substring(0, 8)}...`, `(DM) ${data.content}`, 'dm');
} else {
this.addMessage(`${data.from.substring(0, 8)}...`, data.content);
}
}
});
this.mesh.addEventListener('peerEvicted', (_data) => {
this.updateUI();
this.updateDiscoveredPeers();
});
this.mesh.addEventListener('peersUpdated', () => {
this.updateDiscoveredPeers();
});
this.mesh.addEventListener('connectionStats', (_stats) => {
// Handle connection stats if needed
});
// DHT event listeners
this.mesh.addEventListener('dhtValueChanged', (data) => {
const { key, newValue, timestamp } = data;
this.addDHTLogEntry(`🔔 Value Changed: ${key} = ${JSON.stringify(newValue)} (timestamp: ${timestamp})`);
});
// Media event listeners
this.mesh.addEventListener('localStreamStarted', (data) => {
this.handleLocalStreamStarted(data);
});
this.mesh.addEventListener('localStreamStopped', () => {
this.handleLocalStreamStopped();
});
this.mesh.addEventListener('mediaError', (data) => {
this.addMessage('System', `Media error: ${data.error.message}`, 'error');
});
// Listen for remote streams
this.mesh.addEventListener('remoteStream', (data) => {
this.handleRemoteStream(data);
});
}
handleStatusChange(data) {
switch (data.type) {
case 'initialized':
this.updateUI();
break;
case 'connecting':
this.addMessage('System', 'Connecting to signaling server...');
this.updateUI();
break;
case 'connected':
this.addMessage('System', 'Connected to signaling server');
this.updateUI();
break;
case 'disconnected':
this.addMessage('System', 'Disconnected from signaling server');
this.updateUI();
this.updateDiscoveredPeers();
break;
case 'error':
this.addMessage('System', data.message, 'error');
this.updateUI();
break;
case 'warning':
this.addMessage('System', data.message, 'warning');
break;
case 'info':
this.addMessage('System', data.message);
break;
case 'urlLoaded':
document.getElementById('signaling-url').value = data.signalingUrl;
this.addMessage('System', `API Gateway URL loaded: ${data.signalingUrl}`);
break;
case 'setting': {
const settingName = data.setting === 'autoDiscovery'
? 'Auto discovery'
: data.setting === 'evictionStrategy'
? 'Smart eviction strategy'
: data.setting === 'xorRouting'
? 'XOR-based routing'
: data.setting === 'connectionType' ? 'Connection type' : data.setting;
if (data.setting === 'connectionType') {
this.addMessage('System', `${settingName} set to: ${data.value}`);
} else {
this.addMessage('System', `${settingName} ${data.value ? 'enabled' : 'disabled'}`);
}
break;
}
}
}
bindDOMEvents() {
// Connection controls
document.getElementById('connect-btn').addEventListener('click', () => {
this.handleConnect();
});
document.getElementById('disconnect-btn').addEventListener('click', () => {
this.handleDisconnect();
});
document.getElementById('cleanup-btn').addEventListener('click', () => {
this.handleCleanup();
});
document.getElementById('health-check-btn').addEventListener('click', () => {
this.handleHealthCheck();
});
document.getElementById('refresh-btn').addEventListener('click', () => {
window.location.reload();
});
// Messaging
document.getElementById('send-message-btn').addEventListener('click', () => {
this.sendMessage();
});
document.getElementById('message-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
// Manual connection
document.getElementById('connect-peer-btn').addEventListener('click', () => {
this.connectToPeer();
});
document.getElementById('target-peer').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.connectToPeer();
}
});
// Settings
document.getElementById('min-peers').addEventListener('change', (e) => {
this.mesh.setMinPeers(parseInt(e.target.value));
});
document.getElementById('max-peers').addEventListener('change', (e) => {
this.mesh.setMaxPeers(parseInt(e.target.value));
});
document.getElementById('auto-discovery-toggle').addEventListener('change', (e) => {
this.mesh.setAutoDiscovery(e.target.checked);
});
document.getElementById('eviction-strategy-toggle').addEventListener('change', (e) => {
this.mesh.setEvictionStrategy(e.target.checked);
});
document.getElementById('xor-routing-toggle').addEventListener('change', (e) => {
this.mesh.setXorRouting(e.target.checked);
});
// DHT controls
this.setupDHTControls();
// Collapsible sections
this.setupCollapsibleSections();
// Media controls
this.setupMediaControls();
}
setupDHTControls() {
// DHT input fields and controls
const dhtKey = document.getElementById('dht-key');
const dhtValue = document.getElementById('dht-value');
const dhtGetKey = document.getElementById('dht-get-key'); // Separate key field for retrieval
const dhtPutBtn = document.getElementById('dht-put-btn');
const dhtUpdateBtn = document.getElementById('dht-update-btn');
const dhtGetBtn = document.getElementById('dht-get-btn');
const dhtSubscribeBtn = document.getElementById('dht-subscribe-btn');
const dhtUnsubscribeBtn = document.getElementById('dht-unsubscribe-btn');
const dhtEnableTtl = document.getElementById('dht-enable-ttl');
const dhtTtlGroup = document.getElementById('dht-ttl-group');
const dhtTtl = document.getElementById('dht-ttl');
// Debug: Check if elements are found
console.log('DHT Controls setup - Elements found:', {
dhtKey: !!dhtKey,
dhtValue: !!dhtValue,
dhtGetKey: !!dhtGetKey,
dhtPutBtn: !!dhtPutBtn,
dhtUpdateBtn: !!dhtUpdateBtn,
dhtGetBtn: !!dhtGetBtn,
dhtSubscribeBtn: !!dhtSubscribeBtn,
dhtUnsubscribeBtn: !!dhtUnsubscribeBtn
});
if (!dhtKey) {
console.error('DHT key input element not found!');
this.addDHTLogEntry('❌ Error: DHT key input field not found in DOM');
}
if (!dhtValue) {
console.error('DHT value input element not found!');
this.addDHTLogEntry('❌ Error: DHT value input field not found in DOM');
}
if (!dhtGetKey) {
console.error('DHT get-key input element not found!');
this.addDHTLogEntry('❌ Error: DHT get-key input field not found in DOM');
}
// Toggle TTL input visibility
if (dhtEnableTtl && dhtTtlGroup) {
dhtEnableTtl.addEventListener('change', (e) => {
dhtTtlGroup.style.display = e.target.checked ? 'block' : 'none';
});
}
// Store in DHT
if (dhtPutBtn) {
dhtPutBtn.addEventListener('click', async () => {
const key = dhtKey?.value?.trim();
const value = dhtValue?.value?.trim();
// Debug logging
console.log('DHT PUT - Elements found:', {
dhtKey: !!dhtKey,
dhtValue: !!dhtValue,
keyValue: key,
valueValue: value
});
if (!key || !value) {
this.addDHTLogEntry('❌ Error: Both key and value are required');
this.addDHTLogEntry(` Debug: key="${key}", value="${value}"`);
return;
}
if (!this.mesh.isDHTEnabled()) {
this.addDHTLogEntry('❌ Error: WebDHT is disabled');
return;
}
try {
let parsedValue;
try {
parsedValue = JSON.parse(value);
} catch {
parsedValue = value; // Use as string if not valid JSON
}
const options = {};
if (dhtEnableTtl?.checked && dhtTtl?.value) {
options.ttl = parseInt(dhtTtl.value) * 1000; // Convert to milliseconds
}
const success = await this.mesh.dhtPut(key, parsedValue, options);
if (success) {
this.addDHTLogEntry(`✅ Stored: ${key} = ${JSON.stringify(parsedValue)}`);
if (options.ttl) {
this.addDHTLogEntry(` TTL: ${options.ttl / 1000} seconds`);
}
this.addDHTLogEntry(` 🏆 This peer is now an ORIGINAL STORING PEER for "${key}"`);
// Auto-subscribe to the key we just stored (essential for original storing peers)
try {
await this.mesh.dhtSubscribe(key);
this.addDHTLogEntry(`🔔 Auto-subscribed as original storing peer: ${key}`);
this.addDHTLogEntry(' 📡 Will receive and relay all future updates for this key');
} catch (subscribeError) {
this.addDHTLogEntry(`⚠️ Failed to auto-subscribe to ${key}: ${subscribeError.message}`);
}
} else {
this.addDHTLogEntry(`❌ Failed to store: ${key}`);
}
} catch (error) {
this.addDHTLogEntry(`❌ Error storing ${key}: ${error.message}`);
}
});
}
// Update in DHT
if (dhtUpdateBtn) {
dhtUpdateBtn.addEventListener('click', async () => {
const key = dhtKey?.value?.trim();
const value = dhtValue?.value?.trim();
// Debug logging
console.log('DHT UPDATE - Elements found:', {
dhtKey: !!dhtKey,
dhtValue: !!dhtValue,
keyValue: key,
valueValue: value
});
if (!key || !value) {
this.addDHTLogEntry('❌ Error: Both key and value are required');
this.addDHTLogEntry(` Debug: key="${key}", value="${value}"`);
return;
}
if (!this.mesh.isDHTEnabled()) {
this.addDHTLogEntry('❌ Error: WebDHT is disabled');
return;
}
try {
let parsedValue;
try {
parsedValue = JSON.parse(value);
} catch {
parsedValue = value; // Use as string if not valid JSON
}
const options = {};
if (dhtEnableTtl?.checked && dhtTtl?.value) {
options.ttl = parseInt(dhtTtl.value) * 1000; // Convert to milliseconds
}
const success = await this.mesh.dhtUpdate(key, parsedValue, options);
if (success) {
this.addDHTLogEntry(`🔄 Updated: ${key} = ${JSON.stringify(parsedValue)}`);
if (options.ttl) {
this.addDHTLogEntry(` TTL: ${options.ttl / 1000} seconds`);
}
this.addDHTLogEntry(` 🏆 This peer is now an ORIGINAL STORING PEER for "${key}"`);
// Auto-subscribe to the key we just updated (essential for original storing peers)
try {
await this.mesh.dhtSubscribe(key);
this.addDHTLogEntry(`🔔 Auto-subscribed as original storing peer: ${key}`);
this.addDHTLogEntry(' 📡 Will receive and relay all future updates for this key');
} catch (subscribeError) {
this.addDHTLogEntry(`⚠️ Failed to auto-subscribe to ${key}: ${subscribeError.message}`);
}
} else {
this.addDHTLogEntry(`❌ Failed to update: ${key}`);
}
} catch (error) {
this.addDHTLogEntry(`❌ Error updating ${key}: ${error.message}`);
}
});
}
// Get from DHT
if (dhtGetBtn) {
dhtGetBtn.addEventListener('click', async () => {
const key = dhtGetKey?.value?.trim(); // Fixed: Use dhtGetKey instead of dhtKey
// Debug logging
console.log('DHT GET - Elements found:', {
dhtGetKey: !!dhtGetKey,
keyValue: key
});
if (!key) {
this.addDHTLogEntry('❌ Error: Key is required');
this.addDHTLogEntry(` Debug: key="${key}"`);
return;
}
if (!this.mesh.isDHTEnabled()) {
this.addDHTLogEntry('❌ Error: WebDHT is disabled');
return;
}
try {
this.addDHTLogEntry(`🔍 Fetching from ORIGINAL STORING PEERS: ${key}`);
this.addDHTLogEntry(' 📡 Bypassing local cache to ensure fresh data');
// Force fresh retrieval from original storing peers
const value = await this.mesh.dhtGet(key, { forceRefresh: true });
if (value !== null) {
this.addDHTLogEntry(`📥 Retrieved from original peers: ${key} = ${JSON.stringify(value)}`);
this.addDHTLogEntry(' ✅ Data is FRESH from authoritative source');
if (dhtValue) {
dhtValue.value = typeof value === 'string' ? value : JSON.stringify(value);
}
// Auto-subscribe to the key we just retrieved for future updates
try {
await this.mesh.dhtSubscribe(key);
this.addDHTLogEntry(`🔔 Auto-subscribed for future updates: ${key}`);
this.addDHTLogEntry(' 📡 Will receive notifications when original peers update this key');
} catch (subscribeError) {
this.addDHTLogEntry(`⚠️ Failed to auto-subscribe to ${key}: ${subscribeError.message}`);
}
} else {
this.addDHTLogEntry(`❌ Not found on original storing peers: ${key}`);
this.addDHTLogEntry(' 💡 Key may not exist or all original storing peers are offline');
}
} catch (error) {
this.addDHTLogEntry(`❌ Error retrieving ${key}: ${error.message}`);
}
});
}
// Subscribe to DHT key
if (dhtSubscribeBtn) {
dhtSubscribeBtn.addEventListener('click', async () => {
// Check both key fields - use whichever has a value
const putKey = dhtKey?.value?.trim();
const getKey = dhtGetKey?.value?.trim();
const key = putKey || getKey;
if (!key) {
this.addDHTLogEntry('❌ Error: Key is required (enter key in either Put/Update or Get field)');
return;
}
if (!this.mesh.isDHTEnabled()) {
this.addDHTLogEntry('❌ Error: WebDHT is disabled');
return;
}
try {
this.addDHTLogEntry(`🔔 Explicitly subscribing to key: ${key}`);
this.addDHTLogEntry(' 📡 Will receive notifications when original storing peers update this key');
const value = await this.mesh.dhtSubscribe(key);
this.addDHTLogEntry(`✅ Subscribed to: ${key}`);
if (value !== null) {
this.addDHTLogEntry(` Current value from original storing peers: ${JSON.stringify(value)}`);
if (dhtValue) {
dhtValue.value = typeof value === 'string' ? value : JSON.stringify(value);
}
} else {
this.addDHTLogEntry(' No current value found on original storing peers');
}
} catch (error) {
this.addDHTLogEntry(`❌ Error subscribing to ${key}: ${error.message}`);
}
});
}
// Unsubscribe from DHT key
if (dhtUnsubscribeBtn) {
dhtUnsubscribeBtn.addEventListener('click', async () => {
// Check both key fields - use whichever has a value
const putKey = dhtKey?.value?.trim();
const getKey = dhtGetKey?.value?.trim();
const key = putKey || getKey;
if (!key) {
this.addDHTLogEntry('❌ Error: Key is required (enter key in either Put/Update or Get field)');
return;
}
if (!this.mesh.isDHTEnabled()) {
this.addDHTLogEntry('❌ Error: WebDHT is disabled');
return;
}
try {
await this.mesh.dhtUnsubscribe(key);
this.addDHTLogEntry(`🔕 Unsubscribed from: ${key}`);
} catch (error) {
this.addDHTLogEntry(`❌ Error unsubscribing from ${key}: ${error.message}`);
}
});
}
}
setupMediaControls() {
// Start media button
document.getElementById('start-media-btn').addEventListener('click', async () => {
await this.startMedia();
});
// Stop media button
document.getElementById('stop-media-btn').addEventListener('click', async () => {
await this.stopMedia();
});
// Toggle video button
document.getElementById('toggle-video-btn').addEventListener('click', () => {
this.toggleVideo();
});
// Toggle audio button
document.getElementById('toggle-audio-btn').addEventListener('click', () => {
this.toggleAudio();
});
// Audio test button
document.getElementById('test-audio-btn').addEventListener('click', () => {
this.testAudio();
});
// Media type checkboxes
document.getElementById('enable-video').addEventListener('change', () => {
this.updateMediaButtonStates();
});
document.getElementById('enable-audio').addEventListener('change', () => {
this.updateMediaButtonStates();
});
// Device selection dropdowns
document.getElementById('camera-select').addEventListener('change', () => {
// Auto-restart media if it's currently active
if (this.mesh.getMediaState().hasLocalStream) {
this.startMedia();
}
});
document.getElementById('microphone-select').addEventListener('change', () => {
// Auto-restart media if it's currently active
if (this.mesh.getMediaState().hasLocalStream) {
this.startMedia();
}
});
}
setupCollapsibleSections() {
const sections = [
'discovered-peers',
'connected-peers',
'manual-connection',
'settings',
'dht',
'media'
];
sections.forEach(sectionId => {
const toggle = document.getElementById(`${sectionId}-toggle`);
const content = document.getElementById(`${sectionId}-content`);
if (toggle && content) {
toggle.addEventListener('click', () => {
this.toggleSection(sectionId);
});
}
});
}
toggleSection(sectionId) {
const toggle = document.getElementById(`${sectionId}-toggle`);
const content = document.getElementById(`${sectionId}-content`);
if (!toggle || !content) {
console.warn(`Could not find elements for section: ${sectionId}`);
return;
}
const section = toggle.closest(`.${sectionId}`);
if (!section) {
console.warn(`Could not find section container for: ${sectionId}`, {
toggle,
toggleParent: toggle.parentElement,
toggleParentParent: toggle.parentElement?.parentElement,
classList: toggle.parentElement?.parentElement?.classList
});
return;
}
const isExpanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', !isExpanded);
// Add null check before calling setAttribute
if (section) {
section.setAttribute('aria-expanded', !isExpanded);
}
if (isExpanded) {
content.style.display = 'none';
toggle.textContent = toggle.textContent.replace('▼', '▶');
} else {
content.style.display = 'block';
toggle.textContent = toggle.textContent.replace('▶', '▼');
}
}
// Media-related methods
async initializeMedia() {
try {
// Initialize media manager
await this.mesh.initializeMedia();
// Populate device lists
await this.updateDeviceLists();
// Update button states
this.updateMediaButtonStates();
console.log('Media initialized successfully');
} catch (error) {
console.error('Failed to initialize media:', error);
this.addMessage('System', `Failed to initialize media: ${error.message}`, 'error');
}
}
async updateDeviceLists() {
try {
const devices = await this.mesh.enumerateMediaDevices();
// Update camera list
const cameraSelect = document.getElementById('camera-select');
if (cameraSelect) {
cameraSelect.innerHTML = '<option value="">Select camera...</option>';
devices.cameras.forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || `Camera ${device.deviceId.substring(0, 8)}...`;
cameraSelect.appendChild(option);
});
cameraSelect.disabled = devices.cameras.length === 0;
}
// Update microphone list
const micSelect = document.getElementById('microphone-select');
if (micSelect) {
micSelect.innerHTML = '<option value="">Select microphone...</option>';
devices.microphones.forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || `Microphone ${device.deviceId.substring(0, 8)}...`;
micSelect.appendChild(option);
});
micSelect.disabled = devices.microphones.length === 0;
}
} catch (error) {
console.error('Failed to update device lists:', error);
}
}
updateMediaButtonStates() {
const videoEnabled = document.getElementById('enable-video')?.checked || false;
const audioEnabled = document.getElementById('enable-audio')?.checked || false;
const mediaState = this.mesh.getMediaState();
// Enable start button if at least one media type is selected
const startBtn = document.getElementById('start-media-btn');
if (startBtn) {
startBtn.disabled = !(videoEnabled || audioEnabled) || mediaState.hasLocalStream;
}
// Enable stop button if media is active
const stopBtn = document.getElementById('stop-media-btn');
if (stopBtn) {
stopBtn.disabled = !mediaState.hasLocalStream;
}
// Enable toggle buttons if media is active
const toggleVideoBtn = document.getElementById('toggle-video-btn');
const toggleAudioBtn = document.getElementById('toggle-audio-btn');
if (toggleVideoBtn) {
toggleVideoBtn.disabled = !mediaState.hasLocalStream;
toggleVideoBtn.textContent = mediaState.videoEnabled ? 'Turn Off Video' : 'Turn On Video';
}
if (toggleAudioBtn) {
toggleAudioBtn.disabled = !mediaState.hasLocalStream;
toggleAudioBtn.textContent = mediaState.audioEnabled ? 'Turn Off Audio' : 'Turn On Audio';
}
}
async startMedia() {
try {
const videoEnabled = document.getElementById('enable-video')?.checked || false;
const audioEnabled = document.getElementById('enable-audio')?.checked || false;
// Initialize audio context for better audio support
if (audioEnabled && (window.AudioContext || window.webkitAudioContext)) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
try {
const audioContext = new AudioContext();
if (audioContext.state === 'suspended') {
await audioContext.resume();
console.log('Audio context resumed for local media');
}
console.log('Audio context state:', audioContext.state);
} catch (e) {
console.log('Could not initialize audio context:', e);
}
}
const deviceIds = {};
const cameraSelect = document.getElementById('camera-select');
const micSelect = document.getElementById('microphone-select');
if (cameraSelect?.value) {
deviceIds.camera = cameraSelect.value;
}
if (micSelect?.value) {
deviceIds.microphone = micSelect.value;
}
await this.mesh.startMedia({
video: videoEnabled,
audio: audioEnabled,
deviceIds
});
this.addMessage('System', 'Media started successfully');
this.updateMediaButtonStates();
} catch (error) {
console.error('Failed to start media:', error);
this.addMessage('System', `Failed to start media: ${error.message}`, 'error');
}
}
async stopMedia() {
try {
await this.mesh.stopMedia();
this.addMessage('System', 'Media stopped');
this.updateMediaButtonStates();
this.clearLocalVideo();
} catch (error) {
console.error('Failed to stop media:', error);
this.addMessage('System', `Failed to stop media: ${error.message}`, 'error');
}
}
toggleVideo() {
try {
const enabled = this.mesh.toggleVideo();
this.addMessage('System', `Video ${enabled ? 'enabled' : 'disabled'}`);
this.updateMediaButtonStates();
} catch (error) {
console.error('Failed to toggle video:', error);
this.addMessage('System', `Failed to toggle video: ${error.message}`, 'error');
}
}
toggleAudio() {
try {
const enabled = this.mesh.toggleAudio();
this.addMessage('System', `Audio ${enabled ? 'enabled' : 'disabled'}`);
this.updateMediaButtonStates();
} catch (error) {
console.error('Failed to toggle audio:', error);
this.addMessage('System', `Failed to toggle audio: ${error.message}`, 'error');
}
}
async testAudio() {
try {
console.log('🔊 Starting audio test...');
this.addMessage('System', '🔊 Testing audio system...', 'info');
// First, run diagnostics to understand current state
this.logPeerDiagnostics();
// Test 0: Verify we have actual peer connections (not just self)
const allPeers = this.mesh.getPeers();
const connectedPeers = allPeers.filter(peer => peer.status === 'connected');
const peersWithMedia = allPeers.filter(peer => {
const peerConnection = this.mesh.connectionManager.getPeer(peer.peerId);
return peerConnection && (peerConnection.remoteStream || peerConnection.connection?.connectionState === 'connected');
});
console.log('🔊 All peers:', allPeers.length);
console.log('🔊 Connected peers (data channel):', connectedPeers.length);
console.log('🔊 Peers with media/WebRTC:', peersWithMedia.length);
// Log detailed peer status
allPeers.forEach(peer => {
const peerConnection = this.mesh.connectionManager.getPeer(peer.peerId);
console.log(`🔊 Peer ${peer.peerId.substring(0, 8)}:`, {
status: peer.status,
webrtcState: peerConnection?.connection?.connectionState,
dataChannelState: peerConnection?.dataChannel?.readyState,
hasRemoteStream: !!peerConnection?.remoteStream,
hasLocalStream: !!peerConnection?.localStream
});
});
this.addMessage('System', `🔊 Found ${allPeers.length} total peers (${connectedPeers.length} with data channel, ${peersWithMedia.length} with media)`, 'info');
if (peersWithMedia.length === 0) {
this.addMessage('System', '❌ No peers with active media! You need to open this in TWO different browser tabs/windows and connect them.', 'error');
console.log('❌ NO PEERS WITH MEDIA: Open this URL in two different browser tabs and ensure they connect to each other');
return;
}
// Test each peer connection individually
peersWithMedia.forEach((peer, index) => {
const connection = this.mesh.connectionManager.peers.get(peer.peerId);
if (connection) {
console.log(`🔊 Peer ${index + 1} (${peer.peerId.substring(0, 8)}...):`);
const localStream = connection.getLocalStream();
const remoteStream = connection.getRemoteStream();
// Check local stream
if (localStream) {
const localAudioTracks = localStream.getAudioTracks();
console.log(` - Local audio tracks: ${localAudioTracks.length}`);
this.addMessage('System', ` Peer ${index + 1} - Local audio: ${localAudioTracks.length} tracks`, 'info');
localAudioTracks.forEach((track, i) => {
console.log(` Track ${i}: enabled=${track.enabled}, id=${track.id.substring(0, 8)}...`);
});
} else {
console.log(' - No local stream');
this.addMessage('System', ` Peer ${index + 1} - No local stream`, 'warning');
}
// Check remote stream
if (remoteStream) {
const remoteAudioTracks = remoteStream.getAudioTracks();
console.log(` - Remote audio tracks: ${remoteAudioTracks.length}`);
this.addMessage('System', ` Peer ${index + 1} - Remote audio: ${remoteAudioTracks.length} tracks`, remoteAudioTracks.length > 0 ? 'success' : 'warning');
remoteAudioTracks.forEach((track, i) => {
console.log(` Track ${i}: enabled=${track.enabled}, id=${track.id.substring(0, 8)}..., readyState=${track.readyState}`);
});
// CRITICAL: Check if remote stream ID matches local stream ID (loopback detection)
if (localStream && remoteStream.id === localStream.id) {
console.error('❌ LOOPBACK DETECTED: Remote stream ID matches local stream ID!');
this.addMessage('System', `❌ Peer ${index + 1} - LOOPBACK: Getting own audio back!`, 'error');
} else {
console.log(`✅ Stream IDs different: local=${localStream?.id.substring(0, 8) || 'none'}, remote=${remoteStream.id.substring(0, 8)}`);
this.addMessage('System', `✅ Peer ${index + 1} - Stream IDs are different (good!)`, 'success');
}
// Check video elements playing this stream
const videoElements = document.querySelectorAll('video');
let foundVideoElement = false;
videoElements.forEach(video => {
if (video.srcObject === remoteStream) {
foundVideoElement = true;
console.log(` - Video element: volume=${video.volume}, muted=${video.muted}, paused=${video.paused}, readyState=${video.readyState}`);
this.addMessage('System', ` Peer ${index + 1} - Video element: volume=${video.volume}, muted=${video.muted}`, video.muted ? 'warning' : 'success');
}
});
if (!foundVideoElement) {
console.log(' - ❌ No video element found for this remote stream');
this.addMessage('System', ` Peer ${index + 1} - ❌ No video element displaying this stream`, 'error');
}
} else {
console.log(' - ❌ No remote stream');
this.addMessage('System', ` Peer ${index + 1} - ❌ No remote stream received`, 'error');
}
} else {
console.log(`❌ No connection object found for peer ${peer.peerId}`);
this.addMessage('System', `❌ No connection for peer ${index + 1}`, 'error');
}
});
// Test 1: Audio Context
if (window.AudioContext || window.webkitAudioContext) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
console.log('Audio context state:', audioContext.state);
if (audioContext.state === 'suspended') {
await audioContext.resume();
console.log('Audio context resumed');
}
// Generate a test tone
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.setValueAtTime(440, audioContext.currentTime); // A4 note
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); // Low volume
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.5); // Play for 0.5 seconds
this.addMessage('System', '🔊 Audio test tone played (440Hz for 0.5s)', 'success');
} else {
this.addMessage('System', '❌ Audio context not supported', 'error');
}
// Test 2: Check current media streams
const peers = this.mesh.getPeers();
console.log('Current peers:', peers.length);
peers.forEach(peer => {
const connection = this.mesh.connectionManager.peers.get(peer.peerId);
if (connection) {
const localStream = connection.getLocalStream();
const remoteStream = connection.getRemoteStream();
console.log(`Peer ${peer.peerId.substring(0, 8)}:`);
if (localStream) {
const audioTracks = localStream.getAudioTracks();
console.log(`- Local audio tracks: ${audioTracks.length}`);
audioTracks.forEach((track, i) => {
console.log(` Track ${i}: enabled=${track.enabled}, readyState=${track.readyState}, muted=${track.muted}`);
});
}
if (remoteStream) {
const audioTracks = remoteStream.getAudioTracks();
console.log(`- Remote audio tracks: ${audioTracks.length}`);
audioTracks.forEach((track, i) => {
console.log(` Track ${i}: enabled=${track.enabled}, readyState=${track.readyState}, muted=${track.muted}`);
});
// Check if there are video elements for this stream
const videoElements = document.querySelectorAll('video');
videoElements.forEach(video => {
if (video.srcObject === remoteStream) {
console.log(`- Video element: volume=${video.volume}, muted=${video.muted}, paused=${video.paused}`);
}
});
}
}
});
// Test 3: Check microphone access
try {
const testStream = await navigator.mediaDevices.getUserMedia({ audio: true });
console.log('✅ Microphone access working');
this.addMessage('System', '✅ Microphone access: OK', 'success');
testStream.getTracks().forEach(track => track.stop());
} catch (error) {
console.log('❌ Microphone access failed:', error);
this.addMessage('System', `❌ Microphone access failed: ${error.message}`, 'error');
}
} catch (error) {
console.error('Audio test failed:', error);
this.addMessage('System', `❌ Audio test failed: ${error.message}`, 'error');
}
}
handleLocalStreamStarted(data) {
this.updateMediaButtonStates();
// Display local video if video is enabled
if (data.video) {
this.displayLocalVideo(data.stream);
}
const mediaTypes = [];
if (data.video) mediaTypes.push('video');
if (data.audio) mediaTypes.push('audio');
this.addMessage('System', `Local ${mediaTypes.join(' and ')} started`);
}
handleLocalStreamStopped() {
this.updateMediaButtonStates();
this.clearLocalVideo();
this.addMessage('System', 'Local media stopped');
}
displayLocalVideo(stream) {
const localVideoContainer = document.getElementById('local-video-container');
if (!localVideoContainer) return;
let video = localVideoContainer.querySelector('video');
if (!video) {
video = document.createElement('video');
video.autoplay = true;
video.muted = true;
video.playsInline = true;
video.style.width = '100%';
video.style.borderRadius = '8px';
localVideoContainer.appendChild(video);
}
video.srcObject = stream;
}
clearLocalVideo() {
const localVideoContainer = document.getElementById('local-video-container');
if (localVideoContainer) {
const video = localVideoContainer.querySelector('video');
if (video) {
video.srcObject = null;
video.remove();
}
}
}
handleRemoteStream(data) {
console.log('🎵 Handling remote stream:', data);
const { peerId, stream } = data;
// CRITICAL: Prevent audio loopback - check if this is our own stream
if (peerId === this.mesh.peerId) {
console.warn('🚨 LOOPBACK DETECTED: Received our own stream as remote! This should not happen.');
console.warn('Stream ID:', stream.id);
// Still display it but it will be muted by displayRemoteVideo
}
// Detailed audio analysis
const audioTracks = stream.getAudioTracks();
const videoTracks = stream.getVideoTracks();
console.log(`Stream from ${peerId.substring(0, 8)}: ${audioTracks.length} audio, ${videoTracks.length} video tracks`);
audioTracks.forEach((track, i) => {
console.log(`🎵 Audio track ${i}:`, {
id: track.id,
kind: track.kind,
enabled: track.enabled,
readyState: track.readyState,
muted: track.muted,
label: track.label
});
// Add track event listeners for state monitoring
track.addEventListener('ended', () => {
console.log(`🎵 Audio track ${i} from peer ${peerId.substring(0, 8)} ENDED`);
});
track.addEventListener('mute', () => {
console.log(`🎵 Audio track ${i} from peer ${peerId.substring(0, 8)} MUTED`);
});
track.addEventListener('unmute', () => {
console.log(`🎵 Audio track ${i} from peer ${peerId.substring(0, 8)} UNMUTED`);
});
});
this.displayRemoteVideo(peerId, stream);
// Audio data summary
const audioSummary = {
peerIdShort: peerId.substring(0, 8),
audioTrackCount: audioTracks.length,
videoTrackCount: videoTracks.length,
audioTracksEnabled: audioTracks.filter(t => t.enabled).length,
streamActive: stream.active,
streamId: stream.id
};
console.log(`🎵 AUDIO DATA EXPECTATION for peer ${audioSummary.peerIdShort}:`, audioSummary);
if (audioTracks.length > 0) {
console.log(`✅ EXPECTING AUDIO DATA from peer ${audioSummary.peerIdShort} - ${audioSummary.audioTracksEnabled}/${audioSummary.audioTrackCount} tracks enabled`);
} else {
console.log(`❌ NO AUDIO TRACKS from peer ${audioSummary.peerIdShort} - video only`);
}
this.addMessage('System', `🎵 Remote stream received from ${peerId.substring(0, 8)}... (${audioTracks.length} audio, ${videoTracks.length} video)`);
}
displayRemoteVideo(peerId, stream) {
console.log(`Displaying remote video for ${peerId}:`, stream);
console.log('Stream tracks:', stream.getTracks().map(t => ({ kind: t.kind, enabled: t.enabled })));
// CRITICAL: Prevent audio loopback - check if this is our own stream
const isOwnStream = peerId === this.mesh.peerId;
if (isOwnStream) {
console.warn('🚨 LOOPBACK DETECTED: Received our own stream! Muting audio to prevent feedback.');
}
const remoteVideosContainer = document.getElementById('remote-videos-container');
if (!remoteVideosContainer) return;
let remoteVideoItem = remoteVideosContainer.querySelector(`[data-peer-id="${peerId}"]`);
if (!remoteVideoItem) {
remoteVideoItem = document.createElement('div');
remoteVideoItem.className = 'remote-video-item';
remoteVideoItem.setAttribute('data-peer-id', peerId);
const title = document.createElement('div');
title.className = 'video-title';
title.textContent = `Peer ${peerId.substring(0, 8)}...`;
const video = document.createElement('video');
video.autoplay = true;
video.playsInline = true;
video.controls = true;
// CRITICAL: Mute if this is our own stream to prevent audio feedback
video.muted = isOwnStream; // Mute our own audio, unmute others
video.style.width = '100%';
video.style.borderRadius = '8px';
// Enhanced audio debugging
video.addEventListener('loadedmetadata', () => {
console.log('Video element loaded metadata:', {
duration: video.duration,
audioTracks: video.audioTracks?.length || 'N/A',
volume: video.volume,
muted: video.muted
});
video.play().then(() => {
console.log('Video/audio playback started successfully');
// Check audio context state
if (window.AudioContext || window.webkitAudioContext) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
console.log('Audio context state:', audioContext.state);
if (audioContext.state === 'suspended') {
console.log('Audio context is suspended - may need user interaction');
}
}
// Hide any play button if playback started
const playButton = remoteVideoItem.querySelector('.manual-play-button');
if (playButton) {
playButton.style.display = 'none';
}
}).catch(error => {
console.log('Autoplay failed, user interaction required:', error);
// Show a manual play button
const playButton = document.createElement('button');
playButton.className = 'manual-play-button';
playButton.textContent = '▶ Click to Play Audio';
playButton.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; padding: 10px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;';
playButton.addEventListener('click', async () => {
try {
await video.play();
playButton.style.display = 'none';
console.log('Manual play successful');
} catch (e) {
console.error('Manual play failed:', e);
}
});
remoteVideoItem.style.position = 'relative';
remoteVideoItem.appendChild(playButton);
});
});
// Add event listeners for audio debugging
video.addEventListener('play', () => {
console.log('Video element started playing');
});
video.addEventListener('pause', () => {
console.log('Video element paused');
});
video.addEventListener('volumechange', () => {
console.log('Volume changed:', video.volume, 'Muted:', video.muted);
});
const status = document.createElement('div');
status.className = 'video-status';
remoteVideoItem.appendChild(title);
remoteVideoItem.appendChild(video);
remoteVideoItem.appendChild(status);
remoteVideosContainer.appendChild(remoteVideoItem);
}
const video = remoteVideoItem.querySelector('video');
const status = remoteVideoItem.querySelector('.video-status');
video.srcObject = stream;
// EXPLICIT AUDIO DEBUGGING
console.log('🎵 Setting srcObject for video element:', {
stream,
streamId: stream.id,
streamActive: stream.active,
videoElement: video,
videoMuted: video.muted,
videoVolume: video.volume,
isOwnStream
});
const audioTracks = stream.getAudioTracks();
const videoTracks = stream.getVideoTracks();
console.log('🎵 Stream tracks assigned:', {
audioTracks: audioTracks.map(t => ({
id: t.id,
enabled: t.enabled,
readyState: t.readyState,
muted: t.muted
})),
videoTracks: videoTracks.map(t => ({
id: t.id,
enabled: t.enabled,
readyState: t.readyState,
muted: t.muted
}))
});
// CRITICAL: Set audio state based on whether this is our own stream
if (isOwnStream) {
console.warn('🚨 Muting our own stream to prevent audio feedback');
video.muted = true;
video.volume = 0;
} else {
// Only unmute if this is NOT our own stream
video.muted = false;
video.volume = 1.0;
// Setup audio playback monitoring for remote streams
this.setupAudioPlaybackMonitoring(video, peerId, audioTracks);
}
// Update status based on stream tracks
console.log(`Video tracks: ${videoTracks.length}, Audio tracks: ${audioTracks.length}`);
audioTracks.forEach((track, i) => {
console.log(`Audio track ${i}:`, { enabled: track.enabled, muted: track.muted, label: track.label });
});
// For audio-only streams, ensure the video element is properly configured
if (audioTracks.length > 0 && videoTracks.length === 0) {
console.log('Audio-only stream detected - configuring for audio playback');
video.style.height = '60px'; // Smaller height for audio-only
video.style.backgroundColor = '#f0f0f0';
// Force audio to play by trying to resume audio context
if (window.AudioContext || window.webkitAudioContext) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
try {
const audioContext = new AudioContext();
if (audioContext.state === 'suspended') {
audioContext.resume().then(() => {
console.log('Audio context resumed for remote stream');
});
}
// Test audio by creating a brief analysis
try {
const source = audioContext.createMediaStreamSource(stream);
const analyser = audioContext.createAnalyser();
source.connect(analyser);
const dataArray = new Uint8Array(analyser.frequencyBinCount);
const checkAudio = () => {
analyser.getByteFrequencyData(dataArray);
const sum = dataArray.reduce((a, b) => a + b, 0);
if (sum > 0) {
console.log('Remote audio data detected! Sum:', sum);
}
};
// Check audio data periodically
const audioChecker = setInterval(checkAudio, 1000);
setTimeout(() => clearInterval(audioChecker), 10000); // Stop after 10 seconds
} catch (audioAnalysisError) {
console.log('Could not analyze remote audio:', audioAnalysisError);
}
} catch (e) {
console.log('Could not create/resume audio context:', e);
}
}
}
const statusText = [];
if (videoTracks.length > 0) statusText.push('Video');
if (audioTracks.length > 0) statusText.push('Audio');
status.textContent = statusText.length > 0 ? statusText.join(' + ') : 'Audio only';
status.style.color = '#28a745';
this.addMessage('System', `Receiving ${statusText.join(' + ')} from ${peerId.substring(0, 8)}...`);
}
/**
* Setup audio playback monitoring for video elements playing remote streams
*/
setupAudioPlaybackMonitoring(videoElement, peerId, audioTracks) {
const peerIdShort = peerId.substring(0, 8);
console.log(`🎵 Setting up audio playback monitoring for peer ${peerIdShort}`);
if (audioTracks.length === 0) {
console.log(`🎵 No audio tracks to monitor for peer ${peerIdShort}`);
return;
}
try {
// Monitor video element audio events
videoElement.addEventListener('play', () => {
console.log(`🎵 Video element started playing for peer ${peerIdShort}`, {
muted: videoElement.muted,
volume: videoElement.volume,
audioTracks: audioTracks.length
});
});
videoElement.addEventListener('pause', () => {
console.log(`🎵 Video element paused for peer ${peerIdShort}`);
});
videoElement.addEventListener('volumechange', () => {
console.log(`🎵 Volume changed for peer ${peerIdShort}:`, {
volume: videoElement.volume,
muted: videoElement.muted
});
});
// Create audio context for monitoring actual audio data playback
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) {
console.warn('🎵 AudioContext not available - basic monitoring only');
return;
}
// Wait for video to load before setting up audio analysis
const setupAudioAnalysis = () => {
try {
const audioContext = new AudioContext();
const source = audioContext.createMediaElementSource(videoElement);
const analyser = audioContext.createAnalyser();
const gainNode = audioContext.createGain();
analyser.fftSize = 256;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
// Connect: source -> analyser -> gain -> destination
source.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
let lastLogTime = 0;
let totalSamples = 0;
let playbackSamples = 0;
let maxPlaybackLevel = 0;
const monitorPlayback = () => {
if (videoElement.paused || videoElement.ended) {
return; // Stop monitoring if playback stopped
}
analyser.getByteFrequencyData(dataArray);
// Calculate playback audio level
const average = dataArray.reduce((sum, value) => sum + value, 0) / bufferLength;
const currentTime = Date.now();
totalSamples++;
if (average > 3) { // Lower threshold for playback detection
playbackSamples++;
maxPlaybackLevel = Math.max(maxPlaybackLevel, average);
}
// Log eve