
去年做工业视觉项目时我遇到一个很尴尬的问题网口相机默认输出MJPEG流2592x1944分辨率、30帧CPU软解要占将近80%的负载直接把算法线程的资源全挤没了。同事建议我用瑞芯微平台的MPP框架做硬件解码说VPU一直闲着。但真正上手后才发现MPP的官方示例几乎全都围绕H.264和H.265MJPEG硬件解码这条路几乎没有现成参考网上能找到的信息也是零散的。折腾了一周总算把整条链路跑通——从RTP码流组帧到MPP硬解输出YUV再送RGA做缩放与格式转换CPU占用直接压到5%以内。这篇文章就把这条链路彻底拆开讲一遍给正在踩同样坑的朋友一个完整参考。适用读者是那些做嵌入式图像采集、网络摄像头接入、安防监控、工业视觉并且在RK3568、RK3588、RV1106等平台上做视频流的团队。文章重点是MJPEG硬解码怎么在MPP框架里跑起来、怎么排查“解码失败”这类高频问题以及解码之后如何用RGA把YUV变成真正能用的画面。1. 先说清楚MPP的多媒体处理框架与MJPEG硬解的行业定位MPPMedia Process Platform是瑞芯微提供的用户态多媒体处理库屏蔽了底层VPU的视频编解码能力对外暴露一套统一的MPIMPP InterfaceAPI。它的核心价值在于你不需要关心硬件寄存器、命令队列、驱动交互这些细节只要按MPI的约定把码流送进去再从输出端取解码结果就行。可以把它理解成一个调度平台——VPU硬件是那个实际干活的车间MPP就是帮你安排车间调度和物料进出的中间人。MJPEGMotion JPEG这套格式本身非常简单本质是一系列独立的JPEG静态图片连续播放。每一帧之间没有参考关系编码简单定位精确所以在网络摄像头和工业相机上非常常见。但正因为帧内独立编码它的码流比H.264、H.265这类帧间压缩格式大得多一帧动不动就几百KB甚至几MB如果走软解CPU必定被拖垮。这也是我在项目中选硬解的最直接原因。在MPP框架内MJPEG解码属于VDEC视频解码器模块整体流程和H.264/HEVC解码相似但有几个关键差异没有参考帧管理器。H.264/HEVC需要维护DPB解码图像缓冲用来存放参考帧、管理帧输出顺序MJPEG完全不需要这套机制API使用层面更简单对码流的完整性要求非常苛刻。MJPEG的一帧必须是完整的JPEG结构从SOI标记FFD8到EOI标记FFD9中间任何数据缺失都可能导致解码失败对分辨率变化的容忍度低。H.264流中间分辨率变化时解码器会自适应重建缓冲而MJPEG如果中途分辨率突变输出缓冲配置可能跟不上表现为抽帧、绿屏甚至解码器停滞。1.1 “芯片支持MJPEG硬解”不等于“默认就能跑通”这一点很重要芯片有MJPEG硬解能力只是说明VPU硬件具备了条件不代表你在用户态随便调几个API就能出结果。MPP的解码链路牵涉到码流分包、解码器配置、输出缓冲管理、CMA内存可用性等多个环节任何一环出问题最终表现都是同一句话——mpp解码失败。想确认当前环境是否能解MJPEG最快的方式是跑MPP官方自带测试程序mpi_dec_test。它支持指定输入码流类型把MJPEG文件输入解码后输出YUV文件。这个程序如果跑通了说明驱动、内核、MPP框架的环境基本没问题问题大概率出在你的应用层组包逻辑上如果它都跑不通先别查业务代码先去查驱动加载情况、设备树配置和CMA内存大小。这里有一个容易被忽略的点MPP源码版本差异很大Rockchip官方仓库的最新版本和各家Board SDK内置的版本可能完全不同接口行为、日志格式、支持的硬件范围都不一致。建议先确认SDK里MPP的版本号和源码路径再查阅对应版本的API。直接从网上复制老代码在新SDK上编译报错或行为奇葩都是家常便饭。1.2 为什么MJPEG值得单独讲而不是跟H.264混在一起很多人会问既然MPP解码器API统一H.264怎么解MJPEG就怎么解还用单独研究吗实际操作后你会发现差异集中在三个地方第一输入包的组织方式不同。H.264流通常是一个连续流MPP内部自己去寻找NALU边界MJPEG的每一帧应该是一个完整的JPEG数据单元MPP对边界的识别依赖SOI/EOI标记如果输入流没有按这些标记切分解码器就可能把两个半帧拼在一起导致输出花屏或失败。第二缓冲策略不同。H.264的参考帧需要保存很久MPP的帧缓冲池做得比较深MJPEG一帧解完就算理论上缓冲需求小但大分辨率MJPEG的中间解码缓冲反而惊人CMA内存不够的时候最典型的表现就是大帧解不出来、小帧正常。第三返回帧的方式不同。MJPEG的解码输出帧率基本就是输入帧率帧顺序和输入顺序一一对应没有重排、没有B帧延迟而H.264可能存在B帧重排取帧顺序和输入顺序不一致。这个差异在同步显示时会让很多从H.264迁移过来的代码踩坑——如果你用H.264的PTS重排逻辑来处理MJPEG反而会引入不必要的延迟。2. 跑通一条最小MJPEG硬解链路核心MPI调用流程这一章直接用代码和流程来讲MPI调用MJPEG解码的完整路径。2.1 创建解码器类型选错后面全部白费第一步是创建MPP上下文并初始化解码器MppCtx ctx; MppApi *mpi; mpp_create(ctx, mpi); MPP_RET ret mpp_init(ctx, MPP_CTX_DEC, MPP_VIDEO_CodingMJPEG); if (ret ! MPP_OK) { mpp_destroy(ctx); return ret; }这里有三个关键点第一个参数MppCtx代表一个独立的解码实例多个通道就创建多个实例互不干扰。第二个参数MPP_CTX_DEC表示创建的是解码上下文对应解码方向。如果想做编码这里是MPP_CTX_ENC。第三个参数是MJPEG解码是否成功的关键——MPP_VIDEO_CodingMJPEG。这里一旦填错比如填成H.264对应的枚举解码器会用H.264的分包逻辑去解析JPEG码流后面的每一步都会出错而且错误日志不一定很直观。所以新建解码器的时候第一件事就是确认这个类型枚举值与码流真实编码类型一致。初始化完成后建议再做一个额外配置MppDecCfg cfg; mpp_dec_cfg_init(cfg); mpp_dec_cfg_set_u32(cfg, split_parse, 1); mpi-control(ctx, MPP_DEC_SET_CFG, cfg);split_parse这个参数的作用是让MPP内部对输入码流做拆分和解析。对于MJPEG这种一帧一个完整JPEG的格式打开这个参数后MPP可以尝试从输入缓存中自己寻找SOI/EOI标记来切帧。不过需要注意不同MPP版本对这个参数的行为处理不完全一致有的版本即使打开了也要求你送数据时尽量按帧对齐不能把帧边界打散。2.2 送包与取帧一进一出就是解码器的全部MPP的编解码模型非常清晰输入用MppPacket描述一段码流输出用MppFrame描述一帧图像。送包调用decode_put_packet取帧调用decode_get_frame。先看送包环节。需要把一帧完整的JPEG数据放进MppPacketMppPacket packet NULL; mpp_packet_init_with_buffer(packet, input_buf); mpp_packet_set_length(packet, jpeg_size); mpp_packet_set_pts(packet, pts); mpi-decode_put_packet(ctx, packet); mpp_packet_deinit(packet);input_buf是一个MppBuffer。从网络或文件里读到的完整JPEG字节可以通过mpp_buffer_get_ptr拿到MppBuffer内存指针后memcpy进去也可以走dma_buf方式直接传递物理内存。这里有一个工程上非常重要的习惯不要在送帧循环里频繁分配和释放MppBuffer。最好在启动时分配一个输入缓冲池循环复用否则不断get/put buffer会导致内存碎片和性能抖动时间一长还会出现“解码器越来越慢”的假象。再看取帧环节。这是新手最容易写错的地方很多人会写成高频率轮询MppFrame frame NULL; mpi-decode_get_frame(ctx, frame); if (frame) { MppBuffer buf mpp_frame_get_buffer(frame); void *data mpp_buffer_get_ptr(buf); int width mpp_frame_get_width(frame); int height mpp_frame_get_height(frame); // 这里处理YUV数据 mpp_frame_deinit(frame); }取出MppFrame后要用mpp_frame_get_width和mpp_frame_get_height获取解码出来的宽高。注意这里和mpp_frame_get_hor_stride、mpp_frame_get_ver_stride不同——width/height是实际图像尺寸hor_stride/ver_stride是内存对齐后的跨距。做缩放、拷贝、RGA转换时如果直接把width当stride用大概率会出现画面偏色、带斜条的花屏现象。可以这样类比width/height像是房间实际使用面积stride像是包含墙体的建筑面积你按照使用面积去走访一栋按建筑面积布局的建筑自然每一步都会撞墙。2.3 解码器释放也有讲究reset才是干净的收尾解码结束或切换通道时需要这样收尾mpi-reset(ctx); mpp_destroy(ctx);reset的作用是让MPP把内部缓存、正在处理的包和帧全部清空避免上一个会话的残留数据污染下一个会话。每次反复开关视频通道时这个操作都会触发。如果不做reset直接destroy某些MPP版本下会出现资源无法释放的现象最终表现为“跑几天之后解码器打不开”重启应用都没用只能重启系统。2.4 想彻底搞懂MPI调用链去看官方mpi_dec_test源码如果看完这几个API片段还是觉得云里雾里强烈建议打开MPP源码目录下的test/mpi_dec_test.c读一遍。它相当于官方给的“标准答案”把创建上下文、配置解码器、送包、取帧、收尾每一步都放在一个文件里。我看懂这个文件对MPP的认知才算真正建立起来后面写业务代码时才不会再像无头苍蝇一样试API。我建议照着这个源码改一版属于自己的“MJPEG单帧解码器”输入一张JPEG图片输出一个YUV文件。把这个最小demo跑通后再往里面加网络组帧、多通道和RGA每一步都有据可查出问题能快速定位到具体环节。3. 解码之后别停在这里RGA缩放与格式转换才是真正的生产环境硬解出来的数据通常是NV12YUV420SP但这往往不是业务需要的最终格式。显示框架要的可能是一张BGR888图像视觉算法要OpenCV的Mat窗口是1920x1080但相机原始分辨率是500万像素这些都需要格式转换和缩放。如果这些处理用CPU来做相当于又回到了软解的老路整体性能依旧上不去。所以RGARockchip的2D图形加速硬件必须出场。RGA能完成格式转换、缩放、旋转、裁剪等操作硬件加速开销极低。在RK3568上实测2592x1944的NV12转BGR888并缩放到1920x1080耗时大概1到2毫秒CPU占用几乎为零如果用libyuv走NEON优化软转往往要20毫秒以上。MJPEG的解码加速和RGA的后处理叠加才能真正把CPU解放出来。3.1 零拷贝的关键用文件描述符把MPP和RGA接起来MPP解码输出的MppBuffer可以通过mpp_buffer_get_fd拿到文件描述符再传给RGA的importbuffer_fd让RGA直接操作同一块物理内存。这样整条链路从网络收包、到JPEG解码、再到YUV转换全程不经过CPU拷贝。int fd mpp_buffer_get_fd(mpp_frame_buffer); rga_buffer_t src importbuffer_fd(fd, src_buf_info); rga_buffer_t dst importbuffer_fd(out_fd, dst_buf_info); im_rect src_rect {0, 0, width, height}; im_rect dst_rect {0, 0, dst_width, dst_height}; int ret imresize(src, dst, 0, 0, 0, 0, 0, 0, 0, 0, 0);librga的API在不同SDK版本里有差异新版本可能用improcess、imtranslate这类更抽象的函数但基本思路一致导入源和目标buffer、设置裁剪区域、调用处理函数。理解了这个模型具体函数名反而没那么重要查对应SDK的头文件即可。3.2 RGA踩坑stride对齐和格式支持矩阵实际操作中RGA最典型的问题是宽度对齐。新一代RGA对16字节对齐的要求有所放宽但老一代RGA或者某些驱动版本要求宽度必须按16字节对齐做NV12转RGB888时尤其严格。如果你的目标宽度不是16的倍数就要手工填充stride否则会出现一整条色带或者画面偏移。判断是否需要对齐最简单的方式是先查看RGA驱动版本再查看API文档中关于stride的说明。在代码中建议始终从MppFrame读取hor_stride/ver_stride而不是手动计算这样能兼容更多MPP版本。另一个需要注意的点是格式支持矩阵。不是所有格式转换组合RGA都支持比如某些老版本RGA不支持NV15或NV21的硬件转换硬转会返回错误或者输出黑屏。在项目定型前最好把目标平台可能用到的所有转换组合提前验证一遍做一张自己项目的支持矩阵表。3.3 解码RGA的流水线设计思路如果只是单路采集解码然后转换并按帧显示流程是很简单的。一旦进入多路并发场景就要考虑流水线设计。我的习惯做法是每个通道开一个解码线程解码线程负责取帧把MppFrame放进一个定长队列RGA处理线程从队列里取帧执行转换。解码线程和RGA线程之间用帧数做背压控制而不用QoS控制。这样做的好处是解码帧率是相对稳定的而RGA处理速率可能受目标格式和分辨率影响如果直接用阻塞等待某个通道处理慢会拖垮整个进程。背压控制的实现不复杂定长队列满了就丢弃最旧的帧保证解码器始终不会因为消费者处理慢而内部堆积。对监控类场景来说丢旧帧永远比堆积延迟要好。4. “mpp解码失败”的排查实战症状、根因与验证链路这一章集中回应最常遇到的“mpp解码失败”。我会按症状分类来讲这样对照排查起来更快。4.1 症状一解码器超时或无帧输出这是最常见的故障表现送进去的包解不出来decode_get_frame一直超时。原因按概率排序如下码流不完整。MJPEG的一帧必须是完整JPEGSOI开、EOI结。如果按固定字节数读取文件或从网络流中截取数据很容易把两帧拼接在一起输入了progressive JPEG。MPP的MJPEG解码器一般只支持baseline JPEG而一些相机或图像处理库会输出progressive JPEG。这种码流在PC播放器上能正常显示因为软解支持但硬解直接失败。判断方法很简单查看JPEG的SOF标记SOF0FFC0是baselineSOF2FFC2是progressive解码器输入缓冲设置太小。分辨率大、帧尺寸大的时候MPP内部需要比较大的缓冲region如果CMA内存偏小或者被其他模块占用解码器申请不到连续内存就会失败。排查顺序建议是先用mpi_dec_test单独解一帧静态JPEG确认“MPP有没有能力处理这个JPEG”再导出你项目里的同一帧用ffprobe确认它是baseline还是progressive最后检查dmesg里是否有CMA分配失败、IOMMU缺页的记录。4.2 症状二解码输出花屏、偏色、有条纹这类问题往往不是解码器本身解错了而是你处理输出数据的方式不对。常见情况有三种一是用width访问内存但实际上每行数据之间按hor_stride对齐。当height和ver_stride不一致时就会出现一条一条的错位画面看起来像被切碎了二是直接把输出格式写死为NV12但不同MPP版本或不同分辨率下输出格式可能是NV12、NV15或NV21。代码里一旦写死换平台立刻出事三是RGA转换时目标buffer的stride没对齐RGA写入跨距错位导致最终输出偏色或起纹波。遇到这些问题排查方法很简单取到MppFrame后把mpp_frame_get_fmt、mpp_frame_get_hor_stride、mpp_frame_get_ver_stride、mpp_frame_get_width、mpp_frame_get_height这几个值全部打印出来肉眼对比一下“一行有效像素长度”和“一行内存实际长度”立刻能发现是不是stride的问题。4.3 症状三性能不稳定、偶发卡顿如果解码本身没有报错但帧率忽高忽低最可能是VPU资源被抢占。同一个芯片如果同时做硬编码和硬解码或者有多个解码通道并存VPU会按照某种调度策略分配资源瞬时争抢就会导致MJPEG解码帧率波动。一种解决办法是给解码器配置更大的帧缓冲计数。通过MPP_DEC_SET_CFG调整frame_buffer_count默认可能是4到8对于大分辨率的MJPEG建议调到16以上。这样在瞬时资源紧张时解码器能有更多缓冲空间平滑输出。另一种可能的原因是取帧逻辑写成了非阻塞高频率轮询。即使没有帧decode_get_frame也在空转会把CPU占满反过来拖慢整个系统。建议用带等待的取帧方式或者按输入帧率做节奏控制官方mpi_dec_test里就有按帧率休眠的控制逻辑抄过来就行。4.4 设备树、CMA与IOMMU底层配置决定了硬解的天花板很多人在应用层反复调代码最后发现问题在底层配置。结合“瑞芯微rk3568设备树”这个关键词展开说几句。MPP在内核侧依赖VPU相关设备和驱动。虽然不像I2C、GPIO那样经常改设备树但有两个关键配置是通过设备树或内核引导层面体现的CMA内存大小。解码大分辨率MJPEG或做多路解码时内核的CMA区域要足够大。我遇到过默认CMA只有64MB的板子单路4K MJPEG频繁失败把CMA改成256MB后立刻正常IOMMU是否开启。瑞芯微平台通常默认开启IOMMU如果某些内核配置关掉了驱动会退回物理连续内存申请大块连续内存很难保证解码失败概率大增。建议查看dmesg里是否有vpu相关iommu报错同时对比你们板子的设备树与原厂SDK默认设备树的差异。同一个芯片不同载板、不同屏幕、不同内存布局都会影响CMA可用量不能想当然认为原厂能跑你们就能跑。4.5 根治“mpp解码失败”的三件套自查流程把排查经验总结成一套可复用流程用mpi_dec_test跑官方示例先把问题定性是应用层组包问题还是框架/驱动层面的问题把输入JPEG导出来在PC上用软解验证确认码流属于baseline且内容完整打开MPP自身日志和dmesg关注解码器初始化、CMA分配、IOMMU这三个关键节点。按这套流程走下来绝大多数“mpp解码失败”都能定位到具体环节而不是在代码里瞎猜。5. 从RK3568到RK3588、RV1106芯片差异与平台迁移经验在RK3568上验证通过以后通常要考虑平台迁移同一个方案能不能直接上RK3588RV1106这种低功耗IPC SoC行不行我的经验是MPP接口层是统一的上层代码改动很小但必须在项目初期就把解码能力余量算明白因为不同芯片的VPU能力差异非常大。芯片MJPEG解码能力标称与RGA配合的典型场景注意事项RK35684K级可支撑多路1080p预览相机接入、NVR预览、工业视觉大帧建议调大解码缓冲CMA不建议低于128MBRK3588VPU更强8K级解码多路并发更友好多通道监控、视频拼接、后处理librga版本差异较大接口以SDK为准RV1106编码为主解码能力有限适合轻量IPC智能摄像头内的JPEG抓图/回显不要把它当4K MJPEG解算平台低功耗下内存预算有限老平台RK3399/RK3288有VDEC但规格较弱适合小分辨率单路简单视频回显老SDK的RGA是RGA1对齐要求苛刻这张表的意义在于MJPEG码流尺寸非常大同样1080p/30帧H.264可能只要4MbpsMJPEG能到40Mbps以上。这不仅消耗解码器带宽更吃内存总线带宽和存储带宽。选型时不能只看芯片标称支持4K还要把码率换算成系统总线占用率和内存、CPU、外设的带宽需求放在一起评估。5.1 关于“全志、海思、瑞芯微的MPP”是不是一回事网上经常看到“全志mpp”“海思mpp”“瑞芯微mpp”的说法。这里可以明确“MPP”这个缩写在海思那边是Media Process Platform是一整套多媒体处理中间件瑞芯微的MPP也采用类似命名全志的SDK里也有视频编解码相关库但实现思路、API命名和硬件架构都有自己独立的体系。不存在谁“仿”谁的问题而是“硬件视频编解码加速”这个需求催生了几乎所有SoC厂商都设计类似的软硬件分层方案——硬件里放一个专用的编解码引擎软件里封装一套统一API。思路相似是必然的具体代码和框架则各自独立。对开发者来说从海思平台迁到瑞芯微最需要适应的一点是海思MPP更像一个强管理型框架buffer分配、通道绑定、VPSS/VENC流程都帮你规划好瑞芯微MPP更像一套“裸API”集合——它提供create/init/put/get这些原语但业务编排、buffer管理、格式转换链路的实现都由自己搭。这种差异没有好坏之分但直接影响写代码的思维方式。5.2 平台迁移时值得注意的三个细节第一不要轻易升级MPP版本。SDK里自带哪个版本就把哪个版本跑稳定除非有明确的bug修复需求否则不要顺手把MPP源码替换成GitHub上的最新版。解码器的寄存器配置依赖配套内核驱动混合版本很容易翻车。第二提前做好stride和格式的动态适配。不要把宽高、stride、像素格式写成常量尽量在运行时从MppFrame里读取。这样换芯片时格式这块不容易出问题。第三把“CPU解码占比”纳入性能监控。用top或perf看CPU占用曲线的同时也要用MPP自身的统计接口观测解码器帧率、丢帧数、buffer利用率。只有把应用层CPU占用、解码器帧率、RGA耗时这三条曲线放在一起看瓶颈到底在哪一层才能一目了然。最后再说一个九成项目都会遇到的情况即便解码和RGA都已经优化到位如果应用层的线程调度不合理照样会卡顿。我习惯在性能追踪里同时看三件事应用层核心线程的CPU占用、MPP解码输出帧率、RGA单帧耗时。三条曲线一对比问题在上游码流、中间解码、还是下游处理立刻见分晓。