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

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

  1. 加一条 AAC 音频再跑一次,看流循环多几行——多一行 0x0F 的音频,PID 另给一个。
  2. 把 section() 里的 pointer_field 那行删掉,看解析结果——PAT 直接解错,table_id 都读不出来。
  3. 打印 PAT 里 节长度 字段,和文件实际长度比——16 = 3 头 + 13 长度,一字节不差。

常见误区

  1. 「PAT 在哪个 PID 要看配置」——不用看,永远是 0。
  2. 「表只发一次就够了」——必须周期重发,否则中途进来的解不了。
  3. 「流类型就是编码的四字符名」——是一个字节的编号,0x1B 才是 H.264。
自测三问(点开对答案) 1. 解析 TS 的五步是什么? —— **找 PID 0 → 读 PAT → 找 PMT → 读 PMT → 按 PID 取数据。** 2. 表为什么必须周期重发? —— **新观众要从中间进来**,重发才能让它们拿到声明;顺便还抗丢包。 3. 0x1B 与 0x0F 分别是什么? —— **H.264 视频与 AAC 音频**的流类型编号。

相关笔记