V4L2框架——视频采集完整链路

如下是 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 里是字符设备:

1
/dev/video20

通过系统调用 open 直接打开:

1
2
3
4
5
int fd = open(device, O_RDWR | O_CLOEXEC);
if (fd < 0) {
perror("open");
return -1;
}
  • 为什么用 O_RDWR:

    1. 后面要 ioctl
    2. 后面要把 buffer QBUF 给驱动
    3. 有些节点只读也能部分工作,但捕获程序常用 O_RDWR
  • 为什么用 O_CLOEXEC:

    1. 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]; // 驱动名称,如 "uvcvideo"
__u8 card[32]; // 设备名称,如 "2K USB Camera"
__u8 bus_info[32]; // 总线位置,如 "usb-fc800000.usb-1.1"
__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
2K 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

三、设置视频格式

3.1 v4l2_format

v4l2_format 是一个聚合结构体,用来描述或协商 V4L2 设备上的数据流格式。它本身不存储具体的图像参数,而是通过一个 union 容纳不同设备类型的格式定义,具体使用哪个成员由 type 字段决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct v4l2_format {
__u32 type; // 数据流类型,决定 fmt 联合体中使用哪个成员
union {
struct v4l2_pix_format pix; // 单平面视频采集/输出 V4L2_BUF_TYPE_VIDEO_CAPTURE
struct v4l2_pix_format_mplane pix_mp; // 多平面视频采集/输出 V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE
struct v4l2_window win; // 视频叠加 V4L2_BUF_TYPE_VIDEO_OVERLAY
struct v4l2_vbi_format vbi; // 原始 VBI V4L2_BUF_TYPE_VBI_CAPTURE
struct v4l2_sliced_vbi_format sliced; // 切片 VBI V4L2_BUF_TYPE_SLICED_VBI_CAPTURE
struct v4l2_sdr_format sdr; // SDR 采集 V4L2_BUF_TYPE_SDR_CAPTURE
struct v4l2_meta_format meta; // 元数据采集 V4L2_BUF_TYPE_META_CAPTURE
__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};

// 1. 指定数据流类型
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

// 2. 设置期望的参数(这里通过 pix 成员操作)
fmt.fmt.pix.width = 2560;
fmt.fmt.pix.height = 1440;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;

// 3. 请求设置格式
if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
perror("VIDIOC_S_FMT");
// 处理错误
}

// 4. 驱动可能调整参数,需要读取实际生效的值
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; // 序号,从 0 开始递增
__u32 type; // 缓冲区类型,如 V4L2_BUF_TYPE_VIDEO_CAPTURE
__u32 flags; // 格式标志,如 V4L2_FMT_FLAG_COMPRESSED(压缩格式)
__u8 description[32]; // 格式的文本描述,如 "YUYV 4:2:2"、"Motion-JPEG"
__u32 pixelformat; // 四字符编码,如 V4L2_PIX_FMT_MJPG
__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') /* 16 YUV 4:2:2 */

下面看一下怎么用它枚举格式。

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; // 数据流类型,如 V4L2_BUF_TYPE_VIDEO_CAPTURE
__u32 memory; // I/O 方式:MMAP、USERPTR 或 DMABUF
__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; // 申请 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 只是完成了缓冲区的申请。之后还需要:

  1. 用 VIDIOC_QUERYBUF 查询每个缓冲区的信息
  2. 用 mmap() 将缓冲区映射到用户空间
  3. 用 VIDIOC_QBUF 将缓冲区放入队列
  4. 用 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; // 缓冲区编号,从 0 开始
__u32 type; // 缓冲区类型,必须与之前设置的一致
__u32 bytesused; // 缓冲区中已用数据量(字节)
__u32 flags; // 状态/属性标志位
__u32 field; // 场序(隔行/逐行)
struct timeval timestamp; // 时间戳
struct v4l2_timecode timecode; // 时间码
__u32 sequence; // 帧序号
__u32 memory; // 内存类型:MMAP / USERPTR / DMABUF
union {
__u32 offset; // MMAP 方式下的偏移量
unsigned long userptr;// USERPTR 方式下的用户指针
struct v4l2_plane *planes; // 多平面缓冲区
__s32 fd; // DMABUF 方式下的文件描述符
} 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;
// 查询buffer信息
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
std::cerr << "VIDIOC_QUERYBUF failed\n";
return false;
}
(*buffers)[i].length = buf.length;
// 将内核buffer映射到用户态
(*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;
}
// 把buffer交给驱动
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;  /* 不一定等于 width * 2 */

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 写帧。

顺序建议:

1
2
3
STREAMOFF
munmap
close

九、释放资源

释放 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
close(fd);

关闭输出文件:

1
close(output_fd);

推荐清理顺序:

1
2
3
4
5
VIDIOC_STREAMOFF
munmap all buffers
close camera fd
close output fd
释放程序内 vector/内存

如果程序中途出错,也要保证清理路径能执行。C 写法常用:

1
goto out;

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_;
};

V4L2框架——视频采集完整链路
http://example.com/2026/08/12/V4L2框架——视频采集完整链路/
作者
Yu xin
发布于
2026年8月12日
许可协议