Updated the live demo link and clarified file transfer test details.
6.5 KiB
QR Stream
Transfer files and text between devices by displaying an animated sequence of QR codes and reading it with a camera.
No network, no Bluetooth, no cables. Everything runs locally in the browser or terminal.
Live demo: https://qr.linkto.host/
Features
- Works fully offline after loading the web app
- No server, no telemetry, no cloud upload
- Send and receive text or files in the browser
- Send files or stdin from the CLI
- Preserves filenames for file transfers
- Instantly displays received text
- Compresses data before transfer, typically saving 3x-10x bandwidth on text files
- Uses RaptorQ fountain coding for efficient recovery from dropped frames
- Uses CRC32 checksums for consistency checks
- Supports GIF export for sharing or embedding QR streams
- Live QR playback is rendered with Canvas, so transfer can start immediately without waiting for GIF generation
- Adjustable QR size, playback interval, scan sampling rate, QR ECC level, and encoding redundancy
Performance
QR Stream has been significantly optimized compared with the original JavaScript-only implementation.
In measured tests, the new pipeline reaches 50x+ higher throughput in practical transfer scenarios.
Measured examples:
| Scenario | Result |
|---|---|
| V20 QR, 4-code parallel playback, 30 FPS | up to 300 decoded QR symbols/s |
| V30 QR | 100+ decoded QR symbols/s |
| 95.2 KB file transfer (V30-L x 4QR@30fps) | 375 ms, about 254.0 KB/s |
| 3.0 MB file transfer (V30-L x 4QR@15fps) | about 45 KB/s |
The 95.2 KB & 3.0MB file test was measured on iPhone 16 + Safari. Actual speed depends on device camera quality, browser performance, lighting, QR size, QR version, playback rate, and scan settings.
The new version also supports larger file transfers more reliably, including fixes for the previous GF(256) division by zero issue and other algorithmic edge cases.
Installation
pnpm
pnpm add -g qr-stream
You can also run it directly without installing:
pnpm dlx qr-stream [file]
npm
npm install -g qr-stream
npx qr-stream [file]
CLI Usage
Encode text or a file into a looping QR sequence
# Read from file
qr-stream document.pdf
# Read from stdin
echo "Hello, world!" | qr-stream
# Pipe file contents
base64 image.png | qr-stream
The terminal clears, enters an alternate screen buffer, and displays the QR frames in a loop.
Press q or Ctrl-C to quit.
Start the web app preview server
qr-stream --serve # start web app preview server
qr-stream --serve --port 8080 # custom port
qr-stream --serve --host 127.0.0.1 # localhost only
Serves the built web UI.
The default port is 3000, and the default host is 0.0.0.0.
You can also set the port with the PORT environment variable:
PORT=8080 qr-stream --serve
The server resolves the dist/ directory automatically, so it works from both the bundled CLI and a local checkout.
CLI flags
| Flag | Description |
|---|---|
-h, --help |
Show usage information |
-s, --serve |
Start the web preview server |
--port <n> |
TCP port for --serve, default: 3000, also supports PORT env |
--host <ip> |
Bind address for --serve, default: 0.0.0.0 |
Development Setup
Prerequisites
Install dependencies
pnpm install
Start the dev server
pnpm dev
Starts Vite with hot reload on:
http://localhost:5173
Build
pnpm build
Produces:
dist/index.htmlanddist/assets/*- the web appdist/qr-stream.js- the self-contained CLI bundle
Preview the production build
pnpm preview
Serves the contents of dist/ locally exactly as it will run in production.
Run tests
pnpm test
Runs the full test suite via Vitest.
Run the CLI from source
pnpm cli
Build the CLI only
pnpm build:cli
RaptorQ WASM
QR Stream now uses a standard RaptorQ fountain-code implementation based on the Rust cberner/raptorq implementation.
The Rust implementation is compiled to WASM and used by the web app and CLI for the high-performance encoding/decoding path.
The previous JavaScript fountain-code implementation is still kept as a compatibility fallback.
The RaptorQ source and build script live under:
src/raptorq/
When changing the Rust implementation, rebuild the WASM package from that directory before rebuilding the main app.
Implementation Notes
QR Stream is built around a camera-friendly, loss-tolerant transfer pipeline:
- The sender compresses the input data.
- The data is encoded with RaptorQ fountain coding.
- Encoded packets are rendered as QR codes.
- The live view draws QR frames directly to Canvas, so playback can begin immediately.
- The receiver scans frames with ZXing WASM.
- Decoded packets are collected until enough symbols are available.
- The receiver reconstructs the original payload and verifies it with CRC32.
The scanner was migrated from jsQR to ZXing WASM, which improves scan throughput and reliability.
GIF generation is now separated from live QR playback. This means the live transfer path no longer needs to wait for GIF generation before starting, while GIF export remains available as a separate sharing/export feature.
How It Works
- Sender compresses your data, splits it into encoded packets, and wraps each packet in a QR code.
- The QR codes are shown as an animated sequence in the terminal or browser.
- Receiver scans the sequence with a camera or uploads a GIF.
- The receiver decodes the frames and reassembles the original file or text.
The protocol uses RaptorQ fountain coding so the transfer can survive dropped frames, glare, and partial obstruction without requiring every single QR frame to be scanned.
For a deep dive into the packet format, algorithms, and design decisions, see ARCHITECTURE.md.
License
MIT - built for fun and utility.