返回学习

学习 2026

啊鸡入坑音视频之《视频帧与时间戳篇》

从 I、P、B 帧的参考依赖出发,解释 GOP、PTS/DTS、Time Base 与 Seek 如何共同决定压缩、解码顺序、呈现时机和随机访问。

音视频视频编码播放器
本文目录16
  1. 1. 为什么视频帧要互相参考
  2. 1.1 I Frame:只依赖自身信息
  3. 1.2 P Frame:参考过去
  4. 1.3 B Frame:还可以参考未来
  5. 2. GOP 怎样在码率与随机访问之间取舍
  6. 3. I Frame 与随机访问点不是完全一回事
  7. 4. PTS 与 DTS 为什么要分开
  8. 5. 音频为什么也需要 PTS
  9. 6. Timestamp 必须结合 Time Base
  10. 7. Seek 不是移动一下文件指针
  11. 8. 为什么 Seek 后必须 Flush Decoder
  12. 9. Fast Seek 与 Accurate Seek
  13. Fast Seek
  14. Accurate Seek
  15. 10. 从三个顺序排查播放问题
  16. 11. 把所有概念放回播放链路

音视频基础系列(三):上一篇是啊鸡入坑音视频之《流媒体封装篇》

拖动播放器进度条,看起来只是把时间从 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,即帧内编码帧。它使用当前画面内部的信息进行压缩,不需要其他视频帧就能恢复当前图像。

text
I

I 帧常被口语化地称为 Key Frame,因为它经常承担随机访问和恢复解码链的作用。它通常比预测帧更大,但能够提供一个新的解码起点。

不过,I 帧并不等于 JPEG,也不严格等于所有 Codec 语境中的随机访问点。I 只说明当前帧采用帧内编码;后续帧是否还可能引用更早的参考状态,是另一个问题。

1.2 P Frame:参考过去

P Frame 是 Predicted Frame。它通常参考已经解码的过去帧,只记录运动与残差,而不是重复保存整张画面。

text
I0
 ↓
P1 = 参考 I0 + 当前差异
 ↓
P2 = 参考已有帧 + 当前差异

如果必要的参考帧没有正确解码,后续依赖它的 P 帧也可能出现花屏、冻结或无法输出。

1.3 B Frame:还可以参考未来

B Frame 是 Bi-directionally Predicted Frame。它可以利用时间轴上过去和未来的参考帧来预测当前画面:

text
显示顺序:I  B1  B2  P
              ↖   ↗
            可参考 I 与 P

更多参考方向通常能提高压缩效率,却引入了一个直接后果:Decoder 必须先取得未来的 P 帧,才能解出排在它之前显示的 B 帧。因此,解码顺序和显示顺序可能分离。

2. GOP 怎样在码率与随机访问之间取舍

I、P、B 帧通常按结构组成 GOP(Group Of Pictures,图像组):

text
I B B P B B P B B P

或更简单:

text
I P P P P P

工程上常用 GOP Length 或 Key Frame Interval 描述随机访问点之间的距离。假设视频为 30 fps,每 60 帧产生一次关键帧,那么关键帧间隔大约是:

text
60 / 30 = 2 秒

GOP 长短形成明确权衡:

GOP优点代价
较长I 帧比例低,通常压缩效率更高Seek、首屏和错误恢复可能更慢
较短更容易随机接入,Seek 与恢复通常更快I 帧更多,平均码率可能升高

这也是录像、点播、直播、RTC 和 IPC 对 GOP 配置要求不同的原因。它不是一个单纯追求“越大越省”或“越小越快”的参数,而是在带宽、延迟、随机访问和恢复时间之间做取舍。

I、P、B 帧依赖与长短 GOP 的工程权衡

3. I Frame 与随机访问点不是完全一回事

在 H.264 中,I Frame 表示当前图像使用帧内编码,但不必然保证后续图像不再引用它之前的画面。

IDR(Instantaneous Decoder Refresh)提供更强的边界语义。遇到 IDR 后,Decoder 可以清理之前的参考图像;后续图像不会越过这个边界引用更早的画面。因此,IDR 更适合作为独立解码的随机访问点。

H.265 中还存在 CRA 等其他随机访问类型,它们对“从哪里开始解码”和“起点之后哪些图像可以安全输出”有更细的规则。

所以日常口头上的“请求一个 I 帧”,在实时视频工程里通常真正想表达的是:

请求一个能让 Decoder 重新建立可靠参考状态的随机访问帧,例如 IDR。

这个区别在 IPC 弱网恢复中很重要。假设一条预测链中间发生丢包:

text
IDR → P → P(丢失)→ P → P

后续帧可能继续依赖已经损坏的参考状态,导致花屏或卡住。客户端请求新的 IDR 后,Decoder 才能丢弃旧参考链并重新开始。

4. PTS 与 DTS 为什么要分开

如果没有帧重排序,解码顺序和显示顺序可能大致相同:

text
I → P → P → P

但 B 帧可以参考未来帧。假设用户应该看到:

text
Presentation Order:I → B1 → B2 → P

为了先得到 B1、B2 依赖的 P,压缩码流可能按下面的顺序送入 Decoder:

text
Decode Order:I → P → B1 → B2

于是需要两个时间概念:

  • DTS(Decoding Timestamp):这个 Packet 何时进入 Decoder;
  • PTS(Presentation Timestamp):解码后的 Frame 何时交给 Renderer 呈现。

一个简化示例是:

解码顺序FrameDTSPTS
1I00
2P13
3B121
4B232

这里 P 的 DTS 小于 PTS:它要提前解码,先成为 B 帧的参考,最后才按时间轴显示。B 帧则在后面解码,却要在 P 之前呈现。

因此不能把文件中 Packet 的先后顺序直接当成用户看到画面的顺序。Demuxer 按码流顺序提供 Packet,Decoder 处理参考依赖与重排,Renderer 再按 PTS 和播放时钟输出。

同一组视频帧的 PTS、DTS 与重排关系

5. 音频为什么也需要 PTS

音频没有视频的 I / P / B 参考关系,但它仍然需要被放到同一条播放时间轴上。

假设 AAC 每个 Frame 包含 1024 Samples,采样率为 48 kHz,那么一帧时长约为:

text
1024 / 48000 ≈ 21.3 ms

连续音频帧的 PTS 可能类似:

text
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。

如果:

text
time_base = 1 / 90000
PTS = 90000

那么真实时间为:

text
Time = Timestamp × Time Base
     = 90000 × 1/90000
     = 1 秒

同一媒体中的不同 Stream 还可能使用不同 Time Base。因此比较音视频时间戳、执行 Seek 或跨组件传递时间时,不能只比较整数值,必须先换算到共同时间单位。

FFmpeg 中经常需要在不同 Time Base 之间做有理数缩放。直接使用浮点数或手写乘除容易引入精度、舍入和溢出问题;工程上应使用库提供的时间戳重缩放能力。

可以记住:

Timestamp 只表示第几个 Tick,Timestamp + Time Base 才表示真实时间。

Timestamp、Time Base 与真实时间的换算关系

7. Seek 不是移动一下文件指针

一次 seek(100s) 可能经历:

text
目标时间 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 秒开始输出:

text
目标 10.3s
  ↓
跳到 10.0s Key Frame
  ↓
立即播放

优点是响应快,代价是呈现位置可能不精确。

Accurate Seek

播放器仍从 10.0 秒随机访问点开始解码,但不呈现目标之前的 Frame:

text
跳到 10.0s Random Access Point
  ↓
Decode 10.0 ... 10.3
  ↓
丢弃 PTS < 10.3s 的结果
  ↓
从目标附近开始 Render

Accurate Seek 不是直接解码目标帧,而是把建立依赖链所需的前置帧“在后台解出来”。目标距离上一个随机访问点越远,需要预解码的内容通常越多。

这正是 GOP 长度影响 Seek 体验的原因之一。

Accurate Seek 从索引定位、Flush 到目标时刻恢复呈现的流程

10. 从三个顺序排查播放问题

遇到视频顺序或跳转问题时,可以先分别画出三个顺序:

text
Bitstream / Demux Order
        ↓ DTS
Decode Order
        ↓ Reorder
Presentation Order
        ↓ PTS + Master Clock
Render

从码流顺序到渲染的播放问题排错路径

然后再检查随机访问过程:

  1. 目标秒数是否按正确 Time Base 转换;
  2. Container Index 是否定位到合适的随机访问点;
  3. Demux 与 Decoder 的旧队列是否清空;
  4. Decoder 是否从足够早的参考帧重新开始;
  5. Accurate Seek 是否丢弃了目标之前的 Frame;
  6. Renderer 是否以新时间线和 Master Clock 恢复。

这样可以避免把“画面顺序异常”全部归因于 Decoder,或把“Seek 不准”简单归因于 Container。

11. 把所有概念放回播放链路

编码侧:

text
Raw Video Frames
      ↓ Video Encoder
I / P / B Access Units
      ↓ 分配 PTS / DTS
Encoded Packets
      ↓ Mux
MP4 / TS / ...

播放与 Seek:

text
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 状态。

理解它们的关键不是背术语,而是始终问:当前讨论的是参考依赖、码流顺序、解码顺序、呈现顺序,还是随机访问后的状态恢复。

返回首页
上一篇啊鸡入坑音视频之《流媒体封装篇》下一篇JavaScript 异步到底在异步什么?从调用栈到事件循环

Discussion

留言与讨论

想法、补充和不同意见都欢迎。