1.V4L2
它是Linux内核中标准的关于视频驱动程序,Video for Linux 2,简称V4L2。
它为Linux下的视频驱动提供了统一的接口,使得应用程序可以使用统一的API操作不同的视频设备。
V4L2支持三类设备:视频输入输出设备 、VBI设备 和radio设备 ,分别会在/dev目录下产生video*、radio*和vbi设备节点。应用程序通过对 videoX 设备文件进行 I/O 操作来配置、使用设备类设备。
2.整体流程
3.V4L2读取摄像头流程
从流程图中可以看到,几乎对摄像头的所有操作都是通过 ioctl() 来完成,搭配不同的 V4L2 指令( request 参数)请求不同的操作,这些指令定义在头文件 linux/videodev2.h 中,在摄像头应用程序代码中,需要包含 头文件 linux/videodev2.h,它 提供了很多ioctl宏来和应用层交互。
每一个不同的指令宏就表示向设备请求不同的操作,调用 ioctl()时需要传入的第三个参数的类型;调用 ioctl()前,定义一个该类型变量,调用 ioctl()时、将变量的指针作为 ioctl()的第三个参数传入
cpp
###define VIDIOC_QUERYCAP _IOR('V', 0, struct v4l2_capability)
struct v4l2_capability cap;
......
ioctl(fd, VIDIOC_QUERYCAP, &cap);
下面列出来一些比较常用的:
在进行V4L2开发中,常用的命令标识符如下:
(1) VIDIOC_REQBUFS: 分配内存;
(2) VIDIOC_QUERYBUF: 把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址;
(3) VIDIOC_QUERYCAP: 查询驱动功能;
(4) VIDIOC_ENUM_FMT: 获取当前驱动支持的视频格式;
(5) VIDIOC_S_FMT: 设置当前驱动的视频捕获格式;
(6) VIDIOC_G_FMT: 读取当前驱动的视频捕获格式;
(7) VIDIOC_TRY_FMT: 验证当前驱动的显示格式;
(8) VIDIOC_CROPCAP: 查询驱动的修剪功能;
(9) VIDIOC_S_CROP: 设置视频信号的边框;
(10)VIDIOC_G_CROP: 读取视频信号的边框;
(11)VIDIOC_QBUF: 把数据从缓存中读取出来;
(12)VIDIOC_DQBUF: 把数据放回缓存队列;
(13)VIDIOC_STREAMON: 开始视频显示函数;
(14)VIDIOC_STREAMOFF:结束视频显示函数;
(15)VIDIOC_QUERYSTD: 检查当前视频设备支持的标准,例如PAL或NTSC;
3.1 打开视频文件设备
- 视频类设备对应的设备节点为/dev/videoX, X 为数字编号,通常从 0 开始 ,使用open打开节点, 应用程序能够使用阻塞模式 或非阻塞模式打开视频设备,如果使用非阻塞模式调用视频设备,即使尚未捕获到信息,驱动依旧会把缓存(DQBUFF)里的东西返回给应用程序。
cpp
//阻塞模式
fd = open("/dev/video0", O_RDWR);
//非阻塞模式
fd = open("/dev/video0", O_RDWR | O_NOBLOCK);
if (0 > fd) {
fprintf(stderr, "open error: %s: %s\n", "/dev/video0", strerror(errno));
return -1;
}
3.2 查询属性、功能
- 查询设备的属性, 使用的指令为 VIDIOC_QUERYCAP,需要传入一个表示属性的结构体
cpp
/* 查询设备功能 */
struct v4l2_capability vcap;
ioctl(fd, VIDIOC_QUERYCAP, &vcap);
/* 判断是否是视频采集设备 */
/*capabilities字段必须包含 V4L2_CAP_VIDEO_CAPTURE,表示它支持视频采集功能*/
if (!(V4L2_CAP_VIDEO_CAPTURE & vcap.capabilities)) {
fprintf(stderr, "Error: No capture video device!\n");
return -1;
}
3.3设置设备参数
- 在设置格式之前,要先枚举出所有的格式,看一看是否支持要设置的格式,然后再进一步设置 。
- **a)****枚举出摄像头支持的所有像素格式:**VIDIOC_ENUM_FMT
ioctl(int fd, VIDIOC_ENUM_FMT, struct v4l2_fmtdesc *fmtdesc);
cpp
/* 枚举出摄像头所支持的所有像素格式以及描述信息 */
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (0 == ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc)) {
printf("fmt: %s <0x%x>\n", fmtdesc.description, fmtdesc.pixelformat);
fmtdesc.index++;
}
- **b)****枚举出摄像头支持的所有像素格式:**VIDIOC_ENUM_FMT
ioctl(int fd, VIDIOC_ENUM_FRAMESIZES, struct v4l2_frmsizeenum *frmsize);
cpp
struct v4l2_frmsizeenum frmsize;
frmsize.index = 0;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
frmsize.pixel_format = V4L2_PIX_FMT_RGB565;
while (0 == ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) {
printf("frame_size<%d*%d>\n", frmsize.discrete.width, frmsize.discrete.height);
frmsize.index++;
}
- **c)****枚举摄像头所支持的所有视频采集帧率:**VIDIOC_ENUM_FRAMEINTERVALS
同理,略
d)设置当前的格式:VIDIOC_S_FMT 像素格式、视频帧大小以及视频采集帧率
int ioctl(int fd, VIDIOC_G_FMT, struct v4l2_format *fmt);//查看get,像素格式、视频帧大小
int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *fmt);//设置set,像素格式、视频帧大小
cpp
/*使用摄像头设备时,type设置成V4L2_BUF_TYPE_VIDEO_CAPTURE ,使用struct v4l2_pix_format来描述摄像头属性*/
struct v4l2_format fmt
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 800;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
if (0 > ioctl(fd, VIDIOC_S_FMT, &fmt)) { //设置格式
perror("ioctl error");
return -1;
}
ioctl(int fd, VIDIOC_G_PARM, struct v4l2_streamparm *streamparm);//设置帧率
cpp
struct v4l2_streamparm streamparm;
streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);
/** 判断是否支持帧率设置 **/
if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) {
streamparm.parm.capture.timeperframe.numerator = 1;
streamparm.parm.capture.timeperframe.denominator = 30;//30fps
if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm))
{ //设置参数
fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
return -1;
}
}
else
fprintf(stderr, "不支持帧率设置");
3.4申请帧缓存
读取摄像头数据的方式有两种,一种是 read 方式(对应设备功能返回的 V4L2_CAP_READWRITE );另一种则是 streaming 方式 (使用mmap,对应设备功能返回的 V4L2_CAP_STREAMING )
我们需要将设备申请帧缓冲,并将帧缓冲映射到应用程序的进程地址空间中。
ioctl(int fd, VIDIOC_REQBUFS, struct v4l2_requestbuffers *reqbuf);//申请缓冲帧
cpp
struct v4l2_requestbuffers reqbuf;
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.count = 3; // 申请 3 个帧缓冲
reqbuf.memory = V4L2_MEMORY_MMAP;
if (0 > ioctl(fd, VIDIOC_REQBUFS, &reqbuf)) {
fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
return -1;
}
使用 VIDIOC_REQBUFS 指令申请帧缓冲,该缓冲区实质上是由内核所维护的, 应用程序不能直接读取该缓冲区的数据 ,我们需要将其映射到用户空间中,这样,应用程序读取映射区的数据实际上就是读取内核维护的帧缓冲中的数据。
cpp
/* 建立内存映射 */
struct v4l2_buffer buf
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for (buf.index = 0; buf.index < 3; buf.index++) {
ioctl(fd, VIDIOC_QUERYBUF, &buf);//查看帧缓冲信息
frm_base[buf.index] = mmap(NULL, buf.length,PROT_READ | PROT_WRITE, MAP_SHARED,fd, buf.m.offset);
if (MAP_FAILED == frm_base[buf.index]) {
perror("mmap error");
return -1;
}
}
3.5入队,开始采集
使用 VIDIOC_QBUF 指令将帧缓冲放入到内核的帧缓冲队列,将三个帧缓冲放入内核的帧缓冲队列(入队操作)中:
cpp
struct v4l2_buffer buf;
/* 入队操作 */
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;/*入队前需先设置 struct v4l2_buffer 类型对象的 memory、type 字段*/
for (buf.index = 0; buf.index < 3; buf.index++) {
if (0 > ioctl(fd, VIDIOC_QBUF, &buf)) {
perror("ioctl error");
return -1;
}
}
开始采集:
cpp
ioctl(int fd, VIDIOC_STREAMON, int *type); //开启视频采集
ioctl(int fd, VIDIOC_STREAMOFF, int *type); //停止视频采集
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_STREAMON, &type)) {
perror("ioctl error");
return -1;
}
3.6出队
边采集边出队
cpp
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for ( ; ; ) {
for(buf.index = 0; buf.index < 3; buf.index++) {
ioctl(fd, VIDIOC_DQBUF, &buf); //出队
// 读取帧缓冲的映射区、获取一帧数据
// 处理这一帧数据
do_something();
// 数据处理完之后、将当前帧缓冲入队、接着读取下一帧数据
ioctl(fd, VIDIOC_QBUF, &buf);
}
}
帧缓冲出队之后,接下来便可读取数据了,然后对数据进行处理, 比如对数据进行转化,如RGB888转RGB565,将转换后的数据刷到FrameBuffer上进行显示。处理完后再入队继续采集。
3.7关闭采集
如果要结束视频采集,使用 VIDIOC_STREAMOFF 指令:
cpp
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_STREAMOFF, &type)) {
perror("ioctl error");
return -1;
}