🎬 第一章 FFmpeg入门简介
📄 一句话概括

四个结构体撑起整条流水线:FormatContext 管文件、CodecContext 管编解码、 Packet 管压缩包、Frame 管像素。 字段名从 C 到 Python 几乎没改,认得出 C 的就认得出属性。

📝 本节和前面几节的关系

1.5.1 讲流水线怎么走,这一节讲流水线上流动的东西长什么样。 第二章开始用命令行,这些结构体就藏在参数背后了。

一、四个结构体,四种角色

流水线每一段手里拿的东西,都对应一个结构体:

结构体 管什么 生命周期


AVFormatContext 一个文件或一条流 从头到尾都在 AVCodecContext 一台编解码器 解码开一次,编码开一次 AVPacket 一段压缩数据 来一个用掉一个 AVFrame 一张展开的画面 来一个用掉一个

第三列很重要:两个 Context 是「活着的」,两个数据结构是「用完就扔」。 Context 里存着状态,比如解码器的参考帧、编码器的 GOP 计数。

所以 Context 不能随手新建。 每新建一个 CodecContext,就等于新开一台编解码器,里面的状态从零开始。 一帧建一个,GOP 就碎了,编码效率立刻掉下来。

二、各自的字段里最该记住的

每个结构体有几十个字段,但常用的就那么几个:

结构体 字段 意义


AVFormatContext streams 有几条流,各是什么类型 AVFormatContext duration 总时长,单位微秒 AVCodecContext width/height 分辨率 AVCodecContext pix_fmt 像素格式 AVPacket pts/dts 显示时刻与解码时刻 AVPacket size 这个包多少字节 AVFrame linesize 每行实际占多少字节 AVFrame key_frame 是不是关键帧

duration 那个单位容易被忽略:容器层的时长是微秒,不是秒。 因为它用的是全局常量 AV_TIME_BASE,值固定是 1000000。

pts 与 dts 的区别要记住一句话: pts 是「什么时候显示」,dts 是「什么时候解码」。 有 B 帧时两者不相等,dts 永远小于等于 pts。 所以你看到的乱序,其实是正常的。

三、时间基:同一个 pts 三种含义

这是最容易搞错的地方,也是最容易被忽略的地方:

在哪一层 时间基从哪来 典型值


容器 固定常量 AV_TIME_BASE 1/1000000 码流 stream.time_base 1/12800 之类的 帧 frame.time_base 通常同码流

算秒数的公式永远是:pts 乘 time_base。 但用哪个 time_base,取决于是哪一层的 pts。

搞错的典型后果是「时长差了一千倍」或者「画面播放速度快得离谱」。 遇到这种症状,先别改参数,先确认自己用的时间基对不对。

四、从 C 到 Python,字段名几乎没改

这是理解各种语言绑定最省力的一条:

C 里的写法 Python 里的写法


ctx->streams[i] container.streams[i] codec_ctx->width stream.codec_context.width pkt->pts packet.pts frame->linesize[0] frame.planes[0].line_size

改名的地方只有几处:-> 变成 .,数组下标换成对象属性。 这是因为 C 里用数组存平面,绑定时包成了对象。

所以看 C 的示例代码能直接翻译成 Python,反之亦然。 这一点让 FFmpeg 的资料特别通用——一份十年前的 C 教程,今天照着写 Python 也没问题。

五、动手 Demo:四个关键结构体——C 里的名字,Python 里的属性

这一段把四个结构体在 Python 里的对应物和关键字段摊开, 再看三种时间基在本机这一份素材上各是什么值。

import av

c = av.open(u"src.mp4")
st = c.streams.video[0]
f = next(c.decode(video=0))

print(u"流数", len(c.streams), u"时长", c.duration)
print(u"分辨率", st.codec_context.width, st.codec_context.height)
print(u"像素格式", st.codec_context.pix_fmt)
print(u"帧 pts", f.pts, u"时间基", f.time_base)
print(u"关键帧", f.key_frame, u"linesize", f.planes[0].line_size)
c.close()

实跑输出:

--------------------------------------------------------------------
四个关键结构体——C 里的名字,Python 里的属性
--------------------------------------------------------------------

**① 先把对应关系摆平:四个结构体,四个 Python 对象**

  C 里的结构体        Python 里是谁      拿的什么
  -----------------------------------------------------------
  AVFormatContext     InputContainer     整个文件或整个流
  AVCodecContext      CodecContext       一台编解码器
  AVPacket            Packet             一段压缩数据
  AVFrame             VideoFrame         一张展开的画面

**② 每个结构体上最该记住的那几个字段**

  结构体              字段               本机这一份的值
  ---------------------------------------------------------
  AVFormatContext     nb_streams         1 条流
  AVFormatContext     duration           1000000 微秒
  AVCodecContext      width × height     160 × 90
  AVCodecContext      pix_fmt            yuv420p
  AVCodecContext      name               h264
  AVFrame             pts                0
  AVFrame             key_frame          是
  AVFrame             linesize           256

**③ 两个时间基,是最容易搞错的地方**

  谁的时间基        本机值                    含义
  ------------------------------------------------------------------
  容器 duration     1/1000000(固定常量)     全局刻度,改不了
  码流              1/12800                   这一条流自己的刻度
  帧                1/12800                   这一帧自己的刻度

**同一个 pts,乘不同的 time_base 得到完全不同的秒数。**
**所以算「第几秒」之前,先看清楚手里这个 pts 是哪一层的时间基。**
**这一条会在后面每一章反复出现,先在这儿立住。**

**④ PyAV 这一层的价值,就是把这些字段直接摊成属性**

**C 里要 avformat_open_input、av_find_best_stream、avcodec_alloc_context3 一大串,**
**Python 里是 av.open、streams.video[0]、codec_context,一步到位。**
**但字段名几乎没改——认得出 C 的名字,就认得出 Python 的属性。**

第一张表把四个结构体和 Python 对象对应起来。 名字几乎一样,只是 -> 变成了 .。

第二张表列了本机这一份的真实值:1 条流、时长 1000000 微秒、160×90。 注意时长是微秒——因为容器层用的是固定常量 AV_TIME_BASE。

第三张表是三种时间基。 容器层固定 1/1000000,码流与帧都是 1/12800。 同一个 pts 乘不同的时间基,得到的秒数差 78 倍—— 这就是「时间基搞错会差一千倍」这类说法背后的机制。

最后一段是这一节真正的价值: C 里要一大串 alloc、open、find 才能拿到的东西,Python 里是三个属性。 但字段名没改,所以两边的知识是通用的。

30 秒实验

  1. 看流结构:ffprobe -show_streams 文件,把每条流的 index、codec_name、time_base 都打出来。对照本文的表,看哪几行能对上。
  2. 算真实秒数:拿到一个包的 pts,乘上 stream.time_base,看是不是等于它在片子里出现的时刻。差得离谱就是时间基用错了。
  3. 验证 keyframe:ffprobe -show_frames -select_streams v 文件 | grep key_frame,数出关键帧有几个。这个数就是整片的 GOP 数量。

常见误区

  1. 以为 duration 的单位是秒。容器层的 duration 是微秒(AV_TIME_BASE=1000000),码流层的 duration 是「多少个 time_base」。两者不能混着用。
  2. 把 pts 和 dts 当成一回事。有 B 帧时两者不相等,dts 是解码顺序,pts 是显示顺序。用错了会花屏或时间戳错乱。
  3. 每帧新建一个 CodecContext。这等于每帧开一台新编码器,GOP 被打碎,压缩效率暴跌。Context 要复用,帧才是一次性的。
自测三问(点开对答案) 1. AVPacket 与 AVFrame 最本质的区别是什么? —— Packet 装的是压缩后的数据,Frame 装的是展开的像素。前者小、需要解码器才能读懂;后者大、可以直接改。 2. 容器层的 duration 是 1000000,这是多少秒? —— 1 秒。因为容器层的时间基固定为 1/1000000,1000000 × 1/1000000 = 1。 3. 为什么说「认得出 C 的字段名,就认得出 Python 的属性」? —— 因为绑定层基本保留了原字段名,只把 `->` 换成 `.`、把数组换成对象。改动集中在平面的访问方式上,其余几乎一一对应。

相关笔记