FFmpeg 9.0「Lei」深度拆解:当音视频框架决定把滤镜链整个搬进 GPU——Vulkan 统一计算层、SMPTE 2094-50 元数据透传与 ONNX Runtime DNN 后端的三条暗线
2026 年 8 月 4 日,FFmpeg 9.0 发布,代号 Lei。
接下来 48 小时,中文技术圈几乎被同一个信息刷屏:这个代号纪念的是一位十年前离世的中国音视频开发者,社区里大家叫他「雷神」。在 2013 到 2016 那几年,AVFormatContext、AVCodecContext、AVPacket、AVFrame、av_read_frame 的调用顺序、pts/dts 到底谁先谁后、sws_scale 的 stride 为什么老是对不齐——这些今天一搜一大把的东西,当年在中文互联网上几乎只有一个人在成体系地写。很多人第一次跑通 ffmpeg -i 之外的东西,是照着他的博客一行行敲出来的。
这件事值得被记住。但作为程序员,我更在意另一件事:几乎所有报道都停在了「新增动画 WebP 解码器」这一行,然后就没有然后了。
而 9.0 这个版本,据社区统计有 2200 多次提交、1781 个文件变更、净增约 5.2 万行代码、160 多位贡献者参与,七个库(libavutil、libavcodec、libavformat、libavdevice、libavfilter、libswscale、libswresample)的 ABI 主版本号全部上跳。距离上一个版本 8.1「Hoare」只隔了四个半月。
这个体量,不是「加了几个解码器」能解释的。
我把 9.0 的完整 Changelog 拉下来逐条读完,又往回追了 8.1、8.0、7.1 的条目,得到的结论是:FFmpeg 正在同时推进三条互相独立、但方向高度一致的架构主线。这三条线加起来,指向同一个定位迁移——
FFmpeg 正在从「一个转码器」,变成「像素与元数据的操作系统」。
这篇文章不复述 Changelog。我们逐条拆这三条主线,讲清楚每条线的历史包袱、技术本质、以及对你手上那条转码流水线到底意味着什么。文中所有可执行的部分我都会给命令,但有一个原则我先说在前面:凡是我不能确认的具体参数名、滤镜名、bsf 名,我不会编,我会给你查询命令。 这也是使用一个刚发布四天的大版本时唯一靠谱的姿势。
一、先看清楚:主版本号跳的是什么
在讨论新特性之前,得先说清楚 9.0 为什么是 9.0 而不是 8.2。
FFmpeg 的版本号规则很朴素:只要有任何一个库发生 ABI 不兼容变更,主版本号就要跳。 9.0 是七个库全跳,这是一次彻底的 ABI 断代。
对使用者来说,这意味着三件完全不同层级的事:
第一层:只用 ffmpeg / ffprobe / ffplay 命令行的人。
基本无感。CLI 层的兼容性 FFmpeg 一向保得很紧,除非你踩到了被移除的选项(下面会讲 NVENC 的坑)。
第二层:链接 libav 系列动态库的人。*
必须重新编译。libavcodec.so.61 变成新的 soname,旧的 .so 不会被自动满足。如果你的发行版同时装了两代 FFmpeg,ldd 一下你的二进制,确认它到底链到了谁:
# 看你的程序实际链接的是哪个版本
ldd ./your_transcoder | grep -E 'libav|libsw'
# 看系统里到底有几代 FFmpeg 库共存
ldconfig -p | grep -E 'libavcodec|libavformat' | sort
# 运行时确认真实加载的版本(比编译期版本更可信)
./your_transcoder 2>&1 | head -5
第三层:写 C 代码直接调 API 的人。
这一层要看具体删了什么。9.0 明确移除的有:
Remove CELT decoding support(不影响 Opus 内部的 CELT 层,删的是独立的 CELT 编解码器)Remove ogg/celt parsingRemove deprecated NVENC options and support for pre-11.1 SDK versions
第三条是最容易在生产环境炸的。如果你的构建机上 NVIDIA Video Codec SDK 还停在 11.0 或更早,9.0 直接不给你编 NVENC。而很多公司的 GPU 转码机是「装完就不动」的,SDK 版本可能已经躺了三四年。
升级前先跑这个检查:
# 1. 确认 nvenc 是否还在编出来的二进制里
ffmpeg -hide_banner -encoders 2>/dev/null | grep -i nvenc
# 2. 确认驱动/SDK 版本
nvidia-smi --query-gpu=driver_version --format=csv,noheader
# 3. 关键:把你线上正在用的 nvenc 参数逐个验一遍
# 被移除的 deprecated 选项不会「警告后忽略」,而是直接报错退出
ffmpeg -hide_banner -h encoder=h264_nvenc | sed -n '/AVOptions/,$p'
第 3 步是重点。FFmpeg 对 deprecated 选项的移除策略是硬移除,不是软降级。 一条跑了三年的转码命令,升级后可能因为一个 -rc 或 -cq 的老写法直接退出码非零。如果你的调度系统只看退出码不看 stderr,这会表现为「升级后转码任务大面积失败但日志里啥也没有」。
我的建议是:任何 FFmpeg 主版本升级,都必须先把线上全量转码命令模板抽出来,在新二进制上空跑一遍参数解析。 具体做法是构造 1 秒的测试源,用真实参数跑通:
# 生成一个 1 秒测试源,用来做参数冒烟测试
ffmpeg -y -f lavfi -i testsrc2=size=1280x720:rate=30:duration=1 \
-f lavfi -i sine=frequency=440:duration=1 \
-c:v libx264 -c:a aac /tmp/smoke.mp4
# 把线上模板里的输入换成 /tmp/smoke.mp4,逐条跑
# 只关心「参数能不能解析」,不关心画质
while read -r cmd; do
if ! eval "${cmd/INPUT//tmp/smoke.mp4}" >/dev/null 2>/tmp/err.log; then
echo "FAIL: $cmd"
tail -3 /tmp/err.log
fi
done < /path/to/your/cmd_templates.txt
这个脚本很土,但它能在升级前一小时之内把 90% 的参数级坑捞出来。
二、主线一:Vulkan 正在吃掉 FFmpeg 的硬件加速层
这是 9.0 最重要、也最被低估的一条线。
2.1 历史包袱:六套硬件后端的碎片化地狱
先看清楚 FFmpeg 现在背着多少套硬件加速方案:
| 后端 | 厂商/平台 | 覆盖范围 | 典型滤镜 |
|---|---|---|---|
| NVENC / NVDEC / CUDA | NVIDIA | 编码、解码、部分滤镜 | scale_cuda、bwdif_cuda、pad_cuda、transpose_cuda(9.0) |
| VAAPI | Linux + Intel/AMD | 编解码 + 滤镜 | scale_vaapi、pad_vaapi、drawbox_vaapi |
| QSV | Intel | 编解码 | vpp_qsv |
| VideoToolbox | Apple | 编解码 | ProRes RAW hwaccel(9.0) |
| AMF | AMD | 编码 + 滤镜 | vpp_amf、frc_amf(9.0)、vqe_amf(9.0) |
| D3D12VA | Windows | 编解码 + 滤镜 | scale_d3d12、mestimate_d3d12、deinterlace_d3d12(8.1) |
| Vulkan | 跨厂商 | 解码、编码、计算滤镜 | *_vulkan 系列 |
七套。每套有自己的 AVHWDeviceContext、自己的 pixel format、自己的一套滤镜命名、自己的帧池管理。
这个碎片化的代价是什么?举个最具体的例子:你想写一条「解码 → 缩放 → 去隔行 → 旋转 → 编码」的全硬件流水线,在 NVIDIA 上你需要 scale_cuda + bwdif_cuda + transpose_cuda,在 AMD 上你需要 vpp_amf 的对应能力,在 Intel Linux 上你需要 scale_vaapi + deinterlace_vaapi,在 Windows 上还有一套 *_d3d12。四套代码路径,四套测试矩阵,四套踩坑记录。
而且只要其中任何一环在某个平台上缺失,整条链就断了。
2.2 为什么 transpose_cuda 这种「平平无奇」的滤镜值得单独讲
9.0 加了一个 transpose_cuda。旋转和转置,听起来是 2005 年就该有的功能。
但它解决的是一个非常昂贵的问题:滤镜链上任何一个 CPU-only 的节点,都会把整条 GPU 流水线劈成两半。
FFmpeg 的硬件帧(AV_PIX_FMT_CUDA、AV_PIX_FMT_VULKAN 等)本质上是一个「显存句柄」,AVFrame->data[0] 不是像素指针而是设备内存引用。CPU 滤镜没法直接吃它。所以当你写:
# 反面教材:链条中间混了一个 CPU-only 滤镜
ffmpeg -hwaccel cuda -hwaccel_output_format cuda -i in.mp4 \
-vf "scale_cuda=1280:720,transpose=1" \
-c:v h264_nvenc out.mp4
FFmpeg 要么直接报错「Impossible to convert between the formats」,要么(在能自动插入转换的场景下)悄悄给你插入 hwdownload → format → hwupload。后者更可怕:它能跑通,所以你不会发现,但每一帧都要走两次 PCIe 往返。
720p NV12 一帧约 1.3 MB。30fps 下这是每秒 40 MB 的下行 + 40 MB 的上行,还要加上 CPU 侧的 memcpy 和同步等待。真实吞吐可能从「一张卡跑 20 路」掉到「一张卡跑 4 路」,而 nvidia-smi 里 GPU 利用率看起来还挺低——因为瓶颈在总线和同步,不在计算单元。
所以 transpose_cuda 补的不是「一个滤镜」,是一条流水线的完整性。
2.3 实战:如何证明你的滤镜链没有隐藏的 PCIe 往返
这是我认为本文最有实用价值的一段。三层验证:
第一层:让 FFmpeg 把它构建的滤镜图打印出来。
ffmpeg -v verbose -hwaccel cuda -hwaccel_output_format cuda -i in.mp4 \
-vf "scale_cuda=1280:720,transpose_cuda=dir=clock" \
-c:v h264_nvenc -f null - 2>&1 | grep -iE 'auto_scale|hwdownload|hwupload|Impossible|format converter'
只要输出里出现 auto_scale、hwdownload、hwupload,你的链就断了。 干净的全 GPU 链,这个 grep 应该是空的。
第二层:看 filtergraph 的实际拓扑。
# -filter_complex 配合 verbose 会打印完整的 graph 结构
ffmpeg -v debug -hwaccel cuda -hwaccel_output_format cuda -i in.mp4 \
-filter_complex "[0:v]scale_cuda=1280:720[a];[a]transpose_cuda=dir=clock[out]" \
-map "[out]" -c:v h264_nvenc -f null - 2>&1 | grep -A30 'Filtergraph'
关注每个 link 的 pixel format。全程应该是 cuda,一旦中间出现 nv12/yuv420p,说明落回主存了。
第三层:直接测总线流量。
# 跑转码的同时,另开一个终端采样 PCIe 吞吐
nvidia-smi dmon -s t -d 1
rxpci / txpci 两列(单位 MB/s)。纯解码+编码的链路,PCIe 流量应该只有码流本身的量级(几 MB/s)。如果你看到几十上百 MB/s,那就是原始帧在来回搬。
这三层验证,我建议直接写进你的 CI。 硬件流水线的性能退化是静默的,靠人工 review 命令行迟早会漏。
2.4 Vulkan 路线的技术本质:不依赖固定功能单元
现在说 Vulkan 这条线为什么不一样。
前面六套后端,本质上都是厂商固定功能单元(fixed-function)的封装。NVENC 是一块专用 ASIC,VAAPI 是 Intel 媒体引擎的抽象层,AMF 是 AMD 的 SDK。它们的共同特点是:能力边界由硬件决定,厂商不实现就没有。
Vulkan 有两条完全不同的路径:
- Vulkan Video(
VK_KHR_video_decode_*/encode_*):仍然是对固定功能单元的抽象,但是跨厂商的标准接口。FFmpeg 6.1 加了 Vulkan decode hwaccel(H.264/HEVC/AV1),7.1 加了 Vulkan H.264/H.265 编码器,8.0 加了 AV1 Vulkan 编码器和 VP9 Vulkan hwaccel。 - Vulkan Compute:用通用计算着色器(compute shader)实现编解码和滤镜。这条路完全不依赖厂商的媒体引擎——只要 GPU 支持 Vulkan 计算,就能跑。
第二条才是真正的破局点。看 8.1 的两行:
- Vulkan compute codec optimizations
- ProRes Vulkan encoder
- ProRes Vulkan hwaccel
- DPX Vulkan hwaccel
- swscale Vulkan support
swscale Vulkan support 这一条尤其关键。swscale 是 FFmpeg 的色彩空间转换和缩放库,是几乎每条转码链路都会经过的地方。把 swscale 搬到 Vulkan 上,意味着跨厂商 GPU 上的通用缩放/色转换成为可能,不再需要 scale_cuda / scale_vaapi / scale_d3d12 三份实现。
而 9.0 在这条线上加了:
- APV Vulkan hwaccel
- Add v360_vulkan filter
再看已经进入下个版本开发分支的:
- APV Vulkan encoder
编解码器正在被 compute shader 重新实现。 ProRes 编码器、DPX 解码、APV 编解码——这些都是没有任何 GPU 有专用 ASIC 的格式,但用 Vulkan compute 全都能跑在 GPU 上。
这是一个方向性判断:未来五年,FFmpeg 的硬件加速会分成两层——热门标准编解码(H.264/HEVC/AV1/VVC)继续走各家 ASIC,长尾格式和所有滤镜逐渐统一到 Vulkan compute。
2.5 v360_vulkan:为什么全景投影是 GPU 的天然工作
v360 是 FFmpeg 里做 360 度全景视频投影转换的滤镜——等距柱状投影(equirectangular)转立方体贴图(cubemap)、转鱼眼、转平面视角,诸如此类。
它的计算模式是逐像素独立的反向映射:对输出图上每个像素 (x, y),算出它在输入球面上对应的方向向量,再投影回输入图的坐标 (u, v),做一次插值采样。
输出像素 (x,y)
→ 归一化到输出投影的参数空间
→ 转成三维方向向量 (X,Y,Z)
→ 按输入投影模型反算 (u,v)
→ 双线性/双三次插值采样
→ 写回输出
每个输出像素完全独立,没有任何跨像素依赖。这是教科书级别的 embarrassingly parallel。 CPU 上一个 8K 等距柱状帧(7680×3840,约 2950 万像素)做一次三次插值重投影,单核要几百毫秒;GPU 上几千个线程齐上,可以压到毫秒级。
而且这个场景没有专用 ASIC——没有哪个厂商会为 360 视频投影做硬件单元。所以它必须走 compute shader。v360_vulkan 是 Vulkan compute 路线价值的最好例证。
用法(注意 hwupload / hwdownload 的位置):
ffmpeg -init_hw_device vulkan=vk:0 -filter_hw_device vk \
-i panorama_8k.mp4 \
-vf "format=yuv420p,hwupload,\
v360_vulkan=input=e:output=flat:h_fov=100:v_fov=60:yaw=30:pitch=-10:w=1920:h=1080,\
hwdownload,format=yuv420p" \
-c:v libx264 -preset medium -crf 20 out.mp4
这里必须 hwdownload 是因为 libx264 是 CPU 编码器。如果你的编码器也在 GPU 上(比如 h264_vulkan),就可以省掉这次回传,那才是真正的全链路 GPU:
# 全 GPU 链路(编码器也在 Vulkan 上)
ffmpeg -init_hw_device vulkan=vk:0 -filter_hw_device vk \
-hwaccel vulkan -hwaccel_output_format vulkan \
-i panorama_8k.mp4 \
-vf "v360_vulkan=input=e:output=flat:h_fov=100:v_fov=60:w=1920:h=1080" \
-c:v h264_vulkan out.mp4
先用这条命令确认你的环境到底有哪些 Vulkan 能力:
# 有哪些 vulkan 滤镜
ffmpeg -hide_banner -filters 2>/dev/null | grep -i vulkan
# 有哪些 vulkan 编解码器
ffmpeg -hide_banner -encoders 2>/dev/null | grep -i vulkan
ffmpeg -hide_banner -decoders 2>/dev/null | grep -i vulkan
# 具体滤镜的参数(不要凭记忆写参数,以这个输出为准)
ffmpeg -hide_banner -h filter=v360_vulkan
# Vulkan 设备能不能初始化
ffmpeg -hide_banner -init_hw_device vulkan=vk:0 -f lavfi -i nullsrc -frames:v 1 -f null -
最后一条特别重要。Vulkan 后端对驱动版本、VK_ICD_FILENAMES、以及是否有可用的 compute queue 都有要求,容器环境里很容易初始化失败。 先用这条空跑命令验通,再去调滤镜。
2.6 APV 与 ProRes RAW:专业制作链路的补完
顺带说清楚 9.0 里两个陌生的名字。
APV(Advanced Professional Video)是一个面向专业制作的编解码格式,特点是帧内编码(intra-only)、高比特深度、低复杂度、高码率。它的定位不是分发,是中间片(mezzanine)——拍摄、剪辑、调色环节里那些需要逐帧随机访问、不能有帧间依赖的场景。
FFmpeg 对 APV 的支持是分批进来的:
- 8.0:APV 解码器、parser、raw bitstream 封装/解封装、通过
libopenapv的编码支持、APV in MP4/ISOBMFF - 9.0:APV Vulkan hwaccel
- 开发分支:APV Vulkan encoder
ProRes RAW 是苹果的 RAW 格式,同样是制作链路的东西。
- 8.0:ProRes RAW 解码器 + ProRes RAW Vulkan hwaccel
- 9.0:ProRes RAW VideoToolbox hwaccel
看出规律了吗?同一个格式,FFmpeg 会同时提供 Vulkan(跨平台通用计算)和厂商原生(VideoToolbox)两条加速路径。 这不是重复造轮子,这是有意为之的双轨:厂商路径在自家硬件上最优,Vulkan 路径保证在所有地方都能用。
这对做跨平台剪辑工具/媒体资产管理系统的人是实打实的好消息——你终于可以在 Linux 服务器上用 GPU 解 ProRes RAW 了,不用非得买 Mac。
三、主线二:元数据从「附属品」升级为「一等公民」
第二条主线更隐蔽,但对做分发的人影响更大。
3.1 传统转码流水线的元数据黑洞
传统的转码思维是:解码成像素 → 处理像素 → 编码像素。 元数据(HDR 参数、色彩体积信息、增强层数据)在这个模型里是「附加信息」,能带就带,带不了就算了。
结果就是我们都很熟悉的那种事故:
- 源片是 HDR10+,转完变成普通 HDR10,动态元数据没了,暗部细节全糊
- 源片是 Dolby Vision Profile 7(双层),转完只剩基础层,播放器不认 DV 了
- 源片带 LCEVC 增强层,转封装之后增强层丢了,画质回落到基础层
这些问题的共性是:像素被正确处理了,但描述「这些像素应该怎么被显示」的信息丢了。
在 SDR 时代这不是大问题,因为 Rec.709 是事实上的唯一解释。但在 HDR 时代,同一组像素值配不同的元数据,出来的画面可以差出一个数量级的观感。
3.2 ST 2094 家族与 9.0 的 SMPTE 2094-50 metadata support and passthrough
SMPTE ST 2094 是动态色彩体积变换元数据(Dynamic Metadata for Color Volume Transform,DMCVT)的标准族。它按「应用」分册,每一册对应一套厂商方案:
- ST 2094-10:对应 Dolby Vision 路线
- ST 2094-40:对应 HDR10+ 路线
- 其余分册对应其他厂商提交的方案
它们要解决的是同一个问题:HDR10 的静态元数据(MaxCLL / MaxFALL / Mastering Display)是整片一套,但一部电影里白天沙漠和夜晚室内的动态范围需求完全不同。 动态元数据允许逐场景、甚至逐帧地告诉显示设备「这一段该怎么做色调映射」。
**ST 2094-50 是这个家族里较新的一册。**关于它的具体归属和技术细节,我建议直接查 SMPTE 官方文档,我不在这里做二手转述——这类标准细节转述错了会误导人。
但架构层面的判断很清楚:Changelog 里写的是 metadata support and passthrough,passthrough(透传)这个词比 support 重要得多。
support 意味着「能解析」。passthrough 意味着「解析之后能原样带到输出」。对转码流水线来说,后者才是有价值的那个——你要的不是 ffprobe 能打印出来,而是转完之后播放器还能读到。
3.3 实战:元数据链路完整性校验
这是我强烈建议加进 CI 的检查。核心思路:转码前后各 probe 一次 side data,逐项对比。
#!/usr/bin/env bash
# meta_check.sh — 校验转码前后 HDR/动态元数据是否完整透传
set -euo pipefail
SRC="$1"
DST="$2"
probe_sidedata() {
ffprobe -v error -select_streams v:0 \
-show_frames -read_intervals "%+#3" \
-show_entries frame=side_data_list \
-of json "$1" 2>/dev/null
}
probe_streamside() {
# 流级别的 side data(Mastering Display / Content Light Level 等)
ffprobe -v error -select_streams v:0 \
-show_entries stream_side_data_list \
-of json "$1" 2>/dev/null
}
probe_color() {
ffprobe -v error -select_streams v:0 \
-show_entries stream=color_range,color_space,color_transfer,color_primaries,pix_fmt \
-of json "$1"
}
echo "=== 色彩描述 ==="
diff <(probe_color "$SRC" | python3 -m json.tool) \
<(probe_color "$DST" | python3 -m json.tool) \
&& echo "OK: 色彩描述一致" || echo "WARN: 色彩描述发生变化(见上方 diff)"
echo "=== 流级 side data ==="
diff <(probe_streamside "$SRC" | python3 -m json.tool) \
<(probe_streamside "$DST" | python3 -m json.tool) \
&& echo "OK: 流级元数据一致" || echo "WARN: 流级元数据发生变化"
echo "=== 帧级 side data(前 3 帧)==="
diff <(probe_sidedata "$SRC" | python3 -m json.tool) \
<(probe_sidedata "$DST" | python3 -m json.tool) \
&& echo "OK: 帧级动态元数据一致" || echo "WARN: 帧级动态元数据发生变化"
用法:
chmod +x meta_check.sh
./meta_check.sh source_hdr10plus.mp4 output.mp4
几个使用要点:
-read_intervals "%+#3"只读前 3 帧,避免对长片全量 probe(那会跑很久)。做严格校验时可以改成采样多个时间点:-read_intervals "10%+#2,50%+#2,90%+#2"。- 色彩描述(
color_transfer是smpte2084还是bt709)是最容易静默丢失的一项,也是最容易发现的一项。如果源片是 PQ(smpte2084)而输出变成bt709,说明你的链路里发生了未预期的色调映射或者干脆只是标记丢了。 - 帧级 side data 里会出现
HDR Dynamic Metadata SMPTE2094-40(HDR10+)、Dolby Vision RPU Data之类的条目。diff 出现差异不一定是错——比如你有意做了 HDR→SDR 转换。关键是「差异是不是你预期的」。
3.4 Dolby Vision 多层 HEVC 拆分 bsf
9.0 加了一条:Bitstream filter to split Dolby Vision multi-layer HEVC。
背景:Dolby Vision Profile 7 是双层结构——基础层(BL,通常是 HDR10 兼容的 HEVC)+ 增强层(EL)+ RPU(Reference Processing Unit,携带动态元数据的 NAL 单元)。UHD 蓝光用的就是这个 profile。
问题在于:大多数流媒体分发场景只接受单层的 Profile 8(BL + RPU,无 EL)。 从 Profile 7 转 Profile 8,第一步就是把多层码流拆开,丢掉 EL,保留 BL 和 RPU。
以前这件事要靠外部工具(dovi_tool 之类)。现在 FFmpeg 内部有 bsf 了。
具体 bsf 的名字我不确定,不编。查询命令:
# 列出所有 bitstream filter,找 dolby vision 相关的
ffmpeg -hide_banner -bsfs | grep -iE 'dov|dolby|dv'
# 找到名字之后看它的参数
ffmpeg -hide_banner -h bsf=<你查到的名字>
这个「先查再用」的习惯,在用刚发布的大版本时是刚需。Changelog 只承诺功能存在,不承诺参数名和你想的一样。
3.5 LCEVC 入 MP4:分层编码的最后一公里
LCEVC track muxing support in MP4 muxer 这条,是一条走了三个版本的长跑。
LCEVC(MPEG-5 Part 2,Low Complexity Enhancement Video Coding)的思路很聪明:用任意现有编解码器(H.264/HEVC/AV1 都行)编一个低分辨率的基础层,再叠一个轻量的增强层来重建全分辨率。
好处是:
- 基础层用老编码器,全世界的硬件解码器都认
- 增强层计算量很低,纯软解也扛得住
- 总码率比直接编全分辨率低
FFmpeg 对 LCEVC 的支持路径:
| 版本 | 能力 |
|---|---|
| 7.1 | LCEVC 滤镜;在 H.26x 和 MP4/ISOBMFF 中导出 enhancement data |
| 8.0 | —— |
| 8.1 | LCEVC parser;LCEVC metadata bitstream filter;MPEG-TS 中导出增强层 |
| 9.0 | LCEVC track 在 MP4 muxer 中的封装支持 |
| 开发分支 | LCEVC payload merging bitstream filter |
从「能解析」到「能作为独立 track 封装进 MP4」,这是从「实验性支持」到「可用于生产分发」的分界线。 之前你只能把增强数据塞在 SEI 里带走,现在它可以是一条正经的 track,有自己的 codec tag,播放器可以按标准流程发现和处理。
再看开发分支那条 LCEVC payload merging bitstream filter——这是在补反向操作:把分开的载荷合并回去。一个格式的支持是否成熟,看的就是「拆」和「合」是不是都齐了。
四、主线三:AI 正式进入滤镜图
这条线在中文报道里我一条都没看到,但它可能是三条线里长期影响最大的。
4.1 ONNX Runtime DNN backend with GPU execution provider support
这一行藏在 9.0 Changelog 的后半段。
先说 FFmpeg 的 DNN 子系统历史。libavfilter/dnn 是一个抽象层,让滤镜可以调用神经网络模型。基于它的滤镜有:
dnn_processing:通用的「一帧进、一帧出」模型推理(超分、去噪、风格化)dnn_classify:分类,结果写进帧的 side datadnn_detect:目标检测,输出检测框derain:去雨sr:超分(已被dnn_processing取代)
后端的演进:
| 后端 | 引入 | 问题 |
|---|---|---|
| native | 早期 | 只支持极少数算子,模型要手工转换,基本没人用 |
| TensorFlow | 早期 | 需要链接完整 libtensorflow,体积巨大,C API 极不友好 |
| OpenVINO | 中期 | 好用,但强绑 Intel 生态 |
| libtorch | 7.0 | 灵活,但依赖沉重 |
| ONNX Runtime + GPU EP | 9.0 | —— |
ONNX Runtime 是这几个里唯一一个「模型格式中立 + 执行后端中立 + 部署轻量」三者兼备的选项。
- 模型中立:PyTorch / TensorFlow / JAX 都能导出 ONNX
- 执行后端中立:ONNX Runtime 的 Execution Provider 机制支持 CUDA、TensorRT、DirectML、CoreML、ROCm、OpenVINO……
- 部署轻量:一个
libonnxruntime.so,几十 MB 量级
而 Changelog 特意点出 with GPU execution provider support——这意味着模型推理可以直接跑在 GPU 上,而不是把帧下载到 CPU 推理再上传。
把这一点和主线一连起来看,你会发现一个很有意思的图景:
GPU 解码 (NVDEC/Vulkan Video)
↓ 帧一直在显存
GPU 滤镜 (scale_cuda / *_vulkan)
↓ 帧一直在显存
GPU 神经网络推理 (ONNX Runtime CUDA EP) ← 9.0 补上的这一环
↓ 帧一直在显存
GPU 编码 (NVENC/Vulkan Video)
这是一条从解码到编码、中间夹着 AI 推理、全程不碰主存的流水线。 在 9.0 之前,中间那一环是断的。
4.2 实战:用 dnn_processing 跑超分
再次强调:具体的 backend 名称和参数以查询为准。
# 1. 确认 dnn_processing 在不在
ffmpeg -hide_banner -filters | grep dnn
# 2. 关键:看 dnn_backend 支持哪些值(不要凭记忆写)
ffmpeg -hide_banner -h filter=dnn_processing
-h filter=dnn_processing 的输出里会列出 dnn_backend 这个 AVOption 的所有可选值,以及 model、input、output、options 等参数。照着这个输出写,不要照着任何博客(包括这篇)写。
典型形态大致是这样(backend 名以上面查到的为准):
ffmpeg -i input_540p.mp4 \
-vf "format=rgb24,dnn_processing=dnn_backend=<查到的名字>:model=./espcn_x2.onnx:input=x:output=y,format=yuv420p" \
-c:v libx264 -crf 18 output_1080p.mp4
几个必须知道的坑:
input和output是模型里张量的名字,不是随便填的。 用 Python 查:
import onnx
m = onnx.load("espcn_x2.onnx")
print("inputs :", [i.name for i in m.graph.input])
print("outputs:", [o.name for o in m.graph.output])
# 顺便看看输入形状,NCHW 还是 NHWC,动态维度有没有
for i in m.graph.input:
dims = [d.dim_value or d.dim_param for d in i.type.tensor_type.shape.dim]
print(f" {i.name}: {dims}")
- 像素格式必须和模型输入匹配。 绝大多数视觉模型吃 RGB float32 归一化到 [0,1] 或 [-1,1],而视频帧是 YUV uint8。
format=rgb24是必须的,但归一化通常要模型自己在图里做(导出 ONNX 时把预处理算子一起导出去),否则你得靠options传参或者干脆改模型。 - 动态分辨率是个大坑。 很多超分模型导出时把输入形状固定成了
[1,3,540,960]。喂一个不同分辨率的视频进去会直接报错。导出 ONNX 时把 H/W 设成动态维度:
torch.onnx.export(
model, dummy_input, "espcn_x2.onnx",
input_names=["x"], output_names=["y"],
dynamic_axes={"x": {0: "N", 2: "H", 3: "W"},
"y": {0: "N", 2: "H2", 3: "W2"}},
opset_version=17,
)
- 性能上,DNN 滤镜几乎必然是整条链的瓶颈。 一个 540p→1080p 的轻量超分模型在消费级 GPU 上大概能到几十 fps;重一点的模型个位数 fps 很正常。不要指望它能实时。 这类链路的正确定位是离线批处理,不是直播。
4.3 别忘了 8.0 的 Whisper 滤镜
FFmpeg 8.0 加了一条 Whisper filter。
这意味着语音转写变成了滤镜图里的一个节点。你不需要「先用 ffmpeg 抽音频 → 存 wav → 调 whisper.cpp → 解析输出 → 生成字幕文件 → 再用 ffmpeg 烧进去」这一串胶水,理论上可以在一条命令里完成。
# 先确认它在不在,以及参数长什么样
ffmpeg -hide_banner -filters | grep -i whisper
ffmpeg -hide_banner -h filter=whisper
把 Whisper 滤镜和 ONNX DNN 后端放在一起看,FFmpeg 的定位变化就非常清楚了:
传统 FFmpeg 处理的是信号——它知道这是一帧 1920×1080 的 YUV420P,但不知道画面里有什么。
带 AI 滤镜的 FFmpeg 处理的是内容——它可以知道这一帧里有几个人、说了什么话、是什么场景。
一旦滤镜图里能跑推理,很多以前需要「拆成三个服务 + 一个消息队列」的流水线,可以塌缩成一条 -filter_complex:
- 检测到人脸 → 自动打码(
dnn_detect+delogo/boxblur+sendcmd) - 检测到静音段 → 自动裁掉(
silencedetect+select) - 转写语音 → 自动生成硬字幕(
whisper+subtitles) - 分类场景 → 按内容动态调整码率(
dnn_classify+ 编码器参数)
这是 FFmpeg 从「转码器」变成「内容处理管线」的关键一步。
我的判断:这条线目前还很粗糙(模型管理、批处理、显存占用、错误处理都不成熟),但方向是对的,而且没有竞品——没有第二个工具能同时把「工业级编解码」和「模型推理」放进同一个进程的同一张显存里。
五、被忽略的两条:HE-AAC 960 与动画 WebP
5.1 HE-AAC 960 decoding (DAB+):一个数字背后的时序约束
这条看起来最不起眼,但它的技术根因很有意思。
标准 AAC-LC 用 1024 个采样作为一个帧(长块),对应 2048 点的 MDCT 窗口。这个数字是 AAC 标准定下来的,绝大多数场景都用它。
但 DAB+(欧洲数字广播)用的是 960 个采样的帧。
为什么?因为 DAB+ 的音频超帧(audio super frame)固定是 120 毫秒。
算一下:
| 采样率 | 960 样本时长 | 120ms 能放几帧 | 1024 样本时长 | 能整除吗 |
|---|---|---|---|---|
| 48 kHz | 20.0 ms | 6 | 21.33 ms | ❌ |
| 32 kHz | 30.0 ms | 4 | 32.0 ms | ❌(3.75 帧) |
| 24 kHz | 40.0 ms | 3 | 42.67 ms | ❌ |
| 16 kHz | 60.0 ms | 2 | 64.0 ms | ❌ |
960 在所有 DAB+ 采样率下都能整除 120ms,1024 一个都不行。
广播系统必须有严格对齐的帧结构——因为要做时间交织(time interleaving)、纠错编码、以及和数据服务复用。帧长和超帧长度不成整数倍,整个复用层的设计就没法做。
所以 DAB+ 选了 960。代价是:这些码流用标准 AAC 解码器解不了。 你需要一个知道帧长是 960 的解码器(MDCT 窗口变成 1920 点,窗函数系数表全部要换一套)。
9.0 之前,FFmpeg 处理 DAB+ 音频要靠外部库。现在原生支持了。
受众很窄,但对做广播监测、数字广播归档、SDR(软件定义无线电)接收的人来说,这是从「不可能」到「一条命令」的变化。
5.2 动画 WebP:为什么等了这么久
Animated WebP decoder + Animated WebP demuxer 是 9.0 被报道最多的一条,但没人讲清楚它为什么难。
WebP 不是一个编解码格式,它是一个 RIFF 容器 + 两种编码格式的组合。
RIFF 容器
└─ 'WEBP'
├─ 'VP8 ' ← 有损:VP8 关键帧的一帧
├─ 'VP8L' ← 无损:一套完全独立的无损编码
├─ 'VP8X' ← 扩展头(声明有没有动画/alpha/ICC/EXIF)
├─ 'ALPH' ← 独立的 alpha 通道数据
├─ 'ANIM' ← 动画全局参数(背景色、循环次数)
└─ 'ANMF' ← 动画帧(每帧一个,含偏移、尺寸、时长、混合/处置模式)
└─ 内嵌 VP8 / VP8L (+ ALPH)
难点在于这是一个「容器套编码」的双层结构,而 FFmpeg 的架构里容器(libavformat)和编码(libavcodec)是严格分离的两层。
静态 WebP 好办:一个 RIFF,里面一帧 VP8,libavcodec 里加个 webp decoder 就完事。
动画 WebP 麻烦在:
- 需要一个真正的 demuxer(解复用器)来遍历
ANMF链,把每帧拆出来,还要正确解析每帧的时长(构造pts) - 每帧有独立的偏移量和尺寸——一帧可能只覆盖画布的一小块矩形区域
- 每帧有混合模式(blend:和上一帧混合还是覆盖)和处置模式(dispose:这一帧显示完之后画布怎么处理)
- Alpha 可能在
ALPHchunk 里(有损路径),也可能内嵌在VP8L里(无损路径) - 有损帧和无损帧可以在同一个动画里混用
第 2、3 条是关键:这和 GIF 的帧处置逻辑几乎是同构的,需要在 demuxer/decoder 里维护一个画布状态机。 FFmpeg 里已经有 GIF 的这套逻辑,但 WebP 的语义细节不一样,得重写一套。
所以 9.0 的 Changelog 分了两行写(decoder 和 demuxer 各一行)——这是两个模块的工作。
实用价值:
# 以前需要 libwebp/libwebpdemux 或者外部工具,现在原生
ffmpeg -i animated.webp -c:v libx264 -pix_fmt yuv420p out.mp4
# 抽帧
ffmpeg -i animated.webp frames_%04d.png
# 看看里面到底有几帧、帧率多少
ffprobe -v error -select_streams v:0 \
-show_entries stream=nb_frames,avg_frame_rate,width,height,pix_fmt \
-of default=noprint_wrappers=1 animated.webp
顺带一个实用的横向对比(具体数字取决于内容,请自测,我给的是量级和权衡,不是 benchmark):
| 格式 | 体积 | 解码成本 | Alpha | 浏览器支持 | 适用场景 |
|---|---|---|---|---|---|
| GIF | 最大(256 色调色板) | 极低 | 1-bit | 全部 | 兼容性兜底 |
| APNG | 大 | 低 | 8-bit | 广泛 | 需要真 alpha 的简单动画 |
| 动画 WebP | 中(通常显著小于 GIF) | 中 | 8-bit | 广泛 | 通用替代 GIF |
| 动画 AVIF | 小 | 高 | 8-bit+ | 较新浏览器 | 追求极致体积 |
| H.264/HEVC MP4 | 最小 | 低(有硬解) | 无 | 全部 | 不需要 alpha 的长动画 |
决策建议: 短循环 + 需要 alpha + 要在 <img> 标签里直接用 → 动画 WebP。长动画(>5 秒)+ 不需要 alpha → 老老实实用 MP4,用 <video autoplay muted loop playsinline>,体积和解码功耗都是碾压级的优势。
六、升级实战与性能验证
6.1 编译配置建议
如果你自己编 FFmpeg 9.0,几个和上面三条主线相关的开关:
./configure \
--enable-gpl --enable-version3 --enable-nonfree \
--enable-vulkan \ # 主线一:Vulkan compute + Vulkan Video
--enable-libshaderc \ # Vulkan 滤镜的着色器编译(或 --enable-libglslang)
--enable-libplacebo \ # 高质量色彩管理 / 色调映射(配合 HDR 链路)
--enable-cuda-nvcc --enable-libnpp \ # CUDA 滤镜
--enable-ffnvcodec \ # NVENC/NVDEC header
--enable-vaapi \
--enable-libx264 --enable-libx265 --enable-libsvtav1 \
--enable-libopus --enable-libfdk-aac \
--enable-libwebp \
--enable-lto \
--enable-optimizations
几个要点:
--enable-vulkan需要--enable-libshaderc或--enable-libglslang,否则 Vulkan 滤镜编不出来(着色器需要在运行时或编译期从 GLSL 编成 SPIR-V)。这是 Vulkan 滤镜「configure 通过了但-filters里没有」的最常见原因。--enable-libplacebo强烈建议开。libplacebo提供了工业级的色调映射和色彩管理,是做 HDR→SDR 转换时唯一靠谱的选项。手写zscale+tonemap参数调出来的效果通常不如它。- ONNX Runtime 后端的 configure 开关名我不确定,请以
./configure --help | grep -i onnx的输出为准。 --enable-nonfree会让产物不可分发(libfdk-aac等)。公司内部用没问题,要对外发布就得去掉。
编完先自检:
# 一次性确认三条主线的能力都在
echo "=== Vulkan 滤镜 ==="; ffmpeg -hide_banner -filters 2>/dev/null | grep -c vulkan
echo "=== CUDA 滤镜 ==="; ffmpeg -hide_banner -filters 2>/dev/null | grep -c cuda
echo "=== DNN 滤镜 ==="; ffmpeg -hide_banner -filters 2>/dev/null | grep dnn
echo "=== 硬件加速 ==="; ffmpeg -hide_banner -hwaccels
echo "=== 编译配置 ==="; ffmpeg -hide_banner -buildconf | grep -E 'vulkan|onnx|placebo|shaderc|cuda'
-buildconf 这条特别有用——它打印的是这个二进制实际编译时的 configure 参数,比你回忆自己敲了什么可信得多。线上排障时第一条就该跑它。
6.2 不要用 -benchmark 骗自己
FFmpeg 有个 -benchmark 选项,会打印 utime/stime/rtime 和内存峰值。很多人拿它做性能对比,然后得出错误结论。
问题在于:-benchmark 测的是整个进程,包含了输入解析、输出写盘、以及最重要的——磁盘 I/O。 你测出来的差异可能只是页缓存冷热不同。
正确的做法:
# 1. 输出丢弃,排除写盘影响
ffmpeg -benchmark -i in.mp4 -vf "..." -c:v h264_nvenc -f null -
# 2. 输入预热到页缓存,排除读盘影响(跑两遍取第二遍)
cat in.mp4 > /dev/null # 预热
ffmpeg -benchmark ... -f null -
# 3. 只测滤镜本身:用 lavfi 生成源,彻底排除解码
ffmpeg -benchmark -f lavfi -i "testsrc2=size=3840x2160:rate=60:duration=10" \
-vf "你要测的滤镜" -f null -
# 4. 多跑几次看方差,单次数据没有意义
for i in 1 2 3 4 5; do
/usr/bin/time -f "%e %U %S" ffmpeg -v quiet -f lavfi \
-i "testsrc2=size=3840x2160:rate=60:duration=10" \
-vf "你要测的滤镜" -f null - 2>&1
done
第 3 条是关键。 用 testsrc2 做源可以把解码开销完全排除,测出来的才是滤镜本身的成本。做 CPU 滤镜 vs GPU 滤镜对比时,这是唯一公平的方法。
另外,GPU 场景下 -benchmark 的 CPU 时间几乎没有参考价值——GPU 在算的时候 CPU 在等,utime 很低不代表快。GPU 场景要看的是:
# 端到端墙钟时间 + GPU 占用率 + PCIe 流量,三个一起看
ffmpeg -v quiet -stats -hwaccel cuda ... -f null - &
FFPID=$!
nvidia-smi dmon -s um -d 1 -c 30 # u=利用率, m=显存
wait $FFPID
6.3 七条调优规律
跑了几年硬件转码流水线,这几条是反复被验证的:
1. 滤镜链的完整性 > 单个滤镜的速度。
一个「慢但在 GPU 上」的滤镜,通常好过「快但要下载到 CPU」的滤镜。先保证不断链,再优化单点。
2. hwupload / hwdownload 的位置决定一切。
能推多晚就推多晚(hwupload 尽早,hwdownload 尽晚)。整条链只应该有一次上传和一次下载。
3. 像素格式转换是隐形杀手。format= 滤镜看起来无害,但 yuv420p → rgb24 → yuv420p 的一来一回既耗 CPU 又损精度。用 -v verbose 看 FFmpeg 自动插了几个 format。
4. 硬件解码的输出格式必须显式指定。
只写 -hwaccel cuda 而不写 -hwaccel_output_format cuda,FFmpeg 会在解码后立刻把帧下载回主存,你的「硬件加速」只加速了解码那一步。这是最常见的性能错觉。
5. 单进程多路 > 多进程单路(在 GPU 上)。
每个 FFmpeg 进程都要初始化自己的 CUDA/Vulkan context,显存和初始化开销都是重复的。多路转码优先用 -filter_complex 在一个进程里做。
6. 编码器的 preset 影响远大于滤镜优化。
在你花两天优化滤镜链之前,先试试把 -preset p7 改成 -preset p4。通常这一个改动的收益超过所有滤镜优化的总和。
7. HDR 链路上,libplacebo 优于手搓 zscale+tonemap。
色调映射是个有大量工程细节的问题(高光滚降曲线、色域压缩、亮度自适应)。除非你是这个领域的专家,否则用现成的。
七、十条踩坑清单
按「踩到的概率 × 排查难度」排的序:
1. 升级后 NVENC 直接编不出来。
9.0 移除了 pre-11.1 SDK 支持。检查:ffmpeg -encoders | grep nvenc。空的话去更新 nv-codec-headers 和驱动。
2. 老的 NVENC 参数导致命令直接退出。
硬移除不是软降级。升级前必须用第一节的冒烟脚本全量验一遍参数。
3. --enable-vulkan 编过了,但 -filters 里没有 vulkan 滤镜。
缺 libshaderc 或 libglslang。用 ffmpeg -buildconf 确认,不要靠回忆。
4. Vulkan 在容器里初始化失败。
容器里需要挂载 /dev/dri、正确的 VK_ICD_FILENAMES、以及和宿主机匹配的驱动。先用 ffmpeg -init_hw_device vulkan=vk:0 -f lavfi -i nullsrc -frames:v 1 -f null - 单独验证设备初始化。
5. 滤镜链里静默插入了 hwdownload/hwupload,性能腰斩但不报错。
用 2.3 节的三层验证。这个坑最阴险,因为它「能跑通」。
6. HDR 元数据在转码后丢失,但画面「看起来还行」。color_transfer 从 smpte2084 变回 bt709 是最常见的表现。用 3.3 节的脚本做 CI 检查。在标准 SDR 显示器上你看不出来,在 HDR 电视上一眼就废。
7. DNN 滤镜报「input/output tensor not found」。input= / output= 填的是模型里张量的真实名字。用 onnx.load 打出来对照。
8. DNN 模型固定了输入分辨率,换个视频就崩。
导出 ONNX 时设 dynamic_axes。
9. 动画 WebP 转出来第一帧正常、后面全花。
大概率是帧的 dispose/blend 模式处理问题,或者你的源文件混用了有损/无损帧。先 ffprobe 确认帧数和帧率,再单独抽帧检查是哪一帧开始坏。这是新代码,遇到问题值得去 FFmpeg trac 提 issue。
10. 同一台机器上多代 FFmpeg 共存,实际跑的不是你以为的那个。which ffmpeg 只告诉你 PATH 里第一个。真正要看的是 ldd 和 ffmpeg -buildconf。生产环境强烈建议用静态编译或者容器固定版本,不要依赖系统包。
八、下一站:开发分支已经在跑的东西
Changelog 顶部的 version <next> 段落已经有了内容,可以看出下一个版本的方向:
- extensively improved AAC encoder
- APV Vulkan encoder
- iTerm2 inline image protocol muxer
- LCEVC payload merging bitstream filter
- MVR demuxer
- latticepal filter
- DVD-Audio LPCM decoder and demuxing support
- AVFoundation input device selection by unique ID and USB serial number
几条值得注意:
extensively improved AAC encoder —— FFmpeg 原生 AAC 编码器的质量长期落后于 libfdk-aac,而 libfdk-aac 因为许可证问题不能进主流发行版的默认构建。如果原生编码器真的追上来了,这会消除一个存在了十年的「必须自己编 FFmpeg」的理由。 这条的实际影响可能比 9.0 的任何一条都大。
APV Vulkan encoder —— 印证了主线一的判断:编码器正在被 compute shader 重新实现。
LCEVC payload merging bitstream filter —— 印证了主线二:LCEVC 的「拆」和「合」正在补齐,走向生产可用。
iTerm2 inline image protocol muxer —— 这个纯属有趣:FFmpeg 可以直接把视频输出到支持 iTerm2 内联图片协议的终端里。ffplay 的终端版本。
九、总结:一个代号,和它背后的三条线
回到开头。
FFmpeg 9.0 的代号 Lei 会被记住,因为它证明了一件事:开源世界的贡献,不只是提交了多少行代码。 一个人可以完全不在核心贡献者名单上,仅仅因为把一个高门槛的领域用中文讲清楚了,就在十年后被一个全球性项目用版本代号纪念。
这件事本身,比任何技术特性都值得写。
但如果只记住这个代号,就浪费了 5.2 万行代码的信息量。这个版本真正在说的是:
第一,硬件加速的碎片化时代正在结束。 Vulkan compute 提供了一条不依赖厂商固定功能单元的路径,swscale Vulkan support、ProRes Vulkan encoder、APV Vulkan hwaccel、v360_vulkan 是同一个方向上的连续动作。长期看,*_cuda / *_vaapi / *_d3d12 / *_amf 这四套并行的滤镜实现会逐渐收敛到 *_vulkan 一套。
第二,元数据正在从附属品变成一等公民。 SMPTE 2094-50 passthrough、Dolby Vision 多层拆分 bsf、LCEVC 入 MP4——这些都在说同一件事:转码不再只是「像素进、像素出」,而是「像素 + 描述像素该如何被显示的完整信息」一起进出。 HDR 时代,丢元数据等于毁画质,而且是静默的。
第三,AI 推理正在进入滤镜图。 ONNX Runtime + GPU EP 补上了「解码→滤镜→推理→编码」全链路不落主存的最后一环。加上 8.0 的 Whisper 滤镜,FFmpeg 开始具备处理「内容」而不只是「信号」的能力。这条线现在还粗糙,但它没有竞争对手。
三条线指向同一个结论:FFmpeg 不再满足于做一个转码工具。它在把自己变成像素和元数据的操作系统——统一的硬件抽象层(Vulkan)、完整的元数据总线(ST 2094 / DV / LCEVC)、以及可编程的内容理解层(DNN)。
对我们这些天天在用它的人来说,实际的行动清单其实很短:
- 升级前跑参数冒烟测试,尤其是 NVENC 相关的命令模板
- 把「滤镜链完整性检查」写进 CI,别让静默的 PCIe 往返吃掉你的吞吐
- 把「元数据 diff」写进 CI,别在 HDR 链路上静默丢信息
- 如果你在做 AI + 视频,现在可以认真评估把推理搬进
-filter_complex了 - 凡是新版本的参数名,一律
-h filter=xxx现查,别信任何博客的硬编码(包括这一篇)
最后一条不是客套。这个版本发布才几天,我文中所有的 <你查到的名字> 占位符都是真的不确定——在快速演进的项目上,「我不知道,这是查询命令」比「我猜是这个参数」有用一百倍。
十年前那位开发者留下的东西里,最有价值的其实也是这个:不是某段能直接抄的代码,而是「把复杂的东西一层层拆开讲清楚」这件事本身。
代号会随着下一个版本翻篇。这个方法不会。