文档修改信息
1 TS 数据流简介
2 TS数据流包头的解析
3 PAT表的解析
4 PMT表的解析
5 SDT表的解析
6 NIT表的解析
7 描述符的解析
8 数据包的处理
9 Makefile的编写
10 总结
11 参考文献
版本 | 作者 | 时间 | 备注 |
---|---|---|---|
1.0 | 李海杰 | 2022-03-07 | 搬运到CSDN并修改相关数据 |
简单来说,数字机顶盒是能够接收到一段段的码流,这样的码流我们可以称之为TS(Transport Stream,传输流),每个TS流里都携带的有一些信息,例如Video、Audio、PAT、PMT等。
TS 流是一种用于数据传输的单一传输流,它是由一些具有共同时间基准或独立时间基准的一个或多个PES复合而成的。
在这里我们需要知道一些其他数据流:
从上图可以看出,视频ES和音频ES通过打包器和共同或独立的系统时间基准形成一个个PES,通过TS复用器复用形成的传输流。注意这里的TS流是位流格式,也就是说TS流是可以按位读取的。
在实际传输中,PES包会被切割后装载到TS包中,上图就是PES和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 | 包递增计数器 |
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 | 无 |
《En300468.V1.7.1_Specification for SI in DVB Systems》
《数字视频广播中文业务信息规范》
DVB和MPEG-II中的表格
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码流的包格。
上面我们对TS数据流进行了简单介绍,了解到了TS数据流的基本结构和组成部分,那么我们就可以着手对TS数据流的解析工作。
在对TS数据流包头进行分析的时候我们主要获取的信息有三点:数据包长度、PID值、负载单元开始标志。所以我们可以按下面步骤来进行初步解析:
我们清楚,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;
}
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;
}
}
}
当我们解析出一段完整的数据包后就可以利用一个数组来对这一段完整的数据进行保存,方便我们后面的解析。对包头解析时我们需要知道我们的包头结构,每个字节代表什么含义,每个数据有占有多少字节位数。下面就是我们的包头结构表:
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_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获取方式一样,我们所保存的结构体里payload_unit_start_indicator就是负载单元开始的标志。通俗来说:
对于包头的解析是我们分析一段数据包的开始,我们可以从解析出来的数据中可以得知该段数据包是不是我们所需要的数据包。在这里我们需要注意的问题就是:
PAT表是我们解析TS流的起点,也是机顶盒接收的入口点,使它获取数据的开始。PAT表定义了我们当前TS流中所有的节目,其PID固定为0x0000。它是PSI的根节点,要查询节目必须从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可以由用户自己定义 |
# | 字段名 | 占位 | 次序 | 说明 |
---|---|---|---|---|
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) |
循环结束 | ||||
- | CRC_32 | 32 bits | 最后4个字节 | - |
注意:
因为我们了解到了,所有PAT表的PID都固定是0x0000,所以在对PAT表获取时就只用利用if()判断语句来判断哪个数据包的PID是0x0000,即可找到PAT表。
if(fgetc(pTsFile) == 0x47)
{
fseek(pTsFile, -TsPackage_len, SEEK_CUR);
fseek(pTsFile, TsPosition - 1, SEEK_CUR);
fread(TSdata, 1, TsPackage_len, pTsFile);
TS_PackageHead = ParseTS_Type();
}
if(TS_PackageHead.PID == 0 && value == 0)
{
TS_PackagePAT = ParseTS_PAT(TsPackage_len, TS_PackageHead);
value++;
}
找到PAT表以后,我们就能够把我们的数据保存在结构体中。
TS_PAT TSpatTable(TS_PAT TS_PackagePAT, unsigned char PATtable[204])
{
int PAT_Length = 0;
TS_PackagePAT.table_id = PATtable[0];
TS_PackagePAT.section_syntax_indicator = PATtable[1] >> 7;
TS_PackagePAT.zero = (PATtable[1] >> 6) & 0x1;
TS_PackagePAT.reserved_1 = (PATtable[1] >> 4) & 0x3;
TS_PackagePAT.section_length = ((PATtable[1] & 0x0F) << 8) | PATtable[2];
TS_PackagePAT.transport_stream_id = (PATtable[3] << 8) | PATtable[4];
TS_PackagePAT.reserved_2 = PATtable[5] >> 6;
TS_PackagePAT.version_number = (PATtable[5] >> 1) & 0x1F;
TS_PackagePAT.current_next_indicator = (PATtable[5] << 7) >> 7;
TS_PackagePAT.section_number = PATtable[6];
TS_PackagePAT.last_section_number = PATtable[7];
PAT_Length = 3 + TS_PackagePAT.section_length;
TS_PackagePAT.CRC_32 = (PATtable[PAT_Length - 4] << 24) | (PATtable[PAT_Length - 3] << 16)
| (PATtable[PAT_Length - 2] << 8) | PATtable[PAT_Length - 1];
return TS_PackagePAT;
}
for(PatProgramPosition = 8; PatProgramPosition < PAT_Length - 4; PatProgramPosition += 4)
{
if (0x00 == ((PATtable[PatProgramPosition ] << 8) | PATtable[1 + PatProgramPosition ]))
{
TS_PackagePAT.network_PID = ((PATtable[2 + PatProgramPosition ] & 0x1F) << 8) | PATtable[3 + PatProgramPosition ];
printf("the network_PID %0x\n", TS_PackagePAT.network_PID);
}
else
{
TS_PackagePAT.astPAT_Program[PAT_ProgramCount].program_number = (PATtable[PatProgramPosition ] << 8)
| PATtable[1 + PatProgramPosition ];
TS_PackagePAT.astPAT_Program[PAT_ProgramCount].reserved_3 = PATtable[2 + PatProgramPosition ] >> 5;
TS_PackagePAT.astPAT_Program[PAT_ProgramCount].program_map_PID = ((PATtable[2 + PatProgramPosition ] & 0x1F) << 8)
| PATtable[3 + PatProgramPosition ];
PAT_ProgramCount++;
}
}
注意:
一定要对负载单元开始标志进行判读,确定好有效数据读取位置。
在PAT表中有个循环,代表了存储的节目号信息。所以要在位运算时格外注意,一旦位置出错,后面的数据存储也会出错。
PAT表会在TS流中循环发送,所以我们只用解析一次,无需一直解析。
虽然PAT表只用解析一次,但是表里的节目号等信息是有很多,一定要解析完整。
PAT表是我们解析的第一张表,解析方法各有不同,但是需要想好。因为后面随着表的数量的增加,如果方法过于复杂,会导致后续表格的解析也会同样复杂,这样就导致了整体代码显的冗长。而且在初步解析时我们同样要理清自己的解析思路,避免后续出现逻辑错误。
PMT是连接节目号与节目元素的桥梁,主要提供节目numbers和节目elements之间的映射关系。在一个TS流中包含有多少个频道,就包含多个PID不同的PMT表。
我们知道,一个电视节目至少包含了视频和音频数据,而每一个节目的视音频数据都是以包的形式在TS流中传输的; 所以说,一个TS流包含了多个节目的视频和音频数据包。 要想过滤出一个TS流中其中一个节目的视频和音频,则需要知道这个节目中视频和音频的标识号PID。 PMT表的作用就在于,它提供了每个节目视频、音频(或其他)数据包的PID。
PMT表中包含的数据如下:
当前频道中所包含的所有Video数据的PID。
当前频道中所包含的所有Audio数据的PID。
和当前频道关联在一起的其他数据的PID(如数字广播,数据通讯等使用的PID)。
所以,当我们解析了PMT表之后,我们就能够获取到频道中所有的PID信息,例如当前频道包含多少Video、共多少个Audio和其他数据,还能知道每种数据对应的PID分别是什么。这样如果我们要选择其中一个Video和Audio收看,那么只需要把要收看的节目的Video PID和Audio PID保存起来,在处理Packet的时候进行过滤即可实现。
PMT表的结构如下:
Syntax(句法结构) | No. of bits(所占位数) | Identifier(识别符) | Note(注释) |
---|---|---|---|
program_map_section(){ | |||
table_id | 8 | uimsbf | 表标识符 |
Section_syntax_indicator | 1 | bslbf | 段语法指示符,通常设置为“1” |
“0” | 1 | bslbf | zero |
Reserved | 2 | bslbf | 保留 |
Section_length | 12 | uimsbf | 段长度,从program_number开始,到CRC_32(含)的字节总数 |
program_number | 16 | uimsbf | 频道号码,表示当前的PMT关联到的频道 |
Reserved | 2 | bslbf | 保留 |
Version_number | 5 | bslbf | 版本号码,如果PMT内容有更新,则它会递增1通知解复用程序需要重新接收节目信息 |
Current_next_indicator | 1 | bslbf | 当前未来标志符 |
Section_number | 8 | uimsbf | 当前段号码 |
last_section_number | 8 | uimsbf | 最后段号码,含义和PAT中的对应字段相同 |
reserved | 3 | bslbf | 保留 |
PCR_PID | 13 | uimsbf | PCR(节目参考时钟)所在TS分组的PID |
reserved | 4 | bslbf | 保留 |
program_info_length | 12 | uimsbf | 节目信息长度(之后的是N个描述符结构,一般可以忽略掉,这个字段就代表描述符总的长度,单位是Bytes)紧接着就是频道内部包含的节目类型和对应的PID号码了,头两位为“00” |
for(i=0;i
|
|
| |
descriptor() | |||
} | |||
for(i=0;i
|
|
| |
stream_type | 8 | uimsbf | 流类型,标志是Video还是Audio还是其他数据 |
reserved | 3 | bslbf | 保留 |
elementary_PID | 13 | uimsbf | 该节目中包括的视频流,音频流等对应的TS分组的PID |
reserved | 4 | bslbf | 保留 |
ES_info_length | 12 | uimsbf | 头两位为"00" |
for(i=0;i
|
|
| |
descriptor() | |||
} | |||
} | |||
CRC_32 | 32 | rpchof | 32位, 表示CRC的值 |
} |
注意:
在这里我们就需要同PAT表的获取做对比。因为在一段TS流中PAT表我们只用解析一次即可,但是PMT表不可以只获取一张。因为PMT表有多张,且携带的信息也不同。我采用的方法是在PAT表解析时,利用一个数组来保存所有的PMT的PID信息,然后在通过循环遍历来找到每一张PMT表。
if((TS_PackageHead.PID == TS_PackagePAT.astPAT_Program[PMTCount].program_map_PID) &&
(TS_PackagePAT.astPAT_Program[PMTCount].program_map_PID != 0))
{
TS_Program = ParseTS_PMT(TsPackage_len, TS_PackageHead);
PMTCount++;
}
注意:
TS流的几张表的解析方式都差不多,同PAT表一样,我们根据负载单元开始标志来确定数据包的有效数据的起始位置。然后将数据存入到对应的结构体当中。
//判断负载单元开始的标志
for(int i = 0; i < PmtStreamCount; i++)
{
if((TS_PackagePMT.astPMT_Stream[i].stream_type == 0x01)|| (TS_PackagePMT.astPMT_Stream[i].stream_type == 0x02)
|| (TS_PackagePMT.astPMT_Stream[i].stream_type ==0x1b))
{
TS_Program.VideoPID = TS_PackagePMT.astPMT_Stream[i].elementary_PID;
}
if((TS_PackagePMT.astPMT_Stream[i].stream_type == 0x03)|| (TS_PackagePMT.astPMT_Stream[i].stream_type == 0x04)
|| (TS_PackagePMT.astPMT_Stream[i].stream_type ==0x0f) || (TS_PackagePMT.astPMT_Stream[i].stream_type == 0x11))
{
TS_Program.AudioPID[ProgramPoint] = TS_PackagePMT.astPMT_Stream[i].elementary_PID;
ProgramPoint++;
}
}
//将数据存入结构体当中
TS_PMT TSPMT_table(TS_PMT TS_PackagePMT, unsigned char PMTtable[204])
{
int PMT_Length = 0;
unsigned char descriptor[204] = {0};
TS_PackagePMT.table_id = PMTtable[0];
TS_PackagePMT.section_syntax_indicator = PMTtable[1] >> 7;
TS_PackagePMT.zero = (PMTtable[1] >> 6) & 0x01;
TS_PackagePMT.reserved_1 = (PMTtable[1] >> 4) & 0x03;
TS_PackagePMT.section_length = ((PMTtable[1] & 0x0F) << 8) | PMTtable[2];
TS_PackagePMT.program_number = (PMTtable[3] << 8) | PMTtable[4];
TS_PackagePMT.reserved_2 = PMTtable[5] >> 6;
TS_PackagePMT.version_number = (PMTtable[5] >> 1) & 0x1F;
TS_PackagePMT.current_next_indicator = (PMTtable[5] << 7) >> 7;
TS_PackagePMT.section_number = PMTtable[6];
TS_PackagePMT.last_section_number = PMTtable[7];
TS_PackagePMT.reserved_3 = PMTtable[8] >> 5;
TS_PackagePMT.PCR_PID = ((PMTtable[8] << 8) | PMTtable[9]) & 0x1FFF;
TS_PackagePMT.reserved_4 = PMTtable[10] >> 4;
TS_PackagePMT.program_info_length = ((PMTtable[10] & 0x0F) << 8) | PMTtable[11];
if (0 != TS_PackagePMT.program_info_length)
{
memcpy(TS_PackagePMT.program_info_descriptor, PMTtable + 12, TS_PackagePMT.program_info_length);
ParseTS_Descriptor(TS_PackagePMT.program_info_descriptor);
}
PMT_Length = TS_PackagePMT.section_length + 3;
TS_PackagePMT.CRC_32 = (PMTtable[PMT_Length - 4] << 24) | (PMTtable[PMT_Length - 3] << 16)
| (PMTtable[PMT_Length - 2] << 8) | PMTtable[PMT_Length - 1];
return TS_PackagePMT;
}
//循环存放
for(PmtStreamPosition += TS_PackagePMT.program_info_length; PmtStreamPosition < (PMT_Length - 4); PmtStreamPosition += 5)
{
unsigned char descriptor[204] = {0};
TS_PackagePMT.astPMT_Stream[PmtStreamCount].stream_type = PMTtable[PmtStreamPosition];
TS_PackagePMT.astPMT_Stream[PmtStreamCount].reserved_5 = (PMTtable[PmtStreamPosition + 1]) >> 5;
TS_PackagePMT.astPMT_Stream[PmtStreamCount].elementary_PID = ((PMTtable[PmtStreamPosition + 1] & 0x1f) << 8)
| PMTtable[PmtStreamPosition + 2];
TS_PackagePMT.astPMT_Stream[PmtStreamCount].reserved_6 = (PMTtable[PmtStreamPosition + 3] >> 4);
TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length = ((PMTtable[PmtStreamPosition + 3] & 0xf) << 8)
| PMTtable[PmtStreamPosition + 4];
if(TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length != 0)
{
memcpy(TS_PackagePMT.astPMT_Stream[PmtStreamCount].descriptor, PMTtable + PmtStreamPosition + 5,
TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length);
PmtStreamPosition += TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length;
ParseTS_Descriptor(TS_PackagePMT.astPMT_Stream[PmtStreamCount].descriptor);
}
}
这样我们就能够获取到PMT表当中的信息。
注意:
相对于解析PAT表,在解析PMT表的过程中增加了对结构体循环数据的存储,和描述符的数据存储。在后面的表格数据存储中,几乎每一张都涉及到了这两个方面。总体来说就是位运算和对每个字节信息的理解。对于关键的信息我们要加强记忆,遇到不懂或者含义不清的地方要及时参考标准文档。
在解析TS流的时候,首先寻找PAT表,根据PAT获取所有PMT表的PID;再寻找PMT表,获取该频段所有节目数据并保存。这样,只需要知道节目的PID就可以根据PacketHeade给出的PID过滤出不同的Packet,从而观看不同的节目。这些就是PAT表和PMT表之间的关系。而由于PID是一串枯燥的数字,用户不方便记忆、且容易输错,所以需要有一张表将节目名称和该节目的PID对应起来,DVB设计了SDT表来解决这个问题。 该表格标志一个节目的名称,并且能和PMT中的PID联系起来,这样用户就可以通过直接选择节目名称来选择节目了。
“SDT描述了业务内容及信息,连接了NIT、EIT和PMT(PSI)”
SDT表主要提供关于Service的信息,如Service name,Service provider 等。
这里所说的业务,即Service;CCTV 1是一个Service(我们说“频道”),湖南卫视也是一个Service
PID = 0x0011
table_id = 0x42 (discribe actual TS,现行TS)
table_id = 0x46 (discribe not actual TS,非现行TS)
SDT表被切分成业务描述段(service_description_section),由PID为0x0011的TS包传输(BAT段也由PID为0x0011的TS包传输,但table_id不同)。
描述现行TS(即包含SDT表的TS)的SDT表的任何段的table_id都为0x42,且具有相同的table_id_extension(transport_stream_id)以及相同的original_network_id。 指向非现行TS的SDT表的任何段的table_id都应取0x46。
业务描述表SDT(见下一节的SDT表结构)中的每一个子表,都用来描述包含于一个特定的传输流中的业务。 该业务可能是现行传输流中的一部分,也可能是其他传输流中的一部分,可以根据table_id 来确定区分上述两种情况。
下表是SDT的结构:
Syntax(句法结构) | No. of bits(所占位数) | Identifier(识别符) | Note(注释) |
---|---|---|---|
service_description_section(){ | |||
table_id | 8 | uimsbf | 表标识符 |
Section_syntax_indicator | 1 | bslbf | 段语法指示符,通常设置为“1” |
Reserved_future_use | 1 | bslbf | 预留使用 |
Reserved | 2 | bslbf | 保留 |
Section_length | 12 | uimsbf | 段长度,12位,指出了此Section的长度,头两位为“00”,值不超过1021 |
transport_stream_id | 16 | uimsbf | 传输流标识符,给出TS的识别号 |
Reserved | 2 | bslbf | 保留 |
Version_number | 5 | bslbf | sub_table的版本号,值为0~31,当sub_table信息改变时,值加1,若值已为31,则变化后值为0(循环),若curren_next_indicator的值为“1”,version_number指当前sub_table,若curren_next_indicator的值为“0”,version_number指下一个sub_table |
Current_next_indicator | 1 | bslbf | 当前后续指示符,1位,“1”指sub_table是current, “0”指sub_table是next |
Section_number | 8 | uimsbf | 段号,8位,给出section号,在sub_table中,第一个section其section_number为"0x00",每增加一个section,section_number加一 |
last_section_number | 8 | uimsbf | 最后段号,sub_table中最后一个section的section_number |
original_nerwork_id | 16 | uimsbf | 原始网络标识符,16位,给出originating delivery system中的网络识别号 |
reserved_future_use | 8 | bslbf | 预留使用 |
for(i=0;i
|
|
| |
service_id | 16 | uimsbf | 业务标识符,16位,用来标识TS中的Service,等同于program_map_section中的program_number |
reserved_future_use | 8 | bslbf | 预留使用 |
EIT_schedule_flag | 1 | bslbf | EIT时间表标志,1位,若为“1”,则EIT schedule信息在当前TS中,为“0”,则TS中不存在EIT schedule信息 |
EIT_present_following_flag | 1 | bslbf | EIT当前后续标志,1位,若为“1”,则EIT_present_following 信息在当前TS中,为“0”,则TS中不存在EIT_present_following信息。 |
running_status | 3 | uimsbf | |
freed_CA_mode | 1 | bslbf | 自由条件接收模式,1位,“0”表示无CA,“1”表示有CA |
descriptors_loop_length | 12 | uimsbf | 描述符循环长度 |
for(i=0;i
|
|
| |
descriptor() | |||
} | |||
} | |||
CRC_32 | 32 | rpchof | 32位, 表示CRC的值 |
} |
注意:
由上面信息可知道,SDT表的PID为0x0011,但是PID为0x0011的表还有BAT, ST。所以我们不能仅仅从PID这一条信息里找到准确的SDT数据包。所以我们在PID = 0x0011的基础上再判断其除包头外的有效字节的第一个字节(table_id)是否为0x0042或者0x0046。结合这两条判断条件,我们就可以准确筛选出来SDT表。
if(TS_PackageHead.PID == 0x0011)
{
value2 = SDT_BAT(TsPackage_len,TS_PackageHead,value2);
}
int SDT_BAT(int TsPackage_len,TS_Package_Head TS_PackageHead, int value2)
{
if(TS_PackageHead.payload_unit_start_indicator == 0)
{
if(TSdata[4] == 0x42 && value2 == 1)
{
ParseTS_SDT(TsPackage_len, TS_PackageHead);
value2++;
return value2;
}else if (TSdata[4] == 0x4a)
{
}
}
if(TS_PackageHead.payload_unit_start_indicator == 1)
{
if(TSdata[5] == 0x42 && value2 == 0)
{
ParseTS_SDT(TsPackage_len, TS_PackageHead);
value2++;
return value2;
}else if (TSdata[5] == 0x4a)
{
}
}
return value2;
}
注意:
一样的顺序,我们需要先建立结构体,再描述符的循环里面可以创建一个unsigned char 类型的数组来保存描述符数据。
TS_SDT TSSDT_Table(TS_SDT TS_PackageSDT, unsigned char SDTtable[204])
{
int SDT_Length = 0;
TS_PackageSDT.table_id = SDTtable[0];
TS_PackageSDT.section_syntax_indicator = SDTtable[1] >> 7;
TS_PackageSDT.reserved_future_use_1 = (SDTtable[1] >> 6) & 0x01;
TS_PackageSDT.reserved_1 = (SDTtable[1] >> 4) & 0x03;
TS_PackageSDT.section_length = ((SDTtable[1] & 0x0F) << 8) | SDTtable[2];
TS_PackageSDT.transport_stream_id = (SDTtable[3] << 8) | SDTtable[4];
TS_PackageSDT.reserved_2 = SDTtable[5] >> 6;
TS_PackageSDT.version_number = (SDTtable[5] >> 1) & 0x1F;
TS_PackageSDT.current_next_indicator = (SDTtable[5] << 7) >> 7;
TS_PackageSDT.section_number = SDTtable[6];
TS_PackageSDT.last_section_number = SDTtable[7];
TS_PackageSDT.original_network_id = (SDTtable[8] << 8) | SDTtable[9];
TS_PackageSDT.reserved_future_use_2 = SDTtable[10];
SDT_Length = TS_PackageSDT.section_length + 3;
TS_PackageSDT.CRC_32 = (SDTtable[SDT_Length - 4] << 24) | (SDTtable[SDT_Length - 3] << 16)
| (SDTtable[SDT_Length - 2] << 8) | SDTtable[SDT_Length - 1];
return TS_PackageSDT;
}
TS_SDT ParseSDT_Section(TS_SDT TS_PackageSDT, unsigned char SDTtable[204])
{
int SDT_Length = 0;
int ServiceCount = 0;
int ServicePosition = 11;
unsigned char descriptor[204];
SDT_Length = TS_PackageSDT.section_length + 3;
for (ServicePosition = 11; ServicePosition < SDT_Length - 4; ServicePosition += 5)
{
TS_PackageSDT.SDT_Service[ServiceCount].service_id = (SDTtable[ServicePosition] << 8)
| SDTtable[ServicePosition + 1];
TS_PackageSDT.SDT_Service[ServiceCount].reserved_future_use = SDTtable[ServicePosition + 2] >> 2;
TS_PackageSDT.SDT_Service[ServiceCount].EIT_schedule_flag = (SDTtable[ServicePosition + 2] & 0x03) >> 1;
TS_PackageSDT.SDT_Service[ServiceCount].EIT_present_following_flag = SDTtable[ServicePosition + 2] & 0x01;
TS_PackageSDT.SDT_Service[ServiceCount].running_status = SDTtable[ServicePosition + 3] >> 5;
TS_PackageSDT.SDT_Service[ServiceCount].free_CA_mode = (SDTtable[ServicePosition + 3] & 0x1F) >> 4;
TS_PackageSDT.SDT_Service[ServiceCount].descriptors_loop_length = ((SDTtable[ServicePosition + 3] & 0x0F) << 8)
| SDTtable[ServicePosition + 4];
if (TS_PackageSDT.SDT_Service[ServiceCount].descriptors_loop_length != 0)
{
memcpy(TS_PackageSDT.SDT_Service[ServiceCount].descriptor, SDTtable + 5 + ServicePosition,
TS_PackageSDT.SDT_Service[ServiceCount].descriptors_loop_length);
ServicePosition += TS_PackageSDT.SDT_Service[ServiceCount].descriptors_loop_length;
ParseTS_Descriptor(TS_PackageSDT.SDT_Service[ServiceCount].descriptor);
}
ServiceCount++;
}
return TS_PackageSDT;
}
void ParseTS_SDT(int TsPackage_len,TS_Package_Head TS_PackageSDTHead)
{
TS_SDT TS_PackageSDT = {0};
unsigned char SDTtable[204] = {0};
if(TS_PackageSDTHead.payload_unit_start_indicator == 0)
{
memcpy(SDTtable, TSdata+4, TsPackage_len-4);
TS_PackageSDT = TSSDT_Table(TS_PackageSDT, SDTtable);
}
if(TS_PackageSDTHead.payload_unit_start_indicator == 1)
{
memcpy(SDTtable, TSdata+5, TsPackage_len-5);
TS_PackageSDT = TSSDT_Table(TS_PackageSDT, SDTtable);
}
TS_PackageSDT = ParseSDT_Section(TS_PackageSDT, SDTtable);
}
这样我们就可保存并获取到SDT表的信息。
注意:
在SDT表的获取过程中,有两个难点。第一个就是对SDT表的获取,如何甄别出SDT,BAT和ST。第二个就是对于描述符的处理,所包含的信息很多。总而言之,每张表的在解析方法上都基本大同小异。
NIT描述了数字电视网络中与网络相关的信息。
NIT表主要提供物理网络本身的一些信息。
PID = 0x0010
table_id = 0x40 (discribe actual newwork)
table_id = 0x41 (discribe not actual newwork)
NIT描述了数字电视网络中与网络相关的信息,但这个表本身的信息有限,更多的信息是依靠插入表中的描述符来提供的。 NIT常用的描述符有:网络名称描述符(network_name_descriptor)、有线传送系统(cable_delivery_system_descriptor)、业务列表描述符(service_list_descriptor)和链接描述符(linkage_descriptor)。
网络信息表NIT传递了与通过一个给定的网络传输的复用流/TS流的物理结构相关的信息,以及与网络自身特性相关的信息。在本标准应用的范围内,original_network_id 和 transport_stream_id 两个标识符相结合唯一确定了网络中的TS流。各网络被分配独立的 network_id值作为网络的唯一识别码。这些码字的分配见ETR 162。当NIT表在生成TS流的网络上传输时,network_id和original_network_id将取同一值。
当转换频道时,为了使存取时间最小,IRD可以在非易失性存储器上存储NIT表信息。除现行网络外,也可以为其他网络传输NIT表信息。现行网络的NIT表与其他网络的NIT表使用不同的table_id值来区分。
NIT的结构如下表:
Syntax(句法结构) | No. of bits(所占位数) | Identifier(识别符) | Note(注释) |
---|---|---|---|
network_information_section(){ | |||
table_id | 8 | uimsbf | 表标识符 |
Section_syntax_indicator | 1 | bslbf | 段语法指示符,通常设置为“1” |
Reserved_future_use | 1 | bslbf | 预留使用 |
Reserved | 2 | bslbf | 保留 |
Section_length | 12 | uimsbf | 段长度,12位,指出了此Section的长度,头两位为“00”,值不超过1021 |
Network_id | 16 | uimsbf | 网络标识符,16位字段。NIT表所描述的传输系统的网络标识,用以区别其他的传输系统。 |
Reserved | 2 | bslbf | 保留 |
Version_number | 5 | uimsbf | sub_table的版本号,值为0~31,当sub_table信息改变时,值加1,若值已为31,则变化后值为0(循环),若curren_next_indicator的值为“1”,version_number指当前sub_table,若curren_next_indicator的值为“0”,version_number指下一个sub_table |
Current_next_indicator | 1 | bslbf | 当前后续指示符,1位,“1”指sub_table是current, “0”指sub_table是next |
Section_number | 8 | uimsbf | 段号,8位,给出section号,在sub_table中,第一个section其section_number为"0x00",每增加一个section,section_number加一 |
last_section_number | 8 | uimsbf | 最后段号,sub_table中最后一个section的section_number |
Reserved_future_use | 4 | bslbf | 预留使用 |
Network_descriptors_length | 12 | uimsbf | 网络描述符长度 |
for(i=0;i
|
|
| |
descriptor() | First descriptor loop | ||
reserved_future_use | 4 | bslbf | - |
transport_stream_loop_length | 12 | uimsbf | 传输流循环长度 |
for(i=0;i
|
|
| |
transport_stream_id | 16 | uimsbf | 传输流标识符,16位, 给出TS的识别号 |
original_network_id | 16 | uimsbf | 原始网络标识符,16位,给出originating delivery system中的网络识别号 |
reserved_future_use | 4 | bslbf | 预留使用 |
transport_descriptors_length | 12 | uimsbf | 传输流描述符长度 |
for(i=0;i
|
|
| |
descriptor() | Second descriptor loop | Second descriptor loop | |
} | |||
} | |||
CRC_32 | 32 | rpchof | 32位, 表示CRC的值 |
} |
注意:
第一层描述符 | 作用域是针对整个网络的,如插入网络名称描述符、链接描述符等 |
---|---|
第二层描述符 | 作用域是第一层循环所代表的一个TS流,如插入有线传输系统描述符 |
第一层包含的描述符:Linkage descriptor(链接描述符)、Multiligual network name descriptor(多语种网络名称描述符)、Network name descriptor(网络名称描述符);
第二层包含的描述符:Delivery system descriptor(交付系统描述符)、Service list descriptor(业务列表描述符)、Frequency list descriptor(频率列表描述符);
此外,NIT表还有如下特点:
NIT表被切分为网络信息段(network_information_section)
任何NIT的段都必须由PID为0x0010的TS包传输
现行网络的NIT表任何段的table_id值应为0x40,且具有相同的table_id_extension即(network_id);
现行网络以外的其他网络NIT表的段table_id值应为0x41
由上面信息可知道,NIT表的PID为0x0010,但是PID为0x0010的表还有 ST。所以我们不能仅仅从PID这一条信息里找到准确的NIT数据包。所以我们在PID = 0x0010的基础上再判断其除包头外的有效字节的第一个字节(table_id)是否为0x0040或者0x0041。结合这两条判断条件,我们就可以准确筛选出来NIT表。
int ParseTS_NIT(int TsPackage_len,TS_Package_Head TS_PackageNITHead, int NITCount)
{
TS_NIT TS_PackageNIT = {0};
unsigned char NITtable[204] = {0};
if(TS_PackageNITHead.payload_unit_start_indicator == 0 && TSdata[4] == 0x40 )
{
memcpy(NITtable, TSdata+4, TsPackage_len-4);
TS_PackageNIT = TSNIT_Table(TS_PackageNIT, NITtable);
ParseNIT_Section(TS_PackageNIT, NITtable);
return ++NITCount;
}
if(TS_PackageNITHead.payload_unit_start_indicator == 1 && TSdata[5] == 0x40)
{
memcpy(NITtable, TSdata+5, TsPackage_len-5);
TS_PackageNIT = TSNIT_Table(TS_PackageNIT, NITtable);
ParseNIT_Section(TS_PackageNIT, NITtable);
return ++NITCount;
}
return NITCount;
}
注意:
我们以文档为标准,建立好NIT的结构体,然后通过位运算,将数据保存在结构体中。
TS_NIT TSNIT_Table(TS_NIT TS_PackageNIT, unsigned char NITtable[204])
{
int NetworkDescriptorLen = 0;
int NIT_Length = 0;
TS_PackageNIT.table_id = NITtable[0];
TS_PackageNIT.section_syntax_indicator = NITtable[1] >> 7;
TS_PackageNIT.reserved_future_use_1 = (NITtable[1] >> 6) & 0x01;
TS_PackageNIT.reserved_1 = (NITtable[1] >> 4) & 0x03;
TS_PackageNIT.section_length = ((NITtable[1] & 0x0F) << 8) | NITtable[2];
TS_PackageNIT.network_id = (NITtable[3] << 8) | NITtable[4];
TS_PackageNIT.reserved_2 = NITtable[5] >> 6;
TS_PackageNIT.version_number = (NITtable[5] >> 1) & 0x1F;
TS_PackageNIT.current_next_indicator = (NITtable[5] << 7) >> 7;
TS_PackageNIT.section_number = NITtable[6];
TS_PackageNIT.last_section_number = NITtable[7];
TS_PackageNIT.reserved_future_use_2 = NITtable[8] >> 4;
TS_PackageNIT.network_descriptors_length = ((NITtable[8] & 0x0F) << 8) | NITtable[9];
NetworkDescriptorLen = TS_PackageNIT.network_descriptors_length;
memcpy(TS_PackageNIT.network_descriptor, NITtable + 10, NetworkDescriptorLen);
ParseTS_Descriptor(TS_PackageNIT.network_descriptor);
TS_PackageNIT.reserved_future_use_2 = NITtable[10 + NetworkDescriptorLen] >> 4;
TS_PackageNIT.transport_stream_loop_length = ((NITtable[10 + NetworkDescriptorLen] & 0x0F) << 8)
| NITtable[10 + NetworkDescriptorLen + 1];
NIT_Length = TS_PackageNIT.section_length + 3;
TS_PackageNIT.CRC_32 = (NITtable[NIT_Length - 4] << 24) | (NITtable[NIT_Length - 3] << 16)
| (NITtable[NIT_Length - 2] << 8) | NITtable[NIT_Length - 1];
return TS_PackageNIT;
}
TS_NIT ParseNIT_Section(TS_NIT TS_PackageNIT, unsigned char NITtable[204])
{
int NIT_Length = 0;
int TransportPosition = 0;
int TranportCount = 0;
int NetworkDescriptorLen = 0;
NetworkDescriptorLen = TS_PackageNIT.network_descriptors_length;
NIT_Length = TS_PackageNIT.section_length + 3;
for (TransportPosition = 10 + NetworkDescriptorLen + 2; TransportPosition < NIT_Length - 4; TransportPosition += 6)
{
TS_PackageNIT.astNIT_Transport[TranportCount].transport_stream_id = (NITtable[TransportPosition] << 8)
| NITtable[TransportPosition + 1];
TS_PackageNIT.astNIT_Transport[TranportCount].original_network_id = (NITtable[TransportPosition + 2] << 8)
| NITtable[TransportPosition + 3];
TS_PackageNIT.astNIT_Transport[TranportCount].reserved_future_use = NITtable[TransportPosition + 4] >> 4;
TS_PackageNIT.astNIT_Transport[TranportCount].transport_descriptors_length = ((NITtable[TransportPosition + 4] & 0x0F) << 8)
| NITtable[TransportPosition + 5];
if (0 != TS_PackageNIT.astNIT_Transport[TranportCount].transport_descriptors_length)
{
memcpy(TS_PackageNIT.astNIT_Transport[TranportCount].descriptor, NITtable + 6 + TransportPosition,
TS_PackageNIT.astNIT_Transport[TranportCount].transport_descriptors_length);
TransportPosition += TS_PackageNIT.astNIT_Transport[TranportCount].transport_descriptors_length;
ParseTS_Descriptor(TS_PackageNIT.astNIT_Transport[TranportCount].descriptor);
}
TranportCount++;
}
return TS_PackageNIT;
}
注意:
描述符的类型也有很多,在标准文档中列举了各个描述符的作用含义和每个数据位所携带的信息。下面主要针对于部分描述符来做一个介绍和解析。
下表列出了部分在标准文档中声明或定义的描述符,并提供了描述符标签值和在SI表中的预期位置。但是这并不意味着它们在其他表中的使用受到限制。
Descriptor | Tag value | NIT | BAT | SDT | EIT | TOT | PMT | SIT (see note 1) |
---|---|---|---|---|---|---|---|---|
network_name_descriptor | 0x40 | * | ||||||
service_list_descriptor | 0x41 | * | * | |||||
stuffing_descriptor | 0x42 | * | * | * | * | * | ||
satellite_delivery_system_descriptor | 0x43 | * | ||||||
cable_delivery_system_descriptor | 0x44 | * | ||||||
VBI_data_descriptor | 0x45 | * | ||||||
VBI_teletext_descriptor | 0x46 | * | ||||||
bouquet_name_descriptor | 0x47 | * | * | * | ||||
service_descriptor | 0x48 | * | * | |||||
country_availability_descriptor | 0x49 | * | * | * | ||||
linkage_descriptor | 0x4A | * | * | * | * | * | ||
NVOD_reference_descriptor | 0x4B | * | * | |||||
time_shifted_service_descriptor | 0x4C | * | * | |||||
short_event_descriptor | 0x4D | * | * | |||||
extended_event_descriptor | 0x4E | * | * | |||||
time_shifted_event_descriptor | 0x4F | * | * | |||||
component_descriptor | 0x50 | * | * | * | ||||
mosaic_descriptor | 0x51 | * | * | * | ||||
stream_identifier_descriptor | 0x52 | * | ||||||
CA_identifier_descriptor | 0x53 | * | * | * | * | |||
content_descriptor | 0x54 | * | * | |||||
parental_rating_descriptor | 0x55 | * | * | |||||
teletext_descriptor | 0x56 | * | ||||||
telephone_descriptor | 0x57 | * | * | * | ||||
… | … |
注意:
从表中我们可以得知不同的描述符的table_id,这个table_id就是我们在解析不同的表中的descriptor()部分的第一个字节。并且我们可以从其他信息中得知descriptor()部分的数据长度,所以我们很容易提取出来描述符的数据。
if(TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length != 0)
{
memcpy(TS_PackagePMT.astPMT_Stream[PmtStreamCount].descriptor, PMTtable + PmtStreamPosition + 5,
TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length);
PmtStreamPosition += TS_PackagePMT.astPMT_Stream[PmtStreamCount].ES_info_length;
ParseTS_Descriptor(TS_PackagePMT.astPMT_Stream[PmtStreamCount].descriptor);
}
注意:
void ParseTS_Descriptor(unsigned char Descriptor[204])
{
switch (Descriptor[0])
{
case 0x41:
Service_List_Descriptor(Descriptor);
break;
case 0x42:
Stuffing_Descriptor(Descriptor);
break;
case 0x48:
Service_Descriptor(Descriptor);
break;
case 0x52:
Stream_Identifier_Descriptor(Descriptor);
break;
default:
break;
}
}
void Service_List_Descriptor(unsigned char Descriptor[204])
{
SERVICR_LIST_DESCRIPTOR ServiceListTable = {0};
int ServiceListPosition = 0;
int ServiceInfoPosition = 0;
ServiceListTable.descriptor_tag = Descriptor[ServiceListPosition];
ServiceListTable.descriptor_length = Descriptor[ServiceListPosition+1];
for(int i = ServiceListPosition+2; i < ServiceListTable.descriptor_length; i += 3)
{
ServiceListTable.service_info[ServiceInfoPosition].service_id = Descriptor[i] << 8 | Descriptor[i+1];
ServiceListTable.service_info[ServiceInfoPosition].service_type = Descriptor[i+2];
ServiceInfoPosition++;
}
}
这样我们就能够得到table_id = 0x41的描述符信息。其他描述符的解析步骤与之类似,需要查看文档来帮助解析。
描述符的信息有很多,种类也有很多。不过在代码结构上,我们只需写出各自的解析函数,然后通过switch()语句使之自行对号入座。
因为在解析过程中需要获取到我们想要的数据,从而得到有用的信息。那么我们就要对解析出来的数据进行数据处理。
这次解析过程中,我主要通过链表的形式来对解析到的表格数据进行保存。首先建立每种类型表的首链表,作为我们提取数据的钥匙。
void List()
{
PmtPoint = (TS_PMT *)malloc(sizeof(TS_PMT));
PmtPoint->next = NULL;
PmtPoint->Previous = NULL;
TS_PackagePmtPoint = PmtPoint;
NitPoint = (TS_NIT *)malloc(sizeof(TS_NIT));
NitPoint->next = NULL;
NitPoint->Previous = NULL;
TS_PackageNitPoint = NitPoint;
BatPoint = (TS_BAT *)malloc(sizeof(TS_BAT));
BatPoint->next = NULL;
BatPoint->Previous = NULL;
TS_PackageBatPoint = BatPoint;
SdtPoint = (TS_SDT *)malloc(sizeof(TS_SDT));
SdtPoint->next = NULL;
SdtPoint->Previous = NULL;
TS_PackageSdtPoint = SdtPoint;
EitPoint = (TS_EIT *)malloc(sizeof(TS_EIT));
EitPoint->next = NULL;
EitPoint->Previous = NULL;
TS_PackageEitPoint = EitPoint;
}
然后在对每个表进行解析的时候进行链表连接操作,这样我们就能将每种类型的表很好的保存起来。
因为有些表的数据长度超过了188个字节,故而无法通过一个数据包来传送,会利用多个数据包来进行分段传送。但是TS流数据包的发送又是随机的,所以我们需要通过一定的过滤机制来过滤出类型正确,顺序正确的数据包。
得到完整的数据包后,我们就要将它们有序的放在一起,然后通过结构体放进我们的链表里。
这里要注意几点:
在代码的结构中,我将不同表的解析函数作为各自的.c文件,结构体和宏定义信息写为各自的.h文件。所以这次的Makefile语句就很简单。
CFLAGS += -g
src = $(wildcard ./*.c)
obj = $(patsubst %.c,%.o,$(src))
target = ts
$(target) : $(obj)
gcc $(obj) -o $(target)
%.o : %.c
gcc $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -rf $(obj) $(target)
问题: