首页 > 其他分享 >FFmpeg解码的软解及硬解(cuda和qsv)使用方法

FFmpeg解码的软解及硬解(cuda和qsv)使用方法

时间:2022-09-30 13:00:11浏览次数:80  
标签:return FFmpeg 软解 frame avcodec ctx hw cuda iRet

本次使用的ffmpeg版本是4.2,解码的调用方式为:

int32_t iRet = -1;
 
// 最后一个包解码完成后,需要取完解码器中剩余的缓存帧;
// 调用avcodec_send_packet时塞空包进去,;
// 解码器就会知道所有包解码完成,再调用avcodec_receive_frame时,将会取出缓存帧;
// AVPacket    packet;
// av_init_packet(&packet);
// pkt.data = NULL;
// pkt.size = 0;
// avcodec_send_packet(ctx, pkt);
 
iRet = avcodec_send_packet(ctx, pkt);
if (iRet != 0 && iRet != AVERROR(EAGAIN)) {
    get_ffmepg_err_str(iRet);
    if (iRet == AVERROR_EOF)
        iRet = 0;
    return iRet;
}
 
while (true) {
    // 每解出来一帧,丢到队列中;
    iRet = avcodec_receive_frame(ctx, frame);
    if (iRet != 0) {
        if (iRet == AVERROR(EAGAIN)) {
            return 0;
        } else
            return iRet;
    }
    PushRenderBuffer();
    // 音频解码后,如果需要重采样,也可以在此处进行resample;
 
}

以前的版本解码方式为:

int32_t iRet = -1;
iRet = avcodec_send_packet(ctx, pkt);
if (iRet != 0 && iRet != AVERROR(EAGAIN)) {
    get_ffmepg_err_str(iRet);
    if (iRet == AVERROR_EOF)
        iRet = 0;
    return iRet;
}
 
avcodec_decode_video2(pCodecCtx, frame, iGotPicture, pkt);

新旧版本更新时,注意接口的使用方法,新版本avcodec_send_packet一次,需要循环调用avcodec_receive_frame多次,返回EAGAIN后,结束当前这次的解码,音频解码也是一样

C++音视频开发学习地址免費】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发

【文章福利】:免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,大厂面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs),免費分享,如有需要的可以加君羊领取哦!~学习交流君羊994289133点击加入领取资料包

一、软件解码:

先上一段代码,再做下说明,这是解码视频文件,以AVStream的方式获取文件信息,再创建解码器

int32_t ret = avformat_open_input(&m_pAVFormatIC, pPath, NULL, NULL);
if (avformat_find_stream_info(m_pAVFormatIC, NULL) < 0) {
    return;
}
for (uint32_t i = 0; i < m_pAVFormatIC->nb_streams; i++) {
    switch (m_pAVFormatIC->streams[i]->codec->codec_type) {
        case AVMEDIA_TYPE_VIDEO:
            // 视频流;
            AVCodec *find_codec = avcodec_find_decoder(m_pAVFormatIC->streams[i]->codec->codec_id);
            AVCodecContext *codec_ctx = avcodec_alloc_context3(find_codec);
            avcodec_parameters_to_context(codec_ctx, m_pAVFormatIC->streams[i]->codecpar);
            codec_ctx->opaque = this;
            codec_ctx->thread_count = 5;
            codec_ctx->thread_safe_callbacks = 1;
            // avcodec_receive_frame的frame数据内存交由自己申请,自己释放,减少内存申请及拷贝;
            codec_ctx->get_buffer2 = DemuxStream::CustomFrameAllocBuffer;
            avcodec_open2(codec_ctx, find_codec, NULL);
            continue;
        case AVMEDIA_TYPE_AUDIO:
            // 音频流;
            // 同上;
            continue;
    }
}

1、m_pAVFormatIC->streams[i]->codec->codec_type 来判断是否包含音频和视频数据,如果是mp3文件,codec_type有Video是表示MP3的封面图片帧;

2、avcodec_find_decoder和avcodec_alloc_context3创建的指针,不需要覆盖stream中的指针,这是错误的用法,stream只是记录当前文件的流信息,不要用来保存context,创建的context由自己来保管;

3、avcodec_parameters_to_context,一定要做,这是把解析到的留信息设置到context,不需要在自己一个参数一个参数的设置;

4、多线程软解,thread_count不修改的话默认值是1,使用单个线程进行解码,遇到一些大文件,比如4K,30帧以上的视频流时,解码速度是跟不上的,有两种方案:多线程软解或者硬件解码(后面说)

thread_count为0,表示由ffmpeg调用最大线程数来进行软解,我的电脑的6核12线程,被创建了13个线程进行解码,实际上这会资源过剩,可以根据实际使用情况设置指定线程数量进行解码,这里我设置的是5,

切记!在设置thread_count大于1的值时,一定要把thread_safe_callbacks设置为1,否则的话必定会产生崩溃;

默认时,thread_safe_callbacks是为0,thread_count是1或者0的时候,都不需要这个标记

5、自行分配avcodec_receive_frame中frame的数据内存

调用avcodec_receive_frame获取到解码完成后的视频帧,如果需要获取视频的数据流,需要将frame中的data拷贝到连续的内存地址中,具体方法:

uint8_t* AllocBufferByFrame(const AVFrame *frame)
{
    if (!frame)
        return nullptr;
    int32_t s = av_image_get_buffer_size((AVPixelFormat)frame->format,
                                         frame->width,
                                         frame->height, 1);
    if (s <= 0)
        return nullptr;
    return new uint8_t[s]{0};
}
 
int32_t FillBufferFromFrame(uint8_t* alloced_buffer, const AVFrame *frame)
{
    if (!alloced_buffer || !frame)
        return -1;
 
    uint8_t* dst_data[4]{};
    int dst_linesize[4]{};
    int32_t ret = av_image_fill_arrays(dst_data, dst_linesize, alloced_buffer,
        (AVPixelFormat)frame->format,
                                       frame->width,
                                       frame->height,
                                       1);
    if (ret <= 0)
        return ret;
    av_image_copy(dst_data, dst_linesize,
        (const uint8_t **)frame->data,
                  frame->linesize,
                  (AVPixelFormat)frame->format,
                  frame->width,
                  frame->height);
    return ret;
}
 
uint8_t *buffer = AllocBufferByFrame(frame);
FillBufferFromFrame(buffer, frame);
// 使用完buffer的地方,释放buffer的内存;

按照这种使用方法,同一帧的数据会申请两份,解码器中申请了一份,拷贝到buffer中一份,增加了内存消耗以及数据拷贝,ffmpeg提供了解码时frame的内存由自己管理的回调接口,get_buffer2,这个回调接口在调用avcodec_receive_frame时,如果自定义了函数指针,将会调用自定义的函数接口,在接口内完成frame的data,linesize,buf的内容填充,在回调时,frame中的format,width,height已经被填充,可以直接拿来使用,注意:在get_buffer2的回调里,flag是AV_GET_BUFFER_FLAG_REF(1),我们申请的内存会被其他frame复用,不能直接释放,应该通过绑定的free接口来释放,我这里申请的是连续内存,回调free的接口每个平面都会调用,只有第一个平面也就是data[0]的时候,才能delete,方法如下:

void DemuxStream::CustomFrameFreeBuffer(void *opaque, uint8_t *data)
{
    //申请的空间是连续的,只有第一个data,也就是data[0]的时候再删除,否则会崩溃;
    int i = (int)opaque;
    if (i == 0)
        delete data;
}
 
int DemuxStream::CustomFrameAllocBuffer(struct AVCodecContext *s, AVFrame *frame, int flags)
{
    int32_t size = av_image_get_buffer_size((AVPixelFormat)frame->format,
                                            frame->width,
                                            frame->height, 1);
    if (size <= 0)
        return -1;
    // 这是由自己申请的内存,avcodec_receive_frame使用完后要自己释放;
    uint8_t *buffer = new uint8_t[size]{0};
    uint8_t* dst_data[AV_NUM_DATA_POINTERS]{};
    int dst_linesize[AV_NUM_DATA_POINTERS]{};
    int32_t ret = av_image_fill_arrays(dst_data, dst_linesize, buffer,
        (AVPixelFormat)frame->format, frame->width, frame->height, 1);
    if (ret < 0) {
        delete[] buffer;
        return ret;
    }
    for (int i = 0; i < AV_NUM_DATA_POINTERS; i++) {
        frame->linesize[i] = dst_linesize[i];
        frame->data[i] = dst_data[i];
        frame->buf[i] = av_buffer_create(frame->data[i],
                                         frame->linesize[i] * frame->height,
                                         DemuxStream::CustomFrameFreeBuffer,
                                         (void *)i, 0);
    }
    return 0;
}
 
// 解码时伪代码;
avcodec_send_packet(ctx, pkt);
while (true) {
    // 每解出来一帧,丢到队列中;
    iRet = avcodec_receive_frame(ctx, frame);
    if (iRet != 0) {
        if (iRet == AVERROR(EAGAIN)) {
            return 0;
        } else
            return iRet;
    }
    push_render_buffer(frame);
    //使用完成后,调用av_frame_unref,内存回收会在CustomFrameFreeBuffer执行;
    //因为自行申请的buffer,交给ffmpeg后,有可能会被复用,不能直接删除buffer;
}

C++音视频开发学习地址免費】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发

【文章福利】:免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,大厂面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs),免費分享,如有需要的可以加君羊领取哦!~学习交流君羊994289133点击加入领取资料包

二、硬件解码

硬件解码需要编译的ffmpeg库支持,具体编译方法就不再此赘述,本次用到的硬件解码为英伟达cuda和英特尔的qsv,硬解解码器的创建跟软解有所不同,使用的过程也只是存在一点差异

1、英伟达,cuda

AVPixelFormat DemuxStream::GetHwFormat(AVCodecContext * ctx, const AVPixelFormat * pix_fmts)
{
    const enum AVPixelFormat *p;
    DemuxStream *pThis = (DemuxStream *)ctx->opaque;
    for (p = pix_fmts; *p != -1; p++) {
        if (*p == pThis->m_AVStreamInfo.hw_pix_fmt) {
            return *p;
        }
    }
 
    fprintf(stderr, "Failed to get HW surface format.\n");
    return AV_PIX_FMT_NONE;
}
AVCodecContext * DemuxStream::GetCudaDecoder(AVStream *stream)
{
    int32_t ret = avformat_open_input(&m_pAVFormatIC, pPath, NULL, NULL);
    if (ret < 0)
        return nullptr;
    if (avformat_find_stream_info(m_pAVFormatIC, NULL) < 0)
        return nullptr;
    ret = av_find_best_stream(m_pAVFormatIC, AVMEDIA_TYPE_VIDEO, -1, -1, &find_codec, 0);
    if (ret < 0)
        return nullptr;
    for (int i = 0;; i++) {
        const AVCodecHWConfig *config = avcodec_get_hw_config(find_codec, i);
        if (!config) {
            // 没找到cuda解码器,不能使用;
            return nullptr;
        }
        if (config->device_type == AV_HWDEVICE_TYPE_CUDA) {
            // 找到了cuda解码器,记录对应的AVPixelFormat,后面get_format需要使用;
            m_AVStreamInfo.hw_pix_fmt = config->pix_fmt;
            m_AVStreamInfo.device_type = AV_HWDEVICE_TYPE_CUDA;
            break;
        }
    }
    AVCodecContext *decoder_ctx = avcodec_alloc_context3(find_codec);
 
    avcodec_parameters_to_context(decoder_ctx, m_pAVFormatIC->stream[video_index]->codecpar);
    decoder_ctx->opaque = this;
    decoder_ctx->get_format = DemuxStream::GetHwFormat;
 
    if (m_AVStreamInfo.hw_device_ctx) {
        av_buffer_unref(&m_AVStreamInfo.hw_device_ctx);
        m_AVStreamInfo.hw_device_ctx = NULL;
    }
    if (av_hwdevice_ctx_create(&m_AVStreamInfo.hw_device_ctx, m_AVStreamInfo.device_type,
                               NULL, NULL, 0) < 0) {
        fprintf(stderr, "Failed to create specified HW device.\n");
        // 创建硬解设备context失败,不能使用;
        return nullptr;
    }
    decoder_ctx->hw_device_ctx = av_buffer_ref(m_AVStreamInfo.hw_device_ctx);
    avcodec_open2(decoder_ctx, find_codec, NULL);
}

avcodec_get_hw_config,从当前硬解的配置中,遍历寻找适用于当前AvCodec的cuda配置,找到后记录cuda解码后的AVPixelFormat,后面解码器初始化时会使用到这个pix_fmt;

设置get_format的回调接口,在回调接口里面,会遍历cuda解码器支持的输出格式,匹配记录的pix_fmt才可以使用,在get_format如果没有找到匹配的硬解设置,可以返回指定类型的软解输出格式,解码器会自动切换到软解进行解码

初始化cuda解码器之前,先创建硬解设备context,赋值AVCodecContext->hw_device_ctx为其引用

至此cuda解码器创建完成,接下来是解码获取视频帧,硬解获得的视频帧后续的处理方式与软解是一样的,硬解比软解多了一步av_hwframe_transfer_data,在avcodec_receive_frame执行完成后,获得的frame中的数据时GPU的数据,是不能直接拿出来用的,需要通过av_hwframe_transfer_data转到内存数据,转换完成后记得把frame的属性通过av_frame_copy_props设置给hw_frame,这里的硬解获取视频帧同样适用于qsv硬解;

iRet = avcodec_send_packet(ctx, pkt);
if (iRet != 0 && iRet != AVERROR(EAGAIN)) {
    get_ffmepg_err_str(iRet);
    if (iRet == AVERROR_EOF)
        iRet = 0;
    return iRet;
}
while (true) {
    // 每解出来一帧,丢到队列中;
    iRet = avcodec_receive_frame(ctx, frame);
    if (iRet != 0) {
        if (iRet == AVERROR(EAGAIN)) {
            return 0;
        } else
            return iRet;
    }
    if (m_pStreamInfo->hw_pix_fmt != AV_PIX_FMT_NONE &&
        m_pStreamInfo->device_type != AV_HWDEVICE_TYPE_NONE) {
        AVFrame *hw_frame = av_frame_alloc();
        iRet = av_hwframe_transfer_data(hw_frame, frame, 0);
        if (iRet < 0) {
            av_frame_unref(hw_frame);
            return iRet;
        }
        av_frame_copy_props(hw_frame, frame);
        // hw_frame中的data,就是硬解完成后的视频帧数据;
    }
}

2、英特尔硬解,qsv

qsv的解码器创建的方式与cuda不同,解码获取视频帧内容与cuda完全一致,解码的部分看上面,qsv的解码器创建和cuda的区别在于,初始化AVCodecContext->hw_frames_ctx,cuda是在avcodec_open2之前创建,qsv是在get_format的回调里创建,并且qsv需要做一些额外的设置;

另外在查找codec_id的时候,需要加上“_qsv”的后缀,这个所支持的qsv的编码格式,可以通过ffmpeg.exe查看到

其他部分与cuda的创建基本一致

AVPixelFormat DemuxStream::GetHwFormat(AVCodecContext * ctx, const AVPixelFormat * pix_fmts)
{
    const enum AVPixelFormat *p;
    DemuxStream *pThis = (DemuxStream *)ctx->opaque;
    for (p = pix_fmts; *p != -1; p++) {
        if (*p == pThis->m_AVStreamInfo.hw_pix_fmt && *p == AV_PIX_FMT_QSV) {
            if (pThis->HwQsvDecoderInit(ctx) < 0)
                return AV_PIX_FMT_NONE;
            return *p;
        }
    }
 
    fprintf(stderr, "Failed to get HW surface format.\n");
    return AV_PIX_FMT_NONE;
}
 
int DemuxStream::HwQsvDecoderInit(AVCodecContext * ctx)
{
    DemuxStream *pThis = (DemuxStream *)ctx->opaque;
    AVHWFramesContext  *frames_ctx;
    AVQSVFramesContext *frames_hwctx;
 
    /* create a pool of surfaces to be used by the decoder */
    ctx->hw_frames_ctx = av_hwframe_ctx_alloc(pThis->m_qsv_device_ref);
    if (!ctx->hw_frames_ctx)
        return -1;
    frames_ctx = (AVHWFramesContext*)ctx->hw_frames_ctx->data;
    frames_hwctx = (AVQSVFramesContext *)frames_ctx->hwctx;
 
    frames_ctx->format = AV_PIX_FMT_QSV;
    frames_ctx->sw_format = ctx->sw_pix_fmt;
    frames_ctx->width = FFALIGN(ctx->coded_width, 32);
    frames_ctx->height = FFALIGN(ctx->coded_height, 32);
    frames_ctx->initial_pool_size = 16;
 
    frames_hwctx->frame_type = MFX_MEMTYPE_VIDEO_MEMORY_DECODER_TARGET;
 
    return av_hwframe_ctx_init(ctx->hw_frames_ctx);
}
 
AVCodecContext * DemuxStream::GetQsvDecoder(AVStream *stream)
{
    AVCodecContext *decoder_ctx = nullptr;
    int ret = av_hwdevice_ctx_create(&m_qsv_device_ref, AV_HWDEVICE_TYPE_QSV,
                                     "auto", NULL, 0);
    if (ret < 0) {
 
        goto failed;
    }
    AVCodec *find_decoder = avcodec_find_decoder(stream->codec->codec_id);
    if (!find_decoder) {
        goto failed;
    }
    find_decoder = avcodec_find_decoder_by_name((std::string(find_decoder->name) + "_qsv").c_str());
    if (!find_decoder) {
        goto failed;
    }
 
    for (int i = 0;; i++) {
        const AVCodecHWConfig *config = avcodec_get_hw_config(find_decoder, i);
        if (!config) {
            fprintf(stderr, "Decoder %s does not support device type %s.\n",
                    find_decoder->name, av_hwdevice_get_type_name(AV_HWDEVICE_TYPE_QSV));
            goto failed;
        }
        if (config->device_type == AV_HWDEVICE_TYPE_QSV) {
            m_AVStreamInfo.hw_pix_fmt = config->pix_fmt;
            m_AVStreamInfo.device_type = AV_HWDEVICE_TYPE_QSV;
            break;
        }
    }
    if (m_AVStreamInfo.device_type == AV_HWDEVICE_TYPE_NONE ||
        m_AVStreamInfo.hw_pix_fmt == AV_PIX_FMT_NONE)
        goto failed;
    decoder_ctx = avcodec_alloc_context3(find_decoder);
    if (!decoder_ctx)
        goto failed;
    if (avcodec_parameters_to_context(decoder_ctx, stream->codecpar) < 0)
        goto failed;
    decoder_ctx->opaque = this;
    decoder_ctx->get_format = DemuxStream::GetHwFormat;
    ret = avcodec_open2(decoder_ctx, NULL, NULL);
    if (ret < 0)
        goto failed;
    return decoder_ctx;
failed:
    m_AVStreamInfo.hw_pix_fmt = AV_PIX_FMT_NONE;
    m_AVStreamInfo.device_type = AV_HWDEVICE_TYPE_NONE;
    av_buffer_unref(&m_qsv_device_ref);
    m_qsv_device_ref = nullptr;
    avcodec_free_context(&decoder_ctx);
    return nullptr;
}

ref:FFmpeg解码的软解及硬解(cuda和qsv)使用方法 - 资料 - 音视频开发中文网 - 构建全国最权威的音视频技术交流分享论坛

标签:return,FFmpeg,软解,frame,avcodec,ctx,hw,cuda,iRet
From: https://www.cnblogs.com/lidabo/p/16744580.html

相关文章

  • ffmpeg5.0+h264+h265 windows下编译方法
    目录前言一,源码包下载1.ffmpeg下载2.下载x264代码3.下载x265代码二,MSYS2安装三,安装cmake工具四,代码编译1.编译x2642.编译x2653.编译ffmpeg五,功能验证1.x264......
  • ffmpeg用法及命令使用总结
      一  用途1  视频格式转码转换  2  音频格式转码转换   https://blog.csdn.net/weixin_40855193/article/details/122903152   ffmpeg-......
  • 9、FFmpeg使用clion+mingw32编译学习y420p,yuv,rgb编码
    基本思想:继续学习ffmpeg基础知识第一步:进行y420p解码,然后将数据转rgb24格式,显示,重点学习了sws_getContext函数,可以通过他进行各种转码cmakelists.txt文件内容cmake_minimum_......
  • 50、ubuntu18.04&20.04+CUDA11.1+cudnn11.3+TensorRT7.2+Deepsteam5.1+vulkan环境搭建
    基本思想:想学习一下TensorRT的使用,随笔记录一下;链接:https://pan.baidu.com/s/1uFOktdF-bHcDDsufIqmNSA 提取码:k55w 复制这段内容后打开百度网盘手机App,操作更方便哦记录......
  • 基于CUDA查询显卡型号和显存大小
    安装驱动后,我们可以用nvidia-smi命令来获取显卡信息。nvidia-smi的一个比较好的博客nvidia-smi详解nvidia-smiTueSep2711:08:372022+---------------......
  • 在ubuntu的docker中apt-get update更新失败:GPG error: https://developer.download.nv
    在ubuntu的docker中执行apt-getupdate时报错:W:GPGerror:https://developer.download.nvidia.cn/compute/cuda/repos/ubuntu1804/x86_64InRelease:Thefollowings......
  • ffmpeg 使用记录
    这周周末尝试把我硬盘上面的视频文件压缩了一下,但是效果并不理想。其中主要有两个原因,视频本来就是h264的编码,再重新编码也没啥用,因为限制大小的主要是码率ffmpegGPU加......
  • 模板分文件编写,CUDA打印
    ifndefFUN_HPPdefineFUN_HPPifdefined(USE_EXPORT)defineEXPORTexportelsedefineEXPORTendifEXPORTtemplatevoidprint_typeof(Tconst&);if!defined(US......
  • ubuntu显卡驱动安装(更新)-CUDA安装-cuDNN安装
    文中参考了多篇文章,链接已放入文中,可自行点击参看 显卡驱动 CUDA安装和cuDNN安装参考链接第一步,安装cuda,参考到官网找到对应版本进行安装安装完成后再~/.bashrc......
  • AV_PIX_FMT_CUDA 数据转 RGB
    ffmpeg的cuvid解码结果数据格式为AV_PIX_FMT_CUDA,实际使用中后续接算法需要转为RGB。算法跑在显卡上,解码也在显卡上,所以转换也定为直接在显卡上进行。 关于ffmpeg的cuv......