1. 从摄像头到码流:RK3568 MPP编码链路的整体设计思路
1.1 为什么要在RK3568上走MPP硬编这条路
先把这个项目的核心说清楚:在RK3568这块瑞芯微的SoC上,把V4L2摄像头采集到的原始视频帧,经过MPP(Media Process Platform)硬件编码器压成H.264或H.265码流,再推出去。整条链路是“采集—格式转换—硬编—封装—推流”。
为什么不用软件编码?你在RK3568上跑个x264软编试试就知道了,1080p30帧,CPU四核A55基本被吃满,稍微加点业务逻辑就掉帧。而RK3568内置的VPU(视频处理单元)支持H.264/H.265的硬件编解码,1080p60对它来说很轻松,CPU占用能压到个位数。这就是硬编的意义——把最重的活交给专用硬件,CPU腾出来干别的。
MPP是瑞芯微提供的一套媒体处理框架,封装了底层VPU的调用。你可以把它理解成一个“翻译官”:上层应用用统一的API调它,它负责跟内核里的VPU驱动打交道。RK3568的MPP支持编码、解码、图像处理(RGA配合),我们这里主要用它的编码能力。
适合谁来参考这篇内容?嵌入式Linux方向的音视频开发者,手上有RK3568板子(或者RK3566,两者MPP接口基本一致),想跑通摄像头实时编码推流。如果你之前只玩过软编或者只在PC上跑过FFmpeg,这篇能帮你把嵌入式硬编的坑趟一遍。
1.2 整条链路的模块拆解与数据流向
把链路拆开看,一共四个环节:
- V4L2采集:摄像头通过MIPI CSI接口接入,内核里的V4L2驱动把sensor数据以视频节点(/dev/videoX)的形式暴露出来。应用层用V4L2的ioctl接口去申请缓冲区、入队、出队,拿到原始帧。
- 格式转换(可选但常见):摄像头出来的格式可能是NV12、YUYV、或者RAW Bayer。MPP编码器一般吃NV12或NV16。如果格式不匹配,要么用RGA做硬件转换,要么在MPP编码前用软件转。RGA是RK3568的2D加速器,做颜色空间转换和缩放比CPU快得多。
- MPP编码:把NV12帧送进MPP编码器,配置好编码参数(分辨率、码率、GOP、profile等),拿到编码后的H.264/H.265码流包。
- 封装与推流:编码出来的是裸码流(Elementary Stream),要推RTMP或者RTSP的话,得先封装成FLV或者RTP包。这一步可以用FFmpeg的库来做,也可以自己按协议拼包。
数据流向是单向的:摄像头→内存缓冲区→(RGA转换)→MPP输入队列→VPU编码→MPP输出队列→封装→网络发送。理解这个流向很重要,因为后面排查问题的时候,你得知道卡在哪一环。
1.3 方案选型的几个关键取舍
用MPP原生API还是FFmpeg的MPP封装?FFmpeg从4.x开始有rkMPP的支持,编译时开--enable-rkmpp就能用h264_rkmpp编码器。好处是接口统一,跟其他平台代码兼容。坏处是FFmpeg的MPP封装有时候版本对不上,而且它内部有自己的缓冲管理,延迟不如直接用MPP API可控。我个人的选择是:快速验证用FFmpeg,产品化用MPP原生API。
H.264还是H.265?RK3568两个都支持硬编。H.265同画质下码率大概省30%-40%,但兼容性差一些,有些老播放器或者浏览器不认。如果是内网推流、自己控制播放端,H.265更划算;如果要推给各种客户端,H.264更稳。另外H.265编码对VPU的负载略高,1080p60的话建议先测一下稳定性。
V4L2直接用还是走GStreamer?GStreamer有v4l2src和mpph264enc插件,搭管道很快。但GStreamer的抽象层厚,出问题不好定位,而且延迟控制不如直接写V4L2。调试阶段我建议直接用V4L2 ioctl,把每个环节都摸清楚,后面要优化也有抓手。
2. V4L2摄像头采集的核心细节与实操要点
2.1 V4L2采集的标准流程与关键ioctl
V4L2的采集流程是固定的套路,不管什么平台都一样:
- 打开设备节点:
open("/dev/video0", O_RDWR) - 查询设备能力:
VIDIOC_QUERYCAP,确认它支持VIDEO_CAPTURE - 设置采集格式:
VIDIOC_S_FMT,指定分辨率、像素格式 - 申请缓冲区:
VIDIOC_REQBUFS,一般用MMAP方式,申请4个缓冲区 - 查询并映射缓冲区:
VIDIOC_QUERYBUF拿到每个buffer的偏移和长度,然后mmap到用户空间 - 入队所有缓冲区:
VIDIOC_QBUF - 启动采集:
VIDIOC_STREAMON - 循环出队取帧:
VIDIOC_DQBUF,处理完再VIDIOC_QBUF放回去 - 停止采集:
VIDIOC_STREAMOFF
这套流程看着简单,但每一步都有坑。比如VIDIOC_S_FMT的时候,驱动可能会调整你请求的参数,你得读回实际设置的格式,不能想当然。
2.2 摄像头sensor适配与设备树配置
RK3568的摄像头接入涉及设备树配置。以OV5695为例(这是RK3568开发板上常见的sensor),设备树里要配好几个节点:
- I2C节点:sensor的寄存器配置走I2C,要确认I2C总线号、从机地址(OV5695一般是0x36)
- MIPI CSI节点:配置lane数、时钟频率。OV5695是2 lane的
- Port/Endpoint:把sensor的output endpoint连到RK3568的CSI input endpoint
- 时钟和电源:sensor需要的mclk、avdd、dvdd、dovdd都要在设备树里配好regulator
设备树配错了,最典型的现象是/dev/videoX节点出不来,或者出来了但VIDIOC_S_FMT报错。调试的时候先看dmesg | grep -i csi和dmesg | grep -i ov5695,驱动probe失败会有明确报错。
注意:RK3568和RK3566的摄像头接口基本兼容,但引脚复用和时钟树有差异,设备树不能直接照搬。RK3568的MIPI CSI有独立的时钟控制器,配置时要确认时钟源选对了。
2.3 像素格式选择与RGA转换的时机
摄像头输出的格式取决于sensor。OV5695支持RAW10,经过ISP处理后输出NV12或者YUYV。如果你的sensor直接出NV12,那太好了,MPP编码器直接能吃。如果出的是YUYV(YUY2),那就得转成NV12。
转换有两条路:
- RGA硬件转换:用librga的
imcvtcolor或者imresize接口,把YUYV转NV12。RGA做这个基本不占CPU,延迟也低。RK3568的RGA支持YUYV→NV12、RGB→NV12等常见转换。 - 软件转换:自己写个循环做YUV格式转换,或者用libyuv。1080p的话CPU占用大概10%-15%,能接受但不划算。
我实测下来,能用RGA就用RGA。但要注意RGA的输入输出格式枚举,有些格式组合它不支持,得查RK3568的RGA规格书。另外RGA的缓冲区要求物理连续,跟V4L2 MMAP出来的buffer对接时要注意DMA fd的传递。
2.4 采集环节的常见坑与排查
坑一:DQBUF阻塞不返回。一般是STREAMON没成功,或者缓冲区没全部入队。检查VIDIOC_STREAMON的返回值,确认QBUF的数量跟REQBUFS一致。
坑二:帧率上不去。先确认sensor的实际输出帧率,用v4l2-ctl --get-parm看。有些sensor默认15帧,得手动设成30。另外MIPI lane速率不够也会限制帧率,1080p30至少需要2 lane跑在较高频率。
坑三:图像偏色或者花屏。大概率是格式不匹配。比如sensor实际输出NV12,你按YUYV去解析,颜色就全乱了。用v4l2-ctl --list-formats-ext确认sensor支持的格式列表。
坑四:多摄像头同时采集带宽不够。RK3568的MIPI CSI和内存带宽有限,两个1080p30同时采集可能就到瓶颈了。这时候要么降分辨率,要么降帧率,要么用RGA做缩放后再编码。
3. MPP编码器的配置与编码实战
3.1 MPP编码器的初始化与参数配置
MPP编码的API调用流程大致是这样:
// 创建编码器上下文 MppCtx ctx; MppApi *mpi; mpp_create(&ctx, &mpi); // 初始化,指定编码类型 mpp_init(ctx, MPP_CTX_ENC, MPP_VIDEO_CodingAVC); // H.264用AVC,H.265用HEVC // 配置编码参数 MppEncCfg cfg; 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:format", MPP_FMT_YUV420SP); // NV12 mpp_enc_cfg_set_s32(cfg, "rc:mode", MPP_ENC_RC_MODE_CBR); // 固定码率 mpp_enc_cfg_set_s32(cfg, "rc:bps_target", 4000000); // 4Mbps mpp_enc_cfg_set_s32(cfg, "rc:gop", 60); // GOP 60帧 mpp_enc_cfg_set_s32(cfg, "h264:profile", 100); // High profile mpp_enc_cfg_set_s32(cfg, "h264:level", 40); // Level 4.0 mpi->control(ctx, MPP_ENC_SET_CFG, cfg);几个关键参数的选择逻辑:
- 码率模式:CBR(固定码率)适合网络推流,带宽可控;VBR(可变码率)画质更好但带宽波动大。推流场景一般用CBR。
- GOP:GOP越大,I帧越少,码率越低,但丢包后恢复越慢。推流一般设30-60。如果网络不稳,设小一点。
- Profile:H.264的High profile比Baseline省码率,但有些老设备不支持。兼容性优先用Baseline或Main。
- 码率目标:1080p30的话,H.264一般4-6Mbps,H.265可以降到3-4Mbps。具体看画质要求。
3.2 输入帧的送入与输出码流的获取
MPP的编码是异步的。你把帧送进去,它不一定马上出码流,可能攒几帧才吐一个包。所以要用两个队列来管理:
- 输入队列:从V4L2拿到的NV12帧,包装成MppBuffer,通过
mpi->encode_put_frame送入。 - 输出队列:通过
mpi->encode_get_packet获取编码后的码流包,每个包包含一段H.264/H.265数据。
这里有个关键点:MppBuffer的分配方式。如果V4L2的buffer能直接给MPP用(通过DMA fd导入),那就零拷贝,效率最高。但实际中V4L2的MMAP buffer和MPP的MppBuffer往往不能直接共享,需要一次memcpy。1080p NV12一帧大概3MB,memcpy一次大概1-2ms,30帧的话CPU占用约5%,可以接受。
如果要追求零拷贝,可以用mpp_buffer_import把V4L2的DMA fd导入成MppBuffer。但这要求V4L2驱动支持DMABUF导出,而且格式和stride要匹配。RK3568的V4L2驱动是支持DMABUF的,但sensor出来的stride可能跟MPP期望的不一致,需要仔细对齐。
3.3 H.264与H.265编码的差异与选择
| 对比项 | H.264 | H.265 |
|---|---|---|
| 同画质码率 | 基准 | 省30%-40% |
| 编码复杂度 | 低 | 高约50% |
| 兼容性 | 极好 | 一般 |
| RK3568硬编支持 | 支持 | 支持 |
| 1080p60 CPU占用 | 约5% | 约8% |
| 推荐场景 | 通用推流 | 内网/可控播放端 |
在MPP里切换H.264和H.265很简单,mpp_init的时候把编码类型从MPP_VIDEO_CodingAVC改成MPP_VIDEO_CodingHEVC就行。但H.265的配置项多一些,比如它没有profile的概念,取而代之的是main/main10等,MPP里用h265:profile来设。
实测下来,RK3568编1080p30的H.265,VPU占用大概40%-50%,还有余量。但如果你同时还在解码或者跑RGA,就要注意VPU的总负载,别超了。
3.4 编码质量调优的几个实用参数
MPP的编码器有一堆调优参数,挑几个最影响实际效果的:
- rc:quality:编码质量等级,范围1-10,默认5。调高画质好但码率可能超,调低码率稳但画质糊。推流场景建议6-7。
- rc:bps_min / bps_max:CBR模式下码率的上下限。设个合理的范围,避免码率波动太大。
- rc:gop_mode:GOP模式,可以设固定GOP或者智能GOP。智能GOP会根据场景变化调整I帧位置,适合画面变化大的场景。
- h264:entropy_coding_mode:熵编码模式,0是CAVLC,1是CABAC。CABAC省码率但费算力,RK3568的VPU支持CABAC,建议开。
调参的时候别一次改太多,改一个测一个,用mpp_info看实际输出的码率和帧大小。我一般会写个统计脚本,把每帧的码流大小打出来,看看波动是否正常。
4. 推流封装与端到端联调
4.1 裸码流到RTMP/RTSP的封装
MPP输出的是裸H.264/H.265码流,每个包是一个NALU或者多个NALU。要推RTMP,得封装成FLV格式;要推RTSP,得封装成RTP包。
RTMP封装:FLV的video tag里放的是AVC/HEVC的格式。H.264的话,第一个包要发AVCDecoderConfigurationRecord(包含SPS/PPS),后面每个包前面加个AVC NALU长度前缀。H.265类似,但配置记录格式不同。
RTSP封装:RTP包直接承载NALU,SPS/PPS通过SDP的sprop参数传递,或者用FU-A分片发送大NALU。
如果不想自己写封装,可以用FFmpeg的libavformat。把MPP输出的码流包喂给AVPacket,用av_interleaved_write_frame写到RTMP或者RTSP的AVFormatContext。这样省事,但要注意FFmpeg的时间戳管理,MPP输出的包可能没有PTS,得自己按帧率推算。
4.2 时间戳与同步的处理
时间戳是推流里最容易出问题的地方。MPP编码出来的包,PTS要么没有,要么是VPU的时钟,跟实际时间对不上。你得自己维护一个时间戳:
- 采集的时候记录每帧的单调时间(
clock_gettime(CLOCK_MONOTONIC)) - 编码后把采集时间戳赋给对应的码流包
- 封装的时候转成毫秒或者90kHz单位
如果时间戳不对,播放端会花屏、卡顿、音视频不同步(如果有音频的话)。我踩过的坑是:MPP输出的包顺序跟输入帧顺序不一定完全一致(B帧的原因),所以不能简单地按顺序赋时间戳,得根据包的PTS或者frame index来对应。
4.3 端到端延迟的测量与优化
延迟是推流的核心指标。整条链路的延迟来源:
| 环节 | 典型延迟 | 优化手段 |
|---|---|---|
| 采集 | 1-2帧 | 减少buffer数量,用低延迟模式 |
| 编码 | 1-3帧 | 关B帧,减小GOP,用低延迟编码 |
| 封装 | <1帧 | 避免额外缓冲 |
| 网络 | 可变 | 用低延迟协议,减少重传 |
| 播放端 | 2-5帧 | 播放器缓冲调小 |
端到端延迟做到200ms以内是可能的,但需要每个环节都优化。采集用2个buffer而不是4个,编码关掉B帧(mpp_enc_cfg_set_s32(cfg, "h264:bframes", 0)),GOP设小一点,播放端用低延迟模式。
实测RK3568从采集到编码输出的延迟大概50-80ms,加上网络和播放端,整体200-300ms。如果要更低,就得牺牲一些画质和稳定性。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| MPP编码失败 | 输入格式不对 | 确认NV12的stride和MPP期望一致 |
| 码流花屏 | 时间戳错乱 | 检查PTS是否单调递增 |
| 推流卡顿 | 码率超过带宽 | 降码率或改VBR |
| CPU占用高 | 用了软转格式 | 改用RGA转换 |
| 编码器初始化失败 | MPP版本不匹配 | 确认librknn和MPP版本 |
| 摄像头无数据 | 设备树配错 | dmesg看CSI和sensor probe |
| 帧率不稳定 | 内存带宽不足 | 降分辨率或帧率 |
| H.265播放不了 | 播放端不支持 | 换H.264或换播放器 |
提示:MPP的日志可以通过
mpp_log_set_level调整,调试的时候开到DEBUG,能看到每个包的详细信息。但生产环境记得关掉,不然日志会刷爆。
5. 实操心得与避坑经验
5.1 内存与缓冲区管理的经验
RK3568的内存有限,1080p NV12一帧3MB,4个采集buffer就是12MB,MPP编码器内部还要几个buffer,加起来几十MB就没了。如果同时跑多个摄像头或者高分辨率,内存很快就紧张。
我的做法是:采集buffer用2-3个就够了,不用4个。MPP的输入buffer复用采集buffer(通过DMABUF导入),减少一次拷贝和一份内存。输出码流buffer及时释放,别攒着。
另外,MPP的buffer分配有MPP_BUFFER_TYPE_DRM和MPP_BUFFER_TYPE_ION两种,RK3568上推荐用DRM,跟内核的DRM子系统配合更好。ION在新内核里逐渐被淘汰了。
5.2 调试工具与日志的用法
调试MPP和V4L2,几个工具必备:
- v4l2-ctl:查看和设置摄像头参数,
--list-formats-ext看支持的格式,--set-fmt-video设格式,--stream-mmap直接采集测试。 - mpp_info:MPP自带的工具,能看编码器的状态和统计信息。
- dmesg:内核日志,CSI、sensor、VPU的报错都在这里。
- top / perf:看CPU和VPU的占用,定位瓶颈。
我习惯在代码里加统计:每秒钟打印一次采集帧率、编码帧率、码率、CPU占用。这样跑起来一眼就能看出哪里不对。
5.3 性能优化的几个方向
如果跑下来性能不达标,按这个顺序优化:
- 零拷贝:V4L2 buffer直接导入MPP,省掉memcpy。
- RGA转换:格式转换用RGA,别用CPU。
- 关B帧:B帧增加延迟和复杂度,推流场景一般不需要。
- 降分辨率或帧率:最直接有效,但画质和流畅度会降。
- 多线程:采集、编码、推流分到不同线程,用队列解耦。但要注意线程间的同步和buffer管理。
RK3568是四核A55,主频最高2.0GHz,性能不算强。如果业务逻辑复杂,建议把编码和推流放到独立线程,主线程只做控制。
5.4 从RK3568到RK3566/RK3588的移植注意
RK3568和RK3566的MPP接口基本一致,代码可以直接复用。但RK3566的VPU频率略低,编码性能稍弱,1080p60可能吃力。RK3588就强多了,8核A76+A55,VPU支持8K编码,MPP的API也兼容,但性能参数和buffer配置要重新调。
移植的时候主要改设备树和时钟配置,MPP的应用层代码基本不用动。这也是用MPP的好处——硬件差异被框架屏蔽了,上层代码可移植性好。
5.5 一个完整的编译与运行示例
最后给个编译命令,基于RK3568的SDK:
# 交叉编译MPP应用 aarch64-linux-gnu-gcc -o mpp_enc_test mpp_enc_test.c \ -I${SDK_PATH}/buildroot/output/rockchip_rk3568/build/rockchip_mpp/inc \ -L${SDK_PATH}/buildroot/output/rockchip_rk3568/build/rockchip_mpp/lib \ -lrockchip_mpp -lrga -lpthread # 板子上运行 ./mpp_enc_test --width 1920 --height 1080 --fps 30 --codec h264 --bitrate 4000000跑起来之后,用ffplay或者vlc拉流验证:
ffplay -fflags nobuffer -flags low_delay rtmp://<板子IP>/live/stream-fflags nobuffer和-flags low_delay能降低播放端缓冲,方便测延迟。
这套东西我在RK3568上跑了几个月,稳定性没问题,7x24小时连续推流没出过崩溃。关键是把buffer管理和时间戳这两块做扎实,剩下的就是调参的事。