FFmpeg 是六个库拼起来的,分工按「数据形态」划界。
libavformat 给包、libavcodec 给帧,这条线决定「转封装」还是「转码」。
前六章一直在用「别人包好的东西」:LibVLC 全包了、Qt 三行代码、OpenCV 一行滤波、SDL 只管画。 第七章回到 FFmpeg 本体——这一章之后,你就不再是「调库的人」,而是「知道每一步在干什么的人」。 而这一切的起点,是搞清楚那六个库分别管哪一段。
一、六个库,按数据形态分工
| 库 | 管什么 | 输入 → 输出 |
|---|---|---|
libavformat |
解封装 | 文件/流 → 包 |
libavcodec |
编解码 | 包 → 帧 |
libavutil |
公共结构 | 帧、包、时间基 |
libswscale |
像素格式与尺寸 | 帧 → 帧 |
libswresample |
音频重采样 | 帧 → 帧 |
libavfilter |
滤镜链 | 帧 → 帧 |
这张表按「数据形态的变化」排队,所以它的顺序就是一次播放的顺序。 第一行把文件变成包,第二行把包变成帧,后面三行都在帧上做事。
第一行与第二行之间那条线,是整本书最重要的一条:它决定了「转封装」和「转码」的区别。
| 做法 | 动到哪一层 | 代价 |
|---|---|---|
| 转封装(remux) | 只动 format | 几乎不花时间 |
| 转码(transcode) | 动到 codec | 要重编,很贵 |
「几乎不花时间」不是修辞:Demo 里把 mp4 整体转封装,只用了两毫秒多一点, 因为它一个像素都没解,只是把包从一套索引搬到另一套索引。
二、老代码为什么跑不动:av_register_all
网上大多数 FFmpeg 教程是 2015 年前后写的,它们的第一步都是 av_register_all()。
这一步现在编不过。
时间线是这样的:FFmpeg 4.0 把它标为废弃(还能编,但会警告),5.0 直接删掉(编不过)。
现在什么都不用做,注册表改成懒加载,avformat_open_input 内部自己初始化。
Demo 里查了一下 hasattr(av, "register_all"),结果是 False——因为 PyAV 跟着 FFmpeg 一起删了。
这条经验要往大里说:音视频领域的老代码失效得特别快,
因为 FFmpeg 每个大版本都会删一批 API:4.0 删了 avcodec_decode_video2,
5.0 删了 av_register_all,写法从「一次解码一个包」变成了「send/receive 模型」。
所以看示例代码的第一件事是看它的版本,比看它写了什么更重要。
判断方法很土但很好用:
看它有没有 av_register_all、有没有 avcodec_decode_video2——有的话就是老代码,整段都要按新 API 重写。
三、两条时间基:包的和帧的
时间戳这件事在 FFmpeg 里有两个层级,而且单位不同:
| 对象 | 时间基从哪来 | 典型值 |
|---|---|---|
AVPacket |
stream.time_base |
1/15360 |
AVFrame |
frame.time_base(解码后重设) |
1/30 |
这是初学者最容易踩的一个坑:拿着 frame.pts 乘 stream.time_base,算出来的时间差得离谱。
因为解码器会把时间基换成「帧率倒数」,两个 pts 根本不在同一个刻度上。
正确做法是「谁的时间基配谁」:包的 pts 乘流的时间基,帧的 pts 乘帧的时间基, 两者算出来都该是「秒」,而且应该对得上。
为什么要有两个:容器里的时间是以「流的时间基」记的(为了精度,常取一个大数分之一), 而解码之后,帧的时间直接就是「第几帧」,因为解码器不知道容器的刻度。
这件事的后果在第九章会显形:音视频同步要比较「视频到哪了」和「音频到哪了」, 而这两条流的时间基常常不一样(一个 1/15360、一个 1/44100)。 不同一化就直接比,同步一定是错的。
四、动手 Demo:六个库的分工,一次解码到底过几道手
这段把六个库的分工列成表,再打出每个库的真实版本号,
最后验证一下 av_register_all 还在不在。
版本号那一组数不是给你背的——是给你「遇到问题时报给别人的」。
import av
for name, ver in av.library_versions.items():
print(name, "%d.%d.%d" % ver)
# 老教程的第一步,现在还在吗
print(hasattr(av, "register_all"))
实跑输出:
--------------------------------------------------------------------
六个库的分工,一次解码到底过几道手
--------------------------------------------------------------------
**FFmpeg 不是一个库,是六个库拼起来的。**
**记住分工,就不会在错误的地方找函数。**
库 管什么 本章会用到的调用
------------------------------------------------------------------------------
libavformat 解封装:把 mp4/rtmp 拆成包 `av_open` / `read`
libavcodec 解码:包变帧 `find_decoder` / `decode`
libavutil 公共结构:帧、包、时间基 `AVFrame` / `AVPacket`
libswscale 像素格式与尺寸 `sws_scale`
libswresample 音频重采样 `swr_convert`
libavfilter 滤镜链 `filter_graph`
**看第一行和第二行的分界**:**`libavformat` 给你的是「包」,`libavcodec` 给你的是「帧」。**
**这条分界线是整本书最重要的一条**——**它决定了「转封装」和「转码」的区别**:
只动 format、不碰 codec → **转封装,不重编码,快**。
碰了 codec → **转码,要重编,慢**。
**再看第一行的老 API**:
libavformat:**62.12.102**
libavcodec:**62.28.102**
libavutil:**60.26.102**
libswscale:**9.5.102**
libswresample:**6.3.102**
libavfilter:**11.14.102**
老教程的第一步 `av_register_all()` 还在吗:**
**不在了**
**
**`av_register_all` 是这一课必须提前说清的一件事**:
**它在 FFmpeg 4.0 被标记为废弃,5.0 直接删了。**
**所以网上 2018 年之前的示例代码,第一步就编不过。**
**现在的做法是「什么都不用做」**——**注册表改成懒加载,`avformat_open_input` 内部自己会初始化。**
**这条经验可以推广**:**音视频领域的老代码失效得特别快**,
**因为 FFmpeg 每个大版本都会删一批 API**。**看示例代码前先看它的版本,比看代码本身更重要。**
六个版本号一起打出来这件事,本身就是个好习惯。 因为 FFmpeg 的 bug 经常跟具体版本绑定,不带版本号去提问,基本得不到答案。
av_register_all 那行返回 False,说明这套 API 已经被删干净了。
而网上能搜到的示例代码,八成还在写它。
这里要提醒一句:别为了「让老代码跑起来」去找兼容层。
FFmpeg 删掉这些 API 是因为新模型更合理——send_packet / receive_frame 这套双向接口,把「一个包可能出多帧」这件事变成了接口的一部分,而不是靠返回值让你自己数。
所以第七章后面几节全部用新模型,看到 avcodec_decode_video2 一律当作历史遗迹。
30 秒实验
- 查一下你手边那段 FFmpeg 示例代码,看它用的是
av_register_all还是新 API。 - 算一下:
stream.time_base是1/15360,一个包的pts是46080,那是第几秒? - 判断一下:把一个 mkv 转成 mp4 但编码不变,这是转封装还是转码?
常见误区
- 照抄带
av_register_all的老代码。 它在 FFmpeg 5.0 已被删除,编不过。 现在的正确做法是「什么都不做」。 - 拿
frame.pts乘stream.time_base。 帧的时间基是解码器重设的,跟流的不一样。 谁的时间基配谁。 - 以为转封装也要解码。 转封装只动
libavformat,一个像素都不解,所以快得离谱。 - 以为一个包一定解出一帧。 有 B 帧时会先攒着,必须写成循环收帧,而且结尾要冲空包。
自测三问(点开对答案)
1. 六个库里,解封装和解码分别是哪个? —— `libavformat` 负责解封装(文件 → 包),`libavcodec` 负责解码(包 → 帧)。 这条分界线决定转封装还是转码。 2. 为什么 `av_register_all` 现在不用写了? —— FFmpeg 4.0 把它标为废弃、5.0 删除; 注册表改成懒加载,`avformat_open_input` 内部会自己初始化。 3. 包的时间基和帧的时间基为什么不一样? —— 包的时间基来自容器(为了精度常取大数分之一),帧的时间基由解码器设置(常是帧率倒数)。 两者不能混用。框架清了,下一节先不写代码。先看一个已经把这些全做好的播放器怎么用——ffplay,最短的那个参考实现。
- ☐ (本节完,下一节 07.02.01 ffplay视频播放)