RK3588视频编码实战:基于MPP的H.264/H.265硬件编码全解析 有人问过我“RK3588做视频项目能不能不碰MPP直接用CPU软编”如果只编一路720p玩一玩当然可以但你要是做1080p60或者4K的编码我劝你趁早断了这个念想。RK3588的八核CPU虽然不弱可软编视频是一个典型的“吃满核还嫌不够”的重负载活跑起来之后系统里其他任务全得靠边站。我自己的方案从一开始就定在MPPRockchip Media Process Platform上也就是瑞芯微官方的媒体处理平台用它来调VPU硬件编码单元把H.264/H.265的编码压力从CPU上彻底卸掉。这篇文章把最近做RK3588视频编码的完整过程写一遍从环境搭建、交叉编译到MPP的MPI接口怎么去调再到把编码器吐出来的裸流封装成MP4、FLV或者推RTSP流。网上的资料大多停在“能跑通demo”的层面真正到自己做项目时每一步都有让你卡一下午的细节。我自己踩过的坑、调过的参数、排查过的内存问题都会写出来希望能让后面做这块的人少走点弯路。1. 效率不是玄学为什么RK3588的视频编码必须走MPP这条路1.1 RK3588的VPU到底能干什么先看硬件底子。RK3588集成的VPU公共资料里写得很明确视频编码支持H.264、H.265、VP8、JPEG最大到8K30fps视频解码支持的格式更多H.265、H.264、VP9、AV1、AVS2都在列。这个编码能力放在单板产品里是非常能打的8K编码意味着4K60fps这种需求对它来说根本不算极限真正干活时甚至可以开多路1080p并发编码。但硬件能力摆在芯片里是一回事能不能把它用起来是另一回事。直接用寄存器去操作VPU不现实瑞芯微也没有对应用户这么干。它提供的方案就是MPP一套运行在用户态、基于Linux的媒体处理平台把VPU的buffer管理、任务调度、编码参数配置这些繁杂细节全部封装掉。你只需要调用它的MPIMedia Process Interface接口把一帧YUV数据送进去然后从另一边把编码后的码流包拿出来。所以做RK3588视频编码本质上是两件事第一理解MPP的接口怎么调用第二把编码器输出的裸流变成业务能用的文件或网络流。前者解决“能不能编出来”后者解决“能不能交付出货”。这篇文章就是围绕这两条主线展开的。1.2 取流、编码、封装的链路中MPP处在哪个位置做视频方案时完整的数据链路通常是这样的摄像头或者ISP单元输出YUV原始帧经过V4L2、RKISP或者直接内存导入的方式把图像数据交给编码器编码器压缩成H.264/H.265码流码流再经过封装层变成MP4文件或者打包成FLV推RTMP或者走RTP推RTSP。MPP在链路中间充当的就是那个“编码引擎”。前面取流是什么方式它不管后面封装成什么容器它也不管它只负责高效地把YUV帧变成H.264/H.265码流包。这个边界划分非常重要很多初学者以为用了MPP就等于把推流也搞定了其实不是。你从MPP拿到的是一包一包的裸流数据要存文件还是推网络还得自己再处理一层。我建议在设计模块时就把这条链路拆开采集线程只负责取帧编码线程只负责和MPP交互封装/推流线程只负责消费码流包。三个线程之间用队列解耦帧数据用引用计数管理避免深拷贝。这样即使后面要换采集方式或者从MP4切换成RTSP代价都很小。1.3 哪些场景其实没必要硬上MPP写到这里可能有人会觉得是不是所有RK3588的视频场景都无脑上MPP也不是。至少有两种情况你可以重新考虑第一种是极低分辨率、极低帧率的场景比如只编一路320x240、5fps的预览画面CPU软编完全扛得住系统负载也很低。此时为了一个MPP引入额外模块反而增加了开发和调试成本。第二种是项目周期非常紧对底层的掌控要求又不高只求快速交差。这种情况下直接使用FFmpeg的h264_rkmpp/hevc_rkmpp编码器或者GStreamer的mpph264enc插件底层都是MPP但对外有更高层的封装开发效率更高。我自己在项目初期做原型验证时就是这么干的等确认方案可行后再回头用原生MPP接口精雕细节。原生MPP接口的价值在于它省掉了FFmpeg/GStreamer那一层的开销和不确定性让你能精确控制编码参数、buffer生命周期和时间戳对延迟、码率、内存占用都能抠到极致。如果你的产品是视频类设备最终大概率还是要走原生MPP这条路。2. 环境搭建在目标板子上跑通第一个H.264裸流2.1 先检查系统里是不是已经有MPP可以白嫖RK3588的开发板大多数Linux发行版固件比如Debian、Ubuntu桌面包里已经预装了rockchip-mpp的动态库和头文件。动手编译之前先上板子执行几行命令确认一下很多时候环境根本不用从头搭。ldconfig -p | grep rockchip_mpp ls /usr/lib/aarch64-linux-gnu/librockchip_mpp* ls /usr/include/rockchip/如果能看到librockchip_mpp.so、librockchip_mpp_rc.so之类的动态库以及rk_mpi.h、mpp_enc.h、mpp_frame.h这些头文件那么恭喜系统自带的MPP可以直接拿来用了。这里要留意的是固件自带版本可能和你从GitHub拉下来的源码版本有差异接口定义偶有调整但核心的MPI接口保持兼容不用太担心。如果你用的固件比较精简或者干脆是自己用Buildroot/Yocto做的系统里面没有MPP那就走源码编译这条路。还有一种情况固件里虽然带库但不带头文件那你最好也自己编译一份开发时以自己编译的头文件为准避免对着旧头文件写代码、链接新库时行为对不上。2.2 从源码编译MPP的两种方式和分支选择MPP源码在GitHub上仓库名是rockchip-linux/mpp主要分支有release和develop。release分支偏向稳定适合在产品上使用develop分支更新更快可能包含新特性但也可能有新引入的问题。我的习惯是做产品用release分支做新功能调研时才会切到develop看。在RK3588板子上编译MPP有两种常见方式。第一种是在板子上直接原生编译。MPP是一个cmake工程体积不大板子上跑编译完全可行。SSH登录到板子执行git clone -b release https://github.com/rockchip-linux/mpp.git cd mpp mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease .. make -j6编译完成后动态库和测试工具会在build目录里生成。第二种是在PC上交叉编译适合公司里集中管理工具链、或者板子磁盘空间紧张的情况。交叉编译时确保你已经安装了aarch64-linux-gnu交叉工具链然后cd mpp mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease \ -DCMAKE_C_COMPILERaarch64-linux-gnu-gcc \ -DCMAKE_CXX_COMPILERaarch64-linux-gnu-g \ .. make -j8交叉编译完成后把编译出来的librockchip_mpp.so和test目录下的可执行文件用scp推到板子上。需要提醒的是如果板子系统里已经装了旧的MPP库新编译的库要放到单独的目录比如/opt/mpp/lib然后用LD_LIBRARY_PATH指过来否则测试程序默认加载系统库你辛苦编出来的新库根本没生效。这个问题我第一次就踩了测试半天发现跑的还是老版本。2.3 mpi_enc_test快速验证VPU有没有正常工作装好MPP之后先别急着写代码用官方测试工具验证一下VPU是不是好的。MPP源码里自带一堆测试程序编码相关的在编译输出的test目录下常见的有mpi_enc_test有些版本叫mpp_enc_test本质一样。跑一个H.264编码测试1080p分辨率编100帧LD_LIBRARY_PATH./lib ./test/mpi_enc_test -t 7 -w 1920 -h 1080 -n 100 -o /tmp/test.h264这里的-t 7表示H.264编码-t 8则是H.265/HEVC。MPP测试工具支持自动生成YUV输入数据不需要外部喂文件所以跑起来非常方便。如果VPU工作正常命令执行结束后/tmp/test.h264就是一份可以直接播放的H.264裸流。把裸流拉回PC验证ffplay -f h264 /tmp/test.h264如果播放出画面说明VPU和MPP链路都是通的环境搭建这一步就算完成了。这一步花不了几分钟但能帮你把“环境问题”和“代码问题”隔离开后面自己写代码跑不通时至少可以确定MPP本身在板子上是好的。3. 编码主流程MPP的MPI接口就是一个“吃帧拉包”的工厂3.1 初始化的关键参数格式、对齐和码控MPP的编码接口本质就是一套“往里送帧、往外拉码流包”的工厂流水线。先把这条流水线建立起来。#include rk_mpi.h #include mpp_enc.h #include mpp_frame.h #include mpp_packet.h MppCtx ctx NULL; MppApi *mpi NULL; MppEncCfg cfg NULL; // 创建编码上下文 mpp_create(ctx, MPP_CTX_ENC); // 初始化为H.264编码器如果要编H.265就改成MPP_VIDEO_CodingHEVC mpp_init(ctx, MPP_CTX_ENC, MPP_VIDEO_CodingAVC); // 拿到底层MPI接口 mpi mpp_get_mpi(ctx);接下来是配置编码器参数。MPP的配置项采用“键值对”风格通过mpp_enc_cfg_set_s32等接口写入。这里最关键的几个参数是宽高、水平/垂直对齐、像素格式、码控模式、码率和GOP大小。mpp_enc_cfg_init(cfg); mpp_enc_cfg_set_s32(cfg, prep:width, 1920); mpp_enc_cfg_set_s32(cfg, prep:height, 1080); mpp_enc_cfg_set_s32(cfg, prep:hor_stride, 1920); mpp_enc_cfg_set_s32(cfg, prep:ver_stride, 1088); 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, 8000000); mpp_enc_cfg_set_s32(cfg, rc:bps_max, 8000000); mpp_enc_cfg_set_s32(cfg, rc:bps_min, 8000000); mpp_enc_cfg_set_s32(cfg, rc:gop, 60); mpp_enc_cfg_set_s32(cfg, codec:type, MPP_VIDEO_CodingAVC); mpi-control(ctx, MPP_ENC_SET_CFG, cfg);解释一下那两个容易看懵的参数。“prep:width”和“prep:height”是你图像的实际尺寸但硬件编码时内存通常需要对齐1080这个高度对齐到16或者32的倍数之后是1088或者1080RK3588上展开了不少对齐策略。所以这里要区分“实际分辨率”和“stride对齐后的内存分辨率”。如果你送给编码器的buffer实际是按对齐后的stride分配的就必须把hor_stride和ver_stride配置成对齐后的值否则编码出来会花屏或者绿屏。我在第一次做4K编码时就吃过这个亏width和height填的3840x2160但buffer是按3840x2176分配的对齐缓冲区stride没告诉MPP结果编码出来的画面底部有绿边排查了整整半天。后来把hor_stride/ver_stride填成对齐值画面立刻正常。这个细节官方demo里通常会有注释但很多照着抄代码的人注意不到。3.2 帧输入MppBuffer、NV12内存布局和PTS编码器初始化完成后接下来要准备输入帧。MPP推荐的做法是使用MppBufferGroup来管理buffer这样buffer可以由MPP内部统一分配编码器访问时效率更高。一个典型的做法如下MppBufferGroup frm_grp NULL; MppBuffer frm_buf NULL; MppFrame frame NULL; // 创建一个DRM buffer groupVPU可以直接访问这种buffer mpp_buffer_group_get(frm_grp, MPP_BUFFER_TYPE_DRM); // 按对齐后的stride计算buffer大小NV12是Y平面加UV交错平面 size_t buf_size hor_stride * ver_stride * 3 / 2; mpp_buffer_get(frm_grp, frm_buf, buf_size); // 把采集到的图像数据拷入buffer void *buf_ptr mpp_buffer_get_ptr(frm_buf); memcpy(buf_ptr, yuv_src, buf_size); // 组装MppFrame mpp_frame_init(frame); mpp_frame_set_width(frame, width); mpp_frame_set_height(frame, height); mpp_frame_set_fmt(frame, MPP_FMT_YUV420SP); mpp_frame_set_buffer(frame, frm_buf); mpp_frame_set_pts(frame, pts);这里的PTS是重中之重。MPP本身不会帮你生成时间戳你送进去的MppFrame带什么pts编码后的packet就带什么pts。如果你的时间戳是乱的后面不管封装MP4还是推RTSP播放端都会出现快进、卡顿、音画不同步。所以在上游采集阶段就一定要维护好线性递增的PTS比如用单调时钟或者帧计数来生成。像素格式也要提一下。MPP_FMT_YUV420SP对应的是NV12也就是YYYYYYYYUVUV这种布局。RK3588的ISP输出或摄像头模组很多直接就是NV12拿到就能用。如果输入是I420/YUV420P也就是Y、U、V三个平面分立的格式要么在进入编码器前转换为NV12要么直接用MPP_FMT_YUV420P配置MPP也支持但需要确认你的固件版本对格式的支持完整。我在实际项目里统一在采集端就转成NV12后面所有环节都按NV12处理省去很多麻烦。3.3 编码循环put_frame与get_packet怎么配合编码器是异步工作的。你用encode_put_frame把帧送进去不能马上从encode_get_packet拿到码流中间需要等待VPU处理完成。一个基础的编码循环长这样while (get_yuv_frame(yuv_src, pts)) { // 拷入帧数据 memcpy(mpp_buffer_get_ptr(frm_buf), yuv_src, buf_size); mpp_frame_set_pts(frame, pts); // 送帧进编码器 ret mpi-encode_put_frame(ctx, frame); if (ret ! MPP_OK) { // 处理错误 break; } // 拉取编码后的码流包 MppPacket packet NULL; while (1) { ret mpi-encode_get_packet(ctx, packet); if (ret MPP_OK packet) { void *data mpp_packet_get_data(packet); size_t size mpp_packet_get_size(packet); int64_t p_pts mpp_packet_get_pts(packet); // 把码流数据写入文件或交给封装模块 write_packet(data, size, p_pts); mpp_packet_deinit(packet); } else { break; } } }这里有个容易困惑的地方encode_get_packet返回MPP_OK但packet为NULL是合法情况意思是“当前没有可输出的码流”你继续循环等下一轮就行。如果等到超时仍然没有输出那就要排查是不是编码器出错了。实际项目中我通常使用带超时控制的循环避免在异常状态下死循环卡住线程。还有一个细节是EOS处理。如果你要结束编码官方接口里会送一个带EOS标记的空帧给编码器编码器会把内部缓冲的码流全部吐出来。这个操作在处理“最后一帧”时非常关键漏掉它会导致MP4文件末尾缺帧或者文件损坏。具体做法是用mpp_frame_set_eos(frame, 1)设置一个空帧送入编码器后继续拉packet直到拿到带EOS标记的packet为止。4. 高效封装从裸流到MP4/FLV/RTSP的落地姿势4.1 先定容器再谈封装交付场景决定方案MPP吐出来的H.264/H.265码流是裸流字节流直接存成.h264文件也能播放但没有任何容器结构无法做随机定位、音视频同步也不能用于流媒体传输。真正要交付产品必须把它封装进容器格式。选哪种容器取决于你的业务场景场景推荐容器理由本地录像回放MP4兼容性好支持快进快退和音视频同步录像留存、多音轨需求MKV封装灵活容错强RTMP直播推流FLVRTMP协议天然基于FLV tag结构局域网摄像头预览RTSP/RTP实时性好延迟低播放端普遍支持浏览器直接播fMP4/WebRTC分段mp4或走WebRTC我在项目里用得最多的是MP4和RTSP两条线。录像功能走MP4实时预览走RTSP。两条线共用同一个编码模块只是编码参数不同录像用VBR追求画质预览用CBR保证码率稳定。4.2 SPS/PPS和关键帧识别封装的命门所在无论做哪种封装都绕不开SPS/PPS的处理。H.264裸流是Annex-B格式用起始码00 00 00 01分隔NAL单元。IDR关键帧前面通常会跟着SPSNAL type 7和PPSNAL type 8。做封装时需要从码流中识别出这些参数集。识别NAL类型的方法很简单起始码之后读第一个字节取低5位。7是SPS8是PPS5是IDR帧1是普通非IDR帧。MPP输出的H.264关键帧packet通常会把SPS/PPS和IDR数据一起放在同一个packet里这给裸流直接写文件带来了方便但在封装MP4时需要把SPS/PPS单独提取出来生成AVCDecoderConfigurationRecord也就是avcC box并且写入MP4的extradata。H.265稍有不同对应的参数集是VPStype 32、SPStype 33、PPStype 34封装HEVC时需要生成hvcC box。如果你同时支持H.264和H.265建议封装层把两套参数集提取逻辑都实现好后面切换编码格式时就只是换一个函数的问题。4.3 手写MP4 box和用FFmpeg接力两条路怎么选封装MP4这件事有两条路线一条是纯手写box另一条是借用FFmpeg的libavformat。手写box适合那种“我不想引入FFmpeg这么大一个依赖”的场景。MP4核心就是若干个boxftyp、moov、mdat。mdat存的是码流数据moov里面包含mvhd、trak、stts、stss、stsz等一系列索引信息。你只需要实现一个精简的MP4写入器支持H.264/H.265的单一视频轨。我自己早期做过一个极简版核心逻辑就几百行但非常麻烦的点在于如果写的是普通MP4moov默认在文件末尾播放器必须下载完整个文件才能播放。要在网页上边下边播还得做“moov前置”也就是写完后修正文件头或者一开始就预留moov空间。这些细节很折磨人。如果你产品里本来就用到了FFmpeg或者你愿意链接libavformat那用FFmpeg封装是最省力的。一个典型的流程是把MPP输出的packet直接组装成AVPacket然后交给av_interleaved_write_frameAVFormatContext *ofmt NULL; avformat_alloc_output_context2(ofmt, NULL, mp4, record.mp4); AVStream *st avformat_new_stream(ofmt, NULL); st-codecpar-codec_type AVMEDIA_TYPE_VIDEO; st-codecpar-codec_id AV_CODEC_ID_H264; st-codecpar-width 1920; st-codecpar-height 1080; // 把从裸流里提取的SPS/PPS拼接成extradata st-codecpar-extradata (uint8_t *)av_mallocz(extradata_size); memcpy(st-codecpar-extradata, extradata, extradata_size); st-codecpar-extradata_size extradata_size; avformat_write_header(ofmt, NULL); AVPacket pkt; av_init_packet(pkt); pkt.pts p_pts; pkt.dts p_pts; pkt.data packet_data; pkt.size packet_size; pkt.stream_index st-index; // 这个flag要自己判断关键帧 if (is_idr) { pkt.flags | AV_PKT_FLAG_KEY; } av_interleaved_write_frame(ofmt, pkt); av_write_trailer(ofmt);用FFmpeg封装时最关键的是外部DTS。MPP的H.264输出大多数配置下帧顺序就是显示顺序B帧较少用嵌入式编码通常关闭B帧以降低延迟所以DTS可以直接等于PTS。如果你为了画质开了B帧那么DTS和PTS就会出现偏移封装层必须正确处理否则播放时就会出现“卡顿后面的画面先出来”的错乱。我的经验是实时视频场景一律关B帧封装逻辑就能保持简单延迟也能稳定压住。4.4 直播场景下的GOP与延迟控制做RTSP或RTMP直播时GOP关键帧间隔的设置直接影响延迟和协议行为。GOP越大关键帧越少客户端从拉流到首帧显示的时间越不确定GOP越小码流中I帧变多同样码率下画质会被摊薄。我一般把GOP设成帧率的整数倍比如30fps下GOP设60即2秒一个IDR。这个配置兼顾了“进流后快速出图”和“画质不过分损失”。RTP封装H.264时还有一个MTU分片的问题。一个IDR帧的单个NAL单元可能超过1400字节RTP包需要按RTP payload格式进行FU-A分片。如果你是自己实现RTSP推流这个分片逻辑省不了如果是用GStreamer或FFmpeg的RTSP muxer它们会自己处理。我的建议是直播链路在原型阶段直接用GStreamer的rtspsrc/rtspclientsink组合验证量产阶段再决定要不要自研RTSP server避免一上来就被RTP分片、SDP生成这些细节拖住进度。5. 实测排障记录帧率、码控和buffer分配三个坑5.1 1080p60与4K30的实测数据先给一组我自己在RK3588板子上实测的数据供参考。需要说明的是同一颗芯片、不同板卡散热、不同固件版本、不同输入源数据都会有差异不要拿我的数字去怼供应商但数量级可以参考。编码配置实际帧率CPU占用编码线程备注1080p60 H.264 CBR 8Mbps60fps稳定5%-10%最常用的预览/录像组合4K30 H.265 CBR 20Mbps30fps稳定8%-15%高分辨率录像场景4K60 H.265 CBR 25Mbps接近60fps15%-25%对BNB显存/内存带宽要求高CPU占用低是硬件编码最大的优势。软编1080p60在这颗芯片上CPU占用会直接飙到70%以上还伴随着掉帧。用MPP后编码线程的CPU占用可以忽略不计剩下的算力全部留给算法、协议栈和应用逻辑。这也是我在方案选型时坚持MPP的核心原因。5.2 帧率抖动先怀疑输入源再怀疑码控有一次做1080p60录像编码器输出帧率在55到60fps之间来回跳怎么看都不对劲。首先排查的方向是输入源。我先用mpi_enc_test的YUV自生成模式跑了一遍发现编码器本身能稳定跑满60fps问题显然出在采集链路。接着查采集线程发现它从V4L2拿到buffer之后在memcpy之前做了一次图像旋转用的是普通CPU逐像素复制这一步吃掉不少时间导致喂帧速率不稳定。把这个旋转操作换成基于DMA的实现或者干脆去掉旋转、让显示端旋转帧率就稳定了。排查这类问题我总结了一条原则先隔离编码器再怀疑输入源最后才回头看码控。MPP自带的测试工具就是最好的隔离手段它能帮你快速判断“到底是谁拖慢了谁”。5.3 mpp_buffer分配失败CMA内存耗尽怎么查MPP的buffer大多来自DRM/CMA内存。系统跑久了如果buffer没有正确释放CMA内存会被耗尽然后出现mpp_buffer_get失败、编码卡死、甚至VPU任务不提交的现象。排查方法cat /proc/meminfo | grep -i cma看到CmaTotal和CmaFree之后如果CmaFree长期为一个很小的值基本可以确定是buffer泄漏。常见的泄漏点有几个第一个packet拿到后忘记调mpp_packet_deinit。这个是最容易犯的每帧泄漏一个packet跑个几万帧系统就废了。第二个输出packet处理后没有及时把packet的数据指针归还。MPP的packet内部引用的是编码器内部bufferdeinit操作会回收但如果你把packet的data指针存到另一个队列里迟迟不释放底层buffer一样被占着。第三个MppBuffer本身没有正确put。MppBuffer的上游是MppBufferGroupbuffer get之后用完必须调用mpp_buffer_put归还给group否则group里的内存越占越多。我自己的模块里输入帧buffer用的是循环池固定几个buffer轮转避免反复get/put带来的额外开销和碎片。5.4 码控模式选不对画质和码率两头受气MPP的码控模式rc:mode主要有CBR、VBR和FIXQP各有适用场景。CBR模式下编码器会严格控制输出码率瞬时码率波动很小适合RTSP/RTMP这类需要带宽可控的传输场景。但代价是画面运动剧烈时为了压低码率会降低画面质量出现模糊或马赛克。所以我做CBR时bps_max不会紧贴着bps设而是留出1.5到2倍的上浮空间让编码器在复杂场景下有喘息余地。VBR模式的码率波动大但画质稳定适合本地存储。这个模式在运动场景下能明显看出画质比同规格CBR好。缺点是如果突发码率过高存储系统写入压力会增大需要评估SD卡或硬盘的持续写能力。FIXQP适合做算法调试比如你想对比不同分辨率下的编码画质固定QP后输出结果更纯粹。我自己在测试VPU能力边界时会用FIXQP但在产品代码里录像用VBR预览用CBR这个搭配比较无脑也不会出错。关于码控还有一个坑是“码率设置太小导致掉帧”。有一次我把1080p的bps设成1Mbps结果编码器明显开始丢帧。原因是极限低码率下编码器需要做复杂的码率控制决策处理不过来。它不是不能编到1Mbps而是需要更多时间决定量化参数输入帧率一高就会落后。解决方法是提高bps或者降低输入帧率。所以配置码率时不要拍脑袋要结合分辨率和帧率估算一个合理区间1080p30起步4Mbps1080p60起步8Mbps4K30起步16Mbps这只是经验值具体还要看画面复杂度。5.5 关于时间戳和多路并发的两个补充点最后补充两个容易被忽视的点。时间戳基准如果同时做音视频录制音频和视频必须共用一个时钟基准。我通常使用CLOCK_MONOTONIC作为系统时间基准采样到时间后统一换算成微秒再转成对应容器的时间戳。这样即使系统时间被NTP调了音视频同步也不受影响。很多项目的音画不同步问题根源不是封装层而是时间戳源头就乱了。多路并发RK3588的VPU可以同时跑多路编码但总资源是有限的。多路并发时每路的编码参数要提前做整体规划比如两路1080p60加一路4K30就需要估算总码率和总内存占用给各路分配独立的MppBufferGroup避免互相争抢。我自己做过四路1080p30同时编码VPU资源还剩富余但内存带宽明显吃紧后来通过降低某些路的帧率优先级整体才算稳定。综合这些实测和排障经验我的感受是RK3588的MPP编码本身是成熟可靠的项目的难点基本都在“你以为配置对了其实没有”的细节上比如stride对齐、buffer生命周期、时间戳一致性。把这三个基础问题打牢后面的封装、推流、存储都会顺畅很多。希望这篇实战记录能帮你把这段路走得更顺一点。