本博客的目的是手写一个程序DEMO,它的作用是将一段TS封装格式的视频解析为一段包含H264编码的ES视频流。
一,DEMO前期准备。
1.1 知识准备。TS全称transport stream,是基于MPEG-2的封装格式(所以也叫MPEG-TS),通常后缀为.ts,.mpg,.mpeg。TS封装格式如今广泛应用于数字电视,在即时通讯传输业务上大方光彩。具体相关的知识讲解可以看我的另一篇博客:https://blog.csdn.net/qq_41786131/article/details/90405715。有必要了解PAT,PMT,TS层,PES层的各个重要的句法都代表什么含义。
1.2 工具准备。TS封装格式解析工具,我推荐EasyICE,具体工具和相关ts视频素材我上传了,如果想要更多素材也可以私我哦:https://download.csdn.net/download/qq_41786131/11191889
二,代码编写。
要解析es视频流,我们必须知道这个视频流的PID,视频流的PID只存在于PMT表中,PMT表也存在自己的PID,它的PID由PAT表指出。可以看出,PAT表是我们分析ts视频的起始,不论干什么都要先找到它。而PAT表的PID值是固定的0x00,所以我们只需要遍历到它就可以开始我们的分析啦。
具体代码结构如下:
pat.class存储了pat表的句法信息,同理pes.class,pmt.class,tsheader.class都存储着各自的句法信息。我们知道PMT表的最后包含着一个节目信息的循环体,PMT最后是一个数据流类型的循环体,我在这里把它俩放到section.class里。调整字段我们暂时用不上,就不做分析(这些类中好多数据都用不上)。我们在analysis.class分析处理ts视频,主要分为三步,第一步获取包长和有效包起始位置,第二部获取ts文件的视频流PID和音频流PID,第三部读取ts文件,将pes,es数据分别存储到demo.pes,demo.es中。大致的流程图如下:
关于PAT,PMT,PES,TSHEADER的句法解析就不列出来了,具体网上很多,也可以参考我在博客尾部留下的完整工程。section字段的代码如下:
class PROGRAM_INFO{
public:
PROGRAM_INFO(char* data);
~PROGRAM_INFO();
public:
unsigned int program_number; //16b
// unsigned int reserved; //3b
unsigned int PMT_PID; //13b
};
class STREAM_TYPE{
public:
STREAM_TYPE(char* data);
~STREAM_TYPE();
public:
unsigned int stream_type; //8b
// unsigned int reserved1; //3b
unsigned int elementry_PID; //13b
// unsigned int reserved2; //4b
unsigned int ES_info_length; //12b
};
可以看出PROGRAM_INFO结构是为了适配PAT最后的节目信息映射表(PMT),STREAM_TYPE结构是为了适配每一个PMT表的流信息(一般为视音频流),这里就不再多说了。
然后就是analysis.class的结构,它主要有三个功能(获取包长,读取PID信息,存储es文件)。它的整体结构如下:
class ANALYSIS{
public:
ANALYSIS(char* file);
~ANALYSIS();
//判断TS包长是否是188字节
unsigned int get_packet_length(FILE* fp);
//获取节目信息到map里面
bool get_infos();
//获取pes文件,es文件
void get_pes_es(unsigned int pid);
bool execute_parse();
private:
PAT* pat;
PMT* pmt;
PES* pes;
TSHEADER* tsheader;
char* ts_path;
unsigned int packet_length = 0;
unsigned int packet_start_position = 0;
//存储PAT表中的节目信息。
vector<PROGRAM_INFO*> program_infos;
//将节目信息中的PID值与PMT表分别匹配
map<unsigned int, vector<STREAM_TYPE*>> infos;
前三个函数对应三个功能,execute_parse()供main函数调用。这几个字段类型的实例声明在类内方便自动销毁;packet_length是包的长度,packet_start_position是有效包的起始位置,这两个成员会在get_packet_length函数调用后会被填充。program_infos和infos会在get_infos函数调用时填充。
类的执行开始是在main函数中调用execute_parse()开始的,这个函数的定义如下:
bool ANALYSIS::execute_parse(){
if(!get_infos()){
printf("get the pid infos lose!\n");
return -1;
}
printf("the pid's read had finished---\n");
for(auto it = infos.begin(); it != infos.end(); ++it){
printf("the PMT PID is : %d\n",(*it).first);
for(auto stream_it = (*it).second.begin(); stream_it != (*it).second.end(); ++stream_it){
printf("the STREAM_TYPE is: %s, it's PID is: %d\n",stream_type_map[(*stream_it)->stream_type].c_str(),(*stream_it)->elementry_PID);
}
}
printf("plese enter the PID which you want to save:\n");
unsigned int PID = 0;
scanf("%d", &PID);
get_pes_es(PID);
return 1;
}
明显,它的作用是顺序执行analysis类的三个功能(getlength函数在getinfos函数中调用),infos被填充后我们就遍历一遍给出用户数据,供用户交互。stream_type_map是一个各种流的编号unsigned int到名字string的映射。用户选择了一个PID后,我们就get_pes_es遍历ts文件根据这个PID来选择保存数据。
判断ts数据包的length方法主要通过遍历找出第一个0x47,说明这是一个包的开始(有没有可能是前一个包的数据,虽然一般情况下第一个包的包头都是0x47),然后我们从这个位置向后遍历十个packet_length的大小,如果每个包的起始位置都是0x47,那么就可以断定出这个ts文件的包的大小。action:fgetc()后文件指针会自动向后偏移一位。具体代码实现如下:
unsigned int judge_packet_length(FILE* fp, unsigned int ts_position, unsigned int length){
if(fseek(fp, ts_position + 1, SEEK_SET)== -1){
perror("seek lose:");
}
for(int i = 0; i != 10; ++i){
if(fseek(fp, length-1, SEEK_CUR) == -1){
perror("fssek error:");
return -1;
}
if(feof(fp)){
return -1;
}
if(fgetc(fp) != 0x47){
perror("fgetc is not the 0x47, error:");
return -1;
}
}
printf("---------------------------------------\n"
"the transport stream packet length is %d\n",length);
return length;
}
unsigned int ANALYSIS::get_packet_length(FILE* fp){
//ts包前面可能有同步字节,我们循环跳过
while(!feof(fp)){
if(getc(fp) == 0x47){
if(judge_packet_length(fp,packet_start_position,PACKET_LENGTH_188) == PACKET_LENGTH_188){
return PACKET_LENGTH_188;
}
fseek(fp, packet_start_position, SEEK_SET);
if(judge_packet_length(fp,packet_start_position,PACKET_LENGTH_204) == PACKET_LENGTH_204){
return PACKET_LENGTH_204;
}
return -1;
}
packet_start_position++;
}
return -1;
}
通过之上的操作我们可以得到当前ts文件的packet_length和packet_start_position。这两个玩意儿是我们等下遍历文件的前提条件。
然后就是获取各个流PID信息的函数--get_infos()。它主要通过遍历找到PAT表的信息,通过PAT表得到所有的PMT信息并存入vector<PROGRAM_INFO*> program_infos中,然后接着遍历,不论当前包类型如何,都存入到PMT中,存入之后如果它的table_id等于0x02,那么恭喜你,它是一个PMT表,如果这个PMT表的我们将它的PID,和它包含的各种流的PIDs存入map<unsigned int, vector<STREAM_TYPES*>> infos(使用map的好处就体现了--避免重复)。就这样一直遍历,直到infos.size()和program_infos.size()大小一样,那么就结束遍历。这个一般不会遍历很多次就结束了。具体代码如下,请参考:
bool ANALYSIS::get_infos(){
//获取ts包的长度
FILE* fp = fopen(ts_path, "rb");
if(fp == NULL){
perror("open the ts file lose:");
return -1;
}
unsigned int ts_position = packet_start_position;
packet_length = get_packet_length(fp);
unsigned int payload_position = 0;
unsigned int pat_exit_flag = 0;
unsigned int program_infos_size = 0;
char* buffer_data = new char[packet_length];
//跳跃到第一个包的位置
fseek(fp, ts_position, SEEK_SET);
while(!feof(fp)){
//将当前包存入buffer_data
if(fread(buffer_data, packet_length, 1, fp) == 0){
perror("fread error:");
return -1;
}
//跳跃到下一个包
ts_position += packet_length;
if(fseek(fp, ts_position, SEEK_SET) == -1){
perror("get_infos fseek error:");
return -1;
}
//通过包头tsheader判断payload位置
tsheader = new TSHEADER(buffer_data);
/*switch(tsheader->adapation_field_control){
case 0:
break;
case 1:
}*/
if(tsheader->adapation_field_control == 0 || tsheader->adapation_field_control == 2){
printf("no payload, continue the next packet");
continue;
}else if(tsheader->adapation_field_control == 1){
payload_position = 4;
}else{
//pes header头部第一个字节代表剩下长度,再加上它本身
payload_position = 4 + buffer_data[4] +1;
}
//对含有有效负载偏移的情况。当这个值为1,说明含有1个字节的FieldPointer,这个FieldPointer的值又代表了PSI,SI表在payload开头占据的字节数。
if(tsheader->payload_uint_start_indicator/* == 1*/){
payload_position += buffer_data[payload_position] + 1;
}
//分析PID值,PAT表只需要分析一次即可,以后不再分析。得到一个map,first代表pid值,second是pmt表中的stream_types信息,比如音频,食品。
if(tsheader->PID == 0x00 && !pat_exit_flag){
pat = new PAT(buffer_data + payload_position);
pat->get_program_info(program_infos);
pat_exit_flag = 1;
program_infos_size = program_infos.size();
}else{
pmt = new PMT(buffer_data + payload_position);
if(program_infos_size && (pmt->table_id == 0x02)){
for(auto program_it = program_infos.begin(); program_it != program_infos.end(); ++program_it){
if((*program_it)->program_number == pmt->program_number){
vector<STREAM_TYPE*> stream_types;
pmt->get_stream_types(stream_types);
infos.insert(make_pair((*program_it)->PMT_PID, stream_types));
if(infos.size() == program_infos_size){
return 1;
}
// for(auto stream_it = stream_types.begin(); stream_it != stream_types.end(); ++stream_it){
// infos.insert(make_pair((*stream_it)->elementry_PID, )
// }
}
}
}
}
}
return 1;
}
再然后就是我们最关注的es文件的生成。我们使ts_fp再次从packet_start_position开始遍历,我们每次读取[188*204*5]字节的数据存入到buffer_data中用来做数据转存。因为pes包一般是一帧,一个包才188k,所以会出现下面这样的存储结构:
每个小方格代表一帧,图有点磕碜,大致这个意思。pes包会被切分,每个packet包含它的一部分。一帧的第一个packet可能包含pes和es,其他packet可能就不包含pes了,直接是es。所以我们存储es数据的大致思路就是每当一帧的开始,就存储es数据,存储多少呢?存储所有pes数据减去前面的头部那一部分。我们只需要判断出每个pes包除了es的最后位置,剩下的都是有效数据,将这些数据memcpy到es_buffer里,每当遇到一个新的帧,就将es_buffer读入到文件中。代码如下:
void ANALYSIS::get_pes_es(unsigned int pid){
FILE* ts_fp = fopen(ts_path, "rb");
FILE* pes_fp = fopen("demo.pes", "wb");
FILE* es_fp = fopen("demo.es", "wb");
unsigned int ts_position = 0;
unsigned int read_size = 0;
unsigned int received_length = 0;
unsigned int available_length = 0;
unsigned int max_ts_length = 65536;
unsigned int es_packet_count = 0;
bool start_flag = 0;
bool received_flag = 0;
char* payload_position = NULL;
char buffer_data[188*204*5] = {};
char* es_buffer = (char*)malloc(max_ts_length);
char* current_p = NULL;
if(ts_fp == NULL || pes_fp == NULL || es_fp == NULL){
perror("open file lose:");
}
if(fseek(ts_fp, ts_position, SEEK_SET) == -1){
perror("seek the file lose:");
}
while(!feof(ts_fp)){
read_size = fread(buffer_data, 1, sizeof(buffer_data), ts_fp);
if(read_size == 0){
perror("fread lose:");
}
current_p = buffer_data;
//处理读到的数据
while(current_p < buffer_data + read_size){
tsheader = new TSHEADER(current_p);
if(tsheader->PID == pid){
es_packet_count++;
if(tsheader->adapation_field_control == 1){
payload_position = current_p + 4;
}else if(tsheader->adapation_field_control == 3){
payload_position = current_p + 4 + current_p[4] + 1;
}
//不是完整帧的直接丢弃
if(tsheader->payload_uint_start_indicator != 0){
start_flag =1;
}
if(start_flag && payload_position && pes_fp){
available_length = packet_length + current_p -payload_position;
//写入pes文件
fwrite(current_p, available_length, 1, pes_fp);
//写入es文件
if(tsheader->payload_uint_start_indicator != 0){
if(received_length > 0){
pes = new PES(es_buffer);
if(pes->packet_start_code_prefix != 0x000001){
printf("pes is not correct.received %d es packet\n",es_packet_count);
return;
}
if(pes->PES_packet_data_length != 0){
fwrite(pes->elementy_stream_position, received_length, 1, es_fp);
}
memset(es_buffer, 0, received_length);
received_length = 0;
}
received_flag = 1;
}
if(received_flag){
if(received_length + available_length > max_ts_length){
max_ts_length *= 2;
es_buffer = (char*)realloc(es_buffer,max_ts_length);
}
memcpy(es_buffer + received_length, payload_position, available_length);
received_length += available_length;
}
}
}
current_p += packet_length;
}
}
printf("the packet number is: %d\n"
"and the demo.pes, demo.es has been saved at current directory.\n",es_packet_count);
if(es_buffer){
free(es_buffer);
}
if(es_fp){
fclose(es_fp);
}
if(pes_fp){
fclose(pes_fp);
}
if(ts_fp){
fclose(ts_fp);
}
}
大致的要点就只写了。时间仓促,笼统介绍一下。具体代码请参考:https://github.com/Vashonisonly/TS_Parse
部分代码摘自网络,侵权立删。