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    };
25    if (parsed.location) {
26      nativeEvent.cornerPoints = [
27        parsed.location.topLeftCorner,
28        parsed.location.bottomLeftCorner,
29        parsed.location.topRightCorner,
30        parsed.location.bottomRightCorner,
31      ];
32    }
33    return nativeEvent;
34  }
35  return parsed;
36};
37
38function useRemoteJsQR() {
39  return useWorker(qrWorkerMethod, {
40    remoteDependencies: ['https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.min.js'],
41    autoTerminate: false,
42  });
43}
44
45export function useWebQRScanner(
46  video: React.MutableRefObject<HTMLVideoElement | null>,
47  {
48    isEnabled,
49    captureOptions,
50    interval,
51    onScanned,
52    onError,
53  }: {
54    isEnabled: boolean;
55    captureOptions: Pick<CameraPictureOptions, 'scale' | 'isImageMirror'>;
56    interval?: number;
57    onScanned?: (scanningResult: { nativeEvent: BarCodeScanningResult }) => void;
58    onError?: MountErrorListener;
59  }
60) {
61  const isRunning = React.useRef<boolean>(false);
62  const timeout = React.useRef<number | undefined>(undefined);
63
64  const [decode, clearWorker] = useRemoteJsQR();
65
66  async function scanAsync() {
67    // If interval is 0 then only scan once.
68    if (!isRunning.current || !onScanned) {
69      stop();
70      return;
71    }
72    try {
73      const data = captureImageData(video.current, captureOptions);
74
75      if (data) {
76        const nativeEvent: BarCodeScanningResult | any = await decode(data);
77        if (nativeEvent?.data) {
78          onScanned({
79            nativeEvent,
80          });
81        }
82      }
83    } catch (error) {
84      if (onError) {
85        onError({ nativeEvent: error });
86      }
87    } finally {
88      // If interval is 0 then only scan once.
89      if (interval === 0) {
90        stop();
91        return;
92      }
93      const intervalToUse = !interval || interval < 0 ? 16 : interval;
94      // @ts-ignore: Type 'Timeout' is not assignable to type 'number'
95      timeout.current = setTimeout(() => {
96        scanAsync();
97      }, intervalToUse);
98    }
99  }
100
101  function stop() {
102    isRunning.current = false;
103    clearTimeout(timeout.current);
104  }
105
106  React.useEffect(() => {
107    if (isEnabled) {
108      isRunning.current = true;
109      scanAsync();
110    } else {
111      stop();
112    }
113  }, [isEnabled]);
114
115  React.useEffect(() => {
116    return () => {
117      stop();
118      clearWorker.kill();
119    };
120  }, []);
121}
122