# QR scanner snappiness — research + companion-dev handover *2026-07-29. Owner: web side = node repo (this doc's "web" items); native side = companion app dev (Mac). Backlog origin: UNIFIED-TASK-TRACKER "optimise companion QR scan (quicker start/decode, low-light)".* ## Where scanning happens today | Path | Stack | Used when | |---|---|---| | Web live scan | nimiq `qr-scanner` 1.4.x over `getUserMedia`, in `WalletScanModal.vue` | HTTPS browsers / secure contexts | | Photo fallback | `` photo → `BarcodeDetector` if present, else `qr-scanner.scanImage` multi-pass (`decodePhotoRobust`) | Plain-http (LAN) where `getUserMedia` doesn't exist | | Native scan | `ArchipelagoQr` JS bridge → companion's native scanner (0.5.22 fixed dense invoice QRs) | Inside the companion app | ## What makes it feel slow (ranked) 1. **Camera cold-start** — the stream starts only after the user reaches the scan pane; on phones `getUserMedia` + first frame is routinely 600–1500ms, and the native path pays a similar CameraX bind + ML Kit model cold-start. 2. **Decode cadence** — web live scan was capped at 4 scans/sec (WebView preview lagged at 10/s when decoding on the JS worker). A hand-held code therefore waits up to 250ms *after* it's already sharp and centered. 3. **Low light / focus hunting** — no torch control anywhere; no explicit continuous-focus request. Dense LN invoices need sharpness more than resolution. 4. **Dense-QR decode budget** — big bolt11/catalog QRs push the JS decoder hard; the native ML Kit path is far better at these (proven by 0.5.22). ## Web side (node repo — can be done here) - ✅ DONE (2026-07-29): scan at **10/s when `BarcodeDetector` exists** (Chrome/ Android WebView decode natively — cheap), keep 4/s only for the JS-worker fallback. - **Pre-warm the camera**: start `getUserMedia` the moment the modal opens (action pane), not when the scan pane is reached — hide the preview until needed. Saves the entire cold-start from the user's perceived timeline. - **Torch toggle**: `qr-scanner` exposes `hasFlash()/turnFlashOn()` — add a 🔦 button on the scan pane (it silently no-ops where unsupported). - **Continuous focus + modest resolution**: pass constraints `{ focusMode: 'continuous', width: { ideal: 1280 } }` — 720p-class frames start faster AND decode faster than 1080p+, with no loss for QR density that matters to us. - **Don't stop/start between panes**: returning from amount → scan currently re-inits the scanner; keep the (paused) stream alive while the modal lives. ## Native side (companion dev handover) The `ArchipelagoQr` bridge overlay is the right architecture — these are tuning items inside the native scanner activity: 1. **Pre-warm CameraX + ML Kit**: bind the camera provider and instantiate `BarcodeScanning.getClient(...)` when the WebView *requests* the overlay — or even when the wallet modal opens (add a `ArchipelagoQr.prewarm()` bridge method; the web side will call it if present). ML Kit's first-inference model load is 100–300ms — pay it before the user aims. 2. **Restrict formats**: `BarcodeScannerOptions` with `FORMAT_QR_CODE` only — skipping the other symbologies measurably cuts per-frame latency. 3. **Analysis resolution ≈ 1280×720** with `ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST` — never queue stale frames; decode the newest one only. 4. **Continuous autofocus + tap-to-focus** on the preview, and a **torch toggle** (low-light was an explicit user complaint). 5. **`zoomRatio` nudge for small codes**: if no hit after ~2s, step zoom to 1.5× — helps distant/small printed codes without user action. 6. **Success haptic + instant dismiss**: vibrate on decode and close the overlay immediately; perceived speed is heavily back-loaded. 7. Optional: **ML Kit `enableAllPotentialBarcodes` off** and skip inverted scans unless first pass fails (inverted QRs are rare; halves work). ## Acceptance criteria - Cold open → first successful scan of a normal invoice QR in **< 2s** on the companion app, **< 3s** in a mobile browser. - Dense (700+ char) bolt11 QR decodes in **< 1.5s** once framed, both paths. - Dim-room scan succeeds with the torch toggle without leaving the scanner. ## Verification notes for whoever implements - Measure with a timestamp log: overlay-requested → camera-first-frame → decode-success. The three deltas map 1:1 onto items above. - Web `BarcodeDetector` presence differs per WebView/Play-Services build — keep the JS-worker fallback path intact.