音视频开发26 FFmpeg 音频重采样实战化思考前提 - 时间问题整理,avcodec_send_packet源码分析,avcodec_receive_frame源码分析

time_base 、pts、dts、duration

  • time_base:时间基,所谓时间基表示的就是每个刻度是多少秒 ,例如
    • 如果把1秒分为25等份,你可以理解就是一把尺,那么每一格表示的就是1/25秒。此时的time_base={1,25} ,
    • 如果你是把1秒分成90000份,每一个刻度就是1/90000秒,此时的time_base={1,90000}。
    • 在ffmpeg中。av_q2d(time_base)=每个刻度是多少秒
  • PTS:Presentation Time Stamp。PTS主要用于度量解码后的视频帧什么时候被显示出来,即显示时间戳,某一帧视频什么时候开始显示
    • pts的值就是占多少个时间刻度(占多少个格子)。它的单位不是秒,而是时间刻度。
  • DTS:Decode Time Stamp。DTS主要是标识读入内存中的帧数据流在什么时候开始送入解码器中进行解码,即解码时间戳
  • duration:某一帧视频显示持续时间,duration和pts单位一样,duration表示当前帧的持续时间占多少格。

1.代码中的 不同结构体中的 time_base,pts,dts,duration的含义到底是啥?

在学习这块的时候,发现AVPacket,AVFrame,AVStream中都有time_base,duration,AVPacket,AVFrame中都有pts 和 dts,如果搞不清这些值的真正含义,则有可能在计算的时候出现bug,且不知道怎么fix。

思路:弄一个mp4文件或者mp3文件,解复用,然后解码,中间debug看AVPacket,AVFrame,AVStream中间的这些值。当前测试用了一个mp3文件,主要是先测试一个纯音频。再测试一个h264的,最后再搞一个音视频结合的mp4.

我们就按照这个思路来学习,。先搞个mp3测试一下

1. 1Debug代码:

int DDUtils::MP3toH264(MP3Class & mp3instance, H264class & h264instance){
    //测试将mp3变成pcm数据。
    char *outfilename = "D:/ffplayresource/MP3TO.pcm";
    FILE *outfile = fopen(outfilename, "wb");

    cout<<"func MP3toH264 call start "<<endl;
    int ret =0;

    //解码相关,解码的很多参数是从 mp3instance文件中获得的,因此开始的时候,不能直接设置mp3instance的相关值。
    AVFormatContext* avFormatInputFileContext = nullptr;
    AVStream* mp3avstrem = nullptr;
    const AVCodec* mp3decoder = nullptr ;
    AVCodecContext* mp3decodercontext = nullptr;
    AVPacket* mp3avpacket = nullptr;
    AVFrame* mp3avframe = nullptr;


    //编码相关,我们编码是要变成aac,这是确定的,因此很多参数都是可以直接设定。
    //    AVFormatContext* avFormatOutputFileContext = nullptr;
    //    const AVOutputFormat *avOutputFormat = av_guess_format(nullptr, h264instance.filename.c_str(), nullptr);


    //音频重采样相关
    SwrContext * swrcontext = nullptr;


    //    AVAudioFifo 相关,我们的流程是这样的,将 源文件最终的pcm 通过 重采样成 目标文件的pcm,然后将本应该存到目标文件的pcm,存储到avaduiofifo
    AVAudioFifo *avaduiofifo = nullptr;


    if(mp3instance.filename == ""){

    }
    //第一步 负责申请一个AVFormatContext 结构的内存,并进行简单初始化
    avFormatInputFileContext = avformat_alloc_context();
    if(!avFormatInputFileContext){
        ret = -1;
        ERROR_BUF(ret);
        cout<<"error: avformat_alloc_context error"<<endl;
        goto ddutilsend;
    }


    //第二步:打开媒体文件并获取媒体文件信息的函数
    ret = avformat_open_input(&avFormatInputFileContext, (const char *)mp3instance.filename.c_str(), nullptr, nullptr);
    if(ret != 0){
        cout<<"error: avformat_open_input error"<<endl;
        ERROR_BUF(ret)
                goto ddutilsend;
    }

    //第三步 avformat_find_stream_info():获取音视频文件信息,
    //avformat_find_stream_info()函数是用于获取媒体文件中每个音视频流的详细信息的函数,包括解码器类型、采样率、声道数、码率、关键帧等信息

    ret = avformat_find_stream_info(avFormatInputFileContext,nullptr);
    if(ret < 0){
        cout<<"error: avformat_find_stream_info error"<<endl;
        ERROR_BUF(ret)
                goto ddutilsend;
    }

    //第四步,找到最合适的audio 流
    mp3instance.best_audio_index = av_find_best_stream(avFormatInputFileContext,AVMEDIA_TYPE_AUDIO,-1,-1,nullptr,0);

    if(mp3instance.best_audio_index <0 ){
        ret = mp3instance.best_audio_index;
        cout<<"error: av_find_best_stream find audio error"<<endl;
        ERROR_BUF(ret)
                goto ddutilsend;
    }

    av_dump_format(avFormatInputFileContext, 0, mp3instance.filename.c_str(), 0);

    cout<<"func MP3toH264 call 111111 mp3instance.best_audio_index = "<< mp3instance.best_audio_index << endl;



    //第五步:找到解码器。到这里我们已经对于mp3的流进行了解封装,那么下来就应该对mp3进行解码了,解码是为了得到 pcm流,然后转换
    //5.1 那么第一个问题就是,我们怎么知道这个mp3文件用什么解码合适呢?这里就要用到 AVStream->codecpar->codec_id了。
    mp3avstrem = avFormatInputFileContext->streams[mp3instance.best_audio_index];
    cout<<"111"<<endl;
    printfAVStream(mp3avstrem);
    cout<<"222"<<endl;
    cout<<" mp3avstrem->codecpar->codec_id = " << mp3avstrem->codecpar->codec_id <<endl;
    
    mp3decoder  = avcodec_find_decoder(mp3avstrem->codecpar->codec_id);
    if(!mp3decoder){
        ret = -3;
        cout<<"error: avcodec_find_decoder find audio error"<<endl;
        goto ddutilsend;
    }

    //第六步:关联解码器上下文 avcodec_alloc_context3(): 分配解码器上下文
    mp3decodercontext = avcodec_alloc_context3(mp3decoder);
    if(!mp3decodercontext){
        ret = -4;
        cout<<"error: avcodec_alloc_context3 mp3decoder  error"<<endl;
        goto ddutilsend;
    }

    //第七步,给给解码器上下文添加参数, avcodec_parameters_to_context():

    ret = avcodec_parameters_to_context(mp3decodercontext, mp3avstrem->codecpar);

    if(ret < 0){
        cout<<"error: avcodec_parameters_to_context   error"<<endl;
        ERROR_BUF(ret)
                goto ddutilsend;
    }

    //第八步:打开编解码器 avcodec_open2():

    ret = avcodec_open2(mp3decodercontext,mp3decoder,nullptr);

    if(ret < 0){
        cout<<"error: avcodec_open2   error"<<endl;
        ERROR_BUF(ret)
                goto ddutilsend;
    }

    //第十步,音频重采样相关
    swrcontext = swr_alloc();



    //第九步:到这里,就可以开始读取数据了,读取的数据要存储在avpacket中,因此先要调用av_packet_alloc创建一个avpacket
    //9.1 创建avpacket 和 avframe
    mp3avpacket =  av_packet_alloc();
    mp3avframe = av_frame_alloc();

    if(!mp3avpacket){
        ret = -5;
        cout<<"error: av_packet_alloc error"<<endl;
        goto ddutilsend;
    }
    //9.2 从avFormatContext 对应的file 中读取数据,ffmpeg中的av_read_frame()的作用是读取码流中的音频若干帧或者视频一帧
    // @return 0 if OK, < 0 on error or end of file
    while(av_read_frame(avFormatInputFileContext, mp3avpacket) >=0){


        //额外的说明,这时候mp3avpacket中存储的是压缩过的mp3格式的数据,如果我们是从mp4文件读取的avpacket,这时候mp3avpacket中应该存储的aac文件,
        //那么就可以直接存储这个aac文件,如果想不起来,可以回顾0702 从mp4文件中抽取 aac,这是因为从mp4中获取的是aac,和要存储的aac是一样的,加上aac需要的头部 adts_header,就可以生成需要的aac
        //可惜我们这里的目的是:

        //9.2.1那么这时候 mp3avpacket 中就有数据了,那么下来需要将这些 avpacket 的数据发送到解码器,发送完成后,需要调用av_packet_ubref 将 refcount减去1
        // debug 位置1
        avcodec_send_packet(mp3decodercontext,mp3avpacket);
        av_packet_unref(mp3avpacket);


        //9.2.2 这时候就可以从解码器中拿数据了,注意的是,一次send可能对应多次receive,因此这里也要用一个循环

        for(;;)
        {
            //从线程中获取解码接口,一次send可能对应多次receive,读取的数据这时候就已经到了mp3avframe中了,返回值是0表示成功。
            ret = avcodec_receive_frame(mp3decodercontext, mp3avframe);
            if (ret != 0) break;
            // debug 位置2
            static int s_print_format = 0;
            static int data_size =0;
            static int isplanar = 0;
            //根据自己在该方法前面加的log打印,就会明白,这里为什么要有一个 static int s_print_format,因为这个方法会不停的走进来,打印的太多了
            if(s_print_format == 0)
            {
                data_size = av_get_bytes_per_sample((enum AV
### 使用 `avcodec_send_packet` 和 `avcodec_receive_frame` 在 FFmpeg 库中,`avcodec_send_packet` 函数用于向解码器发送编码后的数据包以便处理。而 `avcodec_receive_frame` 则是从解码器获取已解码的数据帧。 #### 初始编解码上下文 为了能够正常使用这两个函数,首先需要初始一个合适的 `AVCodecContext` 实例并打开相应的编解码器: ```c // 找到所需的解码器 const AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_H264); if (!codec) { fprintf(stderr, "Failed to find decoder\n"); exit(1); } // 创建一个新的编解码上下文实例 AVCodecContext* codec_context = avcodec_alloc_context3(codec); if (!codec_context) { fprintf(stderr, "Could not allocate video codec context\n"); exit(1); } // 设置参数... int ret; if ((ret = avcodec_open2(codec_context, codec, NULL)) < 0) { fprintf(stderr, "Cannot open codec: %s\n", av_err2str(ret)); exit(1); } ``` #### 向解码器提交数据包 当准备好要解码的数据包之后,可以使用 `avcodec_send_packet` 将其传递给解码器进行处理: ```c AVPacket packet; // 假设已经填充好了packet... ret = avcodec_send_packet(codec_context, &packet); if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) { fprintf(stderr, "Error during decoding: %s\n", av_err2str(ret)); } else if (ret == AVERROR(EAGAIN)){ // 需要更多输入或等待输出可用 } ``` 这里需要注意的是,在某些情况下可能会返回 `EAGAIN` 错误码表示当前无法接受新的数据包直到有空间为止;或者是遇到其他类型的错误时应该适当地处理这些异常情况[^2]。 #### 获取解码完成的视频帧 一旦成功地把数据送入了解码队列里边,则可以通过调用 `avcodec_receive_frame` 来取出已经被完全解析出来的图像信息: ```c AVFrame* frame = av_frame_alloc(); if (!frame) { fprintf(stderr, "Out of memory\n"); exit(1); } do { ret = avcodec_receive_frame(codec_context, frame); if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF){ fprintf(stderr, "Error receiving a frame from the decoder.\n"); break; } if (ret >= 0) { // 处理解码得到的一帧画面 // 渲染或其他操作完成后记得释放资源 av_frame_unref(frame); } } while (ret == EAGAIN || ret == 0); av_frame_free(&frame); ``` 上述代码片段展示了如何持续尝试接收来自解码器的新帧直至不再可能获得更多的有效输出位置。如果此时仍然在未被消耗掉的数据包则应当继续重复此过程来确保所有的媒体流都被正确地转换成可视的形式[^3]。 #### 关闭编解码器 最后不要忘记关闭之前开启过的任何资源以防止内泄漏等问题的发生: ```c avcodec_close(codec_context); av_freep(&codec_context->extradata); avcodec_free_context(&codec_context); ``` 通过以上步骤就可以实现基本的功能需求了。当然实际应用当中还涉及到很多细节上的优以及对于不同场景下的特殊考虑,比如多线程支持、硬件加速等等特性都需要额外关注。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值