如下是 V4L2 应用程序开发的一般流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 用户态程序 | | open(" /dev/video20") v file descriptor fd | | VIDIOC_QUERYCAP v 确认是 Video Capture + Streaming | | VIDIOC_S_FMT v 驱动返回实际格式:width/height/fourcc/stride/sizeimage | | VIDIOC_REQBUFS v 申请 mmap buffer 队列 | | VIDIOC_QUERYBUF + mmap + VIDIOC_QBUF v buffer 交给驱动 | | VIDIOC_STREAMON v 驱动开始采帧 | | select + VIDIOC_DQBUF v 用户态拿到一帧 | | 处理帧数据 v 保存 / 转换 / 送 MPP/RGA/RKNN | | VIDIOC_QBUF v 归还 buffer | | VIDIOC_STREAMOFF + munmap + close v 结束
本文将围绕这个流程图详细展开。
一、打开设备文件
V4L2 摄像头在 Linux 里是字符设备:
通过系统调用 open 直接打开:
1 2 3 4 5 int fd = open(device, O_RDWR | O_CLOEXEC);if (fd < 0 ) { perror("open" ); return -1 ; }
为什么用 O_RDWR:
后面要 ioctl
后面要把 buffer QBUF 给驱动
有些节点只读也能部分工作,但捕获程序常用 O_RDWR
为什么用 O_CLOEXEC:
exec 子进程时自动关闭 fd,避免泄漏
常见错误:
错误
原因
No such file or directory
/dev/videoN 不存在
Permission denied
当前用户不在 video 组,或 udev 权限不对
Device or resource busy
摄像头被其他进程占用
V4L2 里常见的 ioctl 命令:
命令
作用
VIDIOC_QUERYCAP
查询设备能力(是否支持 Video Capture、Metadata Capture 等)
VIDIOC_ENUM_FMT
枚举设备支持的像素格式
VIDIOC_ENUM_FRAMESIZES
枚举某格式支持的分辨率
VIDIOC_ENUM_FRAMEINTERVALS
枚举某分辨率的帧率
VIDIOC_S_FMT
设置采集格式(分辨率、像素格式)
VIDIOC_G_FMT
获取当前采集格式
VIDIOC_REQBUFS
申请缓冲区
VIDIOC_QBUF / VIDIOC_DQBUF
把缓冲区放入队列 / 从队列取出
VIDIOC_STREAMON / VIDIOC_STREAMOFF
开始 / 停止视频流
V4L2 支持三种常见 I/O。由于视频数据量大、帧率高,必须避免在用户空间和内核空间之间来回拷贝,推荐使用 MMAP:
方式
特点
read/write
简单,但通常有拷贝,不适合高性能
mmap streaming
驱动和用户态共享 buffer,V4L2 主线
userptr / dmabuf
更底层的零拷贝/跨设备共享
二、查询设备能力
1 2 3 4 5 6 7 8 9 10 [linux/videodev2.h]struct v4l2_capability { __u8 driver[16 ]; __u8 card[32 ]; __u8 bus_info[32 ]; __u32 version; __u32 capabilities; __u32 device_caps; __u32 reserved[3 ]; };
v4l2_capability 是用来描述一个视频设备“能做什么、叫什么、由谁驱动”。它是配合 VIDIOC_QUERYCAP 这个 ioctl 命令使用的数据结构。当你调用:
1 2 3 struct v4l2_capability cap ;memset (&cap, 0 , sizeof (cap)); ioctl(fd, VIDIOC_QUERYCAP, &cap);
内核就会把这个设备的能力信息填进 cap 里。
capabilities 和 device_caps 的区别,核心在于**“物理设备” vs “当前节点”**
字段
描述对象
回答的问题
capabilities
整个物理设备
这个硬件整体能做什么?
device_caps
当前这个 /dev/videoX 节点
我现在打开的这个节点能做什么?
1 2 3 2 K USB Camera(一个物理设备) ├── /dev/ video20 → 视频采集节点 └── /dev/ video21 → 元数据节点
对整个摄像头 来说,它既能采集视频,也能输出元数据。所以 capabilities 会同时包含 V4L2_CAP_VIDEO_CAPTURE 和 V4L2_CAP_META_CAPTURE。
但具体到某个节点 :
video20 只能采集视频 → device_caps 只有 V4L2_CAP_VIDEO_CAPTURE
video21 只能采集元数据 → device_caps 只有 V4L2_CAP_META_CAPTURE
capabilities / device_caps 是位掩码,常见的有:
标志
含义
V4L2_CAP_VIDEO_CAPTURE
支持视频采集(读画面)
V4L2_CAP_VIDEO_OUTPUT
支持视频输出
V4L2_CAP_VIDEO_CAPTURE_MPLANE
支持多平面视频采集
V4L2_CAP_META_CAPTURE
支持元数据采集
V4L2_CAP_STREAMING
支持流式 I/O(mmap + 队列)
V4L2_CAP_READWRITE
支持 read()/write() 方式
V4L2_CAP_DEVICE_CAPS
表示 device_caps 字段有效
需要注意的是,通过 capabilities 是无法判断前面摄像头的两个节点(因为它们来自同一个物理设备)。要通过 device_caps 才可以判断节点是 V4L2_CAP_VIDEO_CAPTURE 还是 V4L2_CAP_META_CAPTURE。
因为老版本内核的驱动可能只填了 capabilities,没有填 device_caps。这时候如果程序直接读 device_caps,拿到的可能是 0 或者垃圾值,所以要做如下判断。
1 2 3 4 uint32_t effectiveCaps (const v4l2_capability &cap) { return (cap.capabilities & V4L2_CAP_DEVICE_CAPS) ? cap.device_caps : cap.capabilities; }
然后就可以判断设备节点是视频节点还是数据节点:
1 2 3 4 if (caps & V4L2_CAP_VIDEO_CAPTURE) if (caps & V4L2_CAP_META_CAPTURE)
当你用 v4l2-ctl --info 时,看到的输出就是来自这个结构体。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 $ sudo v4l2-ctl -d /dev/video20 --info Driver Info: Driver name : uvcvideo Card type : 2K USB Camera: 2K USB Camera Bus info : usb-fc800000.usb-1.1 Driver version : 6.1.99 Capabilities : 0x84a00001 Video Capture Metadata Capture Streaming Extended Pix Format Device Capabilities Device Caps : 0x04200001 Video Capture Streaming Extended Pix Format Media Driver Info: Driver name : uvcvideo Model : 2K USB Camera: 2K USB Camera Serial : 46435000_P030D00_SN0002 Bus info : usb-fc800000.usb-1.1 Media version : 6.1.99 Hardware revision: 0x00001100 (4352) Driver version : 6.1.99 Interface Info: ID : 0x03000002 Type : V4L Video Entity Info: ID : 0x00000001 (1) Name : 2K USB Camera: 2K USB Camera Function : V4L2 I/O Flags : default Pad 0x01000007 : 0: Sink Link 0x02000010: from remote pad 0x100000a of entity 'Extension 4' (Video Pixel Formatter): Data, Enabled, Immutable
三、设置视频格式
v4l2_format 是一个聚合结构体 ,用来描述或协商 V4L2 设备上的数据流格式。它本身不存储具体的图像参数,而是通过一个 union 容纳不同设备类型的格式定义,具体使用哪个成员由 type 字段决定。
1 2 3 4 5 6 7 8 9 10 11 12 13 struct v4l2_format { __u32 type; union { struct v4l2_pix_format pix ; struct v4l2_pix_format_mplane pix_mp ; struct v4l2_window win ; struct v4l2_vbi_format vbi ; struct v4l2_sliced_vbi_format sliced ; struct v4l2_sdr_format sdr ; struct v4l2_meta_format meta ; __u8 raw_data[200 ]; } fmt; };
v4l2_format 需要与 ioctl 系统调用配合使用:
命令
作用
VIDIOC_G_FMT
获取当前采集格式
VIDIOC_S_FMT
设置采集格式(分辨率、像素格式)
VIDIOC_TRY_FMT
测试格式,不实际改变设备状态
以前面的 USB 摄像头为例,设置分辨率和像素格式的典型流程是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct v4l2_format fmt = {0 }; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 2560 ; fmt.fmt.pix.height = 1440 ; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1 ) { perror("VIDIOC_S_FMT" ); }printf ("实际分辨率: %ux%u\n" , fmt.fmt.pix.width, fmt.fmt.pix.height);printf ("实际格式: %c%c%c%c\n" , fmt.fmt.pix.pixelformat & 0xff , (fmt.fmt.pix.pixelformat >> 8 ) & 0xff , (fmt.fmt.pixelformat >> 16 ) & 0xff , (fmt.fmt.pix.pixelformat >> 24 ) & 0xff );
3.2 v4l2_fmtdesc
v4l2_fmtdesc 是 V4L2 里用来描述一种像素格式 的结构体。它配合 VIDIOC_ENUM_FMT 这个 ioctl 使用,用来枚举设备支持的所有格式。
之前执行 v4l2-ctl --list-formats-ext 时,输出里那些 [0]: 'YUYV'、[1]: 'MJPG'、[2]: 'NV12',就是从这个结构体读出来的。
1 2 3 4 5 6 7 8 9 [linux/videodev2.h]struct v4l2_fmtdesc { __u32 index; __u32 type; __u32 flags; __u8 description[32 ]; __u32 pixelformat; __u32 reserved[4 ]; };
pixelformat 是四字符编码,V4L2 用 4 个字符 表示一种像素格式,通常用宏定义:
宏
四字符
含义
V4L2_PIX_FMT_YUYV
'YUYV'
YUV 4:2:2 未压缩
V4L2_PIX_FMT_MJPEG
'MJPG'
Motion-JPEG 压缩
V4L2_PIX_FMT_NV12
'NV12'
Y/UV 4:2:0
V4L2_PIX_FMT_RGB24
'RGB3'
24 位 RGB
1 2 3 4 #define v4l2_fourcc(a, b, c, d)\ ((__u32)(a) | ((__u32)(b) << 8) | ((__u32)(c) << 16) | ((__u32)(d) << 24)) #define V4L2_PIX_FMT_YUYV v4l2_fourcc('Y' , 'U' , 'Y' , 'V' )
下面看一下怎么用它枚举格式。
VIDIOC_ENUM_FMT 是迭代式 的:从 index = 0 开始,每次调用递增 index,直到返回错误(EINVAL)为止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct v4l2_fmtdesc fmt ;memset (&fmt, 0 , sizeof (fmt)); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;for (fmt.index = 0 ; ; fmt.index++) { if (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) == -1 ) { break ; } printf ("[%u] %s (%c%c%c%c)\n" , fmt.index, fmt.description, fmt.pixelformat & 0xff , (fmt.pixelformat >> 8 ) & 0xff , (fmt.pixelformat >> 16 ) & 0xff , (fmt.pixelformat >> 24 ) & 0xff ); }
四、申请缓冲区
4.1 REQBUFS
v4l2_requestbuffers 是 V4L2 中用于申请和管理视频缓冲区 的结构体。它配合 VIDIOC_REQBUFS ioctl 使用,是流式 I/O(streaming I/O)流程中必不可少的一步。
1 2 3 4 5 6 7 8 struct v4l2_requestbuffers { __u32 count; __u32 type; __u32 memory; __u32 capabilities; __u8 flags; __u8 reserved[3 ]; };
调用 VIDIOC_REQBUFS 时,驱动会尝试分配指定数量的缓冲区。驱动返回的 count 不一定等于请求值 :
可能更小 :内存不足时。
可能更大 :驱动需要更多缓冲区才能正常工作。
可能为 0 :没有可用内存。
因此,调用后必须检查 count 的实际值 ,而不是假设请求的数量会被完全满足。
当 count 设为 0 时,会释放所有已分配的缓冲区,并隐含执行 VIDIOC_STREAMOFF。
1 2 3 4 5 6 7 8 9 10 11 12 struct v4l2_requestbuffers req = {0 }; req.count = 4 ; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1 ) { perror("VIDIOC_REQBUFS" ); }printf ("实际申请到 %u 个缓冲区\n" , req.count);
VIDIOC_REQBUFS 只是完成了缓冲区的申请 。之后还需要:
用 VIDIOC_QUERYBUF 查询每个缓冲区的信息
用 mmap() 将缓冲区映射到用户空间
用 VIDIOC_QBUF 将缓冲区放入队列
用 VIDIOC_STREAMON 启动视频流
如果使用 V4L2_MEMORY_MMAP,缓冲区由驱动分配在内核空间,mmap 之后应用才能直接读写。如果使用 V4L2_MEMORY_USERPTR,缓冲区由应用自己分配,这个 ioctl 只是让驱动切换到对应模式。
4.2 QUERYBUF + mmap + QBUF
v4l2_buffer 是 V4L2 流式 I/O 中单个缓冲区的“身份证”和“状态报告” 。应用程序通过它告诉驱动要操作哪个缓冲区(例如 QBUF 入队),驱动则通过它告诉应用缓冲区的状态和数据信息(例如 DQBUF 出队后)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct v4l2_buffer { __u32 index; __u32 type; __u32 bytesused; __u32 flags; __u32 field; struct timeval timestamp ; struct v4l2_timecode timecode ; __u32 sequence; __u32 memory; union { __u32 offset; unsigned long userptr; struct v4l2_plane *planes ; __s32 fd; } m; __u32 length; __u32 reserved2; __u32 reserved; };
flags 字段是缓冲区状态和属性的位掩码,常见的有:
标志
含义
V4L2_BUF_FLAG_MAPPED
缓冲区已映射到用户空间
V4L2_BUF_FLAG_QUEUED
缓冲区已在驱动队列中等待处理
V4L2_BUF_FLAG_DONE
缓冲区处理完成,应用可以取走
V4L2_BUF_FLAG_ERROR
缓冲区已就绪,但数据可能已损坏
V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC
时间戳来自 CLOCK_MONOTONIC 时钟
当 QBUF 成功时,驱动会设置 V4L2_BUF_FLAG_QUEUED 并清除 V4L2_BUF_FLAG_DONE;当 DQBUF 成功时,驱动会设置 V4L2_BUF_FLAG_DONE 并清除 V4L2_BUF_FLAG_QUEUED。
使用时每个 buffer 都要处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 struct Buffer { void *start; size_t length; };std ::vector <Buffer> *buffers for (uint32_t i = 0 ; i < req.count; ++i) { v4l2_buffer buf{}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0 ) { std ::cerr << "VIDIOC_QUERYBUF failed\n" ; return false ; } (*buffers)[i].length = buf.length; (*buffers)[i].start = mmap(nullptr, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if ((*buffers)[i].start == MAP_FAILED) { (*buffers)[i].start = nullptr; std ::cerr << "mmap failed: " << std ::strerror(errno) << "\n" ; return false ; } if (ioctl(fd, VIDIOC_QBUF, &buf) < 0 ) { std ::cerr << "VIDIOC_QBUF failed\n" ; return false ; } }
这里的含义就是:
QUERYBUF:问驱动这个 buffer 有多长、偏移在哪
mmap:把驱动 buffer 映射到用户态
QBUF:把 buffer 交给驱动,等待写帧
五、启动视频流
上一步通过 VIDIOC_QBUF 将缓冲区送给驱动,现在就可以启动视频流将数据写到缓冲区中。
1 2 3 4 5 6 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;if (ioctl(fd, VIDIOC_STREAMON, &type) < 0 ) { perror("VIDIOC_STREAMON" ); return -1 ; }
执行后,驱动开始把摄像头帧写进 queue 里的 buffer。
到这里,采集基础设施已经建立:
格式已配置
buffer 已申请
buffer 已 mmap
buffer 已 QBUF
stream 已 ON
六、捕获视频帧
6.1 select 等待数据
直接 DQBUF 可能阻塞。实际程序常用 select 加超时,确认有数据可取了再去取:
1 2 3 4 5 6 7 8 9 10 11 12 13 fd_set fds; FD_ZERO(&fds); FD_SET(fd, &fds);struct timeval tv ; tv.tv_sec = 2 ; tv.tv_usec = 0 ;int ret = select(fd + 1 , &fds, NULL , NULL , &tv);if (ret <= 0 ) { perror("select" ); return -1 ; }
6.2 DQBUF 取出一帧
1 2 3 4 5 6 7 8 9 10 struct v4l2_buffer buf ;memset (&buf, 0 , sizeof (buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP;if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0 ) { perror("VIDIOC_DQBUF" ); return -1 ; }
DQBUF 返回后可以读:
字段
含义
buf.index
哪一个 buffer 有数据
buf.bytesused
当前帧实际字节数
buf.sequence
帧序号
buf.timestamp
驱动时间戳
错误做法:
1 2 3 DQBUF QUERYBUF 读 sequence/timestamp
QUERYBUF 不会返回当前帧序号和时间戳。必须保存 DQBUF 返回的 v4l2_buffer。
七、处理帧数据
拿到 buffer:
1 2 void *frame_data = buffers[buf.index].start;size_t frame_bytes = buf.bytesused;
不同格式处理方式不同。
7.1 MJPG
1 2 3 4 frame_data 指向一个完整 JPEG frame_bytes 是这一帧 JPEG 的大小 可以直接写入文件 后面可以交 MPP JPEG decoder
写入文件:
1 write(output_fd, frame_data, frame_bytes);
注意:
不是写 sizeimage
不是写 width * height
写 bytesused
7.2 YUYV
1 2 3 未压缩 YUV 4:2:2 每像素约 2 字节 需要看 stride
一行数据大小:
1 size_t line_bytes = stride;
7.3 NV12
1 2 3 Y plane + interleaved UV plane 每像素平均 1.5 字节 需要看 stride 和 plane layout
7.4 处理完后归还 buffer
1 2 3 4 if (ioctl(fd, VIDIOC_QBUF, &buf) < 0 ) { perror("VIDIOC_QBUF" ); return -1 ; }
忘记 QBUF,队列很快被耗尽,后续 DQBUF 会卡住或超时。
八、停止视频流
1 2 3 4 5 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0 ) { perror("VIDIOC_STREAMOFF" ); }
停止后驱动不再向这些 buffer 写帧。
顺序建议:
九、释放资源
释放 mmap:
1 2 3 4 5 6 for (size_t i = 0 ; i < buffers.size(); ++i) { if (buffers[i].start) { munmap(buffers[i].start, buffers[i].length); buffers[i].start = nullptr; } }
关闭 fd:
关闭输出文件:
推荐清理顺序:
1 2 3 4 5 VIDIOC_STREAMOFF munmap all buffers close camera fd close output fd 释放程序内 vector/内存
如果程序中途出错,也要保证清理路径能执行。C 写法常用:
C++ 写法可以用 RAII:
1 2 3 4 5 6 7 8 class V4l2Fd {public : explicit V4l2Fd (int fd) : fd_(fd) { } ~V4l2Fd () { if (fd_ >= 0 ) close (fd_); } int get () const { return fd_; }private : int fd_; };