| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Low-Bandwidth Connect</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .video-container { | |
| position: relative; | |
| width: 100%; | |
| padding-bottom: 56.25%; /* 16:9 aspect ratio */ | |
| background-color: #1e293b; | |
| border-radius: 0.5rem; | |
| overflow: hidden; | |
| } | |
| .video-element { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .connection-quality { | |
| position: absolute; | |
| bottom: 10px; | |
| right: 10px; | |
| background-color: rgba(0,0,0,0.5); | |
| color: white; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| } | |
| .bandwidth-optimizer { | |
| transition: all 0.3s ease; | |
| } | |
| .bandwidth-optimizer:hover { | |
| transform: scale(1.05); | |
| } | |
| .pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.7); } | |
| 70% { box-shadow: 0 0 0 10px rgba(74, 222, 128, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-gray-100 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <header class="flex justify-between items-center mb-8"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-signal text-green-400 text-2xl mr-3"></i> | |
| <h1 class="text-2xl font-bold bg-gradient-to-r from-green-400 to-blue-500 bg-clip-text text-transparent"> | |
| LowBand Connect | |
| </h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <div class="hidden md:flex items-center space-x-2 text-sm"> | |
| <span class="text-gray-400">Optimized for</span> | |
| <span class="px-2 py-1 bg-gray-800 rounded-full text-green-400 font-medium"> | |
| <i class="fas fa-wifi mr-1"></i> Low Bandwidth | |
| </span> | |
| </div> | |
| <button id="settingsBtn" class="p-2 rounded-full hover:bg-gray-800 transition"> | |
| <i class="fas fa-cog text-gray-400"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <main> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> | |
| <!-- Local Video --> | |
| <div class="video-container"> | |
| <video id="localVideo" class="video-element" autoplay muted></video> | |
| <div class="connection-quality hidden"> | |
| <i class="fas fa-signal mr-1"></i> | |
| <span>Local</span> | |
| </div> | |
| <div class="absolute top-2 left-2 bg-gray-900 bg-opacity-70 px-2 py-1 rounded text-sm"> | |
| You | |
| </div> | |
| </div> | |
| <!-- Remote Video --> | |
| <div class="video-container"> | |
| <video id="remoteVideo" class="video-element" autoplay></video> | |
| <div class="connection-quality hidden"> | |
| <i class="fas fa-signal mr-1"></i> | |
| <span>Remote</span> | |
| </div> | |
| <div class="absolute top-2 left-2 bg-gray-900 bg-opacity-70 px-2 py-1 rounded text-sm"> | |
| Partner | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="text-sm bg-gray-800 px-3 py-1 rounded-full"> | |
| <span class="text-gray-400">Connection:</span> | |
| <span id="connectionStatus" class="font-medium text-yellow-400">Disconnected</span> | |
| </div> | |
| <div id="bandwidthIndicator" class="text-sm bg-gray-800 px-3 py-1 rounded-full hidden"> | |
| <span class="text-gray-400">Bandwidth:</span> | |
| <span id="bandwidthValue" class="font-medium">-- kbps</span> | |
| </div> | |
| </div> | |
| <div class="flex space-x-3"> | |
| <button id="toggleVideoBtn" class="bg-gray-800 hover:bg-gray-700 text-white p-3 rounded-full transition"> | |
| <i class="fas fa-video"></i> | |
| </button> | |
| <button id="toggleAudioBtn" class="bg-gray-800 hover:bg-gray-700 text-white p-3 rounded-full transition"> | |
| <i class="fas fa-microphone"></i> | |
| </button> | |
| <button id="callBtn" class="bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-full font-medium flex items-center pulse"> | |
| <i class="fas fa-phone mr-2"></i> | |
| <span>Start Call</span> | |
| </button> | |
| <button id="endCallBtn" class="bg-red-600 hover:bg-red-700 text-white px-6 py-3 rounded-full font-medium flex items-center hidden"> | |
| <i class="fas fa-phone-slash mr-2"></i> | |
| <span>End Call</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Bandwidth Optimizer Panel --> | |
| <div id="optimizerPanel" class="mt-8 bg-gray-800 rounded-lg p-4 hidden"> | |
| <h3 class="text-lg font-medium mb-4 flex items-center"> | |
| <i class="fas fa-tachometer-alt mr-2 text-blue-400"></i> | |
| Bandwidth Optimizer | |
| </h3> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div class="bandwidth-optimizer bg-gray-700 p-4 rounded-lg cursor-pointer" data-preset="low"> | |
| <div class="flex items-center mb-2"> | |
| <i class="fas fa-bicycle text-green-400 mr-2"></i> | |
| <h4 class="font-medium">Low Bandwidth</h4> | |
| </div> | |
| <p class="text-sm text-gray-400">Optimized for slow connections (64-128 kbps)</p> | |
| <div class="mt-3 text-xs text-gray-500"> | |
| <span>• 160x120 resolution</span><br> | |
| <span>• 10fps</span><br> | |
| <span>• Low bitrate</span> | |
| </div> | |
| </div> | |
| <div class="bandwidth-optimizer bg-gray-700 p-4 rounded-lg cursor-pointer" data-preset="medium"> | |
| <div class="flex items-center mb-2"> | |
| <i class="fas fa-car text-yellow-400 mr-2"></i> | |
| <h4 class="font-medium">Medium Bandwidth</h4> | |
| </div> | |
| <p class="text-sm text-gray-400">Balanced quality and bandwidth (128-256 kbps)</p> | |
| <div class="mt-3 text-xs text-gray-500"> | |
| <span>• 320x240 resolution</span><br> | |
| <span>• 15fps</span><br> | |
| <span>• Medium bitrate</span> | |
| </div> | |
| </div> | |
| <div class="bandwidth-optimizer bg-gray-700 p-4 rounded-lg cursor-pointer" data-preset="high"> | |
| <div class="flex items-center mb-2"> | |
| <i class="fas fa-rocket text-red-400 mr-2"></i> | |
| <h4 class="font-medium">High Bandwidth</h4> | |
| </div> | |
| <p class="text-sm text-gray-400">For better connections (256+ kbps)</p> | |
| <div class="mt-3 text-xs text-gray-500"> | |
| <span>• 640x480 resolution</span><br> | |
| <span>• 24fps</span><br> | |
| <span>• High bitrate</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Settings Modal --> | |
| <div id="settingsModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center hidden z-50"> | |
| <div class="bg-gray-800 rounded-lg p-6 w-full max-w-md"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-medium"> | |
| <i class="fas fa-cog mr-2 text-blue-400"></i> | |
| Settings | |
| </h3> | |
| <button id="closeSettingsBtn" class="text-gray-400 hover:text-white"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Video Source</label> | |
| <select id="videoSource" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm"> | |
| <option value="">Default Camera</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Audio Source</label> | |
| <select id="audioSource" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm"> | |
| <option value="">Default Microphone</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="flex items-center space-x-2"> | |
| <input type="checkbox" id="enableBandwidthDetection" class="rounded bg-gray-700 border-gray-600" checked> | |
| <span class="text-sm">Auto-detect bandwidth</span> | |
| </label> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Default Bandwidth</label> | |
| <select id="defaultBandwidth" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm"> | |
| <option value="low">Low (64-128 kbps)</option> | |
| <option value="medium" selected>Medium (128-256 kbps)</option> | |
| <option value="high">High (256+ kbps)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="mt-6 flex justify-end space-x-3"> | |
| <button id="saveSettingsBtn" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded font-medium"> | |
| Save Settings | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Connection Modal --> | |
| <div id="connectionModal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center hidden z-50"> | |
| <div class="bg-gray-800 rounded-lg p-6 w-full max-w-md text-center"> | |
| <div class="mb-4"> | |
| <i class="fas fa-link text-blue-400 text-5xl mb-3"></i> | |
| <h3 class="text-xl font-medium mb-2">Establishing Connection</h3> | |
| <p class="text-gray-400 text-sm">Optimizing for low bandwidth...</p> | |
| </div> | |
| <div class="w-full bg-gray-700 rounded-full h-2 mb-4"> | |
| <div id="connectionProgress" class="bg-blue-500 h-2 rounded-full" style="width: 0%"></div> | |
| </div> | |
| <button id="cancelConnectionBtn" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded font-medium"> | |
| Cancel | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // DOM Elements | |
| const localVideo = document.getElementById('localVideo'); | |
| const remoteVideo = document.getElementById('remoteVideo'); | |
| const callBtn = document.getElementById('callBtn'); | |
| const endCallBtn = document.getElementById('endCallBtn'); | |
| const toggleVideoBtn = document.getElementById('toggleVideoBtn'); | |
| const toggleAudioBtn = document.getElementById('toggleAudioBtn'); | |
| const connectionStatus = document.getElementById('connectionStatus'); | |
| const bandwidthIndicator = document.getElementById('bandwidthIndicator'); | |
| const bandwidthValue = document.getElementById('bandwidthValue'); | |
| const optimizerPanel = document.getElementById('optimizerPanel'); | |
| const settingsBtn = document.getElementById('settingsBtn'); | |
| const settingsModal = document.getElementById('settingsModal'); | |
| const closeSettingsBtn = document.getElementById('closeSettingsBtn'); | |
| const saveSettingsBtn = document.getElementById('saveSettingsBtn'); | |
| const connectionModal = document.getElementById('connectionModal'); | |
| const connectionProgress = document.getElementById('connectionProgress'); | |
| const cancelConnectionBtn = document.getElementById('cancelConnectionBtn'); | |
| // State variables | |
| let localStream; | |
| let peerConnection; | |
| let isCallActive = false; | |
| let isVideoEnabled = true; | |
| let isAudioEnabled = true; | |
| let currentBandwidthPreset = 'medium'; | |
| // Initialize the app | |
| async function init() { | |
| try { | |
| // Get media devices | |
| await getMediaDevices(); | |
| // Set up event listeners | |
| setupEventListeners(); | |
| // Show bandwidth optimizer panel | |
| optimizerPanel.classList.remove('hidden'); | |
| // Set default bandwidth preset | |
| applyBandwidthPreset(currentBandwidthPreset); | |
| } catch (error) { | |
| console.error('Initialization error:', error); | |
| } | |
| } | |
| // Set up event listeners | |
| function setupEventListeners() { | |
| // Call buttons | |
| callBtn.addEventListener('click', startCall); | |
| endCallBtn.addEventListener('click', endCall); | |
| // Toggle buttons | |
| toggleVideoBtn.addEventListener('click', toggleVideo); | |
| toggleAudioBtn.addEventListener('click', toggleAudio); | |
| // Bandwidth optimizers | |
| document.querySelectorAll('.bandwidth-optimizer').forEach(optimizer => { | |
| optimizer.addEventListener('click', () => { | |
| const preset = optimizer.getAttribute('data-preset'); | |
| applyBandwidthPreset(preset); | |
| }); | |
| }); | |
| // Settings | |
| settingsBtn.addEventListener('click', () => settingsModal.classList.remove('hidden')); | |
| closeSettingsBtn.addEventListener('click', () => settingsModal.classList.add('hidden')); | |
| saveSettingsBtn.addEventListener('click', saveSettings); | |
| // Connection modal | |
| cancelConnectionBtn.addEventListener('click', cancelConnection); | |
| } | |
| // Get media devices | |
| async function getMediaDevices() { | |
| try { | |
| localStream = await navigator.mediaDevices.getUserMedia({ | |
| video: true, | |
| audio: true | |
| }); | |
| localVideo.srcObject = localStream; | |
| // Populate device selectors | |
| const devices = await navigator.mediaDevices.enumerateDevices(); | |
| const videoSource = document.getElementById('videoSource'); | |
| const audioSource = document.getElementById('audioSource'); | |
| devices.forEach(device => { | |
| if (device.kind === 'videoinput') { | |
| const option = document.createElement('option'); | |
| option.value = device.deviceId; | |
| option.text = device.label || `Camera ${videoSource.length + 1}`; | |
| videoSource.appendChild(option); | |
| } else if (device.kind === 'audioinput') { | |
| const option = document.createElement('option'); | |
| option.value = device.deviceId; | |
| option.text = device.label || `Microphone ${audioSource.length + 1}`; | |
| audioSource.appendChild(option); | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('Error accessing media devices:', error); | |
| alert('Could not access camera or microphone. Please check permissions.'); | |
| } | |
| } | |
| // Start a call | |
| async function startCall() { | |
| if (!localStream) { | |
| alert('Please allow camera and microphone access first.'); | |
| return; | |
| } | |
| // Show connection modal | |
| connectionModal.classList.remove('hidden'); | |
| simulateConnectionProgress(); | |
| try { | |
| // Create peer connection | |
| const configuration = { | |
| iceServers: [ | |
| { urls: 'stun:stun.l.google.com:19302' }, | |
| // Add your TURN server here for NAT traversal | |
| ] | |
| }; | |
| peerConnection = new RTCPeerConnection(configuration); | |
| // Set up event handlers | |
| peerConnection.onicecandidate = handleICECandidateEvent; | |
| peerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent; | |
| peerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent; | |
| peerConnection.onsignalingstatechange = handleSignalingStateChangeEvent; | |
| peerConnection.ontrack = handleTrackEvent; | |
| // Add local stream tracks | |
| localStream.getTracks().forEach(track => { | |
| peerConnection.addTrack(track, localStream); | |
| }); | |
| // Create offer | |
| const offer = await peerConnection.createOffer({ | |
| offerToReceiveAudio: true, | |
| offerToReceiveVideo: true | |
| }); | |
| await peerConnection.setLocalDescription(offer); | |
| // In a real app, you would send the offer to the other peer via signaling | |
| // For this demo, we'll simulate the connection | |
| setTimeout(() => { | |
| // Simulate receiving an answer | |
| simulateAnswer(); | |
| // Update UI | |
| connectionStatus.textContent = 'Connected'; | |
| connectionStatus.className = 'font-medium text-green-400'; | |
| // Show bandwidth indicator | |
| bandwidthIndicator.classList.remove('hidden'); | |
| updateBandwidthDisplay(); | |
| // Toggle call buttons | |
| callBtn.classList.add('hidden'); | |
| endCallBtn.classList.remove('hidden'); | |
| // Hide connection modal | |
| connectionModal.classList.add('hidden'); | |
| isCallActive = true; | |
| }, 2000); | |
| } catch (error) { | |
| console.error('Error starting call:', error); | |
| connectionModal.classList.add('hidden'); | |
| alert('Failed to start call. Please try again.'); | |
| } | |
| } | |
| // Simulate connection progress | |
| function simulateConnectionProgress() { | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += 5; | |
| connectionProgress.style.width = `${progress}%`; | |
| if (progress >= 100) { | |
| clearInterval(interval); | |
| } | |
| }, 200); | |
| } | |
| // Simulate receiving an answer (for demo purposes) | |
| function simulateAnswer() { | |
| if (!peerConnection) return; | |
| // In a real app, you would receive this from the other peer | |
| const answer = { | |
| type: 'answer', | |
| sdp: `v=0 | |
| o=- 123456789 2 IN IP4 127.0.0.1 | |
| s=- | |
| t=0 0 | |
| a=group:BUNDLE 0 1 | |
| a=msid-semantic: WMS | |
| m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126 | |
| c=IN IP4 0.0.0.0 | |
| a=rtcp:9 IN IP4 0.0.0.0 | |
| a=ice-ufrag:xyz | |
| a=ice-pwd:abc | |
| a=fingerprint:sha-256 AA:BB:CC | |
| a=setup:active | |
| a=mid:0 | |
| a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level | |
| a=sendrecv | |
| a=rtpmap:111 opus/48000/2 | |
| a=fmtp:111 minptime=10;useinbandfec=1 | |
| a=rtpmap:103 ISAC/16000 | |
| a=rtpmap:104 ISAC/32000 | |
| a=rtpmap:9 G722/8000 | |
| a=rtpmap:0 PCMU/8000 | |
| a=rtpmap:8 PCMA/8000 | |
| a=rtpmap:106 CN/32000 | |
| a=rtpmap:105 CN/16000 | |
| a=rtpmap:13 CN/8000 | |
| a=rtpmap:110 telephone-event/48000 | |
| a=rtpmap:112 telephone-event/32000 | |
| a=rtpmap:113 telephone-event/16000 | |
| a=rtpmap:126 telephone-event/8000 | |
| a=maxptime:60 | |
| a=ssrc:12345678 cname:audio | |
| m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 | |
| c=IN IP4 0.0.0.0 | |
| a=rtcp:9 IN IP4 0.0.0.0 | |
| a=ice-ufrag:xyz | |
| a=ice-pwd:abc | |
| a=fingerprint:sha-256 AA:BB:CC | |
| a=setup:active | |
| a=mid:1 | |
| a=extmap:2 urn:ietf:params:rtp-hdrext:toffset | |
| a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time | |
| a=extmap:4 urn:3gpp:video-orientation | |
| a=sendrecv | |
| a=rtpmap:96 VP8/90000 | |
| a=rtcp-fb:96 goog-remb | |
| a=rtcp-fb:96 transport-cc | |
| a=rtcp-fb:96 ccm fir | |
| a=rtcp-fb:96 nack | |
| a=rtcp-fb:96 nack pli | |
| a=rtpmap:97 rtx/90000 | |
| a=fmtp:97 apt=96 | |
| a=rtpmap:98 VP9/90000 | |
| a=rtcp-fb:98 goog-remb | |
| a=rtcp-fb:98 transport-cc | |
| a=rtcp-fb:98 ccm fir | |
| a=rtcp-fb:98 nack | |
| a=rtcp-fb:98 nack pli | |
| a=rtpmap:99 rtx/90000 | |
| a=fmtp:99 apt=98 | |
| a=rtpmap:100 H264/90000 | |
| a=rtcp-fb:100 goog-remb | |
| a=rtcp-fb:100 transport-cc | |
| a=rtcp-fb:100 ccm fir | |
| a=rtcp-fb:100 nack | |
| a=rtcp-fb:100 nack pli | |
| a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f | |
| a=rtpmap:101 rtx/90000 | |
| a=fmtp:101 apt=100 | |
| a=rtpmap:102 H264/90000 | |
| a=rtcp-fb:102 goog-remb | |
| a=rtcp-fb:102 transport-cc | |
| a=rtcp-fb:102 ccm fir | |
| a=rtcp-fb:102 nack | |
| a=rtcp-fb:102 nack pli | |
| a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f | |
| a=ssrc-group:FID 12345678 12345679 | |
| a=ssrc:12345678 cname:video | |
| a=ssrc:12345679 cname:video` | |
| }; | |
| peerConnection.setRemoteDescription(new RTCSessionDescription(answer)); | |
| } | |
| // End the call | |
| function endCall() { | |
| if (peerConnection) { | |
| peerConnection.close(); | |
| peerConnection = null; | |
| } | |
| if (remoteVideo.srcObject) { | |
| remoteVideo.srcObject.getTracks().forEach(track => track.stop()); | |
| remoteVideo.srcObject = null; | |
| } | |
| // Update UI | |
| connectionStatus.textContent = 'Disconnected'; | |
| connectionStatus.className = 'font-medium text-yellow-400'; | |
| // Hide bandwidth indicator | |
| bandwidthIndicator.classList.add('hidden'); | |
| // Toggle call buttons | |
| callBtn.classList.remove('hidden'); | |
| endCallBtn.classList.add('hidden'); | |
| isCallActive = false; | |
| } | |
| // Cancel connection attempt | |
| function cancelConnection() { | |
| if (peerConnection) { | |
| peerConnection.close(); | |
| peerConnection = null; | |
| } | |
| connectionModal.classList.add('hidden'); | |
| } | |
| // Toggle video | |
| function toggleVideo() { | |
| if (!localStream) return; | |
| const videoTrack = localStream.getVideoTracks()[0]; | |
| if (videoTrack) { | |
| isVideoEnabled = !videoTrack.enabled; | |
| videoTrack.enabled = isVideoEnabled; | |
| toggleVideoBtn.innerHTML = isVideoEnabled ? '<i class="fas fa-video"></i>' : '<i class="fas fa-video-slash"></i>'; | |
| toggleVideoBtn.classList.toggle('bg-gray-800'); | |
| toggleVideoBtn.classList.toggle('bg-red-600'); | |
| // Update connection if active | |
| if (isCallActive) { | |
| updateBandwidthSettings(); | |
| } | |
| } | |
| } | |
| // Toggle audio | |
| function toggleAudio() { | |
| if (!localStream) return; | |
| const audioTrack = localStream.getAudioTracks()[0]; | |
| if (audioTrack) { | |
| isAudioEnabled = !audioTrack.enabled; | |
| audioTrack.enabled = isAudioEnabled; | |
| toggleAudioBtn.innerHTML = isAudioEnabled ? '<i class="fas fa-microphone"></i>' : '<i class="fas fa-microphone-slash"></i>'; | |
| toggleAudioBtn.classList.toggle('bg-gray-800'); | |
| toggleAudioBtn.classList.toggle('bg-red-600'); | |
| } | |
| } | |
| // Apply bandwidth preset | |
| function applyBandwidthPreset(preset) { | |
| currentBandwidthPreset = preset; | |
| // Update UI | |
| document.querySelectorAll('.bandwidth-optimizer').forEach(opt => { | |
| opt.classList.remove('border-2', 'border-green-400'); | |
| if (opt.getAttribute('data-preset') === preset) { | |
| opt.classList.add('border-2', 'border-green-400'); | |
| } | |
| }); | |
| // Update connection if active | |
| if (isCallActive) { | |
| updateBandwidthSettings(); | |
| } | |
| // Update bandwidth display | |
| updateBandwidthDisplay(); | |
| } | |
| // Update bandwidth settings for the connection | |
| function updateBandwidthSettings() { | |
| if (!peerConnection || !isCallActive) return; | |
| const senders = peerConnection.getSenders(); | |
| senders.forEach(sender => { | |
| if (sender.track.kind === 'video') { | |
| const parameters = sender.getParameters(); | |
| if (!parameters.encodings) { | |
| parameters.encodings = [{}]; | |
| } | |
| // Apply bandwidth constraints based on preset | |
| switch (currentBandwidthPreset) { | |
| case 'low': | |
| parameters.encodings[0].maxBitrate = 128000; // 128 kbps | |
| break; | |
| case 'medium': | |
| parameters.encodings[0].maxBitrate = 256000; // 256 kbps | |
| break; | |
| case 'high': | |
| parameters.encodings[0].maxBitrate = 512000; // 512 kbps | |
| break; | |
| } | |
| // Apply resolution scaling based on preset | |
| if (sender.track.kind === 'video') { | |
| const constraints = {}; | |
| switch (currentBandwidthPreset) { | |
| case 'low': | |
| constraints.width = { ideal: 160 }; | |
| constraints.height = { ideal: 120 }; | |
| constraints.frameRate = { ideal: 10 }; | |
| break; | |
| case 'medium': | |
| constraints.width = { ideal: 320 }; | |
| constraints.height = { ideal: 240 }; | |
| constraints.frameRate = { ideal: 15 }; | |
| break; | |
| case 'high': | |
| constraints.width = { ideal: 640 }; | |
| constraints.height = { ideal: 480 }; | |
| constraints.frameRate = { ideal: 24 }; | |
| break; | |
| } | |
| sender.track.applyConstraints(constraints); | |
| } | |
| sender.setParameters(parameters); | |
| } | |
| }); | |
| updateBandwidthDisplay(); | |
| } | |
| // Update bandwidth display | |
| function updateBandwidthDisplay() { | |
| let bandwidthText = ''; | |
| switch (currentBandwidthPreset) { | |
| case 'low': | |
| bandwidthText = '64-128 kbps'; | |
| break; | |
| case 'medium': | |
| bandwidthText = '128-256 kbps'; | |
| break; | |
| case 'high': | |
| bandwidthText = '256-512 kbps'; | |
| break; | |
| } | |
| bandwidthValue.textContent = bandwidthText; | |
| } | |
| // Save settings | |
| function saveSettings() { | |
| const videoSource = document.getElementById('videoSource').value; | |
| const audioSource = document.getElementById('audioSource').value; | |
| const enableBandwidthDetection = document.getElementById('enableBandwidthDetection').checked; | |
| const defaultBandwidth = document.getElementById('defaultBandwidth').value; | |
| // In a real app, you would save these settings to localStorage or a server | |
| console.log('Settings saved:', { | |
| videoSource, | |
| audioSource, | |
| enableBandwidthDetection, | |
| defaultBandwidth | |
| }); | |
| // Apply default bandwidth if changed | |
| if (defaultBandwidth !== currentBandwidthPreset) { | |
| applyBandwidthPreset(defaultBandwidth); | |
| } | |
| // Close settings modal | |
| settingsModal.classList.add('hidden'); | |
| alert('Settings saved successfully!'); | |
| } | |
| // WebRTC event handlers | |
| function handleICECandidateEvent(event) { | |
| if (event.candidate) { | |
| // In a real app, you would send the candidate to the other peer | |
| console.log('ICE candidate:', event.candidate); | |
| } | |
| } | |
| function handleICEConnectionStateChangeEvent() { | |
| if (peerConnection) { | |
| console.log('ICE connection state:', peerConnection.iceConnectionState); | |
| } | |
| } | |
| function handleICEGatheringStateChangeEvent() { | |
| if (peerConnection) { | |
| console.log('ICE gathering state:', peerConnection.iceGatheringState); | |
| } | |
| } | |
| function handleSignalingStateChangeEvent() { | |
| if (peerConnection) { | |
| console.log('Signaling state:', peerConnection.signalingState); | |
| } | |
| } | |
| function handleTrackEvent(event) { | |
| if (event.streams && event.streams[0]) { | |
| remoteVideo.srcObject = event.streams[0]; | |
| } | |
| } |