QRbitr App: https://austinwin.github.io/qrbitr/
QRbitr is a browser-only application for sending and receiving small files or text via a rapid sequence of QR codes.
It’s private by design - no one, including IT admins or network monitors, can see your transfer because nothing ever leaves your devices.
No setup, no account, no Bluetooth pairing, no cables - just open it in a browser and transfer.
Perfect for:
- Moving data between a work laptop and a personal phone without triggering network logging.
- Sharing files when you have no internet, no USB cable, or no permission to install software; just pwa.
- Quick one-off transfers without signing into cloud services.
- Large text files — automatically compressed before sending and decompressed on the receiver's side for faster, more efficient transfer.
Because QRbitr runs entirely in your browser and operates offline, it leaves no server trace, requires no pairing, and is as simple as pointing a camera at a screen either through the online web app or pwa.
- Offline file & text transfer (client-side only, no servers).
- Privacy-first – nothing is sent over a network.
- Fountain coding (LT codes) with robust soliton distribution for resilience to frame drops.
- Peeling decoder with Gaussian elimination fallback for complete recovery.
- Automatic compression (Deflate via pako) when beneficial — great for large text files.
- Real-time QR generation at configurable FPS.
- Adaptive redundancy (extra fountain frames) to improve success in noisy conditions.
- CRC-32 integrity checks for final verification.
- On-screen debug logs – easily view detailed transfer logs directly in the browser (works even on mobile).
- Works on modern desktop & mobile browsers, installable as a PWA.
QRbitr implements a one-way, high-redundancy broadcast protocol over visual QR frames.
It uses LT (Luby Transform) fountain codes to generate a stream of QR frames where each frame carries either original data chunks or XOR combinations.
The receiver collects any sufficient subset of frames to reconstruct the payload — no need for every frame to arrive.
This is ideal for environments where frame loss is common, like camera scans of rapidly changing QR codes.
flowchart TD
subgraph Sender [Sender]
A[File/Text Input] --> B[Chunking]
B --> C[Optional Compression Deflate]
C --> D[Metadata Frame]
C --> E[Base Chunks]
E --> F[LT Encoder - robust soliton]
D --> G[QR Frame Builder]
F --> G
G --> H[QR Renderer @ FPS]
end
subgraph Receiver [Receiver]
I[Camera] --> J[jsQR Decoder]
J --> K[Frame Parser]
K --> L[Chunk Store + Index]
L --> M[Peeling Decoder]
M -->|stalled| N[Gaussian Elimination]
M -->|recovered| O[Reassembly]
N --> O
O --> P[Decompression if used]
P --> Q[CRC-32 Verify & Download]
end
Pipeline summary
- Sender splits data into fixed-size chunks and creates a metadata frame.
- LT encoder emits a mix of original and fountain (XOR) chunks following a robust soliton distribution.
- Each chunk becomes a QR frame; frames cycle at the configured FPS.
- Receiver scans frames; peeling + (if needed) Gaussian elimination reconstruct missing chunks.
- Payload is reassembled, decompressed (if used), CRC-checked, and saved.
- Fountain codes – general concept: Wikipedia: Fountain code
- LT (Luby Transform) codes – encoding/decoding method used here:
Wikipedia: LT codes - Peeling decoder – iterative XOR elimination: see LT codes link above.
- Gaussian elimination – linear algebra fallback:
Wikipedia: Gaussian elimination - CRC-32 – integrity check:
Wikipedia: Cyclic redundancy check (CRC-32 section) - Deflate (pako) – optional compression:
Wikipedia: Deflate
index.html– App UI and wiring.qr-stream.js– Send/receive logic, frame creation, progress, events.lt-codes.js– Fountain code encoder/decoder, soliton distribution.prng.js– Pseudo-random number generator for chunk selection.utils.js– CRC-32, Base64 helpers, formatting, decompression.jsQR.js– QR scanning/decoding from camera frames.qrcode.min.js– QR generation.pako.min.js– Deflate/inflate compression.
- Open
index.htmlin a modern browser (desktop recommended for sending). - Send mode: pick a file or paste text → set FPS/chunk size/redundancy → Start.
- Receive mode: allow camera access → point at sender’s screen → wait for progress → file auto-downloads.
No build required. Can be hosted as static files for easy sharing.
| Param | Default | What it controls |
|---|---|---|
chunkSize |
800 bytes |
Data bytes per QR frame before encoding. |
redundancy |
0.5 |
Ratio of extra fountain frames for resilience. |
fps |
20 |
QR frames per second displayed. |
maxFileSize |
10 MB |
Safety cap for payload size. |
solitonC |
0.03 |
Robust soliton distribution parameter. |
solitonDelta |
0.05 |
Robust soliton distribution parameter. |
- Keep
chunkSizemoderate (600–900 bytes) to balance frame count vs QR size. - Stay ≤ 10 FPS unless your receiver can decode faster.
- Increase
redundancyfor poor lighting or shaky scanning. - Use a bright screen and steady positioning for best results.
- Modern Chrome, Edge, Firefox.
- iOS Safari works but may have lower camera FPS.
import { QRStream } from './lib/qr-stream.js';
// Sending
const sender = new QRStream({
debugCallback: console.log,
statusCallback: console.log
});
const fileInput = document.querySelector('#fileInput');
const sendCanvas = document.querySelector('#sendCanvas');
fileInput.onchange = () => {
sender.startSending(fileInput.files[0], sendCanvas);
};
// Receiving
const receiver = new QRStream({
debugCallback: console.log,
statusCallback: console.log,
resultCallback: html => { document.getElementById('result').innerHTML = html; }
});
const video = document.querySelector('#video');
const recvCanvas = document.querySelector('#recvCanvas');
document.querySelector('#startReceive').onclick = () => {
receiver.startReceiving(video, recvCanvas);
};- Create a QRStream instance with desired config and callbacks.
- Call
startSending(file, canvas):file: aFileobject (from<input type="file">).canvas: a<canvas>element to render QR codes.
- QR codes will be displayed on the canvas in a loop for scanning.
- Create a QRStream instance with desired config and callbacks.
- Call
startReceiving(video, canvas):video: a<video>element (camera preview).canvas: a<canvas>element (for internal decoding, not shown to user).
- Grant camera access and point at sender's QR codes.
- When transfer completes, the result is provided via the
resultCallback.
Below are compact examples showing common usage patterns and advanced operations.
Use this as a starting point in a single page — pick a file to send, then point the receiver camera at the sender canvas.
<!-- Minimal app: send + receive -->
<input id="fileInput" type="file" />
<button id="startSend">Start Send</button>
<canvas id="sendCanvas" width="512" height="512"></canvas>
<video id="video" autoplay playsinline style="width:240px;height:180px;"></video>
<canvas id="recvCanvas" style="display:none;"></canvas>
<button id="startReceive">Start Receive</button>
<div id="status"></div>
<div id="progress"></div>
<div id="result"></div>
<script type="module">
import { QRStream } from './lib/qr-stream.js';
// Sender
const sendCanvas = document.getElementById('sendCanvas');
const fileInput = document.getElementById('fileInput');
const sender = new QRStream({
fps: 10,
redundancy: 0.6,
debugCallback: msg => console.log('[SEND]', msg),
statusCallback: s => document.getElementById('status').innerText = s
});
document.getElementById('startSend').onclick = () => {
if (!fileInput.files[0]) return alert('Pick a file first');
sender.startSending(fileInput.files[0], sendCanvas);
};
// Receiver
const video = document.getElementById('video');
const recvCanvas = document.getElementById('recvCanvas');
const receiver = new QRStream({
debugCallback: msg => console.log('[RECV]', msg),
statusCallback: s => document.getElementById('status').innerText = s,
progressCallback: p => document.getElementById('progress').innerText = p + '%',
resultCallback: html => document.getElementById('result').innerHTML = html
});
document.getElementById('startReceive').onclick = () => {
receiver.startReceiving(video, recvCanvas);
};
</script>You can stop the sender loop and restart from a percentage location in the prepared frames.
// Stop sending temporarily
sender.stopSending();
// Resume sending from beginning
sender.restartSending(sendCanvas, 0);
// Resume sending from 25% into the data frames
sender.restartSending(sendCanvas, 25);
// Note: restartSending expects the same canvas and that startSending was called earlierTo explicitly mark the end of the transfer you can display the trailer frames. This is useful for receivers to trigger final decoding attempts.
// Show trailer burst (pauses normal loop briefly)
sender.sendTrailerFrames();Tune decoding strategy when initializing the receiver.
// enablePeeling = false to skip peeling decoder
// enableGaussian = true to allow gaussian elimination fallback
receiver.startReceiving(video, recvCanvas, false, true);- fps: frames-per-second the sender renders (lower helps unreliable cameras).
- redundancy: fraction of extra fountain chunks (0.5 = 50% extra).
- chunkSize: bytes per chunk (600–900 recommended).
- solitonC / solitonDelta: LT code distribution parameters (advanced).
Example config:
const senderFast = new QRStream({ fps: 15, redundancy: 0.75, chunkSize: 800 });
const receiverSlow = new QRStream({ debugCallback: console.log });MIT (see LICENSE).