【问题标题】:Encode video from C++ using libavcodec and VAAPI使用 libavcodec 和 VAAPI 从 C++ 编码视频
【发布时间】:2020-04-27 05:20:48
【问题描述】:

我正在尝试使用 libavcodec(版本 3.4.6)以 H.264 编码视频。当我使用软件编码器“libx264”时它可以工作,但是当我尝试将英特尔 cpu 的硬件编码器与 VAAPI 一起使用时它不起作用。通过 VAAPI 使用 ffmpeg 进行硬件编码可在命令行中工作(使用来自 here 的命令)。

显然,没有示例或教程如何使用 VAAPI 和 libav* 进行编码。我通读了ffmpeg's examples 中涉及相关用例(硬件解码、软件编码、复用)的内容,并尝试对它们进行相应的调整。

当我设置 VAAPI 编码器时,avcodec_open2() 返回AVERROR(EINVAL) (-22) 并将以下错误消息打印到控制台:

AVCodecContext.pix_fmt 和 AVHWFramesContext.format 不匹配

您可以在我的代码中Encoder::setupEncoder() 的末尾找到它。我错过了什么?

下面是我的代码,分为三个文件:

编码器.h

#ifndef ENCODER_H
#define ENCODER_H
#include <cassert>

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

class Encoder
{
public:
    Encoder(const bool hwAccel);
    void addFrame(AVFrame* frame);
    void flush();

    static constexpr int s_width = 640;
    static constexpr int s_height = 480;
    static constexpr int s_fps = 25;
private:
    void setup();
    void setupEncoder();
    void encodeFrame(AVFrame* frame);

    // members
    int m_frameId = 1;
    const bool m_hardwareAcceleration = false;

    AVCodecContext* m_encoder = nullptr;
    AVFormatContext* m_muxer = nullptr;
    AVStream* m_avStream = nullptr;
    AVBufferRef* m_device = nullptr;

    AVFrame* m_hwFrame = nullptr;
};

#endif // ENCODER_H

编码器.cpp

#include "encoder.h"

extern "C" {

static enum AVPixelFormat get_vaapi_format(AVCodecContext*, const enum AVPixelFormat *pix_fmts)
{
    const enum AVPixelFormat *p;
    for (p = pix_fmts; *p != AV_PIX_FMT_NONE; p++) {
        if (*p == AV_PIX_FMT_VAAPI)
            return *p;
    }
    fprintf(stderr, "Failed to get HW surface format.\n");
    return AV_PIX_FMT_NONE;
}

}

Encoder::Encoder(const bool hwAccel)
  : m_hardwareAcceleration(hwAccel)
{
    setup();
}
void Encoder::addFrame(AVFrame* frame)
{
    AVFrame* frameToEncode = frame;
    if(m_hardwareAcceleration) {
        assert(frame->format == AV_PIX_FMT_NV12);
        av_hwframe_transfer_data(m_hwFrame, frame, 0);
        assert(m_hwFrame->format == AV_PIX_FMT_VAAPI);
        frameToEncode = m_hwFrame;
    }

    frameToEncode->pts = m_frameId++;
    encodeFrame(frameToEncode);
}
void Encoder::flush()
{
    encodeFrame(nullptr);
    av_write_trailer(m_muxer);
}

void Encoder::setup()
{
    assert(avformat_alloc_output_context2(&m_muxer, nullptr, "matroska", nullptr) == 0);
    assert(m_muxer != nullptr);

    setupEncoder();

    m_avStream = avformat_new_stream(m_muxer, nullptr);
    assert(m_avStream != nullptr);
    m_avStream->id = m_muxer->nb_streams-1;
    m_avStream->time_base = m_encoder->time_base;

    // Some formats want stream headers to be separate.
    if(m_muxer->oformat->flags & AVFMT_GLOBALHEADER)
        m_encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

    assert(avcodec_parameters_from_context(m_avStream->codecpar, m_encoder) == 0);
    assert(avio_open(&m_muxer->pb, m_hardwareAcceleration? "hardware.mkv": "software.mkv", AVIO_FLAG_WRITE) == 0);
    assert(avformat_write_header(m_muxer, nullptr) == 0);
}
void Encoder::setupEncoder()
{
    const char* encoderName = m_hardwareAcceleration? "h264_vaapi": "libx264";
    AVCodec* videoCodec = avcodec_find_encoder_by_name(encoderName);
    m_encoder = avcodec_alloc_context3(videoCodec);
    m_encoder->bit_rate = s_width * s_height * s_fps * 2;
    m_encoder->width = s_width;
    m_encoder->height = s_height;
    m_encoder->time_base = (AVRational){1, s_fps};
    m_encoder->framerate = (AVRational){s_fps, 1};

    m_encoder->gop_size = s_fps;  // have at least 1 I-frame per second
    m_encoder->max_b_frames = 1;
    m_encoder->pix_fmt = AV_PIX_FMT_YUV420P;

    if(m_hardwareAcceleration) {
        m_encoder->pix_fmt = AV_PIX_FMT_VAAPI;
        m_encoder->get_format = get_vaapi_format;

        assert(av_hwdevice_ctx_create(&m_device, AV_HWDEVICE_TYPE_VAAPI, "/dev/dri/renderD128", nullptr, 0) == 0);

        AVHWDeviceContext* deviceCtx = (AVHWDeviceContext*) m_device->data;
        assert(deviceCtx->type == AV_HWDEVICE_TYPE_VAAPI);

        m_encoder->hw_device_ctx = av_hwframe_ctx_alloc(m_device);
        m_encoder->hw_frames_ctx = av_buffer_ref(m_device);
        m_hwFrame = av_frame_alloc();
        av_hwframe_get_buffer(m_encoder->hw_device_ctx, m_hwFrame, 0);
    }

    assert(avcodec_open2(m_encoder, videoCodec, nullptr) == 0);  // <-- returns -22 (EINVAL) for hardware encoder

    m_muxer->video_codec_id = videoCodec->id;
    m_muxer->video_codec = videoCodec;
}
void Encoder::encodeFrame(AVFrame* frame)
{
    assert(avcodec_send_frame(m_encoder, frame) == 0);

    AVPacket packet;
    av_init_packet(&packet);
    int ret = 0;
    while(ret >= 0) {
        ret = avcodec_receive_packet(m_encoder, &packet);
        if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return;  // nothing to write
        }
        assert(ret >= 0);

        av_packet_rescale_ts(&packet, m_encoder->time_base, m_avStream->time_base);
        packet.stream_index = m_avStream->index;
        av_interleaved_write_frame(m_muxer, &packet);
        av_packet_unref(&packet);
    }
}

main.cpp

#include "encoder.h"

AVFrame* createFrame(const int format)
{
    AVFrame* frame = av_frame_alloc();
    frame->format = format;
    frame->width  = Encoder::s_width;
    frame->height = Encoder::s_height;
    assert(av_frame_get_buffer(frame, 0) == 0);
    assert(av_frame_make_writable(frame) == 0);

    // Y
    for(int y=0; y<frame->height; y++) {
        for(int x=0; x<frame->width; x++) {
            frame->data[0][y * frame->linesize[0] + x] = 0;
        }
    }

    // CbCr
    const int widthCbCr  = frame->width / 2;
    const int heightCbCr = frame->height / 2;

    if(format == AV_PIX_FMT_YUV420P) {
        for(int y=0; y<heightCbCr; y++) {
            for(int x=0; x<widthCbCr; x++) {
                frame->data[1][y * frame->linesize[1] + x] = 0;  // Cb
                frame->data[2][y * frame->linesize[2] + x] = 0;  // Cr
            }
        }
        return frame;
    }
    else if(format == AV_PIX_FMT_NV12) {
        for(int y=0; y<heightCbCr; y++) {
            for(int x=0; x<widthCbCr; x++) {
                frame->data[1][y * frame->linesize[0] + x] = 0;
            }
        }
        return frame;
    }

    return nullptr;
}

int main()
{
    av_register_all();

    AVFrame* yuv420pFrame = createFrame(AV_PIX_FMT_YUV420P);
    AVFrame* nv12Frame = createFrame(AV_PIX_FMT_NV12);

    // works well
    Encoder softwareEncoder(false);
    for(int i=0; i<100; ++i)
        softwareEncoder.addFrame(yuv420pFrame);
    softwareEncoder.flush();

    // does not work
    Encoder hardwareEncoder(true);
    for(int i=0; i<100; ++i)
        hardwareEncoder.addFrame(nv12Frame);
    hardwareEncoder.flush();

    return 0;
}

请注意,我有意省略了各种 free() 函数和析构函数以保持代码简短。

【问题讨论】:

    标签: c++ c encoding libavcodec vaapi


    【解决方案1】:

    avcodec.h 文件中有一个很大的注释,这个(复制不佳)摘录所在的位置:

    • 不允许在同一个 AVCodecContext 上混合新旧函数调用,

    • 并且会导致未定义的行为。

    • 某些编解码器可能需要使用新的 API;使用旧 API 会返回

    • 调用时出错。所有编解码器都支持新的 API。

    这(和周围的内容)表明您看到错误的可能原因是其中一个,而不是另一个。

    【讨论】:

    • 感谢您的快速回复!是否有已弃用的函数的完整列表?您指出我的评论部分只提到了直接参与实际编码过程的功能。我的错误已经发生在 setup 步骤中。我忘了提到我使用的是 libav* 的 3.4.6 版本,它目前由 Ubuntu 18.04 LTS 分发。
    • @gretel99 - 是否有完整的已弃用函数列表?我不知道,但您可以尽我所能谷歌。您是否浏览过其余的 cmets。有很多,其中一些最终可能会将您指向其他信息来源。这是使用第三方甚至开源库时的常见过程。
    【解决方案2】:

    我仍然不知道上面的代码有什么问题。但是,我通过调试 ffmpeg 并在我的代码中模仿它的行为来让它工作。 ffmpeg 利用其过滤框架,而不是手动将帧传输到 GPU 内存。

    对于那些感兴趣的人,这是我适应 ffmpeg 的过滤 API 的代码(仍然没有释放任何资源):

    编码器.h

    #ifndef ENCODER_H
    #define ENCODER_H
    #include <cassert>
    
    extern "C" {
    #include <libavcodec/avcodec.h>
    #include <libavformat/avformat.h>
    #include <libavutil/opt.h>
    #include <libavutil/hwcontext.h>
    
    #include <libavfilter/avfilter.h>
    #include <libavfilter/buffersink.h>
    #include <libavfilter/buffersrc.h>
    }
    
    class Encoder
    {
    public:
        Encoder(const bool hwAccel);
        void addFrame(AVFrame* frame);
        void flush();
    
        static constexpr int s_width = 640;
        static constexpr int s_height = 480;
        static constexpr int s_fps = 25;
    private:
        void setup();
        void setupEncoder();
        void initFilters();
        void initInputFilters(AVFilterInOut* inputs);
        void initOutputFilters(AVFilterInOut* outputs);
        void filterFrame(AVFrame* inFrame, AVFrame* outFrame);
        void encodeFrame(AVFrame* frame);
    
        // members
        int m_frameId = 1;
        const bool m_hardwareAcceleration = false;
    
        AVCodecContext* m_encoder = nullptr;
        AVFormatContext* m_muxer = nullptr;
        AVStream* m_avStream = nullptr;
        AVBufferRef* m_device = nullptr;
    
        AVFrame* m_hwFrame = nullptr;
    
        AVFilterGraph* m_filterGraph = nullptr;
        AVFilterContext* m_bufferSrc = nullptr;
        AVFilterContext* m_bufferSink = nullptr;
        AVFilterContext* m_formatFilter = nullptr;
    };
    
    #endif // ENCODER_H
    

    编码器.c

    #include "encoder.h"
    
    extern "C" {
    
    static enum AVPixelFormat get_vaapi_format(AVCodecContext*, const enum AVPixelFormat *pix_fmts)
    {
        const enum AVPixelFormat *p;
        for (p = pix_fmts; *p != AV_PIX_FMT_NONE; p++) {
            if (*p == AV_PIX_FMT_VAAPI)
                return *p;
        }
        fprintf(stderr, "Failed to get HW surface format.\n");
        return AV_PIX_FMT_NONE;
    }
    
    }
    
    Encoder::Encoder(const bool hwAccel)
      : m_hardwareAcceleration(hwAccel)
    {
        setup();
    }
    void Encoder::addFrame(AVFrame* frame)
    {
        AVFrame* frameToEncode = frame;
        if(m_hardwareAcceleration) {
            filterFrame(frame, m_hwFrame);
            assert(m_hwFrame->format == AV_PIX_FMT_VAAPI);
            frameToEncode = m_hwFrame;
        }
    
        frameToEncode->pts = m_frameId++;
        encodeFrame(frameToEncode);
    }
    void Encoder::flush()
    {
        encodeFrame(nullptr);
        av_write_trailer(m_muxer);
    }
    
    void Encoder::setup()
    {
        assert(avformat_alloc_output_context2(&m_muxer, nullptr, "matroska", nullptr) == 0);
        assert(m_muxer != nullptr);
    
        setupEncoder();
    
        m_avStream = avformat_new_stream(m_muxer, nullptr);
        assert(m_avStream != nullptr);
        m_avStream->id = m_muxer->nb_streams-1;
        m_avStream->time_base = m_encoder->time_base;
    
        // Some formats want stream headers to be separate.
        if(m_muxer->oformat->flags & AVFMT_GLOBALHEADER)
            m_encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    
        assert(avcodec_parameters_from_context(m_avStream->codecpar, m_encoder) == 0);
        assert(avio_open(&m_muxer->pb, m_hardwareAcceleration? "hardware.mkv": "software.mkv", AVIO_FLAG_WRITE) == 0);
        assert(avformat_write_header(m_muxer, nullptr) == 0);
    }
    void Encoder::setupEncoder()
    {
        const char* encoderName = m_hardwareAcceleration? "h264_vaapi": "libx264";
        AVCodec* videoCodec = avcodec_find_encoder_by_name(encoderName);
        m_encoder = avcodec_alloc_context3(videoCodec);
        m_encoder->bit_rate = s_width * s_height * s_fps * 2;
        m_encoder->width = s_width;
        m_encoder->height = s_height;
        m_encoder->time_base = (AVRational){1, s_fps};
        m_encoder->framerate = (AVRational){s_fps, 1};
    
        m_encoder->gop_size = s_fps;  // have at least 1 I-frame per second
        m_encoder->max_b_frames = 1;
        m_encoder->pix_fmt = AV_PIX_FMT_YUV420P;
    
        if(m_hardwareAcceleration) {
            m_encoder->pix_fmt = AV_PIX_FMT_VAAPI;
            m_encoder->get_format = get_vaapi_format;
    
            assert(av_hwdevice_ctx_create(&m_device, AV_HWDEVICE_TYPE_VAAPI, "/dev/dri/renderD128", nullptr, 0) == 0);
            const AVHWDeviceContext* deviceCtx = (AVHWDeviceContext*) m_device->data;
            assert(deviceCtx->type == AV_HWDEVICE_TYPE_VAAPI);
    
            initFilters();
    
            m_encoder->hw_device_ctx = nullptr;
            m_encoder->hw_frames_ctx = av_buffer_ref(av_buffersink_get_hw_frames_ctx(m_bufferSink));
        }
    
        assert(avcodec_open2(m_encoder, videoCodec, nullptr) == 0);
    
        if(m_hardwareAcceleration) {
            m_encoder->hw_device_ctx = av_hwframe_ctx_alloc(m_device);
            m_hwFrame = av_frame_alloc();
            av_hwframe_get_buffer(m_encoder->hw_device_ctx, m_hwFrame, 0);
        }
    
        m_muxer->video_codec_id = videoCodec->id;
        m_muxer->video_codec = videoCodec;
    }
    void Encoder::initFilters()
    {
        AVFilterInOut* inputs = nullptr;
        AVFilterInOut* outputs = nullptr;
        m_filterGraph = avfilter_graph_alloc();
        assert(avfilter_graph_parse2(m_filterGraph, "format=nv12,hwupload", &inputs, &outputs) == 0);
    
        for(unsigned i=0; i<m_filterGraph->nb_filters; i++) {
            m_filterGraph->filters[i]->hw_device_ctx = av_buffer_ref(m_device);
            assert(m_filterGraph->filters[i]->hw_device_ctx != nullptr);
        }
    
        initInputFilters(inputs);
        initOutputFilters(outputs);
    
        assert(avfilter_graph_config(m_filterGraph, nullptr) == 0);
    }
    void Encoder::initInputFilters(AVFilterInOut* inputs)
    {
        assert(inputs != nullptr);
        assert(inputs->next == nullptr);
    
        char args[512];
        snprintf(args, sizeof(args),
                "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
                s_width, s_height, AV_PIX_FMT_YUV420P,
                1, s_fps,
                1, 1);
    
        assert(avfilter_graph_create_filter(&m_bufferSrc, avfilter_get_by_name("buffer"), "in",
                                            args, nullptr, m_filterGraph) == 0);
        assert(avfilter_link(m_bufferSrc, 0, inputs->filter_ctx, inputs->pad_idx) == 0);
    }
    void Encoder::initOutputFilters(AVFilterInOut* outputs)
    {
        assert(outputs != nullptr);
        assert(outputs->next == nullptr);
    
        assert(avfilter_graph_create_filter(&m_bufferSink, avfilter_get_by_name("buffersink"), "out",
                                           nullptr, nullptr, m_filterGraph) == 0);
        assert(avfilter_graph_create_filter(&m_formatFilter, avfilter_get_by_name("format"), "format",
                                           "vaapi_vld", nullptr, m_filterGraph) == 0);
        assert(avfilter_link(outputs->filter_ctx, outputs->pad_idx, m_formatFilter, 0) == 0);
        assert(avfilter_link(m_formatFilter, 0, m_bufferSink, 0) == 0);
    }
    void Encoder::filterFrame(AVFrame* inFrame, AVFrame* outFrame)
    {
        assert(av_buffersrc_add_frame_flags(m_bufferSrc, inFrame, AV_BUFFERSRC_FLAG_KEEP_REF) == 0);
        assert(av_buffersink_get_frame(m_bufferSink, outFrame) == 0);
    }
    void Encoder::encodeFrame(AVFrame* frame)
    {
        assert(avcodec_send_frame(m_encoder, frame) == 0);
    
        AVPacket packet;
        av_init_packet(&packet);
        int ret = 0;
        while(ret >= 0) {
            ret = avcodec_receive_packet(m_encoder, &packet);
            if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                return;  // nothing to write
            }
            assert(ret >= 0);
    
            av_packet_rescale_ts(&packet, m_encoder->time_base, m_avStream->time_base);
            packet.stream_index = m_avStream->index;
            av_interleaved_write_frame(m_muxer, &packet);
            av_packet_unref(&packet);
        }
    }
    

    【讨论】:

      【解决方案3】:

      可能有点晚了,但是对于任何仍然感兴趣的人来说,原始代码(没有过滤 API)实际上可以工作,但是 gretel99 错过了执行 av_hwframe_ctx_alloc,请参阅 doc/examples/vaapi_transcode.c 中的 FFMmpeg 示例。

      这是一个固定的 Encoder::setupEncoder() 版本,其中包含需要更改的 cmets。不仅仅是缺少 hwframe 缓冲区分配,至少对于我的 VAAPI 驱动程序 rc_mode=CQP 是必需的,并且 global_quality 必须设置为关闭警告。

      void Encoder::setupEncoder()
      {
          const char* encoderName = m_hardwareAcceleration? "h264_vaapi": "libx264";
          AVCodec* videoCodec = avcodec_find_encoder_by_name(encoderName);
          m_encoder = avcodec_alloc_context3(videoCodec);
          m_encoder->bit_rate = s_width * s_height * s_fps * 2;
          m_encoder->width = s_width;
          m_encoder->height = s_height;
          m_encoder->time_base = (AVRational){1, s_fps};
          m_encoder->framerate = (AVRational){s_fps, 1};
      
          m_encoder->gop_size = s_fps;  // have at least 1 I-frame per second
          m_encoder->max_b_frames = 1;
          m_encoder->pix_fmt = AV_PIX_FMT_YUV420P;
      
          if(m_hardwareAcceleration) {
              m_encoder->pix_fmt = AV_PIX_FMT_VAAPI;
              m_encoder->get_format = get_vaapi_format;
      
              assert(av_hwdevice_ctx_create(&m_device, AV_HWDEVICE_TYPE_VAAPI, "/dev/dri/renderD128", nullptr, 0) == 0);
      
              AVHWDeviceContext* deviceCtx = (AVHWDeviceContext*) m_device->data;
              assert(deviceCtx->type == AV_HWDEVICE_TYPE_VAAPI);
      
              // Fix error: Mismatching AVCodecContext.pix_fmt and AVHWFramesContext.format
              // See doc/examples/vaapi_transcode.c "set_hwframe_ctx()"
              {
                  AVBufferRef *hw_frames_ref;
                  AVHWFramesContext *frames_ctx = NULL;
      
                  assert((hw_frames_ref = av_hwframe_ctx_alloc(m_device)) != nullptr);
                  frames_ctx = (AVHWFramesContext *)(hw_frames_ref->data);
                  frames_ctx->format    = AV_PIX_FMT_VAAPI;
                  frames_ctx->sw_format = AV_PIX_FMT_NV12;
                  frames_ctx->width     = s_width;
                  frames_ctx->height    = s_height;
                  frames_ctx->initial_pool_size = 20;
                  assert(av_hwframe_ctx_init(hw_frames_ref) == 0);
                  m_encoder->hw_frames_ctx = av_buffer_ref(hw_frames_ref);
                  assert(m_encoder->hw_frames_ctx != nullptr);
              }
      
              // Fix error: Driver does not support any RC mode compatible with selected options (supported modes: CQP).
              assert(av_opt_set(m_encoder->priv_data, "rc_mode", "CQP", AV_OPT_SEARCH_CHILDREN) == 0);
      
              // Fix warning, cosmetical only: No quality level set; using default (20).
              m_encoder->global_quality = 20;
      
              m_encoder->hw_device_ctx = av_hwframe_ctx_alloc(m_device);
              //m_encoder->hw_frames_ctx = av_buffer_ref(m_device);           // Fix: Not required, done by av_hwframe_ctx_alloc
              m_hwFrame = av_frame_alloc();
              av_hwframe_get_buffer(m_encoder->hw_frames_ctx, m_hwFrame, 0); // Fix: Must pass hw_frames_ctx, not m_encoder->hw_device_ctx
              assert(m_hwFrame != nullptr);
          }
      
          assert(avcodec_open2(m_encoder, videoCodec, nullptr) == 0);  // <-- returns -22 (EINVAL) for hardware encoder
      
          m_muxer->video_codec_id = videoCodec->id;
          m_muxer->video_codec = videoCodec;
      }
      

      【讨论】:

        猜你喜欢
        • 2012-11-12
        • 2015-03-08
        • 2013-10-27
        • 2013-12-09
        • 2019-01-21
        • 2011-01-25
        • 2013-01-16
        • 2018-04-29
        • 2019-04-07
        相关资源
        最近更新 更多