一次播放是八步:开文件、找流信息、找解码器、开解码器,然后循环读包解码。 前四步只做一次,后面是循环;前四步最容易写错。
前面四节都在命令行层面,从这一节开始写 API。
先把骨架立起来,再往里填肉——否则你会陷入「每个函数都知道,但串不起来」的状态。
这八步是第八章 ffplay 和第九章同步逻辑的共同基础。
一、八步,前四步只做一次
| 步骤 | 做什么 | 失败会怎样 |
|---|---|---|---|
| 1 | avformat_open_input 开文件 | 返回空,后面全是空指针 |
| 2 | avformat_find_stream_info 找流信息 | 拿不到宽高与时长 |
| 3 | av_find_best_stream 找解码器 | 拿到错误的解码器 |
| 4 | avcodec_open2 打开解码器 | 解码返回负数 |
| 5 | 循环 av_read_frame 读包 | — |
| 6 | avcodec_send_packet / receive_frame | 循环写错就丢帧 |
| 7 | 转格式 / 渲染 | — |
| 8 | avcodec_free_context 等释放 | 内存泄漏 |
第 2 步是新手最容易跳过的一步:
avformat_open_input 只读了文件头,很多信息(时长、帧率、真实编码参数)还没解析。
find_stream_info 会「试读一段」来探测,
所以它可能花几十毫秒——这就是 Demo 里第 1、2 步加起来能占三分之一时间的原因。
第 3 步那个「找解码器」值得说清:流里只记了 codec_id(比如 H.264 是 27),
你要拿它去查「本机有没有对应的解码器」。
所以「这个文件放不了」有两种可能:编码不认识(没有 codec_id),
或者认识但没有解码器(没编进来)。
这两种情况的处理方式完全不同。
二、循环里最容易写错的一句
老 API 是「一次调用解一帧」:
// 老写法(已删除)
ret = avcodec_decode_video2(ctx, frame, &got, pkt);
新 API 是「发一个包、收若干帧」:
avcodec_send_packet(ctx, pkt);
while (avcodec_receive_frame(ctx, frame) == 0) {
// 处理这一帧
}
为什么改:因为「一个包可能出零帧、一帧、也可能出多帧」。 老接口把这个事实藏在返回值里,让调用方自己数;新接口把它变成「循环收」。 这就是一次「让接口匹配现实」的改造。
新模型有三个返回值必须分清:
0:收到一帧,继续收。
EAGAIN:暂时没有,该去发下一个包了。
EOF:解码器彻底结束,只能冲空包。
把 EAGAIN 当成错误处理,是最常见的写法错误:它不是错,是「正常的分支」。
日志里天天刷 EAGAIN 的项目,八成就是这里判断写反了。
三、为什么「只解一小段」特别慢
Demo 的五段计时里有个反直觉的结果:开文件两毫秒多、开解码器一毫秒多, 而整个解码循环只花了七毫秒。 也就是说「启动」占了三分之一还多。
这在工程上意味着:如果你只想「取一帧做封面」,可能启动比解码还贵。
所以「从视频里抽第一帧」这类需求,优化点不在解码,在「能不能跳过 find_stream_info 的探测」。
find_stream_info 有一个参数可以调:probesize 与 max_analyze_duration。
调小能加快启动,代价是「可能探测不准」——比如误判帧率、拿不到时长。
这是一个典型的「准确性与启动时间」的取舍。
最后说一个数字上的规律:第 1 步两毫秒、第 4 步一毫秒,这个量级跟文件大小关系不大, 主要是「打开文件 + 初始化查表」的固定开销。 所以「解一个 4 KB 的小视频」和「解一个 4 MB 的」,启动时间差不多。 这个认知对做「批量处理」很有用——小文件多的时候,瓶颈在启动,不在解码。
四、动手 Demo:解码的八步,每步各花多少
这段把八步缩成五段(第 5~7 步合成一段),逐段打时间戳,最后给出包数与帧数。
注意第 3 步:建解码器必须手动把 extradata 拷过去,
否则 send_packet 直接报「数据无效」。
c = av.open(src) # 1 open_input
s = c.streams.video[0] # 2 find_stream_info
cc = av.CodecContext.create(s.codec_context.name, "r")
cc.extradata = s.codec_context.extradata # 少了它解不出来
cc.open() # 3 4 找解码器并打开
for pk in c.demux(s): # 5 6 读包 + 解码
if not pk.size:
continue
for fr in cc.decode(pk):
pass
for fr in cc.decode(None): # 冲空包
pass
实跑输出:
--------------------------------------------------------------------
解码的八步,每步各花多少
--------------------------------------------------------------------
**FFmpeg 解码的流程是固定的八步**,**背下来以后,看任何示例代码都是这个骨架。**
测试片 **13718 字节**
**逐段计时**:
步骤 耗时 毫秒 说明
------------------------------------------------
1 open_input 1.98
2 find_stream_info 0.01
3 find_decoder + open 1.01
4 读包 + 解码循环 7.12
5 close 0.13
**包 90 个、帧 90 个**
**「读包 + 解码循环」那一行是大头**——**这是当然的,因为只有它在做真正的工作。**
**但前面三步加起来也不小**:**开文件要读索引、找解码器要初始化查表**,
**这两件事加起来能占三分之一。**
**所以「只解一小段就退出」的程序,时间基本花在启动上。**
**但前三步恰恰是最容易写错的**:**少一步就是空指针,顺序反了就是崩溃。**
**所以「慢的部分不用管,快的部分必须对」**——**这句经验在几乎所有 I/O 流程里都成立。**
**注意这里 `frames` 与 `packets` 的关系**:**这次恰好相等**(90 对 90),
**但这不是规律,是这段测试片太规整。**
**换成有 B 帧重排的片子**,**一个包可能解出零帧或两帧**,**一帧也可能跨多个包**。
**所以代码不能假设「一进一出」**,**必须写成 `for fr in decode(pk)` 的循环。**
五段耗时的分布是「三七开」:启动三步(1、2、3)加起来三毫秒多,解码循环七毫秒。 这个比例在小文件上尤其明显。
extradata 那一行是本段最值钱的一句:它是 SPS/PPS 之类的「初始化参数」,
从流里拷到解码器上下文,没有它解码器不知道分辨率、不知道 profile。
第一次跑这段时报的错是 Invalid data found when processing input——一句话跟「参数没给」完全没关系。
这正好演示了 FFmpeg 的报错风格:它告诉你「数据无效」,但不说「缺了哪一项」。 所以「按骨架逐步核对」比「读错误信息」有效得多。
包数与帧数这次恰好相等(都 90), 但这不改变「要写成循环收帧」这条规矩——换一段带 B 帧重排的片子,立刻就不等了。 用「恰好相等」的样本验证代码,是最容易埋雷的做法。
30 秒实验
- 把
find_stream_info的probesize调小,看启动时间少多少、时长还准不准。 - 算一下:解一个 4 KB 的小视频和一个 4 MB 的,启动时间差多少?为什么?
- 判断一下:
receive_frame返回EAGAIN是错误吗?该怎么处理?
常见误区
- 跳过
find_stream_info。 只open_input拿不到时长和真实参数,后面会拿到 0 或者错的值。 - 用老 API
avcodec_decode_video2。 它在 FFmpeg 4.0 就废弃了,新模型是 send/receive。 - 把
EAGAIN当错误。 它表示「暂时没有帧,去发下一个包」,是正常分支。 - 建解码器时忘了拷
extradata。 报「数据无效」,而真实原因是「缺初始化参数」。
自测三问(点开对答案)
1. 播放流程的前四步是什么? —— `avformat_open_input`、`avformat_find_stream_info`、`av_find_best_stream`、`avcodec_open2`。 这四步只做一次,而且最容易写错。 2. 为什么 `find_stream_info` 可能很慢? —— 它要「试读一段」来探测真实参数(时长、帧率),所以耗时跟探测长度有关, 可以调 `probesize` 换速度。 3. `receive_frame` 返回 `EAGAIN` 是什么意思? —— 暂时没有可出的帧,应该去发下一个包。它不是错误,是正常分支。骨架有了,下一节补一件琐事:环境配置。C++ 里用 FFmpeg 比用 OpenCV 多一个坑——extern "C"。
- ☐ (本节完,下一节 07.03.02 配置Qt和VS 2015的FFmpeg开发环境)