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