API reference

Every public export, the <ur-scanner> surface, and the complete error taxonomy. Types are the source of truth; this mirrors them.

Entry points

ImportWhatDOM-safe?
@nkwib/ur-scannercore + frame sources + detector seamyes (safe in SSR bundles)
@nkwib/ur-scanner/elementside-effectful: registers <ur-scanner>no (browser only)

class URReceiver

The pure decode core. new URReceiver(options?).

URReceiverOptions

OptionTypeDefaultNotes
expectedTypestringnoneReject any part whose UR type differs (case-insensitive). Emits UNEXPECTED_TYPE.
stallTimeoutMsnumberoffEmit DECODE_STALL if no new part arrives in this window while incomplete.
onProgress(p: Progress) => voidConvenience for on('progress', ...).
onComplete(r: DecodedUR) => void
onError(e: URScannerError) => void
onIgnore(i: IgnoredFrame) => void

Methods and properties

MemberSignatureNotes
addPart(text: string) => ProgressFeed one scanned string. Idempotent; safe with noise. Returns progress after this frame.
progressProgress (getter)Current snapshot without feeding.
isCompleteboolean (getter)
typestring | null (getter)Locked UR type, or null before the first accepted part.
on(event, listener) => () => voidReturns an unsubscribe function. Events: progress, complete, error, ignore.
off(event, listener) => void
reset() => voidDiscard all state to start a fresh scan.
dispose() => voidClear the stall timer and all listeners.

Progress

interface Progress {
  type: string | null;          // locked UR type
  receivedParts: number;        // distinct source fragments recovered
  expectedPartCount: number;    // K; 0 until the first well-formed part
  estimatedPercent: number;     // 0..1 from the decoder, NOT received/expected
  framesSeen: number;           // every raw frame fed, incl. dupes and noise
  canStartAnywhere: true;       // a fact about fountain codes
  complete: boolean;
}

DecodedUR (delivered on completion)

interface DecodedUR {
  type: string;                 // e.g. 'bytes', 'crypto-hdkey'
  cbor: Uint8Array;             // the raw UR body (CBOR), always present
  wasSinglePart: boolean;       // true => a static QR would have done; not an error
  decodeCbor(): unknown;        // CBOR-decoded value; for ur:bytes this is your payload bytes
}

IgnoredFrame

interface IgnoredFrame {
  reason: 'not-a-ur' | 'mixed-type' | 'malformed' | 'duplicate';
  text: string;                 // truncated
  seenType?: string;            // when reason is 'mixed-type'
}

Frame sources

fromCamera(options?): Promise<CameraController>

CameraSourceOptions extends URReceiverOptions with:

OptionTypeDefaultNotes
videoHTMLVideoElementone is createdCaller-owned, so layout stays yours.
constraintsMediaStreamConstraints{ video: { facingMode: 'environment' } }Passed straight to getUserMedia.
receiverURReceivera new oneShare it with a file-input fallback.
detectorQRDetectornative, then jsqrSee the detector seam below.
scanIntervalMsnumbernoneMinimum ms between detect attempts. Unset scans every delivered camera frame.
fallbackMaxSizenumber960Long-edge cap before the jsqr fallback decodes. Ignored for native or explicit detectors.

The loop runs once per delivered camera frame via requestVideoFrameCallback, falling back to requestAnimationFrame capped at about 33 ms where that API is missing. Every frame the sender displays and the loop does not look at is payload thrown away, so the scan rate tracks the camera rather than a timer. Set scanIntervalMs to cap it for battery: it was 120 by default before 0.2.0, which limited the receiver to about 8 parts per second regardless of the sender.

CameraController: receiver, video, stop(), hasTorch(), torch(on), listVideoInputs(), switchCamera(deviceId). See camera selection and torch and benchmarking.

fromImage(source, options?): Promise<{ receiver, progress, found }>

source: Blob | File | ImageBitmap | HTMLImageElement | HTMLCanvasElement | string(url). Decodes every QR in one still and feeds them. Thread a receiver across calls for multi-part uploads. See file and image input.

fromFixture(parts, options?): { receiver, progress }

Feed a string[] synchronously. playFixture(parts, { intervalMs, loop, onFrame, ... }) plays them on a timer and returns { receiver, stop, done }. Test utilities: dropFraction(parts, fraction, seed?) and shuffle(parts, seed?) (both deterministic). See testing without a camera.

Detector seam

  • nativeDetector(): QRDetector | null : native BarcodeDetector, or null if absent.
  • fallbackDetector(options?: { maxSize?: number }): Promise<QRDetector> : lazily import()s jsqr; throws DETECTOR_UNSUPPORTED if not installed. maxSize caps the long edge before decoding.
  • resolveDetector(explicit?, options?: { maxSize?: number }): Promise<QRDetector> : explicit, then native, then fallback. options only reaches the fallback.
interface QRDetector {
  detect(source: CanvasImageSource): Promise<DetectedCode[]>;
  readonly acceptsVideo?: boolean;   // set it to be handed the <video> directly
}
interface DetectedCode { rawValue: string }

detect took an HTMLCanvasElement before 0.2.0. The widening is source compatible: a detector written against HTMLCanvasElement still compiles, and still receives a canvas. Set acceptsVideo to be handed the live <video> instead, which saves the camera loop a full-resolution copy per scan. Both built-in detectors set it.

<ur-scanner> custom element

Register with import '@nkwib/ur-scanner/element' (or call defineURScanner(tag?)).

Attributes

AttributeValuesNotes
auto-startboolean (presence)Start on connect.
expected-typestringForwarded to the receiver.
facing-modeenvironment | userCamera preference.
scan-intervalnumber (ms)Cap on detect attempts. Unset scans every delivered camera frame.
fixtureJSON string[]One-device / demo mode: play these parts instead of the camera.

Methods, events, CSS parts

  • Methods: start(), stop(), reset().
  • Events (bubbling, composed CustomEvents): ur-progress (detail Progress), ur-complete (detail DecodedUR), ur-error (detail URScannerError), ur-ignore (detail IgnoredFrame).
  • CSS parts: container, video, overlay, ring, status, controls, camera-select, torch-button. Style the ring fill via ::part(ring) descendants; the accent stroke is a plain SVG you can override.

Error taxonomy

Every failure is a URScannerError with a stable code. Branch on code, never on message.

codeLayerTriggerFatal?Suggested user copy
INSECURE_CONTEXTcamerapage served over plain HTTPyes"Camera needs a secure (HTTPS) page."
CAMERA_UNSUPPORTEDcamerano getUserMediayes"This browser cannot open the camera. Upload a photo instead."
CAMERA_PERMISSION_DENIEDcameraNotAllowedErroryes"Camera permission was denied. Enable it in site settings."
CAMERA_NOT_FOUNDcameraNotFoundError / OverconstrainedErroryes"No camera found."
DETECTOR_UNSUPPORTEDdetectorno native detector and jsqr missingyes"Install the QR fallback, or use a Chromium browser."
UNEXPECTED_TYPEcorefirst part's type != expectedTypeyes (rejects)"That is not the code we expected."
MIXED_UR_TYPEScorea foreign UR type appears mid-scanno (warns, keeps going)usually silent; keep scanning the right code
DECODE_STALLcoreno new part within stallTimeoutMsno (informational)"Not receiving frames. Move closer / clean the lens."
DECODE_FAILEDcoredecoder reports an unrecoverable stream erroryes"That code stream could not be decoded."

Two related non-errors, documented here because people look for them:

  • Single-part UR: not an error. The scan completes and DecodedUR.wasSinglePart is true, meaning the payload fit in one static QR and animation was unnecessary. Surface it as a hint if you like.
  • Non-UR noise (a URL, a Wi-Fi QR): not an error either. It is counted in framesSeen and reported via the ignore event with reason not-a-ur, so the scanner shrugs off unrelated codes instead of failing.

Event lifecycle

stateDiagram-v2
  [*] --> Idle
  Idle --> Scanning: start() / first addPart()
  Scanning --> Scanning: ur-progress (each accepted part)
  Scanning --> Scanning: ur-ignore (dupe / noise / mixed-type)
  Scanning --> Complete: ur-complete (isSuccess)
  Scanning --> Failed: ur-error (fatal code)
  Complete --> Idle: reset()
  Failed --> Idle: reset()
  Complete --> [*]: stop() / dispose()
ur-scanner Browser receiver for animated BC-UR QR codes. Camera to bytes, no wallet required.