import Quagga, { QuaggaJSCodeReader, QuaggaJSResultObject, } from "@ericblade/quagga2"; import { useState, useCallback, useEffect } from "react"; const constraints = { width: 640, height: 480, }; const locator = { patchSize: "medium", halfSample: true, willReadFrequently: true, }; const readers: QuaggaJSCodeReader[] = ["ean_reader"]; export const useScanner = ({ onDetected, }: { onDetected?: (result: string) => void; }): { setScannerRef: (ref: HTMLDivElement | null) => void; scannerError: string | undefined; } => { const [scannerError, setScannerError] = useState<string | undefined>(); const [scannerRef, setScannerRef] = useState<HTMLDivElement | null>(null); const getMedian = useCallback((arr: (number | undefined)[]) => { const newArr = [...arr].filter((x) => typeof x !== "undefined"); if (!newArr.length) { return; } newArr.sort((a, b) => a! - b!); const half = Math.floor(newArr.length / 2); if (newArr.length % 2 === 1) { return newArr[half]; } return (newArr[half - 1]! + newArr[half]!) / 2; }, []); const getMedianOfCodeErrors = useCallback( (result: QuaggaJSResultObject) => { const errors = result.codeResult.decodedCodes.flatMap((x) => x.error); const medianOfErrors = getMedian(errors); return medianOfErrors; }, [getMedian] ); const errorCheck = useCallback( (result: QuaggaJSResultObject) => { if (!onDetected) { return; } const err = getMedianOfCodeErrors(result); if (!err) { return; } if (err < 0.1 && result.codeResult.code) { onDetected(result.codeResult.code); } }, [getMedianOfCodeErrors, onDetected] ); const handleProcessed = (result: QuaggaJSResultObject) => { const drawingCtx = Quagga.canvas.ctx.overlay; const drawingCanvas = Quagga.canvas.dom.overlay; drawingCtx.font = "24px Arial"; drawingCtx.fillStyle = "green"; const width = drawingCanvas.getAttribute("width"); const height = drawingCanvas.getAttribute("height"); if (result) { if (result.boxes && width && height) { drawingCtx.clearRect(0, 0, parseInt(width), parseInt(height)); result.boxes .filter((box) => box !== result.box) .forEach((box) => { Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, { color: "purple", lineWidth: 2, }); }); } if (result.box) { Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, drawingCtx, { color: "blue", lineWidth: 2, }); } if (result.codeResult && result.codeResult.code) { drawingCtx.font = "24px Arial"; drawingCtx.fillText(result.codeResult.code, 10, 20); } } }; const getCameraId = useCallback(async () => { await navigator.mediaDevices.getUserMedia({ video: true, }); const devices = await navigator.mediaDevices .enumerateDevices() .then((ds) => ds.filter((d) => d.kind === "videoinput")); const back = devices.filter((d) => d.label === "Back Camera")[0]; return !back ? devices[0].deviceId : back.deviceId; }, []); const initQuagga = useCallback(async () => { try { const cameraId = await getCameraId(); if (!scannerRef || !cameraId) { console.log("fehlt", scannerRef, cameraId); return; } await Quagga.init( { inputStream: { type: "LiveStream", constraints: { ...constraints, ...(cameraId && { deviceId: cameraId }), }, target: scannerRef, willReadFrequently: true, }, locator, decoder: { readers }, locate: true, }, async (err) => { Quagga.onProcessed(handleProcessed); if (err) { return console.error("Error starting Quagga:", err); } if (scannerRef) { Quagga.start(); } } ); Quagga.onDetected(errorCheck); } catch (e: unknown) { setScannerError(JSON.stringify(e)); } }, [errorCheck, getCameraId, scannerRef]); const stopQuagga = useCallback(async () => { await Quagga.CameraAccess.release(); await Quagga.stop(); Quagga.offDetected(errorCheck); Quagga.offProcessed(handleProcessed); }, [errorCheck]); useEffect(() => { const init = () => { initQuagga(); }; const disable = () => { stopQuagga(); }; if (scannerRef) { init(); } return disable(); }, [stopQuagga, initQuagga, scannerRef]); return { setScannerRef, scannerError, }; };