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;                   //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

        部分代码摘自网络,侵权立删。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值