/** * scanner.ts — camera-based Aztec scanning for Codex restoration. * * Wraps `@zxing/library`'s `BrowserAztecCodeReader` with a tiny lifecycle: * one `startScanner` returns a `{ stop }` handle that releases the camera * via `reset()`. The decoder fires on every frame; `NotFoundException` is * the library's "no code in this frame" signal and must be swallowed * silently — otherwise the console fills up at ~30 errors/second. * * Deduplication and envelope parsing are NOT this module's job. Callers * feed every decoded string through `parseRawScan` + `ChunkBuffer` in * `./restore.ts`. * * Browser-only. Never imported by tests directly — vitest runs in `node` * environment per `vitest.config.ts` and `navigator.mediaDevices` is * undefined there. */ import { BrowserAztecCodeReader, NotFoundException } from "@zxing/library"; export interface ScannerOptions { videoEl: HTMLVideoElement; /** Called for every successful Aztec decode. The caller dedups via ChunkBuffer. */ onScan: (text: string) => void; /** Reported on hard errors only — NotFoundException is filtered out. */ onError?: (err: Error) => void; } export interface ScannerHandle { stop: () => void; } export async function startScanner( opts: ScannerOptions, ): Promise { const reader = new BrowserAztecCodeReader(); // Passing `null` for deviceId lets zxing pick the default camera (typically // the back camera on mobile, the only camera on a laptop). The library // attaches the MediaStream to the provided