From c39c565630f3a09b76da7a3f0a4d7a37945e0325 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 19 Oct 2026 00:44:32 -0700 Subject: Watching a stream: a stream that's on plays through hls.js, held near the live edge (1.5 s back, up to 1.1× when it drifts past 5 s) — Chrome's own HLS player starts three segments back, never catches up, and fails outright (DEMUXER_ERROR_COULD_NOT_PARSE) if a page changes its speed. With the live server's 1 s segments a viewer is 3.5 s behind the broadcaster, from 8.6 s (go-live-test LATENCY=1 measures it: the broadcaster's camera is a clock in black and white blocks, read off the viewer's frame). Recordings, and streams where hls.js can't run (iOS), stay on the browser's player. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg --- src/features/messages.js | 12 ++++++++++-- src/hls-player.js | 7 +++++-- tools/go-live-test.js | 47 +++++++++++++++++++++++++++++++++++++++++++++-- tools/live-stream-test.js | 3 ++- 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/features/messages.js b/src/features/messages.js index 0b6d8f0..13e328a 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -8903,8 +8903,16 @@ export function messagesFeature(ctx) { el.addEventListener('hlsfatal', fail); el.addEventListener('playing', () => { if (w.err) { w.err = null; scheduleRepaint(); } }); const go = () => el.play().catch(() => { el.muted = true; el.play().catch(() => {}); }); - if (!isHls(src) || canHls()) { el.src = src; go(); return; } - import('../hls-player.js').then((m) => m.attachHls(el, src)).then((hls) => { + // A stream that's on plays through hls.js wherever it runs: it keeps the + // viewer near the live edge (starts 1.5 s back, speeds up when it falls + // behind). Chrome's own HLS player starts three segments back, never + // catches up, and fails outright if a page changes its speed. Its player + // still takes recordings, and live streams where hls.js can't run (iOS). + // Measured (go-live-test LATENCY=1, one machine): 2 s segments on + // Chrome's player 8.6 s behind the broadcaster; 1 s segments 4.5 s. + const live = src === liveStream(ev) && isHls(src) && typeof MediaSource !== 'undefined'; + if (!isHls(src) || (canHls() && !live)) { el.src = src; go(); return; } + import('../hls-player.js').then((m) => m.attachHls(el, src, { live })).then((hls) => { if (w.video !== el) { try { hls && hls.destroy(); } catch {} return; } if (!hls) { w.err = 'noplayer'; scheduleRepaint(); return; } w.hls = hls; go(); diff --git a/src/hls-player.js b/src/hls-player.js index d0878e2..18292dd 100644 --- a/src/hls-player.js +++ b/src/hls-player.js @@ -2,10 +2,13 @@ //