音视频基础系列(三):上一篇是啊鸡入坑音视频之《流媒体封装篇》。
拖动播放器进度条,看起来只是把时间从 01:20 改成 05:35。但播放器通常不能直接找到 5 分 35 秒对应的压缩帧并把它显示出来。
原因在于,视频压缩帧之间可能互相依赖;文件中的读取顺序、Decoder 的解码顺序和用户看到的显示顺序也不一定相同。一次 Seek 往往要同时处理四件事:
- I / P / B 帧描述帧之间的压缩依赖;
- DTS描述 Packet 应该何时送入 Decoder;
- PTS描述解码结果应该何时呈现;
- Seek根据时间轴找到合适的随机访问点,再重建解码状态。
把这四层分开,GOP、关键帧、音画同步和拖动进度条就能串成一条完整链路。
1. 为什么视频帧要互相参考
如果视频每一帧都保存成完整图像,数据量会非常大。现实画面在相邻帧之间通常只有局部变化:人物移动了一点,摄像机轻微转动,背景大部分保持不变。
H.264、H.265 等 Codec 会利用空间和时间上的重复信息。用最简化的模型看,编码帧可以分为 I、P、B 三类。
1.1 I Frame:只依赖自身信息
I Frame 是 Intra-coded Frame,即帧内编码帧。它使用当前画面内部的信息进行压缩,不需要其他视频帧就能恢复当前图像。
I
I 帧常被口语化地称为 Key Frame,因为它经常承担随机访问和恢复解码链的作用。它通常比预测帧更大,但能够提供一个新的解码起点。
不过,I 帧并不等于 JPEG,也不严格等于所有 Codec 语境中的随机访问点。I 只说明当前帧采用帧内编码;后续帧是否还可能引用更早的参考状态,是另一个问题。
1.2 P Frame:参考过去
P Frame 是 Predicted Frame。它通常参考已经解码的过去帧,只记录运动与残差,而不是重复保存整张画面。
I0
↓
P1 = 参考 I0 + 当前差异
↓
P2 = 参考已有帧 + 当前差异
如果必要的参考帧没有正确解码,后续依赖它的 P 帧也可能出现花屏、冻结或无法输出。
1.3 B Frame:还可以参考未来
B Frame 是 Bi-directionally Predicted Frame。它可以利用时间轴上过去和未来的参考帧来预测当前画面:
显示顺序:I B1 B2 P
↖ ↗
可参考 I 与 P
更多参考方向通常能提高压缩效率,却引入了一个直接后果:Decoder 必须先取得未来的 P 帧,才能解出排在它之前显示的 B 帧。因此,解码顺序和显示顺序可能分离。
2. GOP 怎样在码率与随机访问之间取舍
I、P、B 帧通常按结构组成 GOP(Group Of Pictures,图像组):
I B B P B B P B B P
或更简单:
I P P P P P
工程上常用 GOP Length 或 Key Frame Interval 描述随机访问点之间的距离。假设视频为 30 fps,每 60 帧产生一次关键帧,那么关键帧间隔大约是:
60 / 30 = 2 秒
GOP 长短形成明确权衡:
| GOP | 优点 | 代价 |
|---|---|---|
| 较长 | I 帧比例低,通常压缩效率更高 | Seek、首屏和错误恢复可能更慢 |
| 较短 | 更容易随机接入,Seek 与恢复通常更快 | I 帧更多,平均码率可能升高 |
这也是录像、点播、直播、RTC 和 IPC 对 GOP 配置要求不同的原因。它不是一个单纯追求“越大越省”或“越小越快”的参数,而是在带宽、延迟、随机访问和恢复时间之间做取舍。

3. I Frame 与随机访问点不是完全一回事
在 H.264 中,I Frame 表示当前图像使用帧内编码,但不必然保证后续图像不再引用它之前的画面。
IDR(Instantaneous Decoder Refresh)提供更强的边界语义。遇到 IDR 后,Decoder 可以清理之前的参考图像;后续图像不会越过这个边界引用更早的画面。因此,IDR 更适合作为独立解码的随机访问点。
H.265 中还存在 CRA 等其他随机访问类型,它们对“从哪里开始解码”和“起点之后哪些图像可以安全输出”有更细的规则。
所以日常口头上的“请求一个 I 帧”,在实时视频工程里通常真正想表达的是:
请求一个能让 Decoder 重新建立可靠参考状态的随机访问帧,例如 IDR。
这个区别在 IPC 弱网恢复中很重要。假设一条预测链中间发生丢包:
IDR → P → P(丢失)→ P → P
后续帧可能继续依赖已经损坏的参考状态,导致花屏或卡住。客户端请求新的 IDR 后,Decoder 才能丢弃旧参考链并重新开始。
4. PTS 与 DTS 为什么要分开
如果没有帧重排序,解码顺序和显示顺序可能大致相同:
I → P → P → P
但 B 帧可以参考未来帧。假设用户应该看到:
Presentation Order:I → B1 → B2 → P
为了先得到 B1、B2 依赖的 P,压缩码流可能按下面的顺序送入 Decoder:
Decode Order:I → P → B1 → B2
于是需要两个时间概念:
- DTS(Decoding Timestamp):这个 Packet 何时进入 Decoder;
- PTS(Presentation Timestamp):解码后的 Frame 何时交给 Renderer 呈现。
一个简化示例是:
| 解码顺序 | Frame | DTS | PTS |
|---|---|---|---|
| 1 | I | 0 | 0 |
| 2 | P | 1 | 3 |
| 3 | B1 | 2 | 1 |
| 4 | B2 | 3 | 2 |
这里 P 的 DTS 小于 PTS:它要提前解码,先成为 B 帧的参考,最后才按时间轴显示。B 帧则在后面解码,却要在 P 之前呈现。
因此不能把文件中 Packet 的先后顺序直接当成用户看到画面的顺序。Demuxer 按码流顺序提供 Packet,Decoder 处理参考依赖与重排,Renderer 再按 PTS 和播放时钟输出。

5. 音频为什么也需要 PTS
音频没有视频的 I / P / B 参考关系,但它仍然需要被放到同一条播放时间轴上。
假设 AAC 每个 Frame 包含 1024 Samples,采样率为 48 kHz,那么一帧时长约为:
1024 / 48000 ≈ 21.3 ms
连续音频帧的 PTS 可能类似:
Audio Frame 1 PTS = 0 ms
Audio Frame 2 PTS ≈ 21.3 ms
Audio Frame 3 PTS ≈ 42.7 ms
播放器会选择一个 Master Clock,将音频 PTS 和视频 PTS 映射到同一播放时间。到达呈现时刻时播放音频或显示视频;如果某一路过早、过晚或漂移,还可能等待、丢帧或做时钟补偿。
PTS 的作用因此不局限于 B 帧,它是整个 A/V Sync 的基础。
6. Timestamp 必须结合 Time Base
PTS 和 DTS 通常不是“毫秒值”,而是某个时间基上的整数 Tick。
如果:
time_base = 1 / 90000
PTS = 90000
那么真实时间为:
Time = Timestamp × Time Base
= 90000 × 1/90000
= 1 秒
同一媒体中的不同 Stream 还可能使用不同 Time Base。因此比较音视频时间戳、执行 Seek 或跨组件传递时间时,不能只比较整数值,必须先换算到共同时间单位。
FFmpeg 中经常需要在不同 Time Base 之间做有理数缩放。直接使用浮点数或手写乘除容易引入精度、舍入和溢出问题;工程上应使用库提供的时间戳重缩放能力。
可以记住:
Timestamp 只表示第几个 Tick,Timestamp + Time Base 才表示真实时间。

7. Seek 不是移动一下文件指针
一次 seek(100s) 可能经历:
目标时间 100s
↓
转换到目标 Stream 的 Timestamp
↓
查询 Container Index
↓
找到目标之前可用的随机访问点,例如 98.5s IDR
↓
移动 Demux 读取位置
↓
清理旧 Packet、Frame、参考图像与重排缓存
↓
从随机访问点重新 Demux / Decode
↓
丢弃目标时间之前的解码结果
↓
从 100s 附近恢复 Render
Container Index 负责把时间映射到 Sample、Key Frame 和文件 Offset。如果索引不存在或不可用,Demuxer 可能需要扫描更多数据,Seek 就会变慢。
但索引只能告诉播放器“从哪里重新读”。真正恢复正确画面还依赖 Codec 的随机访问点、Decoder 状态和时间戳处理。
8. 为什么 Seek 后必须 Flush Decoder
Decoder 不是无状态函数。尤其存在帧间预测和 B 帧重排序时,它内部可能保存:
- 之前的参考图像;
- 尚未输出的重排 Frame;
- 已输入但仍在处理的 Packet;
- 与旧时间线相关的状态。
从 30 秒跳到 100 秒后,如果这些状态没有清掉,旧画面可能在新位置闪现,参考链可能混用,PTS 队列也可能错乱。
因此典型 Seek 流程会暂停供给、清空队列和 Decoder,再从新的随机访问点重新建立状态。多线程播放器还要防止 Seek 前后的异步任务交叉写入;只调用底层文件 Seek 而不重置整条流水线通常不够。
9. Fast Seek 与 Accurate Seek
两种 Seek 的差别主要在“随机访问点之后怎么处理”。
Fast Seek
目标是 10.3 秒,索引找到 10.0 秒随机访问点,播放器直接从 10.0 秒开始输出:
目标 10.3s
↓
跳到 10.0s Key Frame
↓
立即播放
优点是响应快,代价是呈现位置可能不精确。
Accurate Seek
播放器仍从 10.0 秒随机访问点开始解码,但不呈现目标之前的 Frame:
跳到 10.0s Random Access Point
↓
Decode 10.0 ... 10.3
↓
丢弃 PTS < 10.3s 的结果
↓
从目标附近开始 Render
Accurate Seek 不是直接解码目标帧,而是把建立依赖链所需的前置帧“在后台解出来”。目标距离上一个随机访问点越远,需要预解码的内容通常越多。
这正是 GOP 长度影响 Seek 体验的原因之一。

10. 从三个顺序排查播放问题
遇到视频顺序或跳转问题时,可以先分别画出三个顺序:
Bitstream / Demux Order
↓ DTS
Decode Order
↓ Reorder
Presentation Order
↓ PTS + Master Clock
Render

然后再检查随机访问过程:
- 目标秒数是否按正确 Time Base 转换;
- Container Index 是否定位到合适的随机访问点;
- Demux 与 Decoder 的旧队列是否清空;
- Decoder 是否从足够早的参考帧重新开始;
- Accurate Seek 是否丢弃了目标之前的 Frame;
- Renderer 是否以新时间线和 Master Clock 恢复。
这样可以避免把“画面顺序异常”全部归因于 Decoder,或把“Seek 不准”简单归因于 Container。
11. 把所有概念放回播放链路
编码侧:
Raw Video Frames
↓ Video Encoder
I / P / B Access Units
↓ 分配 PTS / DTS
Encoded Packets
↓ Mux
MP4 / TS / ...
播放与 Seek:
Container Index + Target Timestamp
↓
找到 Random Access Point
↓ Demux
Encoded Packets
↓ 按 DTS 输入 Decoder
Reference + Reorder
↓ 按 PTS 输出
Renderer + Master Clock
最终可以压缩成四句话:
- I / P / B 帧定义压缩参考关系;
- DTS定义解码时间和输入顺序;
- PTS定义呈现时间和播放顺序;
- Seek从时间轴定位随机访问点,并重新建立 Demux、Decode 与 Render 状态。
理解它们的关键不是背术语,而是始终问:当前讨论的是参考依赖、码流顺序、解码顺序、呈现顺序,还是随机访问后的状态恢复。
Discussion
留言与讨论
想法、补充和不同意见都欢迎。