用 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 级:
- 信号级:
ffprobe -v error -select_streams v:0 -show_entries stream_tags,看到alpha_mode=1即轨道头声明了 alpha; - 载荷级:
ffprobe -show_packets | grep -c BlockAdditional,数量 ≈ 总帧数即每帧都带 alpha 码流; - 内容级(最强,直接看图):
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,就能让整条工具链对着一个完好的文件齐声说谎。下次验收多媒体文件之前,也许先该问一句:我用来「看」的这个工具,它看得见吗?