Phase 7+11: token-based publishing — MediaMTX authMethod http -> dumpd /mtx-auth (bridge-only port 8081); dumpd mints 12 h HMAC tokens for Discord-authenticated streamers; dock rewritten (no passwords, naming rules with live preview, prefix/suffix, manual-setup accordion with hidden token); portal uses tokens; configure.py drops streamers; MediaMTX hot-reload, Caddy reload — no stream interruption

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 15:22:15 -05:00
parent 2a74cb7b40
commit dcc347e1ea
11 changed files with 301 additions and 144 deletions

View File

@@ -65,8 +65,9 @@ export default function GoLive({ onExit }) {
<video ref={videoRef} autoPlay playsInline muted className={phase === 'live' || phase === 'capturing' ? 'show' : ''} />
{phase !== 'live' && (
<div className="golive-form">
<label>Stream title <small>(becomes the folder and the Jellyfin title)</small></label>
<label>Stream title <small>(becomes the Jellyfin title; letters, digits, _ . - ; no double underscore)</small></label>
<input value={title} onChange={e => setTitle(e.target.value)} disabled={phase === 'capturing'} />
<div className="hint" style={{marginTop: '-4px'}}>stream name: <b>{key ? key.user : '<you>'}/{cleanTitle(title) || '—'}</b></div>
<div className="hint">
Click <b>Start</b>, then pick the <b>WarDogs</b> window in Chrome's dialog and tick <b>Share audio</b>.
<b>Windows</b> with <b>Chrome or Edge 141 or newer</b> is the supported setup: window audio and the GPU

View File

@@ -7,7 +7,8 @@ export function defaultTitle(now = new Date()) {
const d = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 10)
return `HelmetCam-${d}`
}
export const cleanTitle = t => t.trim().replace(/\s+/g, '-').replace(/[^A-Za-z0-9_.-]/g, '')
// Must match the dock and the server: the sweep publishes <Title>__<timestamp>.mp4, so "__" is reserved.
export const cleanTitle = t => t.trim().replace(/\s+/g, '-').replace(/[^A-Za-z0-9_.-]/g, '').replace(/_{2,}/g, '_').replace(/^[.-]+/, '').slice(0, 64)
export async function captureWindow() {
// Window-only picker with that window's own audio. Chrome/Edge 141+ honour windowAudio: "window"
@@ -105,7 +106,8 @@ export async function publish({ stream, path, user, pass, onState }) {
}
export async function fetchStreamKey() {
const r = await fetch('/me/stream-key', { credentials: 'same-origin' })
// a fresh 12 h publish token for the logged-in Discord user (member of streamers); nothing is stored
const r = await fetch('/me/stream-token', { credentials: 'same-origin' })
const body = await r.json().catch(() => ({}))
if (!r.ok) throw new Error(body.error || `stream key ${r.status}`)
return body