C++ ffmpeg+dxva2实现硬解码

0.前言

参考博客:ffmpeg实现dxva2硬件加速
下载源码:GitHub:https://github.com/Yacov-lu/ffmpeg-DXVA-decode
                  百度网盘:https://pan.baidu.com/s/1fFm4Ra5ka2bPJeIRig14wA?pwd=qwer 
                                   提取码:qwer
该源码下载后,将播放的视频路径(filename)修改为你自己的,便可直接运行。

1.实现效果

C++ ffmpeg+dxva2实现硬解码_第1张图片

 明显看到使用硬解码后CPU下来了,GPU上去了

2.实际使用步骤

2.1、新建程序并配置

1、重新创建一个MFC的程序 MFCApplication2(基于对话框):
        将源码中的文件夹《D3D》《include》《lib》、文件《D3DVidRender.h》《D3DVidRender.cpp》《ffmpeg_dxva2.h》《ffmpeg_dxva2.cpp》、还有《Debug》文件中的《avcodec-57.dll》《avdevice-57.dll》《avfilter-6.dll》《avformat-57.dll》《avutil-55.dll》《postproc-54.dll》《swresample-2.dll》《swscale-4.dll》拷贝到新建的程序文件中。
2、配置项目环境:
        项目-->属性-->VC++目录-->包含目录:D3D\include;include
        项目-->属性-->VC++目录-->库目录:D3D\lib;lib
        项目-->属性-->链接器-->输入-->附加依赖项:avcodec.lib;avdevice.lib;avfilter.lib;avformat.lib;avutil.lib;swscale.lib;swresample.lib;SDL2.lib;SDL2main.lib;postproc.lib
3、编写步骤:
        3.1、在对话框中拖了一个PictureControl控件【想让视频在该控件中展示】
        3.2、添加一个全局变量:HWND g_hwWnd;
                在OnInitDialog方法中:g_hwWnd1 = GetDlgItem(IDC_STATIC)->m_hWnd;
        3.3、将源码中的 “GetHwFormat”,“ThreadProc” 两个方法拷贝到CMFCApplication2Dlg.cpp文件中
        3.4、引入头文件

2.2、生成时出现问题

2.2.1、问题1:error C2061: 语法错误: 标识符“DXGI_JPEG_AC_HUFFMAN_TABLE”

解决办法:项目-->属性-->VC++目录-->包含目录:$(WindowsSDK_IncludePath);D3D\include;include;$(IncludePath)
参考:warning:4005 DXGI_STATUS_OCCLUDED,宏重定义_superbin的博客-CSDN博客

2.2.2、问题2:'avcodec_decode_video2': 被声明为已否决 

参考:ffmpeg 的各种声明已被否决,整理_顺其自然~的博客-CSDN博客_被声明为已否决
当我写这博客想复现问题的时候,它居然又不报错了,想不通。。。

2.2.3、问题3:执行到sws_getContext异常退出

调试发现:dxva2_init 返回值 -22,而源码执行发现返回值0;修改添加:C++ ffmpeg+dxva2实现硬解码_第2张图片

修改后 dxva2_init 返回0 ,便不再执行 sws_getContext 这段代码

2.3、本程序解码流程

2.3.1、int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

打开多媒体数据并且获取一些参数信息
ps:函数调用成功之后处理过的AVFormatContext结构体。
file:打开的视音频流的URL。
fmt:强制指定AVFormatContext中AVInputFormat的。这个参数一般情况下可以设置为NULL,这样FFmpeg可以自动检测AVInputFormat。
dictionay:附加的一些选项,一般情况下可以设置为NULL。

2.3.2、int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

读取一部分视音频数据并且获得一些相关的信息(AVStream)
ic:输入的AVFormatContext结构体。
options:额外的选项,目前没有深入研究过。

2.3.3、int av_find_best_stream(AVFormatContext *ic,
                        enum AVMediaType type,
                        int wanted_stream_nb,
                        int related_stream,
                        AVCodec **decoder_ret,
                        int flags);

获取音视频对应的流索引(stream_index)及存储编解码器信息的结构体AVCodec
ic:媒体文件句柄
type:流类型:视频、音频、字幕等。
Wanted_stream_nb:用户请求的流号, 或 -1 用于自动选择
related_stream:尝试查找相关的流(例如,在相同的program) 到这个,或者 -1 如果没有
decoder_ret:如果非空,返回解码器选定的流
flags:标志;目前没有定义
返回:音视频对应的流索引(stream_index)

2.3.4、AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

申请AVCodecContext空间。需要传递一个编码器,也可以不传,但不会包含编码器。

2.3.5、int avcodec_parameters_to_context(AVCodecContext *codec,
                                  const AVCodecParameters *par);

该函数用于将流里面的参数,也就是AVStream里面的参数直接复制到AVCodecContext的上下文当中。

2.3.6、int dxva2_init(AVCodecContext *s, HWND hwnd);

这步骤就是配置硬解码,函数dxva2_init是初始化配置dxva2解码器的入口,配置工作主要就是由它来完成
hwnd:窗口句柄

2.3.7、int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

用于初始化一个视音频编解码器的 AVCodecContext
avctx:需要初始化的 AVCodecContext。
codec:输入的AVCodec。
options:一些选项。例如使用libx264编码的时候,“preset”,“tune”等都可以通过该参数设置。

2.3.8、int av_read_frame(AVFormatContext *s, AVPacket *pkt);

读取码流中的音频若干帧或者视频一帧
s: 文件格式上下文,输入的AVFormatContext
pkt:这个值不能传NULL,必须是一个空间,输出的AVPacket
返回值:return 0 is OK, <0 on error or end of file

2.3.9、int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

发送视频一帧到解码器中。ret=0:成功

2.3.10、int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

从解码器中获取解码的输出数据。ret=0:成功,返回一帧数据

2.3.11、int dxva2_retrieve_data_call(AVCodecContext *s, AVFrame *frame);

获取数据同时直接渲染

2.3.12、释放资源

av_packet_unref(AVPacket *pkt);   参考讲解:不释放会造成内存泄露
avcodec_flush_buffers(AVCodecContext *avctx);    在再次解码之前,必须重新编码。
avcodec_close(AVCodecContext *avctx);    参考讲解:关闭编码器
avcodec_free_context(AVCodecContext **avctx); 释放解码器上下文,包含了avcodec_close()
av_frame_free(AVFrame **frame);   参考讲解:不释放会造成内存泄露
avformat_close_input(AVFormatContext **s); 参考讲解:关闭AVFormatContext

3.主要代码 

    char* url = "D:\\GoogleDownload\\video-h265.mkv";
    int ret;
    /******************获取码流参数信息******************/
    AVFormatContext* fmt_ctx = NULL;    //包含码流参数较多的结构体
   
    /***
    * 打开多媒体数据并且获取一些参数信息
    * ps:函数调用成功之后处理过的AVFormatContext结构体。
    * file:打开的视音频流的URL。
    * fmt:强制指定AVFormatContext中AVInputFormat的。这个参数一般情况下可以设置为NULL,这样FFmpeg可以自动检测AVInputFormat。
    * dictionay:附加的一些选项,一般情况下可以设置为NULL。
    **/
    ret = avformat_open_input(&fmt_ctx, url, NULL, NULL);   ///1.
    if (ret < 0)
    {
        fprintf(stderr, "Could not open input\n");
        goto end;
    }
    /***
    * 读取一部分视音频数据并且获得一些相关的信息(AVStream)
    * ic:输入的AVFormatContext。
    * options:额外的选项,目前没有深入研究过。
    **/
    ret = avformat_find_stream_info(fmt_ctx, NULL);         ///2.
    if (ret < 0)
    {
        fprintf(stderr, "Could not find stream information\n");
        goto end;
    }


    AVCodec* pCodec = NULL;             // 存储编解码器的结构体
    /***
    * 获取音视频对应的流索引(stream_index)
    **/
    ret = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &pCodec, 0);    ///3.
    if (ret < 0)
    {
        fprintf(stderr, "Cannot find a video stream in the input file\n");
        goto end;
    }
    int video_stream = ret;             // 视频对应流索引

   

    ///***************获取解码器并解码*************************/
    if (pCodec == NULL)
        pCodec = avcodec_find_decoder(fmt_ctx->streams[video_stream]->codecpar->codec_id);// 通过ID号查找解码器


    AVCodecContext* pCodecCtx = NULL;   // 解码器上下文       
    pCodecCtx = avcodec_alloc_context3(pCodec);           ///4.                                // 配置解码器,申请AVCodecContext空间。需要传递一个编码器,也可以不传,但不会包含编码器。
    avcodec_parameters_to_context(pCodecCtx, fmt_ctx->streams[video_stream]->codecpar);   ///5.// 该函数用于将流里面的参数,也就是AVStream里面的参数直接复制到AVCodecContext的上下文当中。
  

    配置硬解码    
    switch (pCodec->id)
    {
        case AV_CODEC_ID_MPEG2VIDEO:
        case AV_CODEC_ID_H264:
        case AV_CODEC_ID_VC1:
        case AV_CODEC_ID_WMV3:
        case AV_CODEC_ID_HEVC:
        case AV_CODEC_ID_VP9:
        {
            pCodecCtx->thread_count = 1;  // Multithreading is apparently not compatible with hardware decoding
            InputStream* ist = new InputStream();
            ist->hwaccel_id = HWACCEL_AUTO;
            ist->active_hwaccel_id = HWACCEL_AUTO;
            ist->hwaccel_device = "dxva2";
            ist->dec = pCodec;
            ist->dec_ctx = pCodecCtx;         

            pCodecCtx->coded_width = pCodecCtx->width;
            pCodecCtx->coded_height = pCodecCtx->height;

            pCodecCtx->opaque = ist;
            if (dxva2_init(pCodecCtx, g_hwWnd1) == 0)    ///6.
            {
                pCodecCtx->get_buffer2 = ist->hwaccel_get_buffer;
                pCodecCtx->get_format = GetHwFormat;
                pCodecCtx->thread_safe_callbacks = 1;

                break;
            }

            break;
        }
        default:
            break;
    }
    
    ret = avcodec_open2(pCodecCtx, pCodec, NULL);           ///7.                         // 初始化一个视音频编解码器的AVCodecContext
    if (ret < 0)
    {
        fprintf(stderr, "Cannot open decode\n");
        goto end;
    }

    AVPacket packet;                    // 解码前的音频或者视频数据
    AVFrame* frame = av_frame_alloc();  // 用来存储解码后的(或原始)音频或视频数据
                                        // 必须由av_frame_alloc()分配内存,同时必须由av_frame_free()释放
    while (m_threadLoop)    //循环读取
    {    

        if ((ret = av_read_frame(fmt_ctx, &packet)) < 0)   //读取码流中的音频若干帧或者视频一帧
        {
            //av_read_frame 会产生 6MB 的堆内存。
            //如果不进行 av_packet_unref,则会导致内存泄漏。
            //即使是同一个栈变量 pkt,即使出了这个栈变量的作用范围、这个栈变量被系统收回,那些每次产生的 6MB 堆内存们,也不会被收回。
           
            break;
        }
        if (video_stream == packet.stream_index)
        {
            ret = avcodec_send_packet(pCodecCtx, &packet); //发送视频一帧到解码器中
            if (ret < 0)
            {
                av_packet_unref(&packet);           // 将缓存空间的引用计数-1,并将Packet中的其他字段设为初始值。如果引用计数为0,自动的释放缓存空间。
                avcodec_flush_buffers(pCodecCtx);   // 清空内部缓存的帧数据
                continue;
            }

            while (ret >= 0)
            {
                ret = avcodec_receive_frame(pCodecCtx, frame);    // 从解码器中获取解码的输出数据。ret=0:成功,返回一帧数据
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) // AVERROR(EAGAIN):当前输出无效,用户必须发送新的输入,AVERROR_EOF:解码器已经完全刷新,当前没有多余的帧可以输出
                {
                    break;
                } 
                else if (ret < 0)                                 // 对应其他的解码错误
                {
                    break;
                }


                //获取数据同时渲染
                dxva2_retrieve_data_call(pCodecCtx, frame);
                
                Sleep(30u);
                av_packet_unref(&packet);

            }
        }
        av_packet_unref(&packet);
    }

    av_packet_unref(&packet);

    avcodec_flush_buffers(pCodecCtx);
    avcodec_close(pCodecCtx);  //close,如果为rk3399的硬件编解码,则需要等待MPP_Buff释放完成后再关闭?是否需要这样不知道

end:
    av_frame_free(&frame);
    avformat_close_input(&fmt_ctx);
    avcodec_free_context(&pCodecCtx);

4.源码下载

你可能感兴趣的:(MFC,C++,ffmpeg,dxva2)