"use client"; import { useEffect, useRef, useState } from "react"; import type HlsType from "hls.js"; 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, * 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); const [videoFailed, setVideoFailed] = useState(false); const [tick, setTick] = useState(() => Date.now()); const useVideo = !!camera.video_url && !videoFailed; useEffect(() => { if (useVideo) return; const id = setInterval(() => setTick(Date.now()), SNAPSHOT_REFRESH_MS); 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; if (!useVideo || !video || !src) return; let hls: HlsType | null = null; let cancelled = false; if (video.canPlayType("application/vnd.apple.mpegurl")) { video.src = src; // Safari/iOS play HLS natively } else { import("hls.js") .then(({ default: Hls }) => { if (cancelled) return; if (!Hls.isSupported()) { setVideoFailed(true); return; } hls = new Hls({ maxBufferLength: 10, backBufferLength: 0 }); 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); }) .catch(() => setVideoFailed(true)); } return () => { cancelled = true; hls?.destroy(); video.removeAttribute("src"); video.load(); // stop the download, not just the playback }; }, [useVideo, camera.video_url]); if (useVideo) { return