☰
RK3568 MPP硬编实战:V4L2摄像头H.264/H.265编码推流全链路
2026/9/28 6:09:42 网站建设 项目流程

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的采集流程是固定的套路,不管什么平台都一样:

  1. 打开设备节点:open("/dev/video0", O_RDWR)
  2. 查询设备能力:VIDIOC_QUERYCAP,确认它支持VIDEO_CAPTURE
  3. 设置采集格式:VIDIOC_S_FMT,指定分辨率、像素格式
  4. 申请缓冲区:VIDIOC_REQBUFS,一般用MMAP方式,申请4个缓冲区
  5. 查询并映射缓冲区:VIDIOC_QUERYBUF拿到每个buffer的偏移和长度,然后mmap到用户空间
  6. 入队所有缓冲区:VIDIOC_QBUF
  7. 启动采集:VIDIOC_STREAMON
  8. 循环出队取帧:VIDIOC_DQBUF,处理完再VIDIOC_QBUF放回去
  9. 停止采集: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.264H.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 性能优化的几个方向

如果跑下来性能不达标,按这个顺序优化:

  1. 零拷贝:V4L2 buffer直接导入MPP,省掉memcpy。
  2. RGA转换:格式转换用RGA,别用CPU。
  3. 关B帧:B帧增加延迟和复杂度,推流场景一般不需要。
  4. 降分辨率或帧率:最直接有效,但画质和流畅度会降。
  5. 多线程:采集、编码、推流分到不同线程,用队列解耦。但要注意线程间的同步和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管理和时间戳这两块做扎实,剩下的就是调参的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询