const socket = io(); const chatBox = document.getElementById('chat-box'); const chatInput = document.getElementById('chat-input'); const sendBtn = document.getElementById('send-btn'); const joinBtn = document.getElementById('join-btn'); const statusText = document.getElementById('status'); const audioContainer = document.getElementById('audio-container'); let localStream; const peers = {}; // Store connections to other users // --- WebRTC Configuration --- // Using Google STUN and your dedicated Metered.live TURN server const rtcConfig = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, // Backup STUN { urls: 'turn:saman.metered.live:80', username: '1bb6c42832c6a26b4db391e4', credential: 'axHWrYuQbsBaG5u9' }, { urls: 'turn:saman.metered.live:443', username: '1bb6c42832c6a26b4db391e4', credential: 'axHWrYuQbsBaG5u9' }, { urls: 'turn:saman.metered.live:443?transport=tcp', username: '1bb6c42832c6a26b4db391e4', credential: 'axHWrYuQbsBaG5u9' } ] }; // --- CHAT LOGIC --- sendBtn.onclick = () => { const text = chatInput.value.trim(); if (text) { socket.emit('chat-message', text); chatInput.value = ''; } }; socket.on('chat-message', (data) => { const div = document.createElement('div'); div.className = 'message ' + (data.id === socket.id ? 'self' : ''); div.innerText = `${data.id === socket.id ? 'You' : data.id.substring(0,4)}: ${data.text}`; chatBox.appendChild(div); chatBox.scrollTop = chatBox.scrollHeight; }); // Allow hitting "Enter" to send a chat message chatInput.addEventListener("keypress", function(event) { if (event.key === "Enter") { event.preventDefault(); sendBtn.click(); } }); // --- AUDIO/WEBRTC LOGIC --- joinBtn.onclick = async () => { try { // Request microphone access localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); statusText.innerText = "Mic Online - You are in the call"; joinBtn.disabled = true; // Let the server know we are ready to connect to others socket.emit('join-room'); } catch (err) { alert("Microphone access denied or not found."); console.error(err); } }; // When a new user connects, we (if we have our mic on) initiate a call to them socket.on('user-connected', (userId) => { if (!localStream) return; // Don't call if we aren't in the audio room appendSystemMessage(`User ${userId.substring(0,4)} joined. Attempting connection...`); const peer = createPeerConnection(userId, true); peers[userId] = peer; }); // Handle incoming signals (Offers, Answers, ICE candidates) socket.on('signal', async (data) => { if (!localStream) return; const { from, signal } = data; // If we don't have a peer connection for this user yet, create one if (!peers[from]) { peers[from] = createPeerConnection(from, false); } const peer = peers[from]; try { if (signal.type === 'offer') { await peer.setRemoteDescription(new RTCSessionDescription(signal)); const answer = await peer.createAnswer(); await peer.setLocalDescription(answer); socket.emit('signal', { to: from, signal: peer.localDescription }); } else if (signal.type === 'answer') { await peer.setRemoteDescription(new RTCSessionDescription(signal)); } else if (signal.candidate) { await peer.addIceCandidate(new RTCIceCandidate(signal)); } } catch (err) { console.error("Signaling error:", err); } }); // Clean up when someone leaves socket.on('user-disconnected', (userId) => { if (peers[userId]) { peers[userId].close(); delete peers[userId]; // Remove their audio element const audioEl = document.getElementById(`audio-${userId}`); if (audioEl) audioEl.remove(); appendSystemMessage(`User ${userId.substring(0,4)} left.`); } }); // Helper function to create a WebRTC Peer Connection function createPeerConnection(targetUserId, isInitiator) { const peer = new RTCPeerConnection(rtcConfig); // Add our local microphone stream to the connection localStream.getTracks().forEach(track => peer.addTrack(track, localStream)); // Send ICE candidates to the other user peer.onicecandidate = (event) => { if (event.candidate) { socket.emit('signal', { to: targetUserId, signal: event.candidate }); } }; // TRACK THE CONNECTION STATUS peer.oniceconnectionstatechange = () => { console.log(`Connection state with ${targetUserId}:`, peer.iceConnectionState); if (peer.iceConnectionState === 'failed') { appendSystemMessage(`❌ Connection to ${targetUserId.substring(0,4)} failed. Firewalls blocked it.`); } else if (peer.iceConnectionState === 'connected') { appendSystemMessage(`✅ Successfully connected to ${targetUserId.substring(0,4)}! Audio flowing.`); } else if (peer.iceConnectionState === 'disconnected') { appendSystemMessage(`⚠️ Connection to ${targetUserId.substring(0,4)} lost. Trying to reconnect...`); } }; // When we receive their audio stream, play it peer.ontrack = (event) => { let audioEl = document.getElementById(`audio-${targetUserId}`); if (!audioEl) { audioEl = document.createElement('audio'); audioEl.id = `audio-${targetUserId}`; audioEl.autoplay = true; audioContainer.appendChild(audioEl); } audioEl.srcObject = event.streams[0]; // Force the browser to play the audio and catch any autoplay errors audioEl.play().catch(error => { console.error("Browser blocked autoplay:", error); appendSystemMessage("Audio blocked by browser. Click anywhere on the page to allow audio."); }); }; // If we are initiating the call, create an offer if (isInitiator) { peer.createOffer().then(offer => { peer.setLocalDescription(offer); socket.emit('signal', { to: targetUserId, signal: offer }); }); } return peer; } function appendSystemMessage(msg) { const div = document.createElement('div'); div.className = 'message system'; div.innerText = msg; chatBox.appendChild(div); chatBox.scrollTop = chatBox.scrollHeight; }