Bandito / static /style.css
makinuh's picture
Create style.css
6234d40 verified
<!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];
}
}