CameraFeed: recover from HLS blips, retry video instead of sticking on the still
Any fatal hls.js error set videoFailed permanently, so a brief 511NY feed drop left the tile on a 60s snapshot until it was reopened. Now fatal media/network errors get up to 3 in-place recoveries, and a tile that did fall back retries video every 30s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e2364096f1
commit
fb299008f8
@@ -6,11 +6,17 @@ import type { Ny511Camera } from "@/lib/types";
|
|||||||
|
|
||||||
// 511NY stills are served with max-age=60, so refreshing faster buys nothing.
|
// 511NY stills are served with max-age=60, so refreshing faster buys nothing.
|
||||||
const SNAPSHOT_REFRESH_MS = 60_000;
|
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
|
* 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
|
* 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 }) {
|
export function CameraFeed({ camera, className }: { camera: Ny511Camera; className?: string }) {
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
@@ -24,6 +30,12 @@ export function CameraFeed({ camera, className }: { camera: Ny511Camera; classNa
|
|||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [useVideo]);
|
}, [useVideo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoFailed || !camera.video_url) return;
|
||||||
|
const id = setTimeout(() => setVideoFailed(false), VIDEO_RETRY_MS);
|
||||||
|
return () => clearTimeout(id);
|
||||||
|
}, [videoFailed, camera.video_url]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
const src = camera.video_url;
|
const src = camera.video_url;
|
||||||
@@ -39,7 +51,15 @@ export function CameraFeed({ camera, className }: { camera: Ny511Camera; classNa
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (!Hls.isSupported()) { setVideoFailed(true); return; }
|
if (!Hls.isSupported()) { setVideoFailed(true); return; }
|
||||||
hls = new Hls({ maxBufferLength: 10, backBufferLength: 0 });
|
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.loadSource(src);
|
||||||
hls.attachMedia(video);
|
hls.attachMedia(video);
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user