useOverlayCameraRecovery.ts 1.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041
  1. import { useCallback, useEffect, useRef, useState } from 'react';
  2. // MJPEG <img> connections can stop delivering frames without firing onError.
  3. // Renew only the camera connection, keeping the overlay and its credentials.
  4. // This also renews healthy connections; it does not measure frame freshness.
  5. const STREAM_RENEWAL_MS = 60_000;
  6. const ERROR_RETRY_MS = 3_000;
  7. export function useOverlayCameraRecovery(enabled: boolean) {
  8. const [imageKey, setImageKey] = useState(Date.now);
  9. const retryTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  10. const renew = useCallback(() => {
  11. if (retryTimer.current !== null) {
  12. clearTimeout(retryTimer.current);
  13. retryTimer.current = null;
  14. }
  15. setImageKey((key) => key + 1);
  16. }, []);
  17. useEffect(() => {
  18. if (!enabled) return;
  19. // Start a fresh deadline after each retry so a newly opened connection
  20. // isn't immediately replaced by the previous connection's renewal timer.
  21. const renewalTimer = setTimeout(renew, STREAM_RENEWAL_MS);
  22. return () => {
  23. clearTimeout(renewalTimer);
  24. if (retryTimer.current !== null) {
  25. clearTimeout(retryTimer.current);
  26. retryTimer.current = null;
  27. }
  28. };
  29. }, [enabled, imageKey, renew]);
  30. const handleStreamError = useCallback(() => {
  31. if (!enabled || retryTimer.current !== null) return;
  32. retryTimer.current = setTimeout(renew, ERROR_RETRY_MS);
  33. }, [enabled, renew]);
  34. return { imageKey, handleStreamError };
  35. }