🎬 第三章 夯实FFmpeg基础:重要数据结构及API
📄 一句话概括

四个结构体里,输入与输出被分开了,格式定义合成一个。 后缀名只用来猜格式,真正定格式的是文件头。

📝 本节和前面几节的关系

3.3 讲输入从哪来,这一节讲输入怎么被拆成语义。 下一篇 3.5 进编解码层,封装拆出来的包在那里变成帧。

一、AVFormatContext 是总账本

它是整个封装层的入口对象,一个文件对应一个。 里头挂着:格式定义、流的数组、metadata、总时长、总码率。 你在 3.1.3 读到的那些数,全从这个对象上取。 C 侧字段 存什么 Python 侧


iformat/oformat 格式定义 c.format streams 流数组 c.streams duration 总时长 c.duration bit_rate 总码率 c.bit_rate metadata 附加信息 c.metadata

注意它同时管输入与输出:读的时候 iformat 有值、写的时候 oformat 有值。 PyAV 干脆分成两个类,InputContainer 与 OutputContainer,这样类型检查能帮上忙。

二、两个格式定义合成一个

C 侧 AVInputFormat 与 AVOutputFormat 是两个结构体,Python 侧只有一个 ContainerFormat。 C 侧 Python 侧 靠什么区分


AVInputFormat ContainerFormat is_input AVOutputFormat ContainerFormat is_output

mp4 的两个标记都是 True,因为解复用与复用是同一份代码的两种用法。 MP4 的盒子结构读写对称,这个设计很自然。 no_file 为 False 说明它要真实文件(至少要可 seek 的字节流)。 有些设备采集格式 no_file 为 True,因为它们压根不落盘。

三、AVStream 一路一本分账

流对象管的是「这一路在容器里的位置与时间」,不编码参数。 字段 含义


index 在容器里的序号 time_base 这一路的时间刻度 duration 这一路的时长 start_time 第一帧的时间戳 average_rate 平均帧率 codec_context 挂着的编解码上下文

这是最容易混的一处:流上有 bit_rate,上下文上也有 bit_rate。 流上的是「这一路在文件里占多大带宽」,上下文上的是「编码器按什么码率编」。两者接近但不相等。 下一节 3.5 讲上下文。 现在只需要记住:时间归流,参数归上下文。

四、后缀名只用来猜,不定义格式

mp4 这个格式定义认十五种后缀:mp4、mov、m4a、3gp、3g2、mj2、heic、avif 等。 它们共用同一套盒子结构。 所以改后缀骗不过它:读的是文件头里的盒子,不是文件名。 反过来,把 mp4 改名成 .bin,它照样能读——后缀猜不中就回退到探测内容。 这个设计有个副作用:格式探测要读文件头,所以网络流必须等够字节才能认出格式。 这就是为什么某些流前几秒会卡——不是解码慢,是在等头部。

五、动手 Demo:封装层——一个对象管输入又管输出

查一下格式定义与流对象,看输入输出怎么合一。

import av

print("rtmp" in av.formats_available)
f = av.ContainerFormat("mp4")
print(f.name, f.is_input, f.is_output, f.no_file)
print(len(f.extensions), f.long_name)

实跑输出:

--------------------------------------------------------------------
封装层——一个对象管输入又管输出
--------------------------------------------------------------------

**① C 侧四个,Python 侧两个名字**

  教材结构体          Python 侧
  ---------------------------------------------------------
  AVFormatContext     InputContainer / OutputContainer
  AVInputFormat       ContainerFormat(is_input)
  AVOutputFormat      ContainerFormat(is_output)
  AVStream            VideoStream / AudioStream

**输入输出被分成了两个类**,这是 PyAV 的取舍:读和写的方法集不一样,挤在一个类里容易调错。
代价是**类型检查得自己记住**。

**② ContainerFormat 一个类干两份活**

  属性           值
  ---------------------------------------
  name           mp4
  long_name      MP4 (MPEG-4 Part 14)
  is_input       True
  is_output      True
  no_file        False
  extensions     15 个

**is_input 与 is_output 同为 True,说明同一个 mp4 解复用器与复用器是一份代码。**这不奇怪:
MP4 的盒子结构读写对称。

**no_file 是 False**,意思是它需要真实文件(或至少是可 seek 的字节流)。
有些格式 no_file 为 True——它们压根不落盘,比如某些设备采集格式。

**③ extensions 那一堆后缀名是同一件事**

mp4 这个解复用器认 15 种后缀:3g2、3gp、avif、f4v、heic、heif、ism、isma、ismv、m4a 等。

**这就是为什么改后缀名骗不过它**:认的是文件头里的盒子,不是名字。
反过来,把 mp4 改名成 .bin 它照样能读——后缀只用来猜,猜错了会回退到探测。

**④ 本机的 formats_available 是个集合,不是字典**

这是一个容易踩的坑:想按名字查属性,得先构造 av.ContainerFormat('名字'),
不能直接 av.formats_available['名字']。集合只告诉你「有没有」。

'mp4' in formats_available = **True**,'rtmp' in formats_available = **False**。

**这个集合里躺的全是「容器格式」的名字,一个协议名都没有。** 拿它查 http、file、tcp 一律查不到,
而那几个协议明明能用。3.3 已经说过这一点,这里再确认一遍——**别把它当协议表用。**

**⑤ 流对象的四个时间字段别混**

  字段             值          含义
  -----------------------------------------------------
  time_base        1/12800     刻度,一秒分成多少格
  duration         25600       以 time_base 为单位
  start_time       0           第一帧的时间戳
  average_rate     25          平均帧率

**duration 乘 time_base 才得秒**:25600 × 1/12800 = 2.00 秒。

这个集合里全是容器格式名,rtmp 不在不代表协议没编进来。 is_input 与 is_output 都是 True,说明读写共用一份实现。

30 秒实验

  1. av.ContainerFormat("flv") 看它的 extensions 有几个。
  2. av.ContainerFormat("matroska") 对比 long_name。
  3. 把一个 mp4 改名成 .bin,试着打开,看能不能读。

常见误区

  1. 靠后缀名判断格式。 后缀只用来加快猜测,定格式的是文件头。
  2. 在流对象上找 profile 与 level。 那是编码参数,在上头。
  3. 以为输入输出是两个格式定义就不能合并。 mp4 的读写对称,本来就一份代码。
自测三问(点开对答案) 1. 为什么 ContainerFormat 的 is_input 与 is_output 能同时为 True? —— 同一套盒子结构的读写实现,两个方向共用代码。 2. 流的 bit_rate 与上下文的 bit_rate 差在哪? —— 流上的是这一路在文件里实际占的带宽,上下文上的是编码器设定的目标码率。 3. 改后缀名能骗过 FFmpeg 吗? —— 不能。它读文件头定格式,后缀只是猜测的起点。

相关笔记