FFmpeg 不是一个库,是六个各管一段的库。
播放器把它们串起来的方式,就是「包进帧出」这一条链——理解了 AVPacket 到 AVFrame 的转换,
就理解了整个架构。
上一节画了五段流水的骨架,这一节把每一段对应的库对上号。 这一节也是后面七章的地图:第二到六章讲别人怎么用这些库(VLC、Qt、OpenCV、SDL), 第七章自己动手用,第八九章拆开 FFplay 看官方是怎么用的。
一、六个库的分工
名字里都带 av,因为它们共享一套基础类型(libavutil)。
| 库 | 管什么 | 播放器里用在哪 |
|---|---|---|
| libavformat | 容器与协议 | 打开输入、读包 |
| libavcodec | 编解码器 | 把包变成帧 |
| libavutil | 公共结构与工具 | 帧、包、时间基、像素格式 |
| libswscale | 图像缩放与转换 | YUV 转 RGB、改尺寸 |
| libswresample | 音频重采样 | 改采样率、改声道布局 |
| libavfilter | 滤镜图 | 播放器很少用,转码常用 |
这六个库里,播放器必须用的是前五个。libavfilter 在播放器里基本不用——它的价值在转码与特效(31 课讲的就是它)。
从依赖关系看更清楚:libavformat 和 libavcodec 都依赖 libavutil, swscale 与 swresample 又各自依赖 avutil。所以 avutil 是最底层,所有类型都从它来。
二、四个关键对象
整个播放链路就是四个对象在传递:
| 对象 | 代表什么 | 谁产生 | 谁消费 |
|---|---|---|---|
| AVFormatContext | 一个打开的文件或流 | avformat_open_input | 解封装 |
| AVStream | 里面的一条流 | 同一函数填好 | 选解码器用 |
| AVPacket | 一个压缩包 | av_read_frame | 解码器 |
| AVFrame | 一帧解开的画面或声音 | 解码器 | 转换与渲染 |
这四个对象的顺序就是数据的流向:先有上下文,上下文里有若干流,从流里读出包,包解出帧。
最容易混的是「流」与「上下文」:一个 MP4 里有视频流和音频流两条, 它们共用一个上下文,但各有自己的时间基、各有自己的解码器。同步这件事就是跨这两条流做的。
三、包与帧差多少
这一节最该记住的一个数字:demo 里第一个包 870 字节,解出来的帧 115200 字节——一百三十多倍。
这个倍数就是「压缩」的直观含义:包是「怎么把这一帧画出来」的说明书, 帧是「每个像素什么颜色」的全量数据。
由此可以推出一件工程上的事: 解码器需要缓冲,而且缓冲是按帧算的——缓 10 帧 1080p 就是 30 MB,不是几 KB。 这就是为什么「预缓冲几秒」在 4K 播放器上很吃内存。
四、时间基:时间戳的单位
时间戳不是秒,是一个整数,单位由 time_base 决定。 time_base 是一个分数,比如 1/15360,意思是「一个刻度等于 1/15360 秒」。
为什么要这么绕:因为整数运算快、且没有浮点累积误差。 但麻烦在于三条流的时间基常常不一样:视频可能是 1/15360,音频可能是 1/48000, 容器可能是 1/1000。
所以要比较两个时间戳,必须先换算到同一个时间基:
av_compare_ts 与 av_rescale_q 就是干这个的。
时间基写错是新手最常踩的坑:把视频的 pts 直接和音频的 pts 相减, 得到的数在两边单位不同的情况下毫无意义,但程序不会报错,只会「莫名其妙不同步」。
五、一个最小的播放循环
抛开所有细节,播放器的主循环只有两种写法:
老写法:av_read_frame 读包 → avcodec_decode_video2 出帧。
新写法:av_read_frame 读包 → avcodec_send_packet 送 → avcodec_receive_frame 收。
新写法(2016 年引入)之所以必要,是因为它允许「一次进、多次出」:送一个包进去, 可以收零帧(还在缓存)、一帧,甚至多帧。 老写法用返回值表示「消耗了多少字节」,在 B 帧与多 slice 的情况下表达不清,容易写出微妙的 bug。
所以今天的正确循环长这样:
读包;
送包,可能收到 EAGAIN(表示要先收帧);
循环收帧直到再收到 EAGAIN(表示要再喂包);
读完了,送一个空包冲干净。
EAGAIN 不是错误,是流程控制信号——这段循环里最容易写错的,就是把 EAGAIN 当成失败退出。
六、2026 年的现状
这本书成于 2023 年,代码里还有 av_register_all。
这个函数在 FFmpeg 4.0 就废弃、5.0 直接删了。
今天再照抄书上的代码,第一步就编译不过。
另外几处必改的地方:
avcodec_decode_video2 与 avcodec_decode_audio4 已删除,改 send/receive。
avpicture_* 系列已废弃,改用 av_image_*。
AVStream.codec 已废弃,改用 AVStream.codecpar。
错误码的判断从 < 0 改成用 AVERROR 系列的宏——AVERROR(EAGAIN) 与 AVERROR_EOF 要分开处理。
本机装的是 FFmpeg 8.1.2(libavcodec 62、libavformat 62),比书上那代新了两个大版本。 所以本课所有 Demo 都用 Python 的 PyAV 写, 它跟着新版本走,不会出现「书上的 API 已经不存在」的问题。
七、动手 Demo:FFmpeg 不是一个库,是六个库
这段打印本机六个库的版本,再把同一个视频的「包」与「帧」摆在一起对比。 重点看最后那两行的体积差。
for k in (u"libavcodec", u"libavformat", u"libavutil",
u"libswscale", u"libswresample", u"libavfilter"):
v = av.library_versions.get(k)
T([k, u"%d.%d.%d" % v], [0, 0])
pkt = next(c.demux(st))
fr = pkt.decode()[0]
print(pkt.size, fr.width, fr.height, fr.format.name)
实跑输出:
--------------------------------------------------------------------
FFmpeg 不是一个库,是六个库
--------------------------------------------------------------------
**FFmpeg 这个名字底下是六个各有分工的库,播放器把它们串起来。**
库 管什么 播放器里用在哪
---------------------------------------------------------------------
libavformat 容器格式与协议 解封装、读网络流
libavcodec 音视频编解码 解码
libavutil 公共工具与结构 帧、包、时间基、像素格式
libswscale 图像缩放与像素转换 YUV 转 RGB、缩放
libswresample 音频重采样与混音 采样率与声道转换
libavfilter 滤镜图 播放器一般不用,转码才用
**本机这六个库的版本**:
库 版本号
-------------------------------
libavcodec 62.28.102
libavformat 62.12.102
libavutil 60.26.102
libswscale 9.5.102
libswresample 6.3.102
libavfilter 11.14.102
**同一个视频,压缩包和帧长这样**:
对象 字段 值
-----------------------------------------
AVPacket size(字节) 870
AVPacket pts 0
AVPacket dts -1024
AVPacket time_base 1/15360
AVFrame 宽×高 320×240
AVFrame 像素格式 yuv420p
AVFrame pts 0
**这两类对象的区别,就是「解码」这件事的全部意义**:
包是压缩的——几百字节到几十 KB,看不出画面;帧是展开的——一屏像素,直接能贴。
上面这一包里 870 字节,解出来的帧是 320×240 的 yuv420p,光像素就 115200 字节。
**一个包不一定出一帧,一帧也可能要等好几个包**(B 帧会把顺序打乱)。
所以解码循环永远长这样:发一个包进去,可能收零个、一个或多个帧出来。
870 字节对 115200 字节,一百三十二倍。这个比例在小分辨率下还算温和——1080p 的一帧是 3110400 字节,而它的包通常只有几十 KB,倍数上百。
pts 0 与 dts -1024 这一对值得看一眼:第一帧的显示时间是 0,
但解码时间是负的——因为 B 帧要先解后显,所以 dts 必须比 pts 早。
这就是「解码顺序与显示顺序不是一回事」的证据。
time_base 1/15360 是 libx264 的默认值,它等于 30 fps 的一帧再细分 512 份。
记住这个数不是必需的,但要知道「每家的 time_base 都不同」,所以换算不能省。
30 秒实验
- 在任意一个视频文件上跑一次
ffprobe(或 PyAV),看视频流与音频流的 time_base 各是多少。 - 用两个 time_base 把同一个时刻换算成两个整数,验证它们确实不相等。
- 想一想:为什么解码器不能用「一个包一个帧」的方式工作?
常见误区
- 以为 FFmpeg 是「一个播放器」。 它是一套库,ffplay 只是它自带的一个示例程序。
- 把
EAGAIN当成错误。 它是「现在没有帧,你得再喂包」的意思,当成失败退出会解不出任何帧。 - 直接用两条流的 pts 相减做同步。 时间基不同,相减没有意义,必须先换算到同一时间基。
自测三问(点开对答案)
1. 播放器必须用哪几个库?哪个一般不用? —— 必须用 avformat、avcodec、avutil、swscale、swresample 五个; avfilter 在播放器里基本不用,那是转码与特效的领域。 2. 为什么新接口要改成 send/receive 两个函数? —— 因为解码不是一一对应的:一个包可能出零帧、一帧或多帧。 用两个函数把「喂」和「取」分开,才能正确表达这种关系。 3. 书上 `av_register_all` 为什么今天编译不过? —— 它在 FFmpeg 4.0 废弃、5.0 删除。现在格式与编解码器是自动注册的,不再需要显式调用。第一章的骨架到此为止:数据是什么、怎么传、收到之后过几道手,都有了。 下一章换个角度,先看一个已经把这些全做好的播放器——VLC。
- ☐ (本章完,下一节 02.01.01 VLC简介)