File size: 2,604 Bytes
375924d
 
 
 
 
 
 
 
 
 
 
948a968
375924d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542279d
375924d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import { useRef, useEffect, useState } from "react";

interface UseWebcamOptions {
  enabled: boolean;
  onFrame?: (video: HTMLVideoElement, timestamp: number) => void;
  processEveryN?: number;
}

export function useWebcam({
  enabled,
  onFrame,
  processEveryN = 2,
}: UseWebcamOptions) {
  const videoRef = useRef<HTMLVideoElement | null>(null);
  const streamRef = useRef<MediaStream | null>(null);
  const frameCount = useRef(0);
  const rafId = useRef(0);
  const onFrameRef = useRef(onFrame);
  const [error, setError] = useState<string | null>(null);
  const [active, setActive] = useState(false);

  useEffect(() => {
    onFrameRef.current = onFrame;
  }, [onFrame]);

  function teardown() {
    if (rafId.current) cancelAnimationFrame(rafId.current);
    rafId.current = 0;
    if (streamRef.current) {
      streamRef.current.getTracks().forEach((t) => t.stop());
      streamRef.current = null;
    }
    if (videoRef.current) {
      videoRef.current.srcObject = null;
    }
  }

  useEffect(() => {
    if (!enabled) {
      teardown();
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setActive(false);
      return;
    }

    let cancelled = false;

    async function start() {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({
          video: { facingMode: "user", width: 640, height: 480 },
        });
        if (cancelled) {
          stream.getTracks().forEach((t) => t.stop());
          return;
        }
        streamRef.current = stream;
        if (videoRef.current) {
          videoRef.current.srcObject = stream;
          await videoRef.current.play();
        }
        if (cancelled) {
          teardown();
          return;
        }
        setActive(true);
        setError(null);

        function loop(timestamp: number) {
          if (cancelled) return;
          frameCount.current++;
          if (
            frameCount.current % processEveryN === 0 &&
            videoRef.current &&
            onFrameRef.current
          ) {
            onFrameRef.current(videoRef.current, timestamp);
          }
          rafId.current = requestAnimationFrame(loop);
        }
        rafId.current = requestAnimationFrame(loop);
      } catch (e) {
        if (!cancelled) {
          setError(
            e instanceof Error ? e.message : "Webcam access denied"
          );
          setActive(false);
        }
      }
    }

    start();

    return () => {
      cancelled = true;
      teardown();
      setActive(false);
    };
  }, [enabled, processEveryN]);

  return { videoRef, active, error };
}