PS 的解析只有一个动作:反复找 00 00 01,看下一个字节是什么。 难点在长度:pack 头按标志位算,PES 读长度字段。
5.6.1 认了结构,这一节把它走一遍。 5.7 回到 HLS,看 TS 与 m3u8 切片怎么配合。
一、解析只有三步
① 找 00 00 01:所有 PS 结构的共同起点。
② 看下一个字节:决定这一段是什么。
③ 按类型取长度:pack 头是固定长度,PES 要读长度字段。
几个关键字节值: BA 是 pack 头、BB 是系统头、BC 是节目流映射、BD 是私有流、 E0 到 EF 是视频、C0 到 DF 是音频。
这套「起始码 + 类型 + 长度」的设计叫「自描述」。 好处是不需要外部索引,从任意位置扫下去就能解; 坏处是每次都要扫,不能像 TS 那样靠固定偏移直接跳。
二、造一段 PS 再解一遍
实测自造一段 447 字节的 PS,里面有四段: pack 头 14 字节、视频 PES 209 字节、音频 PES 129 字节、视频 PES 89 字节。 遍历指针从 0 一路走到 447,正好走完,一段不多一段不少。 遍历指针停的位置等于总长,这是「解析正确」最硬的证据。
如果指针走到的位置比总长小,说明中间有没认出来的段; 如果比总长大,说明某个长度字段读错了。 这个「指针必须刚好到末尾」的技巧,在做任何二进制解析时都好用。
注意 pack 头这里是 14 字节,不是 5.6.1 里的 20 字节。 差别在标志位:MPEG-1 的 pack 头只有 12 字节,MPEG-2 的是 14 或 20。
三、pack 头的长度怎么定
注意 pack 头长度是「按标志位算的」,不是固定 14 字节。 本例的第 5 个字节是 0x44,最高两位 01 表示「标准 MPEG-2 pack」,字节数就是 14。 如果是 MPEG-1 的 pack,会短很多。 所以解析 pack 头必须先看那个标志字节,不能写死。
同理 PES 头也不能写死: 它的长度取决于「有没有 PTS」「有没有 DTS」「有没有扩展」。 这些都是标志位,得一位一位判。
这就是二进制格式解析的通用规律: 先读「说明怎么读」的那几个位,再按说明去读正文。 TS 的适配字段、RTP 的扩展头、SCTP 的 chunk 标志,全是同一个套路。
四、动手 Demo:PS 解析——按起始码一段段走
自造一段 447 字节的 PS(一个 pack 头加三个 PES),再按起始码遍历。 判据是「遍历指针必须刚好走到末尾」,差一个字节都算失败。
import struct
chunks = []
# pack 头 14 字节:00 00 01 BA + 0x44 + 9 字节 + 填充 0
ph = b"\x00\x00\x01\xBA\x44" + bytes(9)
chunks.append(ph)
def pes(sid, payload):
h = b"\x00\x00\x01" + bytes([sid])
h += struct.pack("!H", 0x8000 | (8 + len(payload)))
h += b"\x80\x00\x00"
return h + payload
chunks.append(pes(0xE0, bytes(200))) # 视频
chunks.append(pes(0xC0, bytes(120))) # 音频
chunks.append(pes(0xE0, bytes(80))) # 视频
stream = b"".join(chunks)
print("自造 PS %d 字节" % len(stream))
i, n = 0, 0
while i + 4 <= len(stream):
if stream[i:i + 3] != b"\x00\x00\x01":
i += 1
continue
sid = stream[i + 3]
n += 1
if sid == 0xBA:
ln = 14 if (stream[i + 4] >> 6) == 1 else 12
kind = "pack 头"
elif 0xE0 <= sid <= 0xEF:
ln = 6 + struct.unpack("!H", stream[i + 4:i + 6])[0]
kind = "视频 PES"
elif 0xC0 <= sid <= 0xDF:
ln = 6 + struct.unpack("!H", stream[i + 4:i + 6])[0]
kind = "音频 PES"
else:
ln, kind = 6, "其他"
print("%d %-10s %d 字节" % (n, kind, ln))
i += ln
print("共 %d 段,指针走到 %d(总长 %d)" % (n, i, len(stream)))
实跑输出:
--------------------------------------------------------------------
PS 解析——按起始码一段段走
--------------------------------------------------------------------
**① 解析只有三步**
**① 找 `00 00 01`**:**所有 PS 结构的共同起点。**
**② 看下一个字节**:**决定这一段是什么**
**③ 按类型取长度**:**pack 头是固定长度,PES 要读长度字段。**
**几个关键字节值:**
**BA 是 pack 头、BB 是系统头、BC 是节目流映射、BD 是私有流、E0 到 EF 是视频、C0 到 DF 是音频。**
**② 造一段 PS 再解一遍**
自造一段 PS,共 447 字节
段号 类型 字节
------------------------------
1 pack 头 14
2 视频 PES 209
3 音频 PES 129
4 视频 PES 89
一共找到 4 段,遍历指针走到 447(总长 447)
**注意 pack 头长度是「按标志位算的」,不是固定 14 字节。**
**本例的第 5 个字节是 0x44,最高两位 01 表示「标准 MPEG-2 pack」,字节数就是 14。**
**如果是 MPEG-1 的 pack,会短很多。**
**所以解析 pack 头必须先看那个标志字节,不能写死。**
447 字节 = 14 + 209 + 129 + 89,四段加起来正好是全部。 PES 的长度字段是「头之后还有多少」,所以总长要加 6——3 起始码 + 1 流 ID + 2 长度。 指针停在 447 与总长相等,这一条就是「解析没错」的判据。
30 秒实验
- 把 pack 头里的 0x44 改成 0x24,看它被当成几字节——12 字节,最高两位不是 01 就走 MPEG-1 分支。
- 在末尾多加 5 个 0xFF,看指针停在哪——停在 447,多出来的填充被跳过,说明容错是对的。
- 把某段 PES 的长度字段改大 10,看指针走到哪——超过总长,这就是「长度读错」的典型现象。
常见误区
- 「pack 头固定 14 字节」——要按标志位判,12 与 20 都可能。
- 「PES 长度字段含头」——不含,它只算头之后的部分。
- 「解析到末尾就算成功」——要「刚好」到末尾,多与少都是错。
自测三问(点开对答案)
1. PS 解析的三步是什么? —— **找 `00 00 01`、看下一字节定类型、按类型取长度。** 2. 怎么判断一次解析是对的? —— **遍历指针刚好走到末尾**,多一个字节少一个字节都算失败。 3. pack 头长度为什么不能写死? —— **要看第 5 字节的最高两位**:01 是 MPEG-2 的 14 字节,否则可能是 12 或 20。- ☐ (本节完,下一节 5.7 TS 格式与 m3u8 切片)