🎬 第七章 AVFormat封装层理论及案例实战
📄 一句话概括

七个 API 管「开、找、读、跳、写头、写包」,一条流水线走完。 在 PyAV 里有一半看不见——它们被藏进了 av.open。

📝 本节和前面几节的关系

7.1 讲结构,这一节讲操作这些结构的函数。 下一篇 7.3 拿它们跑一次完整的解封装。

一、七个函数的顺序

按调用顺序排,比按字母排好记。 顺序 C 的 API 干什么


一 avformat_open_input 打开,认容器 二 avformat_find_stream_info 补全流参数 三 av_find_best_stream 挑出要用的流 四 av_read_frame 读包 五 av_seek_frame 跳(按需) 六 avformat_write_header 写文件头 七 av_interleaved_write_frame 写包

前四个是读方向,后两个是写方向,第五个两边都能用。 这个顺序不能颠倒:没 open 就没流,没流就没包,没写头就写不了包。 FFmpeg 的 API 是状态机的形态,不是可以随便排的工具箱。

二、PyAV 藏起来的那三个

藏起来不是消失,是不用你操心。 C 里要手写 PyAV 里


分配 AVFormatContext av.open 内部做 find_stream_info 探一轮 av.open 内部做 write_header 写头 av.open(..., "w") 内部做

省事的代价是看不见。 出问题的时候不知道它探了几轮、读了多少字节、头写成什么样。 要看得见也有办法:拿一个包装器套住输入(第六章那个 LogIO),把每一次 read 都记下来。 这是排查封装问题的第一招。

三、find_stream_info 到底在探什么

打开容器的头时,参数常常不全。 缺什么 为什么要探 探不到会怎样


分辨率 头里可能只写了个大概 拿到 0 帧率 要数几个时间戳才准 算不出 编码参数 SPS/PPS 在码流里 解不了 时长 有的容器不写 读到 None

它的做法就是「先真解一小段试试」。 所以打开一个网络流时会先卡一下,那是在探。 PyAV 里这个动作是自动的,代价同样存在:av.open 比自己以为的要慢。 要精确控时间,就得知道它在探。

四、交错:封装里最容易忽略的一件事

封装不是把包按流分组放,而是按时间穿插放。 排法 播放效果 为什么会这样排


交错 边读边播 音视频包挨着,都够用 先视频后音频 要缓冲很久 要等音轨才开始

av_interleaved_write_frame 名字里那个 interleaved 就是干这个的。 它内部有队列, 按 pts 排好再写。 这也是为什么不能随手用 av_write_frame 代替:那个是「你喂什么就写什么」,交错得你自己保证。 自己写封装时最常见的问题就是音画不同步,十有八九是交错没做对,而不是时间戳算错了。

五、关的时候要成对

开一个关一个,这是硬规矩。 开 关 漏了会怎样


avformat_open_input avformat_close_input 内存泄漏 avformat_alloc_output_context2 avformat_free_context 同上 avio_open avio_closep 输出文件不完整

最后一行最阴:输出没关好,文件头里的索引就没写完,文件看着有、打不开。 这就是「录了半小时,文件坏了」的常见原因。 PyAV 里对应的就是 container.close(),而且要用 with 或者 try 保证它一定被调到。 写输出时尤其不能省。

六、动手 Demo:封装层六个 API,在 PyAV 里长什么样

先把七个 API 一一对照,再拿看得见的那几个真跑一遍。

import av

c = av.open("tiny.mp4")
vs = c.streams.video[0]
print(c.format.name, len(c.streams), vs.time_base, vs.average_rate)

nv = na = 0
for pk in c.demux():
    if pk.size == 0:
        continue
    if pk.stream.type == "video":
        nv += 1
    else:
        na += 1
c.close()
print("视频包", nv, "音频包", na)

实跑输出:

--------------------------------------------------------------------
封装层六个 API,在 PyAV 里长什么样
--------------------------------------------------------------------

**① 一一对照**

  C 的 API                       PyAV 里怎么用              干什么
  --------------------------------------------------------------------------
  avformat_open_input            av.open(path)              打开,认容器
  avformat_find_stream_info      av.open 里自动做           补全流参数
  av_find_best_stream            streams.video[0]           挑一条流
  av_read_frame                  container.demux()          读一个包
  av_seek_frame                  container.seek()           跳到某个时间
  avformat_write_header          av.open(..., "w") 里做     写文件头
  av_interleaved_write_frame     container.mux(pkt)         写一个包

**七行里有三行在 PyAV 里「看不见」**:打开、找流信息、写文件头,都被包进了 `av.open` 里。
**这是好事,也是坑**——出问题时不知道它替你做了什么。

**② 看得见的那几个,真跑一遍**

认出 **mov,mp4,m4a,3gp,3g2,mj2**,**2 条流**,视频 time_base **1/12800**、音频 **1/44100**,
帧率 **25**。

**一次 demux 循环里一共出来 50 个视频包、88 个音频包**,
它们是被**交错**排好的——一个视频包跟着一两个音频包。

**这就是「封装」两个字的实际含义**:不是把数据打个包,而是**按播放顺序把两条流的包穿插排好**。

**③ 交错排错了会怎样**

**所有视频包排在前面、音频包排在后面**,播放器就得先把视频全缓冲下来,才能配上声音。
**这就是「音画不同步」的一大来源。**

**`av_interleaved_write_frame` 名字里那个 interleaved 就是干这个的**:它内部有个队列,
按 pts 排好再写,不按你喂的顺序写。

一次 demux 出来 50 个视频包、88 个音频包,是交错排好的。 这个「交错」就是封装真正干的活。

30 秒实验

  1. 把 av_find_best_stream 的对应写法找出来(提示:streams.best)。
  2. 在 demux 循环里打印每个包的 pts,看两条流的时间戳是不是交替上升。
  3. 故意不 close 就跑第二次,看会不会报资源占用。

常见误区

  1. 以为 PyAV 少做了事。 它把三步藏进了 av.open。
  2. 用 av_write_frame 代替 interleaved 那个。 交错得自己保证。
  3. 输出容器不开 with 直接跑。 中途出错就留下一个坏文件。
自测三问(点开对答案) 1. 七个 API 里哪几个在 PyAV 中看不见? —— open_input、find_stream_info、write_header,都被 av.open 包了。 2. find_stream_info 为什么慢? —— 它会真解一小段来补全参数,网络流上尤其明显。 3. 交错是什么意思? —— 按时间把多条流的包穿插排好,让播放器边读边播。

相关笔记