GOP 管「多久重来一次」,DTS 管「什么时候解」,PTS 管「什么时候显示」。 有 B 帧时,解码顺序和显示顺序就分家了——这是本节的全部内容。
GOP 在 04.01.03 讲过效果,这里讲它和 PTS/DTS 的关系。 B 帧的参考关系在 26 课,这里只看时间戳怎么排。
一、三个词,别再混着用
| 词 | 管什么 | 谁在用 |
|---|---|---|
| GOP | 两个 I 帧之间有多少帧 | 编码器(决定 seek 粒度) |
| DTS | 什么时候该送去解码 | 解码器(按这个顺序吃) |
| PTS | 什么时候该显示出来 | 播放器(按这个顺序摆) |
没有 B 帧时,DTS 和 PTS 完全一样,所以只看过 IPPP 片子的人会觉得「这俩不是一回事吗」。 一旦有 B 帧,B 帧要参考它后面那一帧,只能「先解后面的、后显示前面的」,两个顺序就分家了。
二、实测:包上的 DTS 是递增的,PTS 是乱的
| 读包序 | dts | pts | 关键帧 |
|---|---|---|---|
| 1 | -1024 | 0 | 是 |
| 2 | -512 | 1536 | 否 |
| 3 | 0 | 512 | 否 |
| 4 | 512 | 1024 | 否 |
dts 一列严格递增,pts 一列是乱的,这就是两个顺序分离的样子。 第 2 个包 pts 是 1536,第 3 个包才 512——中间那一帧被插到后面去了。 原因就在 B 帧:512 那一帧要参考 1536 那一帧,编码器只能先发后者。
第一行的 dts 是负数,这不是笔误:编码器要攒够几帧才敢出第一个包, 所以 dts 起点比 pts 落后。 很多容器(MP4、TS)不收负 dts,封装时会给所有时间戳加一个固定偏移, 把整条时间轴搬到 0 以上——这就是「dts 也从 0 开始但差值不变」的来历。
三、实测:帧上的 PTS 反而是顺的
同一批数据交给解码器,吐出来的帧 pts 从头到尾递增。 也就是说,重排这件事解码器自己做了,你拿到的帧已经是显示顺序。
| 你的代码在做什么 | 要不要自己管顺序 |
|---|---|
调 decode() 拿帧 |
不用,已经排好了 |
| 直接搬包(转封装) | 要,得看 dts |
这是一条很省心的结论:写播放器的人永远不用管 B 帧重排,只有转封装才需要自己看 dts。
而且现在连「这帧原来第几个到」都查不到了:FFmpeg 8 把 frame.pkt_dts 删了,读出来是空。
要解码顺序,只能回包上看 packet.dts——这也印证了 dts 归属:它是包的属性,不是帧的属性。
四、GOP 和这两个数什么关系
GOP 决定「I 帧多久出现一次」,而 I 帧是唯一能独立解码的帧。 所以 GOP 也决定了 dts 的「重启点」:每个 GOP 开头,dts 重新从一个基准往上走。
| GOP | I 帧数(30 帧片子) | seek 粒度 |
|---|---|---|
| 10 | 3 | 最多解 10 帧 |
| 30 | 1 | 最多解 30 帧 |
实测 30 帧、GOP 10 编出来正好 3 个 I 帧,和 30 ÷ 10 对得上。 想 seek 快就调小 GOP,代价是文件变大(I 帧比 P 帧大好几倍)。
最后一句要记牢:pts 和 dts 都只是「数」,单独拿出来没有时间含义, 必须配时间基——这就是下一节。
五、动手 Demo:GOP、PTS、DTS——为什么解码顺序和显示顺序不一样
Demo 用 GOP 10、B 帧 2 编 30 帧,然后分别从「包」和「帧」两个角度看顺序。 包上是乱的,帧上是顺的——这个对比就是本节的全部结论。
st.gop_size = 10
st.max_b_frames = 2
# 看包
for pk in c.demux(s):
print(pk.dts, pk.pts, pk.is_keyframe)
# 看帧
for fr in c.decode(s):
print(fr.pts, int(fr.pict_type), getattr(fr, "pkt_dts", None))
实跑输出:
--------------------------------------------------------------------
GOP、PTS、DTS——为什么解码顺序和显示顺序不一样
--------------------------------------------------------------------
**先把三个词分清,这三个词十个人有八个混着用:**
词 管什么 谁在用
--------------------------------------------------------------
GOP 两个 I 帧之间有多少帧 编码器(决定 seek 粒度)
DTS 什么时候该送去解码 解码器(按这个顺序吃)
PTS 什么时候该显示出来 播放器(按这个顺序摆)
**没有 B 帧时,DTS 和 PTS 完全一样**,所以只看过 IPPP 片子的人会觉得「这俩不是一回事吗」。
**一旦有 B 帧,B 帧要参考它后面那一帧,只能「先解后面的、后显示前面的」**,两个顺序就分家了。
**第一步看包:GOP 设 10、B 帧设 2,编 30 帧,然后按文件里的顺序读包。**
读包序 dts pts 关键帧 pts − dts
------------------------------------------------------
1 -1024 0 是 1024
2 -512 1536 否 2048
3 0 512 否 512
4 512 1024 否 512
5 1024 3072 否 2048
6 1536 2048 否 512
7 2048 2560 否 512
8 2560 4608 否 2048
9 3072 3584 否 512
10 3584 4096 否 512
**这张表的 dts 一列是递增的,pts 一列是乱的**,这就是「解码顺序」和「显示顺序」分离的样子。
**第 2 个包的 pts 是 1536,第 3 个包才 512**——中间那一帧被插到后面去了。
**原因就在 B 帧**:512 那一帧要参考 1536 那一帧,编码器只能先发后者。
**第一行的 dts 是 -1024,负的**。
这不是笔误:编码器要攒够 2 帧才敢出第一个包,所以 dts 的起点比 pts 落后两帧。
**很多容器(MP4、TS)不收负 dts**,封装时会给所有时间戳加一个固定偏移,
把整条时间轴搬到 0 以上——这就是「dts 和 pts 都从 0 开始但差值还是原样」的来历。
**第二步看帧:同一批数据交给解码器,一帧一帧吐出来。**
吐出序 pts 类型 注释
----------------------------------------------------
1 0 I I 帧,独立解
2 512 B B 帧,参考前后
3 1024 B B 帧,参考前后
4 1536 P P 帧,参考前面
5 2048 B B 帧,参考前后
6 2560 B B 帧,参考前后
7 3072 P P 帧,参考前面
8 3584 B B 帧,参考前后
9 4096 B B 帧,参考前后
10 4608 P P 帧,参考前面
**这一列 pts 从头到尾递增:是。**
**也就是说,解码器把重排这件事自己做了**,吐给你的帧已经是显示顺序。
**这是一条很省心的结论**:调 `decode()` 的人永远不用管 B 帧重排,
**只有「直接搬包」的转封装才需要自己看 dts。**
**想看「这帧原来第几个到」已经看不到了**:FFmpeg 8 把 `frame.pkt_dts` 删了,
这次读出来是 `None`。
**要解码顺序,只能回到包上去看 dts**,这也印证了 dts 的归属——它是包的属性,不是帧的属性。
统计项 值
--------------------------------------------------
包总数 30
dts ≠ pts 的包 30 个
I 帧数 3 个
首个包的 dts -1024
时间基 1/15360
**I 帧 3 个,正好是 30 ÷ 10 = 3**——GOP 就是这么算出来的。
**想 seek 快就调小 GOP,代价是文件变大**(I 帧比 P 帧大好几倍)。
**最后一句要记牢:pts 和 dts 都只是「数」**,单独拿出来没有时间含义,必须配时间基。
这就是下一节。
getattr(fr, "pkt_dts", None) 这一行是故意写的:它在 FFmpeg 8 上取到 None,
证明这个字段真没了,不是我没写对。
包表里 dts 一列有负数,
帧表里 pts 一列从 0 开始递增——两张表放在一起,才看得出「谁在重排」。
30 秒实验
- 把
max_b_frames设成 0,重跑,看 dts 和 pts 是不是变成一样了。 - 把 GOP 改成 5,看 I 帧数变成几个。
- 把包表按 pts 排序打印,看和帧表的顺序是不是一致。
常见误区
- 以为 pts 和 dts 是同一个东西。 没有 B 帧时相同,有 B 帧就分家。
- 以为解码出来的帧是乱序的、要自己重排。 解码器已经排好了,只有搬包才要自己看顺序。
- 以为负 dts 是 bug。 编码器攒帧需要延迟,封装时加偏移搬正就行。
自测三问(点开对答案)
1. 为什么有 B 帧时 dts 会小于 pts? —— B 帧要参考后面的帧,**编码器只能先发后面那帧(dts 小),但它显示得晚(pts 大)**。 2. 解码出来的帧需要自己重排吗? —— 不需要,**解码器已经按显示顺序吐出来了**。只有转封装搬包时才要自己看 dts。 3. 为什么 seek 要找 IDR 而不是随便一个 I 帧? —— 只有 IDR 会清空参考帧列表,**从它开始能独立重放**;普通 I 帧可能还引用着之前的帧。下一节讲时间基:pts 是刻度数,离开时间基就没有含义。
- ☐ (本节完,下一节 04.06.02 FFmpeg中的时间基与时间戳)