Abstract. deedy/qr-data-transfer streams a file as animated QR codes on your screen and reads them back with a phone camera. Three things are true at once. The engineering is good, and the fountain-code choice under it is the right one. The idea is eight years old and already ships in production elsewhere. And as a general file-transfer tool it loses to a USB stick by two to four orders of magnitude. The README's 1.40 Mbps is a PHY rate, payload bytes times frames per second, and it says "before camera loss" right there in the same sentence. What's missing is anyone publishing the other number, the one you actually get. Below: how it works, why the number splits in two, and the narrow set of situations where shining data at a camera is the correct answer.
This one is trending today, so I sent it to Grok for an independent read and then checked the load-bearing claims myself. Two of them I verified by hand. Every throughput figure in this post is an estimate, mine or Grok's, and I say so again where they appear.
The short version: it's a well-built optical modem, an old idea, and a bad general-purpose product. All three at once. The verdict is the boring half. The interesting half is why the headline number and the number you get are two different numbers.
What it actually does
The sender runs entirely in the browser. Your file goes through Brotli-q11 or gzip-9, whichever comes out smaller, into a container with metadata and a CRC. That container is chopped into RFC 6330 RaptorQ fountain symbols by a WASM codec. Each symbol becomes a binary QR frame: magic bytes QF4, a 19-byte header, the payload, a CRC-32. Raw byte mode, not Base45, which buys back real capacity. Those frames animate on screen in one or two lanes, synced to the display refresh.
The receiver opens /scan on a phone. Camera into ZXing-C++ compiled to WASM, frames filtered by CRC, RaptorQ decodes, a multi-CRC check runs, file saves. There's a legacy path in the tree with a hand-rolled Luby LT code and Base45 framing, tagged QF3:, but that isn't the production route anymore.
The payload never touches the network. You do download the app JavaScript on first use, so "zero install" is half true until it caches.
RaptorQ is the right call
This is the part I want to give proper credit for. A screen shining at a camera is a one-way lossy channel with no back-channel. The receiver can't ask for a retransmit, can't ACK, and probably wasn't even pointed at the screen when the stream started.
A fountain code solves exactly that. RaptorQ generates an effectively unlimited stream of symbols from the source block, and the receiver reconstructs the file once it has collected slightly more than the original amount, no matter which symbols those were. Point your phone at the screen halfway through and it still works. Blink, drop a dozen frames, still works. No sequencing, no windowing, no negotiation.
Naive frame replay, where you loop frames 1 to N forever and hope the receiver eventually catches each one, is what a lot of amateur attempts do. It's much worse. Miss one frame and you wait a full cycle for it to come round again, and the last few frames take absurdly long by coupon-collector logic. RaptorQ is the correct tool here and it's applied correctly.
The dual-lane trick is the clever bit
Phone cameras use rolling shutter. The sensor reads out row by row rather than all at once, so if the screen changes mid-readout you get a frame that's half the old QR code and half the new one. Torn frames decode to nothing.
The usual response is to slow the animation down, which costs you throughput directly. This repo does something better: it puts two QR codes side by side and alternates which one updates. One lane holds still while the other flips. Whatever the camera catches mid-tear, at least one lane in that exposure was stable for the whole readout.
Two codes at half the flip rate each beat one code at the full rate, because a torn frame is worth zero and a stable frame is worth its full payload. That's a good piece of engineering, and it's the thing I'd steal from this repo.
It reduces tearing. It doesn't abolish it. Autofocus hunting, glare, moire against the display's pixel grid and hand shake all still cost you frames.
The number
The README quotes a nominal 1.40 Mbps optical payload channel "before camera loss". That figure is payload bytes per frame, times frames per second, times eight.
It's a PHY rate. It's the same class of number as the 1200 Mbps on a Wi-Fi router box, or the 10 Gbps on an SFP module. It describes what the transmitter emits, not what arrives.
This is where I disagreed with Grok's read. Grok scored the honesty of the claims at 3 out of 10. I think that's harsher than the text supports. The words "before camera loss" are the author's own, sitting in the same sentence as the megabit figure. The author hedged it. Calling that dishonest is unfair.
The real gap is different. Camera decode rate is not camera frame rate. A phone that records 30 fps will decode considerably fewer dense V30 or V40 codes per second, and the difference depends on lighting, distance, focus, screen brightness and how steady your hand is. That decode rate is the number that matters, and there is no published end-to-end benchmark anywhere in the repo. No wall-clock KB/s for a real file on a real phone. So the only figure a reader gets is the optimistic one.
What you'd actually get
Nobody has measured this repo end to end, including me. The two rows below for the QR path are estimates, and the estimate rests on a guessed 30 to 70 percent decode-loss range. Treat them as an order of magnitude, not a measurement.
| Path | Rate | 10 MB takes |
|---|---|---|
| USB stick or SD card | 10-100+ MB/s (measured, everywhere) | Seconds |
AirDrop, LocalSend, python -m http.server | Tens of MB/s (measured, everywhere) | Seconds |
| Animated QR, hand-held | 5-40 KB/s (estimated) | Minutes |
| Animated QR, phone propped up and close | 50-150 KB/s (estimated) | 1-3 minutes |
The QR rows are inferred from the nominal channel rate minus an assumed decode loss. They are not benchmarks. The upstream codec project claims 184-254 KB/s with four lanes at 30 fps on an iPhone 16, which is its own claim and I did not verify it either.
The upper row is a lab configuration: phone landscape, propped, close, sender fullscreen, V40 codes, good light. The lower row is what a person does, which is hold a phone in one hand and get bored. Minutes of holding still to move a file is a bad experience, and people bail and email it to themselves instead.
One detail that reads like a footgun: the browser build accepts files up to 512 MB. At the optimistic end of my estimates that's over an hour of staring at a screen.
It's not a new idea
This is the claim I checked most carefully, because it's the one that changes how you read the repo.
divan/txqr did animated QR plus fountain codes in 2018. LT codes rather than RaptorQ, and Grok cites roughly 25 kbps peak on a 13 KB file. Same architecture, same reasoning, eight years earlier.
Blockchain Commons' Uniform Resources have been shipping multipart fountain-coded QR in production for years, and if you've moved a PSBT between a hardware wallet and a host you have used one. The BCR-2020-005 spec is public. That corner of the industry settled on animated fountain QR because it was the correct answer to a real constraint, and it settled a while ago.
There are several browser clones from 2024 to 2026 too: qifi-dev/qrs, qr-stream, bitfountain. Same genus.
And this repo depends on someone else's codec. I checked package.json myself:
"@raptorqr/core": "0.1.1"
"@raptorqr/fast-qr-wasm": "0.1.1"
"@raptorqr/raptorq-wasm": "0.1.1"
All three come from infrost/RaptorQR, a separate project. The codec, the QR encoder and the RaptorQ implementation are upstream. What this repo adds is the QF4 framing, the dual-lane scheduling and a Next 16 front end. That's real work. It isn't a new codec, and the trending post doesn't make that especially clear.
Where shining data at a camera is the right answer
There is a real set of situations here, and it's narrower than "file transfer".
- True air-gapped machines where removable media is banned outright, because a USB stick is a malware vector and a screen isn't.
- Monitored or hostile networks where the thing you're avoiding is opening a socket at all. No packet, no log line.
- No shared layer 2. Guest wifi with client isolation, a corporate laptop next to a personal phone, a captive portal. AirDrop and LocalSend both need a path that doesn't exist.
- VDI, Citrix, browser-only kiosks where clipboard and USB redirection are locked down and the pixels on screen are the only channel left.
- Small secrets. Seed phrases, SSH and GPG keys, PSBT-sized blobs. Hundreds of bytes to low tens of KB, which is seconds even at the pessimistic rate. Hardware wallets landed on this years ago and they were right.
Notice what those have in common. In every one of them the other channels are closed, so the value is that this one exists at all. Speed never enters into it. Once AirDrop, scp or a USB port is permitted, this loses on every axis.
One thing worth saying out loud, because the air-gap framing invites the wrong assumption: this is not confidential. Anybody who can see or film the screen gets the file. The confidentiality is up to you. Encrypt before you transmit if it matters.
What I verified and what I didn't
I fetched the README and package.json before reading Grok's output, so those two are mine.
| Claim | Status |
|---|---|
Built on the three @raptorqr/* packages at 0.1.1 | Verified in package.json |
| RaptorQ, CRC-32, raw byte mode, Brotli/gzip pick-smaller | Verified, stated in the README |
| 1.40 Mbps is nominal, hedged with "before camera loss" | Verified, the author's own wording |
| No published end-to-end benchmark in the repo | Verified, no wall-clock KB/s anywhere |
| Every KB/s figure in this post | Estimated. The decode-loss range is a guess |
| RaptorQR's 184-254 KB/s | Upstream's own claim, not checked by me |
| txqr's ~25 kbps, and the other prior-art attributions | Grok says it fetched them. I did not re-verify |
The verdict
Brilliant as a fountain-coded optical modem demo. Unoriginal as an idea. Impractical as a product. If you want a single number on a dumb-to-brilliant axis, Grok pinned it at 4.5 out of 10 and I'd leave it there.
What I'd take away is smaller than the repo. Fountain codes over a one-way lossy channel, and the dual-lane trick against rolling shutter, are both worth knowing about for any problem where you can't ACK. That's the reusable part. The file-transfer product wrapped around it is a demo of the technique, and it's fine that it is one.
If you want to poke at it: deedy/qr-data-transfer. And if anyone does run a real end-to-end benchmark on a phone, that's the missing number and I'd like to see it.