我的透明 WebM,被 ffmpeg 冤枉了

用 ffmpeg 把绿幕素材抠成透明 WebM,ffprobe 一扫:pix_fmt=yuv420p,看上去透明通道没了。排查了一整晚:滤镜链、编码器、容器结构挨个过了一遍,中途一度「确认」文件缺失关键信号,还专门写了个二进制修补器去补。最后发现这根本不是一桩罪案:文件从头到尾清清白白,「没有透明」只是 ffmpeg 自己的眼睛看错了。这篇记录整场翻案的全过程,附一套以后不会再被坑的验收方法。

案发现场

起因很日常:给博客准备一段透明底的视频素材,走标准绿幕抠像流程,命令是网上抄来微调的:

ffmpeg -i input.mp4 \
  -vf "chromakey=0x00FF00:0.12:0.08,despill=type=green,format=yuva420p" \
  -c:v libvpx-vp9 -crf 30 -b:v 0 -auto-alt-ref 0 -an output.webm

跑完验收,证据一条比一条难看:

  • ffprobe 报告 pix_fmt=yuv420p——链尾明明显式转了 yuva420p,那个 a 去哪了?
  • 想抽出 alpha 平面看看到底抠得干不干净,alphaextract 直接报错:Requested planes not available;
  • 于是写下结论:输出没有 alpha 通道。

第一轮:滤镜链有内鬼?

嫌疑最大的是 despill——它紧排在 chromakey 后面,而视频滤镜圈的常识是:很多滤镜根本不认 alpha 通道,一个不支持 alpha 的滤镜插进链里,像素格式的自动协商就会把 alpha 悄悄转没了,全程无报错。

实验设计很简单:不让输出进编码器,把滤镜链的结果直接导成一帧 PNG(PNG 保留 alpha),逐像素读:

背景像素: (0, 0, 0, 0)      ← 完全透明
主体像素: (201, 31, 32, 255) ← 不透明,颜色正常

滤镜链无罪,despill 无罪——编码之前,alpha 活得好好的。

第二轮:编码器吞了它?

下一个怀疑对象是 VP9 编码器。对照组一路升级:去掉 despill,再去掉整个滤镜链,甚至用 lavfi 现造一个天生带 alpha 的源直接编码——输出的 webm 照样「没有 alpha」。

这下范围锁死了:问题出在「编码 → 容器 → 读回」这条链的某一段,与我那套滤镜组合毫无关系。当时差点就此结案,判定「这套 ffmpeg 的 VP9 编码器不支持 alpha」——后来证明,这个结论离真相只差半步,只是方向反了。

转机:每帧都多出一件行李

换个角度查物证:不问「格式是什么」,问「容器里到底装了什么」。ffprobe -show_packets 把每个数据包翻出来看,出现了本案第一条真正的线索:

  • 每一个数据包都挂着一份 Matroska BlockAdditional 附加数据,一个不少;
  • 流标签里还挂着一条 TAG:alpha_mode=1。

BlockAdditional 是 Matroska/WebM 里「每帧可以携带额外码流」的机制——而 WebM 的透明通道,恰恰就是每帧一条独立的 alpha 码流,存在这里。也就是说:alpha 数据一帧不少地躺在文件里,轨道头也声明了 alpha 模式。

那为什么所有工具都说没有?此时又冒出一个更诡异的细节:ffprobe 显示的 alpha_mode 标签,在文件的原始字节里搜不到这个字符串。标签是真的,字节里却没有——它只能是解复用器读到某个结构后自己「翻译」出来的。

弯路:一个记错的十六进制数

要确认「文件是否按规范声明了 alpha」,就得下到 EBML 层(Matroska 的二进制格式)逐字节看。关键信号元素叫 AlphaMode——我印象里它的 EBML ID 是 0x53B8。

全文件搜字节 53 B8:零命中。

于是结论顺理成章:信号元素缺失,muxer 有 bug。我写了个小小的 EBML 修补器——定位 Video 元素,插入 4 字节的「AlphaMode=1」,把各级父容器的尺寸字段一并修正,自检全部通过,放进浏览器做最终验收。

然后傻眼了:修补后的文件,videoWidth 变成了 640。素材明明是 320 宽。

盯着修补后 Video 元素的十六进制看了半分钟,真相就拍在脸上:0x53B8 不是 AlphaMode,是 StereoMode——我等于给视频贴了张「左右并排的双目立体视频」的标签,浏览器非常敬业地把两只眼睛的宽度拼在了一起。真正的 AlphaMode,是 0x53C0。

拿着正确的 ID 回头看原始文件的 Video 元素,它就大剌剌躺在那里:

... 53 c0 81 01 ...     ← AlphaMode = 1,从第一天起就在

修补器就此作废——此案不需要它。而这个错误还歪打正着地完成了翻案:测试页里,未修补的原件和修补版并排摆在红底上,原件的背景透出了红色。它从头到尾都是透明的。

真相:不是文件瞎,是工具瞎

到这里证据链闭合了,三端各自独立验证过:

  • 编码端:每帧的 BlockAdditional 里都有 alpha 码流——把这些载荷抽出来、包成独立的视频流解码,得到的是一张完美的剪影图:背景亮度 0,主体亮度 255;
  • 容器端:AlphaMode=1 按规范声明;
  • 播放端:Chromium 渲染透明一切正常。

那「没有 alpha」的印象从哪来的?全部来自 ffmpeg 自己的「验收」——真相是:ffmpeg 的 VP8/VP9 原生解码器会整段丢弃 BlockAdditional 里的 alpha 数据。这是一个挂了很多年的已知问题(Trac #11165 的标题就是「默认 vp9 解码器没有正确处理 alpha」,2024 年开的;更早的 #8344 从 2019 年挂到现在,状态还是 new),我核对时 ffmpeg 9.0 刚发布不久:changelog 没有相关条目、解码器源码里没有任何 alpha 处理、邮件列表上 2026 年 7 月还在讨论「丢弃的时候至少警告一句」的补丁——连警告都还没落地。

于是所有基于 ffmpeg 的验收手段——ffprobe 的 pix_fmt、alphaextract、ffmpeg 解码播放——全都在用一双看不见 alpha 的眼睛做鉴定。证词高度一致,但这些证词本身无效。

这里补全 WebM 透明通道的架构,本案的一切都由此解释:alpha 不是像素格式的一部分,而是每帧附带的一条独立 VP8/VP9 码流,挂在容器的 BlockAdditional 里,由轨道头的 AlphaMode=1 声明。主码流永远是 yuv420p——所以 pix_fmt 从来就不是证据;ffprobe 显示的 TAG:alpha_mode=1 是解复用器读到真正的 AlphaMode 元素后翻译给你的元数据,文件里自然搜不到这个字符串。

解药:换一双眼睛解码

ffmpeg 里其实一直存在第二条解码路径:libvpx 解码器从 2019 年起就支持从 BlockAdditional 还原 alpha,只是默认轮不到它——ffmpeg 对 VP9 默认用自家原生解码器。解药就一行,放在输入侧:

ffmpeg -c:v libvpx-vp9 -i input.webm ...   # VP8 素材用 -c:v libvpx

同一个文件,默认解码 yuv420p、alphaextract 报错;强制 libvpx 之后 alpha 平面完整还原,剪影清清楚楚。顺着这条路径,之前搁置的「透明视频慢放」也一并跑通了——光流插帧全链路 alpha 无损:

ffmpeg -c:v libvpx-vp9 -i overlay.webm \
  -vf "setpts=2*PTS,minterpolate=fps=30:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1,format=yuva420p" \
  -c:v libvpx-vp9 -crf 20 -b:v 0 -auto-alt-ref 0 -an output.webm

也就是说,透明素材不必再走「先慢放、后抠像」的绕路——抠好的 WebM 母带随时可以回来做慢放,只要记得解码时换一双眼睛。

这套流程后来用在了正式素材的体检上:593 帧个个带着 alpha 码流,抽出的剪影 82% 全透明、17% 不透明,边缘干净无噪——结案。

透明通道验收工具箱(下次直接抄)

三级检查,按代价从低到高,至少做到第 2 级:

  1. 信号级:ffprobe -v error -select_streams v:0 -show_entries stream_tags,看到 alpha_mode=1 即轨道头声明了 alpha;
  2. 载荷级:ffprobe -show_packets | grep -c BlockAdditional,数量 ≈ 总帧数即每帧都带 alpha 码流;
  3. 内容级(最强,直接看图):ffmpeg -c:v libvpx-vp9 -i x.webm -vf alphaextract -frames:v 1 -update 1 alpha.png,应得一张黑白剪影。

零工具版:文件直接拖进 Chrome/Edge 就能看见透明底(Chromium 自带 libvpx 解码器)。mpv 要手动把解码器点破:mpv --vd=libvpx-vp9,libvpx --background=none x.webm——默认解码路径同样看不见 alpha。

伪判据黑名单——这些都不能作为「没有 alpha」的证据:

  • pix_fmt=yuv420p:主码流本来就是 420,alpha 藏在容器里;
  • 默认解码下 alphaextract 报 Requested planes not available:是原生解码器读不到,不是文件没有;
  • ffmpeg 系播放器/剪辑软件里看到不透明:它们多半也在用那双瞎眼睛。

编码侧两条守则:

  • -pix_fmt yuva420p 和 -auto-alt-ref 0 必须成对出现——VP9 编码 alpha 时不禁用 alt-ref 帧会静默丢失或损坏透明通道,且不报任何错误;
  • crf 对透明素材别太狠:30 的 alpha 边缘肉眼可见地糙,18~24 合适。

尾声

之前写过一桩悬案(一个 p 标签,弄哑了我的 SPA 导航):一个非法的 <p> 标签弄哑了整个 SPA 导航,真凶真实存在,而所有工具集体沉默、console 干净如洗。这一桩正好是它的镜像——罪行根本不存在,所有工具却众口一词地指证。两个案子拼在一起,是同一条教训的正反面:单一工具链的验收结果不构成证据,必须有一条独立信源交叉验证。这次扮演「说真话的孩子」的,是浏览器。

还有一条更技术性的:「格式规范」和「工具实现」是两回事。WebM 规范里 alpha 写得明明白白,ffmpeg 的编码端也严格遵守——但解码端一个挂了很多年的 ticket,就能让整条工具链对着一个完好的文件齐声说谎。下次验收多媒体文件之前,也许先该问一句:我用来「看」的这个工具,它看得见吗?