节奏解析工具-透明视频导出开发记录

透明谱面视频导出的目标是提供单一视频文件输出,替代 PNG 序列 zip,方便二次剪辑

基于 ffmpeg.wasm v0.12.10 实现

一、VP9 alpha:直接炸内存

VP9 是 libvpx 的新一代编码器,alpha 通道支持更完善,理论上压缩率优于 VP8。ffmpeg.wasm 已编译 libvpx-vp9,因此优先尝试 VP9 alpha

实测启动即 OOM

第一段编码 exec 刚启动就稳定崩溃:

RuntimeError: memory access out of bounds

测试条件:

  • 2 倍导出分辨率(2716×280):崩溃
  • 半分辨率降采样:崩溃
  • ffmpeg.wasm 0.12.6 与 0.12.10 两个版本:仍崩溃

为什么

32-bit WASM 的堆上限约为 2GB。VP9 编码器的上下文结构占用较大,即使在低分辨率下,初始化也接近可用堆上限。加上 wasm 文件系统已为帧 PNG 预留部分空间,直接越界

降采样不能阻止崩溃,如果瓶颈在像素数,降采样应能缓解;照样崩溃说明问题出在 VP9 编码器的内部固定开销,与分辨率无关

结论

VP9 alpha 在 32-bit wasm 下无法分配足够堆内存,下一个


二、VP8 alpha:参数不支持导致黑底

VP9 走不通后,自然想到 VP8。VP8 编码内存开销更小,且 WebM 容器原生支持 alpha

两条路径均失败

路径一:默认配置(-auto-alt-ref=1

ffmpeg -i frame_%05d.png -c:v libvpx -pix_fmt yuva420p -auto-alt-ref 1 out.webm

encoder 初始化阶段直接断言失败:

Transparency encoding with auto_alt_ref does not work

libvpx 中 alt-ref 参考帧使用 Y 通道重建,alpha 通道无法被正确重建,因此 alpha 与 auto-alt-ref 互斥

路径二:关闭 alt-ref + alpha_mode

ffmpeg -i frame_%05d.png -c:v libvpx -pix_fmt yuva420p -auto-alt-ref 0 -alpha_mode 1 out.webm

按官方文档,关闭 alt-ref 后需通过 -alpha_mode 1 在 bitstream header 中标记 AlphaMode=1,告知解码器存在 alpha 通道

结果这个参数在当前 wasm 构建里压根不认:

Unrecognized option 'alpha_mode'

后果

没有 alpha_mode,WebM 文件里的 AlphaMode 字段默认就是 0,所有剪辑软件和浏览器都会将 yuva420p 的 alpha 通道视为 padding 丢弃——输出必然是黑底

结论

VP8 alpha 在本 ffmpeg.wasm 构建下无法生成有效透明 WebM,下一个


三、MOV PNG 段编码 + concat demuxer:累积同步误差

排除 libvpx 两条路径后,可行的透明编码方案仅剩:

MOV 容器 + FFmpeg 内置 PNG 编码器 + pix_fmt rgba

优势:

  • 不依赖 libvpx,编码内存仅为单帧 zlib 压缩缓冲区(峰值不到 30MB)
  • 无损 + 真 alpha,剪辑软件原生识别

初版架构:分段编码

出于内存安全考虑,设计成每 10 帧一段:

  • 每段独立 new/terminate ffmpeg 实例,避免同实例累积损坏
  • 段产物存主线程 Blob,不占 wasm FS
  • concat demuxer 无损拼接所有段,再另起一个实例混入 AAC 音频

崩溃问题与修复

开发过程中修复了若干崩溃问题:

问题根因解决
段 146 累积崩溃同实例多段编码导致永久损坏每段独立创建和销毁实例
92% mux 阶段崩溃两次 exec 导致堆增长冲突分两个独立实例
DataCloneErrorlogger 函数传入 load config,postMessage 时无法克隆改用 on("log") 先绑定回调,load 仅传 URL 对象
alert undefinedwasm abort 抛出非 Error 异常toErr(e) 统一规范化

核心问题:音画累积不同步

此模式下画面比声音越来越慢

同一套画帧逻辑和总帧数计算,在 mp4/zip 模式下完全正常,问题必然出在 MOV 链路——段编码 + concat demuxer + AAC mux

多轮修复均无效

为定位和修复同步问题,先后尝试了以下方案:

  1. 段编码加 -vf settb=AVTB,setpts=N/(FPS*TB) 写死每帧 PTS
  2. 合成后叠加 fps=30 + settb=AVTB + setpts=N/(FPS*TB)
  3. 音频加 asetpts=N/SR/TB 按采样号写死 PTS
  4. 时长从 JS 理论值改为 AudioBuffer 真实物理时长
  5. -frames:v totalFrames 强制输出帧数
  6. -t durSec 强制截流
  7. 调整参数顺序:-r-i 前 + -fflags +genpts + -avoid_negative_ts make_zero

全部无效。

根因:concat demuxer 的累积 rounding

concat demuxer 的段尾 dur 推断与时基 rounding 误差累积不可修复

具体机制:

  • 每段 MOV PNG 的最后一帧 duration,在 concat demuxer 进行 -c copy 合并时,存在不可见的 rounding 误差
  • MOV 容器时基(tbn)约为 1/15360,stts 表段尾做 dur 推断时可能多加 1 个 tbn 的段尾 gap
  • 474 段累积后,视频总时长明显长于音频真实物理时长
  • fps filter + setpts 仅在输入时长范围内重采样,不会缩短总时长或丢帧——输入已偏长,输出仍偏慢
  • -t durSec / -frames:v totalFrames 仅做截断,不修复前面帧的平均间隔问题

结论

只要依赖 concat demuxer copy 合并多段,累积误差就无法通过后处理消除。段编码 + concat 架构整体推翻


四、最终方案:单实例一次性编码

既然 PNG 序列 zip 模式音画 100% 同步,透明视频的帧循环直接复用 zip 的画帧逻辑与时间参数,然后一次性喂给 ffmpeg 编码,同步精度应与 zip 同级

完整流程

单 new FFmpeg() 实例

  ├ 逐帧循环 (idx 0..totalFrames)
  │   drawExportFrame((idx / EXPORT_FPS) * speed)   ← 与 zip 模式参数完全一致
  │   → canvas.toBlob('image/png')
  │   → Uint8Array
  │   → inst.writeFile("frame_" + pad(idx,5) + ".png", u8)
  │   每 5 帧更新进度 (0% → 75%)

  ├ 有音频时: audioBufferToWav(mixed) → writeFile("audio.wav")

  ├ 一次 exec 编码
  │   -f image2 -start_number 0 -r FPS -i frame_%05d.png
  │   -i audio.wav
  │   -fflags +genpts -avoid_negative_ts make_zero
  │   -map 0:v:0 -map 1:a:0
  │   -vf fps=FPS:round=near:start_time=0,settb=AVTB,setpts=N/(FPS*TB)
  │   -frames:v totalFrames
  │   -c:v png -pred mixed -pix_fmt rgba
  │   -af asetpts=N/SR/TB
  │   -c:a aac -b:a 192k -ar 48000 -ac 2
  │   -t durSec
  │   -f mov out.mov

  ├ 删除所有帧 PNG + audio.wav(释放 FS 空间)

  ├ readFile out.mov → Uint8Array

  └ Blob → 格式转换 → 下载

几个需要注意的参数:

  • -f image2 -start_number 0 -r FPS 必须置于 -i 之前:image2 demuxer 的参数顺序敏感,放错位置会被当作输出参数,时间戳生成直接乱掉
  • fps=FPS:round=near:start_time=0:对齐帧率网格,从零时刻开始计数
  • settb=AVTB, setpts=N/(FPS*TB):切换时基后按帧号直接写死 PTS,完全绕过 demuxer 的 dur 推断,从根源消除 rounding
  • asetpts=N/SR/TB:按采样号写死音频 PTS,抵消 AAC 编码器 pre-roll 偏移
  • -frames:v totalFrames + -t durSec 双保险:视频帧数与音频时长双重约束,末尾截流对齐

验证结果

  • 无 OOM 完成整张谱导出与下载
  • 音画全程同步
  • 透明通道有效

好耶,可以睡觉了!

评论

No comments yet. Why don’t you start the discussion?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注