V4L2框架用法笔记
V4L2框架用法笔记
V4L2(Video for Linux 2)是Linux系统下用于视频采集的标准框架,主要用于摄像头等视频设备的应用层开发(非驱动开发),核心是通过标准接口与摄像头驱动通信,实现画面采集、显示、拍照、录像等功能。本文结合实际代码,梳理V4L2框架的完整用法、核心函数及关键细节。
一、V4L2核心定位与开发场景
- 定位:应用层与摄像头驱动的“通信桥梁”,驱动已由内核实现(如uvcvideo驱动),我们只需通过V4L2标准接口调用驱动,无需操作硬件寄存器。
- 开发场景:嵌入式Linux摄像头采集、预览、拍照、录像、推流等(本文基于MMAP内存映射模式,效率最高、最常用)。
- 核心原则:所有操作通过「ioctl函数+标准结构体」实现,驱动只识别V4L2标准接口,不识别自定义变量/结构体。
二、V4L2完整采集流程(必记)
整个流程从设备初始化到采集结束,按顺序执行,缺一不可,对应代码中核心函数的调用顺序:
- 打开摄像头设备(open)
- 设置摄像头格式(分辨率、图像格式)→ v4l2_set_format
- 向驱动申请帧缓冲区 → v4l2_request_buffers
- 将内核缓冲区映射到应用层 → v4l2_mmap_buffers
- 将所有缓冲区放入驱动队列(QBUF)
- 开启视频流采集 → VIDIOC_STREAMON
- 循环采集:等待数据(select)→ 取帧(DQBUF)→ 处理画面(显示/拍照/录像)→ 还帧(QBUF)
- 停止视频流(VIDIOC_STREAMOFF)
- 释放资源(解除映射、关闭设备)
三、核心函数详解(结合实际代码)
以下函数均为应用层核心函数,对应流程中的关键步骤,逐函数解析功能、参数及作用。
1. 打开/关闭摄像头设备(基础操作)
// 打开摄像头(只读/读写模式)g_ui.v4l2_fd = open("/dev/video0", O_RDWR);
// 关闭摄像头(资源释放)close(g_ui.v4l2_fd);关键:/dev/video0是摄像头设备节点,不同摄像头节点号可能不同(video1、video2等);打开失败会返回-1,需做错误处理。
2. 设置视频格式 → v4l2_set_format
功能:告诉驱动,我们需要的画面分辨率、图像格式,驱动会返回实际支持的参数(避免设置不支持的分辨率)。
static int v4l2_set_format(int width, int height){ struct v4l2_format fmt; memset(&fmt, 0, sizeof(fmt)); // 结构体清零,避免旧数据干扰 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 固定:视频采集类型 fmt.fmt.pix.width = width; // 期望的宽度 fmt.fmt.pix.height = height; // 期望的高度 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; // 图像格式:YUYV(裸数据、低延迟) fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; // 固定:逐行扫描
// 向驱动发送“设置格式”命令 if (ioctl(g_ui.v4l2_fd, VIDIOC_S_FMT, &fmt) < 0) { LV_LOG_ERROR("Failed to set format: %s", strerror(errno)); return -1; }
// 保存驱动实际返回的分辨率(可能与期望不同) g_ui.v4l2_width = fmt.fmt.pix.width; g_ui.v4l2_height = fmt.fmt.pix.height; LV_LOG_INFO("V4L2 format set: %dx%d (YUYV)", g_ui.v4l2_width, g_ui.v4l2_height); return 0;
}关键:① 图像格式常用YUYV(无压缩),需与后续处理(显示、转码)一致;② 必须保存驱动返回的实际分辨率,后续操作需用此参数。
3. 申请帧缓冲区 → v4l2_request_buffers
功能:向驱动申请指定数量的帧缓冲区(内存块),用于存放摄像头采集的画面,缓冲区数量一般设为4(循环使用,保证流畅)。
static int v4l2_request_buffers(void){ struct v4l2_requestbuffers req; memset(&req, 0, sizeof(req)); req.count = V4L2_BUFFERS_COUNT; // 申请的缓冲区数量(如4) req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 视频采集类型 req.memory = V4L2_MEMORY_MMAP; // 内存模式:MMAP(零拷贝)
// 向驱动发送“申请缓冲区”命令 if (ioctl(g_ui.v4l2_fd, VIDIOC_REQBUFS, &req) < 0) { LV_LOG_ERROR("Failed to request buffers: %s", strerror(errno)); return -1; }
// 保存驱动实际分配的缓冲区数量(可能少于申请数量) g_ui.v4l2_buffer_count = req.count; LV_LOG_INFO("V4L2 buffers requested: %d", g_ui.v4l2_buffer_count); return 0;}关键:① MMAP模式是V4L2效率最高的模式,无需拷贝数据;② 缓冲区数量固定,后续需循环使用、及时归还。
4. 内存映射 → v4l2_mmap_buffers
功能:将驱动内核空间的帧缓冲区,映射到应用层地址空间,让应用程序能直接读写缓冲区(拿到画面数据),是V4L2核心步骤。
static int v4l2_mmap_buffers(void){ unsigned int i; // 循环映射每一个缓冲区 for (i = 0; i < g_ui.v4l2_buffer_count; i++) { struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; // 要查询的第i号缓冲区
// 第一步:查询第i号缓冲区的信息(大小、内核偏移地址) if (ioctl(g_ui.v4l2_fd, VIDIOC_QUERYBUF, &buf) < 0) { LV_LOG_ERROR("Failed to query buffer %d: %s", i, strerror(errno)); return -1; }
// 第二步:将内核缓冲区映射到应用层,直接操作指向内核空间的指针 g_ui.v4l2_buffers[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, g_ui.v4l2_fd, buf.m.offset); // 映射失败判断 if (g_ui.v4l2_buffers[i] == MAP_FAILED) { LV_LOG_ERROR("Failed to mmap buffer %d: %s", i, strerror(errno)); return -1; } } LV_LOG_INFO("V4L2 buffers mmapped"); return 0;}关键细节:
- VIDIOC_QUERYBUF:查询缓冲区信息,获取缓冲区大小(buf.length)和内核偏移地址(buf.m.offset),为映射做准备;
- mmap参数:PROT_READ | PROT_WRITE(可读可写)、MAP_SHARED(共享内存,驱动和应用可同时访问);
- 映射后,g_ui.v4l2_buffers[i] 是应用层指针,直接指向画面数据,后续显示、拍照均用此指针。
5. 开启/停止视频流
功能:控制摄像头开始/停止采集画面,开启后驱动才会真正向缓冲区填充数据。
// 开启视频流int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;if (ioctl(g_ui.v4l2_fd, VIDIOC_STREAMON, &type) < 0) { LV_LOG_ERROR("Failed to start stream"); return -1;}
// 停止视频流(采集结束时调用)ioctl(g_ui.v4l2_fd, VIDIOC_STREAMOFF, &type);关键:必须在“缓冲区映射+入队”完成后,才能开启视频流;停止流后,需释放资源。
6. 循环采集(核心业务逻辑)
开启视频流后,通过循环实现持续采集,核心是“等待数据→取帧→处理→还帧”,配合select实现高效等待(不浪费CPU)。
// 循环采集逻辑(一般在单独线程中执行)while (g_ui.v4l2_thread_running) { fd_set fds; struct timeval tv;
// 1. 初始化监听集合,等待摄像头数据 FD_ZERO(&fds); // 清空监听列表 FD_SET(g_ui.v4l2_fd, &fds); // 监听摄像头文件描述符 tv.tv_sec = 2; // 超时时间2秒 tv.tv_usec = 0;
// 等待摄像头有数据(阻塞,无数据则休眠) int r = select(g_ui.v4l2_fd + 1, &fds, NULL, NULL, &tv);//监听文件描述符集合里的内容 if (r < 0) break; // 出错退出 if (r == 0) continue; // 超时,继续等待
// 2. 取帧(DQBUF):从驱动队列取出一帧就绪画面 struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (ioctl(g_ui.v4l2_fd, VIDIOC_DQBUF, &buf) < 0) { LV_LOG_ERROR("DQBUF error: %s", strerror(errno)); break; }
// 3. 处理画面(显示、拍照、录像、推流) if (g_ui.display_enabled) { // 显示画面:传入画面数据指针和数据大小 display_frame(g_ui.v4l2_buffers[buf.index], buf.bytesused); } if (g_ui.photo_requested) { // 拍照:保存当前帧,用ffmpeg转成JPG save_photo(g_ui.v4l2_buffers[buf.index], buf.bytesused); } if (g_ui.record_requested && g_ui.record_file) { // 录像:将当前帧写入文件 fwrite(g_ui.v4l2_buffers[buf.index], 1, buf.bytesused, g_ui.record_file); fflush(g_ui.record_file); // 强制写入磁盘,避免数据丢失 }
// 4. 还帧(QBUF):将用完的缓冲区还给驱动,循环使用 if (ioctl(g_ui.v4l2_fd, VIDIOC_QBUF, &buf) < 0) { LV_LOG_ERROR("QBUF error: %s", strerror(errno)); break; }}关键细节:
- select:监听摄像头数据,无数据时休眠,有数据时唤醒,避免死循环占用CPU;
- VIDIOC_DQBUF:取帧,驱动会返回就绪缓冲区的索引(buf.index),通过该索引找到画面数据(g_ui.v4l2_buffers[buf.index]);
- VIDIOC_QBUF:还帧,必须执行!缓冲区数量固定,不归还会导致缓冲区耗尽,摄像头卡死;
- buf.bytesused:当前帧的实际数据大小,用于显示、写入文件时避免越界。
7. 资源释放(收尾操作)
采集结束后,需按顺序释放资源,避免内存泄漏、设备占用。
// 停止视频流int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;ioctl(g_ui.v4l2_fd, VIDIOC_STREAMOFF, &type);
// 解除内存映射for (int i = 0; i < g_ui.v4l2_buffer_count; i++) { munmap(g_ui.v4l2_buffers[i], buf.length);}
// 关闭摄像头设备close(g_ui.v4l2_fd);8. 具体用法
void start_camera(void){ if (g_ui.v4l2_thread_running) { LV_LOG_WARN("Camera already running"); return; }
ensure_photo_dir(); init_v4l2_flags();
int width = get_resolution_width(g_ui.resolution); int height = get_resolution_height(g_ui.resolution);
/* 1. 打开 V4L2 设备 */ if (v4l2_open_device() < 0) { return; }
/* 2. 设置格式 */ if (v4l2_set_format(width, height) < 0) { v4l2_close_device(); return; }
/* 3. 申请缓冲区 */ if (v4l2_request_buffers() < 0) { v4l2_close_device(); return; }
/* 4. mmap 缓冲区 */ if (v4l2_mmap_buffers() < 0) { v4l2_unmap_buffers(); v4l2_close_device(); return; }
/* 5. 把缓冲区放入队列 */ if (v4l2_queue_buffers() < 0) { v4l2_unmap_buffers(); v4l2_close_device(); return; }
/* 6. 开始流采集 */ if (v4l2_start_streaming() < 0) { v4l2_unmap_buffers(); v4l2_close_device(); return; }
/* 7. 启动采集线程 */ g_ui.v4l2_thread_running = true; /* 启用画面显示 */ g_ui.display_enabled = true; if (pthread_create(&g_ui.v4l2_thread, NULL, v4l2_capture_thread, NULL) != 0) { LV_LOG_ERROR("Failed to create capture thread"); g_ui.v4l2_thread_running = false; g_ui.display_enabled = false; v4l2_stop_streaming(); v4l2_unmap_buffers(); v4l2_close_device(); return; }
LV_LOG_INFO("Camera started (V4L2)");}四、核心结构体说明
V4L2的所有操作都依赖标准结构体,重点掌握3个核心结构体,均来自头文件 <linux/videodev2.h>。
1. struct v4l2_format
用途:设置/获取视频格式(分辨率、图像格式),核心成员:
- type:固定为V4L2_BUF_TYPE_VIDEO_CAPTURE(视频采集);
- fmt.pix.width/height:画面宽高;
- fmt.pix.pixelformat:图像格式(如V4L2_PIX_FMT_YUYV);
- fmt.pix.field:扫描方式(固定为V4L2_FIELD_INTERLACED)。
2. struct v4l2_requestbuffers
用途:向驱动申请缓冲区,核心成员:
- count:申请的缓冲区数量;
- type:V4L2_BUF_TYPE_VIDEO_CAPTURE;
- memory:V4L2_MEMORY_MMAP(MMAP模式)。
3. struct v4l2_buffer
用途:最常用的“万能传话筒”,不同场景下使用不同成员,核心场景:
- 查询缓冲区(VIDIOC_QUERYBUF):用index(缓冲区索引)、length(缓冲区大小)、m.offset(内核偏移);
- 取帧/还帧(DQBUF/QBUF):用index(就绪缓冲区索引)、bytesused(当前帧大小)。
五、关键注意事项(避坑重点)
- 所有V4L2结构体使用前必须用memset清零,避免旧数据导致驱动误判;
- DQBUF和QBUF必须成对出现,取帧后必须归还,否则缓冲区耗尽,摄像头卡死;
- 设置分辨率时,驱动可能返回与期望不同的参数,必须保存实际返回的宽高;
- mmap映射后,必须用munmap解除映射,否则会造成内存泄漏;
- 采集线程中,用select实现高效等待,避免死循环占用CPU;
- 录像时,用fflush强制将数据写入磁盘,避免掉电、程序崩溃导致视频损坏;
- 所有ioctl操作都要做错误判断,确保程序健壮性。
六、总结
V4L2框架的核心逻辑是“通过标准接口与驱动通信,实现缓冲区的循环使用”,核心流程可简化为: 初始化(打开设备→设置格式→申请缓冲区→映射内存)→ 开启采集→循环(等待→取帧→处理→还帧)→ 停止采集→释放资源。 掌握上述流程和核心函数,就能实现Linux下摄像头的基础采集、显示、拍照、录像等功能,后续可基于此扩展推流、编码等更复杂的需求。
Some information may be outdated