Go Live: request window-scoped audio (windowAudio: window, Chrome 141+); keep the picker window-only

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 01:44:45 -05:00
parent f3388f52bc
commit b634413773
4 changed files with 9 additions and 8 deletions

View File

@@ -205,7 +205,7 @@ asked by `configure.py --full`) can post when a stream starts.
embedded outpost → group policy (e.g. everyone in the server, or `streamers`+`editors`+`admins`). embedded outpost → group policy (e.g. everyone in the server, or `streamers`+`editors`+`admins`).
3. Dev box: `./deployLive.sh` builds `live/` and copies `dist/` to `/srv/live` (Caddy serves it directly). 3. Dev box: `./deployLive.sh` builds `live/` and copies `dist/` to `/srv/live` (Caddy serves it directly).
**Go Live** button: broadcast a window from the browser (no OBS) as `<user>/HelmetCam-<date>` — key comes **Go Live** button (Chrome/Edge 141+ for window audio): broadcast a window from the browser (no OBS) as `<user>/HelmetCam-<date>` — key comes
from `/me/stream-key` (dumpd reads `/etc/vanguard/streamers.json`, identity from the Authentik header), so a from `/me/stream-key` (dumpd reads `/etc/vanguard/streamers.json`, identity from the Authentik header), so a
streamer's Authentik username must equal their MediaMTX name. streamer's Authentik username must equal their MediaMTX name.

View File

@@ -22,6 +22,6 @@ Toolbar → **Go Live**: closes all viewer windows, fetches the logged-in user's
`/me/stream-key` (dumpd, identity from Authentik's header), opens Chrome's window picker (windows only, `/me/stream-key` (dumpd, identity from Authentik's header), opens Chrome's window picker (windows only,
audio requested), and publishes over WebRTC/WHIP to `<user>/HelmetCam-<date>` at ≤12 Mbps, H.264, up to audio requested), and publishes over WebRTC/WHIP to `<user>/HelmetCam-<date>` at ≤12 Mbps, H.264, up to
1440p60. Recording, the "is live" post, the sweep and Jellyfin follow exactly as for an OBS stream. 1440p60. Recording, the "is live" post, the sweep and Jellyfin follow exactly as for an OBS stream.
Window audio: Chrome on Windows. Linux/Mac: pick a tab or the whole screen for sound. Window audio: Chrome/Edge 141+ (`windowAudio: "window"`); older browsers share the window without sound.
Requires the Authentik username to equal the MediaMTX streamer name (configure.py keeps them in sync via Requires the Authentik username to equal the MediaMTX streamer name (configure.py keeps them in sync via
`/etc/vanguard/streamers.json`). `/etc/vanguard/streamers.json`).

View File

@@ -66,8 +66,8 @@ export default function GoLive({ onExit }) {
<input value={title} onChange={e => setTitle(e.target.value)} disabled={phase === 'capturing'} /> <input value={title} onChange={e => setTitle(e.target.value)} disabled={phase === 'capturing'} />
<div className="hint"> <div className="hint">
Click <b>Start</b>, then pick the <b>WarDogs</b> window in Chrome's dialog and tick <b>Share audio</b>. Click <b>Start</b>, then pick the <b>WarDogs</b> window in Chrome's dialog and tick <b>Share audio</b>.
Window audio works in Chrome on Windows; on Linux/Mac pick a tab or the whole screen for sound. Window audio needs <b>Chrome or Edge 141 or newer</b> (chrome://version). Older browsers capture the window
Capped at {MAX_KBPS / 1000} Mbps, up to 1440p60, H.264. without sound. Capped at {MAX_KBPS / 1000} Mbps, up to 1440p60, H.264.
</div> </div>
{err && <div className="err">{err}</div>} {err && <div className="err">{err}</div>}
<button className="primary" disabled={!key || phase === 'capturing'} onClick={start}>{phase === 'capturing' ? 'Waiting for the window picker…' : 'Start'}</button> <button className="primary" disabled={!key || phase === 'capturing'} onClick={start}>{phase === 'capturing' ? 'Waiting for the window picker…' : 'Start'}</button>

View File

@@ -10,14 +10,15 @@ export function defaultTitle(now = new Date()) {
export const cleanTitle = t => t.trim().replace(/\s+/g, '-').replace(/[^A-Za-z0-9_.-]/g, '') export const cleanTitle = t => t.trim().replace(/\s+/g, '-').replace(/[^A-Za-z0-9_.-]/g, '')
export async function captureWindow() { export async function captureWindow() {
// Window-only picker with audio. Chrome on Windows captures that window's audio; elsewhere the user // Window-only picker with that window's own audio. Chrome/Edge 141+ honour windowAudio: "window"
// should pick a tab or the whole screen to get sound. // (only the shared window's sound); older Chrome offers no audio for a window at all.
const constraints = { const constraints = {
video: { displaySurface: 'window', frameRate: { ideal: 60, max: 60 }, width: { max: 2560 }, height: { max: 1440 } }, video: { displaySurface: 'window', frameRate: { ideal: 60, max: 60 }, width: { max: 2560 }, height: { max: 1440 } },
audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false, sampleRate: 48000, channelCount: 2 }, audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false, sampleRate: 48000, channelCount: 2 },
systemAudio: 'include', windowAudio: 'window', // capture the picked window's audio (Chrome 141+)
systemAudio: 'exclude',
selfBrowserSurface: 'exclude', selfBrowserSurface: 'exclude',
monitorTypeSurfaces: 'exclude', // hide "Entire screen" so people pick the game window monitorTypeSurfaces: 'exclude', // hide "Entire screen": this is a single-window helmet cam
surfaceSwitching: 'include', surfaceSwitching: 'include',
preferCurrentTab: false, preferCurrentTab: false,
} }