3、TS容器深度解析:TS包结构(188字节)、PES封装、PAT/PMT表、PCR机制

各位好,今天我们聊一个硬核话题——TS容器。做流媒体这行,TS(Transport Stream)是绕不开的坎。我入行头两年,天天跟这188字节的包打交道。说白了,TS就是MPEG-2标准里定义的一种传输流格式,专门用来在容易出错的网络环境里传输音视频数据。

你可能会问:为什么是188字节?不是190,也不是200?嗯,这跟ATM信元的设计思路有关,188字节刚好能塞进几个ATM信元里,早期硬件设计就这么定下来了。后来大家用习惯了,也就成了铁律。

3.1 TS包结构:188字节的硬核拆解

每个TS包固定188字节。我刚开始做转码的时候,总觉得这结构太死板,后来才发现——正是这种固定长度,让解复用变得极其高效。

一个TS包分两部分:

  • 包头(4字节):同步字节、传输错误指示、PID、加扰控制等
  • 有效载荷(184字节):PES数据或PSI/SI表数据

来看个具体的包结构:

| 字节0 | 字节1 | 字节2 | 字节3 | 字节4~187 |
|-------|-------|-------|-------|-----------|
| 0x47  | 标志位 | PID低8位 | 控制字段 | 有效载荷  |
| 同步字 | +PID高5位 |        |         | 184字节   |

同步字节固定是0x47。我在项目里遇到过一个问题:解码器突然丢同步了,画面花成一片。排查了半天,发现是网络丢包导致同步字节被篡改。从那以后,我习惯在解复用器里加一个同步状态机,连续检测到5个正确的0x47才确认同步。

核心字段速查表

字段位数说明
sync_byte8固定0x47,同步用
transport_error_indicator11表示当前包有错误
payload_unit_start_indicator11表示PES/PSI起始
PID13包标识符,核心字段
adaptation_field_control200=保留,01=无适配域,10=仅适配域,11=有适配域
continuity_counter40~15循环,检测丢包

PID是TS的灵魂。每个音视频流、每个PSI表,都有自己的PID。解复用器就是靠PID来区分哪个包是视频、哪个是音频、哪个是PAT表。

3.2 PES封装:从ES到PES的打包艺术

原始的音视频编码数据叫ES(Elementary Stream)。但ES不能直接塞进TS包,得先包一层PES(Packetized Elementary Stream)。

PES包的结构是这样的:

| PES包头(可变长) | PES有效载荷 |
|-------------------|-------------|
| 起始码 + 流ID + PES长度 + 可选字段 | 原始ES数据 |

PES包头里最重要的东西是PTS(显示时间戳)和DTS(解码时间戳)。没有这两个时间戳,解码器就不知道什么时候该显示、什么时候该解码。

我记得有一次做多码率自适应转码,发现画面频繁卡顿。查了半天,原来是PES包里的PTS间隔不均匀。有些PES包没带时间戳,解码器只能靠猜,结果就卡了。解决方案很简单——强制每个视频PES包都携带PTS。

实战经验:H.264视频的PES包,我建议每个IDR帧单独一个PES包,并且必须带PTS。非IDR帧可以多个帧合在一个PES包里,但PTS要填第一个帧的时间。

PES包长度是可变的。对于视频,PES包通常很大,一个视频帧就是一个PES包。对于音频,PES包一般比较小,比如一个AAC帧也就几百字节。

3.3 PAT/PMT表:TS流的节目指南

TS流里可能包含多个节目(比如一套高清、一套标清)。解复用器怎么知道哪个PID是视频、哪个是音频?靠的就是PSI表。

PSI表有四种,最核心的是PAT和PMT:

  • PAT(节目关联表):PID固定为0x0000,列出所有节目及其对应的PMT的PID
  • PMT(节目映射表):列出某个节目包含的所有流(视频、音频、字幕等)及其PID

我画了一张图,帮你理解它们的关系:

PAT/PMT 层级关系图 PAT (PID=0x0000) PMT (节目1, PID=0x0100) PMT (节目2, PID=0x0200) 视频 (PID=0x0110) 音频 (PID=0x0120) 字幕 (PID=0x0130) 视频2 (PID=0x0210) 解复用流程:先找PAT → 获取PMT的PID → 再找PMT → 获取音视频PID → 开始解复用

解复用器启动时,首先在PID 0x0000上找PAT。PAT里写着:节目1的PMT在PID 0x0100,节目2的PMT在PID 0x0200。然后去PID 0x0100找PMT,PMT里写着:视频在PID 0x0110,音频在PID 0x0120。拿到这些信息后,解复用器就知道该收哪些PID的包了。

注意:PAT和PMT是周期性发送的,通常每隔100ms就重传一次。但有些编码器配置不当,PMT发送间隔太长,导致解码器启动时迟迟找不到PMT,画面黑屏很久。我建议PMT的发送间隔不要超过200ms。

3.4 PCR机制:系统时钟的定海神针

PCR(Program Clock Reference)是TS流里最容易被忽视但又最关键的东西。它用来恢复解码器的27MHz系统时钟。

为什么需要PCR?因为编码器和解码器的时钟不可能完全同步。编码器用27MHz时钟编码,解码器也得用27MHz时钟解码,否则就会出现音画不同步、缓存上溢或下溢。

PCR的精度要求很高——27MHz时钟的周期大约是37ns。PCR字段在TS包的适配域里,占6字节,包含两部分:

  • PCR_base(33位):以300为单位的计数器
  • PCR_ext(9位):0~299的细粒度计数器

计算公式:PCR = PCR_base × 300 + PCR_ext,单位是27MHz时钟周期。

我曾经遇到过一个很头疼的问题:转码后的TS流在某些机顶盒上播放正常,但在另一些上就音画不同步。排查了三天,最后发现是PCR间隔不均匀。有些TS包之间PCR间隔超过100ms,解码器的PLL锁不住。

避坑指南:PCR的发送间隔,我建议不超过40ms。对于25fps的视频,每帧至少插入一次PCR。如果码流里有多个节目,每个节目都要有自己的PCR,而且PCR的PID必须在PMT里明确标出来。

PCR还有一个重要用途——计算码率。通过两个PCR包的时间差和它们之间的字节数,可以精确计算出TS流的实时码率。这个技巧在做码率自适应转码时特别有用。

3.5 实战:如何手动解析一个TS包

说了这么多理论,我们来点实际的。假设你拿到一个TS文件,想手动解析第一个包:

  1. 找到0x47同步字节,确认包起始
  2. 读取第1~2字节,提取PID(高5位在字节1,低8位在字节2)
  3. 检查adaptation_field_control:如果是01,直接读有效载荷;如果是11,先读适配域长度,跳过适配域再读有效载荷
  4. 如果payload_unit_start_indicator为1,说明有效载荷起始是一个PES包或PSI段的起始

我刚开始学TS解析时,就是拿一个十六进制编辑器,对着188字节的包一个一个字段地看。虽然慢,但确实能帮你把每个字段的含义刻在脑子里。

小结一下本章核心:

  • TS包固定188字节,同步字0x47,PID是核心标识
  • PES是ES的容器,PTS/DTS在PES包头里
  • PAT(PID=0x0000)指向PMT,PMT指向具体流
  • PCR用于恢复27MHz系统时钟,间隔不超过40ms

嗯,TS容器的内容就讲到这里。这些基础概念搞清楚了,后面讲解复用、重封装、转码流程的时候,你就能理解每一步在做什么了。