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