// Kamerasucher für Strichcodes. // // Zwei Wege, in dieser Reihenfolge: // 1. BarcodeDetector - nativ in Chrome und auf Android, erkennt auch // QR- und Datamatrix-Codes, läuft außerhalb des Hauptthreads. // 2. Eigener Decoder aus barcode.js - überall sonst, insbesondere // Safari auf iPhone und iPad. // // Wenn beides ausfällt (keine Kamera, Zugriff verweigert), bleibt die // Eingabe von Hand. Die Ziffernfolge steht unter jedem Strichcode. "use strict"; import { decodeImage } from "../barcode.js"; import { clear, el } from "../dom.js"; const SCAN_INTERVAL_MS = 120; async function nativeDetector() { if (!("BarcodeDetector" in window)) return null; try { const formats = await window.BarcodeDetector.getSupportedFormats(); const wanted = ["ean_13", "ean_8", "upc_a", "upc_e", "code_128", "qr_code"] .filter((f) => formats.includes(f)); if (!wanted.length) return null; return new window.BarcodeDetector({ formats: wanted }); } catch { return null; } } /** * Öffnet den Sucher als Überlagerung. * @returns {Promise} erkannte Ziffernfolge oder null bei Abbruch */ export function scanBarcode() { return new Promise((resolve) => { let stream = null; let timer = null; let closed = false; const video = el("video", { playsInline: true, muted: true, autoplay: true, className: "scan-video", }); const canvas = document.createElement("canvas"); const hint = el("p.scan-hint", {}, "Kamera wird geöffnet …"); const manualField = el("input", { type: "text", inputMode: "numeric", placeholder: "Ziffern unter dem Strichcode", maxLength: 32, onkeydown: (ev) => { if (ev.key === "Enter") useManual(); }, }); function useManual() { const value = manualField.value.replace(/\D/g, ""); if (value.length < 6) { hint.textContent = "Bitte mindestens sechs Ziffern eingeben."; return; } finish(value); } const overlay = el("div.scan-overlay", {}, el("div.scan-stage", {}, video, el("div.scan-frame", {})), hint, el("div.scan-manual", {}, el("label", {}, "Oder von Hand eingeben"), el("div.row", {}, manualField, el("button.primary", { type: "button", onclick: useManual }, "Übernehmen"))), el("button.secondary.scan-close", { type: "button", onclick: () => finish(null) }, "Abbrechen") ); function finish(code) { if (closed) return; closed = true; if (timer) clearInterval(timer); // Kamera zuverlässig freigeben - sonst leuchtet die Anzeigeleuchte // weiter und der Akku leidet. if (stream) for (const track of stream.getTracks()) track.stop(); overlay.remove(); document.body.classList.remove("scanning"); window.removeEventListener("keydown", onKey); resolve(code); } function onKey(ev) { if (ev.key === "Escape") finish(null); } window.addEventListener("keydown", onKey); document.body.classList.add("scanning"); document.body.append(overlay); (async () => { try { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" }, width: { ideal: 1280 }, height: { ideal: 720 }, }, audio: false, }); } catch (err) { hint.textContent = err.name === "NotAllowedError" ? "Kein Zugriff auf die Kamera. Bitte in den Browsereinstellungen erlauben – oder die Ziffern von Hand eingeben." : "Keine Kamera verfügbar. Bitte die Ziffern von Hand eingeben."; manualField.focus(); return; } video.srcObject = stream; try { await video.play(); } catch { // Manche Browser verlangen eine Nutzeraktion; das Bild erscheint // dann trotzdem, sobald die Wiedergabe anläuft. } const detector = await nativeDetector(); hint.textContent = detector ? "Strichcode in den Rahmen halten." : "Strichcode in den Rahmen halten. Für eine gute Erkennung waagerecht und gut ausgeleuchtet."; timer = setInterval(async () => { if (closed || video.readyState < 2) return; const width = video.videoWidth; const height = video.videoHeight; if (!width || !height) return; if (detector) { try { const found = await detector.detect(video); if (found.length) { const value = String(found[0].rawValue || "").trim(); if (value) return finish(value); } } catch { // Weiter mit dem eigenen Decoder. } } // Nur den mittleren Streifen auswerten: Dort liegt der Code, // und es spart die meiste Rechenzeit. const bandHeight = Math.round(height * 0.35); const y = Math.round((height - bandHeight) / 2); canvas.width = width; canvas.height = bandHeight; const context = canvas.getContext("2d", { willReadFrequently: true }); context.drawImage(video, 0, y, width, bandHeight, 0, 0, width, bandHeight); const code = decodeImage(context.getImageData(0, 0, width, bandHeight)); if (code) finish(code); }, SCAN_INTERVAL_MS); })(); }); } /** Ob ein Sucher überhaupt sinnvoll ist. Über reines HTTP verweigert der * Browser den Kamerazugriff - dann direkt zur Eingabe von Hand. */ export function cameraAvailable() { return Boolean(navigator.mediaDevices?.getUserMedia) && window.isSecureContext; }