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

@upekshaip/qr-stream

v0.1.2

Published

Adaptive QR streaming: offline screen-to-camera file transfer over animated QR codes with configurable grid size, frame interval, CRC32/SHA-256 integrity, and optional AES-256-GCM encryption.

Readme

@upekshaip/qr-stream

npm license

Offline screen-to-camera file transfer over animated QR codes.

One device plays a file as a looping animation of QR codes; another points a camera at the screen and reassembles the file — no network, no cables, no pairing. The link is strictly one-way light, which makes it useful for air-gapped machines, kiosk provisioning, data diodes, and anywhere radios are unavailable or unwelcome.

qr-stream is the protocol and engine behind the research project "An Adaptive QR Streaming Framework for Offline Screen–Camera Data Transmission" — try the live demo at qr.upekshaip.com.

Features

  • Self-describing simplex protocol — every QR carries its own sequence metadata, so chunks arrive in any order across any number of cycles
  • Spatial multiplexing — 1×1, 2×2, or 3×3 QR grids per frame
  • Temporal multiplexing — configurable frame interval (100–1000 ms)
  • Integrity built in — CRC-32 per chunk, SHA-256 per file
  • Phase-lock-proof cycling — optional per-cycle frame shuffle so slow receivers converge instead of stalling (why)
  • Selective retransmission — build a stream carrying only the chunks a receiver reports missing
  • Optional AES-256-GCM encryption — PBKDF2 key derivation with the iteration count carried in-stream
  • Two detection engines — native BarcodeDetector (Chromium) with a jsQR fallback everywhere else
  • Headless simulation — model receivers and channel loss in Node, no camera required (docs)
  • Capacity guards — typed QrCapacityError at plan time instead of a silent render failure
  • Time estimation & campaigns — theoretical transfer-time baseline, capture-window recommendation, and experiment-campaign expansion with wall-clock ETAs (docs)

Install

npm install @upekshaip/qr-stream

No registry configuration and no token: the package is public on npmjs.com. It is also published to GitHub Packages in parallel, for consumers who prefer to resolve the @upekshaip scope there — map the scope in .npmrc and authenticate with a read:packages token if you want that route.

The runnable examples use a local file: dependency, so a repo clone is enough to try everything without installing from any registry.

Requires Node ≥ 20 for Node-side use (global Web Crypto). Rendering (TxEngine, composeFrame) and detection (QrScanner) need a browser; the protocol, crypto, and simulation layers run anywhere. Nothing touches browser APIs at import time, so the package is SSR-safe.

Quick start — sender

import {
  PROTOCOL, segment, sha256Hex, buildFramePlan, TxEngine,
} from "@upekshaip/qr-stream";

const bytes = new Uint8Array(await file.arrayBuffer());
const chunkBytes = 512;
const chunks = segment(bytes, chunkBytes);

const meta = {
  protocol: PROTOCOL,
  name: file.name,
  size: bytes.length,
  sha256: await sha256Hex(bytes),
  total: chunks.length,
  chunkBytes,
};

const frames = buildFramePlan(chunks, meta, 2 /* 2×2 grid */, {
  metaEvery: 16,   // repeat META so slow receivers catch it fast
  ecLevel: "M",    // validate every payload against QR capacity now
});

const engine = new TxEngine(document.querySelector("canvas")!);
await engine.start({
  frames,
  intervalMs: 300,
  gridSize: 2,
  sidePx: 768,
  ecLevel: "M",
  loop: true,
  rotatePerCycle: true, // shuffle each cycle — slow receivers can't phase-lock
  onError: (err) => console.error(err),
});
// engine.stop() ends the run instantly; onState("stopped") always fires

Quick start — receiver

import {
  QrScanner, drawSourceToCanvas, parsePayload, Reassembler, sha256Hex,
} from "@upekshaip/qr-stream";

const scanner = new QrScanner();
scanner.gridHint = 2;          // used only by the jsQR fallback
await scanner.whenReady();
const reasm = new Reassembler();
const scratch = document.createElement("canvas");

while (!reasm.complete) {
  drawSourceToCanvas(video, scratch, 1280); // downscale: faster mobile decode
  const { values } = await scanner.scan(scratch);
  for (const value of values) {
    const p = parsePayload(value);
    if (p.type === "META") reasm.setMeta(p.meta);
    else if (p.type === "DATA" && p.crcOk) reasm.add(p.seq, p.total, p.bytes);
  }
  await new Promise((r) => setTimeout(r, 0));
}

const bytes = reasm.reconstruct();
const ok = (await sha256Hex(bytes)) === reasm.meta!.sha256;

Encryption (optional)

import { encryptFile, verifyPassword, decryptFile } from "@upekshaip/qr-stream";

// sender: stream `ciphertext` instead of the plaintext and put `encMeta`
// into FileMeta.encryption
const { ciphertext, encMeta } = await encryptFile(bytes, password);

// receiver: cheap password pre-check, then authenticated decryption
if (await verifyPassword(password, meta.encryption!)) {
  const plain = await decryptFile(assembled, password, meta.encryption!);
}

The PBKDF2 iteration count (default 600 000) travels inside encMeta, so future changes never break old captures. Read the threat model in docs/security.md before relying on it.

Simulation (no camera needed)

import { simulateTransfer, mulberry32 } from "@upekshaip/qr-stream";

// a receiver decoding every 2nd frame, 95% per-cell detection
const r = simulateTransfer({
  totalChunks: 64, gridSize: 1, metaEvery: 16, rotatePerCycle: true,
  channel: { samplingPeriod: 2, cellDetectProb: 0.95 },
  random: mulberry32(42), // reproducible
});
console.log(r.cyclesToComplete, r.perCycle);

Recipes

Selective retransmission — the receiver reports what's missing (a human can relay it: read it aloud, type it in); the sender streams only those chunks. The stream is one-way, so the operator is the back-channel:

// receiver side: which chunks are still missing?
const missing = reasm.missing(); // e.g. [5, 12, 33, 34, 35]

// sender side: stream META + just those chunks (reuse the SAME chunks/meta
// as the original run — re-segmenting with other settings would shift
// chunk boundaries)
import { buildFramePlanForSeqs } from "@upekshaip/qr-stream";
const frames = buildFramePlanForSeqs(chunks, meta, 1, missing, { ecLevel: "M" });
await engine.start({ frames, intervalMs: 300, gridSize: 1, sidePx: 768, ecLevel: "M", loop: true });

Validate settings before streaming — chunk size and EC level trade off against QR capacity; check combinations up front instead of catching QrCapacityError later:

import { isChunkEcValid, maxChunkBytesForEc, QR_BYTE_CAPACITY } from "@upekshaip/qr-stream";

isChunkEcValid(1024, "H");   // false — 1024 B never fits at EC H
maxChunkBytesForEc("H");     // largest chunk that fits at EC H
QR_BYTE_CAPACITY.M;          // raw v40 byte capacity at EC M

Reproducible runs — inject a seeded PRNG anywhere randomness appears, so an experiment (or a bug report) can be replayed exactly:

import { mulberry32 } from "@upekshaip/qr-stream";

await engine.start({ ...opts, rotatePerCycle: true, random: mulberry32(42) });
simulateTransfer({ ...simOpts, random: mulberry32(42) });

Estimate transfer time before starting:

import { estimateCycleMs } from "@upekshaip/qr-stream";

const cycleMs = estimateCycleMs(frames.length, intervalMs);
// a clean capture completes in ~1 cycle; slow/occluded receivers need a few

Wire protocol (qrstream/1)

| Frame | Payload | |---|---| | META | M\|<base64(JSON FileMeta)> | | DATA | D\|<seq>\|<total>\|<crc32hex>\|<base64(chunk)> |

The pipe character never occurs in base64, so parsing is unambiguous. Full grammar, field tables, and compatibility rules: docs/protocol.md.

Browser support

| Capability | Chromium (desktop/Android) | Safari / Firefox | |---|---|---| | Transmit (Canvas) | ✅ | ✅ | | Detect — 1×1 grid | ✅ native BarcodeDetector | ✅ jsQR fallback | | Detect — 2×2 / 3×3 grids | ✅ native, all codes per frame | ⚠️ jsQR slices by gridHint; slower, needs aligned framing | | Encryption (Web Crypto) | ✅ | ✅ |

For phone receivers on jsQR, prefer 1×1 grids, pass maxDim ≈ 1280 to drawSourceToCanvas, and transmit with rotatePerCycle + metaEvery. More tuning guidance: docs/adaptive-tuning.md.

Documentation

Research

This package is the Phase-2 deliverable of a BSc (Hons) Computer Science research project at NSBM Green University studying the throughput-vs- reliability surface of spatial × temporal QR multiplexing. The experiment harness lives in the app repository at /auto/tx + /auto/rx.

License

MIT © Upeksha Indeewara Perera