WEB SENDER (browser) RECEIVER
───────────────────────────────────────── ──────────────────────────────────────────────
Source file / folder
│
▼
Auto-compress (gzip level 6)
Skip if result ≥ 95% of raw size
Skip if payload < 256 bytes
│
▼
Split into N fixed-size chunks
│
├── Sequential mode (stream_id 0)
│ Chunks cycle 0…N-1 continuously
│ Manifest re-shown at start of each cycle
│
└── Fountain mode (stream_id 2)
Generate infinite XOR-coded packets
Each packet = deterministic XOR of
k source chunks (k chosen by degree
distribution from seeded PRNG)
│
▼
Prepend 9-byte wire header to each frame
│
▼
Encode as byte-mode QR (qrcode.js) ──► Web receiver: webcam → jsQR decode
Flash on screen at configured FPS Python client: mss capture → pyzbar decode
│
▼
Strip 9-byte header
Route by stream_id:
255 → parse manifest
0 → sequential buffer[sequence]
2 → fountain decoder
│
Sequential: all N chunks received?
Fountain: belief-propagation complete?
│
┌───────┴────────┐
No Yes
│ │
Keep scanning Decompress if flagged
Unpack zip if directory
Save / auto-download file
Every QR payload begins with a fixed 9-byte header:
| Bytes | Field | Type | Notes |
|---|---|---|---|
| 0 | stream_id |
uint8 | 0 = sequential chunk; 2 = fountain packet; 255 = manifest |
| 1–4 | sequence |
uint32 BE | Sequential: zero-based chunk index. Fountain: packet seed value. |
| 5–8 | total_chunks |
uint32 BE | Sequential: total chunk count. Fountain: total source chunk count N. |
The header is followed immediately by the raw chunk payload.
The web sender uses qrcode.js with mode: 'byte'. Binary data is converted to a Latin-1 string via String.fromCharCode and passed to the library, which internally UTF-8 encodes the string into the QR byte stream.
The web receiver decodes with jsQR, which UTF-8 decodes byte-mode QR data into code.data. Each byte is recovered with code.data.charCodeAt(k) & 0xFF, which correctly round-trips all byte values 0–255 regardless of UTF-8 expansion.
The Python client decodes with pyzbar, which returns raw bytes directly — no encoding round-trip.
stream_id = 255 is reserved for the manifest. It is shown as the first QR of every transfer and (in sequential mode) at the start of every display cycle, so a late-joining receiver picks up stream metadata before processing data frames.
The sender auto-pauses on the manifest QR and waits for the user to press Resume, giving the receiver time to scan it before data chunks begin.
Manifest body (UTF-8 JSON):
{
"v": 1,
"filename": "report.pdf",
"size": 2097152,
"original_size": 3145728,
"compressed": true,
"is_directory": false,
"num_streams": 1,
"total_chunks": 512,
"chunk_size": 600,
"error_correction": "L",
"fountain": false
}
| Field | Description |
|---|---|
size |
Bytes of transferred data (post-compression if compressed = true) |
original_size |
Bytes of the original file before compression |
total_chunks |
Number of source chunks (sequential) or source chunks N (fountain) |
fountain |
true = fountain mode; false = sequential mode |
Fountain mode implements a simplified LT code:
For each packet with seed s:
s = (1664525 × s + 1013904223) mod 2³²k from the distribution:
[2][seed:4 BE][N:4 BE] + XOR payload.All source chunks are zero-padded to chunk_size bytes for XOR compatibility. The last chunk is trimmed to its true length using manifest.size during reassembly.
The receiver runs iterative belief-propagation (peeling decoder):
Both sender and receiver use the same LCG with identical call order (chooseDegree then chooseIndices), so the same seed always produces the same set of XOR indices.
Compression runs automatically before chunking:
compressed = true in the manifest.The receiver decompresses transparently after reassembly when compressed = true.
Folders are packed as a .zip archive (web sender using JSZip) before chunking. The manifest carries the zip filename. The web receiver auto-downloads the .zip; the Python client unpacks it into a subdirectory in the output folder when is_directory = true.