FFmpeg学习(四):硬件加速编解码

941 阅读18分钟

0. 前言

多媒体文件的便解码任务向来是个计算密集型任务,所以使用GPU或其他专用硬件来实现便解码功能是自然而然可以想到的一种加速方法。目前,不同的硬件设备厂商都提供了不同的硬件加速方案,但还没有统一的工业标准。FFmpeg尝试在框架层面来统一这些硬件加速方式,提供统一的接口来完成这些操作。今天我们就来讨论使用FFmpeg来进行硬件加速的方式。

使用硬件加速的方式也是在libavcodec的编解码处理框架中进行扩展,主体流程可以参考之前的文章FFmpeg学习(三):编码和解码

1. 相关结构介绍

使用FFmpeg来进行硬件加速编解码的架构图如下。
AVCodecContext之前已经介绍过,是用于编码和解码时的上下文环境,而在使用硬件加速时,我们主要需要控制的是其成员hw_device_ctxhw_frames_ctx,分别对应的AVHWDeviceContextAVHWFramesContext结构。其在不同平台上底层代码会应用不同的硬件加速接口,如在IOS平台上的VideoToolBox,Android平台上的MediaCodec,或者在PC上使用Nvidia的CUDA接口。FFmpeg使用了这两个结构来隐藏不同平台下的加速接口差异。

FFmpeg硬件加速.png

1.1 AVHWDeviceContext

AVHWDeviceContext描述的是被用于编解码的硬件上下文环境,根据所支持的加速API的不同,其底层数据结构指向不同的硬件加速API句柄,如在Android上使用MediaCodec进行加速,其私有结构中会包含由NDK提供的MeidaCodec C++接口对象AMeidaCodec
部分解码器可能也需要在创建hw_frames_ctx之前设置hw_device_ctx字段,这种情况下,所有硬件帧的上下文环境也应该由同一个Device上下文创建。换言之,一旦AVCodecContext需要两个字段都设置时,他们都需要是一个上下文
libavutil提供了一些方法来控制底层的硬件API,以下是libavutil/hwcontext.h中开放的AVHWDeviceContext结构。

AVHWDeviceContext.png

1.2 AVHWFramesContext

AVHWFramesContext描述的是在硬件加速编解码过程中使用到的一组硬件帧或一个硬件帧池。由于操作到硬件帧必然涉及到对应的硬件设备,所以AVHWFramesContext需要与一个AVHWDeviceContext相关联。

相比于AVHWDeviceContext, 它更提供了访问硬件设备内具体的一系列buffer的能力。(能够访问硬件Buffer,那么一定也需要对应的硬件设备上下文,所以它更像是AVHWDeviceContext的扩展结构)。 AVHWFramesContext的参数设置(如硬件帧支持的format,对应的软件帧format,最小最大宽高)这些数据可以通过av_hwdevice_get_hwframe_constraints()返回的AVHWFramesConstraints获取。

AVHWFramesContext实际上是FFmpeg提供了一种方式,使得我们不用手动去维护硬件帧的生命周期,帧的分配和回收均交由libavcodec去管控。如果我们有用原生硬件加速API处理帧的需求,需要手动通过CUDA/MediaCodec这类接口去使用硬件内存(且用ffmpeg硬件编解码器接口进行编解码),那么我们可以不设置AVHWFramesContext而进行手动控制,直接使用硬件内存指针来包装AVFrame来作为编解码器的输入,使用完后手动释放。当然,这也需要我们自行保证帧的内存安全。

在解码时,可以为AVCodecContext设置get_format()回调方法,该回调方法会用于解码时创建AVFramesContext,设置帧的具体格式。

以下是libavutil/hwcontext.h中开放的AVHWFramesContext结构体。

image.png

可以看到,可以看到,其成员中多了能够索引多张硬件帧的bufferpool, 并且支持一些可以获取到硬件buffer的接口,特别是能够在硬件设备buffer和内存之间转移数据。

1.3 AVFrame

这里要提及的是“硬件帧”,帧的概念在硬件加速的场景下中也是用的AVFrame来表示的,但是不同于之前软编解码时存放的是数据帧内容,在硬件加速的场景下,AVFrame中存放的往往是“硬件Buffer的索引”,而不是具体的图像数据。FFmpeg框架会在内部处理过程中,或者是我们调用av_hwframe_transfer_data()在硬件Buffer与内存中转移数据时,根据AVFrame中保存的索引访问到硬件设备中具体的图像数据。
另外,硬件帧在设备Buffer中的也是按照一定的格式来存放和处理的,如格式为AV_PIX_FMT_VIDEOTOOLBOX(其底层的数据结构是MAC/IOS平台上的CVPixelBufferRef)的硬件帧在设备Buffer中的实际布局也是NV12格式。具体的硬件帧数据格式可以通过AVHWFramesContextsw_format字段判断。

如图是笔者的mac上的一帧硬件加速帧,可以看见其对应的format是158(即AV_PIX_FMT_VIDEOTOOLBOX),data[3]中保存的是对应硬件Buffer的索引。 截屏2025-03-05 21.28.17.png

2. 常用API

2.1 获取Codec支持的硬件加速信息

我们设置硬件加速环境的位置在打开编解码器之前avcodec_open2(),在我们知道了需要使用的具体的编解码器之后,可以查询本机所支持的硬件加速方式,接口API原型如下:

const AVCodecHWConfig *avcodec_get_hw_config(const AVCodec *codec, int index);

在使用时需要手动传递获取的下标,当超出列表长度时会返回NULL。

// 获取Codec支持的硬件加速配置情况,这里主要是打印看看
int hwindex = 0;
std::vector<const AVCodecHWConfig*> hwconfigs;
const AVCodecHWConfig* hwconfig = nullptr;
while((hwconfig = avcodec_get_hw_config(codec, hwindex)) != nullptr) {
    av_log(NULL, AV_LOG_INFO, "HW %d : %d, %d, %d \n", hwindex, hwconfig->pix_fmt, hwconfig->methods, hwconfig->device_type);
    hwconfigs.push_back(hwconfig);
    ++hwindex;
}

2.2 创建AVHWDeviceContext

在获取到本机上对于目标Codec的硬件加速支持情况后,就可以创建AVHWDeviceContext,将其设置给AVCodecContext。可以使用下面的方法:

// 下面两种均可以:
// 1. -----------------------------------
// 根据查到的hwdevicetype,创建AVHWDeviceContext
AVBufferRef *av_hwdevice_ctx_alloc(enum AVHWDeviceType type);
// 初始化
int av_hwdevice_ctx_init(AVBufferRef *ref);

// 2. -----------------------------------
// 相当于alloc+init方法的便捷化版本,
int av_hwdevice_ctx_create(AVBufferRef **device_ctx, enum AVHWDeviceType type,
                           const char *device, AVDictionary *opts, int flags);

2.3 创建AVHWFramesContext

当我们有需要操作硬件帧的需求时,应该是创建和使用AVHWFramesContext,可以用如下方式:

// 1.----------------------------------------------
// 创建一个AVHWFramesContext,需要传入一个已经初始化的的AVHWDeviceContext
AVBufferRef *av_hwframe_ctx_alloc(AVBufferRef *device_ctx);
// 初始化
int av_hwframe_ctx_init(AVBufferRef *ref);

如果我们是为了给AVCodecContext设置AVHWFramesContext,那么也可以:
(1) 为其设置hw_device_ctx
(2) 设置get_format()回调
这样在之后编解码过程中,AVCodecContext会自动创建AVHWFramesContext并复制给hw_frames_ctx字段。

// 1.先前获取到的AVCodecHWConfig
hwconfig = hwconfigs[0];
// 创建hwdevicectx
if(av_hwdevice_ctx_create(&codecCtx->hw_device_ctx, hwconfig->device_type, NULL, NULL, 0) < 0) {
    av_log(NULL, AV_LOG_ERROR, "Create hw device error.\n");
    avcodec_free_context(&codecCtx);
    avformat_close_input(&inFmtCtx);

    return;
}
// 2.hwconfigs信息放入私有数据,方便后面回调使用。
codecCtx->opaque = &hwconfigs;

// 3.设置get_format回调,在处理过程中codecCtx回触发,根据其返回的格式
// 确认是否启用硬解,完成后codecCtx会创建hw_frames_ctx
codecCtx->get_format = [](AVCodecContext *s, const AVPixelFormat *fmt) -> enum AVPixelFormat {
    if(s->opaque) {
        auto pHwConfigs = static_cast<std::vector<const AVCodecHWConfig*> *>(s->opaque);
        if(!pHwConfigs->empty()) {
            auto hwconfig = pHwConfigs->at(0);
            av_log(NULL, AV_LOG_INFO, "get format : hwconfig pix_fmt : %d\n", hwconfig->pix_fmt);
            return hwconfig->pix_fmt;
        }
    }
    av_log(NULL, AV_LOG_ERROR, "Error state, not found hw config, set YUV420P\n");
            
    return s->sw_pix_fmt ? s->sw_pix_fmt : AV_PIX_FMT_YUV420P;
};

2.4 在硬件Buffer和CPU内存之间传输数据

内存中的音视频帧数据,不能直接被硬件设备所访问。而硬件设备中的音视频数据,也没办法直接被CPU所获取。要打通这条双向链路,必须要使用到AVHWFramesContext,有下面的接口:

// 主要是获得硬件帧av_hwframe_transfer_data()时buffer中所支持的format 
//(是指帧的内存布局,具体的如NV12,YUV420P这种)  
// 两个方向:  
// 1. AV_HWFRAME_TRANSFER_DIRECTION_FROM  
//     硬件->CPU,转移到CPU时,帧所支持的format  
// 2. AV_HWFRAME_TRANSFER_DIRECTION_TO  
//     CPU->硬件,转移到硬件时,帧所支持的format
int av_hwframe_transfer_get_formats(AVBufferRef *hwframe_ctx,
                                    enum AVHWFrameTransferDirection dir,
                                    enum AVPixelFormat **formats, int flags);
                                    
// 在CPU和硬件设备之间转移数据,主要有2个用法:  
// 1. 将硬件帧的数据拷贝到CPU内存的AVFrame中  
// 2. 将CPU内存的AVFrame数据拷贝至一张的硬件帧  
// src,dst至少要有一个是有AVHWFramesContext关联的
int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags);

3. 主要流程

3.1 硬件加速解码流程

使用硬件加速解码的流程图如下,相较于一般的FFmpeg解码流程,主要是需要创建硬件加速的设备,以及可选的从硬件设备中转移帧数据的步骤,可以参考图中使用背景色标记的部分流程。

硬件加速解码.png

3.2 硬件加速编码流程

类似的,硬件加速编码时,硬件帧要么是来源于硬件加速解码的出帧,要么是由内存中存入编码设备的帧,所以用户一定涉及到硬件帧的生命周期,所以这里使用AVHWFramesContext

硬件加速编码.png

4. 示例

硬件加速的代码首先是需要系统或设备支持对应的硬件加速接口,以下代码主要是笔者在自己的mac上使用videotoolbox的试验代码,所以不一定不同的机型都可以跑通,另外也需要测试已经安装的ffmpeg支持对应的硬件加速功能。

可以用以下方式来测试使用videotoolbox的h264加速功能。

ffmpeg -i input.mp4 -c:v h264_videotoolbox output.mp4

4.1 解码

使用硬件解码的任务可以参考github.com/zzakafool/M… 文件夹8_HWDecode下的代码,其中使用videotoolbox对测试视频进行了解码,并将视频第一帧保存成nv12格式。

#include <hwdecode.h>
#include <vector>
#include <sstream>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/hwcontext.h>
}

// 可以使用下面的命令进行显示
// ffplay -pixel_format nv12 -f rawvideo -video_size 640x360 firstPic.nv12
void saveNV12(AVFrame *frame, std::string outfile) {
    // 打开输出文件
    FILE* file = fopen(outfile.c_str(), "wb");
    if (!file) {
        av_log(NULL, AV_LOG_ERROR, "cannot open file");
        return;
    }
    // 写入Y分量
    for (int i = 0; i < frame->height; ++i) {
        fwrite(frame->data[0] + i * frame->linesize[0], 1, frame->width, file);
    }
    // 写入UV分量
    for (int i = 0; i < frame->height / 2; ++i) {
        fwrite(frame->data[1] + i * frame->linesize[1], 1, frame->width, file);
    }
    // 关闭输出文件
    fclose(file);
}

void transferFrameHwToCPU(AVCodecContext *codecCtx, AVFrame *cpuFrame, AVFrame *hwFrame) {
    // 这里是查找后续调用av_hwframe_transfer_data(), 可能返回的软件帧格式
    // 比如对于MAC的videotoolbox,这里可以返回的是nv12
    AVPixelFormat *formats = nullptr;
    if(av_hwframe_transfer_get_formats(codecCtx->hw_frames_ctx, AV_HWFRAME_TRANSFER_DIRECTION_FROM, &formats, 0) < 0) {
        av_log(NULL, AV_LOG_ERROR, "hw frame transfer get fromats failed\n");
    }

    // 打印一下所有的拷贝支持的格式
    cpuFrame->format = AV_PIX_FMT_NONE;
    std::stringstream sstrm;
    sstrm << "\ntransfer support format : \n";
    AVPixelFormat *p = formats;
    while(p != nullptr && *p != AV_PIX_FMT_NONE) {
        sstrm << "\t Fmt " << *p << std::endl;
        ++p;
    }
    av_log(NULL, AV_LOG_INFO, "%s", sstrm.str().c_str());

    // 这里也选择第一种
    if(formats) {
        cpuFrame->format = formats[0];
    }

    // 拷贝到cpu
    if(av_hwframe_transfer_data(cpuFrame, hwFrame, 0) < 0) {
        av_log(NULL, AV_LOG_ERROR, "transfer to cpu failed\n");
    }

    av_log(NULL, AV_LOG_INFO, "transfer to cpu success.\n");
}

void hwdecode(std::string url) {
    AVFormatContext *inFmtCtx = nullptr;
    if(avformat_open_input(&inFmtCtx, url.c_str(), NULL, NULL) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Open input format failed\n");
        return;
    }

    if(avformat_find_stream_info(inFmtCtx, NULL) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Find stream info failed\n");
        avformat_close_input(&inFmtCtx);
        return;
    }

    int vstreamid = av_find_best_stream(inFmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if(vstreamid < 0) {
        avformat_close_input(&inFmtCtx);
        av_log(NULL, AV_LOG_ERROR, "Find video stream error\n");
        return;
    }

    const AVCodec* codec = avcodec_find_decoder(inFmtCtx->streams[vstreamid]->codecpar->codec_id);
    if(codec == nullptr) {
        avformat_close_input(&inFmtCtx);
        av_log(NULL, AV_LOG_ERROR, "Find Codec error\n");
        return;
    }

    AVCodecContext *codecCtx = avcodec_alloc_context3(codec);
    if(codecCtx == nullptr) {
        av_log(NULL, AV_LOG_ERROR, "Cannot alloc Codec context\n");
        avformat_close_input(&inFmtCtx);
        return;
    }

    if(avcodec_parameters_to_context(codecCtx, inFmtCtx->streams[vstreamid]->codecpar) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Fill Codec context failed\n");
        avcodec_free_context(&codecCtx);
        avformat_close_input(&inFmtCtx);
    }
    
    // 获取Codec支持的硬件加速配置情况,这里主要是打印看看
    int hwindex = 0;
    std::vector<const AVCodecHWConfig*> hwconfigs;
    const AVCodecHWConfig* hwconfig = nullptr;
    while((hwconfig = avcodec_get_hw_config(codec, hwindex)) != nullptr) {
        av_log(NULL, AV_LOG_INFO, "HW %d : %d, %d, %d \n", hwindex, hwconfig->pix_fmt, hwconfig->methods, hwconfig->device_type);
        hwconfigs.push_back(hwconfig);
        ++hwindex;
    }

    // 按照第一种硬件加速配置,设置解码器
    if(!hwconfigs.empty()) {
        hwconfig = hwconfigs[0];
        // 创建hwdevicectx
        if(av_hwdevice_ctx_create(&codecCtx->hw_device_ctx, hwconfig->device_type, NULL, NULL, 0) < 0) {
            av_log(NULL, AV_LOG_ERROR, "Create hw device error.\n");
            avcodec_free_context(&codecCtx);
            avformat_close_input(&inFmtCtx);

            return;
        }
        // hwconfigs信息放入私有数据,方便后面回调使用。
        codecCtx->opaque = &hwconfigs;

        // 设置get_format回调,在处理过程中codecCtx回触发,根据其返回的格式
        // 确认是否启用硬解,完成后codecCtx会创建hw_frames_ctx
        // 如果设置了codecCtx->get_format则会忽视hw_device_ctx字段
        codecCtx->get_format = [](AVCodecContext *s, const AVPixelFormat *fmt) -> enum AVPixelFormat {
            if(s->opaque) {
                auto pHwConfigs = static_cast<std::vector<const AVCodecHWConfig*> *>(s->opaque);
                if(!pHwConfigs->empty()) {
                    auto hwconfig = pHwConfigs->at(0);
                    av_log(NULL, AV_LOG_INFO, "get format : hwconfig pix_fmt : %d\n", hwconfig->pix_fmt);
                    return hwconfig->pix_fmt;
                }
            }
            av_log(NULL, AV_LOG_ERROR, "Error state, not found hw config, set YUV420P\n");
            
            return s->sw_pix_fmt ? s->sw_pix_fmt : AV_PIX_FMT_YUV420P;
        };
    }

    // 设置完硬解环境后,open
    if(avcodec_open2(codecCtx, codec, NULL) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Cannot open codec.\n");

        if(codecCtx->hw_device_ctx) {
            av_buffer_unref(&codecCtx->hw_device_ctx);
        }
        avcodec_free_context(&codecCtx);
        avformat_close_input(&inFmtCtx);
    }

    av_log(NULL, AV_LOG_INFO, "Create hw decoder success");
    
    AVPacket *inPacket = av_packet_alloc();
    AVFrame *inFrame = av_frame_alloc();

    int decodedFrameNum = 0;
    AVFrame *cpuFrame = av_frame_alloc();

    av_log(NULL, AV_LOG_INFO, "Start decoding...\n");
    while(av_read_frame(inFmtCtx, inPacket) >= 0) {
        if(inPacket->stream_index != vstreamid) {
            continue;
        }

        int err = avcodec_send_packet(codecCtx, inPacket);
        if(err < 0) {
            // 因为尽量消耗解码输出,所以应该不会有EAGAIN,所以这种情况应该无法恢复
            av_log(NULL, AV_LOG_ERROR, "Error when decode send packet.\n");
            return;
        }

        while(err >= 0) {
            err = avcodec_receive_frame(codecCtx, inFrame);
            if(err == AVERROR(EAGAIN) || err == AVERROR_EOF) {
                break;
            } else if(err < 0) {
                av_log(NULL, AV_LOG_ERROR, "Error when decode receive frame \n");
                // 无法恢复的错误
                return;
            }

            ++decodedFrameNum;
            // 解码出一张Frame
            av_log(NULL, AV_LOG_INFO, "\rdecode %d frame, format : %d", decodedFrameNum, inFrame->format);
            
            // 下面的inFrame还是得到返回的硬件帧,可以看成数据是硬件中某个buffer的索引。
            // inFrame不能直接做数据处理相关的操作,如果需要,需要先拷贝到CPU
            // 只拷贝第一张回CPU做验证
            if(decodedFrameNum == 1) {
                // 从硬件帧拷贝一张到CPU
                transferFrameHwToCPU(codecCtx, cpuFrame, inFrame);
                // 保存一张NV12图片到本地 
                saveNV12(cpuFrame, "firstPic.nv12");
            }
        }
    }

    int err = avcodec_send_packet(codecCtx, NULL);
    if(err < 0) {
        // 因为尽量消耗解码输出,所以应该不会有EAGAIN,所以这种情况应该无法恢复
        av_log(NULL, AV_LOG_ERROR, "Error when decode send packet.\n");
        return;
    }

    while(err >= 0) {
        err = avcodec_receive_frame(codecCtx, inFrame);
        if(err == AVERROR(EAGAIN) || err == AVERROR_EOF) {
            break;
        } else if(err < 0) {
            av_log(NULL, AV_LOG_ERROR, "Error when decode receive frame \n");
            // 无法恢复的错误
            return;
        }

        // 解码出一张Frame
        av_log(NULL, AV_LOG_INFO, "\rdecode %d frame, format : %d", decodedFrameNum, inFrame->format);
    }

    if(codecCtx->hw_device_ctx) {
        av_buffer_unref(&codecCtx->hw_device_ctx);
    }
    avcodec_free_context(&codecCtx);
    avformat_close_input(&inFmtCtx);
    return;
}

4.2 编码

硬件编码可以参考github.com/zzakafool/M… 文件夹9_HWEncode下的代码,其中将4.1中保存下来的一张nv12图片,从文件中读入内存,并使用硬件编码成一段10s的视频。

#include "hwencode.h"

extern "C" {
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
    #include <libavutil/opt.h>
    #include <libavutil/hwcontext.h>
}

const int FRAME_WIDTH = 640;
const int FRAME_HEIGHT = 360;

const int TEST_FRAME_SIZE = 30 * 10;

// 用读到的nv12数据,初始化软件帧
void initSWFrame(AVFrame *swFrame, uint8_t *imgDataBuf, int bufSize) {

    swFrame->width = FRAME_WIDTH;
    swFrame->height= FRAME_HEIGHT;
    swFrame->format= AV_PIX_FMT_NV12;

    auto ret = av_frame_get_buffer(swFrame, 0);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "allocate frame buffer failed\n");
        return;
    }

    ret = av_frame_make_writable(swFrame);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "av frame make writable failed\n");
        return;
    }

    int offset = 0;
    // 写入Y分量
    for (int i = 0; i < swFrame->height; ++i) {
        memcpy(swFrame->data[0] + i * swFrame->linesize[0], imgDataBuf + offset, swFrame->width);
        offset += swFrame->width;
    }
    // 写入UV分量
    for (int i = 0; i < swFrame->height / 2; ++i) {
        memcpy(swFrame->data[1] + i * swFrame->linesize[1], imgDataBuf + offset, swFrame->width);
        offset += swFrame->width;
    }

    // 应该正好符合大小
    assert(offset == bufSize);
}

// 读取输入的一张nv12图做测试数据
void readNv12ToBuf(uint8_t *&buf, int &length, std::string srcNv12) {
    FILE *file = fopen(srcNv12.c_str(), "rb"); // 以二进制模式打开文件
    if (file == NULL) {
        av_log(NULL, AV_LOG_ERROR, "Failed to open file");
    }

    fseek(file, 0, SEEK_END);
    length = ftell(file);
    fseek(file, 0, SEEK_SET);

    buf = (uint8_t *)malloc(length);
    if (buf == NULL) {
        av_log(NULL, AV_LOG_ERROR, "Failed to allocate memory");
        fclose(file);
        return;
    }

    size_t bytes_read = fread(buf, 1, length, file);
    if (bytes_read != length) {
        perror("Failed to read file");
        free(buf);
        fclose(file);
        return;
    }

    fclose(file);
}

void hwencode(std::string dst, std::string srcNv12) {
    AVFormatContext* fmtCtx = nullptr;
    int ret = avformat_alloc_output_context2(&fmtCtx, NULL, NULL, dst.c_str());
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "allocate output format context failed\n");
        return;
    }

    ret = avio_open(&fmtCtx->pb, dst.c_str(), AVIO_FLAG_WRITE);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "avio open failed\n");
        return;
    }

    AVStream* strm = avformat_new_stream(fmtCtx, nullptr);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "add stream failed\n");
        return;
    }
    strm->time_base = AVRational{1, 30};
    strm->avg_frame_rate = AVRational{30, 0};

    // 需要显式指定h264_videotoolbox, hevc_videotoolbox(h265)
    // 如果用avcodec_find_encoder(AV_CODEC_ID_H264)的方式默认返回软件实现,如x264
    auto codec = avcodec_find_encoder_by_name("h264_videotoolbox");
    if(codec == nullptr) {
        av_log(NULL, AV_LOG_ERROR, "Cannot find codec h264\n");
        return;
    }

    AVCodecContext* codecCtx = avcodec_alloc_context3(codec);
    if(codecCtx == nullptr) {
        av_log(NULL, AV_LOG_ERROR, "Cannot allocate context\n");
        return;
    }

    codecCtx->bit_rate = 100000;
    codecCtx->width = FRAME_WIDTH;
    codecCtx->height = FRAME_HEIGHT;
    codecCtx->time_base = AVRational{1, 30};
    codecCtx->framerate = AVRational{30, 0};
    codecCtx->gop_size = 12;
    // codecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    av_opt_set(codecCtx->priv_data, "preset", "slow", 0);
    av_opt_set(codecCtx->priv_data, "profile", "main", 0);
    av_opt_set(codecCtx->priv_data, "level", "5.0", 0);

    // 获取Codec支持的硬件加速配置情况,这里主要是打印看看
    // int hwindex = 0;
    // std::vector<const AVCodecHWConfig*> hwconfigs;
    // const AVCodecHWConfig* hwconfig = nullptr;

    // while((hwconfig = avcodec_get_hw_config(codec, hwindex)) != nullptr) {
    //     av_log(NULL, AV_LOG_INFO, "HW %d : %d, %d, %d \n", hwindex, hwconfig->pix_fmt, hwconfig->methods, hwconfig->device_type);
    //     hwconfigs.push_back(hwconfig);
    //     ++hwindex;
    // }

    // **这里实测AVCodecHWConfig找不到VideoToolBox的编码器配置,所以上面注释掉**
    // 但还是可以手动设定和使用

    // 创建hwdevicectx
    if(av_hwdevice_ctx_create(&codecCtx->hw_device_ctx, AV_HWDEVICE_TYPE_VIDEOTOOLBOX, NULL, NULL, 0) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Create hw device error.\n");

        return;
    }

    // 获取AVHWFramesContext的限制要求
    auto hwFramesConstraints = av_hwdevice_get_hwframe_constraints(codecCtx->hw_device_ctx, NULL);
    if(hwFramesConstraints == nullptr) {
        av_log(NULL, AV_LOG_ERROR, "Cannot get hwframes constraints.\n");
        return;
    }

    // 创建成功,设置对应的AVHWFramesContext
    if(codecCtx->hw_device_ctx) {
        codecCtx->hw_frames_ctx = av_hwframe_ctx_alloc(codecCtx->hw_device_ctx);
        if(codecCtx->hw_frames_ctx == nullptr) {
            av_log(NULL, AV_LOG_ERROR, "Allocate hw frames ctx error.\n");

            return;
        }

        // 设置inital_pool_size
        auto hwFramesCtx = reinterpret_cast<AVHWFramesContext *>(codecCtx->hw_frames_ctx->data);
        if(hwFramesConstraints->min_width > FRAME_WIDTH || hwFramesConstraints->max_width < FRAME_WIDTH) {
            av_log(NULL, AV_LOG_ERROR, "frames width is not suitable \n");
        }
        if(hwFramesConstraints->min_height > FRAME_HEIGHT || hwFramesConstraints->max_height < FRAME_HEIGHT) {
            av_log(NULL, AV_LOG_ERROR, "frames height is not suitable \n");
        }
        hwFramesCtx->width = FRAME_WIDTH;
        hwFramesCtx->height = FRAME_HEIGHT;
        hwFramesCtx->initial_pool_size = 30;
            
        // 设置pixelformat
        for(int i = 0; hwFramesConstraints->valid_hw_formats[i] != AV_PIX_FMT_NONE; ++i) {
            // 打印一下
            av_log(NULL, AV_LOG_INFO,"Valid HWFormats %d : %d\n", i, hwFramesConstraints->valid_hw_formats[i]);
            if(hwFramesConstraints->valid_hw_formats[i] == AV_PIX_FMT_VIDEOTOOLBOX) {
                hwFramesCtx->format = hwFramesConstraints->valid_hw_formats[i];
                codecCtx->pix_fmt = hwFramesCtx->format;
            }
        }
            
        for(int i = 0; hwFramesConstraints->valid_sw_formats[i] != AV_PIX_FMT_NONE; ++i) {
            // 打印一下
            av_log(NULL, AV_LOG_INFO,"Valid SWFormats %d : %d\n", i, hwFramesConstraints->valid_sw_formats[i]);
            if(hwFramesConstraints->valid_sw_formats[i] == AV_PIX_FMT_NV12) {
                hwFramesCtx->sw_format = hwFramesConstraints->valid_sw_formats[i];
                codecCtx->sw_pix_fmt = hwFramesCtx->sw_format;
            }
        }

        if(av_hwframe_ctx_init(codecCtx->hw_frames_ctx) < 0) {
            av_log(NULL, AV_LOG_ERROR, "Init hw frames ctx error.\n");
            return;
        }
    }

    ret = avcodec_open2(codecCtx, codec, NULL);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "codec open failed\n");
        return;
    }

    ret = avcodec_parameters_from_context(strm->codecpar, codecCtx);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Copy parameters from context failed\n");
        return;
    }

    // 读取输入的nv12图片
    uint8_t *nv12Buf = nullptr;
    int bufSize = 0;
    readNv12ToBuf(nv12Buf, bufSize, srcNv12);

    // write header
    ret = avformat_write_header(fmtCtx, NULL);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "write header failed\n");
        return;
    }

    // 软件帧
    AVFrame *swFrame = av_frame_alloc();
    initSWFrame(swFrame, nv12Buf, bufSize);

    AVPacket *packet = av_packet_alloc();

    for(int i = 0;i < TEST_FRAME_SIZE; ++i) {
        // 每次都申请一张hwFrame
        AVFrame *hwFrame = av_frame_alloc();
        if(av_hwframe_get_buffer(codecCtx->hw_frames_ctx, hwFrame, 0) < 0) {
            av_log(NULL, AV_LOG_ERROR, "get a hwFrame error\n");
            return;
        }
        // 将帧数据传给设备
        if(av_hwframe_transfer_data(hwFrame, swFrame, 0) < 0) {
            av_log(NULL, AV_LOG_ERROR, "transfer data to hw failed\n");
            return;
        }

        hwFrame->pts = i;
        int ret = avcodec_send_frame(codecCtx, hwFrame);
        if(ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "send hwFrame failed\n");
            return;
        }
        av_frame_unref(hwFrame);
        av_frame_free(&hwFrame);

        while(ret >= 0) {
            ret = avcodec_receive_packet(codecCtx, packet);
            if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                break;
            } else if(ret < 0) {
                av_log(NULL, AV_LOG_ERROR, "receive packet failed\n");
                return;
            }

            packet->stream_index = strm->index;
            packet->dts = av_rescale_q(packet->dts, AVRational{1, 30}, strm->time_base);
            packet->pts = av_rescale_q(packet->pts, AVRational{1, 30}, strm->time_base);
            
            ret = av_interleaved_write_frame(fmtCtx, packet);
            if(ret < 0) {
                av_log(NULL, AV_LOG_ERROR, "write frame failed\n");
                return;
            }

            av_packet_unref(packet);
        }
    }

    ret = avcodec_send_frame(codecCtx, NULL);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "send NULL failed\n");
        return;
    }

    while(ret >= 0) {
        ret = avcodec_receive_packet(codecCtx, packet);
        if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if(ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "receive packet failed\n");
            return;
        }

        packet->stream_index = strm->index;
        packet->dts = av_rescale_q(packet->dts, AVRational{1, 30}, strm->time_base);
        packet->pts = av_rescale_q(packet->pts, AVRational{1, 30}, strm->time_base);

        ret = av_interleaved_write_frame(fmtCtx, packet);
        if(ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "write frame failed\n");
            return;
        }

        av_packet_unref(packet);
    }

    ret = av_write_trailer(fmtCtx);
    if(ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "write trailer failed\n");
        return;
    }

    avcodec_close(codecCtx);
    avio_closep(&fmtCtx->pb);
    av_frame_free(&swFrame);
    av_packet_free(&packet);
    avformat_free_context(fmtCtx);
}

5. 参考资料

  1. 《深入理解FFmpeg》 :刘歧等
  2. FFmpeg官方文档 AVHWFramesContext