| Server IP : 127.0.0.1 / Your IP : 216.73.216.48 Web Server : Apache/2.4.58 (Win64) OpenSSL/3.1.3 PHP/8.2.12 System : Windows NT DESKTOP-3H4FHQJ 10.0 build 19045 (Windows 10) AMD64 User : win 10 ( 0) PHP Version : 8.2.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : D:/xampp/htdocs-coblaa/pureFaith/ |
Upload File : |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi-User Live Stream Signaling (Passcode Role)</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
background-color: #0F172A; /* Slate 900 */
}
.stream-container {
width: 100%;
max-width: 900px;
}
.aspect-16-9 { aspect-ratio: 16 / 9; }
.aspect-4-3 { aspect-ratio: 4 / 3; }
.aspect-1-1 { aspect-ratio: 1 / 1; }
#video-feed {
object-fit: cover;
width: 100%;
height: 100%;
background-color: #1E293B; /* Slate 800 */
}
.recording-pulse {
animation: pulse-red 1s infinite;
}
@keyframes pulse-red {
0%, 100% { background-color: rgba(239, 68, 68, 0.9); }
50% { background-color: rgba(239, 68, 68, 0.4); }
}
</style>
</head>
<body class="min-h-screen flex flex-col items-center p-4">
<header class="text-center mb-6 w-full max-w-2xl">
<h1 class="text-3xl font-bold text-white mb-1">Live Stream Signaling with Role Gate</h1>
<p id="user-info" class="text-sm text-gray-400">Loading User ID...</p>
<p id="role-status" class="text-base font-semibold mt-2 p-2 rounded-lg bg-indigo-900 text-indigo-300">
Role: Viewer (Default)
</p>
<p id="streamer-status" class="text-base font-semibold mt-2 p-2 rounded-lg bg-gray-700 text-gray-300">
Stream Status: Offline
</p>
</header>
<!-- Main Stream Container -->
<div id="stream-container" class="stream-container aspect-16-9 rounded-xl shadow-2xl overflow-hidden relative border-4 border-indigo-500 mb-8">
<video id="video-feed" autoplay playsinline class="rounded-lg"></video>
<!-- Status Overlay for loading, errors, and viewing other streams -->
<div id="status-overlay" class="absolute inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center p-6 transition-opacity duration-300">
<div class="text-center space-y-4">
<svg id="status-icon" class="w-12 h-12 mx-auto text-indigo-400 animate-spin" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" class="opacity-25"></circle>
<path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" class="opacity-75"></path>
</svg>
<p id="status-text" class="text-lg font-semibold text-white">Initializing application...</p>
<div id="error-details" class="text-sm text-red-400 hidden p-2 bg-gray-700 rounded-lg"></div>
</div>
</div>
</div>
<!-- Controls Container -->
<div class="w-full max-w-xl space-y-4">
<!-- Admin Access Input (Visible until successfully logged in as Admin) -->
<div id="admin-login-panel" class="p-4 bg-gray-800 rounded-xl shadow-lg border border-yellow-500">
<p class="text-sm font-semibold text-yellow-300 mb-3 text-center">Admin ID Access: (Passcode is **45544554**)</p>
<div class="flex space-x-2">
<input type="password" id="admin-id-input" placeholder="Enter Admin Passcode" class="flex-grow p-3 rounded-lg bg-gray-700 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-yellow-500">
<button id="admin-check-btn" class="p-3 bg-yellow-600 text-white rounded-lg font-semibold hover:bg-yellow-700 transition-colors shadow-md">
Check ID
</button>
</div>
<p id="login-message" class="text-center mt-2 text-sm hidden"></p>
</div>
<!-- Viewer Controls (Visible only when watching a live stream) -->
<div id="viewer-controls" class="p-4 bg-gray-700 rounded-xl shadow-lg hidden">
<p class="text-sm font-semibold text-gray-300 mb-3 text-center">Viewing Controls</p>
<div class="flex items-center space-x-4">
<button id="mute-btn" class="p-3 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors shadow-md flex-shrink-0">
<!-- Mute Icon -->
<svg id="mute-icon" class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm-5 8h4v2H5V10zM11 6h4v2h-4V6zm0 4h4v2h-4v-2zm-6 4h4v2H5v-2z"></path></svg>
</button>
<input type="range" id="volume-slider" min="0" max="100" value="100" class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer range-lg">
</div>
</div>
<!-- Admin Controls (Visible only to the designated Admin) -->
<div id="admin-controls" class="space-y-4 hidden">
<!-- Live Stream Control Buttons (Go Live/Stop) -->
<div class="p-3 bg-gray-800 rounded-xl shadow-lg">
<p class="text-sm font-semibold text-gray-300 mb-3 text-center">Admin Live Controls:</p>
<div class="flex flex-col md:flex-row space-y-3 md:space-y-0 md:space-x-4">
<button id="start-btn" class="w-full bg-indigo-600 text-white p-3 rounded-lg font-semibold hover:bg-indigo-700 transition-colors duration-200 shadow-md disabled:opacity-50" disabled>
Go Live
</button>
<button id="switch-camera-btn" class="w-full md:w-1/2 bg-gray-600 text-white p-3 rounded-lg font-semibold hover:bg-gray-700 transition-colors duration-200 shadow-md hidden disabled:opacity-50" disabled>
Switch Camera
</button>
<button id="stop-btn" class="w-full md:w-1/2 bg-red-600 text-white p-3 rounded-lg font-semibold hover:bg-red-700 transition-colors duration-200 shadow-md hidden disabled:opacity-50" disabled>
Stop Stream
</button>
</div>
</div>
<!-- Recording Controls -->
<div class="p-3 bg-gray-800 rounded-xl shadow-lg" id="recording-panel">
<p class="text-sm font-semibold text-gray-300 mb-3 text-center">Recording (Only when Live):</p>
<div class="flex justify-center space-x-3">
<button id="record-btn" class="p-2 w-1/2 rounded-lg bg-green-600 text-white text-sm font-medium hover:bg-green-700 transition-colors shadow-md disabled:opacity-50" disabled>
<span id="record-icon">
<svg class="w-5 h-5 inline-block mr-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"></path></svg>
</span>
<span id="record-text">Start Recording</span>
</button>
<button id="stop-record-btn" class="p-2 w-1/2 rounded-lg bg-red-800 text-white text-sm font-medium hover:bg-red-900 transition-colors shadow-md hidden disabled:opacity-50" disabled>
<svg class="w-5 h-5 inline-block mr-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9 9a1 1 0 011-1h1a1 1 0 110 2h-1a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>
Stop & Download
</button>
</div>
</div>
<!-- Aspect Ratio Controls -->
<div class="p-3 bg-gray-800 rounded-xl shadow-lg">
<p class="text-sm font-semibold text-gray-300 mb-3 text-center">Aspect Ratio (Requires Restart):</p>
<div id="ratio-controls" class="flex justify-center space-x-3">
<button data-ratio="16/9" data-class="aspect-16-9" class="ratio-btn p-2 rounded-lg bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-700 transition-colors shadow-md disabled:opacity-50">16:9</button>
<button data-ratio="4/3" data-class="aspect-4-3" class="ratio-btn p-2 rounded-lg bg-gray-700 text-white text-sm font-medium hover:bg-indigo-500 transition-colors shadow-md disabled:opacity-50">4:3</button>
<button data-ratio="1/1" data-class="aspect-1-1" class="ratio-btn p-2 rounded-lg bg-gray-700 text-white text-sm font-medium hover:bg-indigo-500 transition-colors shadow-md disabled:opacity-50">1:1</button>
</div>
</div>
</div>
</div>
<script type="module">
// Combined Imports for the module scope
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, doc, setDoc, onSnapshot } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
document.addEventListener('DOMContentLoaded', () => {
const videoFeed = document.getElementById('video-feed');
const streamContainer = document.getElementById('stream-container');
const startBtn = document.getElementById('start-btn');
const stopBtn = document.getElementById('stop-btn');
const switchCameraBtn = document.getElementById('switch-camera-btn');
const ratioButtons = document.querySelectorAll('.ratio-btn');
const statusOverlay = document.getElementById('status-overlay');
const statusText = document.getElementById('status-text');
const statusIcon = document.getElementById('status-icon');
const errorDetails = document.getElementById('error-details');
const userInfoElement = document.getElementById('user-info');
const roleStatusElement = document.getElementById('role-status');
const streamerStatusElement = document.getElementById('streamer-status');
// Role-specific control containers
const adminControls = document.getElementById('admin-controls');
const viewerControls = document.getElementById('viewer-controls');
// Admin Login Elements
const adminLoginPanel = document.getElementById('admin-login-panel');
const adminIdInput = document.getElementById('admin-id-input');
const adminCheckBtn = document.getElementById('admin-check-btn');
const loginMessage = document.getElementById('login-message');
// Admin Recording elements
const recordBtn = document.getElementById('record-btn');
const stopRecordBtn = document.getElementById('stop-record-btn');
const recordIcon = document.getElementById('record-icon');
const recordText = document.getElementById('record-text');
// Viewer elements
const muteBtn = document.getElementById('mute-btn');
const muteIcon = document.getElementById('mute-icon');
const volumeSlider = document.getElementById('volume-slider');
// --- Configuration & Global State ---
/**
* The hardcoded Admin Passcode/ID as requested by the user.
*/
const HARDCODED_ADMIN_ID = "45544554";
let app;
let db;
let auth;
let currentUserId = null; // Firebase UID, used as the unique streamer identifier in Firestore
let isLocallyElevatedAdmin = false; // Flag set if HARDCODED_ADMIN_ID is entered
let currentStream = null;
let isStreaming = false;
let isViewer = false;
let currentStreamerId = null; // The Firebase UID of the active streamer
let currentFacingMode = 'user';
let currentAspectRatio = 16 / 9;
let currentAspectRatioClass = 'aspect-16-9';
// Recording state
let mediaRecorder = null;
let recordedChunks = [];
const mimeType = 'video/webm;codecs=vp8,opus';
// --- Utility Functions ---
function updateStatus(message, isError = false, details = '') {
statusOverlay.classList.remove('hidden');
statusText.textContent = message;
// Only show spin icon if not an error and we aren't waiting for a stream to start
statusIcon.classList.toggle('hidden', isError || isViewer);
statusIcon.classList.toggle('animate-spin', !isError && !isViewer);
statusText.classList.toggle('text-white', !isError);
statusText.classList.toggle('text-red-400', isError);
if (isError && details) {
errorDetails.textContent = details;
errorDetails.classList.remove('hidden');
} else {
errorDetails.classList.add('hidden');
}
if (!isViewer && videoFeed.srcObject) {
videoFeed.srcObject = null;
}
}
function hideStatus() {
statusOverlay.classList.add('opacity-0');
setTimeout(() => {
statusOverlay.classList.add('hidden');
statusOverlay.classList.remove('opacity-0');
}, 300);
}
function renderRole() {
// Set Admin/Viewer status based on the *entered* ID, not the Firebase UID
if (isLocallyElevatedAdmin) {
roleStatusElement.textContent = "Role: Admin (Controls Unlocked)";
roleStatusElement.classList.add('bg-purple-800', 'text-purple-300');
roleStatusElement.classList.remove('bg-indigo-900');
adminLoginPanel.classList.add('hidden');
} else {
roleStatusElement.textContent = "Role: Viewer (Default)";
roleStatusElement.classList.add('bg-indigo-900', 'text-indigo-300');
roleStatusElement.classList.remove('bg-purple-800');
adminLoginPanel.classList.remove('hidden');
}
}
function setControlState(state) {
// state: 'OFFLINE', 'STREAMING', 'VIEWING', 'LOADING'
// 1. Manage Admin Controls Visibility - ONLY visible if locally elevated
adminControls.classList.toggle('hidden', !isLocallyElevatedAdmin);
// 2. Manage Viewer Controls Visibility
viewerControls.classList.toggle('hidden', state !== 'VIEWING');
// 3. Manage Admin Streamer Control buttons (Only relevant if Admin)
if (isLocallyElevatedAdmin) {
const enableStreamerControls = state === 'STREAMING';
const enableOfflineControls = state === 'OFFLINE';
const disableAllControls = state === 'LOADING';
startBtn.disabled = disableAllControls || enableStreamerControls;
startBtn.classList.toggle('hidden', state === 'STREAMING');
stopBtn.disabled = !enableStreamerControls;
stopBtn.classList.toggle('hidden', state !== 'STREAMING');
switchCameraBtn.disabled = !enableStreamerControls;
switchCameraBtn.classList.toggle('hidden', state !== 'STREAMING');
recordBtn.disabled = !enableStreamerControls;
ratioButtons.forEach(btn => btn.disabled = !enableOfflineControls);
}
}
// --- Admin ID Check Logic ---
function checkAdminId() {
const enteredId = adminIdInput.value.trim();
loginMessage.classList.remove('hidden', 'text-red-400', 'text-green-400');
loginMessage.textContent = '';
if (enteredId === HARDCODED_ADMIN_ID) {
isLocallyElevatedAdmin = true;
loginMessage.textContent = 'Admin Access Granted! Controls Unlocked.';
loginMessage.classList.add('text-green-400');
renderRole();
// Set initial state after successful login
setControlState(isStreaming ? 'STREAMING' : 'OFFLINE');
updateStatus(isStreaming ? 'Admin: You are currently streaming.' : 'Admin: Ready to Go Live.');
} else {
isLocallyElevatedAdmin = false;
loginMessage.textContent = 'Access Denied. Incorrect ID.';
loginMessage.classList.add('text-red-400');
renderRole();
setControlState('OFFLINE');
}
}
// --- Firebase Initialization and Auth ---
async function initFirebase() {
try {
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const firebaseConfig = JSON.parse(typeof __firebase_config !== 'undefined' ? __firebase_config : '{}');
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
if (!firebaseConfig.apiKey) {
throw new Error("Firebase configuration is missing or invalid.");
}
app = initializeApp(firebaseConfig);
db = getFirestore(app);
auth = getAuth(app);
if (initialAuthToken) {
await signInWithCustomToken(auth, initialAuthToken);
} else {
await signInAnonymously(auth);
}
onAuthStateChanged(auth, (user) => {
if (user) {
currentUserId = user.uid; // Store Firebase UID for data path use
userInfoElement.textContent = `Your session ID: ${currentUserId.substring(0, 8)}... (Used as Streamer ID in database)`;
renderRole();
startStreamListener(appId);
setControlState('OFFLINE');
updateStatus('Enter Admin Passcode to enable streaming controls.');
} else {
currentUserId = null;
updateStatus('Authentication failed. Please refresh.', true);
}
});
} catch (error) {
console.error("Firebase Initialization Error:", error);
updateStatus('Firebase Error.', true, `Could not initialize multi-user features: ${error.message}`);
setControlState('LOADING');
}
}
// --- Firestore Stream Signaling ---
function getStreamStatusDocRef(appId) {
// Public data path: /artifacts/{appId}/public/data/live_stream_status/current_stream
return doc(db, `artifacts/${appId}/public/data/live_stream_status`, 'current_stream');
}
function startStreamListener(appId) {
const streamRef = getStreamStatusDocRef(appId);
onSnapshot(streamRef, (docSnap) => {
const data = docSnap.exists() ? docSnap.data() : { streamerId: null };
currentStreamerId = data.streamerId;
streamerStatusElement.classList.remove('bg-green-700', 'bg-red-700', 'bg-indigo-900', 'text-white', 'text-indigo-300');
if (currentStreamerId && currentStreamerId === currentUserId && isLocallyElevatedAdmin) {
// THIS USER IS THE STREAMER (Admin)
isStreaming = true;
isViewer = false;
streamerStatusElement.textContent = "Stream Status: YOU ARE LIVE!";
streamerStatusElement.classList.add('bg-green-700', 'text-white');
setControlState('STREAMING');
if (videoFeed.srcObject) hideStatus();
} else if (currentStreamerId) {
// THIS USER IS A VIEWER
isViewer = true;
isStreaming = false;
// Stop local camera if it was running
if (currentStream) stopLocalStream();
streamerStatusElement.textContent = `Stream Status: LIVE by Streamer ${currentStreamerId.substring(0, 8)}...`;
streamerStatusElement.classList.add('bg-red-700', 'text-white');
// Update video area to show viewing status (simulate receiving stream)
updateStatus(`Live Stream is active. Watching ${currentStreamerId.substring(0, 8)}...`);
statusIcon.classList.add('hidden');
statusOverlay.style.backgroundImage = 'radial-gradient(circle, rgba(29, 78, 216, 0.5) 0%, rgba(15, 23, 42, 0.9) 100%)';
setControlState('VIEWING');
} else {
// NO ACTIVE STREAMER
isStreaming = false;
isViewer = false;
// Stop local stream if it was running
if (currentStream) stopLocalStream();
streamerStatusElement.textContent = "Stream Status: Offline";
streamerStatusElement.classList.add('bg-gray-700', 'text-gray-300');
statusOverlay.style.backgroundImage = 'none';
setControlState('OFFLINE');
if (isLocallyElevatedAdmin) {
updateStatus('Admin: Ready to Go Live.');
} else {
updateStatus('Enter Admin Passcode to enable streaming controls.');
}
}
});
}
async function goLive() {
if (!isLocallyElevatedAdmin || isViewer) return;
// 1. Start the local camera stream
await startLocalStream();
// 2. Announce the stream in Firestore using the Firebase UID as the identifier
const streamRef = getStreamStatusDocRef(appId);
await setDoc(streamRef, {
streamerId: currentUserId, // Use the Firebase UID here
isLive: true,
});
}
async function stopLive() {
if (!isLocallyElevatedAdmin || !isStreaming) return;
// 1. Announce stream is off in Firestore
const streamRef = getStreamStatusDocRef(appId);
await setDoc(streamRef, {
streamerId: null, // Clear the streamer ID
isLive: false,
});
// 2. Stop local camera stream (the listener handles setting OFFLINE state)
stopLocalStream();
}
// --- Local Stream Control Logic (Unchanged) ---
async function startLocalStream() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
updateStatus('Error: Media devices not supported.', true);
return;
}
updateStatus(`Requesting Camera at ${currentAspectRatio.toFixed(2)} ratio...`);
setControlState('LOADING');
if (currentStream) stopLocalStream();
streamContainer.className = 'stream-container ' + currentAspectRatioClass + ' rounded-xl shadow-2xl overflow-hidden relative border-4 border-indigo-500 mb-8';
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: currentFacingMode,
aspectRatio: currentAspectRatio,
width: { ideal: 1280 },
height: { ideal: 720 }
},
audio: true
});
currentStream = stream;
videoFeed.srcObject = stream;
videoFeed.muted = true; // Streamer must always be locally muted
await videoFeed.play();
hideStatus();
} catch (err) {
let errorMessage = 'An unknown error occurred while trying to access the camera.';
let errorHint = 'Ensure your device has a camera and try refreshing the page.';
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
errorMessage = 'Access Denied.';
errorHint = 'You must grant camera/microphone permission.';
} else if (err.name === 'NotFoundError' || err.name === 'OverconstrainedError') {
errorMessage = 'No Camera Found or Ratio Not Supported.';
errorHint = `No suitable camera device found.`;
} else if (err.name === 'SecurityError') {
errorMessage = 'Security Error: HTTPS Required.';
errorHint = 'This feature only works on secure connections (HTTPS) or localhost.';
}
updateStatus(errorMessage, true, errorHint);
console.error('getUserMedia Error:', err);
setControlState('OFFLINE');
}
}
function stopLocalStream() {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
if (currentStream) {
currentStream.getTracks().forEach(track => track.stop());
currentStream = null;
videoFeed.srcObject = null;
}
}
function switchCamera() {
if (!isStreaming) return;
currentFacingMode = (currentFacingMode === 'user' ? 'environment' : 'user');
setControlState('LOADING');
startLocalStream();
}
function handleRatioSwitch(event) {
if (isStreaming) return;
const ratioStr = event.currentTarget.getAttribute('data-ratio');
const ratioClass = event.currentTarget.getAttribute('data-class');
currentAspectRatio = eval(ratioStr);
currentAspectRatioClass = ratioClass;
streamContainer.className = 'stream-container ' + currentAspectRatioClass + ' rounded-xl shadow-2xl overflow-hidden relative border-4 border-indigo-500 mb-8';
updateRatioButtonStyles();
}
function updateRatioButtonStyles() {
ratioButtons.forEach(btn => {
const btnRatio = btn.getAttribute('data-ratio');
const isActive = (eval(btnRatio) === currentAspectRatio);
if (isActive) {
btn.classList.add('bg-indigo-600', 'hover:bg-indigo-700');
btn.classList.remove('bg-gray-700', 'hover:bg-indigo-500');
} else {
btn.classList.add('bg-gray-700', 'hover:bg-indigo-500');
btn.classList.remove('bg-indigo-600', 'hover:bg-indigo-700');
}
});
}
// --- Viewer Controls Logic (Unchanged) ---
function toggleMute() {
const isMuted = videoFeed.muted;
videoFeed.muted = !isMuted;
updateMuteButtonUI(!isMuted);
}
function updateMuteButtonUI(isMuted) {
if (isMuted) {
muteIcon.innerHTML = `<path d="M10 2a8 8 0 100 16 8 8 0 000-16zm-5 8h4v2H5V10zM11 6h4v2h-4V6zm0 4h4v2h-4v-2zm-6 4h4v2H5v-2z"></path>`;
muteBtn.classList.remove('bg-green-600');
muteBtn.classList.add('bg-gray-500');
} else {
muteIcon.innerHTML = `<path fill-rule="evenodd" d="M3 10a1 1 0 011-1h2.586l3.243-3.243A1 1 0 0111 6v8a1 1 0 01-1.171.97l-3.243-3.243H4a1 1 0 01-1-1v-2zM15.5 8.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z" clip-rule="evenodd"></path><path d="M16.146 6.854a.5.5 0 010 .708L14.707 9.5l1.439 1.938a.5.5 0 01-.708.708l-1.47-1.96V11a.5.5 0 01-1 0v-.586l-1.439 1.938a.5.5 0 01-.708-.708L13.293 9.5l-1.439-1.938a.5.5 0 01.708-.708L14 8.293V7a.5.5 0 011 0v1.586l1.146-1.532a.5.5 0 01.708 0z" fill-rule="evenodd"></path>`;
muteBtn.classList.add('bg-green-600');
muteBtn.classList.remove('bg-gray-500');
}
}
function setVolume() {
videoFeed.volume = volumeSlider.value / 100;
}
// --- Recording Logic (Only available to Admin) (Unchanged) ---
function startRecording() {
if (!currentStream || !isLocallyElevatedAdmin) return;
recordedChunks = [];
if (!window.MediaRecorder || !MediaRecorder.isTypeSupported(mimeType)) {
updateStatus('Error: Recording is not supported.', true, `Your browser does not support the required video/webm MIME type.`);
return;
}
try {
mediaRecorder = new MediaRecorder(currentStream, { mimeType: mimeType });
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `webcam-recording-${new Date().toISOString().replace(/[:.]/g, '-')}.webm`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Reset UI
recordBtn.classList.remove('hidden');
recordBtn.disabled = false;
stopRecordBtn.classList.add('hidden');
stopRecordBtn.disabled = true;
recordText.textContent = 'Start Recording';
recordIcon.innerHTML = `<svg class="w-5 h-5 inline-block mr-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"></path></svg>`;
if (isStreaming) hideStatus();
};
mediaRecorder.start(100);
// Update UI state
recordBtn.classList.add('hidden');
recordBtn.disabled = true;
stopRecordBtn.classList.remove('hidden');
stopRecordBtn.disabled = false;
recordText.textContent = 'Recording...';
recordIcon.innerHTML = `<span class="recording-pulse inline-block w-4 h-4 rounded-full mr-2"></span>`;
hideStatus();
} catch (e) {
updateStatus('Recording failed.', true, `Error during MediaRecorder initialization: ${e.message}`);
console.error('MediaRecorder init error:', e);
}
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
stopRecordBtn.disabled = true;
updateStatus('Processing recording for download...');
}
}
// --- Event Listeners and Initial Setup ---
// New Listener for Admin Login
adminCheckBtn.addEventListener('click', checkAdminId);
adminIdInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
checkAdminId();
}
});
// Admin Controls
startBtn.addEventListener('click', goLive);
stopBtn.addEventListener('click', stopLive);
switchCameraBtn.addEventListener('click', switchCamera);
recordBtn.addEventListener('click', startRecording);
stopRecordBtn.addEventListener('click', stopRecording);
// Admin/Global Controls
ratioButtons.forEach(button => {
button.addEventListener('click', handleRatioSwitch);
});
// Viewer Controls
muteBtn.addEventListener('click', toggleMute);
volumeSlider.addEventListener('input', setVolume);
// Initial setup
updateRatioButtonStyles();
setControlState('LOADING');
updateMuteButtonUI(true); // Start muted
initFirebase();
});
</script>
</body>
</html>