TS封装格式解析出ES视频流
本博客的目的是手写一个程序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; //3bunsigned int PMT_PID; //13b
};class STREAM_TYPE{
public:STREAM_TYPE(char* data);~STREAM_TYPE();public:unsigned int stream_type; //8b// unsigned int reserved1; //3bunsigned int elementry_PID; //13b// unsigned int reserved2; //4bunsigned 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_dataif(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
部分代码摘自网络,侵权立删。
TS封装格式解析出ES视频流相关推荐
- 视音频编解码学习工程:TS封装格式分析器
===================================================== 视音频编解码学习工程系列文章列表: 视音频编解码学习工程:H.264分析器 视音频编解码学习 ...
- 我的开源项目:TS封装格式分析器
本文介绍一个自己的开源小项目:TS封装格式分析器.TS全称是 MPEG 2 Transport Stream(MPEG2 传输流),广泛用于广播电视系统,比如说数字电视,以及IPTV.我这个项目规模不 ...
- 【网络通信 -- 直播】音视频常见封装格式 -- MEPG2 TS
[网络通信 -- 直播]音视频常见封装格式 -- MEPG2 TS [1]相关码流基本概念 ES 流(Elementary Stream)基本码流,直接取自编码器的数据流,可以为音频(AAC 等).视 ...
- 视音频编解码学习工程:FLV封装格式分析器
===================================================== 视音频编解码学习工程系列文章列表: 视音频编解码学习工程:H.264分析器 视音频编解码学习 ...
- FLV封装格式分析器
===================================================== 视音频编解码学习工程系列文章列表: 视音频编解码学习工程:H.264分析器 视音频编解码学习 ...
- MPEG-TS封装格式
先讲一下 MPEG 是什么,MPEG 全称 Moving Picture Experts Group (动态影像专家小组),该专家组是联合技术委员会(Joint Technical Committee ...
- 【音视频零基础入门 1】视频播放器原理、流媒体协议、封装格式、视频编码、音频编码
[音视频零基础入门 1]视频播放器原理.流媒体协议.封装格式.视频编码 一.视频播放器原理 1.1 解协议 1.2 解封装 1.3 解码 1.4 视音频同步 二.流媒体协议 三.封装格式 四.封装格式 ...
- TS流格式小白入门解读
一.背景介绍 之前我做了一个项目,要求写一个TS流解析的模块,因此看了ISOIEC 13818-1文档,外加很多人的博客来帮助理解,来了解TS流格式是个什么东西,收货颇多.因此我觉得是时候发点干货回馈 ...
- 【多媒体封装格式详解】---ASF(WMV/WMA)
[1] ASF全称Advanced Systems Format 高级串流格式,微软出的一种开放封装格式的标准.它可以包含很多内容如:音视频.脚本命令.JPEG.二进制文件.或是由开发者自己定义的内容 ...
最新文章
- 关于c语言程序开发过程 下面说法错误的是,c语言笔试真题
- Springmvc配置定时任务注解开发
- 微信在公众号增开了新广告位 这次是在图文消息头部
- SVN目录结构及作用
- 授予数据库账号dba权限_数据库用户和权限
- IntelliJ IDEA——数据库集成工具(Database)的使用
- 中科大410分计算机排名第几,2021考研成绩发布:中科大400分无缘复试,中山大学321分登顶第二...
- frp源码剖析-frp中的log模块
- 小甲鱼Python教程学习笔记(一)
- Gephi教程——基本操作
- 俄罗斯方块(C语言源代码)
- 在Python应用中Telegram 机器人搭建消息提醒
- 【媒体管家】媒体邀约以及媒介投放策略
- 硬盘磁头坏数据有办法恢复吗?硬盘开盘数据恢复
- HCNA安全学习笔记
- Java 内部类 面试“变态题”
- Windows下运行war包
- Android存储访问框架的使用
- matlab 2022更新
- SpringBoot 在启动时执行某些方法
热门文章
- 阿里云服务 宝塔建站 详细避坑
- composer安装及配置(Windows)
- 【C++】求平均数问题
- TechSmith Snagit for mac(最强大的屏幕截图软件)
- 内外网数据安全摆渡的5种方式介绍及对比
- 微信小程序图片下边阴影
- 最强大的视频弹幕引擎——烈焰弹幕使(DanmakuFlameMaster)使用指南
- 【antd pro】关于 drawer 使用的一些思考
- C语言程序设计大笨钟,《太空序曲》作者:(英) 阿瑟·C·克拉克.pdf
- java每隔 消费队列数据_消费者Rebalance机制