| import { useEffect, useRef, useState } from "react"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const MAX_RETRIES = 4; |
| const BASE_DELAY_MS = 300; |
| |
| |
| |
| const TRANSIENT_ERRORS = new Set(["NotReadableError", "AbortError"]); |
|
|
| export function useCameraStream(deviceId: string, paused: boolean) { |
| const videoRef = useRef<HTMLVideoElement>(null); |
| const [hasError, setHasError] = useState(false); |
| |
| const [retryKey, setRetryKey] = useState(0); |
| |
| const hasErrorRef = useRef(false); |
| hasErrorRef.current = hasError; |
|
|
| |
| |
| |
| |
| useEffect(() => { |
| const onDeviceChange = () => { |
| if (hasErrorRef.current) setRetryKey((k) => k + 1); |
| }; |
| navigator.mediaDevices.addEventListener("devicechange", onDeviceChange); |
| return () => |
| navigator.mediaDevices.removeEventListener("devicechange", onDeviceChange); |
| }, []); |
|
|
| useEffect(() => { |
| if (paused || !deviceId) { |
| if (!deviceId) setHasError(true); |
| return; |
| } |
| let cancelled = false; |
| let stream: MediaStream | null = null; |
| let retryTimer: ReturnType<typeof setTimeout> | null = null; |
| setHasError(false); |
|
|
| const start = async (attempt: number) => { |
| try { |
| stream = await navigator.mediaDevices.getUserMedia({ |
| video: { deviceId: { exact: deviceId } }, |
| }); |
| if (cancelled) { |
| stream.getTracks().forEach((t) => t.stop()); |
| return; |
| } |
| if (videoRef.current) { |
| videoRef.current.srcObject = stream; |
| await videoRef.current.play().catch(() => {}); |
| } |
| } catch (err) { |
| if (cancelled) return; |
| const name = err instanceof DOMException ? err.name : ""; |
| if (attempt < MAX_RETRIES && TRANSIENT_ERRORS.has(name)) { |
| |
| retryTimer = setTimeout( |
| () => start(attempt + 1), |
| BASE_DELAY_MS * 2 ** attempt |
| ); |
| } else { |
| setHasError(true); |
| } |
| } |
| }; |
| start(0); |
|
|
| return () => { |
| cancelled = true; |
| if (retryTimer) clearTimeout(retryTimer); |
| if (stream) stream.getTracks().forEach((t) => t.stop()); |
| }; |
| }, [deviceId, paused, retryKey]); |
|
|
| return { videoRef, hasError }; |
| } |
|
|