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

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

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

趁数字还在手上记一下。

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

浏览器能解这个容器,就用 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

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

相关推荐
山顶夕景10 天前
【全模态】音视频理解模型Audio-Visual Flamingo
音视频·video·vlm·多模态理解
mengyuxuan23 天前
我写了个不用上传的浏览器视频压缩工具,顺便记一个坑了我小半天的 bug
video·compressor·private·compress
YMWM_1 个月前
video.preset的值为null和ultrafast的区别
linux·video
跟着珅聪学java8 个月前
HTML5 Video Controls 属性深度教程
video
core5128 个月前
[硬核解析] 从感知到交互:InternVideo 1/2/2.5 全系列架构演进与原理解析
架构·大模型·交互·视频·video·intern
davenian9 个月前
< Chrome Extension: Video DownloadHelper > 获得 Premium 权限 Ver10.0.271.2
chrome·edge·windows 11·video·downloadhelper
打小就很皮...9 个月前
React VideoPlay 组件封装与使用指南
前端·react.js·video
清水迎朝阳1 年前
火山 RTC 引擎9 ----集成 appkey
实时音视频·video·rtc·appkey
S&Z34631 年前
[FPGA Video IP] Video Processing Subsystem
网络协议·tcp/ip·fpga开发·video