1import * as React from 'react';
2
3import { BarCodeScanningResult, CameraPictureOptions, MountErrorListener } from './Camera.types';
4import { captureImageData } from './WebCameraUtils';
5
6const qrWorkerMethod = ({ data, width, height }: ImageData): any => {
7  // eslint-disable-next-line no-undef
8  const decoded = (self as any).jsQR(data, width, height, {
9    inversionAttempts: 'attemptBoth',
10  });
11
12  let parsed;
13  try {
14    parsed = JSON.parse(decoded);
15  } catch {
16    parsed = decoded;
17  }
18
19  if (parsed?.data) {
20    const nativeEvent: BarCodeScanningResult = {
21      type: 'qr',
22      data: parsed.data,
23      cornerPoints: [],
24      bounds: { origin: { x: 0, y: 0 }, size: { width: 0, height: 0 } },
25    };
26    if (parsed.location) {
27      nativeEvent.cornerPoints = [
28        parsed.location.topLeftCorner,
29        parsed.location.bottomLeftCorner,
30        parsed.location.topRightCorner,
31        parsed.location.bottomRightCorner,
32      ];
33    }
34    return nativeEvent;
35  }
36  return parsed;
37};
38
39const createWorkerAsyncFunction = <T extends (data: any) => any>(fn: T, deps: string[]) => {
40  const stringifiedFn = [
41    `self.func = ${fn.toString()};`,
42    'self.onmessage = (e) => {',
43    '  const result = self.func(e.data);',
44    '  self.postMessage(result);',
45    '};',
46  ];
47
48  if (deps.length > 0) {
49    stringifiedFn.unshift(`importScripts(${deps.map((dep) => `'${dep}'`).join(', ')});`);
50  }
51
52  const blob = new Blob(stringifiedFn, { type: 'text/javascript' });
53  const worker = new Worker(URL.createObjectURL(blob));
54
55  // First-In First-Out queue of promises
56  const promises: {
57    resolve: (value: ReturnType<T>) => void;
58    reject: (reason?: any) => void;
59  }[] = [];
60
61  worker.onmessage = (e) => promises.shift()?.resolve(e.data);
62
63  return (data: Parameters<T>[0]) => {
64    return new Promise<ReturnType<T>>((resolve, reject) => {
65      promises.push({ resolve, reject });
66      worker.postMessage(data);
67    });
68  };
69};
70
71const decode = createWorkerAsyncFunction(qrWorkerMethod, [
72  'https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.min.js',
73]);
74
75export function useWebQRScanner(
76  video: React.MutableRefObject<HTMLVideoElement | null>,
77  {
78    isEnabled,
79    captureOptions,
80    interval,
81    onScanned,
82    onError,
83  }: {
84    isEnabled: boolean;
85    captureOptions: Pick<CameraPictureOptions, 'scale' | 'isImageMirror'>;
86    interval?: number;
87    onScanned?: (scanningResult: { nativeEvent: BarCodeScanningResult }) => void;
88    onError?: MountErrorListener;
89  }
90) {
91  const isRunning = React.useRef<boolean>(false);
92  const timeout = React.useRef<number | undefined>(undefined);
93
94  async function scanAsync() {
95    // If interval is 0 then only scan once.
96    if (!isRunning.current || !onScanned) {
97      stop();
98      return;
99    }
100    try {
101      const data = captureImageData(video.current, captureOptions);
102
103      if (data) {
104        const nativeEvent: BarCodeScanningResult | any = await decode(data);
105        if (nativeEvent?.data) {
106          onScanned({
107            nativeEvent,
108          });
109        }
110      }
111    } catch (error) {
112      if (onError) {
113        onError({ nativeEvent: error });
114      }
115    } finally {
116      // If interval is 0 then only scan once.
117      if (interval === 0) {
118        stop();
119        return;
120      }
121      const intervalToUse = !interval || interval < 0 ? 16 : interval;
122      // @ts-ignore: Type 'Timeout' is not assignable to type 'number'
123      timeout.current = setTimeout(() => {
124        scanAsync();
125      }, intervalToUse);
126    }
127  }
128
129  function stop() {
130    isRunning.current = false;
131    clearTimeout(timeout.current);
132  }
133
134  React.useEffect(() => {
135    if (isEnabled) {
136      isRunning.current = true;
137      scanAsync();
138    }
139
140    return () => {
141      if (isEnabled) {
142        stop();
143      }
144    };
145  }, [isEnabled]);
146}
147