解封装 → 解码 → 滤镜 → 编码 → 封装,五个阶段两种数据。 Packet 是压缩的包,Frame 是展开的像素,两者体积能差几十倍。
1.4 讲了能干什么,这一节讲它内部怎么走完的。 1.5.2 讲这条流水线上流动的那几个结构体。
一、五个阶段,两种数据
整条流水线只有两种东西在流动,认清这一点,后面所有命令都能对号入座:
阶段 手里拿的 谁的活儿 动像素吗
解封装 Packet libavformat,切出一段一段 不动 解码 Frame libavcodec,压缩包展开成像素 动 滤镜 Frame libavfilter,改画面 动 编码 Packet libavcodec,重新压成包 动 封装 文件 libavformat,按格式排好 不动
最后一列是关键:五步里只有三步动像素。 转封装之所以快,就是因为它把那三步全跳过了—— Packet 直接从一个容器搬到另一个容器,中间一次都没展开。
所以「快不快」这个问题,答案永远在「跳过哪几步」上。
跳过解码后面全跳,指令是 -c copy;只跳滤镜跳不过编码,因为编码要的是 Frame。
二、一帧数据怎么胖了又瘦回去
这是本节最值得记住的一组数字关系:
阶段 数据形态 量级
解封装 压缩的包 几百到几万字节 解码 展开的像素 分辨率决定,几十 KB 到几 MB 滤镜 改过的像素 可能更大也可能更小 编码 新的压缩包 回到几百到几万字节
包和帧之间差几十倍,这不是巧合,是压缩的本质: 压缩就是把「一大片有规律的像素」变成「一小段描述规律的代码」。
所以解码是「还原规律」,编码是「重新找规律」。 两个方向的计算量都不小,而滤镜夹在中间,改完还得重新找一遍规律—— 这就是为什么加一个滤镜会让转码变慢,而不是只慢一点点。
三、帧为什么比它「应该」的大
一个 160×90 的 yuv420p 帧,按 160×90×1.5 算是 21600 字节。 但实际分配往往比这个数大,原因是对齐:
概念 是什么 为什么要有
width 有效宽度 画面真正的宽 linesize 每行实际占多少字节 行尾补空字节,凑整 buffer_size 整个平面分配了多少 linesize 乘行数
为什么补:CPU 读内存喜欢「从整数地址开始、按整块读」。 如果每行 160 字节,下一行就从非整数边界开始,读起来要拆两次。 补到 256 或 128 的整数倍,一行一次读完。
这个差异会带来两个很实际的影响: 一是自己算内存要按 buffer_size 算,不能按 width×height 算; 二是同一个分辨率在不同机器上 linesize 可能不同,别硬编码。
四、什么时候能跳过某些阶段
流水线的每一段都能跳,但跳法不同,代价也不同:
想省什么 怎么跳 代价
省掉解码与编码 -c copy 编码方式改不了 省掉滤镜 不加 -vf/-af 什么都没变 只留视频 -an 音频丢了 只留音频 -vn 视频丢了 只取一段 -ss 与 -t 剪点不在关键帧就要重编
第一行和第三、四行经常一起用:-c copy -an 就是「只搬视频流,不要音频」。
这是提取视频轨道最快的方式,秒级完成。
最后一行要留意顺序:-ss 放在 -i 前面是「先跳再解」,放在后面是「边解边跳」。
前者快得多,但定位可能不够准;后者准,但要把前面的都解一遍。
要精确到帧,只能选后者。
五、动手 Demo:一帧数据怎么胖了又瘦回去
这一段把五个阶段各自的字节数量出来,看放大与缩小的倍数。 顺带量两个「反常」的数字,它们都是延迟造成的。
import av
c = av.open(u"src.mp4")
st = c.streams.video[0]
cc = st.codec_context
pkt = frame = None
for p in c.demux(video=0):
if not p.size:
continue
if pkt is None:
pkt = p
for f in cc.decode(p):
if frame is None:
frame = f
print(u"包", pkt.size)
print(u"帧", sum(pl.buffer_size for pl in frame.planes))
print(u"linesize", frame.planes[0].line_size)
实跑输出:
--------------------------------------------------------------------
处理流程五阶段——一帧数据在中间怎么胖了又瘦回去
--------------------------------------------------------------------
**① 五个阶段,每种阶段手里拿的是完全不同的东西**
阶段 手里拿的 谁的活儿
-------------------------------------------------------------
解封装 Packet libavformat,从文件里切出一段一段
解码 Frame libavcodec,把压缩包展开成像素
滤镜 Frame libavfilter,改像素但仍是 Frame
编码 Packet libavcodec,重新压成包
封装 文件 libavformat,把包按格式排好
**关键点:解封装和编码之后是 Packet,解码和滤镜之间是 Frame。**
**Packet 是压缩的、小的;Frame 是展开的、大的。两者相差几十倍。**
**② 真跑一遍,量每一阶段一帧多少字节**
阶段 一个单位多少字节 相对上一阶段
--------------------------------------------------------
解封装出来的包 731 —(起点)
解码出来的帧 34560 ×47
滤镜缩一半后 7264 ×0.21
重新编码后的包 729 ×0.1004
**看第二列:731 字节的包展开成 34560 字节的帧,一下子放大了 47 倍。**
**再压回去只剩 729 字节,跟原来的包差不多——压缩过的数据本来就该这么小。**
**但这里有两个反常的数字,都是延迟造成的,不是 bug:**
**解码那头:25 个包喂进去只吐出 23 帧,差两帧卡在解码器里;**
**编码这头:一帧喂进去当场吐出 0 个包,补一次空包 flush 才吐出 1 个。**
**两头都有缓冲,因为 B 帧要先攒够后面的帧才好排顺序、做预测。**
**所以「喂一个出一个」在编解码这一层从来不成立,收尾必须记得 flush。**
**③ 为什么帧比它「应该有的大小」还大一点**
算法 字节数 说明
----------------------------------------------------------
按 160×90×1.5 算 21600 理论值,不留余量
实际分配 34560 每行按 256 或 128 对齐
**多出来的不是浪费,是对齐。CPU 读像素时按整块读比按单行读快得多,**
**所以每行后面补几个空字节,让下一行从整数边界开始。**
**这个「每行实际多长」叫 linesize,第三节讲 ffprobe 时会再见到它。**
**④ 五个阶段里,四个都在动同一块内存**
**解封装只切不改,封装只排不改;真正动像素的只有解码、滤镜、编码三步。**
**所以想省时间,能省的地方只有这三步——转封装之所以快,就是因为它一步都不碰。**
第一张表把两种数据的分工说清楚:Packet 是压缩的,Frame 是展开的。
第二张表是实测的放大倍数:731 字节的包展开成 34560 字节的帧,47 倍。 滤镜缩一半后是 7264,重新编码后又回到 729。 一个「胖—瘦」的完整来回,就在这四行里。
第三张表解释为什么 34560 比理论值 21600 大:每行按 256 或 128 对齐。 多出来的不是浪费,是为了让 CPU 一次读完一行。
最后那段两个反常数字最值得看: 25 个包只解出 23 帧,1 帧喂进去编码器当场吐 0 个包。 两头都有缓冲,因为 B 帧要先攒够后面的帧才好排序做预测。 所以「喂一个出一个」在编解码这一层从来不成立,收尾必须 flush。
30 秒实验
- 量倍数:
ffprobe -show_packets 文件 | grep size看包大小,用分辨率算出帧大小,两者一比就是压缩比。 - 验证 copy:
ffmpeg -i 文件 -c copy 输出,然后对比两个文件的画面参数。参数一模一样,一个字节都没重算。 - 试 -ss 位置:同一个文件,
-ss 10 -i 文件和-i 文件 -ss 10各跑一次,比较耗时和输出第一帧的画面。快的那次往往不准。
常见误区
- 以为加滤镜不改编码就不会重算。滤镜改的是 Frame,Frame 要变成新文件就必须重新编码。所以任何
-vf都意味着一次完整的编解码。 - 按 width×height 算内存。要按 linesize 乘行数算,也就是每个平面的 buffer_size。差的那部分是对齐补的,虽然不显示,但确实占内存。
- 以为
-ss放哪儿都一样。放-i前是快速定位到关键帧,放后面是解码到指定位置。前者快、可能不准,后者准、必然慢。
自测三问(点开对答案)
1. `-c copy` 跳过了哪几步? —— 跳过了解码、滤镜、编码三步,只做解封装和封装。所以它最快,也改不了编码方式和分辨率。 2. 为什么 160×90 的 yuv420p 帧实际占 34560 字节,而不是 21600? —— 因为每行按 256(Y 平面)或 128(UV 平面)对齐,行尾补了空字节。90×256 + 45×128×2 = 34560。 3. 为什么 25 个包只解出 23 帧? —— B 帧需要后面的帧到了才能确定显示顺序,解码器里会压着几帧。喂完所有包后还要 flush 一次,才会把剩下的吐出来。- ☐ (本节完,下一节 1.5.2 FFmpeg 的关键结构体)