如果你的项目跑在RK3568这类瑞芯微板子上,刚好又要做实时视频推流、录像或者智能分析,那你大概率会遇到跟我一样的问题:CPU软编码一开,其他任务全被拖垮。我最近在做一个双路1080p低延迟图传,最开始图省事用x264直推,结果两路编码直接吃掉大半CPU,系统响应越来越慢,画面一复杂就开始掉帧。后来切换到RKMPP的H.264/H.265硬件编码,同样两路1080p30,CPU占用降到20%以内,编码器本身几乎不占CPU,画面也稳住。
这篇文章不是API文档的复读,而是把我从零接入RKMPP硬件编码的完整过程讲清楚:先是整体架构认知,再是参数细节,然后是代码实现,最后是优化手段和排查经验。如果你正准备在RK平台的嵌入式设备上跑视频编码,这篇文章应该能帮你少走两三天弯路。
1. 为什么我最后选了RKMPP硬件编码
1.1 软编码撑不住时,换个思路
很多团队一上来就直接上x264或者x265软编码,原因是现成、好调、交叉编译也简单。但嵌入式平台的CPU资源是有限预算,尤其是做多路视频时,软编码的钱包很快就见底。我这次的需求是双路1080p30同时编码,每路如果用x264的ultrafast档,单核基本被吃掉80%,两路就直接把四核CPU干到吃紧。系统里还有网络线程、UI线程、日志线程,一忙起来全部跟着卡。
换成RKMPP硬件编码之后,编码这件事从CPU搬到了VEPU单元,CPU只负责喂数据和收码流。实测同样两路1080p30 H.264,CPU占用直接掉到20%以内,而且不会因为画面纹理复杂而产生编码卡顿。这个收益对于长期运行、需要稳定帧率的设备来说,是质的区别。
所以第一个建议:如果你的目标平台是瑞芯微RK3566、RK3568、RK3588、RK3399这类芯片,并且需要跑H.264/H.265编码,不要犹豫,直接走RKMPP。软编码留给那些没有硬件编码器的老平台,或者你只是在电脑上做离线转码。
1.2 RKMPP到底做了什么
RKMPP,全称Rockchip Media Process Platform,是瑞芯微提供的统一媒体处理中间件。它不只是一个简单的编码库,而是把视频编解码、图像处理、内存管理都包了一层。实际编码依赖的是内部的VEPU(Video Encoder Processing Unit)硬件模块,而你通过MPP接口告诉它“我要编H.264、分辨率多少、码率多少”,然后喂NV12帧,它给你吐H.264/H.265码流。
这里要提到一个容易混淆的点:RKMPP不负责色彩空间转换,也不负责缩放。如果摄像头输出的是RGB或BGR,你需要先用RGA或者软件转成NV12,再交给编码器。这跟我第一次用时的预期不一样,我以为传个RGB帧进去就能直接编码,结果要么报错要么出来是花的。后来查文档才知道,MPP编码器的主流输入格式是NV12(YUV420SP),RGB不是它的主战场。
整个数据流大概是这样的:采集设备产生原始帧(通常是NV12或者需要转换的RGB)→ 必要时通过RGA做格式转换/缩放/旋转 → 填入MPP的MppBuffer → 调用编码接口 → VEPU硬件编码 → 取出MppPacket码流 → 封装成H.264/H.265文件或推流。搞清楚这条链路,后面所有的参数配置都不容易跑偏。
1.3 H.264还是H.265,先做选型
RKMPP对H.264和H.265的编码支持,在API层面几乎是一套写法,区别主要在编码类型、profile、level这几个字段上。所以真正要取舍的不是实现难度,而是应用场景。
| 对比维度 | H.264 | H.265 |
|---|---|---|
| 同等画质码率 | 相对偏高 | 通常省30%-50% |
| 播放兼容性 | PC/手机/浏览器普遍支持 | 老设备/部分浏览器不支持 |
| 硬件解码要求 | 低 | 略高 |
| 实时性 | 更容易做到低延迟 | 编码/解码链路上延迟略高 |
| 适合场景 | 实时预览、低延迟图传、兼容性优先 | 存储录像、带宽受限、无线传输 |
我做双路图传时,前端是自研的播放器,解码能力完全可控,所以第一版用了H.264,因为低延迟链路更稳。但如果是做NVR录像或者长时间存储,H.265会明显省空间。以1080p30为例,H.264在CBR模式下通常要给4Mbps才够清晰,H.265给2Mbps就能接近同等观感,对于存储量很大的设备,这个差距非常可观。
选型的另一个建议是看你的下游解码端。如果视频要推给浏览器播放,H.264的兼容性明显更好;如果是在自己的APP或者自研盒子里解码,H.265也完全没问题。RKMPP切换编码类型只需要改一个配置项,所以建议前期把两条路都跑通,后面切换成本几乎为零。
2. 跑通编码前必须搞懂的两组关键结构
2.1 输入帧:NV12、宽高对齐和stride的坑
MPP硬件编码的输入帧,最常用的是NV12,也就是YUV420SP格式。它在一段连续内存里先放Y平面,再放UV交叉排列的数据。以1080p为例,Y平面是1920×1080字节,UV平面是1920×1080/2字节,总大小就是1920×1080×3/2。
但这里有一个非常容易踩的坑:编码器的输入宽高并不一定等于你图像的逻辑宽高。硬件内部是分块处理的,宽高通常需要对齐到16的倍数,甚至某些平台建议对齐到64。如果你只设置了width=1920、height=1080,而实际内存布局按照1920×1088去排,那就要把hor_stride和ver_stride也设置成1920和1088。
stride可以简单理解成内存里每一行实际占用的宽度,对齐是为了方便硬件按宏块读取。1080本身不是16的倍数,1080÷16=67.5,所以需要补到1088。如果你不设置stride或者设置错了,轻则编码出来的画面偏移,重则直接花屏。我当时就是width、height、hor_stride、ver_stride四个字段没对齐,折腾了整整一天。
建议在分配buffer和填写配置时,统一用一个对齐函数:
static int align_to_16(int v) { return (v + 15) & ~15; }然后用对齐后的值去算buffer大小和填充配置。这样能避免很多莫名其妙的显示问题。另外要记住,NV12输入时,buffer大小是hor_stride × ver_stride × 3/2,而不是width × height × 3/2。
2.2 缓冲对象:MppBuffer/MppFrame/MppPacket怎么配合
新手看RKMPP代码时,最容易被MppBuffer、MppFrame、MppPacket这几个对象搞晕。我用自己的话捋一遍:
- MppBuffer:底层的内存块,可能是ION或DMA分配出来的物理连续内存,用来装原始像素数据或者码流数据。
- MppFrame:描述一帧输入画面的结构体,里面包含宽高、stride、格式、pts,以及一个指向MppBuffer的引用。
- MppPacket:描述一帧输出码流的结构体,里面包含码流数据指针、长度和pts。
整个流程就是:你从BufferGroup里拿一个MppBuffer,把一帧NV12数据拷进去,然后用MppFrame把这个Buffer包起来,设置好宽高和pts,调用encode_put_frame送给编码器。编码完成后,用encode_get_packet拿到的MppPacket就是编码后的H.264/H.265数据。
这里要特别注意BufferGroup的作用。每个编码实例最好有一个自己的MppBufferGroup,从组里分配buffer,用完之后放回组里重复使用。千万不要每编码一帧就重新创建buffer、用完就销毁,那样会引入不必要的内存分配开销,长期运行还会造成内存碎片和性能抖动。
Packet的释放也别偷懒。取到packet后,如果你需要拷贝码流数据,就先memcpy出来,拷贝完立即调用mpp_packet_deinit释放掉。如果光取不还,内部缓冲区很快会被占满,延迟越来越大,最后编码器直接罢工。
2.3 一份标准1080p30 H.264编码参数清单
我整理了一份可以直接参考的配置清单,单位、推荐值、坑都写在表里。下面这份是H.264、1080p30、CBR 4Mbps的常用配置。
| 配置项 | 推荐值 | 说明与坑 |
|---|---|---|
| prep:width | 1920 | 图像逻辑宽度 |
| prep:height | 1080 | 图像逻辑高度 |
| prep:hor_stride | 1920 | 对齐后每行宽度,建议16的倍数 |
| prep:ver_stride | 1088 | 对齐后行数,建议16的倍数 |
| prep:format | MPP_FMT_YUV420SP | 即NV12,编码最常用 |
| rc:mode | MPP_ENC_RC_MODE_CBR | 恒定码率;追求画质可以换AVBR |
| rc:bps | 4000000 | 单位是bps,不是kbps,这是重灾区 |
| rc:bps_max | 4000000 | CBR下和bps保持一致 |
| rc:bps_min | 4000000 | CBR下和bps保持一致 |
| rc:gop | 60 | 30fps下代表2秒一个IDR帧 |
| rc:fps_in_flex | 0 | 0为按固定节拍,1为输入灵活 |
| rc:fps_in_num | 30 | 输入帧率分子 |
| rc:fps_in_denorm | 1 | 输入帧率分母 |
| rc:fps_out_flex | 0 | 输出是否跟随输入灵活调整 |
| rc:fps_out_num | 30 | 输出帧率分子 |
| rc:fps_out_denorm | 1 | 输出帧率分母 |
| codec:type | MPP_VIDEO_CodingAVC | H.264;换H.265改成HEVC |
| codec:profile | 100 | 100是High Profile,低延迟可改66 |
| codec:level | 40 | 4.0,对应1080p60以内的常见场景 |
我建议你把这个表保存下来,每次配置都对照一遍。特别强调一下rc:bps的单位,MPP文档里写的是byte per second还是bit per second,不同版本有歧义,但实际在编码配置里用的是bit per second。我第一次设置成4000000,以为单位是kbps,结果码率直接失控,轻轻松松跑到几十Mbps。后来确认,4Mbps就是4000000,不要多乘以1024,也不要少写两个零。
3. 从代码到码流:RKMPP编码器实操
3.1 初始化编码器并下发配置
初始化RKMPP编码器的逻辑,按照官方sample的顺序走就行,核心是mpp_create、mpp_init、mpi->control三步。完整初始化代码类似下面这样:
#include "mpp_enc.h" #include "mpp_enc_cfg.h" MppCtx ctx; MppApi *mpi; MppEncCfg cfg; MppPollType timeout = MPP_POLL_NON_BLOCK; RK_U32 width = 1920; RK_U32 height = 1080; RK_U32 hor_stride = 1920; RK_U32 ver_stride = 1088; // 1. 创建上下文 mpp_create(&ctx, &mpi); // 2. 初始化编码上下文,编码类型为H.264 mpp_init(ctx, MPP_CTX_ENC, MPP_VIDEO_CodingAVC); // 3. 使用非阻塞取包模式,避免取包时卡死 mpi->control(ctx, MPP_SET_OUTPUT_BLOCK, &timeout); // 4. 初始化编码配置结构体 mpp_enc_cfg_init(&cfg); // 5. 配置输入格式相关 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", hor_stride); mpp_enc_cfg_set_s32(cfg, "prep:ver_stride", ver_stride); mpp_enc_cfg_set_s32(cfg, "prep:format", MPP_FMT_YUV420SP); // 6. 配置码控相关 mpp_enc_cfg_set_s32(cfg, "rc:mode", MPP_ENC_RC_MODE_CBR); mpp_enc_cfg_set_s32(cfg, "rc:bps", 4 * 1000 * 1000); mpp_enc_cfg_set_s32(cfg, "rc:bps_max", 4 * 1000 * 1000); mpp_enc_cfg_set_s32(cfg, "rc:bps_min", 4 * 1000 * 1000); mpp_enc_cfg_set_s32(cfg, "rc:gop", 60); mpp_enc_cfg_set_s32(cfg, "rc:fps_in_flex", 0); mpp_enc_cfg_set_s32(cfg, "rc:fps_in_num", 30); mpp_enc_cfg_set_s32(cfg, "rc:fps_in_denorm", 1); mpp_enc_cfg_set_s32(cfg, "rc:fps_out_flex", 0); mpp_enc_cfg_set_s32(cfg, "rc:fps_out_num", 30); mpp_enc_cfg_set_s32(cfg, "rc:fps_out_denorm", 1); // 7. 配置编码器相关 mpp_enc_cfg_set_s32(cfg, "codec:type", MPP_VIDEO_CodingAVC); mpp_enc_cfg_set_s32(cfg, "codec:profile", 100); mpp_enc_cfg_set_s32(cfg, "codec:level", 40); // 8. 把配置下发到编码器 mpi->control(ctx, MPP_ENC_SET_CFG, cfg); // 9. 让每个IDR帧都携带SPS/PPS,方便播放器随时接入 MppEncHeaderMode header_mode = MPP_ENC_HEADER_MODE_EACH_IDR; mpi->control(ctx, MPP_ENC_SET_HEADER_MODE, &header_mode); // 10. 配置结构体不再需要 mpp_enc_cfg_deinit(cfg);这里值得关注的是非阻塞取包设置。如果你用默认的阻塞模式,encode_get_packet在编码器没有输出时可能会一直等下去,一旦喂帧节奏出了点问题,线程就被挂住,整个业务跟着卡。我用MPP_POLL_NON_BLOCK之后,取包变成了“有就取,没有就下一轮再取”的模式,对系统健壮性提升很明显。
3.2 编码主循环:送帧与取包
编码主循环的逻辑比较直接:获取一帧原始数据,放进MppBuffer,包装成MppFrame,调用encode_put_frame送进去,然后立刻encode_get_packet取码流。取到packet后拷贝数据、释放packet,一帧完成。
下面是一段可以跑通的核心循环:
MppBufferGroup group; MppBuffer buf = NULL; MppFrame frame; MppPacket packet = NULL; RK_S64 pts = 0; // 创建buffer group,内部使用ION mpp_buffer_group_get_internal(&group, MPP_BUFFER_TYPE_ION); // 计算NV12 buffer大小 size_t frame_size = hor_stride * ver_stride * 3 / 2; while (running) { // 1. 从group里拿一块buffer mpp_buffer_get(group, &buf, frame_size); // 2. 把NV12原始数据拷贝到buffer里 void *dst = mpp_buffer_get_ptr(buf); memcpy(dst, nv12_src, frame_size); // 3. 构造MppFrame mpp_frame_init(&frame); mpp_frame_set_width(frame, width); mpp_frame_set_height(frame, height); mpp_frame_set_hor_stride(frame, hor_stride); mpp_frame_set_ver_stride(frame, ver_stride); mpp_frame_set_fmt(frame, MPP_FMT_YUV420SP); mpp_frame_set_pts(frame, pts); mpp_frame_set_buffer(frame, buf); // 4. 送入编码器 mpi->encode_put_frame(ctx, frame); // 5. 取码流 mpi->encode_get_packet(ctx, &packet); if (packet) { void *out_ptr = mpp_packet_get_data(packet); size_t out_len = mpp_packet_get_size(packet); if (out_len > 0) { fwrite(out_ptr, 1, out_len, fp); // 这里可以拿到文件格式的H.264数据 } mpp_packet_deinit(&packet); packet = NULL; } // 6. 清理本帧对象,归还buffer mpp_frame_deinit(&frame); mpp_buffer_put(buf); pts += 1000000 / 30; // 按30fps计算pts,单位微秒 }这个循环有两个细节值得注意。第一,mpp_frame_deinit和mpp_buffer_put的顺序不能反,一定要先把frame释放,再把buffer归还原组。如果先归还buffer,frame内部还引用着这块内存,编码器可能还没来得及读取,造成花屏或者数据错乱。第二,packet取出来之后要尽快处理,不要堆积在业务队列里。我遇到过长时间运行后延迟暴涨的情况,最后定位出来就是取包后塞到网络发送队列,网络慢导致队列积压,编码器内部缓存被占满。
3.3 H.265到底改动哪几行
H.265的切换比想象中简单,大部分配置完全复用。真正要改的就三处:
第一个是mpp_init时的编码类型,从MPP_VIDEO_CodingAVC改成MPP_VIDEO_CodingHEVC。第二个是codec:type,也改成MPP_VIDEO_CodingHEVC。第三个是profile,H.264的100对应H.265通常是Main Profile,建议设成1,level可以根据分辨率继续用40。
mpp_init(ctx, MPP_CTX_ENC, MPP_VIDEO_CodingHEVC); mpp_enc_cfg_set_s32(cfg, "codec:type", MPP_VIDEO_CodingHEVC); mpp_enc_cfg_set_s32(cfg, "codec:profile", 1); mpp_enc_cfg_set_s32(cfg, "codec:level", 40);如果之前H.264的码率是4Mbps,切到H.265后可以先按2Mbps试,观察画质再微调。实测在嵌入式设备上,H.265的编码速度并不会比H.264慢太多,因为都是同一块硬件VEPU在处理,区别主要在于码流封装和解码端的兼容性。
验证编码结果的方法很简单,把写入文件的码流拷贝到电脑上,用ffprobe查看:
ffprobe -v error -show_streams -format json output.h264重点看宽高、profile、level和码率是否和你配置一致。如果显示的分辨率不对,或者level不对,说明配置没完全生效,优先检查width和stride有没有配错。也可以抽出几帧,用ffplay直接播放,确认画面没有花屏偏色。
4. 画质、码率和延迟的平衡点:码控和GOP优化
4.1 四种码率控制模式怎么选
RKMPP编码器支持多种码控模式,我实际用下来主要有四种:CBR、VBR、AVBR、FIXQP。它们各自的定位差异很大,选错模式会让你在画质和码率之间反复横跳。
CBR,恒定码率,输出码率最稳定,适合实时传输和带宽受限的场景。我图传项目里用的就是CBR,因为网络管道带宽固定,码率不稳就会卡顿。但CBR的代价是画面复杂时为了控码率,不得不提高量化参数,画质会有所波动。
VBR,可变码率,允许码率在设定范围内浮动,画面复杂时用更多码率,简单时减少码率。比CBR省码率,画质也更均匀,但码率峰值不可控,不适合带宽严格受限的场景。
AVBR,自适应码率,是VBR的增强版。它会根据画面运动程度自动调整码率,同时尽量维持目标码率附近。这个模式在存储场景很实用,实测下来比CBR能省不少空间,画质损失又比VBR可控。
FIXQP,固定量化参数,直接指定QP值,不去管码率。这个模式适合测试和调试。我之前排查画质问题时,就用FIXQP固定成QP=28,对比不同参数下的主观画质,不会被码控策略干扰。
| 模式 | 码率稳定性 | 画质 | 推荐场景 |
|---|---|---|---|
| CBR | 最稳 | 复杂画面可能下降 | 实时图传、网络传输 |
| VBR | 波动 | 好 | 录像、存储 |
| AVBR | 相对稳 | 好 | 折中场景 |
| FIXQP | 不可控 | 可精确控制 | 调试、画质对比 |
4.2 GOP结构和B帧对延迟的影响
GOP是指两个IDR帧之间的帧组长度,intra refresh的节奏。GOP越短,IDR帧越多,码率越高,但播放器可以更快地完成随机接入。GOP越长,码率更省,但断点续播或者网络抖动后重新同步会变慢。
常规场景GOP可以设置成帧率的2倍,比如30fps设60,相当于2秒一个IDR。低延迟交互场景可以更激进,设成30甚至15,但预算要有心理准备,IDR帧体积通常是普通帧的5到10倍,码率会被拉高。
真正影响延迟的还有B帧。B帧会引入多帧重排序,编码端必须等后面的帧到达才能输出前面帧,解码端也面临同样问题。对实时性要求高的场景,我通常建议避开B帧。在H.264里,最直接的办法是使用Baseline Profile,也就是把profile设成66,因为Baseline本身不允许B帧。
当然,High Profile的画质和压缩率更好。如果后端播放器没那么敏感,或者你的延迟预算比较宽松,用High Profile也能接受。这里要看你自己的选择,没有绝对。
RKMPP还提供了MPP_ENC_HEADER_MODE_EACH_IDR选项,让每个IDR帧都携带SPS/PPS。我强烈建议开启,尤其是做RTSP或者FLV等需要随时接入的封装格式时。如果不开启,某些播放器在没有SPS/PPS的情况下,遇到第一个IDR帧可能还解不出画面,黑屏几秒。
4.3 画质微调与码率节省:从配置到策略
除了码控模式和GOP,还有几个参数对画质和码率的平衡影响很大。一个是量化参数的范围限制,在MPP配置里可以设置最大QP和最小QP。比如CBR模式下,为了避免画面极其复杂的瞬间画质崩到没法看,可以把最大QP限制在40左右,牺牲一点码率上限换取基本画质。
另一个是I帧和P帧的质量等级。MPP支持对I帧和P帧分别设置质量区间,比如让I帧稍微多给点码率,因为I帧质量会影响后面一整个GOP的参考基准。实测把I帧质量拉高之后,整个画面组的干净程度会明显提升。
码率节省方面,除了切H.265,还可以考虑在采集端做降分辨率后再编码。比如监控大屏场景不一定要1080p原始分辨率,720p就足够清晰,这时先在RGA里做一次缩放,再送编码器,比在1080p上强行压低码率效果更好,画质损失也更小。
另外,如果硬件支持,尽量让摄像头直接输出NV12,省去RGB转NV12这一步。颜色转换本身不贵,但会引入额外耗时和内存带宽消耗,对实时性要求高的场景能省则省。
4.4 实测数据参考
我这边在RK3568平台上做过一组简单对比,条件固定为1080p30、H.264、画面内容是一段有运动物体的视频:
| 配置 | 平均码率 | 主观画质 | 说明 |
|---|---|---|---|
| CBR 4Mbps | 4Mbps | 清晰 | 稳定但码率偏高 |
| CBR 2Mbps | 2Mbps | 有可见纹理模糊 | 码率压低后画质下降明显 |
| AVBR 2Mbps | 2.1Mbps | 接近CBR 3Mbps | 省码率且画质可接受 |
| H.265 AVBR 2Mbps | 2Mbps | 接近CBR 4Mbps的H.264 | 同码率下H.265优势明显 |
这组数据不一定能直接复用到你的场景,因为画质主观性很强,测试内容不同结果差异也大。我只想说明一个趋势:如果带宽和存储是瓶颈,优先上H.265;如果只能在H.264里选,AVBR通常比CBR更划算。
5. 多路并发与稳定性优化
5.1 一路跑不满,多路怎么保持稳定
RKMPP硬件编码不是只能跑一路,你可以在一个进程里同时创建多个MppCtx,每个实例独立配置、独立编码。我在RK3568上实际跑过双路1080p30 H.264,一路4Mbps一路2Mbps,整体稳定。
但多路并发有几个注意事项。第一,硬件编码器总处理能力有上限,和具体芯片型号强相关,别指望你买的是高配芯片就一定能跑满N路,最好在量产前做压力测试,把每路的负载和内存占用都确认清楚。第二,每个编码实例建议独立使用一个MppBufferGroup,不要在多个ctx之间共享buffer,避免内存访问冲突和同步问题。
线程模型上,我的习惯是一路一个线程,线程内部只做“取帧→送编码→取包→投递码流”的动作,不做其他重活。有条件的话,可以把这个编码线程绑定到特定CPU核心上,减少调度抖动。实测开启线程绑核之后,帧间隔的波动明显变小,对低延迟场景有帮助。
5.2 低延迟链路实战
低延迟是图传和无人机场景的刚需,这里有几个实战技巧可以分享。第一个是喂帧节奏的控制。很多人习惯用usleep(33333)来模拟30fps,但usleep的精度受系统调度影响很大,实际帧间隔可能忽快忽慢。
我建议用pts来驱动,而不是sleep:
struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); RK_S64 target_pts = pts; int64_t current_us = now.tv_sec * 1000000 + now.tv_nsec / 1000; if (current_us < target_pts) { usleep(target_pts - current_us); } pts += 1000000 / 30;这样的好处是,即使某帧处理慢了,下一帧会尽量追回目标时间点,整体节奏比固定sleep更平滑。如果有人告诉你sleep最省事,那是他没在低延迟场景里吃过亏。
第二个是编码器取包模式的设置。在初始化时把输出设为非阻塞,取包时如果拿不到packet,就继续下一轮循环,不要死等。硬件编码虽然快,但也有忙不过来的时候,死等反而会把问题放大。
第三个是编码器配置里fps_in_flex的用法。如果输入帧率是固定的,建议fps_in_flex设为0,让编码器按固定节奏工作。如果你的输入帧率可能抖动,就把fps_in_flex设为1,编码器会尽量跟随输入节奏,减少额外等待。这个字段不理解时容易乱调,我建议先保持0,跑出基线数据后再尝试1。
5.3 内存回收和缓存控制
长时间运行的编码程序,最容易出的问题其实是内存和缓冲区的堆积。MppPacket取出来之后必须及时释放,如果网络发送队列拥塞,packet被积压在业务队列里,编码器内部缓存就会持续增长,延迟和内存一起飙升。
我的做法是限制输出队列深度。每次取到packet后立刻拷贝数据,然后马上deinit,不持有任何MppPacket对象。业务端如果需要缓冲,只缓冲拷贝出来的std::vector或者malloc出来的内存,并设置队列上限,超过上限就丢旧帧或者直接丢当前帧。这样可以把内存占用控制在一个稳定范围。
另外一个值得注意的点是MppBufferGroup的复用。如果每次编码都创建新group、每次取buffer都新建buffer,长时间运行后内存分配次数会非常可观。正确的做法是在初始化时创建一个group,编码循环中重复使用mpp_buffer_get和mpp_buffer_put,让buffer在组内循环复用。
5.4 长期运行的稳定性几个建议
跑视频编码的程序,最怕的不是一开始跑不通,而是跑了一天后才崩。我总结几个长期运行的稳定性建议。
第一,定期检查mpi->control和encode_put_frame的返回值,任何一次非MPP_OK的返回都不应该被忽略。硬件编码器在长时间高负载下偶尔会返回错误,你至少要打印日志,方便事后定位。第二,把编码线程和其他业务线程分开,不要让网络阻塞直接影响编码节奏。第三,对于PTZ、对讲、云台控制这类低时延控制指令,一定不要和编码线程共用同一个锁。第四,如果项目允许,可以做看门狗机制,定期检查编码器是否还在正常出帧,连续超时后进行重启恢复。
这些听起来像老生常谈,但每一句都是我踩过坑之后才深刻体会的。尤其是线程锁的问题,曾经某次升级后控制指令和编码流程共用了互斥锁,结果网络慢的时候控制也卡,排查了两天才发现锁被编码线程占住了。
6. 常见问题排查与自查清单
6.1 首帧迟迟不来或没有SPS/PPS
如果调完配置后,encode_get_packet一直拿不到packet,或者拿到的第一个packet不是SPS/PPS开头,先检查三件事:header_mode有没有设置成MPP_ENC_HEADER_MODE_EACH_IDR;输入帧的width和stride是否一致;encode_put_frame之后是否紧接着调用了encode_get_packet。
我遇到过的情况是,只在初始化时调用了一次encode_get_packet,以为能拿到头信息,但实际上编码器要等到有帧送进去才会产生输出。正确做法是送完首帧后立即取包,这时拿到的才是带SPS/PPS的初始化码流。
6.2 花屏、绿屏和条纹
花屏是最常见的异常,原因通常出在输入数据上。第一个嫌疑是stride没对齐,导致硬件按错误的内存布局读取数据。第二个嫌疑是format填错,明明是NV12却填了I420或其他格式。第三个嫌疑是buffer数据没有写完整,特别是通过Cache写入的内存,需要在写完后做Cache刷新。
如果你用的是普通malloc内存然后memcpy,一般不需要手动刷Cache;但如果用了DMA/ION映射的内存,写入后可能需要调用相关接口保证数据可见。这个跟具体平台和SDK版本有关,遇到花屏时建议先查这三个点。
6.3 码率与设置值严重不符
码率设置的单位是最容易搞错的地方。rc:bps的单位是bit per second,所以4Mbps直接填4000000。如果你填的是4096000或者4,都会得到离谱的结果。
另一个原因是没有正确设置fps_in和fps_out。如果编码器认为输入帧率是10fps,而它内部又按10fps去做码控,实际接收30fps时码率就会膨胀到3倍。所以fps_in_num、fps_in_denorm、fps_out_num、fps_out_denorm这些字段一定要和实际帧率一致。
6.4 帧率上不去或延迟越来越大
帧率上不去,首先要看喂帧节奏。如果你在业务线程里做了很多耗时操作,再调用encode_put_frame,编码器就会等帧,输出自然上不去。建议把喂帧和取包放到一个独立线程里,并且按pts计算时间而非sleep固定值。
延迟越来越大的原因,十有八九是输出队列堆积。检查一下是不是取到packet后没有及时处理,是不是网络发送阻塞了,是不是业务队列没有上限。把packet释放提前到拷贝完成后立刻执行,延迟问题通常会缓解。
6.5 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 首帧不来 | 没送帧就取包 | 先encode_put_frame再encode_get_packet |
| 无SPS/PPS | header_mode配置不对 | 设置MPP_ENC_HEADER_MODE_EACH_IDR |
| 画面花屏 | stride/format/buffer 错误 | 重点检查NV12布局和hor_stride/ver_stride |
| 码率过高 | 单位填错或fps配置错 | 确认rc:bps单位,确认fps_in/out |
| 延迟增大 | 输出队列堆积 | 及时deinit packet,限制队列深度 |
| 程序崩溃 | buffer释放顺序错误 | 先mpp_frame_deinit再mpp_buffer_put |
| 多路互相干扰 | 共用ctx或buffer group | 每路独立ctx和group |
| 长期内存上涨 | packet/buffer泄漏 | 检查所有deinit和put是否成对 |
真要说有什么经验,我最想留给大家的是:拿到新板子,先别急着写业务代码,去跑一遍官方mpi_enc_test,把--type=264、--w=1920、--h=1080、--bps=4000000、--rc_mode=CBR这样的命令试通,确认这条链路在你手里的固件上没问题,再开始动应用层。我踩过最大的坑就是以为参数都对,结果总线上实际跑的stride和申请buffer对不上,画面花了两天,最后一张一张对比官方sample才找出来。硬件编码本身不复杂,复杂的是你愿不愿意把每个字段确认到底。希望这篇实践能帮你少熬夜。