在浏览器里做视频压缩,我踩的三个坑

我做了个纯客户端的视频压缩工具,整个过程不上传文件。

下面三件事里有两件是靠掐表测出来的,代码审查和单元测试都没抓到,

趁数字还在手上记一下。

管线分三条路:文件本来就达标,就直接复制编码样本,一帧都不重编;

浏览器能解这个容器,就用 WebCodecs 转码;两者都不行,退回 ffmpeg.wasm,

慢一个数量级。解复用和封装用的是 mediabunny。

AVI 直接 -c copy 到 Matroska,产出 1151 字节

快速路径要求 mediabunny 能读懂容器。它读不了 AVI,但很多 AVI 里装的是 H.264,

所以思路是无损换个容器、继续留在快速路径上:

ffmpeg -i in.avi -map 0✌️0 -map 0:a? -c copy

-avoid_negative_ts make_zero out.mkv

这条命令是失败的,而且失败得很直接:

matroska Timestamps are unset in a packet for stream 0

matroska Can't write packet with unknown timestamp

AVI 不存逐包时间戳,它靠固定帧率加一个索引,让解复用器自己算。

而 Matroska 要求每个 block 都有时间戳,复制过去就无从下笔。

结果是一个 1151 字节、只有头没有 cluster 的文件,它是合法的 Matroska,

只是里面没有媒体数据。

-avoid_negative_ts 治不了这个。我卡在这里比应该的时间长,

因为时间戳不是「负数」,是「不存在」。两个不同的问题,报错长得像。

修法是输入侧加一个 flag:

ffmpeg -fflags +genpts -i in.avi ...

+genpts 会按流的帧率合成出 PTS。20 秒 1080p30 的素材,产出从 1151 字节

变成 55.8 MB,600 帧全部解码正常。

然后做了个对照组:同样的分辨率、时长、目标,但编码换成 Xvid,

这样它必须走 ffmpeg 那条路。

H.264 AVI,换容器后走 WebCodecs 7.5 秒 53.3 MB → 12.3 MB

Xvid AVI,走 ffmpeg.wasm 53.4 秒 48.2 MB → 12.5 MB

输出体积差不到 2%,墙上时间差 7 倍。这还是无头 Chromium 的软件编码,

真机上有硬件编码器会更好。

另外提一句:ffmpeg.wasm 是把退出码 resolve 出来而不是 reject,

而中途死掉的 muxer 仍会留下一个能读的文件。不检查退出码的话,

截断的产物和完整的产物长得一模一样。

所有 AAC 音轨都会让 mediabunny 的复制路径失效

passthrough 这条路本该什么都不动:给 mediabunny 一个空的 video 配置,

它就直接复制编码样本,因为 forceTranscode 默认是 false。

视频确实复制了,音频没有。3.3 MB 的源出来变成 3.6 MB,于是我去拆 MP4 的 box:

源 moov 33,948 mdat 3,432,287

产物 moov 17,879 mdat 3,788,908

moov 反而更小,增长全在 mdat。分轨看:

avc1 900 个样本 3,071,520 字节 (和源一字节不差)

mp4a 1295 个样本 717,380 字节 (源是 360,476)

视频是精确复制,音频翻了一倍:源 96 kbps,出来约 191 kbps。

原因在 mediabunny 的复制条件里。快速路径要求 !needsTrimming,

而 needsTrimming 是 firstTimestamp < startTimestamp。直接问库:

video codec: avc firstTimestamp: 0

audio codec: aac firstTimestamp: -0.023219954648526078

是负的。而 44100 Hz 下 0.0232 秒正好是 1024 个样本,也就是一个 AAC 帧。

这就是编码器的 priming delay,任何正常编码器产出的 AAC 都带。

所以音频复制路径不是偶尔不可用,是永远不可用。

这个在调用侧绕不过去:传 bitrate 想控制体积,本身就会强制转码,

因为 !trackOptions.bitrate 也是复制条件之一。什么都不传,

它就按自己的默认码率重编。

我最后加了个下限:如果 passthrough 产出的字节数不小于输入,

且源本来就是 MP4,就直接把源文件还回去。实测字节完全一致,

3,466,275 进、3,466,275 出,原来是 3,806,815。对一个本来就接近最优的文件,

「无事可做」比「大了 10%」诚实得多。

多线程的 ffmpeg core 要拿广告收入去换

@ffmpeg/core-mt 比单线程快 4 到 8 倍,但它要 SharedArrayBuffer,

就要跨源隔离,也就是要发 COOP 和 COEP 头。

而 COEP: require-corp 会打断没有主动 opt-in 的第三方嵌入。

对一个靠 AdSense 的站,这不是技术权衡,是收入决策。

我留在单线程,把力气花在「尽量不走到 ffmpeg」上,也就是上面那条换容器的路。

想说的一点

两个真 bug,代码审查和单元测试都没抓到。AVI 那个当时测试全绿、

实现看起来也挺合理。真正找到它的是把一个真实的 AVI 拖进真实的浏览器,

然后发现 54 MB 的输入产出了 1151 字节。

有兴趣可以拿手上难搞的文件试试。被问得最多的两个场景各有单独的页面:

把视频压到 Discord 的 10MB 以内、

以及把视频压缩到 10MB。

所有处理都在你自己机器上。

相关推荐
Cerman20 小时前
VESA 发展史介绍
dp·video·monitor·vesa·edp
不当菜鸡的程序媛1 个月前
Wan 1.3B 结构维度
video
不当菜鸡的程序媛1 个月前
video中的memory kv cache
video
Cerman2 个月前
UEFI GOP driver基础协议解读
video·uefi·gop·edid·gfx
山顶夕景3 个月前
【全模态】音视频理解模型Audio-Visual Flamingo
音视频·video·vlm·多模态理解
mengyuxuan3 个月前
我写了个不用上传的浏览器视频压缩工具,顺便记一个坑了我小半天的 bug
video·compressor·private·compress
YMWM_4 个月前
video.preset的值为null和ultrafast的区别
linux·video
跟着珅聪学java1 年前
HTML5 Video Controls 属性深度教程
video
core5121 年前
[硬核解析] 从感知到交互:InternVideo 1/2/2.5 全系列架构演进与原理解析
架构·大模型·交互·视频·video·intern