国标的媒体流是「PS 装 ES」:外面一层打包流,里面才是裸的 H.264。 转封装就是把这层壳拆掉、换个壳——壳的固定开销只有几十字节,但小帧会被它吃掉一大截。
上一节讲承载,这一节扒到最里头讲封装格式。这是国标媒体链路上最后一个「要拆的东西」。 拆对了,接什么播放器都行;拆错了,一路花屏。
一、PS 里面装了什么
PS 的结构是「段」连「段」,每段都以 00 00 01 加一个字节的类型开头。
四种段要认准:BA 是打包头、BB 是节目流映射、E0 是视频 PES、C0 是音频 PES。
打包头每个「打包」出现一次,它记时间基准(SCR)和复用速率;
节目流映射一般只有开头一个,它声明「0xE0 是 H.264、0xC0 是 G.711」这类对应关系。
PES 才是真正装数据的地方:它的头里有 PTS,也就是这一帧什么时候该显示。
PTS 是整条链路上最容易被弄坏的东西:
重新封装时不带 PTS,播放器就只能按到达顺序播,一遇到抖动就卡。
二、拆包与重封装
拆 PS 有两种做法,可靠程度差很多:
一种是「搜 00 00 01」的粗略扫描,另一种是「按 PES 长度字段跳着走」的精确解析。
粗略扫描的问题是这三个字节在压缩数据里也可能出现,一旦命中就会把数据当段头,后面的全乱。
它只适合做「先看看有没有 PS」这种试探。
精确解析的关键是长度字段:PES 头里有两个字节写着「后面还有多少字节」,
读完这么多就直接跳到下一段,根本不看内容。
重封装时最常丢的三样东西:PTS、关键帧标记、私有数据。
前两样丢了会卡会花,第三样丢了报警和智能分析结果就没了。
三、为什么要对齐 I 帧
转封装不能随便从中间切: 从 P 帧开始的一段裸流,播放器解不出来—— P 帧只记「相对上一帧的变化」,没有参照就是一堆噪声。 所以重封装必须「等到下一个 I 帧」再开始输出。 这就带来一个延迟:I 帧间隔两秒,最坏要等两秒。 这也是「为什么刚点开画面是黑的」的答案:不是网络慢,是在等 I 帧。 平台侧的优化手段是「缓存最近一个 I 帧」,新用户进来直接从这个 I 帧开始播,不用等。 代价是内存:一个 I 帧四十千字节,一千路就是四十兆。 这笔内存换来的是「秒开」,通常很值。
四、动手 Demo:把一段 PS 流拆开,数清哪些字节是头
用代码合成一段 PS:十个打包、一个映射、十帧视频、十帧音频。 再用两种方式解析回来,数段数、算开销。
def pack_header():
# 00 00 01 BA + 01 + SCR(6) + mux_rate(3) + stuffing_len(1)
return b"\x00\x00\x01\xBA" + b"\x01" + b"\x00" * 10
def pes(sid, data, pts):
head = b"\x80\x80\x05" + bytes([...5 字节的 PTS...])
payload = head + data
return (b"\x00\x00\x01" + bytes([sid])
+ struct.pack(">H", len(payload)) + payload)
# 解析:找 00 00 01 xx
name = {0xBA: "打包头", 0xBB: "节目流映射",
0xE0: "视频 PES", 0xC0: "音频 PES"}.get(sc)
实跑输出:
--------------------------------------------------------------------
把一段 PS 流拆开,数清哪些字节是头
--------------------------------------------------------------------
**国标的媒体流是「PS 装 ES」**:**外面一层打包流,里面才是 H.264 的裸数据。**
**要把它接到只认裸流的播放器上,就得拆包。**
**这一节真的造一段 PS,再用解析器拆回来。**
合成了 23693 字节的 PS 流
段类型 出现次数
--------------------------------
**打包头** **10 个**
**节目流映射** **1 个**
**视频 PES** **10 个**
**音频 PES** **10 个**
打包头 15 字节、PES 头 14 字节、映射段 18 字节(只出现一次)
一帧视频 2000 字节 → 头占 1.45%
一帧音频 320 字节 → 头占 9.06%
整段真实负载 23245 字节,总长 23693 字节,封装占 1.89%
**前两行是这一节的重点**:
**同一套封装,装在 2000 字节的视频上只占 1.45%,装在 320 字节的音频上就要占 9.06%。**
**所以「封装开销」从来不是一个固定百分比**——**它取决于负载多大。**
**拆包时踩过的坑值得说一句**:**`00 00 01` 这三个字节在压缩数据里也可能出现**。
**真正的解析器不能只靠搜这三个字节**,**要按 PES 长度字段跳着走**;
**搜字节只能用来做「粗略扫描」。**
**这个 Demo 就是粗略扫描**:**它数对了段数,是因为合成数据里正好没有假的起始码。**
四个段数全对上了:十个打包头、一个映射、十帧视频、十帧音频。
映射只有一个,是因为它只声明一次就够。
头开销那三行是重点:打包头 15 字节、PES 头 14 字节。
一帧视频 2000 字节摊下来是 1.45%,一帧音频 320 字节就是 9.06%。
同一个 29 字节的固定开销,占比差了六倍——因为分母不一样。
这就是「封装开销从来不是固定百分比」的意思,
任何说「PS 开销是百分之几」的说法,不带上负载大小都是错的。
最后那段说明这个 Demo 的局限:它用的是「搜起始码」的粗略扫描,
能数对是因为合成数据里没有假的起始码。
真实码流里 00 00 01 出现得不少,必须按长度字段跳。
30 秒实验
- 算一下:29 字节的固定开销,装在 320 字节的包上占多少?(9.06%)
- 想一下:为什么重封装必须从 I 帧开始?
- 想一下:缓存 I 帧换「秒开」,代价是什么?(内存)
常见误区
- 以为「PS 是一种压缩格式」。 它不压缩,只是把多条流打成一段。
- 以为「转封装不影响延迟」。 等 I 帧会带来一个 I 帧间隔的延迟。
- 以为「搜
00 00 01就能拆包」。 压缩数据里会出现这三个字节,必须按长度字段跳。
自测三问(点开对答案)
1. PS 里四种段的起始码分别是什么? —— **`BA` 打包头、`BB` 节目流映射、`E0` 视频 PES、`C0` 音频 PES**。 2. 为什么「封装开销百分之几」这个说法不严谨? —— **固定头开销除以不同的负载,占比差很多**,**必须带上负载大小才有意义。** 3. 重封装最容易丢哪三样东西? —— **PTS、关键帧标记、私有数据**。下一节换个角度:不拆字节了,看「五千路设备的三套标识怎么对齐」。
- ☐ (本节完,下一节 10.05.09 与其他系统的数据交换)