scanner.ts

1.8 KB · raw

1/**2 * scanner.ts — camera-based Aztec scanning for Codex restoration.3 *4 * Wraps `@zxing/library`'s `BrowserAztecCodeReader` with a tiny lifecycle:5 * one `startScanner` returns a `{ stop }` handle that releases the camera6 * via `reset()`. The decoder fires on every frame; `NotFoundException` is7 * the library's "no code in this frame" signal and must be swallowed8 * silently — otherwise the console fills up at ~30 errors/second.9 *10 * Deduplication and envelope parsing are NOT this module's job. Callers11 * feed every decoded string through `parseRawScan` + `ChunkBuffer` in12 * `./restore.ts`.13 *14 * Browser-only. Never imported by tests directly — vitest runs in `node`15 * environment per `vitest.config.ts` and `navigator.mediaDevices` is16 * undefined there.17 */1819import { BrowserAztecCodeReader, NotFoundException } from "@zxing/library";2021export interface ScannerOptions {22  videoEl: HTMLVideoElement;23  /** Called for every successful Aztec decode. The caller dedups via ChunkBuffer. */24  onScan: (text: string) => void;25  /** Reported on hard errors only — NotFoundException is filtered out. */26  onError?: (err: Error) => void;27}2829export interface ScannerHandle {30  stop: () => void;31}3233export async function startScanner(34  opts: ScannerOptions,35): Promise<ScannerHandle> {36  const reader = new BrowserAztecCodeReader();3738  // Passing `null` for deviceId lets zxing pick the default camera (typically39  // the back camera on mobile, the only camera on a laptop). The library40  // attaches the MediaStream to the provided <video> element internally.41  await reader.decodeFromVideoDevice(null, opts.videoEl, (result, err) => {42    if (result) {43      opts.onScan(result.getText());44      return;45    }46    if (err && !(err instanceof NotFoundException)) {47      opts.onError?.(err);48    }49  });5051  return {52    stop: () => {53      reader.reset();54    },55  };56}