基于RK3568与C++的V4L2视频采集与RKMPP硬件编码实战:从摄像头到MP4文件
1. 环境准备搭建你的嵌入式视频处理工作站在RK3568上玩转视频采集和编码第一步就是把环境给搭好。这就像你要做一顿大餐得先把厨房收拾利索锅碗瓢盆、油盐酱醋都备齐了。我刚开始接触这块的时候也在这第一步上折腾过一阵子后来发现只要把依赖关系理清楚其实并不复杂。硬件清单首先你得有一块RK3568的开发板比如Firefly的ROC-RK3568-PC或者友善之臂的NanoPi R5S这些都是社区里比较热门的选择。摄像头的话市面上常见的USB摄像头比如罗技C920或者MIPI CSI接口的摄像头模组比如OV5640都可以。我个人更推荐用MIPI摄像头因为它直接走板子上的CSI接口带宽足、延迟低更适合嵌入式这种资源受限的环境。USB摄像头虽然即插即用方便但会占用USB总线带宽而且驱动层多了一层转换对CPU会有一点点额外的负担。软件依赖这是核心。你的RK3568板子跑的系统内核必须支持V4L2Video for Linux 2。现在主流的Buildroot或Yocto构建的镜像以及官方发布的Debian/Ubuntu系统基本都默认开启了。你可以用cat /proc/config.gz | gunzip | grep CONFIG_VIDEO_DEV来确认一下。接下来是Rockchip MPP库这是Rockchip自家芯片进行多媒体硬加速的“法宝”编解码、缩放、色彩转换这些重活累活都交给它CPU就能解放出来干别的。你需要从Rockchip的GitHub仓库https://github.com/rockchip-linux/mpp获取源码进行交叉编译。编译的时候有几个关键选项-DHAVE_AVSD、-DHAVE_AVS2D这些解码器支持按需开启但编码器尤其是H.264/H.265一定要确保编译进去。我建议直接使用他们release的预编译库能省不少事。最后是FFmpeg库。很多人会问我们不是用RKMPP编码吗为什么还要FFmpeg这里要搞清楚分工RKMPP是负责“生产”H.264/H.265这种压缩后的视频裸流ES流而FFmpeg是负责“包装”把它封装进MP4、MKV这种容器里加上文件头、索引信息还能处理音频流如果你有的话。所以FFmpeg在这里主要用到它的libavformat和libavcodec用于封装而非编码。同样需要交叉编译配置时记得禁用掉你不需要的组件比如--disable-encoders因为我们用硬件编码但一定要开启--enable-protocolfile和--enable-muxermp4。把这些库都编译好放到开发板的/usr/local/lib下或者设置好LD_LIBRARY_PATH环境变量你的“厨房”就算准备就绪了。我习惯在PC上先用交叉编译工具链把所有的.so文件都准备好打个包然后一次性scp到板子上比在板子上慢慢编译快多了。2. V4L2视频捕获打开摄像头的“眼睛”环境好了我们就要让摄像头“睁眼”看世界了。V4L2是Linux下视频设备的统一接口它提供了一套像文件操作一样的APIopen, ioctl, mmap等来和摄像头打交道。这个过程有点像去图书馆借书先找到图书馆打开设备文件说明你想借什么类型的书设置视频格式申请几个借书卡请求缓冲区然后把书的具体位置记下来内存映射最后开始借阅流程启动流。初始化与格式协商首先用open打开设备文件通常是/dev/video0。接下来是最关键的一步——设置格式。这里有个坑不是所有摄像头都支持所有格式。你需要先查询VIDIOC_ENUM_FMT摄像头支持哪些像素格式比如V4L2_PIX_FMT_YUYV打包的YUV422、V4L2_PIX_FMT_MJPEG、或者V4L2_PIX_FMT_H264如果摄像头直接输出H.264。我们的目标是拿到原始的YUV数据给RKMPP编码所以优先选择V4L2_PIX_FMT_NV12YUV420SP或V4L2_PIX_FMT_YUYV。如果摄像头不支持NV12你可能需要后续做一次色彩空间转换。设置格式时除了宽高和像素格式field字段也很重要对于逐行扫描的摄像头设为V4L2_FIELD_NONE就行。缓冲区管理与内存映射V4L2采集视频流通常使用“内存映射”模式这是效率最高的一种方式。你需要用VIDIOC_REQBUFS告诉驱动“我准备申请4个缓冲区具体数量可以调整”。驱动会在内核空间准备好这些缓冲区。然后对每个缓冲区用VIDIOC_QUERYBUF查询它的长度和偏移量再用mmap系统调用把这个内核缓冲区映射到你的用户空间程序里。这样当摄像头数据填满缓冲区后你就能直接在用户态访问这些图像数据避免了数据从内核到用户空间的拷贝开销。映射完后别忘了用VIDIOC_QBUF把每个缓冲区“入队”交给驱动去填充数据。启动采集与数据循环一切就绪后一个VIDIOC_STREAMON命令摄像头就开始哗哗地往外吐数据了。在主循环里你的工作流程是这样的调用VIDIOC_DQBUF从驱动已填充的队列里“出队”一个装满数据的缓冲区这时你就可以读取mmap得到的指针指向的数据了这就是一帧完整的图像。处理完这帧数据比如送去编码后必须再调用VIDIOC_QBUF把这个缓冲区重新“入队”还给驱动去装下一帧数据。这个过程是循环的保证了视频流的持续采集。这里要特别注意错误处理和资源释放如果DQBUF阻塞太久可能是摄像头断开了或者出了其他问题需要超时机制。3. RKMPP硬件编码初始化唤醒芯片里的编码“引擎”从V4L2拿到原始的YUV数据后如果直接用CPU去压缩成H.264那RK3568的小心脏可能很快就“扑通扑通”跳得飞快了。这时候就该RKMPP出场了它集成了专门的视频编解码硬件单元VPU干这个活又快又省电。初始化RKMPP编码器就像是给这个硬件引擎设定工作模式、燃油标号和输出功率。创建上下文与基础配置首先调用mpp_create创建编码器上下文MppCtx和接口实例MppApi。之后的大部分操作都是通过这个mpi接口调用control方法来完成。第一步是设置输入数据的格式通过MPP_SET_OUTPUT_FORMAT命令注意这个命令名有点反直觉它指的是编码器“输出”给后续处理单元的格式对我们而言就是输入格式。这里必须和V4L2采集上来的格式对齐如果是NV12就设MPP_FMT_YUV420SP如果是YUYV可能需要先转换成NV12再输入或者尝试设置成对应的格式看MPP是否支持。编码参数精细调优接下来是重头戏配置编码参数。主要分三块编码器类型通过MPP_ENC_SET_CODEC_CFG设置coding为MPP_VIDEO_CodingAVC表示我们用H.264编码。如果想用H.265就设为MPP_VIDEO_CodingHEVC。图像预处理配置通过MPP_ENC_SET_PREP_CFG设置prep_cfg。这里除了宽高还有hor_stride和ver_stride这是内存对齐的步长。有时候图像宽度不是16或32的倍数但硬件要求内存对齐这时实际存储一行的字节数stride会大于宽度对应的字节数。如果你从V4L2拿到的数据已经是对齐好的这里就需要正确设置否则会出现画面错位。码率控制配置通过MPP_ENC_SET_RC_CFG设置rc_cfg。这是影响视频体积和画质的关键。rc_mode常用的是MPP_ENC_RC_MODE_CBR恒定码率网络传输常用或MPP_ENC_RC_MODE_VBR可变码率本地存储画质更优。bps_target是目标码率比如4Mbps就设4000000。你还可以设置bps_max,bps_min,qp_init,qp_max,qp_min等来控制画质。我实测下来对于1080p30fps的视频CBR模式设4-5MbpsVBR模式设最大6Mbps、最小2Mbps出来的效果和文件大小都比较均衡。配置完成后编码器就处于待命状态了。这里有个经验RKMPP的编码器对输入帧的时序有一定要求最好能按照你设定的帧率比如30fps稳定地送入帧数据这样编码器内部的状态控制比如I帧间隔会更准确。4. 数据流转与格式桥接让YUV“丝滑”变成H.264环境搭好了摄像头能读了编码器也准备好了现在最难的一步来了怎么把V4L2读出来的一帧帧YUV数据“喂”给RKMPP编码器再把编码器吐出来的H.264数据包“递”给FFmpeg去封装这个数据管道搭建不好整个程序就会卡壳。从V4L2到MPP数据搬运与格式对齐首先从V4L2DQBUF出来后你拿到的是一个指针和这帧数据的大小。这个指针指向的内存格式是你之前设置好的比如YUYV。而RKMPP编码器期望的输入通常需要封装成一个MppFrame对象。你需要创建一个MppFrame设置它的宽、高、格式、像素数据指针以及一个非常重要的参数——时间戳PTS。PTS是这一帧图像在时间轴上的位置单位是时间基timebase。你可以用系统时钟或者递增的帧序号来生成。对于30fps的视频第一帧PTS为0第二帧就可以是1/30秒如果时间基是1/90000那PTS就是3000。把MppFrame通过mpi-encode_put_frame送入编码器。这里最大的一个坑是格式转换。如果你的摄像头只输出YUYVV4L2_PIX_FMT_YUYV但RKMPP编码器只接受NV12MPP_FMT_YUV420SP你就必须在中间做一次转换。我强烈推荐使用libyuv这个开源库来做它用汇编优化过速度比你自己写C语言转换快很多。转换函数大概是YUY2ToNV12这样的。这一步会消耗一些CPU资源但为了能利用硬件编码这是值得的。我曾经尝试过用OpenCV的cvtColor但依赖太大在嵌入式上不划算。从MPP到FFmpeg编码数据包提取编码器不是吃进一帧就立刻吐出一包它可能会缓存几帧或者为了优化码流结构延迟输出。所以你需要在一个循环里不断调用mpi-encode_get_packet尝试获取编码后的MppPacket。拿到MppPacket后从中提取出数据指针和长度这就是一包H.264的NAL单元可能包含一帧或多帧的切片数据。你需要创建一个FFmpeg的AVPacket把数据拷贝进去注意这里通常需要一次内存拷贝因为MPP管理的内存生命周期可能和我们不一样。同时要把MppPacket里携带的PTS、DTS解码时间戳、以及是否为关键帧I帧的标志正确地设置到AVPacket中。关键帧的标志AV_PKT_FLAG_KEY对于视频的随机播放和 seeking 至关重要一定要设对。5. FFmpeg MP4封装给视频流一个“家”RKMPP产出的是一段段连续的H.264裸流数据它没有文件头没有索引播放器不知道这个视频多长、从哪里开始播放、怎么快速跳转到中间。FFmpeg的封装器Muxer就是来解决这个问题的它把裸流打包进一个容器比如MP4生成标准的文件结构。初始化封装上下文首先调用avformat_alloc_output_context2创建一个输出上下文AVFormatContext指定格式为NULL让FFmpeg根据文件名后缀猜测比如.mp4或者直接指定mp4。然后你需要为这个MP4文件创建一个视频流AVStream。关键是要正确设置流的codecpar编解码器参数codec_id设为AV_CODEC_ID_H264codec_type设为AVMEDIA_TYPE_VIDEO宽高也要设置。还有一个容易忽略的是time_base时间基它定义了PTS/DTS数值的单位。这里要和前面生成PTS时用的时间基保持一致或者设置一个通用的比如{1, 90000}。写入文件头与数据包调用avio_open打开实际的文件用于写入。接着avformat_write_header写入MP4文件的文件头。这个函数调用后FFmpeg会在文件开头写入ftyp,moov等盒子Box其中moov盒子包含了整个视频的索引信息。在早期的流式封装中moov在文件末尾不利于网络流播放现在默认会在文件头。之后在主循环中每从RKMPP拿到一个AVPacket就调用av_write_frame把它写入文件。这个函数会处理好数据包在文件中的位置并逐步构建索引。结束写入与资源清理当视频采集结束比如用户按下停止键你需要调用av_write_trailer。这个函数非常重要它会完成moov盒子索引的最终计算和写入如果之前没写完的话确保MP4文件是完整可用的。最后按顺序关闭IO上下文avio_closep并释放格式上下文avformat_free_context。如果不调用av_write_trailer就直接关闭文件生成的MP4文件很可能是损坏的无法播放。我在这踩过坑录了半小时视频最后发现文件是坏的就是因为忘记写 Trailer 了。6. 性能调优与实战踩坑笔记把整个流程跑通只是第一步要让它在RK3568上稳定、高效地跑起来还需要一些调优和避坑。这里分享几个我实战中总结的经验。内存与缓冲区管理V4L2缓冲区数量不是越多越好。太少了容易丢帧因为生产者-消费者速度不匹配太多了会增加内存占用和初始延迟。对于1080p30fps4-6个缓冲区是个不错的起点。你可以通过VIDIOC_REQBUFS尝试不同的数量用top命令看内存变化并观察是否有丢帧警告通过v4l2-ctl --all查看设备状态。MPP输入帧缓冲RKMPP编码器内部也有缓冲队列。如果发现编码输出延迟很大可以尝试在送入MppFrame时检查mpp_frame_put_buffer的返回值如果返回MPP_NOK表示队列可能满了需要等待或丢弃帧。零拷贝尝试理论上V4L2的mmap内存可以直接作为MppBuffer的输入避免一次内存拷贝。这需要深入研究MPP的MppBuffer创建接口看是否支持从已有的用户态内存创建。我试过但没完全成功稳定性有待验证目前更稳妥的做法还是拷贝到MPP自己管理的内存中。参数配置经验I帧间隔GOP在MPP_ENC_SET_CODEC_CFG中可以设置gop参数比如设为30表示每30帧一个关键帧。GOP太长视频 seeking 会慢GOP太短压缩效率低文件变大。对于本地存储可以设大一点比如250-300对于网络流建议设小一点比如30-60。编码预设Profile和LevelH.264有 Baseline、Main、High Profile。RK3568的VPU支持到High Profile。Level如4.2限制了最大分辨率、帧率和码率。根据你的分辨率帧率选择合适的Level在rc_cfg里设置profile和level。一般1080p30fps用 High Profile, Level 4.2 足够了。多线程优化可以考虑将V4L2采集、格式转换、MPP编码、FFmpeg写入放在不同的线程用生产者-消费者队列连接。特别是格式转换比较耗CPU单独一个线程可以防止阻塞采集。但线程间同步和数据拷贝会引入复杂度需要权衡。调试与问题排查使用v4l2-ctl这是调试V4L2的神器。v4l2-ctl --list-formats查看摄像头支持的格式v4l2-ctl --set-fmt-videowidth1920,height1080,pixelformatNV12可以手动设置参数v4l2-ctl --stream-mmap3 --stream-count100 --stream-totest.raw可以直接抓取原始帧数据到文件用ffplay播放来验证采集是否正确。检查MPP日志编译MPP时开启调试选项运行程序时设置环境变量export MPP_LOG_LEVEL5或更高可以在终端看到详细的MPP运行日志对于排查编码器初始化失败、参数错误非常有帮助。分段测试不要试图一口气写完所有代码。先写个简单的程序只用V4L2把数据保存成文件用ffplay播放看是否正确。再写个测试程序用MPP编码一个静态的YUV图像文件。最后再把两者结合起来。分而治之是解决复杂嵌入式问题的不二法门。整个项目从无到有搭建起来确实会遇到不少麻烦比如驱动不匹配、库版本冲突、内存对齐问题等等。但当你最终看到摄像头拍摄的画面被流畅地编码并保存成一个可以在电脑上直接双击播放的标准MP4文件时那种成就感是非常实在的。嵌入式视频开发就是这样每一步都很具体踩的每一个坑都会让你对系统理解更深一层。希望这些详细的步骤和经验能帮你少走些弯路更快地在RK3568上实现自己的视频应用。