瑞芯微 RK 系列 RK3588 使用 ffmpeg-rockchip 实现 MPP 视频硬件编解码-代码版

前言

上一篇文章中,我们讲解了如何使用 ffmpeg-rockchip 通过命令来实现 MPP 视频硬件编解码和 RGA 硬件图形加速,在这篇文章,我将讲解如何使用 ffmpeg-rockchip 用户空间库(代码)实现 MPP 硬件编解码。

本文不仅适用于 RK3588,还适用于 RK 家族系列的芯片,具体的细节可查看官方 MPP 文档

前置条件

本文假设你已经了解或掌握如下知识:

  • ffmpeg 用户空间库使用流程
  • 视频编解码原理

ffmpeg 的处理流程

上面这张图展示了 ffmpeg 的处理流程:

输入源 -> 解复用 -> 解码成帧 -> 执行各种操作,如缩放、旋转等 -> 编码 -> 复用 -> 输出

使用 ffmpeg-rochip 的好处

传统的使用硬件编解码的开发思路是:使用 ffmpeg 获取视频流,然后用 MPP 库进行硬件编解码,最后再传给 ffmpeg 进行复用,生成容器文件或推流。这样做的缺点是整个开发成本较高,需要学习 ffmpeg,还要学习 MPP库。

而现在有了 ffmpeg-rochip 之后,我们可以省略去学习使用 MPP 库的步骤,因为这个库已经帮我们封装好了 MPP 的功能,我们只需要像之前那样使用 ffmpeg 即可,只需在使用编解码器时换成 xxx_rkmpp,比如 h264_rkmpp。这样做的好处就是大大降低我们的开发学习成本。

编写思路

整个编写思路和我们日常编写 ffmpeg 时的思路是一致的,ffmpeg-rockchip 只是在 ffmpeg 的基础上封装了 MPP 和 RGA 的 api,实现了对应编解码器和过滤器,使得我们可以直接使用 ffmpeg 的 api 就能直接调用 MPP 和 RGA 功能。

下面的 demo,使用 cpp 语言,实现:"读取 MP4 文件,使用 MPP 的 h264 进行硬件解码,再使用 MPP 的 H265 进行硬件编码后输出 output.hevc 文件"的功能。

编写思路如下:

  1. 初始化各种上下文
  2. 读取当前目录下的 test.mp4 文件,进行解复用,获取视频流
  3. 使用 h264_rkmpp 解码器对视频帧进行硬解码
  4. 将解码后的视频帧使用 hevc_rkmpp 编码器进行硬编码
  5. 将编码的视频帧写入 output.hevc 文件中
cpp 复制代码
#include <csignal>
#include <iostream>
#include <stdio.h>
#include <stdlib.h>

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

#define MP4_PATH "./test.mp4"
#define OUTPUT_FILENAME "./output.hevc"
#define DECODEC_NAME "h264_rkmpp"
#define ENCODEC_NAME "hevc_rkmpp"

static const AVInputFormat *input_format;
static AVStream *video_in_stream;
static int video_in_stream_idx = -1;
static const AVCodec *rk_h264_decodec;
static const AVCodec *rk_hevc_encodec;
static AVCodecContext *rk_decodec_ctx = nullptr;
static AVCodecContext *rk_encodec_ctx = nullptr;
static AVFormatContext *mp4_fmt_ctx = nullptr;
static FILE *ouput_file;
static AVFrame *frame;
static AVPacket *mp4_video_pkt;
static AVPacket *hevc_pkt;

static void encode(AVFrame *frame, AVPacket *hevc_pkt, FILE *outfile) {
  int ret;

  if (frame)
    printf("Send frame %3" PRId64 "\n", frame->pts);

  ret = avcodec_send_frame(rk_encodec_ctx, frame);
  if (ret < 0) {
    char errbuf[AV_ERROR_MAX_STRING_SIZE];
    av_strerror(ret, errbuf, sizeof(errbuf));
    std::cerr << "Error sending a frame for encoding: " << errbuf << std::endl;
    exit(1);
  }

  while (ret >= 0) {
    ret = avcodec_receive_packet(rk_encodec_ctx, hevc_pkt);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
      return;
    else if (ret < 0) {
      char errbuf[AV_ERROR_MAX_STRING_SIZE];
      av_strerror(ret, errbuf, sizeof(errbuf));
      std::cerr << "Error during encoding: " << errbuf << std::endl;
      exit(1);
    }

    printf("Write packet %3" PRId64 " (size=%5d)\n", hevc_pkt->pts,
           hevc_pkt->size);

    fwrite(hevc_pkt->data, 1, hevc_pkt->size, outfile);

    av_frame_unref(frame);
    av_packet_unref(hevc_pkt);
  }
}

static void decode(AVPacket *mp4_video_pkt, AVFrame *frame) {
  int ret;

  ret = avcodec_send_packet(rk_decodec_ctx, mp4_video_pkt);
  if (ret < 0) {
    char errbuf[AV_ERROR_MAX_STRING_SIZE];
    av_strerror(ret, errbuf, sizeof(errbuf));
    std::cerr << "Error sending a frame for decoding: " << errbuf << std::endl;
    exit(1);
  }

  while (ret >= 0) {
    ret = avcodec_receive_frame(rk_decodec_ctx, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
      return;
    } else if (ret < 0) {
      char errbuf[AV_ERROR_MAX_STRING_SIZE];
      av_strerror(ret, errbuf, sizeof(errbuf));
      std::cerr << "Error during decoding: " << errbuf << std::endl;
      exit(1);
    }

    encode(frame, hevc_pkt, ouput_file);
  }
}

int main(int argc, char **argv) {

  int ret;

  input_format = av_find_input_format("mp4");
  if (!input_format) {
    std::cerr << "Could not find input format" << std::endl;
    return EXIT_FAILURE;
  }

  // 分配一个AVFormatContext。
  mp4_fmt_ctx = avformat_alloc_context();
  if (!mp4_fmt_ctx) {
    std::cerr << "Could not allocate format context" << std::endl;
    return EXIT_FAILURE;
  }

  // 打开输入流并读取头部信息。此时编解码器尚未开启。
  if (avformat_open_input(&mp4_fmt_ctx, MP4_PATH, input_format, nullptr) < 0) {
    std::cerr << "Could not open input" << std::endl;
    return EXIT_FAILURE;
  }

  // 读取媒体文件的数据包以获取流信息。
  if (avformat_find_stream_info(mp4_fmt_ctx, nullptr) < 0) {
    std::cerr << "Could not find stream info" << std::endl;
    return EXIT_FAILURE;
  }

  // 打印视频信息
  av_dump_format(mp4_fmt_ctx, 0, MP4_PATH, 0);

  // 查找视频流
  if ((ret = av_find_best_stream(mp4_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1,
                                 nullptr, 0)) < 0) {
    std::cerr << "Could not find video stream" << std::endl;
    return EXIT_FAILURE;
  }

  video_in_stream_idx = ret;
  video_in_stream = mp4_fmt_ctx->streams[video_in_stream_idx];

  std::cout << "video_in_stream->duration: " << video_in_stream->duration
            << std::endl;

  const char *filename = OUTPUT_FILENAME;
  int i = 0;

  // 查找解码器
  rk_h264_decodec = avcodec_find_decoder_by_name(DECODEC_NAME);
  if (!rk_h264_decodec) {
    std::cerr << "Codec '" << DECODEC_NAME << "' not found" << std::endl;
    exit(1);
  }

  rk_decodec_ctx = avcodec_alloc_context3(rk_h264_decodec);
  if (!rk_decodec_ctx) {
    std::cerr << "Could not allocate video rk_h264_decodec context"
              << std::endl;
    exit(1);
  }

  // 将视频参数复制到rk_h264_decodec上下文中。
  if (avcodec_parameters_to_context(rk_decodec_ctx, video_in_stream->codecpar) <
      0) {
    std::cerr << "Could not copy video parameters to rk_h264_decodec context"
              << std::endl;
    exit(1);
  }

  AVDictionary *opts = NULL;
  av_dict_set_int(&opts, "buf_mode", 1, 0);

  if (avcodec_open2(rk_decodec_ctx, rk_h264_decodec, &opts) < 0) {
    std::cerr << "Could not open rk_h264_decodec" << std::endl;
    exit(1);
  }

  // 查找编码器
  rk_hevc_encodec = avcodec_find_encoder_by_name(ENCODEC_NAME);
  if (!rk_hevc_encodec) {
    std::cerr << "Codec '" << ENCODEC_NAME << "' not found" << std::endl;
    exit(1);
  }

  rk_encodec_ctx = avcodec_alloc_context3(rk_hevc_encodec);

  if (!rk_encodec_ctx) {
    std::cerr << "Could not allocate video rk_hevc_encodec context"
              << std::endl;
    exit(1);
  }

  // 设置编码器参数
  rk_encodec_ctx->width = video_in_stream->codecpar->width;
  rk_encodec_ctx->height = video_in_stream->codecpar->height;
  rk_encodec_ctx->pix_fmt = AV_PIX_FMT_NV12;
  rk_encodec_ctx->time_base = video_in_stream->time_base;
  rk_encodec_ctx->framerate = video_in_stream->r_frame_rate;
  rk_encodec_ctx->gop_size = 50;
  rk_encodec_ctx->bit_rate = 1024 * 1024 * 10;

  av_opt_set(rk_encodec_ctx->priv_data, "profile", "main", 0);
  av_opt_set(rk_encodec_ctx->priv_data, "qp_init", "23", 0);
  av_opt_set_int(rk_encodec_ctx->priv_data, "rc_mode", 0, 0);

  ret = avcodec_open2(rk_encodec_ctx, rk_hevc_encodec, nullptr);
  if (ret < 0) {
    std::cerr << "Could not open rk_hevc_encodec: " << std::endl;
    exit(1);
  }

  mp4_video_pkt = av_packet_alloc();
  if (!mp4_video_pkt)
    exit(1);

  hevc_pkt = av_packet_alloc();
  if (!hevc_pkt)
    exit(1);

  ouput_file = fopen(filename, "wb");
  if (!ouput_file) {
    std::cerr << "Could not open " << filename << std::endl;
    exit(1);
  }

  frame = av_frame_alloc();
  if (!frame) {
    std::cerr << "Could not allocate video frame" << std::endl;
    exit(1);
  }

  while (true) {
    ret = av_read_frame(mp4_fmt_ctx, mp4_video_pkt);
    if (ret < 0) {
      std::cerr << "Could not read frame" << std::endl;
      break;
    }

    if (mp4_video_pkt->stream_index == video_in_stream_idx) {
      std::cout << "mp4_video_pkt->pts: " << mp4_video_pkt->pts << std::endl;

      decode(mp4_video_pkt, frame);
    }
    av_packet_unref(mp4_video_pkt);
    i++;
  }

  // 确保将所有帧写入
  av_packet_unref(mp4_video_pkt);
  decode(mp4_video_pkt, frame);
  encode(nullptr, mp4_video_pkt, ouput_file);

  fclose(ouput_file);

  avcodec_free_context(&rk_encodec_ctx);
  avformat_close_input(&mp4_fmt_ctx);
  avformat_free_context(mp4_fmt_ctx);
  av_frame_free(&frame);
  av_packet_free(&mp4_video_pkt);
  av_packet_free(&hevc_pkt);

  return 0;
}

将上面的代码放入 main.cpp 中,将 test.mp4 文件放入当前目录,在开发板中运行如下命令编译并运行:

sh 复制代码
g++ -o main main.cpp -lavformat -lavcodec -lavutil

./main

确保你的 rk 开发板环境中有 ffmpeg-rockchip 库,如果没有的可以参考我上篇文章的编译教程:《瑞芯微 RK 系列 RK3588 使用 ffmpeg-rockchip 实现 MPP 硬件编解码和 RGA 图形加速-命令版》

查看 VPU 的运行情况,如下说明成功使用了硬件编解码功能。如果不知道怎么查看 VPU 的运行情况,可以参考我这篇文章:《瑞芯微 RK 系列 RK3588 CPU、GPU、NPU、VPU、RGA、DDR 状态查看与操作》

优化点

以上的代码示例有个缺点,就是解码时会将视频帧上传到 VPU,之后传回内存,编码时又上传到 VPU,编码后再传回内存。这样就造成了不必要的数据拷贝,我们可以将视频帧解码之后,在 VPU 编码后再传回内存,提高编解码效率。

实现方案是使用 hw_device_ctx 上下文,由于篇幅问题,这里不给出代码示例。有需要的小伙伴可以在评论区回复或直接私聊我。

结语

本篇文章介绍了如何使用 ffmpeg-rockchip 进行 MPP 硬件编解码,在下一篇文章,我将介绍如何使用 ffmpeg-rockchip 使用 RGA 2D 图形加速,RGA 可以实现图像缩放、旋转、bitBlt、alpha混合等常见的2D图形操作。

如果觉得本文写得不错,请麻烦帮忙点赞、收藏、转发,你的支持是我继续写作的动力。我是 Leon_Chenl,我们下篇文章见~

相关推荐
恬静的小魔龙3 小时前
【Unity3D日常开发】Unity3D中打开Window文件对话框打开文件(PC版)
3d·unity·编辑器·游戏引擎·音视频
明月与玄武8 小时前
FFmpeg入门
ffmpeg
网易智企10 小时前
游戏语音的历史是什么样的?
大数据·人工智能·游戏·ai·音视频·语音识别·实时音视频
daqinzl12 小时前
利用ffmpeg将视频转为m3u8并加密
ffmpeg·openssl·m3u8·加密 解密
小阳生煎14 小时前
播放音频文件同步音频文本
vue·音视频
qq_3667406014 小时前
解决:ubuntu22.04中IsaacGymEnv保存视频报错的问题
python·ubuntu·音视频
renkai72114 小时前
windows动态壁纸音频显示效果推荐
单片机·嵌入式硬件·音视频
无敌最俊朗@15 小时前
Qt+ffmpeg+libVlc 实现简单视频播放器
c语言·开发语言·c++·qt·学习·ffmpeg·音视频
Leon_Chenl15 小时前
瑞芯微 RK 系列 RK3588 使用 ffmpeg-rockchip 实现 RGA 2D 图形操作硬件加速-代码版
图像处理·ffmpeg·音视频·ffmpeg-rockchip·2d图形硬件