bashalarmistalt/decimen-optical-transfer
GitHub: bashalarmistalt/decimen-optical-transfer
仅通过屏幕显示动态二维码、摄像头扫描解码的方式,在无网络连接的设备间实现文件传输的浏览器端概念验证。
Stars: 2152 | Forks: 250
# Decimen Optical Transfer:基于 fountain code 的二维码文件传输
仅通过**屏幕和摄像头**即可在两台设备之间发送文件。
一个页面将文件显示为源源不断的动态二维码流;另一台
设备将摄像头对准它并重建文件。**设备之间无需网络连接,无需应用,无需配对,除了摄像头外无需任何权限。**
数据以光的形式传输。
这是从一个更大型的实验中提取的最小概念验证(PoC),原实验通过更密集的帧、
多二维码网格和纠错颜色通道,实现了手机到手机间 **128 KB/s** 的传输速度。此 PoC 仅保留了
核心技术,以适宜的速率传输 512 KB(或 2 MB,可在发送方设置中选择)的图像。
:5173/receive/`),接受一次证书警告,
点击 **Start camera**,然后将摄像头对准二维码。
- 几秒钟后:显示 *Transfer Complete!* 以及通过
哈希校验的接收图像。
**为什么开发服务器仅支持 https:**接收方使用了 `getUserMedia`,
而浏览器在不安全的来源上会彻底移除该 API:手机通过纯 http
访问你的开发服务器将完全无法使用摄像头(`localhost` 豁免,
但你的手机不是 localhost)。这是 Web 平台的规则,别无选择。因此,开发服务器自带了
自签名证书
(`@vitejs/plugin-basic-ssl`);首次访问时浏览器会发出警告。点击
“Show Details”然后点击“visit this website”(iOS),或者点击“Advanced”然后点击“Proceed”
(Android/桌面端),此时页面仍处于安全上下文中,因此摄像头
可以正常工作。Vite 打印的那个看起来很奇怪的 `lvh.me` 主机是一个公共便捷
域名,它解析到 127.0.0.1(同一台机器,没有运行额外的服务)。
拿稳手机,或者最好是把它靠在固定物体上。手持
抖动导致摄像头不断自动对焦是吞吐量的头号杀手。
## 工作原理
**单向通道问题。** 屏幕到摄像头的链路没有反向通道:
接收方无法请求重传,并且不可避免地会丢失帧
(模糊、刷新跨越、自动对焦)。循环播放帧并抱有侥幸心理是
极其痛苦的:错过一帧,你就得等上一整个周期才能再次看到它。
**Fountain code 彻底解决了这个问题。** 发送方从不直接发送文件的
数据块。每一帧都是数据块的伪随机*子集*的 XOR(异或);
该子集由帧的序列号确定性推导得出,
子集大小则基于鲁棒孤子分布([Luby transform
coding](https://en.wikipedia.org/wiki/Luby_transform_code))。接收方
按任意顺序收集**任意** ~K·1.15 个不同的帧,并从中剥离出
文件。丢失的帧只会消耗一点时间,绝不会影响正确性。发送方
和接收方的帧率完全不需要匹配。
**每一帧都是自我描述的。** 一个 20 字节的头部携带了 session id、
序列号、块数/大小、文件长度和一个哈希值。没有
握手过程:接收方可以在传输过程中锁定数据流,并且重启
发送方(新的 session id)会自动重置接收方。
**解码。** Safari 从未提供过 `BarcodeDetector`(WebKit bug 281848),
因此解码使用的是编译为 WASM 的 [zxing-cpp](https://github.com/zxing-cpp/zxing-cpp),
在由 `requestVideoFrameCallback` 驱动的 worker 中运行。繁忙的 worker
意味着会丢帧,而 fountain code 可以轻松吸收这些丢帧。
## 内置于本 PoC 的来之不易的细节
- **JS 引擎在 `Math.log` 上存在分歧**(它是实现近似值)。
发送方和接收方必须构建位级完全相同的孤子分布,因此
`fountain.ts` 包含了一个由精确定义的 IEEE-754 操作构建的确定性对数。V8 与 JavaScriptCore 的不同步是一种隐蔽且彻底的失败模式。
- **iOS 在摄像头帧率上撒谎。** `frameRate: {ideal: 60}` 会静默地
提供 30 帧;你必须要求 `{exact: 60}`(适用于 1280 宽的捕获)
并提供后备方案。始终要读取 `getSettings()` 的返回值。
- **`requestVideoFrameCallback` 链在其流结束后依然存活**,并在
下一个流上恢复;如果没有代计数器,每次停止/启动都会泄漏一个
僵尸捕获循环。
- **进度条必须跟踪已收集的帧,而不是已解决的块。** LT
剥离会延后其解决级联:块计数的进度在大部分
传输过程中看起来停滞不前,然后瞬间跃升至 100%。
- **二维码的纠错级别设置为最低 (L)。** 帧内 ECC 和
fountain 层解决的是不同的问题(损坏与擦除),但在
这些帧大小下,级别 L 加上帧丢弃是更好的权衡。
## 调优
两个页面都有一个折叠的 **Settings** 面板。在发送方:payload 大小
(512 KB 或 2 MB)、tx fps、每帧字节数、纠错级别和
显示大小。更改任何内容都会重启流,接收方会
根据新的 session id 自动重置。在接收方:捕获宽度、
捕获 fps 和解码 worker 数量,这些在摄像头启动时应用。
| 设置 | 默认值 | 备注 |
|---|---|---|
| tx fps | 24 | 每一帧必须至少占据显示屏的 2 个刷新周期 |
| bytes / frame | 1465 (QR v27) | 如果接收方仍能解码,更密集的帧会更快;2953 (v40) 在近距离手机对手机传输时有效 |
在完全相同的架构加上更密集的帧、120 fps ProMotion 发送方和
堆叠代码的情况下,父实验测得的传输上限:手持约为 128 KB/s,固定状态约为 186 KB/s。
## 类似项目
此处的概念是独立得出的。事实证明
有几个人也有过类似的想法,他们的作品都
值得一看:
- [mohankumarelec/airgapped-qr-code-transfer](https://github.com/mohankumarelec/airgapped-qr-code-transfer):
基于浏览器的二维码文件传输,带有压缩和顺序分块。
在公开演示此项目后发现的;这是趋同进化
的实际案例。
- [divan/txqr](https://github.com/divan/txqr)(2018 年):动态二维码加上
Go 中的 fountain code,并配有两篇优秀的文章,阐述了为什么 fountain coding
胜过顺序循环。
- [sz3/libcimbar](https://github.com/sz3/libcimbar):完全放弃了二维码,转而使用
专为该通道构建的自定义高密度彩色代码。
使用 [node-qrcode](https://github.com/soldair/node-qrcode) 和
[zxing-wasm](https://github.com/Sec-ant/zxing-wasm) 构建。
## License
MIT
Mid-transfer: a phone pulling a 2 MB image out of the air at 129 KB/s.
## 试用 ``` npm install npm run dev ``` - 在**发送**设备上(理想情况下为笔记本电脑):打开 `https://localhost:5173/send/`,它会立即开始流式传输。将 屏幕亮度调到最高会有所帮助。 - 在**接收**设备上(手机):打开 Vite 打印的 `Network` URL (`https://标签:AI工具, 二维码, 光学通信, 喷泉码, 文件传输, 自动化攻击, 音视频处理