diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx index e951fef..95c9b14 100644 --- a/services/web-ui/public/screens-ingest.jsx +++ b/services/web-ui/public/screens-ingest.jsx @@ -387,23 +387,61 @@ function HlsPreview({ assetId, muted = true, controls = false, className }) { if (!assetId || !videoRef.current) return; const url = '/live/' + assetId + '/index.m3u8'; const v = videoRef.current; + let destroyed = false; + let retryTimer = 0; + let retryCount = 0; + const MAX_RETRIES = 8; + + const clearRetry = () => { if (retryTimer) { clearTimeout(retryTimer); retryTimer = 0; } }; // Safari can play HLS natively; everything else needs hls.js. if (v.canPlayType('application/vnd.apple.mpegurl')) { - v.src = url; - const onErr = () => setErr('playback failed'); + const tryLoad = () => { + if (destroyed) return; + v.removeAttribute('src'); + v.load(); + v.src = url; + v.play().catch(() => {}); + }; + const onErr = () => { + if (destroyed || retryCount >= MAX_RETRIES) { setErr('playback failed'); return; } + retryCount++; + clearRetry(); + retryTimer = setTimeout(tryLoad, Math.min(500 * Math.pow(2, retryCount - 1), 8000)); + setErr('connecting…'); + }; v.addEventListener('error', onErr); - return () => v.removeEventListener('error', onErr); + v.addEventListener('playing', () => { retryCount = 0; setErr(null); }, { once: false }); + tryLoad(); + return () => { destroyed = true; clearRetry(); v.removeEventListener('error', onErr); }; } if (!window.Hls) { setErr('hls.js missing'); return; } - const hls = new window.Hls({ liveSyncDurationCount: 2, lowLatencyMode: true }); - hls.loadSource(url); - hls.attachMedia(v); - hls.on(window.Hls.Events.ERROR, (_e, data) => { - if (data.fatal) setErr(data.details || 'hls error'); - }); - return () => { try { hls.destroy(); } catch (_) {} }; + let hls = null; + + const startHls = () => { + if (destroyed) return; + hls = new window.Hls({ liveSyncDurationCount: 2, lowLatencyMode: true }); + hls.loadSource(url); + hls.attachMedia(v); + hls.on(window.Hls.Events.ERROR, (_e, data) => { + if (data.fatal) { + if (retryCount >= MAX_RETRIES) { setErr(data.details || 'hls error'); return; } + retryCount++; + clearRetry(); + try { hls.destroy(); } catch (_) {} + hls = null; + setErr('connecting…'); + retryTimer = setTimeout(startHls, Math.min(500 * Math.pow(2, retryCount - 1), 8000)); + } + }); + hls.on(window.Hls.Events.MANIFEST_PARSED, () => { retryCount = 0; setErr(null); }); + }; + + startHls(); + v.play().catch(() => {}); + + return () => { destroyed = true; clearRetry(); if (hls) { try { hls.destroy(); } catch (_) {} } }; }, [assetId]); return (