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

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


前言

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

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

前置条件

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

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

ffmpeg 的处理流程

瑞芯微 RK 系列 RK3588 使用 ffmpeg-rockchip 实现 MPP 视频硬件编解码-代码版_rk3588 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 文件中
#include #include #include #include extern \"C\" {#include #include #include #include }#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 文件放入当前目录,在开发板中运行如下命令编译并运行:

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 状态查看与操作》。

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

优化点

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

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

结语

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

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