59 lines
2.0 KiB
TypeScript
59 lines
2.0 KiB
TypeScript
// Camera-based QR scanning (mobile/claude.md web stub: "Scans QR codes via
|
|
// the browser MediaDevices camera API"). Decode library: jsQR — pure JS,
|
|
// no WASM, the standard lightweight choice for in-browser barcode reading;
|
|
// not a crypto primitive, so it isn't subject to crypto.md's pinned-library
|
|
// list.
|
|
|
|
import jsQR from 'jsqr';
|
|
|
|
export interface ScannerHandle {
|
|
stop: () => void;
|
|
}
|
|
|
|
/**
|
|
* Opens the camera and polls frames via requestAnimationFrame until a QR
|
|
* code decodes, then calls `onDecode` once and stops polling on its own
|
|
* (the caller is still responsible for calling `stop()` to release the
|
|
* camera stream once it's done with the decoded content).
|
|
*/
|
|
export async function startScanner(
|
|
video: HTMLVideoElement,
|
|
canvas: HTMLCanvasElement,
|
|
onDecode: (content: string) => void,
|
|
): Promise<ScannerHandle> {
|
|
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
|
|
video.srcObject = stream;
|
|
await video.play();
|
|
|
|
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
if (!ctx) throw new Error('2D canvas context unavailable');
|
|
|
|
let stopped = false;
|
|
let decoded = false;
|
|
|
|
function tick(): void {
|
|
if (stopped || decoded) return;
|
|
if (video.readyState === video.HAVE_ENOUGH_DATA) {
|
|
canvas.width = video.videoWidth;
|
|
canvas.height = video.videoHeight;
|
|
ctx!.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
const frame = ctx!.getImageData(0, 0, canvas.width, canvas.height);
|
|
const code = jsQR(frame.data, frame.width, frame.height);
|
|
if (code) {
|
|
decoded = true;
|
|
onDecode(code.data);
|
|
return;
|
|
}
|
|
}
|
|
requestAnimationFrame(tick);
|
|
}
|
|
requestAnimationFrame(tick);
|
|
|
|
return {
|
|
stop: () => {
|
|
stopped = true;
|
|
stream.getTracks().forEach((track) => track.stop());
|
|
},
|
|
};
|
|
}
|