文章目录
1. TS 的"三层结构"是什么
一个完整的 TS 流,从外到内是这样封装的:
┌─────────────────────────────────────────────────┐
│ TS 层(Transport Stream,传输层) │ ← 第1层:固定188字节的包
│ ┌───────────────────────────────────────────┐ │
│ │ PES 层(Packetized ES,打包基本流) │ │ ← 第2层:把ES打成包
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ ES 层(Elementary Stream,基本流) │ │ │ ← 第3层:原始音视频数据
│ │ │ (H.264 / AAC / ...) │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
| 层级 | 名称 | 作用 |
|---|---|---|
| 第1层 | TS 包(Transport Stream Packet) | 传输的最小单元,固定 188 字节,带 PID 标识 |
| 第2层 | PES 包(Packetized Elementary Stream) | 把原始音视频打成包,带 PTS/DTS 时间戳 |
| 第3层 | ES 流(Elementary Stream) | 原始的视频/音频编码数据 |
2. 第1层:TS 包详解(你图里的内容)
2.1 TS 包整体结构
每个 TS 包固定 188 字节,结构如下:
4字节 0~N字节 剩余字节
┌────────┬─────────────────┬──────────────┐
│ header │ adaptation field│ payload │
│ (4B) │ (自适应区) │ (有效载荷) │
└────────┴─────────────────┴──────────────┘
- header:固定 4 字节,你图里那张表就是它
- adaptation field:可选,是否存在由 header 里的
adaptation_field_control决定 - payload:承载的内容,通常是 PES 数据(或者 PAT/PMT 等表)
2.2 TS Header 逐字段解析
4 字节 = 32 bit,各字段的位分布如下:
字节1(8bit) 字节2(8bit) 字节3(8bit) 字节4(8bit)
┌─────────────┬─┬─┬─┬──────────────┬──┬──┬──────────────┬─────────────────┐
│ sync_byte │T│P│T│ PID │T │A │continuity │ (adaptation │
│ (0x47) │E│U│P│ (13bit) │S │F │counter(4bit) │ field / │
│ │I│S│R│ │C │C │ │ payload...) │
└─────────────┴─┴─┴─┴──────────────┴──┴──┴──────────────┴─────────────────┘
对照你图里的字段逐一讲:
| 字段 | 位数 | 含义 | 通俗理解 |
|---|---|---|---|
| sync_byte | 8b | 同步字节,固定 0x47(字符’G’) | 🔑 最重要!解析器就是靠不断找 0x47 来定位每个 TS 包的起点 |
| transport_error_indicator | 1b | 传输错误指示,通常为0 | 这个包传输出错了,丢了就行 |
| payload_unit_start_indicator | 1b | 负载单元起始标识 | 🔑 PES 包的第一个 TS 包标记为1。因为一个 PES 可能被拆到多个 TS 包里,靠这个位来找到 PES 的开头 |
| transport_priority | 1b | 传输优先级,通常0 | 不重要 |
| pid | 13b | Packet ID,包标识 | 🔑 最重要的字段之一!每个 PID 代表一路流(比如视频PID=0x100,音频PID=0x101)。解析器靠 PID 来区分哪个包是视频、哪个是音频 |
| transport_scrambling_control | 2b | 加扰控制,00=未加密 | 付费电视加密用的,01/10/11是不同的加密密钥 |
| adaptation_field_control | 2b | 自适应区控制 | 🔑 决定这个包有没有自适应区和payload:01=只有payload;10=只有adaptation;11=两者都有 |
| continuity_counter | 4b | 连续计数器,0~15循环 | 🔑 每个 PID 独立计数,用来检测丢包(如果序号不连续说明中间丢包了) |
2.3 Adaptation Field(自适应区)
当 adaptation_field_control 是 10 或 11 时存在。主要作用:
- 填充(padding):PES 数据不足 188 字节时,用 0xFF 填充到整包
- PCR(Program Clock Reference):节目时钟参考,用于音视频同步
- 随机访问指示:标记关键帧位置,方便 seek
结构大致是:
adaptation_field_length (1B) + flags (1B) + 各种可选字段 + 填充字节(0xFF)
3. 第2层:PES 包
PES = Packetized Elementary Stream,把原始音视频数据打成大包。
一个 PES 包可能很大(比如一帧视频就几十 KB),所以一个 PES 会被拆成很多个 TS 包来传输。
PES 包结构
6字节 3字节 变长
┌──────────┬───────────┬──────────────┐
│ start code│ PES header│ PES data │
│ 0x000001 │ (含PTS/DTS)│ (ES数据) │
└──────────┴───────────┴──────────────┘
关键字段:
| 字段 | 作用 |
|---|---|
| start code | 0x000001 + stream_id,PES 包的起始码,用来定位 |
| PTS(Presentation Time Stamp) | 显示时间戳——这帧应该在什么时候显示给用户 |
| DTS(Decoding Time Stamp) | 解码时间戳——这帧应该在什么时候送入解码器 |
| PES data | 里面装的就是 ES 流数据(原始音视频帧) |
💡 为什么有 PTS 和 DTS 两个时间戳?
因为视频有 B 帧(双向预测帧),解码顺序和显示顺序不一样。比如显示顺序是 I→B→P,但解码顺序可能是 I→P→B,所以需要两个时间戳分别控制。
4. 第3层:ES 流
ES = Elementary Stream,最原始的编码数据。
- 视频 ES:H.264 / H.265 / AV1 等编码的裸流,由一个个 NALU 组成
- 音频 ES:AAC / MP3 / AC-3 等编码的裸流
PES 的 payload 部分就是 ES 数据。
5. 完整的封装流程(发送端)
原始视频帧(H.264) 原始音频帧(AAC)
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ ES 流 │ │ ES 流 │
└────┬─────┘ └────┬─────┘
│ 打成PES包 │ 打成PES包
▼ ▼
┌──────────┐ ┌──────────┐
│ PES 包 │ │ PES 包 │
│ 带PTS/DTS│ │ 带PTS/DTS│
└────┬─────┘ └────┬─────┘
│ 拆成188B的TS包 │ 拆成188B的TS包
│ 视频PID=0x100 │ 音频PID=0x101
▼ ▼
┌───────────────────────────┐
│ TS 流(混合在一起) │
│ [视频包][音频包][视频包]… │
│ + PAT表 + PMT表 │ ← 告诉播放器哪个PID对应什么流
└───────────────────────────┘
什么是 PAT 和 PMT?
TS 流里除了音视频数据,还有两张特殊的"节目表":
- PAT(Program Association Table):PID = 0x0000,是节目关联表,告诉你 PMT 在哪个 PID
- PMT(Program Map Table):节目映射表,告诉你这个节目里有哪些流、每个流的 PID 是什么、是视频还是音频
播放器启动时的查找顺序:
先找 PID=0 的包 → 解析出 PAT → 找到 PMT 的 PID → 解析 PMT → 知道视频PID/音频PID → 开始播放
6. 解析流程(接收端)
以一个播放器解析 TS 文件为例,步骤是:
1. 同步:从数据流中找 0x47,每隔188字节验证一次,确认找到了TS包边界
↓
2. 找 PAT:提取 PID=0x0000 的 TS 包,解析出 PMT 的 PID
↓
3. 找 PMT:提取 PMT 对应的 PID 包,解析出视频PID、音频PID各是多少
↓
4. 按 PID 分流:把 TS 包按 PID 分别送到视频队列和音频队列
↓
5. 组装 PES:在每个 PID 队列里,找 payload_unit_start_indicator=1 的包
作为 PES 开头,把后续 TS 包的 payload 拼起来
↓
6. 解析 PES:从 PES 头取出 PTS/DTS
↓
7. 取出 ES:PES 的 data 部分就是原始音视频数据
↓
8. 送解码器:按 PTS/DTS 把 ES 数据送入解码器解码播放
7. 一张图总结三层关系
一个TS文件里有N多个TS包(188B每个)
│
├── PID=0x0000 → PAT表(节目索引)
├── PID=0x1000 → PMT表(节目映射:视频=0x101, 音频=0x102)
│
├── PID=0x101 → 视频TS包序列
│ ├── [start=1] → PES包第1部分 → 组装PES → 取出视频ES帧
│ ├── [start=0] → PES包第2部分 ↗
│ ├── [start=0] → PES包第3部分 ↗
│ └── ...
│
└── PID=0x102 → 音频TS包序列
├── [start=1] → PES包第1部分 → 组装PES → 取出音频ES帧
├── [start=0] → PES包第2部分 ↗
└── ...
8. 几个关键的面试/理解要点
-
为什么 TS 包固定 188 字节?
历史原因:ATM 网络的信元是 48 字节有效载荷,188 = 48×4 - 4(4字节头部),适配 ATM 传输。 -
怎么从一堆二进制数据里找到 TS 包的起点?
找0x47(sync_byte),然后每隔 188 字节验证下一个0x47,连续验证几次都对就说明同步上了。 -
一个 PES 包对应一帧视频吗?
通常是的,但也不绝对。一个 PES 可以包含多个帧,一个帧也可能被拆成多个 PES(虽然少见)。 -
PID 是全局唯一的吗?
在同一个 TS 流里,每个 PID 对应一路唯一的流。不同节目可以复用不同的 PID。
2179



被折叠的 条评论
为什么被折叠?



