TS数据流的解析
文档修改信息
版本 | 作者 | 时间 | 备注 |
---|---|---|---|
1.0 | 李海杰 | 2022-03-07 | 搬运到CSDN并修改相关数据 |
1 TS 数据流简介
简单来说,数字机顶盒是能够接收到一段段的码流,这样的码流我们可以称之为TS(Transport Stream,传输流),每个TS流里都携带的有一些信息,例如Video、Audio、PAT、PMT等。
1.1 TS 流的构成
TS 流是一种用于数据传输的单一传输流,它是由一些具有共同时间基准或独立时间基准的一个或多个PES复合而成的。
在这里我们需要知道一些其他数据流:
- ES流(Elementary Stream) 基本码流,不分段的音频、视频或其他信息的连续码流。
- PES流 把基本流ES分割成段,并加上相应头文件打包成形的打包基本码流。
- PS流(Program Stream) 节目流,将具有共同时间基准的一个或多个PES组合(复合)而成的单一数据流(用于播放或编辑系统,如m2p)。
1.2 TS 流的产生
-
从上图可以看出,视频ES和音频ES通过打包器和共同或独立的系统时间基准形成一个个PES,通过TS复用器复用形成的传输流。注意这里的TS流是位流格式,也就是说TS流是可以按位读取的。
-
在实际传输中,PES包会被切割后装载到TS包中,上图就是PES和TS之间的关系图。
注意:
- 一个PES包可以装载到不同的TS包。
- 每一个TS包必须只含有从一个PES来的数据。
- PES包头必须跟在TS包的链接头后面。
- 对于一个特定的PES,最后一个TS包可以含有填充比特。
1.3 TS 流的格式
TS流是基于Packet的位流格式,每个包的长度是固定的188个字节(或者204个字节,原因是在188个字节后加上了16个字节的CRC校验数据,其他格式一样)。整个TS流组成形式如下:
- 包头信息
Packet Header(包头)信息说明 | 标识 | 位数 | 说明 |
---|---|---|---|
1 | sync_byte | 8bits | 同步字节 |
2 | transport_error_indicator | 1bit | 错误指示信息(1:该包至少有1bits传输错误) |
3 | payload_unit_start_indicator | 1bit | 负载单元开始标志(packet不满188字节时需填充) |
4 | transport_priority | 1bit | 传输优先级标志(1:优先级高) |
5 | PID | 13bit | Packet ID号码,唯一的号码对应不同的包 |
6 | transport_scrambling_control | 2bits | 加密标志(00:未加密;其他表示已加密) |
7 | adaptation_field_control | 2bits | 附加区域控制 |
8 | continuity_counter | 4bits | 包递增计数器 |
- 上面表格是一个包(Package)的头(Header)的说明,其中需要注意的是:PID是TS流中唯一识别标志,Packet Data是什么内容就是由PID决定的。 如果一个TS流中的一个Packet的Packet Header中的PID是0x0000,那么这个Packet的Packet Data就是DVB的PAT表而非其他类型数据(如Video、Audio或其他业务信息)。
TS流中PID的分配 | ||
---|---|---|
表 | PID值 | 说明 |
PAT | 0x0000 | - |
CAT | 0x0001 | - |
TSDT | 0x0002 | - |
预留 | 0x0003 至0x000F | 无 |
NIT, ST | 0x0010 | - |
SDT , BAT, ST | 0x0011 | - |
EIT, ST | 0x0012 | - |
RST, ST | 0x0013 | - |
TDT, TOT, ST | 0x0014 | - |
网络同步 | 0x0015 | 无 |
预留使用 | 0x0016 至 0x001B | 无 |
带内信令 | 0x001C | 无 |
DIT | 0x001E | 无 |
SIT | 0x001F | 无 |
- 上面是一些表的PID值,这些值是固定的,不允许更改。
- 包数据结构
- TS流的包数据是承载实际数据的区域,根据不同的PID,有不同的解析方式。
1.4 参考文档
1.5 小结
-
TS流是一种位流(当然就是数字的), 它是由ES流分割成PES后复用而成的;它经过网络传输被机顶盒接收到; 数字电视机顶盒接收到TS流后将解析TS流。
-
TS流是由一个个Packet(包)构成的, 每个包都是由Packet Header(包头)和Packet Data(包数据)组成的。 其中Packet Header指示了该Packet是什么属性的,并给出了该Packet Data的数据的唯一网络标识符PID。
-
TS流和PS流的区别:TS流的包结构是长度是固定的;PS流的包结构是可变长度的。 这导致了 TS流的抵抗传输误码的能力强于PS流 (TS码流由于采用了固定长度的包结构, 当传输误码破坏了某一TS包的同步信息时,接收机可在固定的位置检测它后面包中的同步信息,从而恢复同步,避免了信息丢失。 而PS包由于长度是变化的,一旦某一 PS包的同步信息丢失, 接收机无法确定下一包的同步位置,就会造成失步,导致严重的信息丢失。 因此,在信道环境较为恶劣,传输误码较高时,一般采用TS码流;而在信道环境较好,传输误码较低时,一般采用PS码流)。由于TS码流具有较强的抵抗传输误码的能力,因此目前在传输媒体中进行传输的MPEG-2码流基本上都采用了TS码流的包格。
2 TS数据流包头的解析
上面我们对TS数据流进行了简单介绍,了解到了TS数据流的基本结构和组成部分,那么我们就可以着手对TS数据流的解析工作。
2.1 解析步骤
在对TS数据流包头进行分析的时候我们主要获取的信息有三点:数据包长度、PID值、负载单元开始标志。所以我们可以按下面步骤来进行初步解析:
- 获取TS数据流包长度信息。
- 得到TS数据流包头PID信息。
- 确定每个数据包的负载单元开始标志。
2.2 包长解析
我们清楚,TS数据包长度是固定的,188字节或者204字节。那么我们在对数据包长度进行解析时,就只需获取一段完整的TS数据包,然后来判断它是188字节还是204字节。在这里我是采用if语句来进行判断。
首先第一步获取一段完整的TS数据包。因为每一段TS数据包的包头第一个字节都是0x47,那么我们只用找到这个标志位就能开始下一步判断。
int ParseLen(FILE *pTsFile, int TsPosition)
{
int PackageLength = 0;
while(fgetc(pTsFile) != EOF)
{
if(fgetc(pTsFile) == 0x47)
{
if(JudgmentPackageLength(pTsFile, TsPosition) == 204)
{
PackageLength = 204;
break;
}
if(JudgmentPackageLength(pTsFile, TsPosition) == 188)
{
PackageLength = 188;
break;
}
}
}
return PackageLength;
}
- 我们先通过fopen()函数来打开TS文件,并保存文件流指针。然后用fgetc()函数来读取文件中的每个字节数据,再加上while()循环语句来实现遍历读取。当我们遇到为0x47的数据时,就调用一个判断函数来进行下一步的判断。
for(Time = 0; Time < 10; Time++)
{
if(fseek(pTsFile, TsPosition - 1, SEEK_CUR) == -1)
{
printf("fseek error!");
return value;
}
if(feof(pTsFile))
{
value = -1;
return value;
}
fseek(pTsFile,188,SEEK_CUR);
PackageByte = fgetc(pTsFile);
if(PackageByte == 0x47)
{
value = 188;
}
else
{
fseek(pTsFile,203,SEEK_CUR);
PackageByte = fgetc(pTsFile);
if(PackageByte == 0x47)
{
value = 204;
}
else
{
value = -1;
}
}
}
- 在判断函数中我是采用从当前0x47位置向后偏移188字节或者204字节的方式来判断下一段数据是否为0x47。如果是,说明是一段完整数据包;如果不是,说明我们并未找到包头,那么继续遍历。
- 在这里我们为了排除偶然情况,我们可以选择利用for()函数进行一定次数的循环,往后多移动几次来确定所获取的的确是一段完整的TS数据包。
- 通过这几步骤的循环判断,我们就可解析出我们的数据包长度是188字节还是204字节。
2.3 PID解析
当我们解析出一段完整的数据包后就可以利用一个数组来对这一段完整的数据进行保存,方便我们后面的解析。对包头解析时我们需要知道我们的包头结构,每个字节代表什么含义,每个数据有占有多少字节位数。下面就是我们的包头结构表:
Packet Header(包头)信息说明 | 标识 | 位数 | 说明 |
---|---|---|---|
1 | sync_byte | 8bits | 同步字节 |
2 | transport_error_indicator | 1bit | 错误指示信息(1:该包至少有1bits传输错误) |
3 | payload_unit_start_indicator | 1bit | 负载单元开始标志(packet不满188字节时需填充) |
4 | transport_priority | 1bit | 传输优先级标志(1:优先级高) |
5 | PID | 13bit | Packet ID号码,唯一的号码对应不同的包 |
6 | transport_scrambling_control | 2bits | 加密标志(00:未加密;其他表示已加密) |
7 | adaptation_field_control | 2bits | 附加区域控制 |
8 | continuity_counter | 4bits | 包递增计数器 |
我们可以看出我们所想获取到的PID是包头的第2个字节的5bits加上第3个字节的8bits,那么我们可以先创建一个结构体,来对包头数据进行存储。
typedef struct TS_Package_Head
{
unsigned sync_byte :8;
unsigned transport_error_indicator :1;
unsigned payload_unit_start_indicator :1;
unsigned transport_priority :1;
unsigned PID :13;
unsigned transport_scrambling_control :2;
unsigned adaptation_field_control :2;
unsigned continuity_counter :4;
}TS_Package_Head;
- 因为我们知道,TS数据包的包头是固定4个字节。那么我们就将之前所保存的数据包数据取出前四个字节来放入结构体中。这样一来我们就能够通过结构体来获取到我们该段数据包的PID数据。
TS_Package_Head ParseTS_PackageHead(TS_Package_Head TS_PackageHead, unsigned char PackageHead_data[4])
{
TS_PackageHead.sync_byte = PackageHead_data[0];
TS_PackageHead.transport_error_indicator = PackageHead_data[1] >> 7;
TS_PackageHead.payload_unit_start_indicator = (PackageHead_data[1] >> 6) & 0x01;
TS_PackageHead.transport_priority = (PackageHead_data[1] >> 5) & 0x01;
TS_PackageHead.PID = ((PackageHead_data[1] & 0x1F) << 8) | PackageHead_data[2];
TS_PackageHead.transport_scrambling_control = PackageHead_data[3] >> 6;
TS_PackageHead.adaptation_field_control = (PackageHead_data[3] >> 4) & 0x03;
TS_PackageHead.continuity_counter = PackageHead_data[3] & 0x0f;
return TS_PackageHead;
}
- PID是不同的表之间的重要标识,所以我们可以通过PID来获取到我们所需要解析的表的数据包。
2.4 负载单元开始标志解析
同PID获取方式一样,我们所保存的结构体里payload_unit_start_indicator就是负载单元开始的标志。通俗来说:
- 当该位置为’1’时,说明这段数据包含有该pid表的首张数据表,并且有效数据从该数据包的第五个字节开始。
- 当该位置为’0’时,说明这段数据包不包含该pid表的首张数据表,而是该pid表的后续数据。有效数据从该数据包的第四个字节开始。
- 这个标志位是我们获取pid表的第一张表的有效标志。
2.5 小结
对于包头的解析是我们分析一段数据包的开始,我们可以从解析出来的数据中可以得知该段数据包是不是我们所需要的数据包。在这里我们需要注意的问题就是:
- 我们对数据进行遍历的时候,可以将所获的一段完整数据包给单独提取出来进行保存。这样我们在进行解析的时候就可以对这段保存的数据来进行操作,避免了反复移动文件指针来进行数据的操作,极大的方便了我们代码的可阅读性。
- 在进行下一步解析之前我们就应该构思好我们代码的初步框架,避免后来出现代码逻辑混乱和反复循环读取数据的情况。
- 包头的PID,payload_unit_start_indicator, continuity_counter,这三个重要数据是我们判断一个完整数据包的依据。
3 PAT表的解析
PAT表是我们解析TS流的起点,也是机顶盒接收的入口点,使它获取数据的开始。PAT表定义了我们当前TS流中所有的节目,其PID固定为0x0000。它是PSI的根节点,要查询节目必须从PAT表开始。
3.1 解析步骤
- 寻找到PID为0x0000的TS数据包
- 根据解析出来的负载单元开始标志,确定数据包除包头外的有效数据起始位置,将有效数据提取保存。
- 将获取的有效数据存入PAT表的对应结构体中,方便保存。
3.2 PAT表的描述
- PAT表主要携带有以下信息:
TS流ID | transport_stream_id | 该ID标志唯一的流ID |
---|---|---|
节目频道号 | program_number | 该号码标志TS流中的一个频道,该频道可以包含很多的节目(即可以包含多个Video PID和Audio PID) |
PMT的PID | program_map_PID | 表示本频道使用哪个PID做为PMT的PID,因为可以有很多的频道,因此DVB规定PMT的PID可以由用户自己定义 |
- PAT表的结构:
# | 字段名 | 占位 | 次序 | 说明 |
---|---|---|---|---|
0* | table_id | 8 bits | 第0个字节 | PAT的table_id只能是0x00 |
1* | section_syntax_indicator | 1 bit | 第1、2个字节 | 段语法标志位,固定为1 |
2* | zero | 1 bit | 第1、2个字节 | |
3* | reserved | 2 bits | 第1、2个字节 | |
4* | section_length | 12 bits | 第1、2个字节 | 意思是 段长度为29字节 |
5* | transport_stream_id | 16 bits | 第3、4个字节 | TS的识别号 |
6* | reserved | 2 bits | 第5个字节 | TS的识别号 |
7* | version_number | 5 bits | 第5个字节 | 一旦PAT有变化,版本号加1 |
8* | current_next_indicator | 1 bit | 第5个字节 | 当前传送的PAT表可以使用,若为0则要等待下一个表 |
9* | section_number | 4 bits | 第6个字节 | 给出section号,在sub_table中, 第一个section其section_number为"0x00", 每增加一个section,section_number加一 |
10* | last_section_number | 4 bits | 第7个字节 | sub_table中最后一个section的section_number |
循环开始(循环内的数据解析见下一节内容!) | ||||
- | program_number | 16 bits | – | 节目号 |
- | reserved | 3 bits | – | - |
- | network_id 或 program_map_PID | 13 bits | – | program_number为0x0000时, 这里是network_id(NIT的PID); 其余情况是program_map_PID(PMT的PID) |
循环结束 |