📹 第四章 H.264/H.265视频编码并存储
📄 一句话概括

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 秒实验

  1. 把 max_b_frames 设成 0,重跑,看 dts 和 pts 是不是变成一样了。
  2. 把 GOP 改成 5,看 I 帧数变成几个。
  3. 把包表按 pts 排序打印,看和帧表的顺序是不是一致。

常见误区

  1. 以为 pts 和 dts 是同一个东西。 没有 B 帧时相同,有 B 帧就分家。
  2. 以为解码出来的帧是乱序的、要自己重排。 解码器已经排好了,只有搬包才要自己看顺序。
  3. 以为负 dts 是 bug。 编码器攒帧需要延迟,封装时加偏移搬正就行。
自测三问(点开对答案) 1. 为什么有 B 帧时 dts 会小于 pts? —— B 帧要参考后面的帧,**编码器只能先发后面那帧(dts 小),但它显示得晚(pts 大)**。 2. 解码出来的帧需要自己重排吗? —— 不需要,**解码器已经按显示顺序吐出来了**。只有转封装搬包时才要自己看 dts。 3. 为什么 seek 要找 IDR 而不是随便一个 I 帧? —— 只有 IDR 会清空参考帧列表,**从它开始能独立重放**;普通 I 帧可能还引用着之前的帧。

下一节讲时间基:pts 是刻度数,离开时间基就没有含义。

相关笔记