FFmpeg 4.3 H265 十六,硬件GPU解码DXVA2相关

前提

1. 为什么需要硬解码?

一个视频文件或者一路视频流还好,如果增加到64路视频流呢,如果是4K、8K这种高分辨率的视频呢,必须安装上硬解码才是上上策。举个例子在电脑上播放4K以上的H265这类的视频文件,如果不开硬解码,很容易出现卡顿现象,在配置高的电脑也容易出现,毕竟非常的耗CPU资源,来不及刷新,上了硬解码之后,明显流畅的不要不要的,怪不得现在的显卡性能越做越牛逼,就是为了在显示这块尽可能的分担CPU的压力,以便留出CPU时间片做其他的事情。

2 为什么前面不学习硬件编码呢:没有意义

3. 硬解码需要 运行程序 的 电脑(手机/pad) 有什么硬件支持?


               有的支持 Windows 平台,有的支持 linux 平台,有的支持 apple ios 平台,有的支持 android 平台。

       二:Windows 平台,我们可以使用利用 DXVA2、DX11、OpenGL、Vulkan、等技术,直接显示 GPU 显卡中的数据。
              FFPLAY 最新源码,使用 Vulkan 的方式来进行硬解加速渲染的。
              为什么使用 Vulkan ?
                 A:Vulkan 跨平台;
                 B:Vulkan 可以做渲染;
                 C:Vulkan 可以做计算,做图像处理;
                 D:Vulkan 可以做视频解码编码;
                 E:Vulkan 可以和 CUDA、DRM、VAAPI 互操作;

        三:使用 DXVA2 硬解加速渲染:
               没有了内存复制(av_hwframe_transfer_data)。
               没有了sws_scale 解码到图片也。
               界面绘制由原来的 GDI 绘制,变成了 DX 绘制。
               非常之高效。

        四:Windows 操作系统支持 DXVA2 方式硬解。
                DXVA2 封装了显卡解码。你就不用操心不同显卡了。
                这也是为什么网上介绍硬解时,大多数是介绍 DXVA2 方式硬解。
                当然,如果你的显卡不支持某种格式视频(H265 格式视频)的硬解,即使你使用 DXVA2 硬解,那也是行不通的。

        五:Windows 操作系统支持 DXVA2 方式硬解。
                不使用 FFMPEG 也是可以的。按照 DXVA2 的规范写就可以了。当然这不在本文讨论范围内。
                感兴趣的可以参看:https://github.com/mofo7777/H264Dxva2Decoder.git

        六:我们只要在 FFMPEG 硬解示例程序的基础上,修改硬解方式为 dxva2,就 OK 了。
               其它不用作任何修改。就是 DXVA2 硬解码了。
               当然我们的目的是使用 DX 的方式来渲染图像。

        七:这个 DX 渲染函数很关键。 
               网上绝大多数的代码,都是将 hwcontext_dxva2.c 代码从 FFMPEG 源代码复制出来,
               添加到自己的代码中,并改写为 ffmpeg_dxva2.cpp 。
               其实完全没有这个必要。而且这种方式肯定也是不可取的。
               而且我相信 FFMPEG 的源代码作者肯定也不想你这么干。
               我使用的方法,适用于所有语言。
               20 行的代码就可以完成 DX 绘制了。更具有通用性。
          
        八:DX 渲染函数原理:
              1:用户在调用 av_hwdevice_ctx_create 函数,初始化硬件加速上下文时,
                   会调用 hwcontext.c 的 av_hwdevice_ctx_create 函数进行 DXVA2 的初始化。
                   av_hwdevice_ctx_create(hwcontext.c) 初始化成功后,将设备上下文信息保存在 data 里面,返回给了调用者;
     

              2:av_hwdevice_ctx_create(hwcontext.c) 会进行 DXVA2 的创建。
                   ret = device_ctx->internal->hw_type->device_create(device_ctx, device, opts, flags);
                   这里将调用 hwcontext_dxva2.c 的 dxva2_device_create 函数进行 DXVA2 的初始化。
                   创建成功后,将 DX 设备信息保存在设备上下文信息的 user_opaque 里面,返回给了调用者。

              3:至此,我们可以拿到:设备上下文信息、DX 设备信息。可以直接进行 DX 界面绘制了。

    九:测试发现:解码效率比网上那些复制改写 hwcontext_dxva2.c 的程序,快了将近 200 多帧。果然高效!!!

     十:核心代码及其说明:     

    while ((ret = avcodec_receive_frame(videoCodecCtx, Frame)) >= 0) 
    {
        ......
        
        surface    = (LPDIRECT3DSURFACE9)Frame->data[3];                     // 待绘制的数据
        device_ctx = (AVHWDeviceContext*)videoCodecCtx->hw_device_ctx->data; // 设备上下文信息
        priv       = (DXVA2DevicePriv*)device_ctx->user_opaque;              // DX 设备信息
     
        ......
     
        /* 开始 DX 渲染 */
        priv->d3d9device->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
        priv->d3d9device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &BackBuffer);
        RECT SourceRect = { 0, 0, videoCodecCtx->width, videoCodecCtx->height };
        priv->d3d9device->StretchRect(surface, &SourceRect, BackBuffer, NULL, D3DTEXF_LINEAR);
        priv->d3d9device->Present(NULL, NULL, NULL, NULL);
     
        ......
    }

    一:FFMPEG 支持的硬解方式:如下都是了解知识

    ffmpeg 硬解码相关知识-CSDN博客

    二 使用ffmpeg api 打印出 支持h264的硬解码技术有哪些

    结果为:

    dxva2
    d3d11va

    #include <iostream>
    using namespace std;
    
    extern "C" {
    #include "libavcodec/avcodec.h"
    }
    
    void test01();
    int main(int argc, char *argv[]) {
    
    
    	test01();
    	return 0;
    }
    
    void test01() {
    	//找到解码器
    	AVCodec* avcodec = avcodec_find_decoder(AV_CODEC_ID_H264);
    
    	/// 硬件解码相关. 打印所有支持H264硬件解码加速方式
    
    	for (int i = 0;; i++)
    	{
    		const AVCodecHWConfig* config = avcodec_get_hw_config(avcodec, i);
    		if (!config) {
    			break;
    		}
    		if (config->device_type) { 
    			cout << av_hwdevice_get_type_name(config->device_type) << endl;
    		}
    	}
    }

    结果为:

    dxva2
    d3d11va

    三 : 具体学习 DXVA2 对于 h264 硬解码的使用

    DXVA2不能单独的编解码,但是可以让硬件解码加速,也就是说,这个DXVA2肯定是要和前面学习的 avcodecContext的 解码 结合起来使用的。

    核心api

    1. 创建 硬解码上下文

    AVBufferRef *av_hwdevice_ctx_alloc(enum AVHWDeviceType type);
     
    ‌函数作用:
    用于创建指定类型的硬件设备上下文

    参数说明:

    ‌设备类型指定‌ 需通过参数指定硬件加速类型(如 CUDA、DXVA2、VideoToolbox 等),FFmpeg 通过枚举 AVHWDeviceType 定义支持的硬件类型67。

    返回值

    函数返回 AVBufferRef* 类型指针,表示对硬件设备上下文的引用,采用引用计数机制管理内存生命周期

    av_hwdevice_ctx_alloc 只是创建 硬件设备 上下文,则一定需要使用 av_hwdevice_ctx_init初始化

    2. 初始化 硬解码上下文

    int av_hwdevice_ctx_init(AVBufferRef *ref);

    通过 av_hwdevice_ctx_init 完成设备初始化,部分平台需额外配置参数(如分辨率、帧格式)

    返回值 :@return 0 on success, a negative AVERROR code on failure

    3. 创建 硬编码上下文 并 初始化硬编码上下文,可以设置参数

    3 = 1+2

    int av_hwdevice_ctx_create(AVBufferRef **device_ctx, enum AVHWDeviceType type,
                               const char *device, AVDictionary *opts, int flags);

    函数作用:
    通过 av_hwdevice_ctx_create 函数,依据AVHWDeviceType 创建 硬件加速上下文

    参数说明 
    AVBufferRef **device_ctx  

            输入输出参数,代表的是硬件加速上下文,该硬件加速上下文,

            需要通过 void av_buffer_unref(AVBufferRef **buf)释放


    enum AVHWDeviceType type  

            输入参数,枚举类型(AVHWDeviceType),

            指定硬件加速类型(如 AV_HWDEVICE_TYPE_CUDA、AV_HWDEVICE_TYPE_VAAPI)


    const char *device‌                

            字符串类型,指定设备路径或标识符(如 /dev/dri/renderD128),部分平台可设为 NULL 自动检测


    AVDictionary *opts                传递平台相关配置参数(如 CUDA 设备编号)


    int flags‌                                    保留参数,当前版本未启用,通常设为 0

    返回值
            若返回 0 表示成功,失败时需检查错误码(如 EINVAL 表示类型不支持)


     

    4. 将硬编码器上下文 绑定到 AVCodecContext->hw_device_ctx 中,

    AVCodec* avcodec = avcodec_find_decoder(AV_CODEC_ID_H264);

    AVCodecContext *codec_ctx = avcodec_alloc_context3(avcodec );

    //将硬编码器上下文device_ctx 绑定到 AVCodecContext->hw_device_ctx

    codec_ctx->hw_device_ctx = av_buffer_ref(device_ctx);

    之所以要将 需通过 av_buffer_ref 增加引用计数有目的是:

    第一:在完成绑定后,我们就可以调用 void av_buffer_unref(&device_ctx) 将创建的 硬件编码器上下文 引用计数减一。并将 device_ctx 置为nullptr.

    第二:而最终释放  最后再通过 void avcodec_free_context(AVCodecContext **pavctx) 的释放,完全释放 device_ctx

    代码示例

    #include <iostream>
    using namespace std;
    
    extern "C" {
    #include "libavcodec/avcodec.h"
    }
    
    void test01();
    int main(int argc, char *argv[]) {
    
    
    	test01();
    	return 0;
    }
    
    void test01() {
    	//找到解码器
    	AVCodec* avcodec = avcodec_find_decoder(AV_CODEC_ID_H264);
    	if (avcodec == nullptr) {
    		cout << "avcodec_find_decoder(AV_CODEC_ID_H264) func error " << endl;
    		return;
    	}
    	/// 硬件解码相关. 打印所有支持H264硬件解码加速方式
    
    	for (int i = 0;; i++)
    	{
    		const AVCodecHWConfig* config = avcodec_get_hw_config(avcodec, i);
    		if (!config) {
    			break;
    		}
    		if (config->device_type) { //devicetype为枚举打印结果为 
    			cout << av_hwdevice_get_type_name(config->device_type) << endl;
    		}
    	}
    	AVBufferRef* device_ctx = nullptr;
    	int ret = av_hwdevice_ctx_create(&device_ctx,
    		AV_HWDEVICE_TYPE_DXVA2,
    		nullptr,
    		nullptr,
    		0);
    	if (ret < 0 ) {
    		cout << "av_hwdevice_ctx_create func error " << endl;
    		return;
    	}
    
    
    
    
    	AVCodecContext* avcodecContext = avcodec_alloc_context3(avcodec);
    	if (avcodecContext == nullptr) {
    		cout << "avcodec_alloc_context3(avcodec) func error " << endl;
    		return;
    	}
    
    	//avcodecContext->hw_device_ctx = device_ctx;//直接传递
    	avcodecContext->hw_device_ctx = av_buffer_ref(device_ctx);//引用计数加1 
    
    
    	//device_ctx 的引用计数就变成了 2了,
    	cout << "av_buffer_get_ref_count(device_ctx)  = " << av_buffer_get_ref_count(device_ctx) << endl;
    
    	//理论上到这里就可以将 device_ctx 调用 av_buffer_unref了,
    	//av_buffer_unref 函数的作用:将引用计数-1,
    	//然后判断如果引用计数>0,则只是device_ctx  == nullptr;
    	// 如果引用计数==0,则会将 device_ctx 里面的avbuf都释放了,并且将 device_ctx==nullptr
    
    	av_buffer_unref(&device_ctx); 
    
    	//这里已经释放了 device_ctx,再去 调用 av_buffer_get_ref_count(device_ctx) 会有 error
    	//cout << "av_buffer_get_ref_count(device_ctx)  = " << av_buffer_get_ref_count(device_ctx) << endl;
    
    	//最后释放 avcodecContext
    	avcodec_free_context(&avcodecContext);
    
    }

    5. 当硬件加速打开后, 通过 avcodecContext 解码后的avframe数据会有变化。

    没有硬件加速的case

    之前我们对于h264文件,通过 avcodec_receive_frame方法得到的avframe 的linesize 是如下的情况:符合YUV420p的数据格式,linesize[0],linesize[1],linesize[2]都是有具体的值,data[0]放的是Y分量,data[1]放的是U分量,data[2]放的是V分量,

    format 是0,对应的就是YUV420P

    打开硬件加速的case

    当我们将硬件加速打开后,通过 avcodec_receive_frame方法得到的avframe 的linesize  和 data 是如下的情况:

    data[0],data[1],data[2],中没有数据,data[3]中有数据

    整个 linesize 数组都是0 。

    format 是53,对应的是 AV_PIX_FMT_DXVA2_VLD

    在  AV_PIX_FMT_DXVA2_VLD  的定义中,也说明了翻译一下:通过DXVA2得到的HW 解码,Picture.data[3]中包含了LPDIRECT3DSURFACE9的指针,也就是在定义中也告诉了大家,我的数据在 data[3]中

    AV_PIX_FMT_DXVA2_VLD ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer

    结论:

    也就是说:我们现在的avframe格式 是 AV_PIX_FMT_DXVA2_VLD ,是通过硬件加速设备显存完成的,存储在显存中,而不是内存中。

    结论测试,

    添加循环,让循环读取h264文件,为了让能够循环读取文件,我们这里改造一下代码。

    主要是改动 不停的读取文件的流程

        //6. 开始读取文件直到文件读取完成,这里为了测试,要写一个循环,让一直读取h264文件
        while (true) {
            readifstream.read((char*)readfilebuf, readfilebufsize);
            if (readifstream.bad() == true) {
                cout << "read file bad" << endl;
                break;
            }
            int realfilebufreadcount = readifstream.gcount(); // gcount() 返回最后一次非格式化输入操作(如 read()、get()、getline())实际读取的字符数(字节数)
            if (realfilebufreadcount <= 0) { //如果读取的数据是<=0的,说明整个h264文件已经读取完成了,或者读取h264文件的时候出现了错误了,那么就要直接退出
                cout << "read file break count  = " << realfilebufreadcount << endl;
                //添加循环用的。先需要调用 clear()函数,通过以 state 的值赋值,设置流错误状态标志。默认赋值 std::ios_base::goodbit ,它拥有的效果为清除所有错误状态标志。
                //void clear( std::ios_base::iostate state = std::ios_base::goodbit );
                readifstream.clear();
                //这时候将 readifstream转到 文件开头
                readifstream.seekg(0, std::ios::beg);
                continue; //为了循环 continue;
                //break;//如果不循环,则这里要break;
            }
            //到这里说明读取到了 realbufreadcount 个字节的数据
            cout << "realfilebufreadcount = " << realfilebufreadcount << endl;
    
            //7. 根据读取到的字节,解析这段字节,解析出来avpacket.
            //我们知道当 h264 数据是 大于 4096字节时,第一次读取的一定是4096 个字节。而从4096中肯定是能解析出来很多个avpacket的
            //而解析的方法是使用 av_parser_parse2方法,av_parser_parse2方法的返回值是已经解析了多少个字节。
            //因此这里要弄一个循环,让 realfilebufreadcount = (从文件读取到的 realfilebufreadcount字节) - (每次av_parser_parse2方法解析过的字节)
            //如果 realbufreadcount 大于0,就可以继续解析,如果
           uint8_t* tempreadfilebuf = readfilebuf;//让 tempreadfilebuf 指向 readfilebuf的指针。这里为什么不直接用readfilebuf呢?从功能实现上直接用buf也是可以的,但是这是C语言的基本功,尽量不要用原始指针。而是找一个复制的指针。
     
           while (realfilebufreadcount > 0 ) {
               int parsebufsize = av_parser_parse2(
                   avcodecParseContext, 
                   avcodecContext,
                   &avpacket->data,
                   &avpacket->size,
                   tempreadfilebuf, 
                   realfilebufreadcount,
                   0, 0, 0);
               cout << "parsebufsize = " << parsebufsize << endl;
               realfilebufreadcount = realfilebufreadcount - parsebufsize;
               cout << " after realfilebufreadcount = " << realfilebufreadcount << endl;
    
               tempreadfilebuf = tempreadfilebuf + parsebufsize;
               cout << "before avpacket->size = " << avpacket->size << endl;
    
               //这时候还要判断是否packet的size是大于0的,说明是正常截取了一段avpacket数据,因为av_parser_parse2方法即使分析据,也并不能保证真正的解析到 avpacket
               if (avpacket->size>0) {
                   cout << "avpacket->size = " << avpacket->size << endl;
    
                   // 如果avpacket 的size 大于0,说明已经有了avpacket了,也就是说,到这里,已经对h264进行了分离,得到了avpacket
                   // 那么下来,我们就需要将 对avpacket进行处理,在最后,记得调用av_packet_unref(avpacket);
                   // 8. 将avpacket发送给 avcodecContext
                   ret = avcodec_send_packet(avcodecContext, avpacket);
                   if (ret == AVERROR_EOF) {
                       //已发送空包(pkt=NULL)触发编解码器刷新,且后续无新数据可处理(流结束标志)。
                       //说明 tempreadfilebuf 中的数据已经读取完成了,那么要continue 还是break呢?
                       //这里应该是continue比较合理,就是我们依赖 将 tempreadfilebuf 读取完成,跳出循环的条件应该是 realfilebufreadcount > 0
                       cout << "avcodec_send_packet return value == AVERROR_EOF " << endl;
                       continue;
                   }
                   else if (ret == AVERROR(EINVAL) || ret == AVERROR(ENOMEM)) {
                       //EINVAL :参数错误,可能原因:
                       //    - 编解码器未通过 avcodec_open2 打开‌;
                       //    - 上下文类型不匹配(如向编码器发送数据包)‌。
                       //ENOMEM:内存不足
                       cout << "avcodec_send_packet return value == AVERROR(EINVAL) || AVERROR(ENOMEM) ret = " <<  ret << endl;
                       //如果是这样情况,应该跳出循环比较好
                       break;
                   }
                   else if (ret == 0 || ret == AVERROR(EAGAIN)) {
                       //ret == AVERROR(EAGAIN) 表示 编解码器内部缓冲区已满,需先调用 avcodec_receive_frame 获取输出数据,释放资源后再重试‌。
                       if (ret == AVERROR(EAGAIN)) {
                           cout << "avcodec_send_packet return value == AVERROR(EAGAIN) " << endl;
                       }
                       // 9.正确的发送数据到 解码器了,那么就通过 avcodec_receive_frame 中得到avframe,这里还是写一个循环
                       while (1) {
                           ret = avcodec_receive_frame(avcodecContext, avframe);
                           
                           if (ret == AVERROR(EAGAIN)) {
                               //AVERROR(EAGAIN) 当前无可用输出帧,需继续发送输入数据(avcodec_send_packet)或重复调用本函数尝试获取数据‌
                               //那就跳出当前while (1) 循环,
                               cout << "avcodec_receive_frame return value == EAGAIN " << endl;
    
                               break;
                           }
                           else if (ret == AVERROR_EOF) {
                               //编解码器已完全刷新(如调用 avcodec_send_packet(NULL) 发送结束标记后),后续无更多数据输出‌
                               cout << "avcodec_receive_frame return value == AVERROR_EOF " << endl;
                               break;
                           }
                           else if (ret == AVERROR(EINVAL)) {
                               //编解码器未正确初始化或上下文参数非法‌
                               cout << "avcodec_receive_frame return value == EINVAL " << endl;
                               break;
                           }
                           else if (ret == AVERROR_INPUT_CHANGED) {
                               //编解码器未正确初始化或上下文参数非法‌
                               cout << "avcodec_receive_frame return value == AVERROR_INPUT_CHANGED " << endl;
                               break;
                           }
                           else if (ret < 0 ) {
                               cout << "avcodec_receive_frame return value == other  ret = " << ret << endl;
                               break;
                           }
                           else if (ret == 0 ) {
                               //这时候才有正确的avframe 被解析出来
                               //1.存储 avframe
                               cout << "avframe->format = " << avframe->format << endl;
                               cout << "avframe->width = " << avframe->width << endl;
                               cout << "avframe->height = " << avframe->height << endl;
    
                               cout << "avframe->linesize[0] = " << avframe->linesize[0] << endl;
                               cout << "avframe->linesize[1] = " << avframe->linesize[1] << endl;
                               cout << "avframe->linesize[2] = " << avframe->linesize[2] << endl;
                               cout << "avframe->pkt_size = " << avframe->pkt_size << endl;
    
                               //如果是硬件加速方式,就不能这么存储了,我们需要先转换一下。将 AV_PIX_FMT_DXVA2_VLD 格式转成 AV_PIX_FMT_YUV420P
                               //av_hwframe_transfer_data()
                               //注意这里的写法,要先写完Y,再写U,再写V
                               //for (int j = 0; j < avframe->height; j++) {
                               //    writefstream.write((char*)(avframe->data[0] + j * avframe->linesize[0]), avframe->width);
                               //}
                               //for (int j = 0; j < avframe->height / 2; j++) {
                               //    writefstream.write((char*)(avframe->data[1] + j * avframe->linesize[1]), avframe->width / 2);
                               //}
                               //for (int j = 0; j < avframe->height / 2; j++) {
                               //    writefstream.write((char*)(avframe->data[2] + j * avframe->linesize[2]), avframe->width / 2);
                               //}
    
                               //解码相关。计算1秒钟解码了多少次
                               xjiemacount++;
                               //正常写法,但是有我们测试的视频太短了,不能说明问题
                               //auto cur = NowMs();
                               //if (cur -starttime>=1000) {
                               //	cout << "1秒钟解码了" << xjiema <<"次"<< endl;
                               //	xjiema = 0;
                               //	cur = 0;
                               //}
    
                               auto cur = NowMs();
                               if (cur - starttime >= 100) {
                                   cout << "bbbbbbb 1秒钟解码了" << xjiemacount * 10 << "次" << endl;
                                   xjiemacount = 0;
                                   starttime = cur;
                               }
                               
    
                               //2.显示avframe
                               //加入xvideoview 相关,init的工作只需要做一次就好,因此添加一个
                               if (!is_init_win)
                               {
                                   is_init_win = true;
                                   xvideoview->Init(avframe->width, 
                                       avframe->height, 
                                       (XVideoView::Format)avframe->format);
                               }
                               xvideoview->DrawFrame(avframe);
    
    
                               //3.将avframe unref
                               av_frame_unref(avframe);
                           }
                       }
    
                   }
    
    
                   //这里为了安全期间,最好还是调用一下 av_packet_unref(avpacket);实际上不调用,应该也问题,除了在流媒体24小时不间断播放的情况下,avpacket的ref count 才有可能超过int的最大值
                   av_packet_unref(avpacket);//
                }
           }
       
           //在循环的最后判断是否到了文件的最后.能走到这里的机会不多,
           //我们假设 要读取的文件大小是4000字节,而我们每次读取1000个字节,那么第4次的时候,就刚好读取文件文件的结尾了,就能走到这个逻辑了
           //实际上我们大多数情况下,文件的大小和 我们每次读取的字节,都不会刚好能整除的。
           //这块逻辑实际上也是可以不写的,不写大不了再次读取一次,最终都是要会读取的文件的大小为0,也就是在前面根据 realfilebufreadcount <= 0 跳出循环 
           // 我们这里要加这个,主要是为了 循环读取这个文件。
           if (readifstream.eof() == true) {
               cout << "readifstream.eof() = true " << realfilebufreadcount << endl;
               //添加循环用的。先需要调用 clear()函数,通过以 state 的值赋值,设置流错误状态标志。默认赋值 std::ios_base::goodbit ,它拥有的效果为清除所有错误状态标志。
               //void clear( std::ios_base::iostate state = std::ios_base::goodbit );
               readifstream.clear();
               //这时候将 readifstream转到 文件开头
               readifstream.seekg(0, std::ios::beg);
               continue;//为了循环 continue;
               //break;//如果不循环,则这里要break;
           }
        }
    

    6. 将通过硬件加速得到的 在显卡中的 avframe 转成 拷贝到内存 能够播放的yuv420p的 avframe。

    也就是说,我们还需要定义一个 AVFrame *, 我们这里叫做 send_avframe

    int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags);

    作用是:将src 转移 到 dst,转移后 dstavframe的格式 和 srcavframe的格式很大可能不一致。

    核心作用
    1. 硬件数据到系统内存的转移
      该函数负责将硬件解码器(如 GPU/NPU)输出的数据从硬件专用内存(如显存)复制到系统内存的 AVFrame 中,便于后续 CPU 处理。
      示例场景:硬解后的 YUV 数据若需通过 OpenCV 处理或显示在 UI 框架中,必须通过此函数完成内存迁移。

    2. 格式兼容性适配
      硬件解码器输出的像素格式可能与标准软件处理流程不兼容(如 NVIDIA 硬解的 NV12 格式需转换为 YUV420P)。此函数在转移过程中自动完成格式转换。


    性能与限制
    • 耗时问题‌:
      转移数据涉及内存复制,高分辨率场景(如 4096x4096)可能产生显著延迟(实测达 24ms)2。若需直接使用 GPU 数据(如 UE4/Unity 渲染),应避免调用此函数,转而通过共享纹理等技术直接访问硬件内存26。

    • 调用时机‌:
      在解码流程中,需先通过 avcodec_receive_frame 获取硬件帧,再调用此函数转移数据到系统帧。

    实际测试:在windows 上转出来的 send_avframe的格式是 23,linesize[0],linesize[1]是有值的,23对应的是    AV_PIX_FMT_NV12,如下是说明:

    AV_PIX_FMT_NV12,      ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)

    也就是说,通过 av_hwframe_transfer_data 得到的 avframe 格式是 AV_PIX_FMT_NV12。

    如下是debug时候的结果。

    那么我们如果要通过前几章  的 xvideoview 去show 这个avframe,还需要改动 xvideoview的逻辑,让其支持 显示 AV_PIX_FMT_NV12。或者得到dstavframe后,转换成我们前面 xvideoview支持的YUV420P格式,然后让其再显示。

    7. 方案一 改动 xvideoview的逻辑,让其支持 渲染显示 AV_PIX_FMT_NV12

    刚开始的想法是:

    7.1. 在  SDL_CreateTexture 方法中参数 format为 SDL_PIXELFORMAT_NV12(对应 pixelformat 为 AV_PIX_FMT_NV12)。

    7.2.那么在 使用 SDL_UpdateYUVTexture 传递 send_frame的 Y,U,V 就行了。

    7.3.实际测试不行,原因是 

    SDL_UpdateYUVTexture设计用于平面YUV格式(如YUV420P),而NV12是半平面格式,导致参数传递错误。
     

    extern DECLSPEC SDL_Texture * SDLCALL SDL_CreateTexture(SDL_Renderer * renderer,
                                                            Uint32 format,
                                                            int access, int w,
                                                            int h);

     

    extern DECLSPEC int SDLCALL SDL_UpdateYUVTexture(SDL_Texture * texture,
                                                     const SDL_Rect * rect,
                                                     const Uint8 *Yplane, int Ypitch,
                                                     const Uint8 *Uplane, int Upitch,
                                                     const Uint8 *Vplane, int Vpitch);

    7.4.那么就不能用 SDL_UpdateYUVTexture方法,我们再来回顾一下 NV12 的格式,如果我们把 data[0],data[1]中看成一段数据,调用SDL_UpdateTexture 呢?

    extern DECLSPEC int SDLCALL SDL_UpdateTexture(SDL_Texture * texture,
                                                  const SDL_Rect * rect,
                                                  const void *pixels, int pitch);

    这里要考虑字节对齐问题

    8. 之前我们的还需要将解析avframe 存储 yuv文件。这里也要考虑 转换成 AV_PIX_FMT_NV12格式后的存储问题

    这里也要考虑字节对齐问题

    相关代码:

    xvideoview.h

    //
    /// XVideoView 为 视频渲染接口类 
    /// 实现功能有如下三个:
    /// 隐藏SDL实现,目的是如果今后不使用SDL实现了,可以使用OPENGL等实现,需要对客户隐藏
    /// 渲染方案可替代
    /// 线程安全
    
    #pragma once
    #include <mutex>
    #include <thread>
    #include <string>
    #include <fstream>
    
    struct AVFrame;
    
    class XVideoView
    {
    
    public:
    	enum  Format{
    		YUV420P = 0,    ///  对应 pixfmt  中的为 AV_PIX_FMT_YUV420P,   ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
    		NV12 = 23,      ///  对应 pixfmt  中的为 AV_PIX_FMT_NV12  planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)
    		ARGB = 25,      ///  对应 pixfmt  中的为 AV_PIX_FMT_ARGB < packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
    		RGBA = 26,      ///< 对应 pixfmt  中的为 AV_PIX_FMT_RGBA packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
    		ABGR = 27,      ///< 对应 pixfmt  中的为 AV_PIX_FMT_ABGR   packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
    		BGRA = 28	    ///< 对应 pixfmt  中的为 AV_PIX_FMT_BGRA  packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
    	};
    	enum ViewType {
    		SDL_TYPE,
    		OPENGL_TYPE
    	};
    
    
    /// <summary>
    /// 如下的接口是要给 调用着使用的,因此需要public
    /// </summary>
    public:
    
    	//
    	/// 纯虚函数,初始化渲染窗口 线程安全
    	/// 子类必须实现,如果子类是XSDL,那么init方法里面就需要完成关于sdl的初始化实现
    	/// @param width  视频文件的分辨率 ,例如 1080 * 720,width的值就是1080
    	/// @param height 视频文件的分辨率 ,例如 1080 * 720,height的值就是720
    	/// @param fmt 类似 RGB888,YUV420P ,表示的是绘制的格式
    	/// @param winid 窗口句柄,如果为空,创建新窗口
    	/// @return 是否创建成功
    	/// 
    	//virtual bool Init(int width, int height, Format fmt = YUV420P, void* winid = nullptr) = 0;
    	virtual bool Init(int width, int height, Format fmt = YUV420P) = 0;
    
    
    	/// 这里将init 函数的 原本的 winid单独的拿出来,通过 setWinid接口设置,是为了在多窗口都显示视频的时候好处理
    	///且此接口应该是 不管是sdl 实现还是 opengl实现,设置 winid的方法,和获得winid的方法都应该一样,因此不需要设置成纯虚函数,在自己的cpp文件中就可以实现
    	void setWinid(void* winid);
    	void* getWinid();
    
    	//
    	/// 渲染图像 线程安全
    	///@para data 渲染的二进制数据
    	///@para linesize 一行数据的字节数,对于YUV420P就是Y一行字节数
    	/// linesize<=0 就根据宽度和像素格式自动算出大小
    	/// @return 渲染是否成功
    	virtual bool Draw(const unsigned char* data, int linesize = 0) = 0;
    
    	//清理所有申请的资源,包括关闭窗口
    	virtual void Close() = 0;
    
    	/// 处理窗口退出事件,这个方法主要是为了在不使用qt的windows的情况下,意味着创建的显示sdl的window是和 label没有关系的
    	/// 那么这个窗口的关闭是要接受  sdl的quit_event 事件,即:我们在 isexit 内部实现中调用 SDL_WaitEventTimeout
    	/// 那么在什么时候调用这个isexit方法呢?在qt的 timeevent中调用就OK了
    	virtual bool IsExit() = 0;
    
    
    	/// <summary>
    	/// 
    	/// 此方法在 xvideoview.cpp 中实现。
    	/// 此方法的目的是:在测试程序使用的 RGB或者YUV数据的时候,分开调用
    	/// Draw(const unsigned char* data, int linesize = 0) = 0;
    	/// 或者 virtual bool Draw(const unsigned  char* y, int y_pitch,const unsigned  char* u, int u_pitch,const unsigned  char* v, int v_pitch) = 0;
    	/// 由于此函数内部 是调用的两个 线程安全的函数,因此函数实现中不能使用 _mutex lock,否则会导致死锁问题
    	/// </summary>
    	/// <param name="frame"></param>
    	/// <returns></returns>
    	bool DrawFrame(AVFrame* frame);
    
    	/// <summary>
    	/// 
    	/// </summary>
    	/// <param name="y"> Y 分量开始的指针</param>
    	/// <param name="y_pitch"> Y 分量一行的大小 ,也就是 avframe-linesize[0] 的大小 </param>
    	/// <param name="u">U 分量开始的指针</param>
    	/// <param name="u_pitch"> U 分量一行的大小 ,也就是 avframe-linesize[1] 的大小</param>
    	/// <param name="v">V 分量开始的指针</param>
    	/// <param name="v_pitch"> V 分量一行的大小 ,也就是 avframe-linesize[2] 的大小</param>
    	/// <returns></returns>
    	virtual bool Draw(
    		const unsigned  char* y, int y_pitch,
    		const unsigned  char* u, int u_pitch,
    		const unsigned  char* v, int v_pitch
    	) = 0;
    
    
    	///打开文件和读取文件的操作,都通过 xvideoview 封装起来,user不需要知道实现细节。内部通过 ifstream 实现
    	//打开文件
    	bool openfile(std::string filepath);
    
    	//读取文件
    	AVFrame * readAVFrameFormfile();
    
    public:
    	
    	///工厂模式 ViewType 应该是一个枚举,包含支持的类型,比如说SDL, OPENGL,这一步让user 选择使用SDL 显示,还是使用 其他方式显示
    	///默认参数是SDL_TYPE,如果不传递参数,默认就是使用SDL 渲染图像
    	static XVideoView* create(ViewType type = SDL_TYPE);
    
    
    protected:
    	int width_ = 0;				//视频文件的分辨率 例如 1080 * 720,width_的值就是1080
    	int height_ = 0;			//视频文件的分辨率 例如 1080 * 720,height_的值就是720
    	Format fmt_ = YUV420P;		//视频文件的像素格式
    	std::mutex mtx_;			//确保线程安全
    
    
    	int lablewidth_ = 0; //放置 视频的 lable 的宽和高
    	int lableheight_ = 0;
    	void* winid_ = nullptr;
    
    	//帧率相关
    	int render_fps_ = 0;       //显示帧率
    	long long beg_ms_ = 0;       //计时开始时间
    	int count_ = 0;              //统计显示次数
    
    
    public:
    	//当我们将widget的大小变化的时候,存放视频的 lable 控件的大小也要跟着变化,我们需要记录 lable的宽和高
    	void setscaleLable(int labelwidth, int lableheight);
    	int render_fps(); 
    
    
    private:
    	std::ifstream ifs_;
    	AVFrame* avframe_ = nullptr;
    	uint8_t* NV12_data_ = nullptr;
    
    public:
    	virtual ~XVideoView();
    };
    
    

    xvideoview.cpp

    #include "xvideoview.h"
    #include "xsdl.h"
    using namespace std;
    
    extern "C" {
    	#include "libavutil/frame.h"
    }
    
    
    void XVideoView::setWinid(void* winid)
    {
    	this->winid_ = winid;
    }
    
    void* XVideoView::getWinid()
    {
    	return this->winid_;
    }
    
    bool XVideoView::DrawFrame(AVFrame* frame)
    {
    	if (frame == nullptr || frame->data[0] == nullptr) {
    		cout << "DrawFrame(AVFrame* frame) error because frame == nullptr || frame->data[0] == nullptr"<<endl;
    		return false;
    	}
    	///4.计算fps
    	count_++;
    	if (beg_ms_ <= 0)
    	{
    		beg_ms_ = clock();
    	}
    	//计算显示帧率
    	else if ((clock() - beg_ms_) / (CLOCKS_PER_SEC / 1000) >= 1000) //一秒计算一次fps
    	{
    		render_fps_ = count_;
    		count_ = 0;
    		beg_ms_ = clock();
    	}
    
    	int linesize = 0;
    	switch (frame->format)
    	{
    	case AV_PIX_FMT_YUV420P:
    		return Draw(frame->data[0],
    			frame->linesize[0],
    			frame->data[1],
    			frame->linesize[1], 
    			frame->data[2],
    			frame->linesize[2]);
    	case AV_PIX_FMT_ARGB:
    	case AV_PIX_FMT_RGBA:
    	case AV_PIX_FMT_ABGR:
    	case AV_PIX_FMT_BGRA:
    
    		return Draw(frame->data[0], frame->linesize[0]);
    	case AV_PIX_FMT_NV12:
    		if (frame->linesize[0] <=0 || frame->height <=0 || frame->width <=0) {
    			cout << "DrawFrame error because frame->linesize[0] = " << frame->linesize[0]
    				<< "  frame->height = " << frame->height 
    				<< "  frame->width = " << frame->width << endl;
    			return false;
    		}
    		linesize = frame->width;
    		if (!NV12_data_)
    		{
    			//最大支持4k的
    			NV12_data_ = new uint8_t[4096 * 2160 * 1.5];
    
    			//NV12_data_ = new uint8_t [frame->linesize[0] * frame->height * 1.5];
    		}
    		memset(NV12_data_, 0, frame->linesize[0] * frame->height * 1.5);
    		if (frame->linesize[0] == frame->width)
    		{
    			// 如果没有字节对齐问题,直接先copy avframe->data[0];然后再copy avframe->data[1]
    			memcpy(NV12_data_, frame->data[0], frame->linesize[0] * frame->height); //Y
    			memcpy(NV12_data_ + frame->linesize[0] * frame->height, frame->data[1], frame->linesize[1] * frame->height / 2); //UV
    		}
    		else //逐行复制
    		{
    			//这里字节有对齐问题的时候,为什么要这样 copy 呢?
    			//而且实验测试,当 avframe是 400 * 300 时候,字节对齐有问题时候,就是用这种方法就能解决。
    			for (int i = 0; i < frame->height; i++) //Y
    			{
    				memcpy(NV12_data_ + i * frame->width,
    					frame->data[0] + i * frame->linesize[0],
    					frame->width
    				);
    			}
    			for (int i = 0; i < frame->height / 2; i++)  //UV
    			{
    				auto p = NV12_data_ + frame->height * frame->width;// 移位Y
    				memcpy(p + i * frame->width,
    					frame->data[1] + i * frame->linesize[1],
    					frame->width
    				);
    			}
    		}
    
    		return Draw(NV12_data_, linesize);
    
    	default:
    		break;
    	}
    	return false;
    }
    
    bool XVideoView::openfile(std::string filepath)
    {
    	if (ifs_.is_open()) {
    		ifs_.close();
    	}
    
    	ifs_.open(filepath, ios_base::binary);
    
    	return ifs_.is_open();
    }
    
    AVFrame* XVideoView::readAVFrameFormfile()
    {
    	if (width_<=0 || height_<=0 || ifs_.is_open() == false) {
    		//qDebug() << "readAVFrameFormfile error because width_ = " << width_
    		//	<< " height_ = " << height_
    		//	<< "  ifs_.is_open() = " << ifs_.is_open() << "  and return nullptr";
    		return nullptr;
    	}
    	//这里分为两步,第一步,分配avframe空间。第二步:从ifs中给avframe空间读取数据。
    	// 第一步的问题:
    	//如果不存在,则肯定是要创建的,且要将avframe的三要素设定。
    	//如果avframe_已经存在了,那么还需要创建吗?如果已经存在了,且三要素没有变化,就不需要创建了;如果存在,但是三要素有变化,则需要先销毁之前的,然后再创建
    	if (avframe_ !=nullptr) {
    		//存在情况下,则要判断 三要素是否已经改动,如果改动了销毁后,重新 av_frame_alloc()
    		if (avframe_->width != width_ || avframe_->height != height_ || avframe_->format != fmt_) {
    			av_frame_free(&avframe_);
    		}
    		
    	}
    	if(avframe_ == nullptr) {
    		//不存在,不管是第一次,还是三要素变化了,avframe_都会变成nullptr
    		avframe_ = av_frame_alloc();
    		if (avframe_ == nullptr) {
    			//失败了,直接return
    			cout << "av_frame_alloc fail,return nullptr";
    			return nullptr;
    		}
    		avframe_->width = width_;
    		avframe_->height = height_;
    		avframe_->format = fmt_;
    
    		//这里根据要读取的视频的 分辨率 和 fmt,设置 avframe linesize,这里是有问题的,要保证 原始的 avframe的linesize[0] 和width一致,没有考虑字节对齐问题
    		if (fmt_ == AV_PIX_FMT_YUV420P) {
    			avframe_->linesize[0] = avframe_->width;
    			avframe_->linesize[1] = avframe_->width / 2;
    			avframe_->linesize[2] = avframe_->width / 2;
    		}
    		else if (fmt_ == AV_PIX_FMT_ARGB || fmt_ == AV_PIX_FMT_RGBA || fmt_ == AV_PIX_FMT_ABGR || fmt_ == AV_PIX_FMT_BGRA) {
    			avframe_->linesize[0] = avframe_->width * 4;
    		}
    
    		int ret = av_frame_get_buffer(avframe_, 0);
    		if (ret < 0) {
    			//av_frame_get_buffer 失败了
    			//1.打印log
    			char errbuf[1024] = { 0 };
    
    			av_strerror(ret, errbuf, sizeof(errbuf) - 1);
    			cout << "av_frame_get_buffer func error errbuf = " << errbuf;
    			//2.释放已经申请的avframe 空间
    			av_frame_free(&avframe_);
    			//3.返回 nullptr
    			return nullptr;
    		}
    	}
    	
    	//到这里分配空间的工作就已经完成了,那么接下来就应该是读取了,实际上每次都是读取一张图片的大小
    	if (avframe_->format == AV_PIX_FMT_YUV420P) {
    		ifs_.read((char *)avframe_->data[0],avframe_->width * avframe_->height);
    		ifs_.read((char*)avframe_->data[1], avframe_->width * avframe_->height /4);
    		ifs_.read((char*)avframe_->data[2], avframe_->width * avframe_->height /4);
    	}
    	else if (fmt_ == AV_PIX_FMT_ARGB || fmt_ == AV_PIX_FMT_RGBA || fmt_ == AV_PIX_FMT_ABGR || fmt_ == AV_PIX_FMT_BGRA) {
    		ifs_.read((char*)avframe_->data[0], avframe_->width * avframe_->height * 4);
    		//qDebug()<< " read count = " << ifs_.gcount();
    	}
    	//这里还要处理一下读取到最后一张图片的操作,如果不是循环播放,这个return nullptr
    	if (ifs_.gcount()==0) {
    		//说明读取到最后啦,
    		//return nullptr;
    		//如果要循环读取
    		if (ifs_.eof()) {
    			ifs_.clear();
    			ifs_.seekg(0, std::ios::beg);
    		}
    	}
    	
    
    	return avframe_;
    }
    
    XVideoView* XVideoView::create(ViewType type)
    {
    	switch (type)
    	{
    	case XVideoView::ViewType::SDL_TYPE:
    		cout << "XVideoView create XSDL.class";
    		return new XSDL();
    		break;
    	default:
    		break;
    	}
    	return nullptr;
    }
    
    void XVideoView::setscaleLable(int labelwidth, int lableheight)
    {
    	this->lablewidth_ = labelwidth;
    	this->lableheight_ = lableheight;
    }
    
    int XVideoView::render_fps() {
    	return render_fps_;
    }
    
    XVideoView::~XVideoView()
    {
    	if (NV12_data_)
    		delete NV12_data_;
    	NV12_data_ = nullptr;
    }
    

    xsdl.h

    #pragma once
    #include "xvideoview.h"
    #include <sdl/SDL.h>
    #include <iostream>
    class XSDL :
        public XVideoView
    {
    public:
        
        /// 初始化渲染窗口 线程安全
        /// @para w 窗口宽度
        /// @para h 窗口高度
        /// @para fmt 绘制的像素格式
        /// @return 是否创建成功
        bool Init(int w, int h,
            Format fmt = YUV420P) override;
    
        //
        /// 渲染图像 线程安全
        ///@para data 渲染的二进制数据
        ///@para linesize 一行数据的字节数,对于YUV420P就是Y一行字节数
        /// linesize<=0 就根据宽度和像素格式自动算出大小
        /// @return 渲染是否成功
        bool Draw(const unsigned char* data, int linesize = 0) override;
    
        void Close() override;
    
        bool IsExit() override;
    
        bool Draw(
            const unsigned  char* y, int y_pitch,
            const unsigned  char* u, int u_pitch,
            const unsigned  char* v, int v_pitch
        ) override;
    private:
        SDL_Window* sdlwindow_ = nullptr;
        SDL_Renderer* sdlrenderer_ = nullptr;
        SDL_Texture* sdltexture_ = nullptr;
    };

    xsdl.cpp

    #include "xsdl.h"
    using namespace std;
    
    static bool InitVideo()
    {
    	//由于我们只希望调用一次,可以使用静态变量完成.
    	///这里为什么要锁住呢?如果有多个线程 想要 init,我们这里只能让一个线程init成功。
    	///那么问题是:什么情况下,有多个线程都想要init呢?这里是有疑问的
    	static bool is_first = true;
    	static mutex mux;
    	unique_lock<mutex> sdl_lock(mux);
    	if (!is_first) {
    		return true;
    	}
    	is_first = false;
    	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0 )
    	{
    		cout << "SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) error " << SDL_GetError();
    		cout << SDL_GetError() << endl;
    		return false;
    	}
    	//锯齿问题的解决
    	//‌SDL_SetHint函数是SDL库中的一个函数,用于设置特定选项的提示值。‌设置不同的key,会起到不同的作用,不同的key,对应的value也不同
    	//我们通过 设置 影响缩放质量的 key :SDL_HINT_RENDER_SCALE_QUALITY, 的值的为 "best" 。来说明当缩放时,我们需要最好的质量。以解决 锯齿问题
    	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
    	return true;
    }
    
    bool XSDL::Init(int w, int h, Format fmt)
    {
    	cout << "xsdl init call";
    	Uint32 sdlformat = SDL_PIXELFORMAT_IYUV; 
    	int retbool = true;
    	if (w <= 0 || h <= 0) {
    		cout << "func XSDL::Init error because w = " << w <<" h = " << h;
    		return false;
    	}
    
    	//第一步,初始化 sdl init,这个在整个开发中,我们只调用一次,因此可以独立的写出来,写成static的 
    	retbool = InitVideo();
    	if (retbool == false) {
    		return false;
    	}
    
    	//如果使用当前窗口,播放多个视频文件,假设一个一个播放,如果不释放 sdltexture_ 和 sdlrenderer_,
    	//就每当播放一个视频文件,就创建一个sdltexture_ 和  sdlrenderer_,造成内存泄漏,因此每次init的时候要判断一下,如果有这两个成员变量,直接变成 销毁,并置为nullptr
    	//那么问题是,SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) 和 sdlwindow_ 要不要创建多次呢?
    	//首先,很显然: SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO),是不需要弄多次的。
    	///问题是sdlwindow 需不需要创建多次呢?如果是使用 SDL_CreateWindowFrom(win_id)显然 也不需要多次,因为 win_id所代表的 label的宽和高,都会重新设置
    	//那么使用 SDL_CreateWindow 创建的sdl_window_ 需要吗?这里实际上也不需要,创建的时候参数 SDL_WINDOW_RESIZABLE就决定了
    	//因此这里只需要 将 sdltexture_ 和 sdlrenderer_ 销毁。
    	///这里还是有一个疑问: 为什么要销毁sdlrenderer_呢?创建sdlrenderer_的时候,又不需要width,height,format。 理论上销毁 sdltexture_ 就可以了,
    
    	if (sdltexture_) {
    		SDL_DestroyTexture(sdltexture_);
    		sdltexture_ = nullptr;
    	}
    	if (sdlrenderer_) {
    		SDL_DestroyRenderer(sdlrenderer_);
    		sdlrenderer_ = nullptr;
    	}
    
    	//第二步 确保线程安全,记录宽,高,
    	unique_lock<mutex> sdl_lock(mtx_);
    	width_ = w;			//记录窗口宽度,由于不管使用SDL还是OPENGL,宽,高,像素格式这三要素都是需要的,因此这三个变量记录在 xsdl的父类xvideoview中
    	height_ = h;		//记录窗口高度
    	fmt_ = fmt;			//记录像素格式
    	//第三步,创建 sdlwindow,这里也是要线程安全。那么创建的 sdlwindow 是sdl特有的变量,因此放在xsdl.h中
    	//sdlwindows 理论上也只能创建一次,
    	if (sdlwindow_ == nullptr) {
    		if (this->winid_ == nullptr) {
    			sdlwindow_ = SDL_CreateWindow("使用sdl技术显示视频文件",
    				SDL_WINDOWPOS_UNDEFINED,
    				SDL_WINDOWPOS_UNDEFINED,
    				width_,
    				height_,
    				SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    			if (nullptr == sdlwindow_) {
    				//创建sdlwindow失败
    				cout << "SDL_CreateWindow SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,width_,height_,SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLEerror "
    					<< "  width_ = " << width_ << " height_ = " << height_
    					<< SDL_GetError();
    				goto end;
    			}
    		}
    		else {
    			sdlwindow_ = SDL_CreateWindowFrom(this->winid_);
    			if (nullptr == sdlwindow_) {
    				//创建sdlwindow失败
    				cout << "SDL_CreateWindowFrom error  win_id = " << this->winid_ << "  SDL_GetError  = " << SDL_GetError();
    				goto end;
    			}
    		}
    	}
    
    	//第四步,创建 sdlrenderer_,这里也是要线程安全。那么创建的 sdlrenderer_ 是sdl特有的变量,因此放在xsdl.h中
    	sdlrenderer_ = SDL_CreateRenderer(sdlwindow_, -1, SDL_RENDERER_ACCELERATED);
    	if (nullptr == sdlrenderer_) {
    		cout << "SDL_CreateRenderer(sdlwindow_, -1, SDL_RENDERER_ACCELERATED) error try to use SDL_RENDERER_SOFTWARE  "  << "  SDL_GetError  = " << SDL_GetError();
    		sdlrenderer_ = SDL_CreateRenderer(sdlwindow_, -1, SDL_RENDERER_SOFTWARE);
    		if (nullptr == sdlrenderer_) {
    			cout << "SDL_CreateRenderer(sdlwindow_, -1, SDL_RENDERER_ACCELERATED) and use SDL_RENDERER_SOFTWARE  error " << "  SDL_GetError  = " << SDL_GetError();
    			goto end;
    		}
    	}
    
    	//第五步,创建 sdltexture_,这里也是要线程安全。那么创建的 sdltexture_ 是sdl特有的变量,因此放在xsdl.h中
    	switch (fmt_)
    	{
    	case XVideoView::YUV420P:
    		sdlformat = SDL_PIXELFORMAT_IYUV;
    		break;
    	case XVideoView::NV12:
    		sdlformat = SDL_PIXELFORMAT_NV12;
    		break;
    
    	case XVideoView::ARGB:
    		sdlformat = SDL_PIXELFORMAT_ARGB32;
    
    		break;
    
    	case XVideoView::RGBA:
    		sdlformat = SDL_PIXELFORMAT_RGBA32;
    
    		break;
    
    	case XVideoView::ABGR:
    		sdlformat = SDL_PIXELFORMAT_ABGR32;
    
    		break;
    
    	case XVideoView::BGRA:
    		sdlformat = SDL_PIXELFORMAT_BGRA32;
    
    		break;
    
    	default:
    		break;
    	}
    
    	//第六步,创建 sdltexture_,这里也是要线程安全。那么创建的 sdltexture_ 是sdl特有的变量,因此放在xsdl.h中
    
    	sdltexture_ = SDL_CreateTexture(sdlrenderer_,
    		sdlformat,
    		SDL_TEXTUREACCESS_STREAMING,
    		width_,
    		height_);
    
    	if (sdltexture_ == nullptr) {
    		cout << " SDL_CreateTexture(sdlrenderer_,sdlformat,SDL_TEXTUREACCESS_STREAMING,width_,height_)  error "
    			<< "  sdlformat = " << sdlformat
    			<< "  width_ = " << width_
    			<< "  height_ = " << height_
    			<< "  SDL_GetError  = " << SDL_GetError();
    		goto end;
    	}
    	//如果没有问题,sdl init就完成了,最终返回true.
    	return true;
    
    //一旦有error就会走到end 这里,不管哪一步有error,最终都是返回false
    end:
    	if (sdltexture_ != nullptr) {
    		SDL_DestroyTexture(sdltexture_);
    		sdltexture_ = nullptr;
    	}
    	if (sdlrenderer_ != nullptr) {
    		SDL_DestroyRenderer(sdlrenderer_);
    		sdlrenderer_ = nullptr;
    	}
    
    	if (sdlwindow_ != nullptr) {
    		SDL_DestroyWindow(sdlwindow_);
    		sdlwindow_ = nullptr;
    	}
    
    	SDL_Quit();
    	return false;
    }
    
    //sdl 的整个渲染过程,我们这里的核心是调用 SDL_UpdateYUVTexture方法,或者SDL_UpdateTexture方法,线程安全的
    bool XSDL::Draw(const unsigned char* data, int linesize)
    
    {
    
    	int ret = 0;
    	if (data == nullptr) {
    		cout << "XSDL::Draw(const char* data, int linesize) error because data = nullptr";
    		return false;
    	}
    	if (linesize <= 0) {
    		cout << "XSDL::Draw(const char* data, int linesize) error because linesize = " << linesize;
    		return false;
    	}
    	unique_lock<mutex> sdl_lock(mtx_);
    	if (!sdlwindow_ || !sdltexture_ || !sdlrenderer_ || width_ <= 0 || height_ <= 0) {
    		cout << "draw error ";
    		return false;
    
    	}
    
    	if (fmt_ == XVideoView::YUV420P) {
    		//如果是yuv格式的,调用 SDL_UpdateYUVTexture 方法
    		ret = SDL_UpdateYUVTexture(sdltexture_,
    			nullptr,
    			(const Uint8*)data, //Y 分量数据指针
    			linesize, // Y分量每行字节数,在没有对齐问题的情况下,linesize = width_,这里传递进来的linesize应该是考虑对齐问题后的值
    			(const Uint8*)data + linesize * height_, // U 分量数据指针,YUV420P的存储是先将 Y分量放置完毕,才会放置U分量,因此指针指向 _yuvdata开头 + 全部Y分量的大小
    			linesize / 2, // U分量 每行字节数  ,这里是_picwidth / 2,是因为 YUV420P的结构体是 Y分量与CbCr分量的水平方向比例是2:1(每2列就有1组CbCr分量);;Y分量与CbCr分量的垂直方向比例是2:1(每2行就有1组CbCr分量);;Y分量与CbCr分量的总比例是4 : 1
    			((const Uint8*)data + linesize * height_) + (linesize * height_ / 4),// V 分量数据指针,这里可以理解为 指针开始的位置 + ( Y分量 + U分量的) 所有大小
    			linesize / 2);// V分量 每行字节数
    }
    	else if (fmt_ == XVideoView::ARGB 
    		|| fmt_ == XVideoView::RGBA 
    		|| fmt_ == XVideoView::ABGR 
    		|| fmt_ == XVideoView::BGRA
    		|| fmt_ == XVideoView::NV12) {
    		//如果是RGB格式的,调用 SDL_UpdateTexture 方法
    		ret = SDL_UpdateTexture(sdltexture_,
    			NULL,
    			data,
    			linesize  //一行 y的字节数
    		);
    	}
    
    	if (ret < 0 ) {
    		cout << "SDL_UpdateYUVTexture or SDL_UpdateTexture error fmt_ = " << fmt_
    			<< " linesize = " << linesize
    			<< " width_ = " << width_
    			<< " height_ = " << height_
    			<< " SDL_GetError () = " << SDL_GetError();
    		return false;
    	}
    
    
    	ret = SDL_RenderClear(sdlrenderer_);
    	if (ret < 0) {
    		cout << "SDL_RenderClear(sdlrenderer_) error   "
    			<< " SDL_GetError () = " << SDL_GetError();
    		return false;
    	}
    	SDL_Rect rect;
    	rect.x = 0;
    	rect.y = 0;
    
    	//这里 SDL_RenderCopy 第三个参数,const SDL_Rect * srcrect,意思是,你要将 texture的哪些部分拿出来显示,传递NULL,表示整个texture
    	//第四个参数,const SDL_Rect * dstrect,意思是:要显示的数据,应该放置在window的什么位置。
    	//这里第四个参数就有说头了,当我们将 widget 的大小变化的时候,希望 视频也跟着变化,因此这里要用 存放视频的lable的大小
    	
    	//容错处理,实际上,在 xvideoviewrefactory的构造函数中我们 我们的设置了lable的宽和高
    	//那么为什么还要加这个容错处理呢?这是因为我们原先的打算就是让xsdl.cpp对 测试者(也就是 xvideoviewrefactory隐藏的)
    	//我们并不能保证 测试开发人员 一定会记录 labelwidth 和labelheight,因此多加了一层保护
    
    	//实现时,先要判断调用者,是否使用了 winid 去创建 sdlwindow,如果没有使用,那么就是后面两个参数都是 nullptr
    	if (winid_ == nullptr) {
    		ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, nullptr, nullptr);
    	}
    	else {
    		if (this->lablewidth_ <= 0) {
    			this->lablewidth_ = width_;
    		}
    		if (this->lableheight_ <= 0) {
    			this->lableheight_ = height_;
    		}
    		rect.w = lablewidth_;//要显示的位置和 当前lable的宽高保持一致
    		rect.h = lableheight_;
    		//这里不能如果要随着 user 将 widget 变大变小,要弄一个数组,记录各个 widget的大小,这里只是简单的用 整个texture(第三个参数),填充整个 sdlrenderer(第四个参数)
    		//ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, NULL, &rect);
    		ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, NULL, nullptr);
    
    	}
    
    	if (ret < 0) {
    		cout << "SDL_RenderCopy(sdlrenderer_, sdltexture_, NULL, &rect)   "
    			<< " SDL_GetError () = " << SDL_GetError();
    		return false;
    	}
    	SDL_RenderPresent(sdlrenderer_);
    
    	return true;
    }
    
    
    void XSDL::Close() {
    	//确保线程安全
    	unique_lock<mutex> sdl_lock(mtx_);
    
    	if (sdltexture_ != nullptr) {
    		SDL_DestroyTexture(sdltexture_);
    		sdltexture_ = nullptr;
    	}
    	if (sdlrenderer_ != nullptr) {
    		SDL_DestroyRenderer(sdlrenderer_);
    		sdlrenderer_ = nullptr;
    	}
    
    	if (sdlwindow_ != nullptr) {
    		SDL_DestroyWindow(sdlwindow_);
    		sdlwindow_ = nullptr;
    	}
    }
    
    bool XSDL::IsExit() {
    	SDL_Event ev;
    	//每次等待1ms,如果没有 quit事件发生,就返回false,有quit事件发生,就返回true
    	SDL_WaitEventTimeout(&ev, 1);
    	if (ev.type == SDL_QUIT) {
    		return true;
    	}
    	return false;
    }
    
    bool XSDL::Draw(
    	const unsigned  char* y, int y_pitch,
    	const unsigned  char* u, int u_pitch,
    	const unsigned  char* v, int v_pitch) {
    
    	//参数检查
    	if (!y || !u || !v) {
    		cout <<"func draw yuv error because (!y || !u || !v) ";
    		//return false;
    	}
    	//qDebug() << "y_pitch = " << y_pitch << " u_pitch = " << u_pitch << "  v_pitch = " << v_pitch;
    	unique_lock<mutex> sdl_lock(mtx_);
    	if (!sdlwindow_ || !sdltexture_ || !sdlrenderer_ || width_ <= 0 || height_ <= 0) {
    		cout << "draw error ";
    		return false;
    	}
    
    	//复制内存到显显存
    	cout << "_fmt = " << fmt_ << endl;
    	auto ret = SDL_UpdateYUVTexture(sdltexture_,
    		nullptr,
    		y, y_pitch,
    		u, u_pitch,
    		v, v_pitch);
    
    	if (ret != 0)
    	{
    		cout << "Draw SDL_UpdateYUVTexture error  y_pitch = " << y_pitch
    			<< " u_pitch = " << u_pitch
    			<< " v_pitch = " << v_pitch
    			<< "  "
    			<< SDL_GetError()
    			<< endl;
    		return false;
    	}
    
    	//清空屏幕
    	ret = SDL_RenderClear(sdlrenderer_);
    	if (ret < 0) {
    		cout << "Draw SDL_RenderClear(sdlrenderer_) error   "
    			<< " SDL_GetError () = " << SDL_GetError();
    		return false;
    	}
    	SDL_Rect rect;
    	rect.x = 0;
    	rect.y = 0;
    
    	//这里 SDL_RenderCopy 第三个参数,const SDL_Rect * srcrect,意思是,你要将 texture的哪些部分拿出来显示,传递NULL,表示整个texture
    	//第四个参数,const SDL_Rect * dstrect,意思是:要显示的数据,应该放置在window的什么位置。
    	//这里第四个参数就有说头了,当我们将 widget 的大小变化的时候,希望 视频也跟着变化,因此这里要用 存放视频的lable的大小
    
    	//容错处理,实际上,在 xvideoviewrefactory的构造函数中我们 我们的设置了lable的宽和高
    	//那么为什么还要加这个容错处理呢?这是因为我们原先的打算就是让xsdl.cpp对 测试者(也就是 xvideoviewrefactory隐藏的)
    	//我们并不能保证 测试开发人员 一定会记录 labelwidth 和labelheight,因此多加了一层保护
    
    	//实现时,先要判断调用者,是否使用了 winid 去创建 sdlwindow,如果没有使用,那么就是后面两个参数都是 nullptr
    	if (winid_ == nullptr) {
    		ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, nullptr, nullptr);
    	}
    	else {
    		if (this->lablewidth_ <= 0) {
    			this->lablewidth_ = width_;
    		}
    		if (this->lableheight_ <= 0) {
    			this->lableheight_ = height_;
    		}
    		rect.w = lablewidth_;//要显示的位置和 当前lable的宽高保持一致
    		rect.h = lableheight_;
    		//这里 如果要跟随者 user 将界面变大变小 实现widgetUI 也跟着变化,那么需要弄多个数组,记录变大变小后的 size
    		//ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, nullptr, &rect);
    		ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, nullptr, nullptr);
    
    	}
    
    	if (ret < 0) {
    		cout << "Draw SDL_RenderCopy(sdlrenderer_, sdltexture_, NULL, &rect)   "
    			<< " SDL_GetError () = " << SDL_GetError();
    		return false;
    	}
    	SDL_RenderPresent(sdlrenderer_);
    
    	return true;
    }

    myutils.h

    #pragma once
    #include <ctime>
    #include <iostream>
    #include <thread>
    
    void MSleep(unsigned int ms);
    
    //获取当前时间戳 毫秒
    long long NowMs();
    

    myutils.cpp

    #include "myutils.h";
    using namespace std;
    
    void MSleep(unsigned int ms)
    {
    	auto beg = clock();
    	for (int i = 0; i < ms; i++)
    	{
    		this_thread::sleep_for(1ms);
    		if ((clock() - beg) / (CLOCKS_PER_SEC / 1000) >= ms)
    			break;
    	}
    }
    
    
    
    // clock()函数的原型是clock_t clock(void);,
    // 它返回一个clock_t类型的值,表示程序从启动到函数调用时消耗的CPU时间,单位是时钟周期数。
    // 这个值并不直接代表实际的时间单位(如秒或毫秒),需要通过CLOCKS_PER_SEC进行转换‌
    //对于 CLOCKS_PER_SEC,在windows上是1000(表示的是毫秒),在linux上是 1000000(表示的是微秒)
    long long NowMs()
    {
    	return clock() / (CLOCKS_PER_SEC / 1000);
    }

    118ParseH264ToAVpacketToAVFrameThoughHwDeviceSimple.cpp

    // 117ParseH264ToAVpacketToAVFrameAndUseSDLShowAVFrame.cpp 
    // 1. 从h264 裸流中 通过ffmpeg方法 中获得 avpacket
    // 2. 将获得的avpacket 转换成 avframe
    // 3. 将avframe存储成 xxx.yuv file 
    // 4. 将avframe 通过sdl 播放出来
    //
    
    #include <iostream>
    #include <fstream>
    using namespace std;
    #include "xsdl.h"
    #include "myutils.h"
    
    extern "C" {
    #include "libavcodec/avcodec.h"
    #include "libavutil/error.h"
    }
    
    #define readfilebufsize 4096                   //从h264文件中每次读取的最大字节数
    
    #undef main
    int main()
    {
    	int avframenumber = 0;
    
    
    	///加入xvideoview 相关。
    	XVideoView* xvideoview = XVideoView::create();
    	bool is_init_win = false;// xvideoview的init只做一次
    
    		//测试一下多线程对于解码的影响,在解码的开始记录时间,然后计算一秒中,解码了多少次
    	long long starttime = NowMs();//测试一下多线程对于解码的影响,
    	int xjiemacount = 0;//测试一下多线程对于解码的影响,
    
    
    	int ret = 0;
    
    	//0.要读取的文件
    
    	//ifstream readifstream("./118/400_300_25.h264", ios_base::binary);
    	ifstream readifstream("./118/test.h264", ios_base::binary);
    	//ofstream writefstream("./118/400_300_25ruanjiema_yuv420p.yuv", ios_base::binary);
    	ofstream writefstream("./118/400_300_25yingjiema_nv12.yuv", ios_base::binary);
    
    	if (readifstream.is_open() == false) {
    		cout << "open read file error" << endl;
    		return -1;
    	}
    	if (writefstream.is_open() == false) {
    		cout << "open write file error" << endl;
    		return -1;
    	}
    
    	//1.创建一个 对应解析器上下文 -  AVCodecParserContext
    	int codec_id = AV_CODEC_ID_H264;
    	AVCodecParserContext* avcodecParseContext = av_parser_init(codec_id);
    
    	//2.创建解码器 和 解码器上下文。 
    	AVCodec* avcodec = avcodec_find_decoder((enum AVCodecID)codec_id);
    	AVCodecContext* avcodecContext = avcodec_alloc_context3(avcodec);
    	avcodecContext->gop_size = 25;
    	avcodecContext->max_b_frames = 0; //让 b 帧的数量变成0,也就是不要b帧。可以不设置
    	avcodecContext->thread_count = 16;//测试开启多线程下,一秒中解析多少次。
    
    	//10. 硬解码相关
    	AVBufferRef* hwbuffer = nullptr;//硬加码上下文
    	ret = av_hwdevice_ctx_create(&hwbuffer, AVHWDeviceType::AV_HWDEVICE_TYPE_DXVA2, nullptr, nullptr, 0);
    	avcodecContext->hw_device_ctx = av_buffer_ref(hwbuffer);
    	av_buffer_unref(&hwbuffer);//顺手就将 hwbuffer 清理了。
    
    	//3.打开编码器
    	ret = avcodec_open2(avcodecContext, nullptr, nullptr);
    	if (ret != 0) {
    		cout << "func avcodec_open2()  fail" << endl;
    		return -2;
    	}
    
    	//4.创建要读取的数据的缓存
    	uint8_t readfilebuf[readfilebufsize + AV_INPUT_BUFFER_PADDING_SIZE] = { 0 };
    	cout << "readfilebuf sizeof(readbuf) = " << sizeof(readfilebuf) << endl; // 结果是4,测试sizeof(指针大小用的,和本代码无关)
    
    
    	//5.1创建avpakcet,目的是 通过av_parser_parse2方法 ,从解码器中 给avpacket的data 和size赋值.
    	AVPacket* avpacket = av_packet_alloc();
    	//5.1创建avframe ,目的是 通过解码器从 avpacket中获得avframe ,注意这里我们并没有设置任何的avframe的分辨率,格式等信息
    	AVFrame* avframe = av_frame_alloc();
    
    	//10 硬编码相关。如果硬编码开启的,
    	// 那么 ,我们 从avcodecContext 获得的avframe 是NV12格式的,
    	// 我们 定义的这个  hw_avframe 用于在硬编码阶段 通过avcodec_receive_frame解析到的avframe
    	//int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *hw_avframe);
    	AVFrame* hw_avframe = av_frame_alloc();
    
    
    
    	//6.我们这里为了看一下使用的CPU 和 GPU 的使用情况,需要用一个比较大的文件,或者循环的解析这个文件。
    	//这里使用循环解析的方法,那么这里最好的方案是知道文件的大小,如果每次读取的数据之和 和 文件大小一样了,则需要gseek 到文件开始文件,重新读取
    
    	//readifstream.seekg(0, std::ios::end); // 指针移至末尾
    	//std::streampos fileallsize = readifstream.tellg(); // C++中tellg()是输入流文件定位函数,主要用于获取当前输入流指针的位置,由于在前面,我们是从文件开头位置 移动到 末尾,因此通过tellg() 就能获得文件的大小
    	//cout << "fileallsize = " << fileallsize << endl; //单位是字节。 这个可以通过每次read 后,得到的gcount 相加,和 fileallsize 的值 比较,判断是否读取到了文件结尾
    
    	//readifstream.seekg(0, std::ios::beg); // 重置指针到开头
    
    
    	//6. 开始读取文件直到文件读取完成,这里为了测试,要写一个循环,让一直读取h264文件
    	while (true) {
    		readifstream.read((char*)readfilebuf, readfilebufsize);
    		if (readifstream.bad() == true) {
    			cout << "read file bad" << endl;
    			break;
    		}
    		int realfilebufreadcount = readifstream.gcount(); // gcount() 返回最后一次非格式化输入操作(如 read()、get()、getline())实际读取的字符数(字节数)
    		if (realfilebufreadcount <= 0) { //如果读取的数据是<=0的,说明整个h264文件已经读取完成了,或者读取h264文件的时候出现了错误了,那么就要直接退出
    			cout << "read file break count  = " << realfilebufreadcount << endl;
    			//添加循环用的。先需要调用 clear()函数,通过以 state 的值赋值,设置流错误状态标志。默认赋值 std::ios_base::goodbit ,它拥有的效果为清除所有错误状态标志。
    			//void clear( std::ios_base::iostate state = std::ios_base::goodbit );
    			//readifstream.clear();
    			这时候将 readifstream转到 文件开头
    			//readifstream.seekg(0, std::ios::beg);
    			//continue; //为了循环 continue;
    			break;//如果不循环,则这里要break;
    		}
    		//到这里说明读取到了 realbufreadcount 个字节的数据
    		cout << "realfilebufreadcount = " << realfilebufreadcount << endl;
    
    		//7. 根据读取到的字节,解析这段字节,解析出来avpacket.
    		//我们知道当 h264 数据是 大于 4096字节时,第一次读取的一定是4096 个字节。而从4096中肯定是能解析出来很多个avpacket的
    		//而解析的方法是使用 av_parser_parse2方法,av_parser_parse2方法的返回值是已经解析了多少个字节。
    		//因此这里要弄一个循环,让 realfilebufreadcount = (从文件读取到的 realfilebufreadcount字节) - (每次av_parser_parse2方法解析过的字节)
    		//如果 realbufreadcount 大于0,就可以继续解析,如果
    		uint8_t* tempreadfilebuf = readfilebuf;//让 tempreadfilebuf 指向 readfilebuf的指针。这里为什么不直接用readfilebuf呢?从功能实现上直接用buf也是可以的,但是这是C语言的基本功,尽量不要用原始指针。而是找一个复制的指针。
    
    		while (realfilebufreadcount > 0) {
    			int parsebufsize = av_parser_parse2(
    				avcodecParseContext,
    				avcodecContext,
    				&avpacket->data,
    				&avpacket->size,
    				tempreadfilebuf,
    				realfilebufreadcount,
    				0, 0, 0);
    
    			realfilebufreadcount = realfilebufreadcount - parsebufsize;
    			tempreadfilebuf = tempreadfilebuf + parsebufsize;
    
    			//这时候还要判断是否packet的size是大于0的,说明是正常截取了一段avpacket数据,因为av_parser_parse2方法即使分析据,也并不能保证真正的解析到 avpacket
    			if (avpacket->size > 0) {
    				cout << "avpacket->size = " << avpacket->size << endl;
    
    				// 如果avpacket 的size 大于0,说明已经有了avpacket了,也就是说,到这里,已经对h264进行了分离,得到了avpacket
    				// 那么下来,我们就需要将 对avpacket进行处理,在最后,记得调用av_packet_unref(avpacket);
    				// 8. 将avpacket发送给 avcodecContext
    				ret = avcodec_send_packet(avcodecContext, avpacket);
    				if (ret < 0) {
    					//已发送空包(pkt=NULL)触发编解码器刷新,且后续无新数据可处理(流结束标志)。
    					//说明 tempreadfilebuf 中的数据已经读取完成了,那么要continue 还是break呢?
    					//这里应该是continue比较合理,就是我们依赖 将 tempreadfilebuf 读取完成,跳出循环的条件应该是 realfilebufreadcount > 0
    					cout << "avcodec_send_packet return error " << endl;
    					break;;
    				}
    				if (ret == 0) {
    
    					// FFmpeg3 版本后解码接口改成了avcodec_send_packet和avcodec_receive_frame,这两个接口需要配合使用。
    					// 当发送一个packet后,可能需要多次调用avcodec_receive_frame来获取所有解码后的帧,尤其是当解码器内部有缓存时。
    					while (true) {
    						// 9.正确的发送数据到 解码器了,那么就通过 avcodec_receive_frame 中得到avframe,这里还是写一个循环
    						if (avcodecContext->hw_device_ctx != nullptr) {
    							//开启了硬件加速,那么将解析的数据放在 hw_avframe 中,
    							// hw_avframe 的 数据格式为 AV_PIX_FMT_DXVA2_VLD。
    							// 我们需要将hw_avframe AV_PIX_FMT_DXVA2_VLD格式的数据  转换到 avframe 中
    							ret = avcodec_receive_frame(avcodecContext, hw_avframe);
    							if (ret < 0) {
    								cout << "hw device func avcodec_receive_frame error" << endl;
    								break;
    							}
    							if (ret == 0) {
    								//将hw_avframe的数据转到 avframe
    								cout << "hw device  hw_avframe->format = " << hw_avframe->format << endl;
    								int ret_av_hwframe_transfer_data = av_hwframe_transfer_data(avframe, hw_avframe, 0);
    								if (ret_av_hwframe_transfer_data < 0) {
    									cout << "hw device  func av_hwframe_transfer_data error" << endl;
    									break;
    								}
    								if (ret_av_hwframe_transfer_data == 0) {
    									//得到正常的 avframe
    									//1.存储数据。
    									//存储 AV_PIX_FMT_NV12 格式的。
    									//那么就要明白 AV_PIX_FMT_NV12 格式是啥样子的,才好存储。
    									//AV_PIX_FMT_NV12 格式参考 : https://www.cnblogs.com/mjios/p/14686970.html
    
    									// Y Y Y Y
    									// Y Y Y Y
    									// U V U V
    									if (avframe->linesize[0] == avframe->width) {
    										//没有字节对齐问题
    										writefstream.write((char*)avframe->data[0], avframe->linesize[0] * avframe->height);
    										writefstream.write((char*)avframe->data[1], avframe->linesize[1] * avframe->height / 2);
    									}
    									else {
    										//有字节对齐问题时候的处理
    										for (int j = 0; j < avframe->height; j++) {
    											writefstream.write((char*)(avframe->data[0] + j * avframe->linesize[0]), avframe->width);
    										}
    										for (int j = 0; j < avframe->height / 2; j++) {
    											writefstream.write((char*)(avframe->data[1] + j * avframe->linesize[1]), avframe->width);
    										}
    									}
    								}
    							}
    						}
    
    						else {
    							//没有开启硬件加速
    							ret = avcodec_receive_frame(avcodecContext, avframe);
    							if (ret < 0) {
    								cout << "software func avcodec_receive_frame error" << endl;
    								break;
    							}
    							if (ret == 0) {
    								//1.存储数据
    								if (avframe->linesize[0] == avframe->width) {
    									//没有字节对齐问题
    									writefstream.write((char*)avframe->data[0], avframe->linesize[0] * avframe->height);
    									writefstream.write((char*)avframe->data[1], avframe->linesize[1] * avframe->height / 2);
    									writefstream.write((char*)avframe->data[2], avframe->linesize[2] * avframe->height / 2);
    								}
    								else {
    									//有字节对齐问题时候的处理
    									// 先写完Y,再写U,再写V
    									for (int j = 0; j < avframe->height; j++) {
    										writefstream.write((char*)(avframe->data[0] + j * avframe->linesize[0]), avframe->width);
    									}
    									for (int j = 0; j < avframe->height / 2; j++) {
    										writefstream.write((char*)(avframe->data[1] + j * avframe->linesize[1]), avframe->width / 2);
    									}
    									for (int j = 0; j < avframe->height / 2; j++) {
    										writefstream.write((char*)(avframe->data[2] + j * avframe->linesize[2]), avframe->width / 2);
    									}
    								}
    							}
    						}
    
    						//2.show 数据
    						if (!is_init_win)
    						{
    							is_init_win = true;
    							xvideoview->Init(avframe->width,
    								avframe->height,
    								(XVideoView::Format)avframe->format);
    						}
    						xvideoview->DrawFrame(avframe);
    
    
    						//3. 记录 
    						//解码相关。计算1秒钟解码了多少次
    						//正常写法,但是有我们测试的视频太短了,不能说明问题
    
    						xjiemacount++;
    						auto cur = NowMs();
    						if (cur - starttime >= 1000) {
    							cout << "1秒钟解码了" << xjiemacount  << "次" << endl;
    							xjiemacount = 0;
    							starttime = cur;
    						}
    					}
    				}
    
    
    
    				//这里为了安全期间,最好还是调用一下 av_packet_unref(avpacket);实际上不调用,应该也问题,除了在流媒体24小时不间断播放的情况下,avpacket的ref count 才有可能超过int的最大值
    				av_packet_unref(avpacket);//
    			}
    
    		}
    
    		//在循环的最后判断是否到了文件的最后.能走到这里的机会不多,
    		//我们假设 要读取的文件大小是4000字节,而我们每次读取1000个字节,那么第4次的时候,就刚好读取文件文件的结尾了,就能走到这个逻辑了
    		//实际上我们大多数情况下,文件的大小和 我们每次读取的字节,都不会刚好能整除的。
    		//这块逻辑实际上也是可以不写的,不写大不了再次读取一次,最终都是要会读取的文件的大小为0,也就是在前面根据 realfilebufreadcount <= 0 跳出循环 
    		// 我们这里要加这个,主要是为了 循环读取这个文件。
    		if (readifstream.eof() == true) {
    			cout << "readifstream.eof() = true " << realfilebufreadcount << endl;
    			//添加循环用的。先需要调用 clear()函数,通过以 state 的值赋值,设置流错误状态标志。默认赋值 std::ios_base::goodbit ,它拥有的效果为清除所有错误状态标志。
    			//void clear( std::ios_base::iostate state = std::ios_base::goodbit );
    			//readifstream.clear();
    			这时候将 readifstream转到 文件开头
    			//readifstream.seekg(0, std::ios::beg);
    			//continue;//为了循环 continue;
    			break;//如果不循环,则这里要break;
    		}
    	}
    
    	//刷新缓冲
    	avcodec_send_packet(avcodecContext, NULL);
    	while (true) {
    		// 9.正确的发送数据到 解码器了,那么就通过 avcodec_receive_frame 中得到avframe,这里还是写一个循环
    		if (avcodecContext->hw_device_ctx != nullptr) {
    			//开启了硬件加速,那么将解析的数据放在 hw_avframe 中,
    			// hw_avframe 的 数据格式为 AV_PIX_FMT_DXVA2_VLD。
    			// 我们需要将hw_avframe AV_PIX_FMT_DXVA2_VLD格式的数据  转换到 avframe 中
    			ret = avcodec_receive_frame(avcodecContext, hw_avframe);
    			if (ret < 0) {
    				cout << "hw device func avcodec_receive_frame error" << endl;
    				break;
    			}
    			if (ret == 0) {
    				//将hw_avframe的数据转到 avframe
    				cout << "hw device  hw_avframe->format = " << hw_avframe->format << endl;
    				int ret_av_hwframe_transfer_data = av_hwframe_transfer_data(avframe, hw_avframe, 0);
    				if (ret_av_hwframe_transfer_data < 0) {
    					cout << "hw device  func av_hwframe_transfer_data error" << endl;
    					break;
    				}
    				if (ret_av_hwframe_transfer_data == 0) {
    					//得到正常的 avframe
    					//1.存储数据。
    					//存储 AV_PIX_FMT_NV12 格式的。
    					//那么就要明白 AV_PIX_FMT_NV12 格式是啥样子的,才好存储。
    					//AV_PIX_FMT_NV12 格式参考 : https://www.cnblogs.com/mjios/p/14686970.html
    
    					// Y Y Y Y
    					// Y Y Y Y
    					// U V U V
    					if (avframe->linesize[0] == avframe->width) {
    						//没有字节对齐问题
    						writefstream.write((char*)avframe->data[0], avframe->linesize[0] * avframe->height);
    						writefstream.write((char*)avframe->data[1], avframe->linesize[1] * avframe->height / 2);
    					}
    					else {
    						//有字节对齐问题时候的处理
    						for (int j = 0; j < avframe->height; j++) {
    							writefstream.write((char*)(avframe->data[0] + j * avframe->linesize[0]), avframe->width);
    						}
    						for (int j = 0; j < avframe->height / 2; j++) {
    							writefstream.write((char*)(avframe->data[1] + j * avframe->linesize[1]), avframe->width);
    						}
    					}
    				}
    			}
    		}
    
    		else {
    			//没有开启硬件加速
    			ret = avcodec_receive_frame(avcodecContext, avframe);
    			if (ret < 0) {
    				cout << "software func avcodec_receive_frame error" << endl;
    				break;
    			}
    			if (ret == 0) {
    				//1.存储数据
    				if (avframe->linesize[0] == avframe->width) {
    					//没有字节对齐问题
    					writefstream.write((char*)avframe->data[0], avframe->linesize[0] * avframe->height);
    					writefstream.write((char*)avframe->data[1], avframe->linesize[1] * avframe->height / 2);
    					writefstream.write((char*)avframe->data[2], avframe->linesize[2] * avframe->height / 2);
    				}
    				else {
    					//有字节对齐问题时候的处理
    					// 先写完Y,再写U,再写V
    					for (int j = 0; j < avframe->height; j++) {
    						writefstream.write((char*)(avframe->data[0] + j * avframe->linesize[0]), avframe->width);
    					}
    					for (int j = 0; j < avframe->height / 2; j++) {
    						writefstream.write((char*)(avframe->data[1] + j * avframe->linesize[1]), avframe->width / 2);
    					}
    					for (int j = 0; j < avframe->height / 2; j++) {
    						writefstream.write((char*)(avframe->data[2] + j * avframe->linesize[2]), avframe->width / 2);
    					}
    				}
    			}
    		}
    
    		//2.show 数据
    		if (!is_init_win)
    		{
    			is_init_win = true;
    			xvideoview->Init(avframe->width,
    				avframe->height,
    				(XVideoView::Format)avframe->format);
    		}
    		xvideoview->DrawFrame(avframe);
    	}
    
    	if (hw_avframe != nullptr) {
    		av_frame_free(&hw_avframe);
    	}
    	if (avframe != nullptr) {
    		av_frame_free(&avframe);
    	}
    	if (writefstream) {
    		writefstream.close();
    	}
    	if (readifstream) {
    		readifstream.close();
    	}
    	if (avpacket != nullptr) {
    		av_packet_free(&avpacket);
    	}
    
    	if (avcodecContext != nullptr) {
    		avcodec_free_context(&avcodecContext);
    	}
    	if (avcodecParseContext != nullptr) {
    		av_parser_close(avcodecParseContext);
    	}
    
    	cout << " avframenumber = " << avframenumber << endl;
    
    	return 0;
    
    }
    
    
    

    bug fix

    1. 在有字节对齐问题的时候,我们从 avframe的data 写入ofstream,将avframe 的data 写入 数组中。

    从 yuv file 中读取数据到avframe 的data 中;;;将avfreame 的data 写入 yuvfile 中;;;将avfreame 的data 写入 数组中-CSDN博客

    NV12

    				if (send_avframe->linesize[0] == send_avframe->width) {
    					//没有字节对齐问题
    					writefstream.write((char*)send_avframe->data[0], send_avframe->linesize[0] * send_avframe->height);
    					writefstream.write((char*)send_avframe->data[1], send_avframe->linesize[1] * send_avframe->height / 2);
    				}
    				else {
    					//有字节对齐问题时候的处理
    					for (int j = 0; j < send_avframe->height; j++) {
    						writefstream.write((char*)(send_avframe->data[0] + j * send_avframe->linesize[0]), send_avframe->width);
    					}
    					for (int j = 0; j < avframe->height / 2; j++) {
    						writefstream.write((char*)(send_avframe->data[1] + j * send_avframe->linesize[1]), send_avframe->width);
    					}
    				}

    yuv420p

    if (send_avframe->linesize[0] == send_avframe->width) {
    					//没有字节对齐问题
    					writefstream.write((char*)send_avframe->data[0], send_avframe->linesize[0] * send_avframe->height);
    					writefstream.write((char*)send_avframe->data[1], send_avframe->linesize[1] * send_avframe->height / 2);
    					writefstream.write((char*)send_avframe->data[2], send_avframe->linesize[2] * send_avframe->height / 2);
    				}
    				else {
    					//有字节对齐问题时候的处理
    					// 先写完Y,再写U,再写V
    					for (int j = 0; j < send_avframe->height; j++) {
    						writefstream.write((char*)(send_avframe->data[0] + j * send_avframe->linesize[0]), send_avframe->width);
    					}
    					for (int j = 0; j < avframe->height / 2; j++) {
    						writefstream.write((char*)(send_avframe->data[1] + j * send_avframe->linesize[1]), send_avframe->width / 2);
    					}
    					for (int j = 0; j < avframe->height / 2; j++) {
    						writefstream.write((char*)(send_avframe->data[2] + j * send_avframe->linesize[2]), send_avframe->width / 2);
    					}
    				}

    从 avframe 的data 中读取数据到 数组中

    NV12_data_ = new uint8_t[4096 * 2160 * 1.5];
    
    if (frame->linesize[0] == frame->width)
    		{
    			// 如果没有字节对齐问题,直接先copy avframe->data[0];然后再copy avframe->data[1]
    			memcpy(NV12_data_, frame->data[0], frame->linesize[0] * frame->height); //Y
    			memcpy(NV12_data_ + frame->linesize[0] * frame->height, frame->data[1], frame->linesize[1] * frame->height / 2); //UV
    		}
    		else //逐行复制
    		{
    			//这里字节有对齐问题的时候,为什么要这样 copy 呢?
    			//而且实验测试,当 avframe是 400 * 300 时候,字节对齐有问题时候,就是用这种方法就能解决。
    			for (int i = 0; i < frame->height; i++) //Y
    			{
    				memcpy(NV12_data_ + i * frame->width,
    					frame->data[0] + i * frame->linesize[0],
    					frame->width
    				);
    			}
    			for (int i = 0; i < frame->height / 2; i++)  //UV
    			{
    				auto p = NV12_data_ + frame->height * frame->width;// 移位Y
    				memcpy(p + i * frame->width,
    					frame->data[1] + i * frame->linesize[1],
    					frame->width
    				);
    			}
    		}

    [记录一个bug]av_hwdevice_ctx_create出现Cannot allocate memory(附带库运行时路径)[已解决]_ffmpeg cannot allocate memory-CSDN博客

    C语言面试八股文是指在春季招聘中常见的C语言相关的面试题目和知识点。下面是一份常见的C语言面试八股文,供您参考: 1. C语言的基本数据类型有哪些? C语言的基本数据类型包括整型、浮点型、字符型和指针型。 2. 请介绍一下C语言中的变量和常量。 变量是用来存储数据的内存位置,可以通过变量名来访问和修改其值。常量是指在程序执行过程中不会改变的值。 3. 什么是数组?请介绍一下C语言中的数组。 数组是一种存储相同类型数据的集合,通过索引来访问数组中的元素。在C语言中,数组的大小在定义时就需要确定,并且数组的下标从0开始。 4. 请介绍一下C语言中的指针。 指针是一个变量,其值为另一个变量的地址。通过指针可以直接访问和修改内存中的数据。使用指针可以提高程序的效率和灵活性。 5. 请介绍一下C语言中的函数。 函数是一段完成特定任务的代码块,可以通过函数名来调用执行。函数可以接收参数并返回一个值,也可以不接收参数或不返回值。 6. 请介绍一下C语言中的流程控制语句。 C语言中的流程控制语句包括条件语句(if-else语句、switch语句)、循环语句(for循环、while循环、do-while循环)和跳转语句(break语句、continue语句、goto语句)。 7. 请介绍一下C语言中的结构体。 结构体是一种自定义的数据类型,可以包含多个不同类型的成员变量。通过结构体可以将多个相关的数据组织在一起。 8. 请介绍一下C语言中的文件操作。 C语言中的文件操作主要包括打开文件、读写文件和关闭文件。可以使用标准库函数来进行文件操作,如fopen、fread、fwrite、fclose等。
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值