本博客的目的是手写一个程序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视频流相关推荐

  1. 视音频编解码学习工程:TS封装格式分析器

    ===================================================== 视音频编解码学习工程系列文章列表: 视音频编解码学习工程:H.264分析器 视音频编解码学习 ...

  2. 我的开源项目:TS封装格式分析器

    本文介绍一个自己的开源小项目:TS封装格式分析器.TS全称是 MPEG 2 Transport Stream(MPEG2 传输流),广泛用于广播电视系统,比如说数字电视,以及IPTV.我这个项目规模不 ...

  3. 【网络通信 -- 直播】音视频常见封装格式 -- MEPG2 TS

    [网络通信 -- 直播]音视频常见封装格式 -- MEPG2 TS [1]相关码流基本概念 ES 流(Elementary Stream)基本码流,直接取自编码器的数据流,可以为音频(AAC 等).视 ...

  4. 视音频编解码学习工程:FLV封装格式分析器

    ===================================================== 视音频编解码学习工程系列文章列表: 视音频编解码学习工程:H.264分析器 视音频编解码学习 ...

  5. FLV封装格式分析器

    ===================================================== 视音频编解码学习工程系列文章列表: 视音频编解码学习工程:H.264分析器 视音频编解码学习 ...

  6. MPEG-TS封装格式

    先讲一下 MPEG 是什么,MPEG 全称 Moving Picture Experts Group (动态影像专家小组),该专家组是联合技术委员会(Joint Technical Committee ...

  7. 【音视频零基础入门 1】视频播放器原理、流媒体协议、封装格式、视频编码、音频编码

    [音视频零基础入门 1]视频播放器原理.流媒体协议.封装格式.视频编码 一.视频播放器原理 1.1 解协议 1.2 解封装 1.3 解码 1.4 视音频同步 二.流媒体协议 三.封装格式 四.封装格式 ...

  8. TS流格式小白入门解读

    一.背景介绍 之前我做了一个项目,要求写一个TS流解析的模块,因此看了ISOIEC 13818-1文档,外加很多人的博客来帮助理解,来了解TS流格式是个什么东西,收货颇多.因此我觉得是时候发点干货回馈 ...

  9. 【多媒体封装格式详解】---ASF(WMV/WMA)

    [1] ASF全称Advanced Systems Format 高级串流格式,微软出的一种开放封装格式的标准.它可以包含很多内容如:音视频.脚本命令.JPEG.二进制文件.或是由开发者自己定义的内容 ...

最新文章

  1. 关于c语言程序开发过程 下面说法错误的是,c语言笔试真题
  2. Springmvc配置定时任务注解开发
  3. 微信在公众号增开了新广告位 这次是在图文消息头部
  4. SVN目录结构及作用
  5. 授予数据库账号dba权限_数据库用户和权限
  6. IntelliJ IDEA——数据库集成工具(Database)的使用
  7. 中科大410分计算机排名第几,2021考研成绩发布:中科大400分无缘复试,中山大学321分登顶第二...
  8. frp源码剖析-frp中的log模块
  9. 小甲鱼Python教程学习笔记(一)
  10. Gephi教程——基本操作
  11. 俄罗斯方块(C语言源代码)
  12. 在Python应用中Telegram 机器人搭建消息提醒
  13. 【媒体管家】媒体邀约以及媒介投放策略
  14. 硬盘磁头坏数据有办法恢复吗?硬盘开盘数据恢复
  15. HCNA安全学习笔记
  16. Java 内部类 面试“变态题”
  17. Windows下运行war包
  18. Android存储访问框架的使用
  19. matlab 2022更新
  20. SpringBoot 在启动时执行某些方法

热门文章

  1. 阿里云服务 宝塔建站 详细避坑
  2. composer安装及配置(Windows)
  3. 【C++】求平均数问题
  4. TechSmith Snagit for mac(最强大的屏幕截图软件)
  5. 内外网数据安全摆渡的5种方式介绍及对比
  6. 微信小程序图片下边阴影
  7. 最强大的视频弹幕引擎——烈焰弹幕使(DanmakuFlameMaster)使用指南
  8. 【antd pro】关于 drawer 使用的一些思考
  9. C语言程序设计大笨钟,《太空序曲》作者:(英) 阿瑟·C·克拉克.pdf
  10. java每隔 消费队列数据_消费者Rebalance机制