RK3588平台MPP源码编译与H.264硬编实践:从构建到踩坑全记录
发布时间:2026/9/25 8:15:46来源:尧图网络
做RK平台音视频开发的人对MPP这三个字母应该都不陌生。瑞芯微的媒体处理平台Media Process Platform简称MPP负责把H.264、H.265、VP9、JPEG这些格式的编解码工作从CPU上转移到VPU硬件上执行。上层不管是FFmpeg、GStreamer还是自己写的应用最终都要通过MPP去驱动硬件编解码。这篇文章是我最近在RK3588板子上从源码编译MPP再跑通一整条H.264编码链路的完整记录包括编译选项、API调用、参数配置以及不少踩坑经验。适合两类人看一类是刚接触RK平台的嵌入式开发者想搞清楚MPP怎么编译、怎么调另一类是已经在用MPP但遇到解码失败、花屏、编码不出流这类问题的人可以直接翻到第6节找排查思路。1. 这块芯片上的媒体处理平台为什么值得你从头编译一次1.1 RK平台视频处理的那条隐形链路在RK3588、RK3568、RK3399这些芯片上跑视频应用最底层干活的是VPU硬件模块也就是视频编解码单元。但VPU不会自己暴露寄存器让应用去操作中间隔了一层软件层这就是MPP。MPP把VPU能力封装成一套C接口让应用层能轻松完成帧分配、编码、解码、取流这些动作。用得最多的场景大概是这几类摄像头原始数据做H.264/H.265编码后推到RTSP流网络收到的H.264码流硬解成YUV送给显示或算法多路视频拼接解码后通过RGA做格式转换给NPU做检测。MPP在这条链路里就是最关键的中间层它向上承接应用向下对接VPU、Ion/DMA内存管理和显示驱动。如果你是第一次接触RK平台很容易把MPP理解成一套带接口的编解码SDK这个说法不离谱。但要注意MPP和FFmpeg里的软编软解完全是两回事MPP不处理封装格式也不好做复杂的滤镜它只专注裸流和裸帧的编解码以及配套的Buffer管理。实际项目中MPP通常和FFmpeg配合——FFmpeg负责协议解析、封装解封装MPP负责硬编硬解。1.2 明明能直接拿来用我为什么还要源码编译很多RK官方SDK里已经带了MPP的编译好的so库和头文件demo板系统的/usr/lib/aarch64-linux-gnu/下可能也预装了librkmp.so。那为什么还要自己从源码编译一遍我这次动手的主要原因有三个。第一是版本不可控。SDK里带的MPP版本往往滞后而GitHub上rockchip-linux/mpp仓库会持续更新新版本会修复旧版在H.264/HEVC编码上的问题比如参考帧管理、码率控制、SPS/PPS输出时机等。如果项目里已经出现了某些奇怪的编码问题换新版本是最容易先做的一步排查。第二是要加日志和调符号。预编译库通常不带充分的调试符号自己编一个Debug版本可以在定位解码失败、buffer泄漏问题时直接看MPP内部的日志输出效率高很多。MPP的日志体系很完整从mpp_log到各模块的dbg开关都能在编译时打开生产环境没法这样看但开发环境完全值得。第三是方便改源码。有些定制需求必须动到MPP本身比如修改解码输出格式、调整硬编参数范围、给特定平台加workaround这些都需要源码在手边。所以自己编译MPP不是折腾而是做RK音视频开发的基操。2. 编译前的准备工作与工具链取舍2.1 准备源码和基础依赖源码获取方式很简单直接用Git克隆rockchip官方仓库就行。git clone https://github.com/rockchip-linux/mpp.git cd mpp git checkout develop注意两点。第一develop分支是日常开发分支相对新但也会偶发不稳定如果做稳定产品建议切到发布版本比如release标签或SDK中固定的commit。第二MPP依赖libdrm因为RK平台的内存分配、帧缓冲管理和DRM子系统的节点关系紧密编译前需要先把系统里装好开发包。在开发板上直接编译的话先装依赖sudo apt update sudo apt install -y cmake gcc g make libdrm-dev如果你记得cmake版本太旧也可以从源码编新版CMake。这里多说一句MPP的CMake最低版本要求不算高但太老的cmake比如3.5以下会在生成编译规则时各种别扭我建议CMake版本至少3.10以上系统源装不了就老老实实源码编一个。2.2 板端直编和PC交叉编译怎么选MPP编译有两种主流路径选择取决于你的构建环境和目标设备。第一种是直接在开发板上编译。比如RK3588刷了Ubuntu系统板子上自带gcc、make、cmake直接在板端跑编译最省事。好处是不用操心交叉编译链生成库能直接用mpi_enc_test这类可执行文件也能立刻在板上跑。缺点就是板子算力有限MPP全量编译大概要几十秒到几分钟没有PC那么快。我这次为了边改边测就是选择了板端直编反正MPP整个工程不算大全量也就一会会儿。第二种是PC交叉编译适合做量产固件、需要把MPP打进rootfs的场合。你要准备好对应芯片的工具链比如aarch64-linux-gnu-系列然后通过CMake指定编译器cmake .. \ -DCMAKE_C_COMPILERaarch64-linux-gnu-gcc \ -DCMAKE_CXX_COMPILERaarch64-linux-gnu-g \ -DCMAKE_BUILD_TYPERelease \ -DRKPLATFORMON交叉编译最大的坑在依赖头文件上。PC机上装了libdrm-dev但交叉工具链默认搜索路径里没有这些头文件和库你需要把SDK rootfs的sysroot传给CMake或者把libdrm源码也交叉编一遍。我见过很多人卡在这里编到一半报找不到drm.h其实就是交叉环境没配置对。如果你是初学者我建议第一轮先直接在板子上编译跑通再说。3. 源码编译完整过程与产物解析3.1 CMake构建与常用配置项MPP用的是CMake构建系统相比传统手写Makefile管理多目录工程的方式清爽得多。MFK在多目录场景里要手工维护依赖关系而MPP的每个子目录都有自己的CMakeLists.txt顶层负责汇总编译时你几乎不需要关心目录之间的头文件依赖CMake会自动处理。基础编译命令我这次用的是cd mpp mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease -DRKPLATFORMON make -j4几个关键选项解释一下。-DRKPLATFORMON表示目标平台是RK芯片编译时会启用平台相关的内存、buffer和硬件适配代码。如果你在PC上编一个纯软件模拟版本少数调试场景可以关掉。-DCMAKE_BUILD_TYPERelease对应优化版本Debug版本会带大量调试日志和符号单帧编码性能差很多但排查问题好用。-DBUILD_TESTON默认会编译mpi_enc_test、mpi_dec_test、mpp_info_test这些测试工具这些工具在验证硬件编解码时非常有用别把它关了。如果需要把生成库安装到系统可以sudo make install默认会装到/usr/lib和/usr/include/rockchip方便后续编译自己应用时直接链接。我实际开发时经常在Release和Debug之间切换。一种比较顺手的方式是建两个build目录比如build_release和build_debug需要哪个编哪个不用反复清理。3.2 编译产物和默认安装路径编译完成后build目录下会生成几个关键的库文件和测试工具。产物说明librkmp.soMPPM核心库应用主要链接这个librkmp.a静态库版本适合打进固件或静态链接场景mpi_enc_test编码测试程序命令行传参即可快速测试mpi_dec_test解码测试程序可以验证H.264/H.265裸流解码mpp_info_test打印MPP版本、能力、硬件编解码器信息mpp_bench_test基准测试工具用来测编解码性能和吞吐如果你用sudo make install安装头文件会在/usr/include/rockchip目录下其中rk_mpi.h是主入口头文件和MPP API交互基本只靠它以及它包含的一系列子头文件比如rk_mpi_cmd.h、rk_mpi_cfg.h、mpp_buffer.h、mpp_frame.h、mpp_packet.h。自己写应用时只需要把librkmp.so路径在CMake里指定好再包含rk_mpi.h就行。3.3 编译期常踩的几个坑源码编译看着简单实际动手还是有几个坑值得说一下。第一个坑是清理不彻底。MPP的CMake承接的是大工程的拓扑如果中途改过CMake选项直接在旧build目录里重编可能带上之前的缓存导致配置不生效。我建议每次修改选项后干脆把build目录整个删掉重建反正编译速度快别舍不得这几分钟。第二个坑是libdrm缺失。前面说过RKPLATFORMON时MPP底层要链接DRMPC或板子环境里如果没装libdrm-dev会直接报头文件缺失或链接失败。Ubuntu系统直接apt install libdrm-dev就好但如果你的板子是裁剪过的系统可能要自己交叉编译libdrm。第三个坑是对齐和内存节点问题在编译期发现不了要运行期才暴露。MPP默认会用ION或DMA-BUF管理buffer某些内核精简过的板卡上/dev/ion节点可能不存在导致程序跑起来就报buffer分配失败。这类问题编译再顺利也没用我在第6节会单独讲排查方法。4. MPP编码核心概念与API调用流程4.1 MppCtx、MppApi 和 Buffer 体系理解了MPP的编码流程你才能把H.264编码测试写出花来。MPP在设计上把会话和操作接口拆开了MppCtx代表一次编解码会话的上下文MppApi是一组操作函数指针实际调用时通过mpi-control()、mpi-encode_put_frame()这些函数去执行。一次H.264编码的典型初始化流程大致如下MppCtx ctx NULL; MppApi *mpi NULL; // 1. 创建上下文 mpp_create(ctx, mpi); // 2. 设置编码模式 RK_U32 coding MPP_VIDEO_CodingAVC; mpi-control(ctx, MPP_CMD_SET_CODEC_TYPE, coding); // 3. 进入编码状态 MppPollType timeout MPP_POLL_NON_BLOCK; mpi-control(ctx, MPP_CMD_SET_POLL_TYPE, timeout); mpi-control(ctx, MPP_CMD_SET_ENC_CFG, cfg);然后是Buffer体系。MPP在编码时输入是MppFrame输出是MppPacket。MppFrame里封装了图像宽高、格式、数据指针或fdMppPacket则是编码后的码流数据。这些数据背后都关联到MppBufferGroup一组MppBuffer构成的内存池。硬件编解码对内存有特殊要求通常必须是ION/DMA-BUF连续内存所以不能简单malloc一个数组当作输入帧。一个常见的Buffer分配代码段如下MppBufferGroup group NULL; MppBuffer frame_buf NULL; mpp_buffer_group_get_internal(group, MPP_BUFFER_TYPE_ION); mpp_buffer_get(group, frame_buf, frame_size);frame_size不能简单用width*height*3/2要按照stride计算。VPU要求宽高对齐RK3588上宽高最好16对齐部分场景64对齐更稳。如果你把stride设成和width一样最终编码出来的画面大概率是斜的或者带绿边。这属于新手最容易犯的错误。正确的计算方式是用width_stride * height_stride * 3 / 2其中width_stride是向上对齐后的宽度。4.2 H.264编码器参数到底怎么配编码参数配置是通过MppEncCfg对象完成的它是一组键值对形式的配置表用MppEncCfg接口逐项设置。我常用的H.264编码配置流程如下MppEncCfg cfg NULL; mpp_enc_cfg_init(cfg); mpp_enc_cfg_set_s32(cfg, prep:width, width); mpp_enc_cfg_set_s32(cfg, prep:height, height); mpp_enc_cfg_set_s32(cfg, prep:hor_stride, width_stride); mpp_enc_cfg_set_s32(cfg, prep:ver_stride, height_stride); mpp_enc_cfg_set_s32(cfg, prep:format, MPP_FMT_YUV420SP); mpp_enc_cfg_set_s32(cfg, rc:mode, MPP_ENC_RC_MODE_CBR); mpp_enc_cfg_set_s32(cfg, rc:bps, bps); // 目标码率 mpp_enc_cfg_set_s32(cfg, rc:bps_min, bps * 80 / 100); mpp_enc_cfg_set_s32(cfg, rc:bps_max, bps * 120 / 100); mpp_enc_cfg_set_s32(cfg, rc:gop, gop); // GOP大小 mpp_enc_cfg_set_s32(cfg, rc:fps, fps); // 帧率 mpp_enc_cfg_set_s32(cfg, codec:type, MPP_VIDEO_CodingAVC); mpp_enc_cfg_set_s32(cfg, codec:avc:profile, 100); // High profile mpp_enc_cfg_set_s32(cfg, codec:avc:level, 40); // Level 4.0prep这部分描述的是输入图像属性rc是码率控制参数codec是编码器专属参数。实际编码时你还要通过MPP_CMD_SET_ENC_HEADER设置SPS/PPS输出方式。这里我建议设置成MPP_ENC_HEADER_MODE_EACH_IDR也就是每个IDR帧都带SPS/PPS。这样做虽然会略微增加码流体积但能极大提高流媒体场景下解码器的兼容性。很多播放器随机seek后黑屏就是SPS/PPS丢失导致的。码率控制模式的选择也要根据场景来。CBR适合直播、通话这类带宽受限的场景能保持码率平稳但高动态画面下画质会有波动。VBR适合本地录像、低带宽波动的场景能在画面复杂时提高码率简单画面时压下来。FIXQP一般只在画质测试或纯实验室场景用设置固定QP。我项目里默认用CBR直播流的带宽预算好算也不会突然撑爆网络。4.3 编码主循环怎么写编码主循环是整个测试的核心。基本思路是每循环一帧把YUV数据填入MppBuffer设置MppFrame的宽高、格式和buffer然后交给编码器。调encode_put_frame放入帧后用循环调encode_get_packet把编码好的码流包取出来写入文件。FILE *fp fopen(output.h264, wb); for (int i 0; i frame_count; i) { // 读取一帧YUV到buffer read(fd, mpp_buffer_get_ptr(frame_buf), frame_size); // 设置输入帧属性 mpp_frame_set_width(frame, width); mpp_frame_set_height(frame, height); mpp_frame_set_hor_stride(frame, width_stride); mpp_frame_set_ver_stride(frame, height_stride); mpp_frame_set_fmt(frame, MPP_FMT_YUV420SP); mpp_frame_set_buffer(frame, frame_buf); // 送入编码器 mpi-encode_put_frame(ctx, frame); // 取出所有编码好的包 while (1) { ret mpi-encode_get_packet(ctx, packet); if (ret ! MPP_OK) break; if (packet) { fwrite(mpp_packet_get_data(packet), mpp_packet_get_size(packet), 1, fp); mpp_packet_deinit(packet); } } } // 通知编码器结束取走剩余包 mpi-encode_put_frame(ctx, NULL); while (1) { ret mpi-encode_get_packet(ctx, packet); if (ret ! MPP_OK) break; if (packet) { fwrite(mpp_packet_get_data(packet), mpp_packet_get_size(packet), 1, fp); mpp_packet_deinit(packet); } } fclose(fp);注意encode_get_packet在非阻塞模式下如果没有包会返回MPP_ERR_TIMEOUT这时候不应该立即退出而是继续等下一帧或等编码器消化完当前帧。我写循环时习惯先encode_put_frame再用内部while把所有包捞空这样能保证每一帧的码流都及时取走不会在缓冲区里堆积。还有一个容易忽略的点mpp_packet_deinit负责释放包资源一定要用。如果包不释放循环跑几十帧后内存和buffer就会耗尽程序卡死或者报buffer分配失败。我早期写的时候漏了这行结果长编测试跑到几百帧就开始报错查了半天才发现是包泄漏。5. H.264编码实测从YUV到H.264文件5.1 用mpi_enc_test快速验证硬件通路自己写代码之前先用MPP自带的mpi_enc_test把硬件通路跑通能省很多排查时间。这个测试工具的作用就是验证这块RK芯片的H.264硬编到底能不能正常工作。我这次按下面的命令跑了一个1080p的编码测试./mpi_enc_test -w 1920 -h 1080 -t 7 -n 300 -f 30 -b 4000 \ -i input.yuv -o output.h264参数含义如下参数含义-w / -h输入图像宽高-t编码格式7为H.2648为H.265-n编码帧数-f帧率-b码率单位一般是kbps-i输入YUV文件路径-o输出H.264码流路径不同release版本的参数可能有小差异运行前建议先./mpi_enc_test --help扫一眼。输入YUV文件我这边用的是NV12格式也就是YUV420SP。如果手里的原始素材是I420YUV420P需要先转一下格式否则编码出来的画面色彩会错乱。命令跑完后输出文件output.h264就是600帧的H.264裸流。如果你连YUV测试素材都没有可以先用FFmpeg在PC上生成一个ffmpeg -f lavfi -i testsrc2duration10:size1920x1080:rate30 \ -pix_fmt nv12 -f rawvideo input.yuv这个素材是标准的1080p测试图做编解码验证足够了。5.2 用ffprobe和播放器验证编码结果编码出来的H.264码流不能只凭文件大小没变就认为成功必须验证码流本身可解、参数正确。我习惯分三步检查。第一步用ffprobe看码流信息ffprobe output.h264能正常解析出h264 (High Profile)、1920x1080、30 fps这些信息说明SPS/PPS和帧结构没问题。如果ffprobe直接报错或者识别不了要么码流文件是空的要么SPS/PPS输出时机不对。第二步用FFmpeg软解回YUV对比输入是否有明显异常ffmpeg -i output.h264 -f rawvideo -pix_fmt nv12 decoded.yuv软解能出画面说明码流本身语法正确。如果你再拿解码后的YUV和原始YUV做逐帧比对比如用PSNR工具就能量化编码损失这个在调试码率配置时特别好用。第三步直接播放ffplay output.h264或拉到板子上用gst-play-1.0播肉眼确认没有花屏、漂色、画面卡顿。注意播放时要区分硬编码流没问题但播放器用软解解码导致卡顿和码流本身有损坏两种情况不要混在一起判断。5.3 各家MPP名字撞车别搞混看到网上有讨论全志音视频的mpp是仿海思的吗这里顺带说清楚。瑞芯微有MPP全志也有叫MPP的中间件海思平台同样有MPP。名字撞车是因为媒体处理平台这个英文缩写太通用大家都不约而同用了这个叫法但三个平台的代码实现、API、底层硬件寄存器操作完全不一样不存在谁仿谁的问题。实际切换平台时你会感受到接口差异很大。瑞芯微MPP的编码入口是encode_put_frame/encode_get_packet全志的方案走MPI_VENC_SendFrame、MPI_VENC_GetStream海思则是VENC_SendFrame、VENC_QueryStream这一套。思路大致相通的都是送一帧原始图像拿一个编码后的码流包但具体参数结构、内存管理方式都不同。熟悉了RK MPP之后转其他平台并不难难的反而是内存管理和码流包的生命周期这层这些通用经验在任何平台都吃得开。6. 常见问题排查与避坑手册6.1 mpp解码失败这类问题怎么排查网上搜MPP相关问题mpp解码失败是出现频率最高的一类。实际上大部分解码失败并不是VPU坏了而是喂给解码器的数据或参数不对。我把最常遇到的几个原因整理成一张排查表你可以直接对照查。现象常见原因排查方向解码器初始化就失败码流格式和MPP_CMD_SET_CODEC_TYPE不一致确认H.264用MPP_VIDEO_CodingAVCH.265用MPP_VIDEO_CodingHEVC解码中途报错码流从非关键帧开始喂入确保视频源从I帧开始或等待第一个IDR再送解码器花屏/绿屏输入YUV格式或stride不对检查prep:format是否是NV12stride是否按16对齐输出黑屏显示格式不匹配解码输出可能是NV12显示层可能需要ARGB需用RGA转格式内存分配失败内核ION/DMA-BUF节点缺失查/dev/ion、/dev/dma_heap/system是否存在首帧延迟很高解码器buffer数量不足调大MPP_DEC_GET_BUFFER_GROUP相关buffer数或用流控模式我碰到过一次很典型的问题从网络摄像头拉流拉到一半突然断流重启解码后一直失败。后来发现是恢复拉流后没有等待新的IDR帧而是直接把中间的P帧塞给了解码器。VPU没有参考帧自然解不出来。解决方式是拉流端重连后先找关键帧再送解码器或者解码器在出错后自动丢弃数据直到下一个IDR。这个逻辑在写播放器时一定要加上否则弱网环境下大概率解码失败。另外一个容易坑人的点是色彩格式。摄像头直出的数据很多是NV12但有的ISP配置输出NV21或者YV12。你如果按NV12配置送进去解码端又会按NV12解释输出最终画面常常整体偏色或者红色蓝色互换。排查时先确定上游输出的精确pixel format再设置prep:format不要想当然。6.2 RGA、VPU和MPP的配合问题在RK平台做视频链路MPP经常和RGA一起出现。RGA是2D图形加速单元负责格式转换、缩放、旋转、裁剪这些操作。为什么需要它因为VPU和NPU、显示控制器对像素格式的要求往往不一致。摄像头ISP输出的可能是NV12算法部门要RGB输入显示层要ARGB这些转换如果都用CPU做跑1080p就够CPU喝一壶了更别说4K多路。我在实际编码链路里常用的做法是ISP输出NV12给MPP硬编同时用RGA把NV12转成RGB给NPU做检测。RGA做NV12到RGB转换的性能比CPU逐像素转换快一个量级而且不占用VPU资源。MPP和RGA在RK生态里是并列的硬件加速单元经常成对出现所以看RK官方文档时常常是MPPRGA放一起讲。如果你发现编码前需要把RGB数据转成NV12建议直接用RGA而不是软件转换。librga的接口大致是rga_import或RgaBlit管理rga_info_t结构指定源缓冲和目标缓冲的格式、stride、裁剪区域调用后由RGA硬件完成转换。相比用CPU转换4K分辨率下差异非常明显。当然前提是你的系统里要有rga驱动RK3588这类芯片基本都带了。6.3 多点实操心得性能调优和后续扩展MPP硬编的性能和稳定性很大程度上取决于参数细节而不是芯片本身。以下是我这次测试下来比较实用的几个心得。第一个是GOP和关键帧间隔的设置。直播场景下GOP不宜过大一般fps*2或fps*4比较稳妥。GOP过大虽然能省码率但丢包后要等很久才能恢复。我习惯配合RTSP推流时用fps*2配合本地存储时用fps*4。第二个是profile和level的匹配。H.264的High profile压缩率更高但一些老设备只支持Baseline或Main。如果解码端是老旧播放器或低端芯片先把profile降到Main试试。level设置也要匹配分辨率帧率比如1080p30大概对应Level 4.04K就需要Level 5.1以上。设置过低轻则编码器报错重则码流在低端解码器上打不开。第三个是IDR帧SPS/PPS输出。前文提过用MPP_ENC_HEADER_MODE_EACH_IDR这里再强调一遍凡是需要码流被seek、切片、录制成MP4的场景都要保证每个IDR都带SPS/PPS。否则很多播放器在非零起点播放时会黑屏问题非常隐蔽。第四个是内存拷贝问题。如果应用层把数据从普通内存复制到ION buffer再送给MPP会引入不必要的拷贝开销。更高效的方式是直接在分配好的MppBuffer上写数据或者用带fd的buffer做零拷贝。尤其4K编码时一帧就8MB多的数据量多一次的memcpy对帧率影响非常明显。后续如果想扩展可以考虑几个方向。一是把output.h264封装成MP4或TSMPP不负责封装需要接FFmpeg的muxer或自写一小段解复用。二是把编码数据通过RTSP/RTP推出去这就涉及RTP打包H.264时的SPS/PPS处理建议参考FFmpeg的rtpenc_h264实现。三是多路编码并行RK3588等芯片支持多路1080p硬编但需要注意VPU会话数量、buffer分组和线程模型避免多个线程共用一个MppCtx那样会出现不可预期的串扰。四是配合RGA、NPU做完整的智能分析链路让视频推流、检测、编码同时工作这也是RK平台最典型的落地形态。最后一个小技巧编出来的H.264裸流我建议你养成用ffprobe快速验证的习惯。硬编不是每次都能一次成功跑完命令行立刻看码流信息能省下大量猜测时间。条件允许的话把软解回YUV和原始帧做一次差分检查比如算一下平均PSNR这样你对当前码率下画质损失心里有数。这比光看文件大小靠谱得多。MPP这套东西耐住性子把底层流程捋一遍后面再做解码、对接GStreamer、多路并发都会顺很多。
网站建设高端定制企业官网