From 9a7ee7a725cf0c8f484c6fe792a42ddf4e005303 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Mon, 14 Sep 2026 07:13:31 -0700 Subject: [PATCH] player: HLS keeps asking for a playlist that isn't there yet (and recovers media errors) instead of giving up, so a viewer waiting on a page starts when the stream does Claude-Session: https://claude.ai/code/session_014aPQ5yqG9QFxKnbwQfCYKa (cherry picked from commit 1337600d5e5253c97674fbe0262f0489edbaa4bd) --- .../src/components/mobile-player/video.tsx | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/js/components/src/components/mobile-player/video.tsx b/js/components/src/components/mobile-player/video.tsx index ae85b39cc..2b6297ca0 100644 --- a/js/components/src/components/mobile-player/video.tsx +++ b/js/components/src/components/mobile-player/video.tsx @@ -396,20 +396,58 @@ export function HLSPlayer(props: VideoProps) { l.height > 0 ? `${l.height}p` : `${Math.round(l.bitrate / 1000)}k`; setPlayingVODRendition(name); }); + // Before a stream is live its playlist is a 404, and a live window + // can briefly have nothing to serve; hls.js treats both as fatal and + // stops. Keep asking (every couple of seconds) until the stream is + // there, so a viewer waiting on the page starts playing when it + // begins — the same "pre-live" wait WebRTC gets from its reconnects. + let retry: ReturnType | null = null; + hls.on(Hls.Events.ERROR, (_, data) => { + if (!data.fatal || hlsRef.current !== hls) return; + if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { + if (retry) clearTimeout(retry); + retry = setTimeout(() => { + retry = null; + if (hlsRef.current !== hls) return; + hls.loadSource(props.url); + hls.startLoad(); + }, 2000); + } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { + hls.recoverMediaError(); + } + }); return () => { + if (retry) clearTimeout(retry); hls.stopLoad(); + hls.destroy(); hlsRef.current = null; setVodLevels([]); setPlayingVODRendition(null); }; } else if (localRef.current.canPlayType("application/vnd.apple.mpegurl")) { - localRef.current.src = props.url; - localRef.current.addEventListener("canplay", () => { - if (!localRef.current) { - return; - } - localRef.current.play(); - }); + // Native HLS (iPhone Safari): the element gives up on a 404 too, so + // re-point it at the playlist until it plays. + const video = localRef.current; + video.src = props.url; + const onCanPlay = () => { + video.play(); + }; + let retry: ReturnType | null = null; + const onError = () => { + if (retry) clearTimeout(retry); + retry = setTimeout(() => { + retry = null; + video.src = props.url; + video.load(); + }, 2000); + }; + video.addEventListener("canplay", onCanPlay); + video.addEventListener("error", onError); + return () => { + if (retry) clearTimeout(retry); + video.removeEventListener("canplay", onCanPlay); + video.removeEventListener("error", onError); + }; } }, [props.url]); -- 2.51.2