initial commit

This commit is contained in:
2026-04-05 17:16:51 -04:00
commit 6bc7cf6318
14 changed files with 1829 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
/*
* 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 () {
const PATH_NAME = 'game';
const API_URL = `/v3/paths/get/${PATH_NAME}`;
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 button --------------------------------------------------
//
// Browsers require a user gesture to play audio. The <video> element
// starts muted so autoplay works; we surface a button to unmute once
// playback begins.
els.unmuteBtn.addEventListener('click', async () => {
try {
els.video.muted = false;
await els.video.play();
els.unmuteBtn.hidden = true;
} catch (err) {
console.warn('Unmute failed:', err);
}
});
function maybeShowUnmute() {
els.unmuteBtn.hidden = !els.video.muted;
}
// ---- 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);
})();