【音视频】TS协议解析
参考博客:https://blog.csdn.net/rell336/article/details/38109621?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param
一、TS协议
1.1 TS流和其他流的关系
1. ES(Elementary Stream,基本码流)
- 定义:未经分段的原始媒体流,是连续的音频、视频或其他信息(如字幕)的原始数据序列。
- 特点:
- 不分段,以连续码流形式存在。
- 是最底层的媒体数据,直接由编码器生成(如H.264视频流、AAC音频流)。
- 不包含传输控制信息,无法直接用于传输。
2. PES(Packetized Elementary Stream,分组的基本码流)
- 定义:将ES流分割为长度可变的数据包,并添加包头后形成的流,用于承载ES流。
- 作用:为ES流添加传输所需的控制信息,实现ES流的分组化传输。
- 结构:
- PES包头:包含时间戳(PTS/DTS)、数据长度、流类型等信息。
- PES有效载荷:分割后的ES流片段。
- 特点:
- 数据包长度可变,适合在可靠环境(如本地存储)中使用。
3. TS(Transport Stream,传输流)
- 定义:由固定长度(188字节)的数据包组成,用于在不可靠环境(如广播电视、网络传输)中传输多个节目。
- 构成:
- 包含一个或多个节目,每个节目由音频、视频等ES流通过PES分组后封装而成。
- 插入PSI/SI表(节目特定信息/服务信息),用于描述节目结构和服务信息。
- 关键特性:
- 固定包长:每个TS包为188字节(含4字节包头),便于硬件快速处理。
- 独立解码:从流中任意位置均可开始解码,容错性强。
- PSI/SI重复传输:PSI/SI表按一定频率重复发送,确保接收端能随时获取节目信息。
4. PS(Program Stream,节目流)
- 定义:用于传输单个节目,由可变长度数据包组成的流,适用于可靠存储或传输环境(如DVD光盘)。
- 与TS流的核心区别:
- 包长度:PS包长度可变,TS包长度固定(188字节)。
- 应用场景:PS流适合本地存储(如文件),TS流适合实时传输(如广播电视)。
四者关系
- ES流是原始媒体数据(如视频帧、音频采样)。
- PES流是ES流的分组形式,添加时间戳等控制信息。
- TS流/PS流是PES流的进一步封装:
- TS流:固定包长,含多节目和PSI/SI,适合传输。
- PS流:可变包长,单节目,适合存储。
简单来说,ES→PES→TS/PS的过程,是媒体数据从原始形式到可传输/存储形式的封装链。
1.2 TS包
TS包的⻓度:188 B或204 B,204 B⻓度是在188B后⾯增加了16 B的CRC校验数据。
-
sync_byte: 1B,固定值0x47,TS包的标识符,正常的TS包在0x47的包头标识符往后188/204B之后仍然是0x47【下⼀个TS包的标识符】
-
transport_error_Indicator: 1bit,当其为1时,表示该TS包中⾄少有⼀个不可纠正的错误位,只有在错误纠正之后,该位才能重新置0【实际获取TS包之后,该位为1的包丢弃】
-
payload_unit_start_indicator: 1bit,对于PSI数据包,该位为1时,表示该TS包是某个Section的第⼀个包,并且该包含有pointer_field,该变量的值意义在于,除了调整字段之外,往后pointer_field个字节开始,才是有效数据。对于空包来说,该值为0。
-
transport_priority: 1bit,表示传输优先级,对于相同PID的TS包,该字段置1的TS包拥有更⾼的优先级。PID:13bit,PID可以标识存储于TS包中有效净荷的数据的类型。PID⽤于TS包阶段⽤于鉴别各种PSI/SI信息表、电视节⽬,区分⾳视频的PES包等,是辨别码流信息性质的关键。
-
transport_scrambling_control:2bit,⽤来指示传送流包Payload的加扰⽅式。【传送流包⾸部包括调整字段,则不应被加扰;空包也不加扰。】
Transport_scrambling_control | 描述 |
---|---|
00 | 未加扰 |
01 | 用户定义 |
10 | 用户定义 |
11 | 用户定义 |
-
adaption_field_control:2bit,表示传送流包⾸部是否跟随调整字段/Payload【如果全部是调整字段则不含payload】
-
continuity_counter: 4bit,随着具有相同PID的TS包增加⽽增加,当它达到最⼤(31)时,⼜恢复为0,如果adaption_field_control = 00/10,该连续计数器不增加,因为不含payload。
1.3解析TS包
1.3.1获取包⻓
TS包的包⻓有两种——188B或者204B,在解析TS包之前,必须要先判断TS包包⻓,以便后续进⾏分析。
1. 标准固定长度
- 常规 TS 包:最常见的长度是 188 字节(包括 4 字节包头 + 184 字节负载)。这是 DVB(欧洲数字电视标准)、ATSC(美国数字电视标准)等系统的默认长度。
- 扩展长度:在某些场景(如 DVB-T2、ISDB-Tb)中,TS 包可能被封装到更大的帧结构中,此时 TS 包长度可能为 204 字节(188 字节 + 16 字节前向纠错码)。但这种情况下,核心 TS 包本身仍为 188 字节,只是外部添加了额外的保护机制。
2. 通过同步字节识别包边界
TS 包的起始位置由 同步字节(固定值 0x47
)标识。接收端通过检测连续的 0x47
来定位每个 TS 包的开始,并根据固定长度(如 188 字节)截取完整的包。例如:
0x47 [TS包头4字节] [负载184字节] 0x47 [下一个TS包...]
如果遇到非 0x47
的字节,则表示传输错误或包丢失。
具体流程如下图(以188B为例):
1.3.2 解析TS包头
在获取包⻓之后,就要对包头信息进⾏解析并获取有效数据,需要定义⼀个结构体存储数据:
/*TS包包头的结构体*/
typedef struct CSTSPacketHeader_S
{BYTE ucSyncByte; //TS包的标识符BYTE ucTransport_error_indicator; //传输错误指示器,当值为时,表示该包有误BYTE ucPayload_unit_start_indicator; //有效净荷开始标记// 注:如果想要节省存储空间,可以使用位域的方式定义结构体。BYTE——unsigned char,Word——unsigned short intBYTE ucTransport_priority; //传输的优先级WORD wPID; //TS包的ID,用于区分不同的sectionBYTE ucTransport_scrambling_control; //指示ts传送流包有效净荷的加扰方式BYTE ucAdaptation_field_control; //指示是否有调整字段和有效净荷BYTE ucContinuity_counter; //随着相同PID TS包的增加而增加
} TSPacketHeader;
假定包⻓为188,我们每获取⼀个TS包,就装进⼀个⻓度为188的BYTE型数组⾥,前4个BYTE就是包头的数据了,获取数据可以逻辑与,左右移,逻辑或的⽅法进⾏,具体例⼦如下:
pstTSHeader->wPID = ((pucTSBuffer[1]& 0x1f) << 8) | pucTSBuffer[2];
1.3.3 判断TS包的有效性
在⼀个码流中,并不全部都是有效的TS包,需要将⼀些⽆效TS包剔除
⽆效TS包的情况分为五种:
- 该TS包往后188B不是0x47的包头标识符(TS包都是连续发送,如果出现包不连续的地⽅,说
明该包数据传送时出错); - TS包存在错误,即transport_error_Indicator的值为1;
- TS包全是调整字段(空包),即adaption_field_control的值为10(⼆进制);
- TS包的调整字段属于保留的情况,即adaption_field_control的值为00(⼆进制);
- TS包被加扰,即transport_scrambling_control不为00,(如果有做解扰可以去掉这种情况);
对于这五种情况的TS包我们⼀律丢弃,直接获取下⼀个TS包。
1.3.4 确定payload的起始位置
1. 定位 TS 包起始
每个 TS 包以 同步字节(固定值 0x47
)开始。找到同步字节后,后续的 187 字节属于当前 TS 包。
2. 解析 TS 包头(4 字节)
TS 包头的结构如下:
字节0: 同步字节 (0x47)
字节1: TEI(1) | PUSI(1) | TP(1) | PID(13)
字节2: PID(继续)
字节3: TSC(2) | AFC(2) | CC(4)
其中关键字段:
- AFC (Adaptation Field Control):位于字节 3 的第 4-5 位(2 比特),指示自适应区和有效载荷的存在情况:
01
:无自适应区,仅有效载荷(Payload 从第 5 字节开始)。10
:仅有自适应区,无有效载荷(Payload 为空)。11
:自适应区后接有效载荷(需进一步计算 Payload 起始)。00
:保留值,不应使用。
3. 根据 AFC 计算 Payload 起始
情况 1:AFC = 01(仅有效载荷)
- Payload 起始位置:第 5 字节(即包头后的第一个字节)。
情况 2:AFC = 10(仅自适应区)
- 无有效载荷,TS 包仅包含包头和自适应区。
情况 3:AFC = 11(自适应区 + 有效载荷)
- 读取自适应区长度:
- 包头后的第 1 个字节(即第 5 字节)是 自适应区长度(
adaptation_field_length
),表示自适应区的总字节数(不包含自身)。
- 包头后的第 1 个字节(即第 5 字节)是 自适应区长度(
- 计算 Payload 起始:
- Payload 起始位置 = 包头(4 字节) + 自适应区长度字节(1 字节) + 自适应区内容(
adaptation_field_length
字节)。 - 公式:
Payload起始 = 5 + adaptation_field_length
。
- Payload 起始位置 = 包头(4 字节) + 自适应区长度字节(1 字节) + 自适应区内容(
4. 示例计算
假设 TS 包数据如下(十六进制):
47 40 00 11 05 FF 00 01 02 03 61 62 63 ...
解析步骤:
- 同步字节:
47
→ 确认 TS 包起始。 - AFC 字段:字节 3 为
11
→ 二进制0001 0001
→ AFC =11
(自适应区 + 有效载荷)。 - 自适应区长度:第 5 字节为
05
→ 自适应区内容长度为 5 字节。 - Payload 起始:5(包头 + 长度字节) + 5(自适应区内容) = 第 10 字节(
61
)。
二、 Section
2.1 Section的概念
-
⼀个TS数据包的最⼤净荷为184个字节,当⼀个PSI/SI表的字节⻓度⼤于184字节时,就要对这个表进⾏分割,形成段(section)来传送。
-
分段机制主要是将⼀个数据表分割成多个数据段。 在PSI/SI表到TS包的转换过程中,段起到了中介的作⽤。由于⼀个数据包只有188字节,⽽段的⻓度是可变的,EIT表的段限⻓4096字节,其余PSI/SI表的段限⻓为1024字节。因此,⼀个段要分成⼏部分插⼊到TS包的payload中。
-
从TS码流中可以获取到TS包,TS包要组成Section,才能提取到想要的信息,所以⾸先要懂得怎么组section。
组Section之前要了解TS包在码流中发送的⼀些情况:
- TS包发送的时候PID是⽆序的,连续的TS包的PID可能都是不⼀样的;
- TS包发送的时候Section是相对有序的,也就是说,对于同⼀个PID的TS包,只有发完了⼀个Section,才会发送下⼀个Section,不然⽆法区分该TS包属于哪⼀个Section,并且对于这个Section,TS包是有序发送的,否则数据会被打乱;
- 某个Section的第⼀个TS包有PSI/SI表的⼀些表头信息(table_id,section_length等信息),我称之为SectionHeader,后⾯的TS包就没有,所以接收某个Section必须先拿到⾸包。
2.2 TS包组Section
-
TS包组section⾸先要找到该section的第⼀个TS包(下⾯简称为⾸包),⾸包含有该section的⻓度,可以⽤来判断⼀个section是不是组完了。
-
通过判断TS包包头中的Payload Unit Start Indicator,该值为1的话,就说明这个TS包是⾸包,可以开始组⼀个section,⾸包含有Section的头部,结构类似下表。
说明:uimsbf
表示无符号整数,最高位在前;bslbf
表示二进制符号位在前(布尔类或标志位常用) ,可用于解析 MPEG - 2 传输流中节目关联段(PAT)的结构。
字段名 | 位宽 | 编码类型 | |
---|---|---|---|
table_id | 8 | uimsbf | |
section_syntax_indicator | 1 | bslbf | |
‘0’ | 1 | bslbf | |
reserved | 2 | bslbf | |
section_length | 12 | uimsbf | |
transport_stream_id | 16 | uimsbf | |
reserved | 2 | bslbf | |
version_number | 5 | uimsbf | |
current_next_indicator | 1 | bslbf | |
section_number | 8 | uimsbf | |
last_section_number | 8 | uimsbf |
拿到⾸包之后,要获取section的⻓度,有效数据的第⼆个字节的后四位和第三个字节组成⼀个12bit的字段,该值就是section_length后⾯数据的⻓度,如果算上前⾯三个字节,整个section的⻓度就是section_length += 3
。
将section_length和TS包有效⻓度进⾏对⽐,
- 如果section_length > TS包的有效数据,证明后⾯还有其他的TS包,将section_length减去TS包有效数据⻓度,获得剩余⻓度;
- 如果是section_length <= TS包的有效数据,证明该section已经结束了。
如果⼀个section还没组完,那么就要获取后续的TS包,后续的TS包应该是和原来相同PID,并且TS包头中continuity_counter要⽐原来的⼤1(31的话要变成0),拿到包后要与剩余⻓度进⾏对⽐,重复上⾯的步骤。
2.3 组多个Section和判全判重
对于⼀些PSI/SI表来说,由于数据较多,有时候不⽌⼀个section,怎么针对这个表将所有的section组全?
子表 Section 组包、判全与判重机制说明
在处理 PSI/SI 表(以 PAT 等为例)的分段 Section 时,需通过以下逻辑实现组包、判全与判重:
1. 子表 Section 数量与组包逻辑
每个子表的 Section 头部包含 last_section_number
字段,规则如下:
- 含义:标识当前子表最后一个 Section 的编号(
section_number
)。 - 数量计算:子表总 Section 数 =
last_section_number + 1
(因section_number
从 0 开始计数 )。 - 组包方式:以链表(或数组)存储同一子表的 Section,按
section_number
顺序拼接,还原完整子表数据。
2. 版本控制(判重核心逻辑)
每个 Section 头部包含 version_number
字段,用于标识子表版本:
- 版本变更:若新 Section 的
version_number
与已存储子表版本不一致,说明子表已更新,需:- 丢弃所有旧版本 Section。
- 重新初始化接收流程,采集新版本 Section。
- 版本未变:继续校验
section_number
,判断是否重复或缺失。
3. 判全与判重逻辑
通过 标记数组 跟踪 Section 接收状态,流程如下:
步骤 | 操作逻辑 | 关键判断 |
---|---|---|
1 | 初始化标记数组 | 以 last_section_number + 1 为长度,初始值全为 0 (未接收) |
2 | 接收新 Section | 解析其 section_number |
3 | 判重校验 | 若标记数组中 section_number 对应下标值为 1 ,说明该 Section 已接收过,丢弃 |
4 | 标记接收状态 | 若未重复,将标记数组中 section_number 对应下标值置为 1 |
5 | 判全校验 | 遍历标记数组,若下标 0 ~ last_section_number 对应值全为 1 ,说明子表所有 Section 已收全,可组装完整子表 |
更多资料:https://github.com/0voice