How a stream of QR codes carries a whole file

Direct answer

A QR code cannot hold a file: the largest symbol carries under three kilobytes. QR Send splits a file into thousands of small packets, draws two per QR frame, and shows fifteen frames a second. A receiver that catches only some of them still rebuilds the whole file, because QRTP is a one-way fountain stream, not a retransmission protocol.

Key takeaways

Why one symbol is not enough

QR codes were designed as labels. The grid is fixed, the data capacity is fixed, and the standard's trade-off is capacity against robustness: pick a higher error-correction level and the same symbol holds fewer bytes. Encoding a 3 MB PDF into one symbol is not slow, it is impossible — you would need a symbol with more than eight million modules.

So the file is not the unit of transmission. The stream is. QR Send takes the file bytes, cuts them into fixed-size packets, and paints new frames on screen continuously. The screen is the transmitter; the camera is the receiver; nothing else is shared.

Reed–Solomon: money inside a symbol

Every QR symbol protects its own data. The payload codewords are followed by Reed–Solomon error-correction codewords, and the four levels the standard defines spend between roughly 7% and roughly 30% of the symbol on that protection. A smudged, slightly blurred, or partly occluded symbol is often still decoded correctly because those redundant codewords fill in the damaged modules.

That layer is strictly local. Reed–Solomon can repair a symbol it can see. It cannot help with a symbol that was never captured, or was captured so badly that no codeword survives.

The QRTP layer: a fountain across packets

QRTP is the transport QR Send puts on top. It takes the file and produces source blocks, then sends Wirehair fountain packets — each one a random linear combination of those blocks, prefixed with a 4-byte block_id that tells the decoder which combination it is. The decoder needs roughly as many distinct packets as there are source blocks, in any order, with the first packets chosen no differently from the last.

That is the difference that matters in the real world: fountain codes turn a lossy optical link into a question of "how many packets did you catch", not "which ones did you miss". There is no acknowledgement, no timeout, and no retransmission — the receiver has no way to speak, and does not need one. A cheap camera on the far side of a room, a hand-held phone, or a device with no network stack at all can all reconstruct the same file from the same photons.

A squeezed frame carrying two large packets looks like this:

qrtp: u16 | transfer_id: u16 | source_total_bytes: u32     <- 8-byte frame header
block_id: u32 | payload: 364 B                             <- packet 1 (368 B)
block_id: u32 | payload: 364 B                             <- packet 2 (368 B)

Full frames carry explicit stream headers, so a frame can mix streams — for example the file data on stream_id = 0 and the network metadata on stream_id = 1. Squeezed frames skip that overhead and imply stream_id = 0; they are the common case for a plain single-file transfer, and they still carry source_total_bytes and the packet's own block_id, which is what lets a receiver join mid-stream.

The sender is just as simple from the outside:

./qrsend --fps 15 --cols 120 --rows 60 --packets-per-qr 2 ./report.pdf
./qrsend --fps 15 --packets-per-qr 2 -        # read the file from stdin

Worked example: a 3 MB PDF

Say the PDF is 3,145,728 bytes, the frame rate is 15 fps, and each frame carries two 368-byte packets.

QuantityArithmeticResult
Bytes per frame2 × 368 B736 B
Wire rate736 B × 15 fps11,040 B/s
Fountain payload per packet368 B − 4 B block_id364 B
Fountain payload per frame2 × 364 B728 B (10,920 B/s)
Source blocks in the file3,145,728 B ÷ 364 B8,643 blocks
Packets per second2 × 15 fps30
Time at perfect capture8,643 ÷ 30≈ 288 s (4 min 48 s)
Time with a few percent fountain overhead≈ 8,900 ÷ 30≈ 297 s (about 5 min)

8,643 blocks sit comfortably inside one fountain, which is capped at 64,000 blocks (about 23 MB at this packet size); a larger file is split into several fountains and the receiver interleaves them. Capture is not required to be perfect — if the camera only decodes part of the frames, the wall-clock time stretches in proportion, but the number of packets needed does not change. The receiver is indifferent to which ones they are.

The same file with the WebRTC upgrade

The optical stream is not only carrying file bytes. A second, low-rate stream (stream_id = 1) describes how the sender can be reached over the local network. When the receiver reads those frames, the two peers negotiate a WebRTC/PeerJS data channel at LAN speed, and the remaining bytes move over it — the same 3 MB, no longer paced by frame rate or lens focus. The QR stream keeps running unchanged as a fallback, and a device that never sees the network metadata just finishes optically, five minutes later. The same mechanism is what lets QR Send carry a live audio and video stream, where MediaRecorder fragments are appended to a media source at roughly 200–300 kbps.

What happens when…

EventWhat the receiver getsCost
A frame is blurredReed–Solomon may repair the symbol outright. If it does not, the frame simply yields zero packets — and the fountain does not care which packets it gets.One frame, about 67 ms
A frame is skipped entirelyThe two packets in it are missing equations, not missing data. The next frame supplies two new combinations of the same source blocks.Two packets, about 67 ms
The receiver starts halfwayIt learns the source length and packet size from the first frame header it decodes, and each packet names its own block_id, so it can rebuild the entire file — including the bytes sent before it was watching — from ~8,643 packets it collects from that point on.The full 8,643 packets, collected later
The receiver scans upside downNothing: a QR symbol's finder patterns let any conforming scanner recover its orientation, so a rotated or inverted image decodes normally. Colour inversion is different — a scanner that expects dark modules on light will not read a light-on-dark frame, which is why the sender exposes --invert, --fg and --bg.None (rotation), a re-render (inversion)

FAQ

Can a QR code hold an entire file?

No. The largest symbol the standard defines holds under 3 kB, so anything bigger has to be split. The one exception is QRTP's direct mode: if the whole payload fits in a single packet, that packet contains the source verbatim and one frame recovers the file.

How long does it take to send a 3 MB file with QR codes?

About five minutes at 15 fps with two 368-byte packets per frame, assuming a good camera. A slightly worse camera does not corrupt the file, it just stretches the scan; a WebRTC upgrade negotiated over the same optical link usually finishes it far sooner.

What happens if the camera misses a lot of frames?

Nothing breaks. The fountain decoder only needs enough distinct packets, so missed frames increase the time on screen rather than the error rate. That is the whole reason QRTP does not implement retransmission.

Do the two devices need a connection in either direction?

No. The sender renders, the receiver scans, and no acknowledgement travels back. The link is unidirectional by design, which is what makes the receiver side work on a phone with no network, on an air-gapped machine, or on a device that only has a camera.

Can the same stream carry something other than a file?

Yes. The same packet mechanics carry live camera or microphone fragments, and the receiver plays them back as they arrive — see how live media works over QR codes.

Related reading

References