Files
obs-game-stream-plugin/frontend/js/app.js
T
bhetherman 180e95f74d Fix audio, routing, auth, and stream lifecycle
- Switch OBS output to RTMP; add FFmpeg AAC->Opus transcoding via MediaMTX
  runOnReady so WebRTC can carry audio (WebRTC requires Opus, not AAC)
- Enable RTSP on localhost so FFmpeg reads game path without publisher conflict;
  viewers connect to game-opus path (H264+Opus)
- Fix WHEP/HLS path prefix stripping in NPM advanced config; move all custom
  locations (/whep, /hls, /v3) out of NPM GUI and into advanced conf so
  trailing-slash proxy_pass correctly strips prefixes before hitting MediaMTX
- Fix MediaMTX API port 49997->19997 (49997 was in Windows ephemeral range)
- Add /status proxy endpoint to OBS HTTP server so frontend can poll stream
  readiness without hitting /v3/ through NPM where auth_request blocked it
- Fix authInternalUsers: split publish (localhost only) from read (any IP)
  so WHEP viewers are not challenged with Basic Auth by MediaMTX
- Remove muted attribute from video element; show unmute/play button on
  autoplay block so viewers get audio after one click
- Fix webrtcAdditionalHosts to include LAN IP 192.168.50.254
- Fix hlsAllowOrigin->hlsAllowOrigins deprecation warning
- Move MediaMTX/HTTP server startup to script_load (not streaming started)
  so MediaMTX is ready before OBS attempts RTMP connection
- Log MediaMTX output to bin/mediamtx.log for easier debugging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 03:42:38 -04:00

165 lines
5.8 KiB
JavaScript

/*
* 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);
})();