let localStream; let peerConnections = {}; let username = ''; let isVideoEnabled = true; let isAudioEnabled = true; let isSelfViewVisible = false; // DOM elements const usernameModal = document.getElementById('usernameModal'); const usernameForm = document.getElementById('usernameForm'); const usernameInput = document.getElementById('usernameInput'); const chatInterface = document.getElementById('chatInterface'); const localVideo = document.getElementById('localVideo'); const selfVideo = document.getElementById('selfVideo'); const remoteVideos = document.getElementById('remoteVideos'); const muteButton = document.getElementById('muteButton'); const videoButton = document.getElementById('videoButton'); const hangupButton = document.getElementById('hangupButton'); const toggleSelfViewButton = document.getElementById('toggleSelfViewButton'); const selfViewContainer = document.getElementById('selfViewContainer'); const participantCount = document.getElementById('participantCount'); const selfViewUsername = document.getElementById('selfViewUsername'); const themeToggle = document.getElementById('themeToggle'); // Initialize Socket.IO connection (but don't connect yet) let socket; // ICE servers configuration for WebRTC const iceServers = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' } ] }; // Username form submission usernameForm.addEventListener('submit', async (e) => { e.preventDefault(); const inputUsername = usernameInput.value.trim(); if (inputUsername.length < 1) { alert('Please enter a valid username'); return; } username = inputUsername; selfViewUsername.textContent = username; try { // Get user media localStream = await navigator.mediaDevices.getUserMedia({video: true, audio: true}); localVideo.srcObject = localStream; selfVideo.srcObject = localStream; console.log('Local stream obtained'); // Hide modal and show chat interface usernameModal.classList.add('hidden'); chatInterface.classList.remove('hidden'); // Add your own video to the grid addLocalVideoToGrid(); // Initialize Socket.IO connection initializeSocket(); } catch (error) { console.error('Error accessing media devices:', error); alert('Unable to access camera/microphone. Please check your permissions and try again.'); } }); // Initialize Socket.IO connection function initializeSocket() { socket = io('https://localhost:3000', { secure: true, rejectUnauthorized: false // For self-signed certificates in development }); setupSocketListeners(); } // Setup Socket.IO event listeners function setupSocketListeners() { socket.on('connect', () => { console.log('Connected to server'); // Join the room after connecting socket.emit('join_room', { username: username }); }); socket.on('disconnect', () => { console.log('Disconnected from server'); }); socket.on('user_joined', (data) => { console.log('User joined:', data.id, data.username); createPeerConnection(data.id, true, data.username); // This client will create offer updateParticipantCount(); }); socket.on('existing_users', (data) => { console.log('Existing users:', data.users); // Connect to existing users (they will create offers to us) data.users.forEach(user => { createPeerConnection(user.id, false, user.username); // Don't create offer, wait for existing users to offer }); updateParticipantCount(); }); socket.on('user_disconnected', (data) => { console.log('User disconnected:', data.id); handleUserDisconnected(data.id); updateParticipantCount(); }); socket.on('offer', async (data) => { console.log('Received offer from:', data.id); await handleOffer(data); }); socket.on('answer', async (data) => { console.log('Received answer from:', data.id); await handleAnswer(data); }); socket.on('ice_candidate', async (data) => { console.log('Received ICE candidate'); await handleIceCandidate(data); }); } // WebRTC functions function createPeerConnection(userId, shouldCreateOffer = false, remoteUsername = null) { const peerConnection = new RTCPeerConnection(iceServers); peerConnections[userId] = { pc: peerConnection, username: remoteUsername }; // Add local stream to peer connection if (localStream) { localStream.getTracks().forEach(track => { peerConnection.addTrack(track, localStream); }); } // Handle remote stream peerConnection.ontrack = (event) => { console.log('Received remote stream from:', userId); addRemoteVideo(userId, event.streams[0], remoteUsername); }; // Handle ICE candidates peerConnection.onicecandidate = (event) => { if (event.candidate) { console.log('Sending ICE candidate'); socket.emit('ice_candidate', { candidate: event.candidate, target_id: userId }); } }; // Create offer if this is the initiating peer if (shouldCreateOffer) { createOffer(peerConnection, userId); } return peerConnection; } async function createOffer(peerConnection, userId) { try { const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); socket.emit('offer', { offer: offer, target_id: userId }); console.log('Offer sent to:', userId); } catch (error) { console.error('Error creating offer:', error); } } async function handleOffer(data) { try { const peerConnection = createPeerConnection(data.id, false); await peerConnection.setRemoteDescription(data.offer); const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); socket.emit('answer', { answer: answer, id: data.id }); console.log('Answer sent to:', data.id); } catch (error) { console.error('Error handling offer:', error); } } async function handleAnswer(data) { try { const peerConnectionObj = peerConnections[data.id]; if (peerConnectionObj) { await peerConnectionObj.pc.setRemoteDescription(data.answer); console.log('Answer processed from:', data.id); } } catch (error) { console.error('Error handling answer:', error); } } async function handleIceCandidate(data) { try { const peerConnectionObj = peerConnections[data.target_id] || peerConnections[Object.keys(peerConnections)[0]]; if (peerConnectionObj && data.candidate) { await peerConnectionObj.pc.addIceCandidate(data.candidate); console.log('ICE candidate added'); } } catch (error) { console.error('Error handling ICE candidate:', error); } } function addRemoteVideo(userId, stream, remoteUsername = null) { // Remove existing video if any const existingContainer = document.getElementById(userId); if (existingContainer) { existingContainer.remove(); } const remoteVideoContainer = document.createElement('div'); remoteVideoContainer.className = 'remoteVideoContainer'; remoteVideoContainer.id = userId; const remoteVideo = document.createElement('video'); remoteVideo.className = 'remoteVideo'; remoteVideo.autoplay = true; remoteVideo.srcObject = stream; remoteVideoContainer.appendChild(remoteVideo); const usernameElement = document.createElement('div'); usernameElement.className = 'remoteUsername'; usernameElement.textContent = `${remoteUsername || `user_${userId.substring(0, 8)}`}`; remoteVideoContainer.appendChild(usernameElement); remoteVideos.appendChild(remoteVideoContainer); } // Add local video to the main grid function addLocalVideoToGrid() { const localVideoContainer = document.createElement('div'); localVideoContainer.className = 'remoteVideoContainer'; localVideoContainer.id = 'local-video-display'; const localVideoDisplay = document.createElement('video'); localVideoDisplay.className = 'remoteVideo'; localVideoDisplay.autoplay = true; localVideoDisplay.muted = true; // Prevent echo localVideoDisplay.srcObject = localStream; localVideoDisplay.style.transform = 'scaleX(-1)'; // Mirror effect localVideoContainer.appendChild(localVideoDisplay); const localUsernameElement = document.createElement('div'); localUsernameElement.className = 'remoteUsername'; localUsernameElement.textContent = `${username} (you)`; localVideoContainer.appendChild(localUsernameElement); remoteVideos.appendChild(localVideoContainer); } // Update participant count function updateParticipantCount() { const count = Object.keys(peerConnections).length + 1; // +1 for local user participantCount.textContent = `${count} user${count !== 1 ? 's' : ''}`; } function handleUserDisconnected(userId) { const videoContainer = document.getElementById(userId); if (videoContainer) { videoContainer.remove(); } const peerConnectionObj = peerConnections[userId]; if (peerConnectionObj) { peerConnectionObj.pc.close(); delete peerConnections[userId]; } } // Button event handlers muteButton.addEventListener('click', () => { if (localStream) { const audioTrack = localStream.getAudioTracks()[0]; if (audioTrack) { audioTrack.enabled = !audioTrack.enabled; isAudioEnabled = audioTrack.enabled; const span = muteButton.querySelector('span'); if (isAudioEnabled) { span.textContent = 'mic'; muteButton.classList.remove('muted'); } else { span.textContent = 'muted'; muteButton.classList.add('muted'); } } } }); videoButton.addEventListener('click', () => { if (localStream) { const videoTrack = localStream.getVideoTracks()[0]; if (videoTrack) { videoTrack.enabled = !videoTrack.enabled; isVideoEnabled = videoTrack.enabled; const span = videoButton.querySelector('span'); // Update local video display visibility const localVideoDisplay = document.querySelector('#local-video-display .remoteVideo'); if (isVideoEnabled) { span.textContent = 'cam'; videoButton.classList.remove('disabled'); if (localVideoDisplay) { localVideoDisplay.style.visibility = 'visible'; } if (selfVideo && isSelfViewVisible) { selfVideo.style.visibility = 'visible'; } } else { span.textContent = 'nocam'; videoButton.classList.add('disabled'); if (localVideoDisplay) { localVideoDisplay.style.visibility = 'hidden'; } if (selfVideo) { selfVideo.style.visibility = 'hidden'; } } } } }); toggleSelfViewButton.addEventListener('click', () => { isSelfViewVisible = !isSelfViewVisible; if (isSelfViewVisible) { selfViewContainer.classList.remove('hidden'); toggleSelfViewButton.textContent = '[HIDE]'; toggleSelfViewButton.title = 'Hide self view'; // Sync video state with main video if (selfVideo && !isVideoEnabled) { selfVideo.style.visibility = 'hidden'; } } else { selfViewContainer.classList.add('hidden'); toggleSelfViewButton.textContent = '[VIEW]'; toggleSelfViewButton.title = 'Show self view'; } }); hangupButton.addEventListener('click', () => { // Close all peer connections Object.values(peerConnections).forEach(pcObj => pcObj.pc.close()); peerConnections = {}; // Stop local stream if (localStream) { localStream.getTracks().forEach(track => track.stop()); localVideo.srcObject = null; selfVideo.srcObject = null; } // Remove all videos (including local video display) while (remoteVideos.firstChild) { remoteVideos.firstChild.remove(); } // Disconnect from server if (socket) { socket.disconnect(); } // Reset UI chatInterface.classList.add('hidden'); usernameModal.classList.remove('hidden'); usernameInput.value = ''; selfViewContainer.classList.add('hidden'); isSelfViewVisible = false; console.log('Left the chat'); }); // Theme toggle functionality function initTheme() { const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); updateThemeToggleIcon(savedTheme); } function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'light' ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); updateThemeToggleIcon(newTheme); } function updateThemeToggleIcon(theme) { themeToggle.textContent = theme === 'light' ? '🌙' : '☀️'; } themeToggle.addEventListener('click', toggleTheme); // Focus username input on load and initialize theme document.addEventListener('DOMContentLoaded', () => { usernameInput.focus(); initTheme(); });