第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_stride 和 ver_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 设计其实挺清晰的,就是参数多了点。你只要把初始化、配置、启动、运行、销毁这五个阶段记牢,剩下的就是填参数的事了。
下一章咱们会深入讲讲 MppPacket 和 MppFrame 这两个核心数据结构,以及它们背后的内存管理机制。到时候你会发现,理解了这两个东西,MPP 就掌握了一半。