|
<!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]; |
|
|
|
} |
|
|
|
} |