npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@elchika-inc/moonqr-scanner

v0.2.0

Published

QR code scanner with camera support for moonqr

Readme

@elchika-inc/moonqr-scanner

Live camera QR scanner built on @elchika-inc/moonqr. Decoding runs in a Web Worker off the main thread (frame buffers are transferred, not copied), with a transparent same-thread fallback for environments without Worker/OffscreenCanvas support.

Install

npm install @elchika-inc/moonqr-scanner

@elchika-inc/moonqr is a regular dependency (installed automatically) — you don't need to add it yourself unless you also want its encode/decode/dom subpaths directly.

HTTPS requirement

QrScanner uses navigator.mediaDevices.getUserMedia, which browsers only expose in a secure context — i.e. HTTPS, or http://localhost during development. QrScanner.hasCamera() (below) checks this for you and returns false on an insecure origin instead of letting getUserMedia throw.

Usage

Live camera scanning

import { QrScanner } from "@elchika-inc/moonqr-scanner";

const video = document.querySelector("video")!;

const scanner = new QrScanner(
  video,
  (result) => {
    console.log("scanned:", result.text, result.version, result.ecLevel);
  },
  {
    preferredCamera: "environment", // "environment" (back camera, default) | "user"
    onError: (error) => console.error(error),
  },
);

await scanner.start(); // requests camera permission, starts the video + decode loop
// ...
scanner.stop(); // releases the camera stream and worker (idempotent, safe to call anytime)

start() rejects if getUserMedia fails (permission denied, no camera, etc.) — it never fails silently. stop() is idempotent and safe to call multiple times or before start() resolves.

One-shot image scanning

import { QrScanner } from "@elchika-inc/moonqr-scanner";

// source: HTMLImageElement | HTMLCanvasElement | ImageBitmap | Blob
const result = await QrScanner.scanImage(fileInput.files[0]);
if (result) {
  console.log(result.text);
  console.log(`decoded at 1/${result.scale} scale`); // 1 = native resolution
  console.log("scales tried (small -> large):", result.attemptedScales);
}

scanImage() runs a multiscale retry: it doesn't just try the image at native resolution. Photos of a QR code shown on a monitor pick up the screen's sub-pixel grid as high-frequency noise that defeats naive binarization — a single-step resize makes this worse (aliasing), so scanImage() builds a box-averaged (2x2 mean, a real low-pass filter) downscale pyramid and tries the smallest images first, escalating to the full-resolution image only as a last resort. The returned scale (e.g. 8 for a 1/8-scale success) and attemptedScales tell you which level actually worked, so you're not left guessing why a "successful" scan took longer than expected. In local benchmarks this small-to-large search order cut a real monitor-photo decode from ~5.4s (large-first) to ~60ms (small-first) — see bench/RESULT.md for the full writeup.

The live-camera path (QrScanner.start()) does not run the full pyramid every frame — that would tank the frame rate. It scans at native capture resolution on the fast path, and only escalates to a multiscale retry after enough consecutive frames have failed (see DEFAULT_ESCALATE_AFTER_FAILURES in src/index.ts), resetting immediately after one attempt so it doesn't camp on the slow path.

Options

new QrScanner(video, onResult, options?):

| Option | Type | Default | Description | |---|---|---|---| | preferredCamera | "environment" \| "user" | "environment" | Back or front camera. | | maxScansPerSecond | number | 25 | Throttles the decode loop. | | invert | boolean | true | Also try an inverted-color QR (black/white swapped). | | onError | (error: Error) => void | logs to console.error | Camera disconnects, worker crashes, etc. Never silently swallowed — the default still surfaces to the console rather than dropping the error. |

API reference

  • new QrScanner(video: HTMLVideoElement, onResult: (result: DecodeResult) => void, options?: QrScannerOptions)
  • scanner.start(): Promise<void> — requests camera access and starts scanning. Rejects on failure.
  • scanner.stop(): void — releases the camera stream and worker. Idempotent.
  • QrScanner.scanImage(source: HTMLImageElement | HTMLCanvasElement | ImageBitmap | Blob): Promise<ScanImageResult | null> — one-shot decode of a still image with multiscale retry.
  • QrScanner.hasCamera(): Promise<boolean> — checks getUserMedia availability and secure-context status.

DecodeResult (text, bytes, version, ecLevel, corners) and ScanImageResult (DecodeResult + scale + attemptedScales) are re-exported — see @elchika-inc/moonqr's README for the shared type shapes.

Browser support

Requires getUserMedia (camera access), Worker, and ideally OffscreenCanvas for off-main-thread decoding — evergreen desktop and mobile browsers all support these. Where Worker/OffscreenCanvas aren't available, decoding transparently falls back to the main thread instead of failing. scanImage() only needs Blob/ImageBitmap/canvas support (no camera), so it also works in Node.js test environments with a DOM shim.

License and attribution

Apache License 2.0 — see LICENSE (also bundled in this package's published tarball).

This package's own logic (camera capture, worker orchestration, multiscale retry) is original. It depends on @elchika-inc/moonqr, whose decoder is partly ported from jsQR (Apache-2.0) — see NOTICE and THIRD_PARTY_LICENSES (also bundled) for the full attribution.