import { useEffect, useRef, useState } from "react"; import mpegts from "mpegts.js"; interface Props { live: boolean; streamKey: string; } const MAX_RETRIES = 8; export default function Player({ live, streamKey }: Props) { const videoRef = useRef(null); const playerRef = useRef(null); const retryTimerRef = useRef | null>(null); const stallTimerRef = useRef | null>(null); const retryCountRef = useRef(0); const lastProgressRef = useRef(0); const [playing, setPlaying] = useState(false); useEffect(() => { const video = videoRef.current; if (!video) return; const clearTimers = () => { if (retryTimerRef.current) { clearTimeout(retryTimerRef.current); retryTimerRef.current = null; } if (stallTimerRef.current) { clearInterval(stallTimerRef.current); stallTimerRef.current = null; } }; const destroyPlayer = () => { if (playerRef.current) { try { playerRef.current.pause(); } catch { /* noop */ } try { playerRef.current.unload(); } catch { /* noop */ } try { playerRef.current.detachMediaElement(); } catch { /* noop */ } try { playerRef.current.destroy(); } catch { /* noop */ } playerRef.current = null; } }; // Tear down when offline if (!live) { clearTimers(); destroyPlayer(); try { video.removeAttribute("src"); video.load(); } catch { /* noop */ } setPlaying(false); return; } if (!mpegts.isSupported()) { setPlaying(false); return; } const src = `${window.location.origin}/ts?v=${encodeURIComponent(streamKey)}`; const scheduleRetry = () => { if (retryCountRef.current >= MAX_RETRIES) { setPlaying(false); return; } retryCountRef.current++; const delay = Math.min(2000 * retryCountRef.current, 10000); setPlaying(false); retryTimerRef.current = setTimeout(start, delay); }; const startStallWatchdog = () => { if (stallTimerRef.current) clearInterval(stallTimerRef.current); lastProgressRef.current = Date.now(); const bump = () => { lastProgressRef.current = Date.now(); }; video.addEventListener("timeupdate", bump); video.addEventListener("progress", bump); stallTimerRef.current = setInterval(() => { if (Date.now() - lastProgressRef.current > 30000) { lastProgressRef.current = Date.now(); start(); } }, 5000); }; function start() { destroyPlayer(); // Config mirrors webplayer.online (proven on this exact bursty upstream): // a deep 30-90s live buffer absorbs the upstream's multi-second delivery // gaps, while gentle 1.1x catch-up keeps latency bounded. const player = mpegts.createPlayer( { type: "mpegts", isLive: true, url: src }, { enableWorker: true, enableStashBuffer: true, stashInitialSize: 8 * 1024 * 1024, liveSync: true, liveSyncMaxLatency: 60, liveSyncTargetLatency: 45, liveSyncPlaybackRate: 1.1, liveBufferLatencyChasing: true, liveBufferLatencyMaxLatency: 90, liveBufferLatencyMinRemain: 30, autoCleanupSourceBuffer: true, autoCleanupMaxBackwardDuration: 120, autoCleanupMinBackwardDuration: 90, fixAudioTimestampGap: true, lazyLoad: false, } ); playerRef.current = player; player.attachMediaElement(video!); player.on(mpegts.Events.ERROR, () => { scheduleRetry(); }); player.on(mpegts.Events.LOADING_COMPLETE, () => { scheduleRetry(); }); player.on(mpegts.Events.MEDIA_INFO, () => { setPlaying(true); }); player.load(); video!.play().catch(() => { /* autoplay handled via muted attribute */ }); startStallWatchdog(); } retryCountRef.current = 0; start(); return () => { clearTimers(); destroyPlayer(); setPlaying(false); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [live, streamKey]); return (
); }