[解决思路]关于h264裸流合成mp4时时间戳添加问题

发布时间:2024年01月12日

问题场景:

????????使用GPU编码(Opencv)生成的h264视频片段中不包含时间戳信息,且含有B帧,直接合成mp4后会导致播放出现问题(瞬间播放完成)。因此,在合成时需要手动添加时间戳。

心路历程:

? ? ? ? 发现生成的视频会瞬间播放完成后,意识到是时间戳的问题,检查时间戳代码:

while (av_read_frame(inputFormatContext, &packet) >= 0) {
    if (packet.pts == AV_NOPTS_VALUE){
        // 判断pts是否为空
        // 添加时间戳... 
    }
}

? ? ? ? 一开始没注意到有B帧,想着把pts、dts设置相等,并且按照间隔递增就可以了:

AVStream *inStream = inputFormatContext->streams[packet.stream_index];
AVStream *outStream = outputFormatContext->streams[packet.stream_index];

// 计算两帧之间的间隔,以输入流的时间基计算
int64_t frameDuration = av_rescale_q(1, av_inv_q(inStream->time_base), inStream->r_frame_rate);
// 转换时间基为输出流的时间基
int64_t _t = av_rescale_q(frameDuration, inStream->time_base, outStream->time_base);

// 当前帧的时间戳等于上一帧的时间戳加上两帧之间的间隔
packet.pts = last_pts + _t;
// dts设置成一样
packet.dts = packet.pts;

? ? ? ? 对于只有I和P帧的h264流,上述方法可以领流正常播放,但如果h264中有B帧,则会导致在某些播放器上播放异常——帧显示顺序错误,物体“颤抖”着移动

? ? ? ? 对于含有B帧的视频,其dts和pts应该是按照这种顺序进行的:

IBPBPBP
dts1234567
pts1325476

? ? ? ? 注:dts需要递增,解码需要按照顺序解码。任何一帧的pts应大于等于dts,因为需要先解码后才能显示。因此上述需要调整:

IBPBPBP
dts1234567
pts2436587

? ? ? ? 为了使视频打开时就有画面能播放,因此需要将第一帧的pts调整为0,dts是可以为负数的,但需要保持递增。调整后:

IBPBPBP
dts-1012345
pts0214365

? ? ? ? 以上是最简单的例子,也可以推导出变形:

IBBPBBP
dts-2-101234
pts0231564

? ? ? ? 因此,时间戳的规律和我们的视频中有多少连续的B帧有关

? ? ? ? 以上讨论的假设都是一个帧组中只有一个I帧的情况下,如果一个GOP中有多个I帧时情况会更复杂一点(没做整理,本文暂不讨论):

I(IDR)BPBPBIBPBPI(IDR)...
dts123456789101112
pts132547698111012

? ? ? ??总结:加时间戳的前提是要知道h264文件帧的类型结构(是否有B帧、连续的B帧数量、一个GOP是否有多个I帧)

后记:

? ? ? ? 由于我的文件都是我自己生成的,帧结构都是统一的,因此可以轻松的找出添加时间戳的规律。但对于不同帧结构的原始流,找出统一规律还是比较麻烦,本文不再阐述。

? ? ? ? 附对于最简单的含有B帧的处理代码(IBPBPBPBP):

// 将多个h264文件合并成mp4文件
int mutexMp4File(){
    // 创建输出文件   
    // m_outputFile:输出文件名 xxx.mp4
    AVFormatContext* outputFormatContext = nullptr;
    if (avformat_alloc_output_context2(&outputFormatContext, nullptr, nullptr, m_outputFile.c_str()) < 0) {
        return -1;
    }

    // 准备工作:遍历输入文件列表   
    // m_inputFiles:输入文件名列表
    for (const auto& inputFile : m_inputFiles) {
        // 打开输入文件
        AVFormatContext* inputFormatContext = nullptr;
        if (avformat_open_input(&inputFormatContext, inputFile.c_str(), nullptr, nullptr) != 0) {
            return -1;
        }

        // 查找输入文件的流信息
        if (avformat_find_stream_info(inputFormatContext, nullptr) < 0) {
            avformat_close_input(&inputFormatContext);
            avformat_free_context(outputFormatContext);
            return -1;
        }

        // 复制输入文件的流到输出文件
        for (unsigned int i = 0; i < inputFormatContext->nb_streams; ++i) {
            // 获取输入流
            AVStream* inputStream = inputFormatContext->streams[i];
            // 创建输出流
            AVStream* outputStream = avformat_new_stream(outputFormatContext, nullptr);
            if (outputStream == nullptr) {
                avformat_close_input(&inputFormatContext);
                avformat_free_context(outputFormatContext);
                return -1;
            }
            // 复制输入流的编解码器参数到输出流
            if (avcodec_parameters_copy(outputStream->codecpar, inputStream->codecpar) < 0) {
                avformat_close_input(&inputFormatContext);
                avformat_free_context(outputFormatContext);
                return -1;
            }
            // 设置输出流的编解码器标志为"copy"
            outputStream->codecpar->codec_tag = 0;
        }

        // 关闭输入文件
        avformat_close_input(&inputFormatContext);
    }

    // 打开输出文件
    AVIOContext* outputIOContext = nullptr;
    if (avio_open(&outputFormatContext->pb, m_outputFile.c_str(), AVIO_FLAG_WRITE) < 0) {
        avformat_free_context(outputFormatContext);
        return -1;
    }

    // 写入文件头部
    if (avformat_write_header(outputFormatContext, nullptr) < 0) {
        avformat_free_context(outputFormatContext);
        return -1;
    }

    int64_t gdts_notime = 0;//拼接多个文件的时间戳

    // 遍历输入文件列表,写入数据到输出文件
    for (const auto& inputFile : m_inputFiles) {
        // 打开输入文件
        AVFormatContext* inputFormatContext = nullptr;
        if (avformat_open_input(&inputFormatContext, inputFile.c_str(), nullptr, nullptr) != 0) {
            avformat_free_context(outputFormatContext);
            return -1;
        }
        // 查找输入文件的流信息
        if (avformat_find_stream_info(inputFormatContext, nullptr) < 0) {
            avformat_close_input(&inputFormatContext);
            avformat_free_context(outputFormatContext);
            return -1;
        }

        // 从输入文件读取数据并写入输出文件
        AVPacket packet;
        int64_t p_max_dts = 0;
        int i = 0;
        while (av_read_frame(inputFormatContext, &packet) >= 0) {
            AVStream *inStream = inputFormatContext->streams[packet.stream_index];
            AVStream *outStream = outputFormatContext->streams[packet.stream_index];
            if (packet.pts == AV_NOPTS_VALUE){
                // 起始 只能针对 IBPBPBP 连续B帧为1的流
                int nalu_type = 0,startIndex = 0;
                if (packet.data[0]==0x00 && packet.data[1]==0x00 && packet.data[2]==0x01){
                    nalu_type = 0x1f & packet.data[3];
                    startIndex = 3;
                }else if(packet.data[0]==0x00 && packet.data[1]==0x00 && packet.data[2]==0x00 && packet.data[3]==0x01){
                    nalu_type = 0x1f & packet.data[4];
                    startIndex = 4;
                }
                
                int64_t frameDuration = av_rescale_q(1, av_inv_q(inStream->time_base), inStream->r_frame_rate);
                int64_t _t = av_rescale_q(frameDuration, inStream->time_base, outStream->time_base);

                p_max_dts = _t*(i+1);
                packet.dts = p_max_dts + gdts_notime - _t;
                int xt = (int)packet.data[startIndex+1] & 0xC0;
                if (nalu_type == 0x05 || nalu_type == 0x06 || nalu_type == 0x07 || nalu_type == 0x08){ //I帧开头
                    packet.pts = packet.dts + _t;
                }else if(nalu_type == 0x01 && xt == 0x80){ //P帧
                    packet.pts = packet.dts;
                }else if(nalu_type == 0x01 && xt == 0xC0){ //B帧
                    packet.pts = packet.dts + _t*2;
                }else{
                    // Error!
                    packet.pts = packet.dts;
                }

            }else{
                // Error!
            }
            
            // 将包的流索引设置为输出流索引
            packet.stream_index = inStream->index;
            // 写入输出文件
            av_interleaved_write_frame(outputFormatContext, &packet);
            av_packet_unref(&packet);
            i++;
        }
        gdts_notime += p_max_dts;
        // 关闭输入文件
        avformat_close_input(&inputFormatContext);
    }
    // 写入文件尾部
    av_write_trailer(outputFormatContext);
    // 关闭输出文件
    avio_close(outputFormatContext->pb);
    // 释放资源
    avformat_free_context(outputFormatContext);
    return 0;
}

? ? ? ? 代码中的求帧类型的方法是旁门左道且有针对性的,可能不适合其他场景。

? ? ? ?

????????对于文中任何错误,欢迎指正。

文章来源:https://blog.csdn.net/LucifeR_Shun/article/details/135546772
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。