🎬 第五章 HLS流媒体协议
📄 一句话概括

ES 是裸码流,PES 给裸码流加时间戳,PS 与 TS 是两种装 PES 的方式。 一层一层看,封装关系就清楚了。

📝 本节和前面几节的关系

5.3 讲完了清单,这一节开始讲「切片里到底装了什么」。 5.4.2 讲这四层怎么一步步变出来,5.4.3 做个小结。

一、四层各是什么

ES(Elementary Stream):编码器输出的裸码流。 H.264 的 ES 就是一串 NAL 单元,AAC 的 ES 就是一串 ADTS 帧。 它只有内容,没有任何时间信息。 PES(Packetized ES):给 ES 加上起始码与时间戳(PTS/DTS)。 加时间戳是它唯一的核心职责。 TS(Transport Stream):把 PES 切成 188 字节定长包。 适合传输与丢包恢复。 PS(Program Stream):把 PES 串成变长包。 适合存储,不允许丢。

一句话:ES 是内容,PES 给内容打时间戳,TS 与 PS 是两种把 PES 装起来的方式。 TS 面向传输,PS 面向存储——所以直播与数字电视用 TS,DVD 与本地文件用 PS。

二、真构造一个 PES 头

PES 头的结构是固定的:起始码 00 00 01 三字节, 然后一个流 ID,再两字节长度,再一串可选字段。

实测构造出来的头: 起始码是 00 00 01,流 ID 是 0xE0(视频用 0xE0 到 0xEF)。 PES 总长 119 字节,其中载荷 104 字节,头开销 15 字节。

为什么头开销是 15 而不是 9? 因为标准头只有 9 字节,剩下 6 字节是「可选头」: 2 字节标志加 4 字节 PTS。 要带 DTS 的话,这 6 字节会变成 10 字节,头开销就到 19。

注意「流 ID」这个字段: 0xE0 到 0xEF 是视频、0xC0 到 0xDF 是音频, 0xBD 是私有流(常见于 AC-3 与字幕)。 解析时靠它区分「这一段是画面还是声音」。

三、四层的开销算下来

ES 层额外 0 字节——裸码流本身。 PES 层额外 14 字节——起始码 3 + 头 9 + 扩展 2。 TS 层额外 4 字节——每 188 字节加 4 字节包头。

按 188 字节一包算,TS 的头开销约 2.1%。 一个 20 KB 的关键帧加上 PES 头,膨胀约 0.07%。

结论:封装开销很小,绝对到不了「影响画质」的程度。 真正的带宽大头永远是编码后的码流本身。 所以别为了省几个头的字节去做奇怪的优化,方向错了。

值得留意的是「PES 头只加一次」这件事: 一个 20 KB 的关键帧会被切成一百多个 TS 包, 但它只有一个 PES 头。 这就是为什么大帧的封装开销反而更低。

四、动手 Demo:ES/PES/PS/TS——四层封装的关系

先把四层的职责摆清楚,再真构造一个 PES 头,最后把开销算一遍。 算完你会发现:封装开销小到可以忽略。

import struct

payload = b"\x00" * 104

# PES 头:起始码 + 流 ID 0xE0 + 长度 + 标准头 + PTS
head = b"\x00\x00\x01" + b"\xE0"
head += struct.pack("!H", 0x8000 | (3 + 9 + 5 + len(payload)))
head += b"\x84\x80\x05"          # 标志:带 PTS
head += b"\x21\x00\x01\x00\x01"  # 5 字节 PTS
pes = head + payload

print("起始码 %s,流 ID 0x%02X" % (pes[:3].hex(), pes[3]))
print("PES 总长 %d,载荷 %d" % (len(pes), len(payload)))
print("头开销 %d 字节" % (len(pes) - len(payload)))

print()
for name, extra, note in (("ES", 0, "裸码流"),
                           ("PES", 14, "起始码 3 + 头 9 + 扩展 2"),
                           ("TS", 4, "每 188 字节 4 字节包头")):
    print("%-4s 额外 %2d 字节  %s" % (name, extra, note))

print()
print("TS 包头占比 %.1f%%" % (4 * 100.0 / 188))
big = 20 * 1024
print("20 KB 关键帧加 PES 头,膨胀 %.2f%%" % (14 * 100.0 / big))

实跑输出:

--------------------------------------------------------------------
ES/PES/PS/TS——四层封装的关系
--------------------------------------------------------------------
**① 四层各是什么**

  层      全称                  装什么
  -----------------------------------------------------------------
  ES      Elementary Stream     编码后的裸码流,如 H.264 的 NAL
  PES     Packetized ES         给 ES 加时间戳(PTS/DTS)
  TS      Transport Stream      定长 188 包,适合传输与丢包恢复
  PS      Program Stream        变长包,适合存储,不允许丢

  **一句话:ES 是内容,PES 给内容打时间戳,TS/PS 是两种把 PES 装起来的方式。**
  **TS 面向传输,PS 面向存储**——**所以直播与数字电视用 TS,DVD 与本地文件用 PS。**

**② 真构造一个 PES 头**

  PES 起始码 00 00 01,流 ID 0xE0(视频常用 0xE0)
  PES 总长 119 字节,其中载荷 104 字节
  头开销 = 15 字节

**③ 四层的开销算下来**

  层      额外字节     说明
  -------------------------------------------------
  ES      0            裸码流本身
  PES     14           起始码 3 + 头 9 + 扩展 2
  TS      4            每 188 字节加 4 字节包头

  按 188 字节一包算,TS 的头开销约 2.1%
  一个 20 KB 的关键帧加上 PES 头,膨胀约 0.07%

  **结论:封装开销很小,绝对到不了「影响画质」的程度。**
  **真正的带宽大头永远是编码后的码流本身。**
  **所以别为了省几个头的字节去做奇怪的优化,方向错了。**

0x8000 | 这一句是给长度字段置最高位,表示「这是 MPEG-2 的 PES」。 PES 头一共 15 字节——3 起始码 + 1 流 ID + 2 长度 + 3 标志 + 5 PTS + 1 长度字节。 20 KB 的帧膨胀 0.07%,说明头部开销在真实码率面前完全可以忽略。

30 秒实验

  1. 把 PTS 那段去掉,看头开销——降到 10 字节,但没有时间戳就没法同步音视频。
  2. 把载荷从 104 字节改成 20000 字节,看膨胀率——降到 0.07%,帧越大头越不值钱。
  3. 把流 ID 改成 0xC0,再写一行判断——能正确判成音频,这就是解析器区分音视频的依据。

常见误区

  1. 「ES 里带时间戳」——不带,时间戳是 PES 才加的。
  2. 「TS 与 PS 是两种编码」——不是编码,是两种封装方式。
  3. 「封装开销会明显影响画质」——不到 3%,可以忽略。
自测三问(点开对答案) 1. PES 层加的到底是什么? —— **起始码与时间戳**(PTS/DTS),这是它唯一的核心职责。 2. TS 与 PS 的根本分工是什么? —— **TS 面向传输(抗丢包),PS 面向存储(不允许丢)。** 3. 为什么大帧的封装开销更低? —— **一个帧只有一个 PES 头**,帧越大头摊得越薄。

相关笔记