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