如何将视频文件.h264和音频文件.mp3复用为输出文件output.mp4?

2023-10-22 13:54:06 浏览数 (2)

一.初始化复用器

  在这个部分我们可以分三步进行:(1)打开输入视频文件上下文句柄 (2)打开输入音频文件上下文句柄 (3)打开输出文件上下文句柄

  1.打开输入视频文件上下文句柄

    在这一步,我们主要用到两个重要的函数:av_find_input_format()和avformat_open_input()。我们先调用av_find_input_format函数得到输入视频文件的格式,然后将该格式和视频文件的路径传入avformat_open_input()函数,就可以打开输入视频文件的上下文句柄。下面给出代码:

代码语言:javascript复制
#define STREAM_FRAME_RATE 25
static AVFormatContext *video_fmt_ctx= nullptr,*audio_fmt_ctx= nullptr,*output_fmt_ctx= nullptr;
static AVPacket *pkt;
static int32_t in_video_st_idx=-1,in_audio_st_idx=-1;
static int32_t out_video_st_idx=-1,out_audio_st_idx=-1;
static int32_t init_input_video(const char *video_input_file,const char *video_format){
    int32_t result=0;
    const AVInputFormat *video_input_format= av_find_input_format(video_format);
    if(!video_input_format){
        cerr<<"Error:av_find_input_format failed."<<endl;
        return -1;
    }
    result= avformat_open_input(&video_fmt_ctx,video_input_file,video_input_format, nullptr);
    if(result<0){
        cerr<<"Error:avformat_open_input failed."<<endl;
        return -1;
    }
    result=avformat_find_stream_info(video_fmt_ctx, nullptr);
    if(result<0){
        cerr<<"Error:avformat_find_stream_info failed."<<endl;
        return -1;
    }
    return 0;
}

  2.打开输入音频文件上下文句柄

    打开输入音频文件上下文句柄的方法和上面的输入视频文件类似,直接上代码:

代码语言:javascript复制
static int32_t init_input_audio(const char *audio_input_file,const char *audio_format){
    int32_t result=0;
    const AVInputFormat *audio_input_format= av_find_input_format(audio_format);
    if(!audio_input_format){
        cerr<<"Error:av_find_input_format failed."<<endl;
        return -1;
    }
    result= avformat_open_input(&audio_fmt_ctx,audio_input_file,audio_input_format, nullptr);
    if(result<0){
        cerr<<"Error:avformat_open_input failed."<<endl;
        return -1;
    }
    result=avformat_find_stream_info(audio_fmt_ctx, nullptr);
    if(result<0){
        cerr<<"Error:avformat_find_stream_info failed."<<endl;
        return -1;
    }
    return 0;
}

  3.打开输出文件上下文句柄

    打开输出文件上下文句柄需要调用函数avformat_alloc_output_context2(),在创建了输出文件上下文句柄后,我们需要添加一路音频流和一路视频流,此时我们需要用到函数avformat_new_stream();在调用此函数后,我们会得到AVStream *类型的指针。然后,我们需要将输入视频文件和音频文件的编码器相关参数复制到输出的视频流和音频流编码器中。最后,打开输出文件,将文件的I/O结构对应到输出文件的AVFormatContext结构。代码如下:

代码语言:javascript复制
static int32_t init_output(const char *output_file){
    int32_t result=0;
    avformat_alloc_output_context2(&output_fmt_ctx, nullptr, nullptr,output_file);
    if(!output_fmt_ctx){
        cerr<<"Error:avformat_alloc_output_context2 failed."<<endl;
        return -1;
    }
    const AVOutputFormat *fmt=output_fmt_ctx->oformat;
    cout<<"Default video codec id:"<<fmt->video_codec<<", audio codec id:"<<fmt->audio_codec<<endl;
    AVStream *video_stream= avformat_new_stream(output_fmt_ctx, nullptr);
    if(!video_stream){
        cerr<<"Error:add video stream to output format context failed."<<endl;
        return -1;
    }
    out_video_st_idx=video_stream->index;
    in_video_st_idx= av_find_best_stream(video_fmt_ctx,AVMEDIA_TYPE_VIDEO,-1,-1, nullptr,0);
    if(in_video_st_idx<0){
        cerr<<"Error:find video stream in input video file failed."<<endl;
        return -1;
    }
    result= avcodec_parameters_copy(video_stream->codecpar,video_fmt_ctx->streams[in_video_st_idx]->codecpar);
    if(result<0){
        cerr<<"avcodec_parameters_copy failed."<<endl;
        return -1;
    }
    video_stream->id=output_fmt_ctx->nb_streams-1;
    video_stream->time_base=AVRational {1,STREAM_FRAME_RATE};
    AVStream *audio_stream= avformat_new_stream(output_fmt_ctx, nullptr);
    if(!audio_stream){
        cerr<<"Error:add audio stream to output format context failed."<<endl;
        return -1;
    }
    out_audio_st_idx=audio_stream->index;
    in_audio_st_idx= av_find_best_stream(audio_fmt_ctx,AVMEDIA_TYPE_AUDIO,-1,-1, nullptr,0);
    if(in_audio_st_idx<0){
        cerr<<"Error:find audio stream in input audio file failed."<<endl;
        return -1;
    }
    result= avcodec_parameters_copy(audio_stream->codecpar,audio_fmt_ctx->streams[in_audio_st_idx]->codecpar);
    if(result<0){
        cerr<<"Error:copy audio codec parameters failed."<<endl;
        return -1;
    }
    audio_stream->id=output_fmt_ctx->nb_streams-1;
    audio_stream->time_base=AVRational {1,audio_stream->codecpar->sample_rate};
    av_dump_format(output_fmt_ctx,0,output_file,1);
    cout<<"Output video idx:"<<out_video_st_idx<<",audio idx:"<<out_audio_st_idx<<endl;
    if(!(fmt->flags&AVFMT_NOFILE)){//判断AVFMT_NOFILE标志位是否设置在fmt->flags中
        result= avio_open(&output_fmt_ctx->pb,output_file,AVIO_FLAG_WRITE);
        if(result<0){
            cerr<<"Error:avio_open failed."<<endl;
            return -1;
        }
    }
    return 0;
}

  下面,给出初始化复用器的完整代码:

代码语言:javascript复制
int32_t init_muxer(const char *video_input_file,const char *audio_input_file,const char *output_file){
    int32_t result= init_input_video(video_input_file,"h264");
    if(result<0){
        return -1;
    }
    result= init_input_audio(audio_input_file,"mp3");
    if(result<0){
        return -1;
    }
    result=init_output(output_file);
    if(result<0){
        return -1;
    }
    return 0;
}

二.复用音频流和视频流

  在这里,我们也可以分三步进行:(1)写入输出文件的头结构 (2)循环写入音频包和视频包 (3)写入输出文件的尾结构

  1.写入输出文件的头结构

    这一步很简单,调用avformat_write_header()函数就可以轻松实现。

  2.循环写入音频包和视频包

    这一步比较复杂,我们首先需要确定音频包和视频包的时间戳,判断写入顺序;这里我们需要比较音频包和视频包的时间戳,如果当前记录的音频时间戳比视频时间戳新,则接下来就应该写入视频数据了。但是,从H.264格式的裸码流中读取的视频包中通常不包含时间戳数据,所以我们需要计算视频包的时间戳。我们可以先计算出每一帧的持续时长,然后乘以帧序号就可以得到这一帧的时间戳了。代码如下:

代码语言:javascript复制
if(pkt->pts==AV_NOPTS_VALUE){
                int64_t frame_duration=(double)AV_TIME_BASE/ av_q2d(in_video_st->r_frame_rate);
                pkt->duration=(double)frame_duration/(double)(av_q2d(in_video_st->time_base)*AV_TIME_BASE);
                pkt->pts=(double)video_frame_idx*pkt->duration;
                pkt->dts=pkt->pts;
                cout<<"frame_duration:"<<frame_duration<<",pkt->duration:"<<pkt->duration<<",pkt->pts:"<<pkt->pts<<endl;
}

  还有一点需要注意的是,从输入文件读取的码流包中保存的时间戳是以输入流的time_base为基准的,在写入输出文件时,需要转换为以输出流的time_base为基准。代码如下:

代码语言:javascript复制
pkt->pts= av_rescale_q_rnd(pkt->pts,input_stream->time_base,output_stream->time_base,(AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt->dts=av_rescale_q_rnd(pkt->dts,input_stream->time_base,output_stream->time_base,(AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt->duration=av_rescale_q(pkt->duration,input_stream->time_base,output_stream->time_base);

  3.写入输出文件的尾结构

    调用av_write_trailer()函数即可

  完整代码如下:

代码语言:javascript复制
int32_t muxing(){
    int32_t result=0;
    int64_t cur_video_pts=0,cur_audio_pts=0;
    AVStream *in_video_st=video_fmt_ctx->streams[in_video_st_idx];
    AVStream *in_audio_st=audio_fmt_ctx->streams[in_audio_st_idx];
    AVStream *output_stream= nullptr,*input_stream= nullptr;
    int32_t video_frame_idx=0;
    result= avformat_write_header(output_fmt_ctx, nullptr);
    if(result<0){
        return result;
    }
    pkt=av_packet_alloc();
    if(!pkt){
        cerr<<"Error:av_packet_alloc failed."<<endl;
        return -1;
    }
    cout<<"Video r_frame_rate:"<<in_video_st->r_frame_rate.num<<"/"<<in_video_st->r_frame_rate.den<<endl;
    cout<<"Video time_base:"<<in_video_st->time_base.num<<"/"<<in_video_st->time_base.den<<endl;
    while(true){
        if(av_compare_ts(cur_video_pts,in_video_st->time_base,cur_audio_pts,in_audio_st->time_base)<=0){
            input_stream=in_video_st;
            result=av_read_frame(video_fmt_ctx,pkt);
            if(result<0){
                av_packet_unref(pkt);
                break;
            }
            if(pkt->pts==AV_NOPTS_VALUE){
                int64_t frame_duration=(double)AV_TIME_BASE/ av_q2d(in_video_st->r_frame_rate);
                pkt->duration=(double)frame_duration/(double)(av_q2d(in_video_st->time_base)*AV_TIME_BASE);
                pkt->pts=(double)video_frame_idx*pkt->duration;
                pkt->dts=pkt->pts;
                cout<<"frame_duration:"<<frame_duration<<",pkt->duration:"<<pkt->duration<<",pkt->pts:"<<pkt->pts<<endl;
            }
            video_frame_idx  ;
            cur_video_pts=pkt->pts;
            pkt->stream_index=out_video_st_idx;
            output_stream=output_fmt_ctx->streams[out_video_st_idx];
        }
        else{
            input_stream=in_audio_st;
            result= av_read_frame(audio_fmt_ctx,pkt);
            if(result<0){
                av_packet_unref(pkt);
                break;
            }
            cur_audio_pts=pkt->pts;
            pkt->stream_index=out_audio_st_idx;
            output_stream=output_fmt_ctx->streams[out_audio_st_idx];
        }
        pkt->pts= av_rescale_q_rnd(pkt->pts,input_stream->time_base,output_stream->time_base,(AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
        pkt->dts=av_rescale_q_rnd(pkt->dts,input_stream->time_base,output_stream->time_base,(AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
        pkt->duration=av_rescale_q(pkt->duration,input_stream->time_base,output_stream->time_base);
        cout<<"Final pts:"<<pkt->pts<<",duration:"<<pkt->duration<<",output_stream->time_base:"<<output_stream->time_base.num<<"/"<<output_stream->time_base.den<<endl;
        if(av_interleaved_write_frame(output_fmt_ctx,pkt)<0){//写入码流包
            cerr<<"Error:failed to mux packet!"<<endl;
            break;
        }
        av_packet_unref(pkt);
    }
    result= av_write_trailer(output_fmt_ctx);
    if(result<0){
        return result;
    }
    return result;
}

三.释放复用器实例

代码语言:javascript复制
void destroy_muxer(){
    avformat_free_context(video_fmt_ctx);
    avformat_free_context(audio_fmt_ctx);
    if(!(output_fmt_ctx->oformat->flags&AVFMT_NOFILE)){
        avio_closep(&output_fmt_ctx->pb);
    }
    avformat_free_context(output_fmt_ctx);
}

四.最终的main函数如下:

代码语言:javascript复制
int main(){
    int32_t result=0;
    result= init_muxer("../input.h264","../input.mp3","../output.mp4");
    if(result<0){
        return result;
    }
    result=muxing();
    if(result<0){
        return result;
    }
    destroy_muxer();
    return 0;
}

  最后,使用以下指令可以播放输出的output.mp4文件:

  ffplay -i output.mp4

0 人点赞