欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

TS封装格式解析出ES视频流

程序员文章站 2022-03-22 21:37:28
...

        本博客的目的是手写一个程序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,所以我们只需要遍历到它就可以开始我们的分析啦。

         具体代码结构如下:

TS封装格式解析出ES视频流

        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中。大致的流程图如下:

TS封装格式解析出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,所以会出现下面这样的存储结构:

TS封装格式解析出ES视频流

       每个小方格代表一帧,图有点磕碜,大致这个意思。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

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