ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Rockchip MPP H.264硬件编码实战:从交叉编译到调优

Rockchip MPP H.264硬件编码实战:从交叉编译到调优 做RK平台音视频开发绕不开MPP。这块Rockchip的媒体处理平台几乎包办了H.264/H.265编解码、转码这类重体力活。我最早在RK3588上调编码功能时最头疼的不是API文档而是从源码编译环境到编码测试的整套链路里到处是文档没写的坑。这篇博文就按我实际走过的流程来写源码获取、交叉编译、H.264编码测试再到参数调优和常见问题排查每一步都尽量说清楚为什么这么做、踩到坑怎么定位。适合读这篇文章的是手里有一块RK系列板子RK3568、RK3588等、想在Linux系统上跑通硬件编码的朋友也适合刚做嵌入式多媒体项目、对MPP编程模型还比较陌生的同学。你不需要很资深但要有Linux命令行基础因为整个过程基本都在终端里完成。1. 先从生态说起Rockchip MPP在音视频链路里到底扮演什么角色1.1 VPU硬件与MPP软件框架的分工先看前提。SoC里的VPUVideo Processing Unit是一块专门的硬件干的就是编解码这种重计算活。在ARM SoC上软编码1080p30的H.264一颗高负载的Cortex-A53核心基本就满载了如果同时还要跑图像算法、跑网络传输CPU早就垮了。硬件VPU做的事情就是把这些像素级运算全部下沉到专用电路里CPU只负责喂数据、取结果。但硬件不会直接变成API。寄存器怎么配、DMA怎么搬、多路任务怎么排队这些逻辑如果完全暴露给应用层写驱动的会很快乐写应用的会骂人。MPP就是夹在中间的软件层全称Media Process Platform是Rockchip对自家VPU的软件封装。它提供一套统一的MPI接口MPP Interface把内部buffer管理、任务调度、编解码状态机都收纳进去上层不需要关心具体芯片的寄存器差异同一套代码可以在RK3399、RK3568、RK3588上跑通。这个思路和很多人的直觉不太一样。你会以为MPP只是个驱动——其实它更像一个小型中间件对外有稳定的C接口对内管理着buffer池、任务队列和硬件同步。理解这一点后面编译、调优、排查才有方向。值得一提的这种软硬分工并不是RK独有的设计思路像ESP32-P4这类带硬件H.264编码器的MCU上层软件框架也会抽象出一层媒体服务只是规模比RK MPP小得多。1.2 海思、全志、瑞芯微的MPP生态对比如果之前做过海思平台的开发对MPP这个词一定不陌生。海思的HiMPP把编解码能力抽象成VENC、VDEC模块接口确实稳定但整个软件栈是闭源的代码出问题基本只能靠文档和脑补。全志的方案习惯叫veVideo Engine接口密度和文档质量一度被开发者吐槽。最近总有人问全志音视频的MPP是不是仿海思的——架构思路上各家都意识到需要一个独立于硬件的媒体框架但具体实现和对外接口差别很大谈不上谁仿谁。RK的rockchip_mpp最明显的优势是源码开放GitHub上就能看到完整实现。这意味着排查问题时可以一直查到硬件抽象层之上的第一行代码不需要在厂商文档没说的地方停住。对于私有协议、特殊需求的改造来说这几乎是决定性的。同时Rockchip这些年社区活跃度不错新芯片出来没多久MPP仓库就会跟上适配版本也多。所以我的结论很简单如果项目选型锁定了RK芯片那么MPP不只是一个可用的库它是你在这个平台上做任何音视频功能都必须学会使用的地基。理解了它和VPU硬件的关系、和同类方案的区别后面所有操作才谈得上心中有数。2. 编译前的关键决策源码版本、编译方式和硬件能力摸底2.1 源码从哪来SDK自带版与GitHub主线版第一个要做的决策竟然不是编译命令而是用哪一份源码。很多RK开发板出厂SDK里其实已经带了MPP路径通常在external/rockchip/mpp或者类似的目录。如果SDK里有我强烈建议先用它。原因很简单SDK里的版本和板子内核、drm驱动版本是配套测试过的踩坑成本最低。如果SDK里没有那就去GitHub拉rockchip-linux/mpp仓库。这里有一个建议不要直接抓master而是挑一个tag。MPP项目更新频繁master上可能引入了还没来得及回归的新特性但板子老内核不一定能配合容易出现莫名其妙的ioctl错误。挑tag时优先用release标记或者文档里推荐的版本。拿到源码后先看一眼README和CHANGELOG确认当前版本对目标芯片的支持状态这比蒙头编译省时间得多。2.2 交叉编译还是板上编译场景对应选择MPP在Linux用户态编译理论上分两种方式在板子上直接编译或者在PC上交叉编译。怎么选看你的工作流。方式优点缺点适用场景板上编译环境天然匹配、不需要维护工具链板子性能有限、占用运行环境、容易缺依赖快速跑通验证、小工程调试交叉编译编译快、可批量集成进BSP/Yocto工具链配置要自己维护、版本必须严格匹配正式项目、多板卡适配我自己的习惯是正式项目一律交叉编译把MPP的编译流程写进SDK层的构建脚本里保证任何同事checkout之后都能一键产出固件。如果刚拿到板子、想快速确认编码功能是否正常那在板子上直接cmake make也完全没问题——MPP的依赖不多只要有网络和编译环境就行。这里顺带说一句源码编译这种事本质就是依赖关系和工具链匹配。我编译过的项目不算少从redis到llama.cpp套路都差不多难点从来不是敲那几条命令而是搞清楚谁依赖谁、版本怎么对齐。MPP算是CMake工程里比较规整的先把构建系统理清楚后面都是顺水推舟的事。2.3 安装交叉工具链与确认硬件编码能力交叉编译需要先在PC上安装aarch64工具链sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu这里记住一件事MPP是C和C混合项目只装gcc不装g链接阶段会报一堆找不到标准库符号的错误。另外某些平台上MPP的buffer管理会直接和DRM子系统打交道编译头文件可能需要libdrm如果cmake报缺drm.h先装libdrm-dev。硬件能力摸底同样重要。编译完成后的mpp_info工具会列出当前芯片的编码、解码能力比如支持哪些格式、最大分辨率、最大帧率。不同芯片差异很大RK3588的VPU支持到8K H.264/H.265编码RK3568通常到4K早期RK3399的H.265编码则不一定在支持列表里。这类信息直接影响产品的分辨率选型别等写完了应用才发现硬件根本不支持。3. 编译全流程实录CMake配置、工具链文件与产物部署3.1 仓库结构速览一眼看懂MPP项目怎么组织拿到源码后别急着敲命令先花十分钟把目录结构看一遍。MPP仓库的核心目录大致是这样inc/对外暴露的头文件上层应用只需要包含这里的头文件。mpi/MPI接口层实现MppApi结构体里的具体函数是衔接用户调用和内部框架的入口。mpp/MPP核心框架任务调度、buffer池、编解码状态机都在这里实现。osal/操作系统抽象层负责线程、互斥锁、内存映射等系统调用的封装。test/大量测试用例最有价值的是mpi_enc_test.c和mpi_dec_test.c。tools/辅助工具包括查询芯片能力的mpp_info。第一次接触MPP我推荐的阅读顺序是先看test/mpi_enc_test.c它告诉你一个编码器是怎么被创建、配置、循环取数的然后按图索骥跳到mpi/里看具体实现最后再看mpp/里的调度细节。不要一上来就啃核心框架容易被各种buffer概念绕晕。3.2 CMake交叉编译命令与toolchain文件MPP使用CMake构建从根目录的CMakeLists.txt就能确定。交叉编译时我的典型命令是这样cd mpp mkdir build cd build cmake .. \ -DCMAKE_TOOLCHAIN_FILE../cmake/toolchain_linux_arm64.cmake \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_INSTALL_PREFIX$PWD/install \ -DBUILD_TESTON make -j$(nproc) make install其中toolchain_linux_arm64.cmake在仓库的cmake/目录下应该能找到。如果仓库版本里没有对应文件也可以自己写一份核心内容就是告诉CMake用哪套交叉编译器set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g)BUILD_TESTON这个开关非常关键。很多人只编了库没有编测试工具等想验证编码功能时发现没有mpi_enc_test还得回头重新编。既然要做编码测试测试工具必须带上。CMAKE_INSTALL_PREFIX建议设置成容易找的绝对路径别让文件散落到系统目录里后面拷贝部署会方便很多。3.3 安装目录解读与板端部署编译安装完成后install目录下会看到三个关键区域include/包含mpp.h、mpp_buffer.h、mpp_frame.h等头文件。lib/librockchip_mpp.so等共享库注意它带版本号安装目录里会生成对应的符号链接。bin/mpi_enc_test、mpi_dec_test、mpp_info等测试工具。部署到板子时把lib拷贝到/usr/lib或/usr/local/lib头文件拷贝到/usr/include测试工具拷贝到/usr/bin然后执行ldconfig刷新动态库缓存。这里有个很实用的小检查在板子上运行ldd /usr/bin/mpi_enc_test确认所有依赖库都能找得到。如果有库路径不对的问题优先检查LD_LIBRARY_PATH但正规做法还是把库放到系统搜索路径里并ldconfig。这一步最容易出问题的就是动态库版本不匹配。如果板子上原来就有一份旧版librockchip_mpp.so你新拷进去的版本可能被系统当作同一个库但其他组件仍然按旧接口调用运行时会炸得很隐蔽。所以部署完成后我建议用strings librockchip_mpp.so | grep MPP_VERSION这类方式确认当前加载的库版本。4. H.264编码测试从YUV输入到H.264码流输出4.1 准备一份合格的YUV420SP输入数据编译完成后第一件事不是看代码而是准备测试数据。MPP的H.264编码器接收原始视频帧通常是YUV格式。最常用的是NV12也就是YUV420SP一个平面的Y数据加交错排列的UV数据。分辨率1920x1080的话一帧大小是1920 * 1080 * 3 / 2字节约3.1MB。生成YUV裸流最方便的方式是用ffmpegffmpeg -i input.mp4 -pix_fmt nv12 -s 1920x1080 -r 30 -t 5 -frames:v 150 input.yuv有几个细节要提前说明。第一分辨率尽量选偶数对齐的值H.264宏块尺寸是16x16虽然编码器内部会处理边界但奇数宽度会带来不必要的对齐计算和兼容风险。第二NV12的stride并不总是等于宽度尤其当宽度不是16的倍数时。MPP编码器内部能容忍一部分stride与width不匹配但你在测试阶段尽量让它们一致能少很多麻烦。第三测试文件给150帧左右足够既能验证编码流程稳定又不至于让测试时间过长。文件太大时中途发现问题改参数重来一次白白浪费的是自己的时间。4.2 mpi_enc_test命令行跑通一个最小用例准备好了YUV数据就可以跑编码测试了。一条典型的命令长这样./mpi_enc_test \ -w 1920 -h 1080 \ -t 7 -f 30 -g 30 \ -n 150 \ -o enc_1080p30.h264跑之前先执行./mpi_enc_test -h看一下参数帮助不同版本之间字段可能略有调整以你手里的版本为准。各参数含义如下参数作用备注-w/-h输入图像的宽高必须和输入YUV的分辨率一致-t编解码器类型这里7对应H.264/AVC具体值以头文件MPP_VIDEO_Coding枚举为准-f帧率编码输出的帧率设定-gGOP大小两个关键帧之间的帧数GOP越大压缩率高但随机访问性差-n编码总帧数测试时控制编码时长-o输出H.264文件路径编码结果写入这个文件跑完后终端会打印每帧编码耗时、码率等统计信息。如果一切正常会得到一个能直接打开的H.264裸流文件。如果中途报错别急着怀疑MPP先检查YUV文件的格式和尺寸这两点出问题的概率远高于代码本身。4.3 回到代码MPI编码接口的调用骨架命令行的mpi_enc_test虽然能解决能不能编的问题但真正要集成到自己的项目里还是要把MPI接口的使用逻辑理清楚。编码流程的核心骨架大概是这样MppCtx ctx; MppApi *mpi; mpp_create(ctx, mpi); mpi-control(ctx, MPP_CTX_SET_VIDEO_ENCODE_TYPE, enc_type); mpp_init(ctx, MPP_CTX_ENC, MPP_VIDEO_CodingAVC); // 配置编码参数 MppEncCfg cfg; 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: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:fps, fps); mpp_enc_cfg_set_s32(cfg, gop:len, gop); mpi-control(ctx, MPP_ENC_SET_CFG, cfg); // 编码主循环 for (i 0; i frame_num; i) { // 读取一帧YUV到输入packet mpp_packet_init_with_buffer(pkt, input_buf, frame_size); mpp_packet_write(pkt, yuv_data, frame_size); mpi-encode_put_packet(ctx, pkt); // 送入编码器 mpi-encode_get_frame(ctx, frame); // 从编码器取回结果 if (frame) { MppPacket packet mpp_frame_get_packet(frame); mpp_packet_read(packet, out_ptr, out_len); // 把out_ptr写入输出文件 } mpp_packet_deinit(pkt); } mpp_destroy(ctx);核心循环只有两个动作送原始帧进去、取码流出来。但这里有个非常重要的理解点编码器不是逐帧同步输出的。它内部有GOP结构、码率控制和参考帧管理所以encode_get_frame可能连续几次返回空然后在某次返回一帧或几帧数据。你不能假定第N帧输入就对应第N帧输出尤其开启B帧之后输出顺序还会重排。所以测试程序里通常要循环调用encode_get_frame直到拿空把所有产出数据都写到文件里。新版MPP还提供了基于mpp_task_queue_get和mpp_task_queue_put的dequeue模式把输入和输出抽象成任务对象更适合多线程场景。第一次入门时先把上面这套老接口跑通理解再深入一层后自然能看懂新接口解决的是什么问题。4.4 验证H.264码流的几个手段编码测试跑完先别高兴太早码流生成了不代表正确了。我通常用金字塔式的三层方法验证。第一层用ffprobe看基本属性ffprobe enc_1080p30.h264能读到H.264、分辨率、帧率、profile这些信息说明SPS/PPS等关键参数已经正确写入。第二层用ffplay或其它播放器试播ffplay enc_1080p30.h264播放器能正常出画面只是入门项重点要看有没有绿屏、花屏、跳帧。画面正常说明码流结构基本没问题。第三层看文件大小估算实际码率特别是你设置了固定码率CBR时。比如150帧、30fps、4Mbps的期望输出大小约是4 * 1000000 * 5 / 8 2.5MB如果文件大小偏差超过10%就要回头检查码率控制配置。第四层用MPP自己的解码工具回环验证./mpi_dec_test -i enc_1080p30.h264 -t 7 -o decoded.yuv然后对比解码出来的YUV和原始YUV的差异。这个验证是闭环的能从编解码两个方向同时暴露问题。5. 编码参数调优与典型问题排查5.1 码率控制、GOP、profile/level该怎么选跑通测试之后就该考虑实际应用了。编码参数的选择直接影响画质、码率和延迟这三者的权衡是嵌入式视频项目最核心的取舍。码率控制方面MPP支持三种常用模式模式行为适用场景CBR固定码率码率恒定画面复杂时主动降质量网络传输带宽受限、需要稳定码率的监控场景VBR可变码率码率随画面复杂度波动平均画质更好本地存储、带宽波动可容忍的场景FIXQP固定量化参数每个P帧QP固定代码简单画质优先的调试场景GOP大小决定关键帧间隔。GOP越大I帧越少相同码率下画质越好但GOP越大随机访问和丢包恢复越困难。视频监控场景常用GOP等于2倍帧率也就是每秒至少一个I帧。profile和level要和分辨率匹配。Baseline profile没有B帧、解码要求低、延迟低High profile压缩率最高但解码器要求也高。常见的1080p监控可以直接用High profile、level 4.0以上。如果做的是实时通话、无人机图传这类延迟敏感场景我建议关掉B帧GOP调到30以内甚至用Baseline profile换稳定性。画质那一点损失远没有半秒延迟的体验伤害大。5.2 编码前的必经之路RGA格式转换与缩放实际项目里MPP编码器很少直接吃干净的NV12数据。摄像头ISP输出的可能是RAW屏幕抓帧出来是BGRA图像拼接模块给过来的是另一个分辨率。这时就需要RGA介入。RGA是Rockchip的2D硬件加速器负责颜色空间转换、缩放、旋转这些工作它和MPP是RK多媒体方案里的固定搭档。使用RGA时流程一般是把源buffer和目标buffer映射进来设定源格式、目标格式和输出尺寸调用一次RGA操作拿到转换后的NV12数据再直接喂给MPP编码器。整个过程中CPU只负责配置参数像素搬运全部由硬件完成。代码的思路大致是这样// 伪代码以实际librga头文件为准 IMHandle handle im2d_open(); imcvtcolor(src_buf, dst_buf, SRC_FORMAT_BGRA, DST_FORMAT_NV12); imresize(src_buf, dst_buf, src_w, src_h, dst_w, dst_h); im2d_close(handle);很多mpp编码失败或编码图像颜色不对的案例最后定位到的问题根本不是MPP而是RGA这步的格式参数没配对或者buffer大小没按目标格式计算。编码之前多花一分钟检查送给MPP的buffer格式能省下后面两小时排查。5.3 mpp解码失败的排查链路mpp解码失败大概是RK音视频开发群里出现频率最高的句子了。但这句话背后可能是完全不同的几个问题我的排查顺序一定是先分阶段再定位。明确是编译失败还是运行失败。编译失败先查工具链、头文件路径、库链接顺序运行失败再往下走。看错误输出发生在哪个阶段。MPP初始化、buffer申请、喂数据、取数据不同阶段对应的问题完全不同。输入数据排查。格式是不是NV12或I420尺寸和stride对不对数据长度是不是超过了buffer容量这一步能定位掉至少一半的问题。我见过有人拿RGB文件当NV12喂进去解码出来满屏绿色还以为是解码器bug。输出侧排查。如果你在自研代码检查buffer释放时机有没有问题。MPP的packet/frame引用计数模型如果没理解透彻最容易出现偶尔成功、偶尔花屏的灵异问题。硬件资源排查。VPU在多路并发时不会自动排队同时跑的解码路数超过硬件能力就会出现超时。如果板子上还有其他程序在占用VPU测试结果就会不稳定。开调试信息。MPP代码里埋了不少调试打印把调试级别调高或者直接查日志里的错误码字段往往能直接看到是哪一步返回了非零值。我通常会在自己的代码里把每次MPI调用的返回值都打印出来排查时一翻日志就知道问题在哪一层。这里分享一个我经历过的实际案例。有一次做多路H.264解码输出画面每隔几秒花屏一次而且出现的位置很随机。一开始怀疑是网络丢包各种加校验都无效。后来打开调试打印发现是输入数据buffer没有在解码开始前完成所有数据填充导致其中一路解码器拿到的是半截码流后面的帧全部错位。把每一帧数据完整写入packet之后再去dequeue任务这个顺序修正问题立刻消失。这种问题靠看代码不如靠打日志定位快。5.4 多路编码与系统集成时容易被忽略的点多路编码场景下第一原则是每一路编码器独立创建MppCtx不要共享上下文除非你非常清楚MPP内部的线程模型。同时要提前规划buffer内存1080p的NV12帧约3.1MB多路并发时内存会迅速膨胀建议用MPP的buffer池而不是每帧都新申请释放。系统集成时容易栽的坑还有几个。链接动态库时-lrockchip_mpp要放在源文件后面这是gcc链接顺序的老问题。拷贝库到板子后第一件事就是检查ldd看有没有抓到系统里旧版本的MPP库。如果板子kernel版本和编译环境不一致注意MPP与内核的ioctl兼容性能同版本就同版本不能的话尽量选release包配套的版本。另外一个常被忽视的点是很多应用希望把MPP封装成daemon或后台服务。这时要特别注意mpp_destroy的资源回收以及编码线程被异常终止时buffer的泄漏。嵌入式产品跑几天后内存越来越小的诡异问题很多都出在这里。6. 最后再分享几句掏心窝的话如果你第一次接触MPP我的建议是别一上来就写业务代码先花一个下午把test/mpi_enc_test.c完整读一遍然后在上面改参数改成自己需要的分辨率、帧率、GOP跑通之后再开始写自己的封装。这个过程基本能覆盖MPP的所有核心概念之后再上手其他接口解码、转码、拼接就是举一反三的事。遇到问题时我的处理顺序永远是先查输入数据再查buffer生命周期最后才怀疑MPP本身。因为绝大多数问题都不是MPP的bug而是使用姿势不对。真到了怀疑库的时候也有一个办法用GitHub上最新tag的MPP重新编译一次如果问题消失多半是你用的版本太老、跟内核不匹配如果问题还在那才轮到去提issue。另外再补一句经验延迟敏感的项目编码延迟从来不是MPP单独决定的。GOP、B帧、buffer数量、线程优先级每一个环节都可能是瓶颈。先把mpi_enc_test这条线跑稳后续优化才有讨论基础。
返回列表