diff --git a/drb-frontend/components/CameraFeed.tsx b/drb-frontend/components/CameraFeed.tsx index c53d23c..8a0d736 100644 --- a/drb-frontend/components/CameraFeed.tsx +++ b/drb-frontend/components/CameraFeed.tsx @@ -6,11 +6,17 @@ import type { Ny511Camera } from "@/lib/types"; // 511NY stills are served with max-age=60, so refreshing faster buys nothing. const SNAPSHOT_REFRESH_MS = 60_000; +// A failed stream is not given up on for good: 511NY feeds drop out briefly, +// and a permanent fallback left pinned tiles showing a stale still image. +const VIDEO_RETRY_MS = 30_000; +// In-place recoveries hls.js gets per session before we fall back to the snapshot. +const MAX_HLS_RECOVERIES = 3; /** * One 511NY camera: the live HLS stream when the camera has one (NYSDOT's * skyvdn hosts send Access-Control-Allow-Origin: *), otherwise its snapshot on - * a timer. A stream that fails falls back to the snapshot rather than a black box. + * a timer. A stream that fails falls back to the snapshot rather than a black box, + * and video is retried every VIDEO_RETRY_MS while it stays down. */ export function CameraFeed({ camera, className }: { camera: Ny511Camera; className?: string }) { const videoRef = useRef(null); @@ -24,6 +30,12 @@ export function CameraFeed({ camera, className }: { camera: Ny511Camera; classNa return () => clearInterval(id); }, [useVideo]); + useEffect(() => { + if (!videoFailed || !camera.video_url) return; + const id = setTimeout(() => setVideoFailed(false), VIDEO_RETRY_MS); + return () => clearTimeout(id); + }, [videoFailed, camera.video_url]); + useEffect(() => { const video = videoRef.current; const src = camera.video_url; @@ -39,7 +51,15 @@ export function CameraFeed({ camera, className }: { camera: Ny511Camera; classNa if (cancelled) return; if (!Hls.isSupported()) { setVideoFailed(true); return; } hls = new Hls({ maxBufferLength: 10, backBufferLength: 0 }); - hls.on(Hls.Events.ERROR, (_evt, data) => { if (data.fatal) setVideoFailed(true); }); + let recoveries = 0; + hls.on(Hls.Events.ERROR, (_evt, data) => { + if (!data.fatal || !hls) return; + if (recoveries++ < MAX_HLS_RECOVERIES) { + if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { hls.recoverMediaError(); return; } + if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { hls.startLoad(); return; } + } + setVideoFailed(true); + }); hls.loadSource(src); hls.attachMedia(video); })