2025-03-24 16:18:23 +01:00
|
|
|
import type { BarcodeFormat, DetectedBarcode } from "barcode-detector/pure";
|
|
|
|
|
|
|
|
/*** select camera ***/
|
|
|
|
export interface Camera {
|
|
|
|
label: string;
|
|
|
|
constraints: {
|
|
|
|
deviceId?: string;
|
|
|
|
facingMode: string;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
export const defaultConstraintOptions: Array<Camera> = [
|
|
|
|
{ label: "rear camera", constraints: { facingMode: "environment" } },
|
|
|
|
{ label: "front camera", constraints: { facingMode: "user" } },
|
|
|
|
];
|
|
|
|
|
2025-03-25 10:42:40 +01:00
|
|
|
export async function getAvailableCameras(useDefault: boolean = false): Promise<Array<Camera>> {
|
2025-03-24 16:18:23 +01:00
|
|
|
// NOTE: on iOS we can't invoke `enumerateDevices` before the user has given
|
|
|
|
// camera access permission. `QrcodeStream` internally takes care of
|
|
|
|
// requesting the permissions. The `camera-on` event should guarantee that this
|
|
|
|
// has happened.
|
|
|
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
|
|
const videoDevices = devices.filter(({ kind }) => kind === "videoinput");
|
|
|
|
|
|
|
|
return [
|
2025-03-25 10:42:40 +01:00
|
|
|
...(useDefault ? defaultConstraintOptions : []),
|
2025-03-24 16:18:23 +01:00
|
|
|
...videoDevices.map(({ deviceId, label }) => ({
|
2025-03-25 10:42:40 +01:00
|
|
|
label: `${label}`, //(ID: ${deviceId})
|
2025-03-24 16:18:23 +01:00
|
|
|
constraints: { deviceId, facingMode: "custom" },
|
|
|
|
})),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/*** track functons ***/
|
|
|
|
export function paintOutline(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) {
|
|
|
|
for (const detectedCode of detectedCodes) {
|
|
|
|
const [firstPoint, ...otherPoints] = detectedCode.cornerPoints;
|
|
|
|
|
|
|
|
ctx.strokeStyle = "red";
|
|
|
|
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.moveTo(firstPoint.x, firstPoint.y);
|
|
|
|
for (const { x, y } of otherPoints) {
|
|
|
|
ctx.lineTo(x, y);
|
|
|
|
}
|
|
|
|
ctx.lineTo(firstPoint.x, firstPoint.y);
|
|
|
|
ctx.closePath();
|
|
|
|
ctx.stroke();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
export function paintBoundingBox(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) {
|
|
|
|
for (const detectedCode of detectedCodes) {
|
|
|
|
const {
|
|
|
|
boundingBox: { x, y, width, height },
|
|
|
|
} = detectedCode;
|
|
|
|
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
ctx.strokeStyle = "#007bff";
|
|
|
|
ctx.strokeRect(x, y, width, height);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
export function paintCenterText(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) {
|
|
|
|
for (const detectedCode of detectedCodes) {
|
|
|
|
const { boundingBox, rawValue } = detectedCode;
|
|
|
|
|
|
|
|
const centerX = boundingBox.x + boundingBox.width / 2;
|
|
|
|
const centerY = boundingBox.y + boundingBox.height / 2;
|
|
|
|
|
|
|
|
const fontSize = Math.max(12, (50 * boundingBox.width) / ctx.canvas.width);
|
|
|
|
|
|
|
|
ctx.font = `bold ${fontSize}px sans-serif`;
|
|
|
|
ctx.textAlign = "center";
|
|
|
|
|
|
|
|
ctx.lineWidth = 3;
|
|
|
|
ctx.strokeStyle = "#35495e";
|
|
|
|
ctx.strokeText(detectedCode.rawValue, centerX, centerY);
|
|
|
|
|
|
|
|
ctx.fillStyle = "#5cb984";
|
|
|
|
ctx.fillText(rawValue, centerX, centerY);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
export const trackFunctionOptions = [
|
|
|
|
{ text: "nothing (default)", value: undefined },
|
|
|
|
{ text: "outline", value: paintOutline },
|
|
|
|
{ text: "centered text", value: paintCenterText },
|
|
|
|
{ text: "bounding box", value: paintBoundingBox },
|
|
|
|
{
|
|
|
|
text: "mixed",
|
|
|
|
value: (detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) => {
|
|
|
|
paintOutline(detectedCodes, ctx);
|
|
|
|
paintCenterText(detectedCodes, ctx);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
/*** barcode formats ***/
|
|
|
|
export const barcodeFormats: Array<BarcodeFormat> = [
|
|
|
|
"aztec",
|
|
|
|
"code_128",
|
|
|
|
"code_39",
|
|
|
|
"code_93",
|
|
|
|
"codabar",
|
|
|
|
"databar",
|
|
|
|
"databar_expanded",
|
|
|
|
"data_matrix",
|
|
|
|
"dx_film_edge",
|
|
|
|
"ean_13",
|
|
|
|
"ean_8",
|
|
|
|
"itf",
|
|
|
|
"maxi_code",
|
|
|
|
"micro_qr_code",
|
|
|
|
"pdf417",
|
|
|
|
"qr_code",
|
|
|
|
"rm_qr_code",
|
|
|
|
"upc_a",
|
|
|
|
"upc_e",
|
|
|
|
"linear_codes",
|
|
|
|
"matrix_codes",
|
|
|
|
];
|
|
|
|
|
|
|
|
/*** error handling ***/
|
|
|
|
export function handleScannerError(err: Error) {
|
|
|
|
let error = `[${err.name}]: `;
|
|
|
|
|
|
|
|
if (err.name === "NotAllowedError") {
|
|
|
|
error += "you need to grant camera access permission";
|
|
|
|
} else if (err.name === "NotFoundError") {
|
|
|
|
error += "no camera on this device";
|
|
|
|
} else if (err.name === "NotSupportedError") {
|
|
|
|
error += "secure context required (HTTPS, localhost)";
|
|
|
|
} else if (err.name === "NotReadableError") {
|
|
|
|
error += "is the camera already in use?";
|
|
|
|
} else if (err.name === "OverconstrainedError") {
|
|
|
|
error += "installed cameras are not suitable";
|
|
|
|
} else if (err.name === "StreamApiNotSupportedError") {
|
|
|
|
error += "Stream API is not supported in this browser";
|
|
|
|
} else if (err.name === "InsecureContextError") {
|
|
|
|
error += "Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.";
|
|
|
|
} else {
|
|
|
|
error += err.message;
|
|
|
|
}
|
|
|
|
|
|
|
|
return error;
|
|
|
|
}
|