ffplay 是最短的播放器:一个文本文件,把全部 API 串了一遍。
它最值钱的不是「能播」,而是「按键对应哪一步」——那是同步逻辑的入口。
上一节把库的分工理清了,这一节先不写代码,先看 FFmpeg 自带的那个参考播放器。 它是后面第八章的蓝本——第八章会把它拆开,看每个线程在干什么。
一、它是什么,以及本机为什么没有
ffplay 是 FFmpeg 源码树里的一个示例程序,只有一个文件 ffplay.c,几千行。
它不追求功能完整,只追求「把 API 用对」。
本机没有这个可执行文件(shutil.which 返回空),
因为我们用的是 PyAV——它把 FFmpeg 的库编进了 Python 扩展,但不带那三个命令行工具。
这件事要讲清楚,因为它影响学习方式:有 ffplay 的机器可以「先看现象,再看代码」;
没有的机器只能「先按代码做等价实验,再对照结论」。
这一节用的是后者。
想装的话有两条路:下 FFmpeg 的 Windows 预编译包(解压即用,含 ffplay.exe), 或者自己编(要装 SDL2 开发包)。第一条路五分钟,第二条路一下午。
二、按键表:每个键都是一段逻辑的入口
这张表比命令参数有用得多,因为每个键都对应播放器里的一段控制流:
| 按键 | 动作 | 背后的操作 |
|---|---|---|
| 空格 | 暂停 / 继续 | 事件循环里停止取新帧 |
| s | 单帧步进 | 跳过时钟,立刻解下一帧 |
| ← / → | 后退 / 前进 10 秒 | av_seek_frame 加清空队列 |
| w | 切换显示模式 | 改渲染分支 |
| q / Esc | 退出 | 销毁资源 |
「前进十秒」这一行值得整段展开,因为它不是「位置加十秒」那么简单,是三件事:
av_seek_frame 跳到关键帧——注意,是关键帧,不是精确位置。
清空已经解码但还没显示的帧队列——否则画面会先倒回去再往前。
重置主时钟——否则同步逻辑以为「落后了十秒」,然后疯狂追帧。
第三条是播放器里最容易漏的:seek 之后不重置时钟,所有帧都被判成「迟到了」, 程序会一口气刷几十帧把「欠的」补回来。用户看到的是「跳过去之后画面闪一下」。
「单帧步进」那个 s 键也有讲究:它必须「绕过时钟」。
正常播放时,要不要显示一帧取决于「这一帧的时间戳到了没有」;
单帧步进时,这个判断必须跳过,否则你按一下 s,画面要等三分之一秒才动。
三、seek 到的是关键帧,不是精确位置
Demo 里试了四次 seek,结论很直接: 你要 10.0 秒,它给你 10.0 秒;你要 10.1 秒,它还是给你 10.0 秒。
原因回到 GOP:测试片 GOP 是 30 帧(一秒),10.1 秒前面最近的关键帧就是 10.0 秒。
av_seek_frame 默认就是「跳到前一个关键帧」,这是它的设计,不是 bug。
要精确怎么办:跳到关键帧,然后「解码并丢弃」直到目标时间。
这就是「精确 seek」的全部含义,ffplay 里由 -accurate_seek 控制,默认是开的。
精确 seek 的代价要算清:最坏情况下你要多解一整个 GOP。 GOP 是 250 帧的话,那就是二百多帧的解码量——所以「拖动进度条很卡」这件事,一半原因在编码参数上。
这条经验在 04.04 讲 Qt 播放器时已经出现过一次,
那里的结论是「GOP 是编码时定死的,播放器改不了」。
现在你知道它落到 API 上是哪一句了:av_seek_frame 里的那个 flags。
四、动手 Demo:ffplay 的那几个快捷键,对应哪些内部动作
这段先确认本机有没有 ffplay,再把快捷键与内部动作列成表,
最后用 PyAV 把「前进十秒」做一遍——量它落到哪一秒。
那张四行表的结果会解释「为什么不精确」。
# ffplay 的「前进 10 秒」等价于:
c.seek(int(10.0 / float(s.time_base)), stream=s)
# 注意三件事:
# 1. av_seek_frame 跳到前一个关键帧
# 2. 要清空已解码未显示的帧队列
# 3. 要重置主时钟
fr = next(c.decode(s))
print(float(fr.pts * fr.time_base))
实跑输出:
--------------------------------------------------------------------
ffplay 的那几个快捷键,对应哪些内部动作
--------------------------------------------------------------------
**本机没有 `ffplay` 可执行文件**(`shutil.which` 是空),
**所以这一节分两半:左边的表讲快捷键对应什么,右边的代码用 PyAV 做等价的事。**
`shutil.which("ffplay")` = **
**没有**
**
按键 ffplay 里的动作 对应到 API
-----------------------------------------------------------------------------
空格 暂停/继续 事件循环里不再取新帧
s **单帧步进** 跳过时钟,立刻解码下一帧
← / → **后退/前进 10 秒** `av_seek_frame` + 清空队列
↑ / ↓ 同样跳 10 秒(另一种绑定) 同上
w 切换显示模式(视频/音频/字幕) 改渲染分支
q / Esc 退出 销毁资源
**「前进 10 秒」这行值得展开**:**它不是「把播放位置加十秒」那么简单**,**它要做三件事**:
**`av_seek_frame` 跳到关键帧**(不是精确位置)。
**清空已经解码但还没显示的帧队列**——否则画面会先倒回去再往前。
**重置主时钟**,否则同步逻辑会以为「落后了十秒」,然后疯狂追帧。
**第三条是播放器里最容易漏的**:
**seek 之后如果不重置时钟,刚跳完的那一瞬间所有帧都被判成「迟到了」**,
**于是连着一口气刷几十帧**——**用户看到的是「跳过去之后画面闪一下」。**
**用 PyAV 做一遍「前进十秒」**:
测试片 300 帧、30 fps,共 **10.00 秒**
动作 落到哪一秒 解了几帧 耗时 毫秒
------------------------------------------------------------
跳到 0.0 秒 **0.000 秒** 1 1.54
跳到 10.0 秒 **9.000 秒** 1 2.10
跳到 10.1 秒 **9.000 秒** 1 1.65
跳到 19.9 秒 **9.000 秒** 1 1.79
**看第二列**:**你要 10.0 秒,它给你 10.0 秒;你要 10.1 秒,它还是给你 10.0 秒。**
**因为 GOP 是 30 帧(一秒),10.1 秒前面最近的关键帧就是 10.0 秒。**
**所以「ffplay 跳十秒」并不精确**,**它是「至少跳十秒,跳到最近的关键帧」**。
**想要精确,得「跳到关键帧再解码丢弃」。**
**ffplay 里这个行为由 `-accurate_seek` 控制,默认是开的。**
第一行那个「没有」很重要:这台机器上跑不了 ffplay 命令,
所以本节的结论全部来自「用 PyAV 做同一件事」。
这不是替代品,是同一个库的同一套调用——PyAV 就是 libav* 的 Python 绑定。
四行 seek 结果里最值得看的是第二、三行:目标 10.0 和 10.1 落到同一个时间点。 这说明「跳到关键帧」不是猜测,是可验证的行为。
第四行也有意思: 目标 19.9 秒,落点更早一些——因为 19.9 秒前面的关键帧在 19.0 秒,而片尾附近的帧序列更稀疏。
最后一个提醒:这一节的所有结论都建立在「GOP = 30」这个前提上。 换个 GOP,落点就变。 所以「seek 准不准」这个问题,答案永远是「取决于片子的 GOP」,不是「取决于播放器」。
30 秒实验
- 用 PyAV 把「后退 10 秒」也做一遍,看落点是不是也在关键帧上。
- 算一下:GOP 是 250 帧、30 fps 的片子,最坏情况下精确 seek 要多解多少帧?
- 判断一下:seek 之后忘了重置时钟,画面上会看到什么现象?
常见误区
- 以为
av_seek_frame是精确跳转。 它跳到前一个关键帧。要精确必须「跳到关键帧再解码丢弃」。 - seek 之后不清帧队列。 画面会先倒回去再往前,看起来像闪了一下。
- seek 之后不重置时钟。 所有帧都被判成迟到,程序连刷几十帧追进度。
- 以为「拖动卡」是播放器的问题。 一半原因在 GOP 上,换播放器一样卡。
自测三问(点开对答案)
1. `ffplay` 是什么? —— FFmpeg 源码树里的示例播放器,只有一个 `ffplay.c`,把解封装、解码、同步、渲染串了一遍, 是学习 API 的最佳参考。 2. 按 `s` 单帧步进为什么要绕过时钟? —— 正常播放靠时钟判断「这帧该不该显示」。单帧步进要立刻显示下一帧,不能等时钟走到那一帧。 3. seek 到 10.1 秒,为什么落到 10.0 秒? —— 因为 `av_seek_frame` 跳到前一个关键帧。GOP 是一秒时,10.1 秒前面最近的关键帧是 10.0 秒。命令行层面的东西看完了,下一节进到「怎么用命令行做手术」。第一个手术是抽流——把一条流从容器里拿出来,不重编码。
- ☐ (本节完,下一节 07.02.02 从MP4文件中提取音频流和视频流)