🎬 第七章 FFmpeg解码音视频及流媒体
📄 一句话概括

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 秒实验

  1. 查一下你手边那段 FFmpeg 示例代码,看它用的是 av_register_all 还是新 API。
  2. 算一下:stream.time_base 是 1/15360,一个包的 pts 是 46080,那是第几秒?
  3. 判断一下:把一个 mkv 转成 mp4 但编码不变,这是转封装还是转码?

常见误区

  1. 照抄带 av_register_all 的老代码。 它在 FFmpeg 5.0 已被删除,编不过。 现在的正确做法是「什么都不做」。
  2. 拿 frame.pts 乘 stream.time_base。 帧的时间基是解码器重设的,跟流的不一样。 谁的时间基配谁。
  3. 以为转封装也要解码。 转封装只动 libavformat,一个像素都不解,所以快得离谱。
  4. 以为一个包一定解出一帧。 有 B 帧时会先攒着,必须写成循环收帧,而且结尾要冲空包。
自测三问(点开对答案) 1. 六个库里,解封装和解码分别是哪个? —— `libavformat` 负责解封装(文件 → 包),`libavcodec` 负责解码(包 → 帧)。 这条分界线决定转封装还是转码。 2. 为什么 `av_register_all` 现在不用写了? —— FFmpeg 4.0 把它标为废弃、5.0 删除; 注册表改成懒加载,`avformat_open_input` 内部会自己初始化。 3. 包的时间基和帧的时间基为什么不一样? —— 包的时间基来自容器(为了精度常取大数分之一),帧的时间基由解码器设置(常是帧率倒数)。 两者不能混用。

框架清了,下一节先不写代码。先看一个已经把这些全做好的播放器怎么用——ffplay,最短的那个参考实现。

相关笔记