v4l2 api基本使用流程 - V4l2学习笔记
v4l2(video for Linux two)是linux为视频设备提供的一套api接口规范,two表示版本为2。利用该接口应用程序可以从摄像头外设读取一系列图片进行处理。本文主要是实现使用该接口获取摄像头图片保存到本地,以此了解v4l2 api的基本使用方法。后续工作如果需要也要深入了解下各种摄像头、图像参数的意义,进阶的话还想学习内核侧v4l2驱动的编写。参考了网络上的一些博客1、2、3等。
调用基本流程
使用v4l2从摄像头读取图片的基本流程为:
- 打开设备
- 检查和设置设备属性
- 设置图片帧格式
- 设置input/output方式
- 循环获取数据
- 关闭设备
v4l2相关的数据结构、枚举值等都定义在<linux/videodev2.h>中。有如下几个需要提前知道的地方。
ioctl()函数用于操作设备,声明如下。
#include <sys/ioctl.h> int ioctl(int fd, unsigned long op, ...); /* glibc, BSD */
其中fd为打开设备的fd。参数op为操作类型,不同类型的设备可能不同,例如v4l2的api使用
VIDIOC_*
的宏来表示这些操作数。其后的参数是不定长的,根据不同的参数op应该接着传入不同的参数,用于设置或获取信息。程序中定义了如下的几个宏,包括LOG_INFO和LOG_ERROR打印日志和errExit用于判断函数是否调用成功,如果失败就打印原因后退出。
#define LOG(level) std::cout << std::endl << #level << " " << __FILE__ << ":" << __LINE__ << " " #define LOG_INFO LOG(INFO) #define LOG_ERROR LOG(ERROR) #define errExit(stmt) \ { \ if ((stmt) < 0) { \ LOG_ERROR << #stmt << ", errno: " << errno << ", errstr: " << strerror(errno); \ exit(1); \ } \ }
打开设备
一般情况下设备地址就是/dev/video0
。对于同一个摄像头物理设备可能对应多个video设备地址,一般第一个可以用来采图,其余有其它作用,具体还未研究。像我的笔记本就有/dev/video0~/dev/video3四个设备,但只有打开第一个能出jpg格式的图片。可以使用如下命令对比这些设备的信息diff:
vimdiff <(v4l2-ctl -d /dev/video0 --all) <(v4l2-ctl -d /dev/video1 --all)
在代码实现中,可以简单的以O_RDWR可读写的方式打开摄像头设备的地址。
int vfd = -1;
errExit(vfd = open("/dev/video0", O_RDWR));
检查设备属性
设备属性通过结构体v4l2_capability
表示,使用op为VIDIOC_QUERYCAP
的ioctrl可以获取设备属性信息。并且检查设备是否支持视频捕获功能。
errExit(ioctl(vfd, VIDIOC_QUERYCAP, &cap));
LOG_INFO << "driver: " << cap.driver << " , card: " << cap.card << " , bus: " << cap.bus_info << " , version: " << cap.version
<< " , cap: " << cap.capabilities;
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
LOG_ERROR << "not capture device";
return -1;
}
其中结构体v4l2_capability
的声明如下。
/**
* struct v4l2_capability - Describes V4L2 device caps returned by VIDIOC_QUERYCAP
*
* @driver: name of the driver module (e.g. "bttv")
* @card: name of the card (e.g. "Hauppauge WinTV")
* @bus_info: name of the bus (e.g. "PCI:" + pci_name(pci_dev) )
* @version: KERNEL_VERSION
* @capabilities: capabilities of the physical device as a whole
* @device_caps: capabilities accessed via this particular device (node)
* @reserved: reserved fields for future extensions
*/
struct v4l2_capability {
__u8 driver[16];
__u8 card[32];
__u8 bus_info[32];
__u32 version;
__u32 capabilities;
__u32 device_caps;
__u32 reserved[3];
};
也可以通过结构体v4l2_fmtdesc
和op为VIDIOC_ENUM_FMT
循环获取支持的格式列表。
v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (ioctl(vfd, VIDIOC_ENUM_FMT, &fmtdesc) != -1) {
LOG_INFO << "index: " << fmtdesc.index << ", desc: " << fmtdesc.description;
++fmtdesc.index;
}
设置图片帧格式
通过结构体v4l2_format
和op为VIDIOC_S_FMT
设置帧格式。
struct v4l2_format 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_JPEG;
fmt.fmt.pix.field = V4L2_FIELD_NONE;
errExit(ioctl(vfd, VIDIOC_S_FMT, &fmt));
设置input/output方式
一般使用mmap传输方式映射内核空间的图片到用户空间,相较于直接read()减少内核空间到用户空间的一次数据拷贝。
首先通过VIDIOC_REQBUFS
向v4l2申请内核空间缓存的数量。
struct v4l2_requestbuffers req;
req.count = BUFFER_COUNT;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
errExit(ioctl(vfd, VIDIOC_REQBUFS, &req));
然后通过VIDIOC_QUERYBUF
获取循环每个缓存在内核buffer中的偏移量,从而使用mmap映射到用户内存空间,并且将用户空间的起始地址和长度保存起来,供后边从中拿取图片。
struct v4l2_buffer buf {};
for (int n_buffer = 0; n_buffer < req.count; ++n_buffer) {
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = n_buffer;
errExit(ioctl(vfd, VIDIOC_QUERYBUF, &buf));
LOG_INFO << "length: " << buf.length << ", offset: " << buf.m.offset << ", fd: " << buf.m.fd << ", index: " << buf.index;
buffers[n_buffer].length = buf.length;
buffers[n_buffer].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, vfd, buf.m.offset);
if (MAP_FAILED == buffers[n_buffer].start) {
perror("mmap");
continue;
}
errExit(ioctl(vfd, VIDIOC_QBUF, &buf)); // 将buf放入驱动队列
}
特别注意要使用VIDIOC_QBUF
将buf放入驱动的队列中,否则队列大小为空直接取数据是会被永远阻塞。
循环获取数据
首先使用VIDIOC_STREAMON
开启数据流。之后通过VIDIOC_DQBUF
从驱动队列中消费图片,找到buf缓冲index对应的mmap映射用户空间的地址来获取图片。最后调用VIDIOC_QBUF
将消费后的图片放入队列中。通过这个循环可以源源不断的取图片形成视频。我这里仅仅保存了8张图片用于验证。
struct v4l2_buffer capture_buf {};
capture_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
capture_buf.memory = V4L2_MEMORY_MMAP;
int img_cnt = 0;
for (int i = 0; img_cnt < BUFFER_COUNT * 2; ++i) {
errExit(ioctl(vfd, VIDIOC_DQBUF, &capture_buf));
LOG_INFO << "idx: " << i << ", bytesused: " << capture_buf.bytesused;
if (0 != capture_buf.bytesused && 0 == i % 10) {
++img_cnt;
// 保存图片到本地
int imgfd = open((std::string("capture_") + std::to_string(i) + ".jpg").c_str(), O_WRONLY | O_CREAT, 0666);
errExit(imgfd);
errExit(write(imgfd, buffers[capture_buf.index].start, capture_buf.bytesused));
errExit(close(imgfd));
}
if (0 != ioctl(vfd, VIDIOC_QBUF, &capture_buf)) {
perror("VIDIOC_QBUF");
return 1;
}
}
关闭设备
最后通过VIDIOC_STREAMOFF
关闭数据流,munmap
取消之前的mmap
映射的地址,close()关闭设备fd。
errExit(ioctl(vfd, VIDIOC_STREAMOFF, &buf.type));
for (int i = 0; i < BUFFER_COUNT; ++i) {
munmap(buffers[i].start, buffers[i].length);
}
close(vfd);
完整代码
#include <fcntl.h>
#include <linux/videodev2.h>
#include <memory.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <unistd.h>
#include <iostream>
#include <string>
#define BUFFER_COUNT 4
#define WIDTH 640
#define HEIGHT 480
#define LOG(level) std::cout << std::endl << #level << " " << __FILE__ << ":" << __LINE__ << " "
#define LOG_INFO LOG(INFO)
#define LOG_ERROR LOG(ERROR)
#define errExit(stmt) \
{ \
if ((stmt) < 0) { \
LOG_ERROR << #stmt << ", errno: " << errno << ", errstr: " << strerror(errno); \
exit(1); \
} \
}
struct buffer {
void *start;
uint length;
} buffers[BUFFER_COUNT];
int main() {
int vfd = -1;
errExit(vfd = open("/dev/video0", O_RDWR));
v4l2_capability cap{0};
errExit(ioctl(vfd, VIDIOC_QUERYCAP, &cap));
LOG_INFO << "driver: " << cap.driver << " , card: " << cap.card << " , bus: " << cap.bus_info << " , version: " << cap.version
<< " , cap: " << cap.capabilities;
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
LOG_ERROR << "not capture device";
return -1;
}
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (ioctl(vfd, VIDIOC_ENUM_FMT, &fmtdesc) != -1) {
LOG_INFO << "index: " << fmtdesc.index << ", desc: " << fmtdesc.description;
++fmtdesc.index;
}
struct v4l2_format 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_JPEG;
fmt.fmt.pix.field = V4L2_FIELD_NONE;
errExit(ioctl(vfd, VIDIOC_S_FMT, &fmt));
struct v4l2_requestbuffers req;
req.count = BUFFER_COUNT;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
errExit(ioctl(vfd, VIDIOC_REQBUFS, &req));
struct v4l2_buffer buf {};
for (int n_buffer = 0; n_buffer < req.count; ++n_buffer) {
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = n_buffer;
errExit(ioctl(vfd, VIDIOC_QUERYBUF, &buf));
LOG_INFO << "length: " << buf.length << ", offset: " << buf.m.offset << ", fd: " << buf.m.fd << ", index: " << buf.index;
buffers[n_buffer].length = buf.length;
buffers[n_buffer].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, vfd, buf.m.offset);
if (MAP_FAILED == buffers[n_buffer].start) {
perror("mmap");
continue;
}
errExit(ioctl(vfd, VIDIOC_QBUF, &buf));
}
errExit(ioctl(vfd, VIDIOC_STREAMON, &buf.type));
// 等待fd可读。可以省略
fd_set fds;
FD_ZERO(&fds);
FD_SET(vfd, &fds);
struct timeval tv {};
tv.tv_sec = 2;
errExit(select(vfd + 1, &fds, NULL, NULL, &tv));
struct v4l2_buffer capture_buf {};
capture_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
capture_buf.memory = V4L2_MEMORY_MMAP;
int img_cnt = 0;
for (int i = 0; img_cnt < BUFFER_COUNT * 2; ++i) {
errExit(ioctl(vfd, VIDIOC_DQBUF, &capture_buf));
LOG_INFO << "idx: " << i << ", bytesused: " << capture_buf.bytesused;
if (0 != capture_buf.bytesused && 0 == i % 10) {
++img_cnt;
int imgfd = open((std::string("capture_") + std::to_string(i) + ".jpg").c_str(), O_WRONLY | O_CREAT, 0666);
errExit(imgfd);
errExit(write(imgfd, buffers[capture_buf.index].start, capture_buf.bytesused));
errExit(close(imgfd));
}
if (0 != ioctl(vfd, VIDIOC_QBUF, &capture_buf)) {
perror("VIDIOC_QBUF");
return 1;
}
}
errExit(ioctl(vfd, VIDIOC_STREAMOFF, &buf.type));
for (int i = 0; i < BUFFER_COUNT; ++i) {
munmap(buffers[i].start, buffers[i].length);
}
close(vfd);
}