import { useCallback, useEffect, useRef, useState, type SyntheticEvent } from 'react';
// MJPEG
connections can stop delivering frames without firing onError.
// Renew only the camera connection, keeping the overlay and its credentials.
// This also renews healthy connections; it does not measure frame freshness.
const STREAM_RENEWAL_MS = 60_000;
const ERROR_RETRY_MS = 3_000;
// The old stream is closed this long before the new one opens: a detached img
// keeps its connection open (measured in Chromium, which OBS browser sources
// run on), and a USB camera serves one reader.
const RENEWAL_GAP_MS = 200;
export function useOverlayCameraRecovery(enabled: boolean) {
const [imageKey, setImageKey] = useState(Date.now);
// While true the img renders with no src, so no re-render reopens it early.
const [closing, setClosing] = useState(false);
const imgRef = useRef(null);
const retryTimer = useRef | null>(null);
const close = useCallback(() => {
// Closed now, not at the next render, which may already be the reopen.
imgRef.current?.removeAttribute('src');
setClosing(true);
}, []);
const renew = useCallback(() => {
if (retryTimer.current !== null) {
clearTimeout(retryTimer.current);
retryTimer.current = null;
}
setClosing(false);
setImageKey((key) => key + 1);
}, []);
useEffect(() => {
if (!enabled) return;
// Start a fresh deadline after each retry so a newly opened connection
// isn't immediately replaced by the previous connection's renewal timer.
const closeTimer = setTimeout(close, STREAM_RENEWAL_MS - RENEWAL_GAP_MS);
const renewalTimer = setTimeout(renew, STREAM_RENEWAL_MS);
return () => {
clearTimeout(closeTimer);
clearTimeout(renewalTimer);
if (retryTimer.current !== null) {
clearTimeout(retryTimer.current);
retryTimer.current = null;
}
setClosing(false);
};
}, [enabled, imageKey, renew, close]);
const handleStreamError = useCallback(
(event?: SyntheticEvent) => {
if (!enabled || retryTimer.current !== null) return;
// Closing the stream (src removed) fires error too.
if (event && !event.currentTarget.getAttribute('src')) return;
close();
retryTimer.current = setTimeout(renew, ERROR_RETRY_MS);
},
[enabled, renew, close],
);
return { imageKey, closing, imgRef, handleStreamError };
}