n39librarian/frontend/src/shared/utils/useScanner.ts

184 lines
4.7 KiB
TypeScript

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,
};
};