cimbar-bigfile 是一层纯前端包装器,没有自己的 wasm 模块;它复用 libcimbar v0.6.4 官方发布的 wasm 引擎,在 JS 层实现”切片 + 多 fountain stream 编码 + 接收端原生分桶 + 浏览器拼接”的工作流。
┌─────────────────────────────────────────────────────────────────────────┐
│ cimbar-bigfile 架构 │
└─────────────────────────────────────────────────────────────────────────┘
[send.html] [reassemble.html]
发送端 (电脑浏览器) 拼接端 (任意浏览器)
│ │
│ 原文件 (e.g. 100MB) │ 收到的 N+1 个文件
│ ↓ JS 切片 + SHA256 │ ↓ 拖拽解析
│ manifest.json + N 个 chunk Uint8Array │ ↓ 校验 SHA256
│ ↓ 调 cimbar wasm encoder │ ↓ 顺序拼接
│ Module._cimbare_init_encode(name, len, id) │ ↓ 全文 SHA256 校验
│ Module._cimbare_encode(data, len) │ ↓ 触发下载
│ ↓ 渲染帧动画到 canvas │ 原文件 (100MB)
│ │
└─→ 屏幕 └───────────────────────┘
↓
彩色码动画 (Mode B, 默认 15 fps)
↓
┌────────────────┬──────────────────────────────┐
│ [手机 CFC] │ [recv.html — PC 接收端] │
│ (不修改) │ (本项目新增, 摄像头/屏幕捕获) │
│ │ │
│ 对屏幕拍摄帧 │ getUserMedia / getDisplayMedia │
│ ↓ 4 worker │ ↓ video element │
│ ↓ fountain │ ↓ vendor recv-worker (复用) │
│ decode │ _cimbard_scan_extract_decode │
│ ↓ 分桶 │ ↓ 主线程 fountain decode │
│ ↓ 落盘 N+1 │ _cimbard_fountain_decode │
│ 文件 │ ↓ 完成即 recover + 流式解压 │
│ │ ↓ 内存存储 + 自动拼接 + 下载 │
│ │ 原文件 (无需转存) │
└────────────────┴──────────────────────────────┘
Send Client Display CFC (Android) Reassemble Client
(Browser) (Screen) (Phone) (Browser, later)
│ │ │ │
│ user drops file │ │ │
│←──────── │ │ │
│ │ │ │
│ slice + SHA256 │ │ │
│ build manifest │ │ │
│ │ │ │
│ encode_init │ │ │
│ ("manifest.json",│ │ │
│ ID_BASE+0) │ │ │
│ feed manifest │ │ │
│ render frames ──▶│ flicker barcodes │ │
│ │ ─────────────────▶ │ extract │
│ │ │ fountain_decode │
│ │ │ stream(ID_BASE+0) │
│ │ │ ↓ done │
│ │ │ save manifest.json │
│ │ │ to user-chosen dir │
│ │ │ │
│ encode_init │ │ (camera keeps │
│ ("name.part00", │ │ scanning) │
│ ID_BASE+1) │ │ │
│ feed chunk 0 │ │ │
│ render frames ──▶│ ───────────────▶ │ stream(ID_BASE+1) │
│ │ │ ↓ done │
│ │ │ save part00.bin │
│ │ │ │
│ ... repeat for chunks 1..N-1 ... │ │
│ │ │ │
│ all chunks sent │ │ all N+1 files saved │
│ (loop for fountain redundancy) │ │
│ │ │ │
│ user transfers files │
│ to reassemble client (USB / cloud / whatever) │
│ ─────────────────────────────────────────▶ │
│ │ user drops files
│ │ parse manifest
│ │ verify each chunk SHA256
│ │ concat ──▶ output
│ │ verify full-file SHA256
│ │ trigger download
完整 API 表见 ../vendor/README.md。这里只列我们实际调用的:
// 配置编码模式(每次会话开始时调一次)
void cimbare_configure(int modeVal, int frameRate);
// modeVal: 4=4C, 66=Bu, 67=Bm, 68=B (我们用 68)
// frameRate: -1 = 不变
// 启动一个新 fountain stream(每个 chunk 调一次)
int cimbare_init_encode(const char* filename, size_t filename_len, int fountain_id);
// fountain_id: -1 自动递增;显式传入则我们手动控制 encode_id
// 拿编码器期望的内部 streaming buffer 大小
int cimbare_encode_bufsize();
// 喂入一段数据(多次调用直到喂完)
int cimbare_encode(const uint8_t* data, size_t len);
// 文件喂完后再调一次 len=0 作为 flush
// 渲染当前帧到 canvas
void cimbare_render();
// 推进到下一帧,返回累计帧计数
int cimbare_next_frame(bool color_balance);
int cimbard_get_bufsize();
void cimbard_configure_decode(int mode);
int64_t cimbard_fountain_decode(const uint8_t* buf, size_t len); // 返回完成的 encode_id 或 0
int cimbard_get_report(uint8_t* out, size_t max_len); // 写入 JSON 进度数组
int64_t cimbard_get_filesize(int64_t encode_id);
int cimbard_get_filename(int64_t encode_id, uint8_t* out, size_t max_len);
cimbar 的 fountain code 设计为单个 encode_id 编码一个文件。同一个 encode_id 内的所有帧组成一个完整的 fountain stream,接收端用 wirehair 解出原数据。
我们需要传多个独立的”文件”(manifest + N 个数据块),每个都需要 CFC 当作独立 stream 来解码。encode_id 是 fountain metadata 的一个字段,CFC 的 fountain_decoder_sink 用 unordered_map<stream_slot, fountain_decoder_stream> 按 encode_id 分桶。
只要发送端给每个 chunk 用不同 encode_id(且接收端 metadata 字段足以表达),CFC 就会自动认为是 N+1 个独立 stream,并发解码各自完成。
不同会话间需要避免 encode_id 碰撞——CFC 已经认为某 encode_id “完成”了,不会再重新解码同 ID 的 stream。如果新会话用了已用过的 encode_id,CFC 会忽略。
时间戳秒数低 16 位每 18 小时循环一次,足够应付实际使用频率;每会话起 N+1 个连续 ID,N 通常 < 100,碰撞概率极低。
libcimbar 的 ZSTD 压缩 header 自带文件名字段(由 _cimbare_init_encode 写入)。CFC 解压时直接用这个字段作为保存文件名,完全无需我们额外传。这让 CFC 在没有任何应用层协议改动的情况下就能用对的文件名落盘。
我们利用这个机制:每 chunk 用合成名(<basename>.partNN.bin),让 CFC 直接把块按这个名字保存。拼接端按文件名匹配 manifest 里的 chunks[].index 即可。
libcimbar v0.6.4 的 wasm encoder 没有暴露”当前 fountain stream 已编码完一轮”的回调。_cimbare_next_frame 返回累计帧数,但没有”循环重启”信号。
可能的方案:
bytes / 7500 * redundancy_factor 估算需要的帧数_fes->blocks_required() * 8 这种计算,但未暴露给 wasm export估算方案的好处是简单可调(用户能改 redundancy 输入框),坏处是可能浪费时间(多发了不需要的帧)或不够(接收端没解出来就被切走,下一轮再补)。fountain 码的好处是即使切走也能在下次循环补齐,所以容错性好。
fountain code 的特性是”持续编码越多冗余越好”。我们的 wrapper 把所有 chunk 各发一遍后回到第 0 个数据块继续循环(manifest 已发完不需要再发)。CFC 在所有 stream 都完成前会持续利用新帧补齐丢失的部分。用户看到全部块都被 CFC 接收并保存后再手动停止发送即可。
复用 vendored 未修改的 recv-worker.2026-01-20T0312.js + 主线程 fountain sink,与上游 recv.html 相同的两段式分工:
video element (getUserMedia / getDisplayMedia)
↓ requestVideoFrameCallback (无/停滞则 rAF + drawImage 回退, 见看门狗)
VideoFrame.copyTo → 原生 NV12/I420/RGBA 紧密排列字节
(缩放、旋转或其他像素格式走 Canvas → RGBA)
↓ postMessage 轮转派发到 N 个 worker (N = min(4, hardwareConcurrency/2))
worker: _cimbard_scan_extract_decode(img, w, h, format, buf, len) ← 图像→fountain frame bytes
↓ postMessage 回主线程
主线程: _cimbard_fountain_decode(bytes) → int64: 0 未完成, >0 = 完成 encode id
↓ 完成即同步处理
_cimbard_get_filename(id) → 内部触发 recover() 释放 stream slot
_cimbard_decompress_read(id) 循环 → 解压字节 → 内存 Map<name, Uint8Array>
↓ manifest.json 到达 → 解析 → 预期文件清单 + 逐块 ✓ 状态
↓ 全部收齐 → 自动拼接 + 逐块/全文 SHA256 校验 + 下载原文件
fountain_decoder_sink 最多同时容纳 8 个 stream(stream_slot = encode_id & 0x7,unordered_map<uint8_t, ...>)。wasm 构建无 onStore 回调,stream 完成后只有 recover() 才从 _streams 移除(mark_done)。若完成后的 stream 不立即 recover:
try_emplace 时命中旧条目,s.data_size() != md.file_size() 返回 -12 丢帧(尺寸不同时);因此 handleCompleted 在 fountain_decode 返回完成 id 后同步执行 get_filename(recover)→ decompress_read 串流。解压全程在主线程同步完成(10MB 约几十 ms),换来的是无竞态、无污染;这与 CFC 原生行为一致。接收端无需 CFC 的”每块弹保存框”,块直接进浏览器内存。
requestVideoFrameCallback 在窗口最小化/完全遮挡/无合成器环境下可能永不回调(实测:隐藏窗口下 4s 内 0 回调,rAF 也被浏览器暂停)。页面加 1.5s 看门狗:等待回调超时就切到 rAF + drawImage + getImageData,本次采集继续使用回退路径。浏览器缺少视频帧回调或对应取消 API 时,也使用回退路径。窗口完全隐藏时两条路径都会被浏览器节流,接收窗口必须保持可见。
页面记录每个待执行回调的类型,停止采集或切换到回退路径时调用对应的取消 API。每次采集使用独立的 generation;异步像素复制和 Worker 返回结果都检查它,过期结果会被丢弃。重新启动时会重新选择采集路径。
直接复制使用 visibleRect 和显式的平面 offset/stride,并检查复制返回的布局。只有显示尺寸与复制尺寸一致、方向无需变换、格式为 RGBA 或偶数宽高的 NV12/I420 时才走这条路径;其他情况交给 Canvas 渲染后取 RGBA,确保 Worker 收到的格式、尺寸与像素数据一致。
recv.standalone.html 由 scripts/build-standalone.py 生成:主线程 glue 与 send 版相同(base64 → Module.wasmBinary);worker 无法在 file:// 下 importScripts,故把 glue + recv-worker 源码与 Module.wasmBinary 注入拼成 blob worker(URL.createObjectURL),替换 recv.html 中的 WORKER_URL 标记行。
cimbar-bigfile/
├── send.html
│ └── vendor/cimbar-wasm-v0.6.4/cimbar_js.2026-01-20T0312.js
│ └── vendor/cimbar-wasm-v0.6.4/cimbar_js.2026-01-20T0312.wasm
├── recv.html (PC 接收端)
│ ├── vendor/cimbar-wasm-v0.6.4/cimbar_js.2026-01-20T0312.js (+ wasm)
│ └── vendor/cimbar-wasm-v0.6.4/recv-worker.2026-01-20T0312.js (未修改复用)
├── reassemble.html (零依赖,纯 JS)
├── scripts/
│ ├── build-standalone.py (send/recv 单文件构建)
│ ├── test-round-trip-100mb.js
│ └── pipeline-test.html (wasm 编解码管线测试页, 无光学链路)
└── docs/
├── manifest-spec.md (协议规范)
└── architecture.md (本文件)
reassemble.html 不依赖 wasm,完全纯 JS。这意味着即使将来 libcimbar 大改 API,只要 manifest 协议不变,已有备份的拼接端永远能用。
recv.html 解码侧只复用未修改的 vendored 文件(glue + wasm + recv-worker),本仓库原创逻辑全部在 recv.html 内联脚本中;vendor 目录保持与上游 v0.6.4 release 位一致。