TS 的解析路线是固定的:PID 0 拿 PAT,PAT 给 PMT 的 PID,PMT 给流的 PID。 三步走完就知道怎么解了。
5.5.2 讲了工具,这一节把工具量出来的结果串成一条路线。 5.5.4 再把 PAT 与 PMT 的字段逐字节拆开。
一、解析的固定路线
① 找 PID 0:PAT 一定在 PID 0 上,这是硬规定。 ② 读 PAT:里面有「节目号 → PMT 的 PID」的对应表。 ③ 找 PMT:按上一步拿到的 PID 找。 ④ 读 PMT:里面有「流类型 → 具体 PID」的对应表。 ⑤ 按 PID 取数据:视频一个 PID、音频一个 PID。
实测这个只含视频的 TS:PAT 16 字节、table_id 0x00, 里面写「节目 1 的 PMT 在 PID 4096」。 PMT 21 字节、table_id 0x02,写「PCR 在 PID 256」。 流循环里只有一条:0x1B 的 H.264,PID 256。
注意 0x1B 这个值: 它是 H.264 的流类型编号,0x0F 是 AAC 音频。 解析器看到 0x1B 就把这一路交给 H.264 解码器,看到 0x0F 交给 AAC。 这就是「流类型」字段的全部意义。
二、为什么表要周期重发
PAT 与 PMT 都是「周期重发」的,不是只发一次。 标准建议间隔 100 毫秒左右。
这样新观众从中间进来,最多等 100 毫秒就能拿到表。 所以「切片开头必须能独立解」这条要求,就是靠重发表满足的。
重发还有一个好处:抗丢包。 如果某一次 PAT 丢了,100 毫秒后还有一次,不用重连。 这就是「无状态」的代价很低的一种设计—— 接收端可以随时加入、随时丢失、随时恢复。
代价就是 5.2.3 里实测到的那个高比例: 51 个包里有 9 个 PAT、9 个 PMT,各占 17.6%。 片越长,这个比例越低——因为重发次数跟时间有关、跟长度无关。
三、一张图看懂三级关系
把三层关系写成链条: PID 0 的 PAT 里写「节目 1 → PMT 在 PID 4096」。 PID 4096 的 PMT 里写「流类型 0x1B → PID 256」。 PID 256 里就是 H.264 的 PES。
这条链子断任何一环,播放器就出不了画面: PAT 丢了,它不知道 PMT 在哪;PMT 丢了,它不知道哪个 PID 是视频。 所以「TS 播不动」第一步永远是查这两张表在不在。
多节目流的 PAT 会有一条以上的记录。 比如一个频点里塞了三套节目,PAT 里就有三条「节目号 → PMT PID」, 这也是数字电视能「一个频点播多套」的原理。
四、动手 Demo:码流结构——PAT 找 PMT,PMT 找音视频
真封一个 TS,然后严格按「PID 0 → PAT → PMT → 流循环」走一遍。 每一层都要打印它给出的下一步线索,链条才完整。
import io, os, tempfile
import av, numpy as np
tmp = os.path.join(tempfile.mkdtemp(), "a.ts")
c = av.open(tmp, "w", format="mpegts")
st = c.add_stream("libx264", rate=25)
st.width, st.height, st.pix_fmt = 320, 240, "yuv420p"
st.options = {"crf": "30", "preset": "veryfast", "g": "25"}
for i in range(25):
f = av.VideoFrame(320, 240, "yuv420p")
y = np.zeros((240, 320), np.uint8)
y[:, :] = np.linspace(16, 200, 320, dtype=np.uint8)[None, :]
f.planes[0].update(y.tobytes())
for pi in (1, 2):
f.planes[pi].update(bytes([128]) * f.planes[pi].buffer_size)
f.pts = i
for pk in st.encode(f):
c.mux(pk)
for pk in st.encode(None):
c.mux(pk)
c.close()
raw = io.open(tmp, "rb").read()
def section(pid):
u"""把这个 PID 的载荷拼起来,跳到 pointer_field 之后。"""
buf = b""
for k in range(len(raw) // 188):
p = raw[k * 188:(k + 1) * 188]
if ((p[1] & 0x1F) << 8) | p[2] != pid:
continue
pos = 4 + ((p[3] >> 4) & 3) - 1
if ((p[3] >> 4) & 3) == 3:
pos += 1 + p[4]
pos += 1 + p[pos] # 跳过 pointer_field
buf += p[pos:]
ln = ((buf[1] & 0x0F) << 8) | buf[2]
return buf[:3 + ln]
pat = section(0)
pmt_pid = ((pat[10] & 0x1F) << 8) | pat[11]
print("PAT %d 字节,节目 1 的 PMT 在 PID %d" % (len(pat), pmt_pid))
pmt = section(pmt_pid)
i = 12 + (((pmt[10] & 0x0F) << 8) | pmt[11])
while i + 5 <= len(pmt) - 4:
t = pmt[i]
pid = ((pmt[i + 1] & 0x1F) << 8) | pmt[i + 2]
print(" 流类型 0x%02X -> PID %d" % (t, pid))
i += 5 + (((pmt[i + 3] & 0x0F) << 8) | pmt[i + 4])
实跑输出:
--------------------------------------------------------------------
码流结构——PAT 找 PMT,PMT 找音视频
--------------------------------------------------------------------
**① 解析的固定路线**
**① 找 PID 0**:**PAT 一定在 PID 0 上,这是硬规定。**
**② 读 PAT**:**里面有「节目号 → PMT 的 PID」的对应表。**
**③ 找 PMT**:**按上一步拿到的 PID 找。**
**④ 读 PMT**:**里面有「流类型 → 具体 PID」的对应表。**
**⑤ 按 PID 取数据**:**视频一 PID、音频一 PID。**
找到 PAT,16 字节,table_id = 0x00
PAT 里写:节目 1 的 PMT 在 PID 4096
PMT 21 字节,table_id = 0x02
PMT 里写:PCR 在 PID 256,节目信息长度 0
**② 把每条流的对应关系列出来**
流类型 含义 PID
---------------------------------
0x1B H.264 视频 256
**实测这个文件只有一条流:0x1B 的 H.264。**
**因为编码时没加音频——真实直播会是两到三条。**
**注意 PAT 与 PMT 都是「周期重发」的,不是只发一次。**
**标准建议间隔 100 毫秒左右。**
**这样新观众从中间进来,最多等 100 毫秒就能拿到表。**
**所以「切片开头必须能独立解」这条要求,就是靠重发表满足的。**
PAT 16 字节、PMT 21 字节,都很小,所以能频繁重发。
pos 那两步最容易写错:先跳过适配字段,再跳过 pointer_field,顺序反了全乱。
流类型 0x1B 就是 H.264,没有音频是因为这一路只编了视频。
30 秒实验
- 加一条 AAC 音频再跑一次,看流循环多几行——多一行 0x0F 的音频,PID 另给一个。
- 把
section()里的 pointer_field 那行删掉,看解析结果——PAT 直接解错,table_id 都读不出来。 - 打印 PAT 里
节长度字段,和文件实际长度比——16 = 3 头 + 13 长度,一字节不差。
常见误区
- 「PAT 在哪个 PID 要看配置」——不用看,永远是 0。
- 「表只发一次就够了」——必须周期重发,否则中途进来的解不了。
- 「流类型就是编码的四字符名」——是一个字节的编号,0x1B 才是 H.264。
自测三问(点开对答案)
1. 解析 TS 的五步是什么? —— **找 PID 0 → 读 PAT → 找 PMT → 读 PMT → 按 PID 取数据。** 2. 表为什么必须周期重发? —— **新观众要从中间进来**,重发才能让它们拿到声明;顺便还抗丢包。 3. 0x1B 与 0x0F 分别是什么? —— **H.264 视频与 AAC 音频**的流类型编号。- ☐ (本节完,下一节 5.5.4 PAT 及 PMT 表格式)