瑞芯微mpp编码

瑞芯微mpp编码

瑞芯微mpp编码

本篇文章将详细讲解瑞芯微mpp的编码使用,从编译开始到API解析到源码全部开放,让大家能够对瑞芯微mpp编码有一定的了解,具体深入可以查看瑞芯微的官方手册

个人gitee仓库源码

笔者使用的芯片是RK3576,但是对应瑞芯微支持mpp的芯片的思想以及调用API都是一样的

mpp编译

mpp交叉编译

环境配置

  1. 更改交叉编译工具链

    在获取到瑞芯微的mpp代码仓库后,进入build/linux/aarch64/目录下

    rk3576是arm64位,如果是32位进入build/linux/arm/

    这里使用的是板厂给的SDK,可以根据自己的板子向厂商要SDK,也可以直接去github上获取瑞芯微官方SDK

    修改arm.linux.cross.cmake 文件里面的交叉编译工具链,改为自己的交叉编译工具链路径

  2. CMake && make

    运行make-Makefiles.bash脚本,通过cmake 生成 Makefile ,最后运行make进行编译。

  3. 安装依赖库

    使用sudo make install命令可以把mpp的相关头文件库文件安装在/usr/local/include/rockchip/usr/local/lib,如果不想安装在默认目录可以执行命令:

     

代码编译

情况:我在~/100ask-rk3576_SDK/external/mpp/test路径下写了一个新的mpp编解码程序,并且只想编译这个程序做一个测试

使用的是命令:

-I 路径说明:

  
-I/usr/local/include/rockchip公共 API:rk_mpi.h,rk_type.h
-I../incMPP 核心头文件(如mpp_frame.h等,虽然部分已安装,但保持一致)
-I../osal/incOS 抽象层头文件
-I../utils关键!包含 mpp_env.h, mpp_mem.h
-I.当前目录,用于mpi_enc_utils.h,utils.h(如果它们在test/目录)

rk3576本地编译

为了方便并且我的RK3576配置足够强大(12+128),我也在rk3576本地编译了mpp库

1.安装基础依赖:

2.获取MPP库,这里就是官方的SDK

3.编译

想要编译自己写的mpp程序,也需要向上面交叉编译一样指定库目录链接目录

过程中遇到的问题就是我根据上面的命令进行编译会报错一大堆mpp的函数未定义,最后将动态链接参数命令放在输出名后面才成功,个人认为是因为gcc的依赖关系是从左到右解析的,所以如果把my_test.cpp -o my_test放在最后那么出现在代码段中的外部链接符号不能被解析,所以要把-lrockchip_mpp -lpthread -ldl放在最后面

但是为何交叉编译就可以动态链接参数在前面?还不知道

最终Makefile模板:

 

vscode中编写mpp

在使用vscode进行开发时遇到问题就是无法识别头文件,哪怕我已经make install了但是也不行,最终查看到/usr/local/include/rockchip/中确实有mpp的公共头文件,但是我在源码中错误的使用了#include "rk_mpi.h"而不是使用<>来包含头文件,正确做法是

对于并不是公共头文件而是源码内的一些官方例程头文件,则需要进一步配置,在项目的根目录创建一个.vscode文件夹,然后创建一个配置json文件c_cpp_properties.json在里面写入

注意includePath中要写入的是实际的头文件所在的一层目录而不是只是mpp源码顶层目录

mpp开发

说明:以下都是基于瑞芯微的mpp

链接utils

在代码编写中要对输入文件进行读取,使用的是utils.c里面的read_image函数

utils.c(以及 utils.h)是 Rockchip MPP SDK 中的“示例工具代码”,通常不被编译进动态库(如 librockchip_mpp.so),而是作为 测试程序的源码组件,需要手动编译链接

librockchip_mpp.so 包含什么?

  • MPP 核心 API:mpp_create, mpp_init, mpp_buffer_get, rk_mpi.h 中声明的接口等。
  • 编码器/解码器硬件抽象层(HAL)
  • 内存管理、任务调度等底层逻辑
  1. 使用命令首先将utils.c编译为可重入文件(注意是使用gcc

    我当前的路径是在test路径之下

  2. 再编译自己的源文件

  3. 最后将两个可重入文件链接起来

     

查看h264文件

内存对齐

Rockchip 的视频编解码硬件(如 VPU / RGA / ISP)通常要求图像数据在内存中的 水平 stride(行跨度)和垂直1.stride(帧高度)必须按特定字节对齐(例如 16 字节或 64 字节),原因包括:

  • DMA 传输效率:硬件 DMA 控制器通常以固定块大小读取内存,非对齐会导致额外处理甚至错误。
  • 内部缓存行对齐:避免跨缓存行访问,提升带宽和性能。
  • 像素格式打包限制:例如 YUV420SP(NV12)格式中,Y 平面每行宽度需对齐,UV 平面也依赖 Y 的对齐。

RK3576 等 Rockchip SoC 上,MPP 驱动明确要求hor_stridever_stride至少 16 字节对齐(有时甚至要求 64 或 256 字节,取决于格式和分辨率)。

2.对齐如何影响 frame_size 计算?

这里使用的是 对齐后的stride,而非原始 width/height。这是因为:

  • 实际分配的缓冲区必须容纳对齐后的每行数据
  • read_image()函数也会按照hor_stride 写入数据(可能在行末填充 padding);
  • 如果不用对齐 stride计算buffer size会导致缓冲区溢出或图像错位。

3.宏原理简析

假设a = 16(二进制 10000),则 (a - 1) = 15(01111),~(a-1) = ...11110000

(x + 15) & ~15:先加 15,再清除低 4 位,实现“向上取整到 16 的倍数”。

因为 当 a 是 2 的幂时,可以用位运算加速

mpp编码代码

设定编码器结构体

编码配置需要使用到很多的变量,为了方便管理使用一个统一的结构体来标识编码器

设定编码器类

对于编码器肯定会有很多操作,将这些操作全部封装成一个类便于管理

初始化

mpp_buffer_get

mpp_buffer_get 函数原型:

  • group:MppBufferGroup类型,表示一个 缓冲区组(buffer pool)
  • buffer:输出参数,用于返回分配的 MppBuffer 句柄;
  • size:要分配的缓冲区大小(字节)。

1.第一个参数 group 的作用

MppBufferGroup是 MPP 中用于 统一管理一组缓冲区 的对象。你可以:

  • 创建一个buffer group(通过mpp_buffer_group_get());
  • 从中分配多个 buffermpp_buffer_get(group, ...));
  • 最后一次性释放整个groupmpp_buffer_group_put(group))。

这种方式适合 需要预分配多个 buffer 的场景(如解码器需要 10 个帧缓存)。

2.为什么可以传 NULL?

当你传入group = NULL时,MPP 会:

  • 自动使用一个内部的“默认/匿名”缓冲区组(anonymous group),为你分配一块独立的 buffer。

    这意味着:

    • 你不需要手动创建和管理 MppBufferGroup
    • 分配的 buffer 由 MPP 内部跟踪;
    • 你只需在用完后调用mpp_buffer_put(buffer) 即可释放。

✅ 适用于“只分配一个 buffer”的简单场景 —— 正如你的代码中只分配一个 frm_buf 用于循环读取 YUV 帧。

 

MppBufferGroup 在 Rockchip MPP 中的作用 类似于一个内存池(memory pool),但更准确地说,它是一个 缓冲区组(buffer group)管理器,用于统一申请、分配和释放一组具有相同属性的缓冲区

3.为什么说它“相当于内存池”?

(1)通过mpp_buffer_group_get()创建一个 buffer group,指定类型(如 ION、malloc)、对齐、数量等

(2)mpp_buffer_get(group, &buf, size)从 group 中取出一个 buffer

(3)mpp_buffer_group_put(group)一次性释放整个 group 中所有 buffer

(4)MPP 内部可复用 buffer,避免频繁调用malloc/ion_alloc

4.什么时候需要显式使用MppBufferGroup

当你需要:

  • 多帧并行处理(如 pipeline 中同时有 3 帧在编码);
  • 零拷贝传递 buffer(如从摄像头 V4L2 直接获取 buffer 给 MPP);
  • 严格控制内存生命周期(避免频繁分配/释放);

5.总结

MppBufferGroup ≈ 内存池:用于高效管理多个 buffer;

  • 传 NULL = 使用 MPP 内部临时池:适合简单、单 buffer 场景;
  • 显式创建 group = 自建内存池:适合高性能、多 buffer 场景。

初始化mpp编码器

  1. mpp_create

    用于创建mpp实例,函数原型:

    • MppCtx 是提供给⽤⼾使⽤的 MPP 实例上下⽂句柄,⽤于指代解码器或编码器实例
    • MppApi结构体封装了 MPP 的对外 API 接口,⽤⼾通过使⽤MppApi结构中提供的函数指针实现视频编解码功能
  2. mpp_init

    用于初始化mpp实例,函数原型:

    • MppCtx 是提供给⽤⼾使⽤的 MPP 实例上下⽂句柄,⽤于指代解码器或编码器实例

    • MppCtxType是一个enum类型,如下:

    • MppCodingType是编码类型,这里选的是MPP_VIDEO_CodingAVC

  3. mpp_enc_cfg_init

    初始化mpp的配置cfg,函数原型如下:

    • MppEncCfg配置单元
  4. 设置cfg

    MPP 推荐使⽤封装后的MppEncCfg 结构通过 control 接口的MPP_ENC_SET_CFG/MPP_ENC_GET_CFG命 令来进⾏编码器信息配置。

    由于编码器可配置的选项与参数繁多,使⽤固定结构体容易出现接口结构体频繁变化,导致接口⼆进制 兼容性⽆法得到保证,版本管理复杂,极⼤增加维护量。 为了缓解这个问题 MppEncCfg使⽤ (void *) 作为类型,使⽤

    < 字符串 值 > 进⾏key map 式的配置,函数接口 分为 s32/u32/s64/u64/ptr ,对应的接口函数分为 setget 两组

  5. 设置SEI

  6. 获取SPS/PPS

    SPS/PPS 是解码必需的元数据 播放器或解码器必须先读到 SPS/PPS,才能正确解析后续的 I/P/B 帧。如果它们被“延迟写入”甚至因程序崩溃而丢失,生成的视频文件将无法播放。

进行编码

CPU与硬件同步

Rockchip MPP(Media Process Platform)中,mpp_buffer_sync_begin()mpp_buffer_sync_end() 是用于 CPU 与硬件(如 VPU、GPU、DMA 引擎)之间内存同步的关键函数,尤其在使用 物理连续内存(如通过 libdrmION 分配的 buffer) 时非常重要。

在 RK3576 这类 SoC 上,MPP 使用的帧缓冲区(frm_buf)通常分配在 DMA 可访问的物理内存中(例如通过 rk_mpi 内部的 MppBuffer 机制)。这类内存可能被:

  • CPU 读写(比如你调用 read_image 把 YUV 数据写入 buffer),
  • VPU(视频编解码硬件) 读取(编码器从该 buffer 读取原始帧)。

而 CPU 和 VPU 通常有各自的缓存(Cache)或内存视图。如果不做同步:

  • CPU 写入的数据可能还停留在 CPU 缓存(Cache) 中,VPU 读到的是旧数据或垃圾;

  • 或者 VPU 写入的数据(如解码输出)CPU 读不到最新内容

    这就需要 显式内存同步(Cache Coherency Management)。

帧编码

  1. 零拷贝

    “零拷贝”(Zero-copy)是指:

    原始图像数据从源头(如摄像头、GPU 渲染输出、DMA 采集)到编码器,全程不经过 CPU 内存拷贝

    实现方式:

    • 使用

      物理连续、DMA 可访问的内存如:

      • ION buffer
      • DMA-BUF
      • DRM PRIME buffer(常用于 GPU/V4L2 交互)
    • 这些 buffer 由内核或专用内存分配器创建,硬件(VPU、ISP、GPU)可直接读写

    • MPP 的 MppBuffer 就是对这类内存的封装。

    优势: ✅ 无 CPU 拷贝开销 ✅ 无 cache 同步开销(或由硬件自动处理) ✅ 低延迟、高吞吐,适合 4K/60fps 等高性能场景

  2. encode_put_frame

    • ctx : MPP 解码器实例
    • frame :待输⼊的图像数据
    • 输⼊ frame 图像数据给 ctx 指定的 MPP 编码器实例

    由于编码器的输⼊图像⼀般都⽐较⼤,如果进⾏拷⻉,效率会⼤幅度下降,所以编码器的输⼊函数需要 等待编码器硬件完成对输⼊图像内存的使⽤,才能把输⼊函数返回,把使⽤后的图像归还给调⽤者。基于以上的考虑, encode_put_frame阻塞式函数,会把调⽤者阻塞住,直到输⼊图像使⽤完成,会⼀定程 度上导致软硬件运⾏⽆法并⾏,效率下降。

  3. encode_get_packet

    • ctx : MPP 解码器实例
    • packet :⽤于获取MppPacket实例的指针
    • ctx 指定的 MPP 编码器实例⾥获取完成编码的 packet 描述信息。

    (1)取头信息与图像数据

    以 H.264 编码器为例,编码器的输出数据分为头信息( sps/pps )图像数据( I/P slice )两部分,头信息 需要通过 control 接口的 MPP_ENC_GET_EXTRA_INFO命令获取,图像数据则是通过 encode_get_packet接口来获取。

    (2) H.264 编码器输出码流的格式

    ⽬前硬件固定输出带00 00 00 01起始码的码流,所以 encode_get_packet函数获取到码流都是带有 00 00 00 01起始码。如果需要去掉起始码,可以从起始码之后的地址进⾏拷⻉。

    (3)码流数据的零拷⻉

    由于使⽤ encode_put_frameencode_get_packet 接口时没有提供配置输出缓存的⽅法,所以使⽤ encode_get_packet⼀定会进⾏⼀次拷⻉。⼀般来说,编码器的输出码流相对于输⼊图像不算⼤,数据 拷⻉可以接受。如果需要使⽤零拷⻉的接口,需要使⽤enqueue/dequeue接口以及MppTask 结构。

获取码流写入文件

将编码器生成的一帧压缩数据(如 H.264/H.265 码流)写入到文件(如 .h264)中,用于后续播放、存储或传输。

  • mpp_packet_get_pos(packet) 中的 pos 是什么?

    pos(position)指的是:码流数据在内存中的起始地址(虚拟地址)

    pos 表示“当前有效数据的起始位置”,因为 packet 内部可能有预留头部或对齐空间,有效负载不一定从 buffer 起始处开始

结束编码

这段代码的作用是:向 MPP 编码器发送“流结束”(End-of-Stream, EOS)信号,并清空编码器内部可能缓存的剩余编码帧(如 B 帧、延迟帧),确保所有已编码的数据都被完整输出。

  1. 为什么需要 EOS?

    MPP 编码器(尤其是 H.264/H.265)在编码过程中可能会:

    • 缓存帧:为了 B 帧参考、码率控制(VBV)、帧重排序(如低延迟关闭时);

    • 延迟输出:最后一帧输入后,可能还有 1~3 帧未输出。

      如果不显式通知“输入结束”,编码器不知道你已经停止送帧,可能不会 flush 出剩余数据,导致:

    • 视频文件缺少最后几帧;

    • 播放时突然截断;

    • 码流不完整,无法正确解析。

  2. 构造一个 EOS 帧

    • 这个 eos_frame 不包含图像数据,只是一个控制信号
    • mpp_frame_set_eos(eos_frame, 1) 表示:“输入流到此结束”;
    • 调用 encode_put_frame 将这个信号送入编码器。

上一篇
下一篇