Air-gapped, one-way file transfer over screen → camera. One device animates a stream of QR codes; another films the screen and reconstructs the file. No network, no pairing, no handshake, no special permissions.
Drop a shared optical-transfer GIF and get the file back — right in your browser, nothing uploaded. Make one on a phone (Share as GIF) or in the viewer's Make a GIF tab.
The transport is built on LT (Luby transform) fountain codes: each QR frame carries
the XOR of a pseudo-random subset of the file's blocks, chosen deterministically from
the frame's sequence number. The receiver collects any ~k·1.15 distinct frames, in
any order, and peels the file out of them. A dropped or blurred frame costs a little
time, never correctness — so there is no back-channel and no retransmission.
This repo is packaging that transport as reusable libraries so mobile (and web) apps can drop it in however they choose.
| Package | Status | What it is |
|---|---|---|
@optical-transfer/core |
✅ working, tested | Pure, dependency-free transport: OpticalSender, OpticalReceiver, fountain codec + frame protocol. Runs on Node, browsers, and React Native (Hermes). |
@optical-transfer/react-native |
✅ iOS device-validated | Sender/receiver components: animated byte-mode QR (react-native-qrcode-svg) + camera capture (react-native-vision-camera v5). Includes a bare-RN example app. Send + receive verified on real iPhones. |
@optical-transfer/gif |
✅ working, tested | Encode a file as a shareable animated GIF of QR frames, and decode a GIF back into the file — asynchronous, offline transfer. Powers the web viewer. |
The repo ships a bare React Native example (packages/react-native/example) — a small
test bench for the transport: pick a media type (or a real photo), tune the transport
(compression, block size, fps), broadcast it as an animated QR stream, and watch another
phone's camera rebuild it. Screens below are iOS (iPhone, iOS 26):
![]() Menu send or receive |
![]() Send pick content · tune transport |
![]() Broadcasting animated QR stream |
![]() Received image rebuilt from frames |
![]() Received text verified & rendered |
Platforms: iOS is device-validated end-to-end (send + receive). On Android, the send side (animated QR display) works today; receive (QR scanning) is iOS-first in Vision Camera v5 and is on the roadmap. See running the example.
Beyond the live screen→camera channel, a file can be packed into a shareable
animated GIF of QR frames (@optical-transfer/gif). This makes
transfer asynchronous — post the GIF anywhere and anyone reconstructs the file,
no two devices in a room. Because the frames are generated (not filmed), decode is
near-perfect, and fountain coding still tolerates a few frames mangled by re-encoding.
- Make a GIF on a phone (Share as GIF) or in the browser.
- Decode a GIF with the self-contained web viewer — drop a GIF, get the file back; nothing is uploaded, it all runs client-side.
import { OpticalSender, OpticalReceiver } from "@optical-transfer/core";
// Sender — turn a file into an endless stream of self-describing frame bytes.
const tx = new OpticalSender(fileBytes, { blockLen: 256 });
for (const frame of tx.stream()) {
renderAsQr(frame); // hand `frame` (Uint8Array) to any byte-mode QR renderer
}
// Receiver — feed decoded QR bytes; it locks on mid-stream and self-verifies.
const rx = new OpticalReceiver();
onQrScanned((bytes) => {
rx.ingest(bytes);
updateProgressBar(rx.progress); // frames-collected, not blocks-solved
if (rx.isComplete) saveFile(rx.result!); // FNV-1a verified before exposed
});The core touches no DOM, canvas, camera, or QR pixels — it only turns seq numbers
into frame bytes and frame bytes back into a file. Platform layers supply the pixels
and the camera.
npm install
npm test # runs the core round-trip suite (lossy + reordered channel)
npm run build # emits packages/core/distThe test suite drives the sender's frame bytes straight into the receiver through a simulated channel that drops 30% of frames and reorders the rest — no camera required — covering mid-stream lock-on, session restarts, a range of file/block sizes, checksum verification, and overhead bounds.
- ✅ Core transport —
OpticalSender/OpticalReceiver, fountain codec, and frame protocol. Unit-tested over a simulated lossy/reordered channel; builds clean. - ✅ React Native layer — sender/receiver components + example app. Typechecks against the real native libs and the example bundles under Metro.
- ✅ On-device iOS — validated end-to-end on real iPhones (iOS 26 and iOS 16): the animated-QR sender and the Vision Camera v5 receiver reconstruct files screen → camera.
- 🟡 Android send — the animated-QR sender is cross-platform and works on Android.
- ⏳ Android receive — v5 QR scanning is iOS-first; a fallback is future work.
The binary-safe QR round-trip (the original sharp edge — scanners hand back UTF-8, not raw bytes) is solved by carrying each frame as base64 text inside the QR, so any off-the-shelf renderer and scanner round-trip it exactly, at ~33% density cost.
- ✅ Done — on-device iOS. Sender + Vision Camera v5 receiver validated on real iPhones (iOS 26 and iOS 16); the example app ships a QR/optical-themed icon.
- ✅ Done — compress the payload before encoding. Optional, pluggable codecs (a 1-byte
self-describing envelope); the RN package ships a
gzipCodec(pako). Fewer blocks → faster transfer for compressible data; auto-skipped when it wouldn't help. - Android receive (a QR-scanning path that isn't v5-iOS-only) — the sender already works cross-platform.
- Optional filename/MIME metadata in core (the example already layers its own
mime\nfilename\nbytesenvelope on top; seeexample/src/payload.ts). - Optionally swap the base64-over-text channel for a raw-bytes frame processor to recover the ~33% density and push throughput.
Air-gapped QR file transfer is an idea several people have reached independently. Two projects are worth knowing — both MIT licensed:
- decimen-optical-transfer — the proof-of-concept this project's fountain codec and frame protocol are derived from (see Attribution).
- mohankumarelec/airgapped-qr-code-transfer
— an earlier, independent web app (Vue) demonstrating the same screen → camera concept. It
gzip-compresses the file (
pako), then streams fixed-size indexed chunks as sequential QR codes and reassembles them by index.
The core difference is resilience: that project uses naive indexed chunking — the
receiver must capture every specific chunk in a single pass, so a missed frame means starting
over. This project uses LT fountain codes, so the receiver reconstructs from any ~k·1.15
frames in any order and tolerates dropped frames without retransmission. Its best idea —
compressing the payload before encoding — we adopted: optional pluggable codecs with a
gzipCodec on React Native (see What's next). No code from that project is
used here; it is acknowledged as independent prior art.
This project is MIT licensed (see LICENSE).
The fountain codec (fountain.ts) and frame protocol (protocol.ts) are derived from
the decimen-optical-transfer
proof-of-concept, which is itself MIT licensed (Copyright © 2026 BashAlarmist). We comply
by preserving the upstream copyright and permission notice in
THIRD-PARTY-NOTICES.md and in each derived file's header — the
only obligation MIT imposes. Everything else in this repo (sender.ts, receiver.ts,
tests, packaging) is original and offered under the same MIT terms.




