本篇主要内容
-
- 一、v4l2多平面采集
-
- [1. v4l2的Multi-Plane(多平面)](#1. v4l2的Multi-Plane(多平面))
- (1)什么是平面(plane)
- (2)单平面(Single-Plane)和多平面(Multi-Plane)
- (3)v4l2_plane结构体
- [2. v4l2多平面捕获摄像头数据](#2. v4l2多平面捕获摄像头数据)
- 二、v4l2+mpp编码
一、v4l2多平面采集
1. v4l2的Multi-Plane(多平面)
(1)什么是平面(plane)
🏸在v4l2框架里,本质是存储视频帧分量的一块内存区域,或者叫缓冲区,是v4l2框架中的一个抽象概念,对承载单分量数据的内存块的属性封装。
为什么在这里提到plane这个概念呢?是因为前面在使用像ov5640或者usb摄像头比较低端的sensor,通常只支持单平面,使用的V4L2_BUF_TYPE_VIDEO_CAPTURE这种方式;相对高端的sensor,如imx415的时候这种方式是不适用的,查阅资料后可以发现imx415支持的是单平面和多平面存储分量视频帧的方式,所以在这里简单记录plane这一概念
(2)单平面(Single-Plane)和多平面(Multi-Plane)
| 维度 | 单平面(Single-planar) | 多平面(Multi-planar) |
|---|---|---|
| 内存布局 | 所有分量数据连续存储在同一块内存 | 不同分量数据存在独立的内存块(平面) |
| V4L2 类型标识 | V4L2_BUF_TYPE_VIDEO_CAPTURE |
V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE |
| 核心结构体 | struct v4l2_buffer(无平面数组) |
struct v4l2_buffer + struct v4l2_plane |
| 适用场景 | 简单格式(如 RGB24、MJPEG) | 复杂格式(YUV420、H.264/H.265、NV12) |
| 内存效率 | 低(需预留最大帧长度的连续内存) | 高(各平面按需分配内存) |
🏸以NV12为例,在v4l2中有以下几个宏定义V4L2_PIX_FMT_NV12、V4L2_PIX_FMT_NV12M、V4L2_PIX_FMT_NV12MT。这三个宏所描述的都是NV12格式(YUV420,Y整行存储,UV交错存储),区别仅在于内存布局、是否支持多平面、是否为平铺(Tile)模式,直接决定摄像头驱动和应用层如何读取/写入图像数据,从我们使用的角度,只需要关心以下区别:
| 宏定义 | 类型 | 一般使用场景 |
|---|---|---|
| V4L2_PIX_FMT_NV12 | 单平面打包格式(num_planes=1) | 通用软解码、普通级sensor |
| V4L2_PIX_FMT_NV12M | 多平面分离格式(num_planes=2) | 硬件编解码、高性能处理器 |
| V4L2_PIX_FMT_NV12MT | 平铺多平面格式(num_planes=2) | 硬件ISP、GPU、高端图像处理器 |
(3)v4l2_plane结构体
🏸如何计算一帧NV12(YUV420)数据的大小?
首先要知道YUV420是指Y、U、V分量的比例为4:2:0(亮度Y,色度U和V),也就是4个像素点里,有4个Y亮度和2个U色度。那么在分辨率为1920x1080的一帧数据中,一共有Y:1920x1080=2073600,U:1920x1080/2=1036800。所以一帧NV12(YUV420)的数据大小是,Y+U:1920x1080x3/2=3110400(byte)
🏸这个结构体主要由底层填充,用于我们获取缓冲区信息或者说内存的信息,使用上需要区别v4l2_buffer这个结构体,以单平面一帧1920x1080的NV12图像为例,其含义如下:
| 字段 | 含义 | 示例值(NV12) |
|---|---|---|
planes.length |
对应的内核缓冲区总内存大小(预分配的字节数) | 3133440字节(对齐后) |
planes.m.mem_offset |
在内核内存中的偏移量(标识内存位置) | 0x0 |
planes.bytesused |
该plane中实际有效数据的字节数 | 3110400字节(有效数据) |
planes.m.fd |
DMA-BUF 模式下有效 | 0 |
2. v4l2多平面捕获摄像头数据
🏸这里实现效果是将NV12数据保存在testfile文件中,可以使用mpi_enc_test这个rk例程转h264播放验证,命令如下:
shell
mpi_enc_test -i ./testfile -o ./test.h264 -w 1920 -h 1080 -f YUV420SP -t h.264 -fps 30


c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <pthread.h>
#include <linux/videodev2.h>
#include <rockchip/rk_mpi.h>
#include <rockchip/rk_mpi_cmd.h>
#include <rockchip/mpp_err.h>
#include <rockchip/rk_venc_kcfg.h>
#include "v4l2_capture.h"
static int planes_num;
int camera_init(struct camera *cam) {
if (cam == NULL) return -1;
/* 打开摄像头设备 */
cam->fd = open(cam->device_node, O_RDWR | O_NONBLOCK);
if (cam->fd < 0) {
perror("open()");
return -1;
}
/* 设置格式 */
struct v4l2_format fmt = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, // 多平面采集类型
.fmt.pix_mp.width = cam->width,
.fmt.pix_mp.height = cam->height,
.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV12M,
.fmt.pix_mp.field = V4L2_FIELD_NONE,
};
if (ioctl(cam->fd, VIDIOC_S_FMT, &fmt) < 0) {
perror("VIDIOC_S_FMT");
close(cam->fd);
return -1;
}
/* 实际生效的格式 */
if (ioctl(cam->fd, VIDIOC_G_FMT, &fmt) < 0){
perror("VIDIOC_G_FMT");
close(cam->fd);
return -1;
}
printf("%s: using format:%c%c%c%c\n", cam->device_node,
fmt.fmt.pix_mp.pixelformat & 0xff,
(fmt.fmt.pix_mp.pixelformat >> 8) & 0xff,
(fmt.fmt.pix_mp.pixelformat >> 16) & 0xff,
(fmt.fmt.pix_mp.pixelformat >> 24) & 0xff);
printf("%s: using plane:%d\n", cam->device_node,fmt.fmt.pix_mp.num_planes);
planes_num = fmt.fmt.pix_mp.num_planes;
/* 申请buffer */
struct v4l2_requestbuffers reqbuf = {
.count = 4,
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
.memory = V4L2_MEMORY_MMAP
};
if (ioctl(cam->fd, VIDIOC_REQBUFS, &reqbuf) < 0) {
perror("VIDIOC_REQBUFS");
close(cam->fd);
return -1;
}
if (reqbuf.count < 0) {
fprintf(stderr, "alloc buf occurred error.%d \n", reqbuf.count);
ioctl(cam->fd, VIDIOC_REQBUFS, &reqbuf);
close(cam->fd);
return -1;
}
/* 映射内核缓冲区到用户空间,并入队 */
struct v4l2_buffer buf_msg;
struct v4l2_plane plane_msg[2] = {0};
for (int i = 0; i < fmt.fmt.pix_mp.num_planes; i++) {
buf_msg.index = i;
buf_msg.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf_msg.memory = V4L2_MEMORY_MMAP;
buf_msg.m.planes = plane_msg;
buf_msg.length = fmt.fmt.pix_mp.num_planes;
// 查询缓冲区信息
if (ioctl(cam->fd, VIDIOC_QUERYBUF, &buf_msg) < 0) {
perror("VIDIOC_QUERYBUF");
ioctl(cam->fd, VIDIOC_REQBUFS, &reqbuf);
close(cam->fd);
return -1;
}
// mmap映射
cam->user_buf[i] = mmap(NULL, buf_msg.m.planes[i].length, PROT_READ | PROT_WRITE,
MAP_SHARED, cam->fd, buf_msg.m.planes[i].m.mem_offset);
if (cam->user_buf[i] == MAP_FAILED) {
perror("mmap");
return -1;
}
// 缓冲区入队
if (ioctl(cam->fd, VIDIOC_QBUF, &buf_msg) < 0) {
perror("VIDIOC_QBUF");
munmap(cam->user_buf[i], plane_msg[i].length);
cam->user_buf[i] = NULL;
return -1;
}
}
return 0;
}
int camera_start_stream(struct camera *cam) {
if (cam == NULL || cam->fd < 0) return -1;
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
if (ioctl(cam->fd, VIDIOC_STREAMON, &type) < 0) {
perror("VIDIOC_STREAMON");
return -1;
}
printf("capture stream on.\n");
return 0;
}
void *capture_thread(void *arg){
struct camera *cam = arg;
/* select监听fd读事件 */
fd_set read_set;
int nready;
int frame_count = 0;
struct timeval tv = {
.tv_sec = 5,
.tv_usec = 0
};
/* NV12数据保存成文件 */
FILE *file = fopen("testfile","wb");
if (!file) {
perror("fopen file");
}
/* 获取采集到的帧 */
struct v4l2_plane planes[VIDEO_MAX_PLANES];
struct v4l2_buffer v4l2_buf = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
.memory = V4L2_MEMORY_MMAP,
.m.planes = planes,
.length = VIDEO_MAX_PLANES
};
FD_ZERO(&read_set);
FD_SET(cam->fd, &read_set);
while(1){
nready = select(cam->fd + 1, &read_set, NULL, NULL, 0);
if (nready < 0) {
perror("select");
break;
}
if (nready == 0) {
fprintf(stderr, "select() times out.\n");
sleep(1);
continue;
}
//出队
if (ioctl(cam->fd, VIDIOC_DQBUF, &v4l2_buf) < 0) {
perror("VIDIOC_DQBUF");
}
frame_count++;
for (int i = 0; i < planes_num; i++) {
fwrite(cam->user_buf[i], v4l2_buf.m.planes[i].bytesused, 1, file);
printf("NV12 frame %d captured.\n",frame_count);
printf("---------> v4l2_buf.m.planes[%d].length=%d.\n", i,v4l2_buf.m.planes[i].length);
printf("---------> v4l2_buf.m.planes[%d].bytesused=%d.\n",i,v4l2_buf.m.planes[i].bytesused);
printf("---------> v4l2_buf.m.planes[%d].m.mem_offset=%d.\n",i, v4l2_buf.m.planes[i].m.mem_offset);
}
//缓冲区重新入队列(循环使用)
if (ioctl(cam->fd, VIDIOC_QBUF, &v4l2_buf) < 0) {
perror("ioctl VIDIOC_QBUF failed (Multiplanar)");
}
}
fclose(file);
}
int camera_capture_frame(struct camera *cam) {
camera_start_stream(cam);
pthread_t tid;
if(pthread_create(&tid,NULL,capture_thread,cam) != 0){
fprintf(stderr,"pthread_create() occurred error.\n");
return -1;
}
return 0;
}
int camera_deinit(struct camera *cam){
//TODO
}
c
#ifndef _V4L2_CAPTURE_H_
#define _V4L2_CAPTURE_H_
#include <linux/videodev2.h>
struct camera{
int fd;
int buffer_count;
int width;
int height;
struct v4l2_buffer *user_buf[4];
char *device_node;
};
int camera_init(struct camera *cam);
int camera_capture_frame(struct camera *cam);
int camera_deinit(struct camera *cam);
#endif
c
#include <stdio.h>
#include <unistd.h>
#include "v4l2_capture.h"
int main(){
struct camera cam = {
.device_node = "/dev/video31",
.width = 1920,
.height = 1080,
};
camera_init(&cam);
camera_capture_frame(&cam);
while(1)sleep(1);
return 0;
}
二、v4l2+mpp编码
从摄像头录制h.264视频
🏸上面我们已经大致封装好v4l2捕获摄像头数据的代码,本小节再此基础上,整合前一篇mpp编码相关的代码,实现从摄像头直接录制出h.264文件这一效果,目录结构如下:

🏸由于多平面捕获摄像头在这里不好处理,于是此处代码改成了单平面格式,demo实现的效果和以下两条命令类似。这里写得也比较简单,很多处理并没有做,编译和运行结果如图。参考代码
shell
gst-launch-1.0 -e v4l2src device=/dev/video31 num-buffers=300 ! video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1 ! filesink location=raw.nv12
mpi_enc_test -i ./raw.nv12 -o ./encoded.h264 -w 1920 -h 1080 -f YUV420SP -t h.264




