FFmpeg 硬件解码全流程解析

ffmpeg在解码过程中,一般使用的是软件解码,即使用CPU去解码; 但如果需要解码4k、8k等分辨率的视频时,再使用软件解码,就会使得CPU占用率非常大,如果CPU跟不上可能会出现某些异常情况; 所以,需要借助硬件解码去处理此类问题,即GPU解码;

流程:

cpp 复制代码
1. 准备硬件
   └─ av_hwdevice_ctx_create() → 创建硬件设备

2. 初始化解码器并绑定硬件
   └─ avcodec_find_decoder() → avcodec_alloc_context3() → 绑定hw_device_ctx → avcodec_open2()

3. 解析裸流,循环解码
   ├─ av_parser_parse2() → 提取一帧压缩数据(AVPacket)
   ├─ avcodec_send_packet() → 送压缩数据
   └─ avcodec_receive_frame() → 取出解码帧(硬件格式)

4. 硬件帧 → 内存(关键)
   └─ av_hwframe_transfer_data() → 将GPU数据拷贝到CPU(sw_frame)

5. 渲染显示
   ├─ 将sw_frame的NV12数据拷贝到nv12data
   └─ SDL_UpdateTexture() → SDL_RenderCopy() → SDL_RenderPresent()

6. 冲刷解码器(发送NULL包,取完剩余帧)

7. 释放资源(解码器、设备、SDL等)

头文件:#include "libavutil/hwcontext.h"

首先查看电脑支持的硬件:

cpp 复制代码
// 查找解码器
const AVCodec *decodec = avcodec_find_decoder(AV_CODEC_ID_H264);

// 打印所有硬件
for (int i = 0; ; ++i) {
    const AVCodecHWConfig *config = avcodec_get_hw_config(decodec, i);
    if (!config) {
        break;
    }

    if (config->device_type) {
        std::cout << av_hwdevice_get_type_name(config->device_type) << std::endl;
    }
}

如下是我电脑所打印结果:

bash 复制代码
dxva2
d3d11va
d3d12va
cuda
vaapi
vulkan

学习就使用‌DXVA2,因为它兼容最广,不容易失败。

ffmpeg也有与之相对应的枚举(AVHWDeviceType),用于查找硬件设备;

bash 复制代码
dxva2							AVHWDeviceType::AV_HWDEVICE_TYPE_DXVA2
d3d11va							AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA
d3d12va							AVHWDeviceType::AV_HWDEVICE_TYPE_D3D12VA
cuda							AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA
vaapi							AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI
vulkan							AVHWDeviceType::AV_HWDEVICE_TYPE_VULKAN

当硬件解码出来的一帧AVFrame,他的格式也是有与之相对应的硬件类型(AVPixelFormat);

bash 复制代码
dxva2							AVPixelFormat::AV_PIX_FMT_DXVA2_VLD
d3d11va							AVPixelFormat::AV_PIX_FMT_D3D11
d3d12va							AVPixelFormat::AV_PIX_FMT_D3D12
cuda							AVPixelFormat::AV_PIX_FMT_CUDA
vaapi							AVPixelFormat::AV_PIX_FMT_VAAPI
vulkan							AVPixelFormat::AV_PIX_FMT_VULKAN

当然,硬件解码出来的AVFrame,是在显存中的,即在GPU中,需要将其拷贝到内存中进行处理渲染处理才能显示,即拷贝到CPU中;(也可以在GPU中直接渲染显示的,这是高级用法了)

通过 av_hwframe_transfer_data 函数,可以将AVFrame从GPU拷贝到CPU中; 拷贝到GPU后的AVFrame,格式为AVPixelFormat::AV_PIX_FMT_NV12类型;

关键的步骤是,第一,硬件设备初始化;第二,将硬件绑定到解码器中;第三,显存拷贝到内存,即GUP拷贝到CPU;下方⭐️标注着。

1. av_hwdevice_ctx_create

av_hwdevice_ctx_create 是 FFmpeg 中用于初始化硬件设备上下文的核心函数。它封装了复杂的硬件初始化过程,是使用 FFmpeg 硬件加速的起点。

①函数原型

cpp 复制代码
int av_hwdevice_ctx_create(
    AVBufferRef **pdevice_ref,      // 输出:硬件设备上下文(引用计数管理)
    enum AVHWDeviceType type,       // 硬件设备类型
    const char *device,             // 设备名称(如 "0" 表示第一个GPU)
    AVDictionary *opts,             // 可选参数字典(通常 NULL)
    int flags                       // 标志位(通常 0)
);

②参数说明

参数 类型 说明
pdevice_ref AVBufferRef ‌** ‌输出参数,指向创建好的硬件设备上下文。通过 AVBuffer 引用计数管理内存,必须用 av_buffer_unref() 释放
type enum AVHWDeviceType 硬件设备类型(见下表)
device const char * 设备标识字符串。如 "0" 表示第一个设备,NULL 表示默认设备
opts AVDictionary * 设备特定的选项参数(如指定 CUDA 设备 ID),不需要填 NULL
flags int 标志位,通常填 0

AVHWDeviceType类型:

类型 说明 平台
AV_HWDEVICE_TYPE_CUDA NVIDIA GPU(NVDEC/NVENC) Windows / Linux
AV_HWDEVICE_TYPE_DXVA2 DirectX Video Acceleration 2 Windows
AV_HWDEVICE_TYPE_D3D11VA Direct3D 11 Video Acceleration Windows
AV_HWDEVICE_TYPE_D3D12VA Direct3D 12 Video Acceleration Windows 10+
AV_HWDEVICE_TYPE_QSV Intel Quick Sync Video Windows / Linux
AV_HWDEVICE_TYPE_VAAPI VAAPI(Intel/AMD) Linux
AV_HWDEVICE_TYPE_VIDEOTOOLBOX Apple VideoToolbox macOS / iOS
AV_HWDEVICE_TYPE_MEDIACODEC Android MediaCodec Android
AV_HWDEVICE_TYPE_VULKAN Vulkan 视频解码 跨平台

③返回值

  • 成功:返回 0
  • 失败:返回一个负值的 AVERROR 错误码

④使用示例

cpp 复制代码
AVHWDeviceType hwtype = AV_HWDEVICE_TYPE_DXVA2;
AVBufferRef *hw_device_ctx = nullptr;

// 创建硬件设备上下文
int ret = av_hwdevice_ctx_create(&hw_device_ctx, hwtype, nullptr, nullptr, 0);
if (ret < 0) {
    std::cout << "av_hwdevice_ctx_create failed!" << std::endl;
    PrintErr(ret);
    return -1;
}

注意,硬件设备需要绑定到解码器上下文中:decodec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);

2. av_hwframe_transfer_data

av_hwframe_transfer_data 是 FFmpeg 中用于在硬件(GPU)和系统内存(CPU)之间传输视频帧数据的核心函数。

简单来说,当使用硬件加速解码后,解码出的 AVFrame 数据可能依然存放在 GPU 显存中。这个函数就是负责将数据从 GPU 显存"搬运"到 CPU 可访问的系统内存中,以便进行后续处理(如保存为文件、通过软件渲染器显示等)。

①函数原型

cpp 复制代码
int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags);

②参数说明

参数 类型 说明
dst AVFrame * ‌目标帧‌(接收数据),通常是软件帧(CPU 内存)
src const AVFrame * ‌源帧‌(提供数据),通常是硬件帧(GPU 显存),由 avcodec_receive_frame() 解码得到
flags int 标志位,通常填 0

③返回值

  • 成功:返回 0
  • 失败:返回一个负值的 AVERROR 错误码

④使用示例

cpp 复制代码
// 显存拷贝到内存 gpu->cpu
re = av_hwframe_transfer_data(sw_frame, hw_frame, 0);
if (re < 0) {
    std::cout << "av_hwframe_transfer_data failed!" << std::endl;
    PrintErr(re);
    return -1;
}
bash 复制代码
GPU 显存(硬件帧)  			──av_hwframe_transfer_data──▶  CPU 内存(软件帧)
hw_frame (format=AV_PIX_FMT_CUDA)                    		sw_frame (format=AV_PIX_FMT_NV12)

3. 编码流程

3.1 打开视频文件

cpp 复制代码
std::ifstream ifs;
ifs.open("800_600_25.h264", std::ios::binary);
if (!ifs.is_open()) {
    std::cout << "ifs open file failed!\n";
    std::cout << "Error code: " << errno << std::endl;
    std::cout << "Error message: " << strerror(errno) << std::endl;
    return -1;
}

3.2 初始化解码器

cpp 复制代码
// 查找解码器
const AVCodec *decodec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!decodec) {
    std::cout << "avcodec_find_decoder failed!" << std::endl;
    return -1;
}

// 创建解码器上下文
AVCodecContext *decodec_ctx = avcodec_alloc_context3(decodec);
if (!decodec_ctx) {
    std::cout << "avcodec_alloc_context3 failed!" << std::endl;
    return -1;
}

// 注意这里暂时还不能打开

3.3 初始化硬件设备 ⭐️

cpp 复制代码
AVHWDeviceType hwtype = AV_HWDEVICE_TYPE_DXVA2;
AVBufferRef *hw_device_ctx = nullptr;

// 创建硬件设备上下文
int ret = av_hwdevice_ctx_create(&hw_device_ctx, hwtype, nullptr, nullptr, 0);
if (ret < 0) {
    std::cout << "av_hwdevice_ctx_create failed!" << std::endl;
    PrintErr(ret);
    return -1;
}

3.4 硬件设备上下文绑定到解码器上下文 ⭐️

cpp 复制代码
decodec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);

3.5 打开解码器上下文

cpp 复制代码
ret = avcodec_open2(decodec_ctx, decodec, 0);
if (0 != ret) {
    std::cout << "avcodec_open2 failed!" << std::endl;
    PrintErr(ret);
    return -1;
}

3.6 初始化音视频码流解析器

cpp 复制代码
AVCodecParserContext *parser_ctx = av_parser_init(AV_CODEC_ID_H264);
if (!parser_ctx) {
    std::cout << "av_parser_init failed!" << std::endl;
    return -1;
}

3.7 初始化SDL部分

注意,因为硬件解码出来再转到cpu中的类型是NV12,所以这里创建纹理时,也是指定了SDL_PIXELFORMAT_NV12去创建,否则sdl渲染会失败; 当然也可以指定YUV420P或RGB24等类型,需要配合sws_scale进行像素格式转换,将NV12类型转换为YUV420P或RGB24等类型即可。

cpp 复制代码
SDL_Init(SDL_INIT_VIDEO);
// 创建窗口
SDL_Window *sdl_win = SDL_CreateWindow("xxxxxxxx", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
                                       800, 600, SDL_WINDOW_OPENGL);
// 创建渲染器
SDL_Renderer *render = SDL_CreateRenderer(sdl_win, -1, SDL_RENDERER_TARGETTEXTURE);
// 创建 纹理|材质
SDL_Texture *texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_NV12, SDL_TEXTUREACCESS_STREAMING, 800, 600);
// 设置线性插值(抗锯齿)
SDL_SetTextureScaleMode(texture, SDL_ScaleModeLinear);

3.8 读取文件数据

cpp 复制代码
unsigned char buf[4096] = { 0 };
int size_in = 0;

while (1) {
    // 读取文件数据
    ifs.read((char *)buf, sizeof(buf));
    size_in = ifs.gcount();
    // 读取失败或读取完毕
    if (0 >= size_in) {
        std::cout << "ifs.read end!" << std::endl;
        break;
    }
	
	// other code ...
}

3.9 解析音视频裸流数据

cpp 复制代码
data_in = buf;
while (0 < size_in) {
    // 解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据
    int re = av_parser_parse2(parser_ctx, decodec_ctx, &pkt->data, &pkt->size,
                         data_in, size_in, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);

    data_in += re;  // 处理了多少,指针移位多少
    size_in -= re;  // 处理了多少,减去多少,小于等于0,则说明读取的字节已经处理完毕


    if (pkt->size) {
		// other code ...
	}
}

3.10 解码

cpp 复制代码
// 发送解码
ret = avcodec_send_packet(decodec_ctx, pkt);
if (0 != ret) {
    std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
    PrintErr(ret);
    goto FAILED;
}

while (0 <= ret) {
    // 接收解码
    ret = avcodec_receive_frame(decodec_ctx, hw_frame);
    if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
        av_packet_unref(pkt);
        break;
    }
    if (0 > ret) {
        std::cout << "avcodec_receive_frame failed! ret = " << ret << std::endl;
        PrintErr(ret);
        av_packet_unref(pkt);
        goto FAILED;
    }

	// other code ...
}

3.11 GPU显存拷贝到CPU内存 ⭐️

cpp 复制代码
AVFrame *display_frame = nullptr;

// 判断是否是硬件解码
if (hw_frame->format == AV_PIX_FMT_DXVA2_VLD ||
    hw_frame->format == AV_PIX_FMT_D3D11 ||
    hw_frame->format == AV_PIX_FMT_VULKAN ||
    hw_frame->format == AV_PIX_FMT_VAAPI ||
    hw_frame->format == AV_PIX_FMT_CUDA ||
    hw_frame->format == AV_PIX_FMT_D3D12) {

    // 显存拷贝到内存 gpu->cpu
    re = av_hwframe_transfer_data(sw_frame, hw_frame, 0);
    if (re < 0) {
        std::cout << "av_hwframe_transfer_data failed!" << std::endl;
        PrintErr(re);
        return -1;
    }

    display_frame = sw_frame;
} else {
    display_frame = hw_frame;
}

3.11 SDL渲染

注意,这里改用了SDL_UpdateNVTexture,因为是NV12类型的。

cpp 复制代码
SDL_UpdateNVTexture(texture, NULL, display_frame->data[0], display_frame->linesize[0],
                    display_frame->data[1], display_frame->linesize[1]);
SDL_RenderClear(render);
SDL_RenderCopy(render, texture, NULL, NULL);
SDL_RenderPresent(render);

当然,也可以将display_frame的内容拷贝到unsigned char *中,然后再进行渲染; 如下代码:

cpp 复制代码
unsigned char *nv12data = (unsigned char *)malloc(4096 * 2160);
memset(nv12data, 0, 4096 * 2160);


// 处理NV12类型数据
if (display_frame->format == AV_PIX_FMT_NV12) {
    // 如果相等,则全部拷贝
    if (display_frame->width == display_frame->linesize[0]) {
        // 拷贝Y
        memcpy(nv12data, display_frame->data[0], display_frame->linesize[0] * display_frame->height);
        // 拷贝UV
        memcpy(nv12data + display_frame->width * display_frame->height,
               display_frame->data[1], display_frame->linesize[1] * display_frame->height/2);
    } else {
        // 逐行复制 Y 和 UV 平面的有效数据,剔除每行末尾的填充字节
        // 拷贝Y
        for (int i = 0; i < display_frame->height; ++i) {
            memcpy(nv12data + i * display_frame->width,
                   display_frame->data[0] + i * display_frame->linesize[0],
                   display_frame->width);
        }

        // 拷贝UV
        auto p = nv12data + display_frame->height * display_frame->width;
        for (int i = 0; i < display_frame->height/2; ++i) {
            memcpy(p + i * display_frame->width,
                   display_frame->data[1] + i * display_frame->linesize[1],
                   display_frame->width);
        }
    }
}

SDL_UpdateTexture(texture, NULL, nv12data, display_frame->width);
SDL_RenderClear(render);
SDL_RenderCopy(render, texture, NULL, NULL);
SDL_RenderPresent(render);

除了这样,还可以进行像素格式转换sws_scale后再渲染显示。

3.12 冲刷解码器

cpp 复制代码
ret = avcodec_send_packet(decodec_ctx, NULL);
if (0 != ret) {
    std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
    PrintErr(ret);
    goto FAILED;
}
while (0 <= ret) {
    ret = avcodec_receive_frame(decodec_ctx, hw_frame);
    if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
        break;
    }
    if (0 > ret) {
        std::cout << "avcodec_receive_packet failed! ret = " << ret << std::endl;
        PrintErr(ret);
        goto FAILED;
    }

    // 判断是否是硬件解码
    if (hw_frame->format == AV_PIX_FMT_DXVA2_VLD ||
        hw_frame->format == AV_PIX_FMT_D3D11 ||
        hw_frame->format == AV_PIX_FMT_VULKAN ||
        hw_frame->format == AV_PIX_FMT_VAAPI ||
        hw_frame->format == AV_PIX_FMT_CUDA ||
        hw_frame->format == AV_PIX_FMT_D3D12) {

        std::cout << "type = " << hw_frame->format << std::endl;

        av_hwframe_transfer_data(sw_frame, hw_frame, 0);

        SDL_UpdateNVTexture(texture, NULL, sw_frame->data[0], sw_frame->linesize[0],
                            sw_frame->data[1], sw_frame->linesize[1]);
        SDL_RenderClear(render);
        SDL_RenderCopy(render, texture, NULL, NULL);
        SDL_RenderPresent(render);
    }

    std::cout << pts++ << "- ";
}

3.12 内存回收

cpp 复制代码
FAILED:
    // 内存回收
    ifs.close();
    av_frame_free(&sw_frame);
    av_frame_free(&hw_frame);
    av_packet_free(&pkt);
    avcodec_free_context(&decodec_ctx);
    av_parser_close(parser_ctx);
    free(nv12data);
    av_buffer_unref(&hw_device_ctx); // 释放硬件设备上下文

    if (texture) {
        SDL_DestroyTexture(texture);
    }
    if (render) {
        SDL_DestroyRenderer(render);
    }
    if (sdl_win) {
        SDL_DestroyWindow(sdl_win);
    }
    SDL_Quit();

4. 代码合集

读取本地h.264视频文件,通过硬件加速解码后,再通过SDL2渲染;

cpp 复制代码
#include <iostream>
#include <fstream>
#include <thread>
#include <chrono>

extern "C" {
#include "libavcodec/avcodec.h"
#include "libavutil/avutil.h"
#include "libswscale/swscale.h"
#include "libavutil/hwcontext.h"
}

#include "SDL.h"



// 打印错误信息
static void PrintErr(int ret) {
    char buf[1024] = { 0 };
    av_strerror(ret, buf, sizeof(buf));
    std::cout << buf << std::endl;
}


#undef main
int main(void)
{
    std::cout << av_version_info() << std::endl;


    // 打开视频文件
    std::ifstream ifs;
    ifs.open("800_600_25.h264", std::ios::binary);
    if (!ifs.is_open()) {
        std::cout << "ifs open file failed!\n";
        std::cout << "Error code: " << errno << std::endl;
        std::cout << "Error message: " << strerror(errno) << std::endl;
        return -1;
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////
    /// 初始化解码器

    // 查找解码器
    const AVCodec *decodec = avcodec_find_decoder(AV_CODEC_ID_H264);
    if (!decodec) {
        std::cout << "avcodec_find_decoder failed!" << std::endl;
        return -1;
    }

    // 创建解码器上下文
    AVCodecContext *decodec_ctx = avcodec_alloc_context3(decodec);
    if (!decodec_ctx) {
        std::cout << "avcodec_alloc_context3 failed!" << std::endl;
        return -1;
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////




    /////////////////////////////////////////////////////////////////////////////////////////////////
    /// 初始化硬件设备

    // 打印所有硬件
    for (int i = 0; ; ++i) {
        const AVCodecHWConfig *config = avcodec_get_hw_config(decodec, i);
        if (!config) {
            break;
        }

        if (config->device_type) {
            std::cout << av_hwdevice_get_type_name(config->device_type) << std::endl;
        }
    }

    /*
     *  dxva2
        d3d11va
        d3d12va
        cuda
        vaapi
        vulkan
     */
    AVHWDeviceType hwtype = AV_HWDEVICE_TYPE_DXVA2;
//    AVHWDeviceType hwtype = AV_HWDEVICE_TYPE_D3D12VA;
    AVBufferRef *hw_device_ctx = nullptr;

    // 创建硬件设备上下文
    int ret = av_hwdevice_ctx_create(&hw_device_ctx, hwtype, nullptr, nullptr, 0);
    if (ret < 0) {
        std::cout << "av_hwdevice_ctx_create failed!" << std::endl;
        PrintErr(ret);
        return -1;
    }

    // 将硬件设备上下文绑定到解码器上下文
    decodec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
    /////////////////////////////////////////////////////////////////////////////////////////////////


    // 打开解码器上下文
    ret = avcodec_open2(decodec_ctx, decodec, 0);
    if (0 != ret) {
        std::cout << "avcodec_open2 failed!" << std::endl;
        PrintErr(ret);
        return -1;
    }


    /////////////////////////////////////////////////////////////////////////////////////////////////
    /// 初始化音视频码流解析器

    AVCodecParserContext *parser_ctx = av_parser_init(AV_CODEC_ID_H264);
    if (!parser_ctx) {
        std::cout << "av_parser_init failed!" << std::endl;
        return -1;
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////



    // 初始化SDL部分
    SDL_Init(SDL_INIT_VIDEO);
    // 创建窗口
    SDL_Window *sdl_win = SDL_CreateWindow("xxxxxxxx", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
                                           800, 600, SDL_WINDOW_OPENGL);
    // 创建渲染器
    SDL_Renderer *render = SDL_CreateRenderer(sdl_win, -1, SDL_RENDERER_TARGETTEXTURE);
    // 创建 纹理|材质
    SDL_Texture *texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_NV12, SDL_TEXTUREACCESS_STREAMING, 800, 600);
    // 设置线性插值(抗锯齿)
    SDL_SetTextureScaleMode(texture, SDL_ScaleModeLinear);


    int pts = 0;
    unsigned char buf[4096] = { 0 };
    unsigned char *data_in = nullptr;
    int size_in = 0;
    AVPacket *pkt = av_packet_alloc();
    AVFrame *hw_frame = av_frame_alloc();
    AVFrame *sw_frame = av_frame_alloc();
    unsigned char *nv12data = (unsigned char *)malloc(4096 * 2160);
    memset(nv12data, 0, 4096 * 2160);

    while (1) {
        // 读取文件数据
        ifs.read((char *)buf, sizeof(buf));
        size_in = ifs.gcount();
        // 读取失败或读取完毕
        if (0 >= size_in) {
            std::cout << "ifs.read end!" << std::endl;
            break;
        }

        data_in = buf;
        while (0 < size_in) {
            // 解析音视频裸流数据,从中分离出一帧一帧的压缩编码数据
            int re = av_parser_parse2(parser_ctx, decodec_ctx, &pkt->data, &pkt->size,
                                 data_in, size_in, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);

            data_in += re;  // 处理了多少,指针移位多少
            size_in -= re;  // 处理了多少,减去多少,小于等于0,则说明读取的字节已经处理完毕


            if (pkt->size) {
                // 发送解码
                ret = avcodec_send_packet(decodec_ctx, pkt);
                if (0 != ret) {
                    std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
                    PrintErr(ret);
                    goto FAILED;
                }

                while (0 <= ret) {
                    // 接收解码
                    ret = avcodec_receive_frame(decodec_ctx, hw_frame);
                    if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
                        av_packet_unref(pkt);
                        break;
                    }
                    if (0 > ret) {
                        std::cout << "avcodec_receive_frame failed! ret = " << ret << std::endl;
                        PrintErr(ret);
                        av_packet_unref(pkt);
                        goto FAILED;
                    }
                    av_packet_unref(pkt);



                    AVFrame *display_frame = nullptr;

                    // 判断是否是硬件解码
                    if (hw_frame->format == AV_PIX_FMT_DXVA2_VLD ||
                        hw_frame->format == AV_PIX_FMT_D3D11 ||
                        hw_frame->format == AV_PIX_FMT_VULKAN ||
                        hw_frame->format == AV_PIX_FMT_VAAPI ||
                        hw_frame->format == AV_PIX_FMT_CUDA ||
                        hw_frame->format == AV_PIX_FMT_D3D12) {

                        // 显存拷贝到内存 gpu->cpu
                        re = av_hwframe_transfer_data(sw_frame, hw_frame, 0);
                        if (re < 0) {
                            std::cout << "av_hwframe_transfer_data failed!" << std::endl;
                            PrintErr(re);
                            return -1;
                        }

                        display_frame = sw_frame;
                    } else {
                        display_frame = hw_frame;
                    }


                    std::cout << pts++ << " ";

                    // 渲染NV12类型
//                    if (display_frame->format == AV_PIX_FMT_NV12) {
//                        // 如果相等,则全部拷贝
//                        if (display_frame->width == display_frame->linesize[0]) {
//                            // 拷贝Y
//                            memcpy(nv12data, display_frame->data[0], display_frame->linesize[0] * display_frame->height);
//                            // 拷贝UV
//                            memcpy(nv12data + display_frame->width * display_frame->height,
//                                   display_frame->data[1], display_frame->linesize[1] * display_frame->height/2);
//                        } else {
//                            // 逐行复制 Y 和 UV 平面的有效数据,剔除每行末尾的填充字节
//                            // 拷贝Y
//                            for (int i = 0; i < display_frame->height; ++i) {
//                                memcpy(nv12data + i * display_frame->width,
//                                       display_frame->data[0] + i * display_frame->linesize[0],
//                                       display_frame->width);
//                            }

//                            // 拷贝UV
//                            auto p = nv12data + display_frame->height * display_frame->width;
//                            for (int i = 0; i < display_frame->height/2; ++i) {
//                                memcpy(p + i * display_frame->width,
//                                       display_frame->data[1] + i * display_frame->linesize[1],
//                                       display_frame->width);
//                            }
//                        }
//                    }

//                    SDL_UpdateTexture(texture, NULL, nv12data, display_frame->width);


                    SDL_UpdateNVTexture(texture, NULL, display_frame->data[0], display_frame->linesize[0],
                                        display_frame->data[1], display_frame->linesize[1]);
                    SDL_RenderClear(render);
                    SDL_RenderCopy(render, texture, NULL, NULL);
                    SDL_RenderPresent(render);


                    // 睡眠 40 毫秒
//                    std::this_thread::sleep_for(std::chrono::milliseconds(40));
                }
            }
        }
    }


    // 冲刷解码器
    ret = avcodec_send_packet(decodec_ctx, NULL);
    if (0 != ret) {
        std::cout << "avcodec_send_packet failed! ret = " << ret << std::endl;
        PrintErr(ret);
        goto FAILED;
    }
    while (0 <= ret) {
        ret = avcodec_receive_frame(decodec_ctx, hw_frame);
        if (AVERROR(EAGAIN) == ret || AVERROR_EOF == ret) {
            break;
        }
        if (0 > ret) {
            std::cout << "avcodec_receive_packet failed! ret = " << ret << std::endl;
            PrintErr(ret);
            goto FAILED;
        }

        // 判断是否是硬件解码
        if (hw_frame->format == AV_PIX_FMT_DXVA2_VLD ||
            hw_frame->format == AV_PIX_FMT_D3D11 ||
            hw_frame->format == AV_PIX_FMT_VULKAN ||
            hw_frame->format == AV_PIX_FMT_VAAPI ||
            hw_frame->format == AV_PIX_FMT_CUDA ||
            hw_frame->format == AV_PIX_FMT_D3D12) {

            std::cout << "type = " << hw_frame->format << std::endl;

            av_hwframe_transfer_data(sw_frame, hw_frame, 0);

            SDL_UpdateNVTexture(texture, NULL, sw_frame->data[0], sw_frame->linesize[0],
                                sw_frame->data[1], sw_frame->linesize[1]);
            SDL_RenderClear(render);
            SDL_RenderCopy(render, texture, NULL, NULL);
            SDL_RenderPresent(render);
        }

        std::cout << pts++ << "- ";
    }


FAILED:
    // 内存回收
    ifs.close();
    av_frame_free(&sw_frame);
    av_frame_free(&hw_frame);
    av_packet_free(&pkt);
    avcodec_free_context(&decodec_ctx);
    av_parser_close(parser_ctx);
    free(nv12data);
    av_buffer_unref(&hw_device_ctx); // 释放硬件设备上下文

    if (texture) {
        SDL_DestroyTexture(texture);
    }
    if (render) {
        SDL_DestroyRenderer(render);
    }
    if (sdl_win) {
        SDL_DestroyWindow(sdl_win);
    }
    SDL_Quit();

    return 0;
}

5. 视频效果截图

最后自己可以在尝试一下别的硬件解码类型,看看会有什么不同效果。

bash 复制代码
dxva2
d3d11va
d3d12va
cuda
vaapi
vulkan
相关推荐
星花月7 小时前
视频压缩为什么会变糊?从码率、分辨率到 H.264/H.265 的参数选择指南
ffmpeg·音视频·h.265·视频编解码·视频
cpp_learner9 小时前
FFmpeg 像素格式转换与图像缩放实战
ffmpeg
程序员老陆11 小时前
FFmpeg 新编码 API 完全指南:avcodec_send_frame / avcodec_receive_packet
ffmpeg·音视频·编码
cpp_learners1 天前
FFmpeg H264视频编码全流程解析
ffmpeg·音视频
cpp_learners1 天前
FFmpeg H264视频解码全流程解析
ffmpeg·音视频·解码
程序员老陆1 天前
FFmpeg 像素格式(pix_fmt)通关指南:从 YUV420P 到 NV12、RGB24 到底怎么选
ffmpeg·音视频·yuv·像素格式
DogDaoDao1 天前
FFmpeg 9.0 “Lei“ 正式发布:十年之后,以你之名
ffmpeg·音视频·实时音视频·视频编解码·ffprobe·ffmpeg 9.0
程序员老陆2 天前
FFmpeg 音频采样格式(sample_fmt)完全手册:FLTP、S16、S32 到底在吵什么
ffmpeg·音视频·音频采样格式
cpp_learner2 天前
FFmpeg H264视频编码全流程解析
ffmpeg