📡 MPEG-2 TS(Transport Stream)传输流详解

一、基本定义与核心定位

TS(Transport Stream,传输流) 是MPEG-2标准(ISO/IEC 13818-1,MPEG-2 Systems)定义的一种面向不可靠传输环境设计的音视频封装格式,全称为MPEG-2 TS

核心设计目标:

  • 有噪声/丢包的传输信道(如地面广播、卫星、有线电视、IP网络)中实现稳定可靠的音视频传输
  • 支持多路节目复用(多套电视节目在同一传输流中)
  • 提供强容错能力快速同步恢复机制
  • 满足实时性要求,适合直播等低延迟场景

二、分层封装结构(ES→PES→TS)

TS流采用三层封装模型,从原始媒体数据到最终传输流的完整链路:

层级 名称 英文全称 核心功能 数据单元
1 基本流 Elementary Stream (ES) 原始压缩媒体数据(H.264/HEVC视频,AAC/MP3音频) 编码帧(如I/P/B帧、音频采样帧)
2 分组基本流 Packetized Elementary Stream (PES) 为ES添加时间戳、媒体类型标识,分组化 可变长度PES包(通常包含一帧完整数据)
3 传输流 Transport Stream (TS) 固定长度打包,添加传输控制信息,支持多节目复用 固定188字节TS包(可扩展至204字节含FEC)

封装流程:原始媒体数据→ES→PES(添加PTS/DTS)→TS(固定长度打包+传输控制信息)→传输→解包→PES→ES→解码播放

PES包详细结构

PES包是ES和TS之间的中间层,理解其结构对TS流解析至关重要。

1
2
3
4
5
┌──────────────┬──────────────┬──────────────┬──────────────┬───────────────┐
│ PES Header │ PES Optional │ PTS/DTS │ 其他可选 │ PES Payload │
│ (3字节固定) │ Header │ (5/10字节) │ 字段 │ (ES数据) │
│ │ (可变长度) │ │ │ │
└──────────────┴──────────────┴──────────────┴──────────────┴───────────────┘

PES包头(6字节固定部分)

字段 位数 取值/含义 核心作用
起始码前缀 24 固定0x000001 标识PES包起始,与ES起始码格式一致
stream_id 8 见下表 标识PES包承载的流类型
PES包长度 16 0~65535 PES包剩余长度(不含起始码前缀和stream_id),0=未指定长度(视频流常用)

stream_id关键取值

stream_id 流类型
0xE0 视频流(MPEG-1/2/4/H.264/HEVC)
0xC0 音频流(MPEG-1/2/AAC)
0xBD 私有流1(AC-3/DTS音频、字幕等)
0xBE 私有流2
0xBC program_stream_map
0xFF program_stream_directory

PES可选头(可变长度)

字段 位数 核心作用
2位标记 2 固定’10’,标识MPEG-2 PES
PES加扰控制 2 标识PES层加扰方式
PES优先级 1 0=普通,1=高优先级
数据对齐指示 1 1=有效载荷从对齐点开始
版权 1 1=有版权
原始或拷贝 1 1=原始数据
PTS_DTS_flags 2 00=无PTS/DTS,01=禁止,10=仅有PTS,11=PTS+DTS
ESCR标志 1 1=存在ESCR(ES时钟参考)
ES速率标志 1 1=存在ES速率
DSM特技模式 1 1=存在DSM特技模式信息
附加复制信息 1 1=存在附加复制信息
CRC标志 1 1=存在PES CRC
扩展标志 1 1=存在PES扩展
PES头数据长度 8 后续可选字段的总字节数

PTS/DTS编码格式(33位时间戳)

PTS和DTS都是33位值,基于90kHz时钟,编码时分散存储在5字节中:

1
2
3
4
5
PTS编码(5字节,33位有效):
4位标记 '0010' | PTS[32..30] | 1位标记 '1' | PTS[29..15] | 1位标记 '1' | PTS[14..0] | 1位标记 '1'

DTS编码(5字节,33位有效):
4位标记 '0001' | DTS[32..30] | 1位标记 '1' | DTS[29..15] | 1位标记 '1' | DTS[14..0] | 1位标记 '1'

33位时间戳的取值范围:0 ~ 2^33-1 = 8589934591
在90kHz时钟下,最大表示时间:8589934591 / 90000 ≈ 95443秒 ≈ 26.5小时
超过此范围后时间戳回绕(wrap around),接收端需要处理回绕逻辑。

ES流映射到PES的规则

  • 视频ES:每个访问单元(Access Unit,即一个编码帧)封装为一个PES包,I帧起始的PES包的payload_start_indicator=1
  • 音频ES:每个音频帧封装为一个PES包
  • PES包长度:视频流通常设为0(未指定),音频流通常指定具体长度

三、TS包详细结构(188字节固定长度)

每个TS包是传输的最小单元,结构如下:

1
2
3
4
5
6
┌─────────────┬─────────────────┬─────────────────────┐
│ TS Header │ Adaptation Field│ Payload (有效载荷) │
│ (4字节固定) │ (可选,0-184字节)│ (剩余字节) │
└─────────────┴─────────────────┴─────────────────────┘

总长度固定188字节:4(Header) + 0~184(AF) + 184~0(Payload)

1. TS包头(4字节,固定)

TS包头是整个TS系统的”导航灯塔”,结构如下(按位定义):

字段 位数 取值/含义 核心作用
同步字节 8 固定为0x47 标识TS包起始,用于接收端同步
传输错误指示 1 0=无错,1=有错 标记包在传输中是否出错,出错包应丢弃
净荷单元起始指示 1 0=非起始,1=起始 标记当前包是否为PES包或PSI Section的起始位置
传输优先级 1 0=普通,1=高优先级 用于QoS控制,优先传输重要数据
PID 13 0x0000~0x1FFF Packet Identifier,包标识符,用于区分不同流(视频/音频/PAT/PMT等)
传输加扰控制 2 00=未加扰,01/10/11=加扰模式 标识内容是否加密及加密方式
适配字段控制 2 见下表 指示包中是否包含适配字段和有效载荷
连续计数器 4 0~15循环 检测包丢失或顺序错误,同一PID的连续包计数器应递增(模16)

适配字段控制(adaptation_field_control)取值

含义 说明
00 保留 ISO/IEC保留,不应使用
01 仅有效载荷 最常见,无适配字段,184字节全部为有效载荷
10 仅适配字段 无有效载荷,用于填充或仅传输PCR
11 适配字段+有效载荷 两者都有,适配字段用于填充+PCR,剩余为有效载荷

关键PID值(预定义)

PID 用途 说明
0x0000 PAT表 必选,节目关联表
0x0001 CAT表 条件接收表(有加密节目时存在)
0x0002 TSDT表 传输流描述表
0x0010 NIT表 网络信息表(实际PID由PAT指定,此为DVB默认值)
0x1FFF 空包(Null Packet) 填充用,接收端应直接丢弃

2. 适配字段(Adaptation Field,可选)

适配字段主要功能:

  • 填充数据,确保TS包固定188字节长度(PES数据不足188字节时用填充补齐)
  • 插入PCR(Program Clock Reference,节目时钟参考) 时间戳
  • 提供其他传输控制信息(如随机访问指示、不连续指示)

适配字段结构(长度可变,首字节为长度标识):

1
2
3
4
5
6
7
8
┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ AF长度 │ 标记位 │ PCR │ OPCR │ 填充字节 │ ... │ │
│ (1字节) │ (1字节) │ (6字节) │ (6字节) │ (可变) │ (其他) │ │
│ │ │ (可选) │ (可选) │ (可选) │ (可选) │ │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘

AF长度字段值 = 后续所有字段的总字节数(不含自身)
当仅含AF长度+标记位时,AF长度=0(最小适配字段为2字节:长度+标记位)

标记位(flags,1字节)详细定义

字段名 含义
7 discontinuity_indicator 1=系统时钟不连续或计数器不连续
6 random_access_indicator 1=当前包包含随机访问点(如I帧起始)
5 elementary_stream_priority_indicator 1=该包中的ES数据为高优先级
4 PCR_flag 1=适配字段中包含PCR
3 OPCR_flag 1=适配字段中包含OPCR(原始PCR,用于级联复用器)
2 splicing_point_flag 1=包含拼接点倒计数
1 transport_private_data_flag 1=包含私有数据
0 adaptation_field_extension_flag 1=包含适配字段扩展

PCR的核心作用与精确结构

PCR携带编码端系统时钟(STC,System Time Clock) 信息,接收端通过PCR恢复与编码端同步的本地时钟,是PTS/DTS时间戳的参考基准。

PCR由两部分组成,共6字节(48位):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PCR结构(6字节,48位):
┌──────────────────────────────────────────┬──────┬──────────┐
│ PCR_base (33位) │ 保留 │ PCR_ext │
│ 基于90kHz时钟 │ 6位 │ (9位) │
│ │ │ 基于27MHz│
└──────────────────────────────────────────┴──────┴──────────┘

PCR = PCR_base × 300 + PCR_extension

PCR_base:33位,基于27MHz时钟÷300 = 90kHz
PCR_extension:9位,基于27MHz时钟的精细部分,范围0~299

PCR完整精度:27MHz(约37ns分辨率)
PCR_base精度:90kHz(约11.1μs分辨率)

PCR关键参数

  • 系统时钟频率:27MHz(±30ppm容差)
  • PCR_base精度:90kHz(与PTS/DTS同频)
  • PCR完整精度:27MHz(约37ns分辨率)
  • PCR最大间隔:100ms(MPEG-2标准要求)
  • PCR抖动容限:±500ns(DVB标准)

PCR时钟恢复原理

  1. 编码端以27MHz时钟驱动STC,周期性将当前STC值编码为PCR插入适配字段
  2. 接收端通过锁相环(PLL)或类似算法,将本地27MHz振荡器锁定到PCR
  3. 本地STC与PTS/DTS比较,控制解码和显示时机
  4. PCR间隔不超过100ms,确保时钟不会严重漂移

3. 有效载荷(Payload)

有效载荷携带实际数据,分为两类:

  1. PES包数据:音频/视频等媒体数据,由PES包分片而来
  2. PSI/SI Section数据:节目特定信息/服务信息,用于描述流结构

PES包与TS包的映射关系

  • 一个PES包通常包含一帧完整媒体数据(如一个I帧、一帧音频)
  • 一个PES包可被拆分为多个TS包传输(因PES包长度可变,通常远大于188字节)
  • TS包的净荷单元起始指示位(payload_start_indicator)=1时,表示当前TS包是一个新PES包的开始
  • PES包的第一个字节必须位于TS包有效载荷的第一个字节(payload_start_indicator=1时)

空包(Null Packet)机制

PID=0x1FFF的TS包为空包,其有效载荷无意义,核心作用包括:

  • 维持恒定比特率(CBR):TS流通常需要恒定比特率传输(如DVB广播),当有效数据不足时插入空包填充
  • 复用器速率匹配:多路节目复用时,各路速率波动,空包用于平滑总比特率
  • 缓冲管理:保持解码器缓冲区的恒定输入速率,避免缓冲区下溢或上溢

空包处理规则:

  • 接收端应直接丢弃空包,不做任何处理
  • 空包的连续计数器也应递增,但接收端通常不检查
  • 空包的适配字段控制通常为01(仅有效载荷),有效载荷全为0xFF

四、PSI/SI表系统(节目导航核心)

PSI(Program Specific Information,节目特定信息)和SI(Service Information,服务信息)是TS流的”导航系统”,使接收端能够正确识别和解析多路节目。

1. PSI Section分段机制

PSI表不是直接放入TS包的,而是通过Section(段) 机制分段传输,这是理解PSI解析的关键前提。

Section结构

1
2
3
4
5
6
7
8
9
┌──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐
│ table_id │ section_ │ 版本/当前 │ section_ │ Section │
│ (1字节) │ syntax_ │ 下一个指示 │ number/ │ 数据 │
│ │ indicator │ │ last_section │ │
│ │ + section_ │ │ number │ │
│ │ length │ │ │ │
│ │ (2字节) │ │ │ │
└──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘
+ CRC32

Section关键特性

  • Section长度可变,最大1024字节(PSI Section)或4096字节(private Section)
  • 一个Section可能跨多个TS包传输(Section长度>TS包有效载荷时)
  • 一个TS包的有效载荷中也可能包含多个Section(Section较短时)
  • Section末尾有CRC32校验(4字节),用于验证Section完整性
  • Section通过版本号机制(version_number,5位,0~31循环)标识表是否更新

Section与TS包的映射规则

  • 当payload_start_indicator=1时,TS包有效载荷的第一个字节为pointer_field(1字节),指示Section数据起始的偏移量
  • pointer_field=0表示Section数据紧跟pointer_field之后
  • pointer_field>0表示pointer_field后有若干填充字节,然后才是Section数据

2. 核心PSI表

表名 英文全称 PID table_id 核心功能
PAT Program Association Table 0x0000 0x00 节目关联表,列出所有节目及其对应的PMT PID
PMT Program Map Table 可变(由PAT指定) 0x02 节目映射表,列出特定节目的所有组件(视频PID、音频PID、字幕PID等)及对应的编码类型
CAT Conditional Access Table 0x0001 0x01 条件接收表,描述加密系统信息(如CAS系统、EMM PID)
TSDT TS Description Table 0x0002 0x03 传输流描述表,提供TS流的整体信息

PAT表详细结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PAT Section:
┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ table_id │ section_ │ transport│ version/ │ section_ │ 节目列表 │ CRC32 │
│ =0x00 │ length │ _stream_ │ current_ │ number/ │ (循环) │ (4字节) │
│ (1字节) │ (2字节) │ id │ next │ last_ │ │ │
│ │ │ (2字节) │ (1字节) │ section │ │ │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘

节目列表每项4字节:
┌──────────────────────┬──────────────────────┐
│ program_number │ PMT PID / NIT PID │
│ (2字节) │ (13位PID + 3位保留) │
└──────────────────────┴──────────────────────┘
program_number=0 → 该PID指向NIT
program_number≠0 → 该PID指向对应节目的PMT

PMT表详细结构

1
2
3
4
5
6
7
8
9
10
11
12
13
PMT Section:
┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ table_id │ section_ │ program_ │ version/ │ PCR_PID │ 节目描述 │ 基本流 │ CRC32 │
│ =0x02 │ length │ number │ current_ │ (13位) │ (可变) │ 列表 │ (4字节) │
│ (1字节) │ (2字节) │ (2字节) │ next │ │ │ (循环) │ │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘

基本流列表每项:
┌──────────┬──────────┬──────────┬──────────┐
│ stream_ │ 基本流 │ ES信息 │ │
│ type │ PID │ (可变) │ │
│ (1字节) │ (13位) │ │ │
└──────────┴──────────┴──────────┴──────────┘

stream_type关键取值

stream_type 编码格式 说明
0x01 MPEG-1 Video
0x02 MPEG-2 Video
0x03 MPEG-1 Audio
0x04 MPEG-2 Audio
0x1B H.264/AVC Video 最常见的高清编码
0x24 HEVC/H.265 Video 4K/UHD编码
0x0F AAC Audio
0x06 PES私有数据 字幕、AC-3/DTS音频等
0x81 AC-3 Audio ATSC标准

PAT→PMT→音视频流的导航流程

  1. 接收端首先解析PID=0x0000的PAT表,获取所有节目号及对应PMT的PID
  2. 根据节目号找到对应的PMT PID,解析PMT表
  3. 从PMT中获取当前节目所有媒体流的PID(视频PID、音频PID等)和编码格式(stream_type)
  4. 从PMT中获取PCR_PID,用于时钟恢复
  5. 筛选出对应PID的TS包,重组为PES包,解码播放

PSI表版本更新机制

  • 每个PSI Section都有version_number(5位,0~31循环)
  • 当表内容发生变化时,version_number递增1(模32)
  • current_next_indicator=1表示当前生效,=0表示预发布(下一版本生效)
  • 接收端应持续监测PAT/PMT的版本号变化,及时更新节目映射关系

3. SI表(扩展信息)

SI表提供额外的服务信息,主要用于数字电视广播(DVB标准定义):

表名 英文全称 PID 核心功能
NIT Network Information Table 0x0010(DVB默认) 描述传输网络参数(频点、调制方式等)
SDT Service Description Table 0x0011 提供节目名称、类型、运行状态等信息
EIT Event Information Table 0x0012 提供节目单、当前/后续节目信息(EPG数据源)
TDT Time and Date Table 0x0014 提供当前UTC时间和日期
TOT Time Offset Table 0x0014 提供本地时间偏移信息
BAT Bouquet Association Table 0x0011 节目组关联表,运营商节目包划分

4. 条件接收(CA)与加扰机制

条件接收系统(CAS,Conditional Access System)是TS流中实现付费电视、内容保护的核心机制。

加扰与加密的区别

  • 加扰(Scrambling):对音视频内容本身进行扰乱处理,使未授权用户无法正常观看,使用控制字(CW,Control Word) 作为密钥
  • 加密(Encryption):对CW进行加密传输,确保只有授权用户能获取CW,使用业务密钥(SK)加密

CAS系统工作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
编码端:
原始音视频 → 加扰器(使用CW) → 加扰后的TS流 → 复用器 → 传输

CW(控制字,通常64位) → 加密器(使用SK) → ECM(Entitlement Control Message)
授权信息 → 加密器(使用PDK) → EMM(Entitlement Management Message)

ECM流 → 分配PID(由PMT中CA描述符指定) → 复用到TS流
EMM流 → 分配PID(由CAT表指定) → 复用到TS流

接收端:
TS流 → 解复用 → 提取ECM/EMM → 智能卡
智能卡:EMM(用PDK解密)→ 获取授权 → ECM(用SK解密)→ 获取CW
CW → 解扰器 → 原始音视频 → 解码播放

关键概念

术语 全称 说明
CW Control Word 控制字,加扰/解扰的直接密钥,通常64位,每隔5~20秒更换一次
SK Service Key 业务密钥,用于加密CW,按频道/节目包分配
PDK Personal Distribution Key 个人分配密钥,用于加密EMM,每张智能卡唯一
ECM Entitlement Control Message 授权控制消息,携带加密后的CW和访问条件,周期性发送
EMM Entitlement Management Message 授权管理消息,携带用户授权信息,周期性发送

CAT表的作用

  • PID=0x0001,列出TS流中使用的所有CAS系统
  • 每个CA描述符包含:CA_system_ID(标识CAS厂商)、EMM PID(该系统的EMM流PID)
  • 接收端根据CAT找到对应CAS系统的EMM PID,提取EMM数据

PMT中的CA描述符

  • PMT的节目级和ES级均可包含CA描述符
  • 节目级CA描述符:整个节目的ECM PID
  • ES级CA描述符:特定基本流的ECM PID

五、时间同步机制(PCR+PTS+DTS)

TS流通过三层时间戳机制确保精准音视频同步

时间戳 英文全称 位置 作用 精度
PCR Program Clock Reference TS适配字段 恢复系统时钟,作为PTS/DTS的时间基准 27MHz(base为90kHz,ext为27MHz精细部分)
PTS Presentation Time Stamp PES包头 指示媒体帧应该何时呈现(显示/播放)给用户 90kHz
DTS Decoding Time Stamp PES包头 指示媒体帧应该何时解码(仅视频B帧需要,I/P帧PTS=DTS) 90kHz

同步原理

  1. 编码端:

    • 所有时间戳基于同一系统时钟(STC,27MHz) 生成
    • PCR = STC当前值(27MHz精度)
    • PTS/DTS = STC当前值 ÷ 300(转换为90kHz精度,33位编码)
    • 视频I/P/B帧需要PTS和DTS(B帧解码顺序与显示顺序不同)
    • 音频帧通常只需要PTS(解码后立即播放)
  2. 接收端:

    • 通过PCR恢复本地STC(27MHz),与编码端保持同步
    • 本地STC ÷ 300 得到90kHz时钟,与PTS/DTS比较
    • 当本地STC ÷ 300 ≥ PTS时,显示该帧
    • 当本地STC ÷ 300 ≥ DTS时,解码该帧
    • 即使部分包丢失,也能通过下一个PCR快速重新同步

PTS/DTS与B帧的关系

视频编码中,B帧的解码顺序与显示顺序不同,导致DTS和PTS值不同:

1
2
3
4
5
6
7
8
显示顺序:I  B  B  P  B  B  P  B  B  P
PTS值: 0 2 3 5 7 8 10 12 13 15

解码顺序:I P B B P B B P B B
DTS值: 0 3 1 2 6 4 5 9 7 8

说明:编码器先编码I帧和P帧(作为B帧的参考帧),再编码B帧
因此P帧的DTS(3)早于其PTS(5),中间的B帧DTS(1,2)在其后

时间戳回绕(Wrap Around)处理

  • PTS/DTS为33位无符号整数,最大值2^33-1
  • 在90kHz时钟下,约26.5小时回绕一次
  • 接收端必须处理回绕:当新时间戳与旧时间戳差值超过2^32时,判定为回绕

六、TS流比特率与速率控制

1. 比特率计算

TS流的比特率可通过多种方式确定:

通过PCR计算

1
2
3
4
5
6
比特率 = (TS包数 × 188 × 8) / (PCR差值 / 27000000)

其中:
TS包数 = 两个PCR之间的TS包总数(含空包)
PCR差值 = 后一个PCR值 - 前一个PCR值(27MHz单位)
27000000 = 27MHz时钟频率

通过TS头信息

  • 适配字段中可选的transport_rate字段直接指示比特率
  • 但该字段在实际TS流中很少使用

2. CBR与VBR

特性 CBR(恒定比特率) VBR(可变比特率)
比特率 恒定不变 随内容复杂度变化
空包使用 大量使用空包填充 较少使用空包
典型场景 数字电视广播(DVB/ATSC) IPTV、网络直播
带宽利用 效率较低(简单场景浪费带宽) 效率较高(按需分配)
缓冲管理 简单(恒定输入速率) 复杂(需处理速率波动)
复用 简单(固定时隙分配) 复杂(统计复用)

统计复用(Statistical Multiplexing)

  • 多路VBR节目共享固定带宽
  • 复用器根据各路节目的瞬时复杂度动态分配比特率
  • 复杂场景(运动剧烈)的节目分配更多带宽,简单场景分配较少
  • 整体带宽利用率显著高于固定时隙分配

3. 缓冲区模型

MPEG-2标准定义了T-STD(Transport Stream System Target Decoder) 缓冲区模型,用于规定解码器的缓冲行为:

1
2
3
4
5
6
7
8
传输 → 传输缓冲区(TB) → 基本流缓冲区(EB) → 解码器 → 显示
(512字节) (可变大小)

关键参数:
TB大小:512字节(所有流共用)
EB大小:由profile/level决定(视频通常几Mbit,音频通常几Kbit)
缓冲区上溢:输入速率过快,需通过PCR控制
缓冲区下溢:输入速率过慢,需插入空包维持

七、关键特性与优势

1. 固定长度包结构(188字节)

  • 最核心特性,使接收端易于同步和错误检测
  • 丢包后仅影响单个包,不破坏整个流结构
  • 便于硬件实现和高速处理
  • 扩展版本:204字节(188+16字节FEC前向纠错码),进一步提升抗干扰能力

2. 强容错与恢复能力

  • 每个TS包独立可识别(同步字节0x47),即使流中出现错误也能快速重新同步
  • 连续计数器检测包丢失或顺序错误
  • 适配字段中的discontinuity指示帮助接收端处理流中断情况
  • CRC32校验确保PSI Section的完整性

3. 多节目复用能力

  • 单TS流可承载多个独立节目(如多套电视频道)
  • 每个节目有独立的PMT表,通过PID区分不同节目组件
  • 支持动态添加/移除节目(如数字电视中的频道切换)
  • 统计复用优化带宽利用率

4. 灵活的传输控制

  • PID机制实现精准流识别,可灵活选择需要的节目组件
  • 适配字段提供丰富的传输控制信息
  • 支持条件接收(加密),通过CAT表和EMM/ECM流实现付费内容管理

5. 实时性保障

  • 固定包结构和高效解析流程,适合低延迟场景
  • PCR间隔通常不超过100ms,确保快速时钟同步
  • 适合直播、视频会议等实时应用

八、与PS流及其他容器格式的对比

1. 与PS流(Program Stream)的核心区别

特性 TS流(Transport Stream) PS流(Program Stream)
包长度 固定188字节 可变长度(通常较大)
应用场景 实时传输(广播、IPTV、直播) 存储介质(DVD、蓝光、本地文件)
容错能力 ,适合不可靠信道 弱,适合无错误环境
节目支持 支持多路节目复用 通常用于单路节目
同步恢复 快速(基于同步字节和PCR) 较慢(依赖完整包结构)
典型应用 DVB、ATSC、IPTV、HLS切片、RTSP直播 DVD视频、本地媒体文件

2. 与其他常见容器格式的对比

特性 TS MP4 FLV MKV
包/单元长度 固定188字节 可变(Atom/Box结构) 可变(Tag结构) 可变(EBML结构)
实时性 (流式解析) 弱(依赖moov atom) 中(流式但延迟较高) 弱(需索引)
容错能力 (独立包) 弱(atom损坏影响全局)
多节目复用 原生支持 不支持 不支持 不支持
时间戳精度 90kHz(PTS/DTS)+ 27MHz(PCR) 1ms(通常) 1ms 1ms
元数据 PSI/SI表 moov atom script tag EBML header
加密/DRM CAS/条件接收 FairPlay/Widevine 不原生支持 不原生支持
适用场景 广播/直播/传输 点播/存储/下载 直播(旧平台) 存储/归档
标准组织 ISO/IEC (MPEG-2) ISO/IEC (MPEG-4) Adobe Matroska

MP4的moov atom问题
MP4文件的元数据(moov atom)通常位于文件末尾,播放器必须下载整个moov atom才能开始播放。对于点播场景,可通过qt-faststart等工具将moov atom移至文件头部实现快速启动,但本质上MP4不适合实时流式传输。

FLV的局限性
FLV曾是网络直播的主流格式,但存在以下问题:不支持HEVC/H.265(需扩展)、时间戳精度有限、不支持多节目、Adobe已停止维护。目前逐步被HLS+TS/CMAF替代。

九、典型应用场景

  1. 数字电视广播:DVB(欧洲)、ATSC(北美)、ISDB(日本)等标准的核心传输格式
  2. IPTV:基于IP网络的电视服务,广泛采用TS流作为传输格式
  3. 视频直播
    • 网络直播平台的直播流封装
    • HLS(HTTP Live Streaming)协议的ts切片文件(HLS将TS流分割为2~10秒的小文件)
    • RTSP/RTP直播的常用封装格式
  4. 监控系统:安防摄像头、NVR等设备的视频传输和存储
  5. 卫星传输:高抗干扰要求的卫星电视广播
  6. 有线电视:数字有线电视网络的标准传输格式

TS over IP / RTP封装

在实际开发中,TS流通过IP网络传输是非常常见的场景,RFC 2250定义了RTP承载MPEG-2 TS的标准。

RTP+TS封装格式

1
2
3
4
5
6
7
8
┌──────────────┬──────────────┬──────────────┬─────┬──────────────┐
│ IP Header │ UDP Header │ RTP Header │ ... │ TS Packets │
│ (20字节) │ (8字节) │ (12字节) │ │ (N×188字节) │
└──────────────┴──────────────┴──────────────┴─────┴──────────────┘

RTP Payload:通常包含7个TS包(7×188=1316字节)
原因:1316 + 12(RTP) + 8(UDP) + 20(IP) = 1356字节 < 1500字节(MTU)
避免IP分片,确保传输效率

RTP头中的关键信息

  • RTP序号:检测丢包和排序
  • RTP时间戳:基于90kHz时钟(与PTS同频),指示第一个TS包的发送时间
  • M标记位:通常不用(RFC 2250未定义特殊含义)

TS over IP的MTU考虑

  • 以太网MTU=1500字节,扣除IP+UDP+RTP头后,有效载荷最大约1460字节
  • 7个TS包=1316字节,是最常见的打包数量
  • 8个TS包=1504字节,超过MTU,会触发IP分片,应避免
  • 在MTU更大的网络(如jumbo frame)中,可打包更多TS包

TS over UDP vs TS over TCP

特性 UDP TCP
延迟 高(重传机制)
可靠性 不保证 保证
适用场景 实时直播 文件传输、VOD
典型协议 RTP/UDP HTTP/TCP(HLS)

十、C++流媒体开发关键要点

1. TS包解析流程(接收端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void parse_ts_packet(uint8_t* ts_packet) {
if (ts_packet[0] != 0x47) {
return;
}

bool transport_error = (ts_packet[1] & 0x80) != 0;
bool payload_start = (ts_packet[1] & 0x40) != 0;
bool priority = (ts_packet[1] & 0x20) != 0;
uint16_t pid = ((ts_packet[1] & 0x1F) << 8) | ts_packet[2];
uint8_t scrambling_control = (ts_packet[3] >> 6) & 0x03;
uint8_t adaptation_field_control = (ts_packet[3] >> 4) & 0x03;
uint8_t continuity_counter = ts_packet[3] & 0x0F;

if (transport_error) {
return;
}

if (pid == 0x1FFF) {
return;
}

int pos = 4;
if (adaptation_field_control & 0x02) {
uint8_t af_length = ts_packet[pos++];
if (af_length > 0) {
uint8_t flags = ts_packet[pos];
if (flags & 0x10) {
uint64_t pcr_base = ((uint64_t)(ts_packet[pos + 1]) << 25) |
((uint64_t)(ts_packet[pos + 2]) << 17) |
((uint64_t)(ts_packet[pos + 3]) << 9) |
((uint64_t)(ts_packet[pos + 4]) << 1) |
((ts_packet[pos + 5] >> 7) & 0x01);
uint16_t pcr_ext = (((ts_packet[pos + 5] & 0x01) << 8) |
ts_packet[pos + 6]);
uint64_t pcr = pcr_base * 300 + pcr_ext;
update_system_clock(pcr);
}
}
pos += af_length;
}

if (adaptation_field_control & 0x01) {
if (pid == 0x0000) {
parse_pat(ts_packet + pos, 188 - pos, payload_start);
} else if (is_pmt_pid(pid)) {
parse_pmt(ts_packet + pos, 188 - pos, payload_start);
} else if (is_video_pid(pid) || is_audio_pid(pid)) {
add_to_pes_buffer(pid, ts_packet + pos, 188 - pos, payload_start);
if (is_pes_complete(pid)) {
PesPacket* pes = get_complete_pes(pid);
decode_pes(pes);
}
}
}
}

2. PAT/PMT解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
struct ProgramInfo {
uint16_t program_number;
uint16_t pmt_pid;
};

struct StreamInfo {
uint8_t stream_type;
uint16_t elementary_pid;
std::vector<uint8_t> es_info;
};

std::vector<ProgramInfo> program_map;

void parse_pat(uint8_t* data, int length, bool payload_start) {
int offset = 0;
if (payload_start) {
uint8_t pointer_field = data[offset++];
offset += pointer_field;
}

uint8_t table_id = data[offset++];
if (table_id != 0x00) return;

uint16_t section_length = ((data[offset] & 0x0F) << 8) | data[offset + 1];
offset += 2;

uint16_t transport_stream_id = (data[offset] << 8) | data[offset + 1];
offset += 2;

uint8_t version_number = (data[offset] >> 1) & 0x1F;
bool current_next = data[offset] & 0x01;
offset++;

uint8_t section_number = data[offset++];
uint8_t last_section_number = data[offset++];

program_map.clear();
int program_data_end = offset + section_length - 5 - 4;
while (offset < program_data_end) {
ProgramInfo info;
info.program_number = (data[offset] << 8) | data[offset + 1];
info.pmt_pid = ((data[offset + 2] & 0x1F) << 8) | data[offset + 3];
offset += 4;
if (info.program_number != 0) {
program_map.push_back(info);
}
}
}

void parse_pmt(uint8_t* data, int length, bool payload_start) {
int offset = 0;
if (payload_start) {
uint8_t pointer_field = data[offset++];
offset += pointer_field;
}

uint8_t table_id = data[offset++];
if (table_id != 0x02) return;

uint16_t section_length = ((data[offset] & 0x0F) << 8) | data[offset + 1];
offset += 2;

uint16_t program_number = (data[offset] << 8) | data[offset + 1];
offset += 2;

uint8_t version_number = (data[offset] >> 1) & 0x1F;
offset++;

uint8_t section_number = data[offset++];
uint8_t last_section_number = data[offset++];

uint16_t pcr_pid = ((data[offset] & 0x1F) << 8) | data[offset + 1];
offset += 2;

uint16_t program_info_length = ((data[offset] & 0x0F) << 8) | data[offset + 1];
offset += 2 + program_info_length;

int stream_end = offset + section_length - 9 - 4;
while (offset < stream_end) {
StreamInfo si;
si.stream_type = data[offset++];
si.elementary_pid = ((data[offset] & 0x1F) << 8) | data[offset + 1];
offset += 2;
uint16_t es_info_length = ((data[offset] & 0x0F) << 8) | data[offset + 1];
offset += 2;
if (es_info_length > 0) {
si.es_info.assign(data + offset, data + offset + es_info_length);
offset += es_info_length;
}
}
}

3. PES重组状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
enum class PesState { WAITING_START, COLLECTING, COMPLETE };

struct PesBuffer {
PesState state = PesState::WAITING_START;
std::vector<uint8_t> data;
uint16_t expected_length = 0;
};

std::map<uint16_t, PesBuffer> pes_buffers;

void add_to_pes_buffer(uint16_t pid, uint8_t* data, int length, bool payload_start) {
auto& buf = pes_buffers[pid];

if (payload_start) {
if (buf.state == PesState::COLLECTING && buf.data.size() > 0) {
process_complete_pes(pid, buf.data);
}
buf.data.clear();
buf.state = PesState::COLLECTING;

if (length >= 6) {
uint8_t stream_id = data[3];
buf.expected_length = ((data[4] << 8) | data[5]);
}
}

if (buf.state == PesState::COLLECTING) {
buf.data.insert(buf.data.end(), data, data + length);

if (buf.expected_length > 0) {
uint16_t pes_payload_size = buf.expected_length + 3;
if (buf.data.size() >= pes_payload_size + 6) {
buf.state = PesState::COMPLETE;
process_complete_pes(pid, buf.data);
buf.data.clear();
buf.state = PesState::WAITING_START;
}
}
}
}

void process_complete_pes(uint16_t pid, const std::vector<uint8_t>& pes_data) {
if (pes_data.size() < 9) return;

uint8_t stream_id = pes_data[3];
uint16_t pes_packet_length = (pes_data[4] << 8) | pes_data[5];

uint8_t pts_dts_flags = (pes_data[7] >> 6) & 0x03;
uint8_t pes_header_data_length = pes_data[8];

int64_t pts = -1, dts = -1;
int offset = 9;

if (pts_dts_flags >= 2) {
pts = ((int64_t)(pes_data[offset] & 0x0E) << 29) |
((int64_t)pes_data[offset + 1] << 22) |
((int64_t)(pes_data[offset + 2] & 0xFE) << 14) |
((int64_t)pes_data[offset + 3] << 7) |
((int64_t)(pes_data[offset + 4] & 0xFE) >> 1);
offset += 5;

if (pts_dts_flags == 3) {
dts = ((int64_t)(pes_data[offset] & 0x0E) << 29) |
((int64_t)pes_data[offset + 1] << 22) |
((int64_t)(pes_data[offset + 2] & 0xFE) << 14) |
((int64_t)pes_data[offset + 3] << 7) |
((int64_t)(pes_data[offset + 4] & 0xFE) >> 1);
}
}

int es_data_offset = 9 + pes_header_data_length;
int es_data_length = pes_data.size() - es_data_offset;

decode_es(pid, stream_id, pts, dts, pes_data.data() + es_data_offset, es_data_length);
}

4. 同步恢复机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define SYNC_BYTE 0x47
#define TS_PACKET_SIZE 188

int find_sync_byte(uint8_t* buffer, int length) {
for (int i = 0; i <= length - TS_PACKET_SIZE; i++) {
if (buffer[i] == SYNC_BYTE) {
bool sync_ok = true;
for (int j = 1; j < 4 && sync_ok; j++) {
if (i + j * TS_PACKET_SIZE < length) {
if (buffer[i + j * TS_PACKET_SIZE] != SYNC_BYTE) {
sync_ok = false;
}
}
}
if (sync_ok) return i;
}
}
return -1;
}

bool check_continuity(uint16_t pid, uint8_t new_cc) {
static std::map<uint16_t, uint8_t> last_cc;
auto it = last_cc.find(pid);
if (it != last_cc.end()) {
uint8_t expected = (it->second + 1) & 0x0F;
if (new_cc != expected) {
return false;
}
}
last_cc[pid] = new_cc;
return true;
}

5. 关键开发要点

  1. 同步恢复机制

    • 实现基于0x47同步字节的滑动窗口检测(至少连续4个188字节间隔出现0x47才确认同步)
    • 处理连续计数器中断,检测包丢失
    • 同步丢失后进入搜索模式,重新定位同步字节
  2. PSI表解析

    • 优先解析PAT→PMT,建立节目与PID的映射关系
    • 处理PSI Section的分段(Section可能跨多个TS包)和更新
    • 监测version_number变化,及时更新节目映射
    • 验证CRC32确保Section完整性
  3. PCR处理

    • 实现高精度PCR时钟恢复(PLL或线性插值),误差控制在几微秒内
    • 处理PCR间隔和不连续性(discontinuity_indicator),避免时钟漂移
    • PCR抖动补偿:使用缓冲和平滑算法消除网络传输引入的PCR抖动
  4. PES重组

    • 根据payload_start_indicator识别PES包起始
    • 处理PES包跨多个TS包的情况,正确重组完整PES包
    • 解析PES包头中的PTS/DTS时间戳
    • 处理PES包长度为0的情况(视频流常见)
  5. 错误处理策略

    • 忽略传输错误指示置位的TS包
    • 处理PID错误、包顺序错误等异常情况
    • 实现流中断后的快速恢复机制
    • CRC32校验失败的PSI Section应丢弃
  6. 性能优化

    • 使用内存池管理TS包和PES包,减少内存分配开销
    • 采用SIMD指令(如SSE/AVX)加速TS包解析和同步字节搜索
    • 多线程处理:解析线程、解码线程、渲染线程分离
    • PID过滤:尽早过滤不需要的PID,减少后续处理负担

十一、扩展应用与C++开发工具

1. 常用TS处理工具库

工具库 功能 适用场景
libavformat (FFmpeg) 完整的TS读写、复用/解复用 音视频转码、播放器开发
libts 轻量级TS解析库 嵌入式设备、资源受限场景
GStreamer 流媒体框架,支持TS插件 复杂流媒体应用、多格式处理
tsduck 专业TS分析工具库 广播级TS流分析、监控

2. 常用调试与分析命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 使用FFprobe分析TS流
ffprobe -show_streams -show_programs input.ts

# 使用tsduck分析TS流结构
tsinfo input.ts

# 分析PSI/SI表
tsdump input.ts

# 分析PCR间隔和抖动
tsanalyze input.ts

# 使用FFmpeg将MP4转为TS
ffmpeg -i input.mp4 -c copy -f mpegts output.ts

# 生成HLS切片
ffmpeg -i input.mp4 -c copy -f hls -hls_time 10 -hls_list_size 0 output.m3u8

# 将RTMP流转为TS流
ffmpeg -i rtmp://server/live/stream -c copy -f mpegts output.ts

3. 典型应用案例

  1. IPTV机顶盒

    • 解析TS流,提取多节目信息
    • 实现频道切换(快速PID过滤)
    • 音视频同步播放
  2. 直播流媒体服务器

    • 将RTMP/HTTP流封装为TS流
    • 支持HLS切片(生成.ts切片文件和.m3u8索引)
    • 多比特率自适应流(ABR)实现
  3. 视频监控系统

    • 实时封装摄像头视频为TS流
    • 支持多路视频复用传输
    • 存储TS流文件,支持快速回放

总结

MPEG-2 TS作为流媒体传输领域的基石技术,以其固定包长、强容错性、多节目复用精准同步等特性,成为广播电视、IPTV、直播等领域的标准封装格式。对于C++流媒体开发者而言,深入理解TS流的结构和机制,掌握高效的解析和处理方法,是开发高性能、高可靠性流媒体应用的核心能力。

TS流的设计理念——在不可靠环境中追求可靠传输——对于今天的网络直播、5G视频传输等场景依然具有重要指导意义,其核心技术思想也被后续的SRT、WebRTC等传输协议所借鉴。