1import { useWorker } from '@koale/useworker';
2import * as React from 'react';
3
4import { BarCodeScanningResult, CameraPictureOptions, MountErrorListener } from './Camera.types';
5import { captureImageData } from './WebCameraUtils';
6
7const qrWorkerMethod = ({ data, width, height }: ImageData): any => {
8  // eslint-disable-next-line no-undef
9  const decoded = (self as any).jsQR(data, width, height, {
10    inversionAttempts: 'attemptBoth',
11  });
12
13  let parsed;
14  try {
15    parsed = JSON.parse(decoded);
16  } catch {
17    parsed = decoded;
18  }
19
20  if (parsed?.data) {
21    const nativeEvent: BarCodeScanningResult = {
22      type: 'qr',
23      data: parsed.data,
24      cornerPoints: [],
25      bounds: { origin: { x: 0, y: 0 }, size: { width: 0, height: 0 } },
26    };
27    if (parsed.location) {
28      nativeEvent.cornerPoints = [
29        parsed.location.topLeftCorner,
30        parsed.location.bottomLeftCorner,
31        parsed.location.topRightCorner,
32        parsed.location.bottomRightCorner,
33      ];
34    }
35    return nativeEvent;
36  }
37  return parsed;
38};
39
40function useRemoteJsQR() {
41  return useWorker(qrWorkerMethod, {
42    remoteDependencies: ['https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.min.js'],
43    autoTerminate: false,
44  });
45}
46
47export function useWebQRScanner(
48  video: React.MutableRefObject<HTMLVideoElement | null>,
49  {
50    isEnabled,
51    captureOptions,
52    interval,
53    onScanned,
54    onError,
55  }: {
56    isEnabled: boolean;
57    captureOptions: Pick<CameraPictureOptions, 'scale' | 'isImageMirror'>;
58    interval?: number;
59    onScanned?: (scanningResult: { nativeEvent: BarCodeScanningResult }) => void;
60    onError?: MountErrorListener;
61  }
62) {
63  const isRunning = React.useRef<boolean>(false);
64  const timeout = React.useRef<number | undefined>(undefined);
65
66  const [decode, clearWorker] = useRemoteJsQR();
67
68  async function scanAsync() {
69    // If interval is 0 then only scan once.
70    if (!isRunning.current || !onScanned) {
71      stop();
72      return;
73    }
74    try {
75      const data = captureImageData(video.current, captureOptions);
76
77      if (data) {
78        const nativeEvent: BarCodeScanningResult | any = await decode(data);
79        if (nativeEvent?.data) {
80          onScanned({
81            nativeEvent,
82          });
83        }
84      }
85    } catch (error) {
86      if (onError) {
87        onError({ nativeEvent: error });
88      }
89    } finally {
90      // If interval is 0 then only scan once.
91      if (interval === 0) {
92        stop();
93        return;
94      }
95      const intervalToUse = !interval || interval < 0 ? 16 : interval;
96      // @ts-ignore: Type 'Timeout' is not assignable to type 'number'
97      timeout.current = setTimeout(() => {
98        scanAsync();
99      }, intervalToUse);
100    }
101  }
102
103  function stop() {
104    isRunning.current = false;
105    clearTimeout(timeout.current);
106  }
107
108  React.useEffect(() => {
109    if (isEnabled) {
110      isRunning.current = true;
111      scanAsync();
112    } else {
113      stop();
114    }
115  }, [isEnabled]);
116
117  React.useEffect(() => {
118    return () => {
119      stop();
120      clearWorker.kill();
121    };
122  }, []);
123}
124