/* * app.js - Glue between the DOM and StreamPlayer. * * - Polls the MediaMTX API (/v3/paths/get/game) to determine if the stream * is live, and updates the status bar accordingly. * - Starts/stops StreamPlayer based on that status. * - Wires up the "Click to unmute" button (browsers autoplay muted). * - Authentik forward auth is handled at the NPM layer, so by the time * this JS runs the user is already authenticated. No auth logic here. */ (function () { // /status is served by the OBS Python HTTP server, which proxies the // MediaMTX API internally. This avoids hitting /v3/ through NPM where // the server-level auth_request directive blocks it. const API_URL = '/status'; const POLL_INTERVAL_MS = 5000; const els = { video: document.getElementById('video'), overlay: document.getElementById('overlay'), overlayTitle: document.getElementById('overlay-title'), overlayMessage: document.getElementById('overlay-message'), overlayCard: document.querySelector('.overlay-card'), statusIndicator: document.getElementById('status-indicator'), viewerCount: document.getElementById('viewer-count'), latency: document.getElementById('latency'), transport: document.getElementById('transport'), unmuteBtn: document.getElementById('unmute-btn'), }; const state = { pathReady: false, playerState: 'offline', }; // ---- overlay helpers ------------------------------------------------- function showOverlay(title, message, isError) { els.overlayTitle.textContent = title; els.overlayMessage.textContent = message; els.overlayCard.classList.toggle('error', !!isError); els.overlay.hidden = false; } function hideOverlay() { els.overlay.hidden = true; } // ---- status bar helpers --------------------------------------------- function setStatusIndicator(label, cls) { els.statusIndicator.textContent = label; els.statusIndicator.className = 'status ' + cls; } function setViewerCount(n) { els.viewerCount.textContent = String(n); } function setLatency(ms) { els.latency.textContent = ms == null ? '--' : `${ms} ms`; } function setTransport(label) { els.transport.textContent = label || '--'; } // ---- MediaMTX API polling ------------------------------------------- async function pollPathStatus() { try { const res = await fetch(API_URL, { credentials: 'include' }); if (res.status === 404) { applyPathStatus(false, 0); return; } if (!res.ok) { // 401/403 means session expired - force reload so NPM can redirect to Authentik. if (res.status === 401 || res.status === 403) { window.location.reload(); } return; } const data = await res.json(); applyPathStatus(Boolean(data.ready), (data.readers || []).length); } catch (_) { applyPathStatus(false, 0); } } function applyPathStatus(ready, viewers) { setViewerCount(viewers); if (ready && !state.pathReady) { state.pathReady = true; setStatusIndicator('LIVE', 'live'); showOverlay('Connecting to stream…', 'Negotiating WebRTC', false); window.StreamPlayer.start(els.video); } else if (!ready && state.pathReady) { state.pathReady = false; setStatusIndicator('OFFLINE', 'offline'); setTransport('--'); setLatency(null); showOverlay('Stream offline', 'Waiting for the streamer to start…', false); window.StreamPlayer.stop(); } } // ---- Unmute / play button ------------------------------------------- // // Browsers block autoplay with audio until a user gesture occurs. // When the video fails to autoplay (or is muted by the browser), we // surface a "Click to play" button so the user can unblock it manually. els.unmuteBtn.addEventListener('click', async () => { try { els.video.muted = false; await els.video.play(); els.unmuteBtn.hidden = true; } catch (err) { console.warn('Unmute/play failed:', err); } }); function maybeShowUnmute() { // Show the button if the video is muted OR paused (autoplay blocked). const needsGesture = els.video.muted || els.video.paused; els.unmuteBtn.hidden = !needsGesture; if (needsGesture) { els.unmuteBtn.textContent = els.video.muted ? 'Click to unmute' : 'Click to play'; } } // ---- StreamPlayer event wiring -------------------------------------- window.StreamPlayer.on('statechange', (s) => { state.playerState = s; if (s === 'playing') { hideOverlay(); maybeShowUnmute(); } else if (s === 'connecting') { showOverlay('Connecting…', 'Negotiating transport', false); } else if (s === 'error') { showOverlay('Playback error', 'Retrying…', true); } else if (s === 'offline' && state.pathReady) { showOverlay('Reconnecting…', 'The stream dropped; trying again', false); } }); window.StreamPlayer.on('transport', (label) => setTransport(label)); window.StreamPlayer.on('latency', (ms) => setLatency(ms)); window.StreamPlayer.on('error', (err) => { console.warn('Stream error:', err); }); // ---- boot ----------------------------------------------------------- setStatusIndicator('OFFLINE', 'offline'); showOverlay('Stream offline', 'Waiting for the streamer to start…', false); pollPathStatus(); setInterval(pollPathStatus, POLL_INTERVAL_MS); })();