第4章:RK MPP 核心API:mpi_decoder/mpi_encoder 初始化、配置、启动与销毁流程

好,咱们今天来聊聊 MPP 最核心的东西——解码器和编码器的生命周期管理。

说白了,就是怎么把一个编解码器“请出来”,告诉它要干什么,让它干活,最后再把它“送走”。

这个流程我刚开始接触 RK 平台时也绕了不少弯路,尤其是参数配置那块,稍不注意就踩坑。

4.1 解码器初始化:从 MPP 到 mpi_decoder

先说说解码器。你拿到一段 H.264 或者 H.265 的码流,想把它变成 YUV 图像,第一步就是创建解码器。

我个人习惯用 mpp_create()mpp_init() 这对组合拳。

MppCtx ctx;
MppApi *mpi;
MppDecCfg cfg;

// 1. 创建上下文
mpp_create(&ctx, &mpi);

// 2. 配置解码类型
mpp_dec_cfg_init(&cfg);
mpp_dec_cfg_set_u32(cfg, "type", MPP_VIDEO_CodingAVC); // H.264
mpp_dec_cfg_set_u32(cfg, "split_parse", 1); // 开启分帧解析
mpp_dec_cfg_set_u32(cfg, "internal_pts", 1); // 内部时间戳

// 3. 初始化解码器
mpp_init(ctx, MPP_CTX_DEC, MPP_CMD_INIT_CFG, cfg);

// 4. 清理配置
mpp_dec_cfg_deinit(cfg);

这里有个关键点——split_parse 这个参数。我遇到过一哥们,码流里没有起始码,结果解码器死活不干活。后来发现是没开这个选项。你想想看,MPP 默认是按帧边界来解析的,如果你的码流是裸流,就得靠它来帮你切分。

我的小经验: 对于实时流(比如摄像头过来的数据),建议把 internal_pts 打开。这样即使上层没传时间戳,MPP 也会帮你生成一个,方便后面做同步。

4.2 编码器初始化:参数配置更讲究

编码器初始化跟解码器类似,但参数更多。嗯,这里要特别注意。

MppCtx ctx;
MppApi *mpi;
MppEncCfg cfg;

mpp_create(&ctx, &mpi);

mpp_enc_cfg_init(&cfg);
mpp_enc_cfg_set_s32(cfg, "type", MPP_VIDEO_CodingAVC);
mpp_enc_cfg_set_s32(cfg, "width", 1920);
mpp_enc_cfg_set_s32(cfg, "height", 1080);
mpp_enc_cfg_set_s32(cfg, "hor_stride", 1920);
mpp_enc_cfg_set_s32(cfg, "ver_stride", 1088); // 注意!16对齐
mpp_enc_cfg_set_s32(cfg, "format", MPP_FMT_YUV420P);
mpp_enc_cfg_set_s32(cfg, "rc_mode", MPP_ENC_RC_CBR); // 固定码率
mpp_enc_cfg_set_s32(cfg, "bps", 2000000); // 2Mbps
mpp_enc_cfg_set_s32(cfg, "fps_in", 30);
mpp_enc_cfg_set_s32(cfg, "fps_out", 30);
mpp_enc_cfg_set_s32(cfg, "gop", 30);

mpp_init(ctx, MPP_CTX_ENC, MPP_CMD_INIT_CFG, cfg);
mpp_enc_cfg_deinit(cfg);

看到 ver_stride 我设了 1088 而不是 1080 吗?

这是 RK 平台的一个硬性要求——宽高必须是 16 的倍数。1080 不是 16 的倍数,所以得往上对齐到 1088。我曾经在这个问题上折腾了大半天,编码出来的图像底部有一条绿边,后来才发现是 stride 没对齐。

注意: 编码器的 hor_stridever_stride 必须按照硬件对齐要求来设置。对于 H.264/H.265,通常是 16 对齐。不对齐的话,轻则图像异常,重则直接崩溃。

4.3 启动流程:准备输入输出缓冲区

初始化完成后,还不能直接干活。你得先准备好“容器”——也就是 MPP 的缓冲区。

我个人习惯用 mpp_buffer_group_get() 来管理一组缓冲区,这样内存分配更高效。

MppBufferGroup group;
MppBuffer buf;

// 创建缓冲区组
mpp_buffer_group_get(&group, MPP_BUFFER_TYPE_ION);

// 分配一个 2MB 的缓冲区
mpp_buffer_get(group, &buf, 2 * 1024 * 1024);

// 把缓冲区挂到解码器上
mpi->control(ctx, MPP_DEC_SET_EXT_BUF_GROUP, group);
mpi->control(ctx, MPP_DEC_SET_INFO_CHANGE, NULL);

这里有个细节——MPP_DEC_SET_INFO_CHANGE 这个命令。它的作用是告诉解码器:“我要开始干活了,你准备好接收数据吧。” 如果不发这个命令,解码器会一直处于等待状态。

编码器这边稍微简单点,一般不需要显式启动,初始化完直接往里面塞帧就行。

4.4 数据流转:喂数据 & 取结果

启动之后,就是核心的数据处理循环了。解码器这边,我习惯用 mpi->decode_put_packet()mpi->decode_get_frame() 这对函数。

MppPacket packet;
MppFrame frame;

// 喂数据
mpp_packet_init(&packet, data, size);
mpi->decode_put_packet(ctx, packet);

// 取结果
while (mpi->decode_get_frame(ctx, &frame) == MPP_OK) {
    if (frame) {
        // 处理 YUV 数据
        process_frame(frame);
        mpp_frame_deinit(&frame);
    }
}
mpp_packet_deinit(&packet);

编码器则是反过来:mpi->encode_put_frame() 塞 YUV,mpi->encode_get_packet() 取码流。

核心要点: 解码器是“吃包拉帧”,编码器是“吃帧拉包”。这个对应关系搞清楚了,整个流程就顺了。

4.5 销毁流程:有始有终

最后一步,也是很多人容易忽略的一步——资源释放。

我见过不少代码,只创建不销毁,跑几天内存就爆了。MPP 的销毁流程其实很简单,但顺序有讲究。

// 1. 先停止解码/编码
mpi->reset(ctx);

// 2. 释放缓冲区
mpp_buffer_group_put(group);

// 3. 销毁上下文
mpp_destroy(ctx);

为什么要先 reset?因为 MPP 内部可能还有正在处理的任务队列。如果不 reset 就直接 destroy,那些未完成的任务会变成野指针,轻则内存泄漏,重则内核崩溃。

我曾经在一个项目中,因为忘记 reset,导致系统跑 24 小时后 OOM(内存耗尽)。排查了好久才发现是这里的问题。从那以后,我每次写销毁代码都会默念一遍:先 reset,再释放,最后 destroy。

避坑指南: 如果你在销毁时遇到 MPP_ERR_UNKNOW 错误,十有八九是缓冲区还没释放干净。检查一下是不是有 frame 或 packet 还引用着 buffer。

4.6 完整流程总结

好了,咱们把整个流程串起来,画个图在脑子里:

阶段 解码器 编码器
创建 mpp_create → mpp_init(MPP_CTX_DEC) mpp_create → mpp_init(MPP_CTX_ENC)
配置 mpp_dec_cfg_set_* mpp_enc_cfg_set_*
启动 分配缓冲区 → 发 INFO_CHANGE 分配缓冲区(可选)
运行 put_packet → get_frame put_frame → get_packet
销毁 reset → 释放缓冲区 → mpp_destroy reset → 释放缓冲区 → mpp_destroy

这个表格你收藏好,写代码时对照着来,基本不会出错。

最后说一句,MPP 的 API 设计其实挺清晰的,就是参数多了点。你只要把初始化、配置、启动、运行、销毁这五个阶段记牢,剩下的就是填参数的事了。

下一章咱们会深入讲讲 MppPacketMppFrame 这两个核心数据结构,以及它们背后的内存管理机制。到时候你会发现,理解了这两个东西,MPP 就掌握了一半。