optical-transfer

license core react-native status live GIF viewer

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.

Try the live GIF viewer

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.

Packages

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 example app

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.

Shareable GIF transfer

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.

Core API

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.

Development

npm install
npm test         # runs the core round-trip suite (lossy + reordered channel)
npm run build    # emits packages/core/dist

The 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.

Status

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.

What’s next

Air-gapped QR file transfer is an idea several people have reached independently. Two projects are worth knowing — both MIT licensed:

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.

Attribution & license

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.