initial commit
This commit is contained in:
@@ -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);
|
||||
})();
|
||||
Reference in New Issue
Block a user