V4L2应用层分析
- 一、总述
- 二、例子
一、总述
V4L2,即Video for Linux 2,是第二代为Linux打造的音频、视频驱动。相比第一代V4L,V4L2支持更多的设备,同时更加稳定。现今的视频设备,如USB摄像头,基本都支持V4L2,故现今无需再学习第一代的V4L。
首先要说明的是,V4L2是一个驱动,而不是链接库,故不要想着找什么动态、静态链接库文件(.so文件和.a文件)。驱动是存在于内核层的代码,用于管理底层硬件。应用层无法直接使用驱动中的变量和函数,只能通过一系列标准调用接口来使用驱动(实现原理为系统调用syscall),常用的接口函数有open、write、read、ioctl、close,他们的声明原型如下:
// 应用层接口
// fcntl.h
extern int open (const char *__file, int __oflag, ...) __nonnull ((1));// unistd.h
extern ssize_t write (int __fd, const void *__buf, size_t __n) __wur;
extern ssize_t read (int __fd, void *__buf, size_t __nbytes) __wur;
extern int close (int __fd);// sys/ioctl.h
extern int ioctl (int __fd, unsigned long int __request, ...) __THROW;
由于 V4L2中只使用了ioctl这一个控制接口函数,故这里只介绍ioctl函数:
参数:int __fd:文件描述符,是open函数的返回值。unsigned long int __request:可以为简单的0、1、2等数字,也可以为规范的编码值,用于告知驱动控制命令的类型。...:三个点表示编译器在编译时不要检查此参数的类型。一般为整型变量或指针。返回值:当ioctl操作成功时返回 0,否则返回负数。
对于一个复杂的驱动来说,应用层的write和read函数由于传入参数单一,已经无法满足复杂驱动的需要。在此种情况下,ioctl凭借他传入参数的多样性成为最常用的接口函数,其传入参数的多样性体现如下:
1.ioctl的第二个参数unsigned long int __request
,通常代表传入的控制命令。不同值的__request,代表着不同含义的控制命令,例如:V4L2中VIDIOC_QUERYCAP
代表查询设备的驱动支持情况,VIDIOC_S_FMT
代表设置设备属性。
__request
可以由驱动开发者自定义使用规则,然后应用层开发者按照此自定义规则传入__request
(如简单的0代表读取属性,1代表写入属性)。
驱动开发者最好遵从Linux内核的规范,按照内核提供好的格式对命令进行编码,详见头文件asm-generic/ioctl.h
,此头文件在内核层和应用层都有提供,可随时查阅。对于应用层开发者来说,只需要按照驱动开发者提供的头文件中的说明,使用相应的命令即可。
2.ioctl的第三个参数 ...
,通常代表需要传入的控制数据。...
意味着编译器不会检查此处变量类型,这就允许第三个参数可以为不同类型的变量,实现c++重载的效果,再结合它对应的内核层的声明(见下段代码),第三个参数可以是简单的整型数值,也可以是指针(地址),当传入的是指针时,驱动层函数得到的即为传入参数的地址。此参数也会参与到第二个参数__request
的编码中。
long (*unlocked_ioctl) (struct file *fl, unsigned int cmd, unsigned long arg);
综上所述,V4L2通过ioctl,通过传入不同的控制命令__request
和不同的控制数据(V4L2中为结构体的地址或指向结构体的指针),来实现对底层硬件的操作。这些控制命令及相应注释见头文件linux/videodev2.h
。
二、例子
V4L2应用层的程序流程如下(以USB摄像头为例):
1.打开USB摄像头的设备节点:int cameraFd = open("/dev/video*", O_RDWR | O_NONBLOCK)
,*的取值根据自己USB摄像头实际的节点号确定,打开方式中,阻塞和非阻塞根据自己需要确定。
2.查询当前USB摄像头的驱动支持情况。由于摄像头驱动开发者可能并没有按照V4L2驱动标准实现全部的驱动功能,而只实现了部分功能,所以需要查询当前摄像头的驱动支持情况,以确定后续通过怎样的方式驱动摄像头。
struct v4l2_capability cap;
ioctl(cameraFd, VIDIOC_QUERYCAP, &cap);
执行完上述ioctl函数后,驱动会根据功能的实现情况填充cap结构体变量的成员。注意,这是一个一般规律,即执行完某个ioctl函数后,驱动会为传入的结构体的成员赋值。
接下来,我们可以把所有成员的值都打印出来,以查看当前摄像头的驱动支持情况。
printf("Driver Name:%s\nCard Name:%s\nBus info:%s\nKernel Version:%u.%u.%u\nCapability is %#x\nDevice capability is %#x\n",cap.driver,cap.card,cap.bus_info,(cap.version>>16)&0XFF, (cap.version>>8)&0XFF,cap.version&0XFF,cap.capabilities,cap.device_caps);
v4l2_capability 结构体定义和成员变量意义说明见头文件linux/videodev2.h
,如下图所示:
我们需要重点关注的是成员capabilities
和device_caps
。按照文档描述,这两个的差异在于capabilities
是描述整个物理设备的驱动支持情况的,而device_caps
是描述当前设备节点的情况的。从逻辑上来看,我们应该查询device_caps
来查看驱动支持情况。不过我的摄像头实测这两个值的差异仅在于有无如下代码块中的最后一个宏定义的值:V4L2_CAP_DEVICE_CAPS
,故两者都可以用来查询驱动支持情况。
capabilities
的值以位掩码(BitMask)的形式来组织。以16进制数输出capabilities
,将他与如下代码块中的宏定义相比较,可以得知驱动是否支持宏定义代表的功能。例如,对于USB摄像头,他显然是一个捕获设备,故capabilities
的最低位一定为1。除此之外,我们需要重点关注的是V4L2_CAP_READWRITE
、V4L2_CAP_ASYNCIO
、V4L2_CAP_STREAMING
,他们代表USB摄像头支持的3种将数据输出到用户空间的方法,分别是通过read函数将内核空间的数据复制到用户空间、通过用户指针得到内核空间的数据、通过将内核空间映射到用户空间来读取数据,其中第二个我还不了解,第一个由于涉及到复制数据,故读取速率很慢,相比之下第三种映射方式读取速度要快得多。这三种读取数据的方法会在后续步骤提到。
/* Values for 'capabilities' field */#define V4L2_CAP_VIDEO_CAPTURE 0x00000001 /* Is a video capture device */#define V4L2_CAP_VIDEO_OUTPUT 0x00000002 /* Is a video output device */#define V4L2_CAP_VIDEO_OVERLAY 0x00000004 /* Can do video overlay */#define V4L2_CAP_VBI_CAPTURE 0x00000010 /* Is a raw VBI capture device */#define V4L2_CAP_VBI_OUTPUT 0x00000020 /* Is a raw VBI output device */#define V4L2_CAP_SLICED_VBI_CAPTURE 0x00000040 /* Is a sliced VBI capture device */#define V4L2_CAP_SLICED_VBI_OUTPUT 0x00000080 /* Is a sliced VBI output device */#define V4L2_CAP_RDS_CAPTURE 0x00000100 /* RDS data capture */#define V4L2_CAP_VIDEO_OUTPUT_OVERLAY 0x00000200 /* Can do video output overlay */#define V4L2_CAP_HW_FREQ_SEEK 0x00000400 /* Can do hardware frequency seek */#define V4L2_CAP_RDS_OUTPUT 0x00000800 /* Is an RDS encoder *//* Is a video capture device that supports multiplanar formats */#define V4L2_CAP_VIDEO_CAPTURE_MPLANE 0x00001000 /* Is a video output device that supports multiplanar formats */#define V4L2_CAP_VIDEO_OUTPUT_MPLANE 0x00002000 /* Is a video mem-to-mem device that supports multiplanar formats */#define V4L2_CAP_VIDEO_M2M_MPLANE 0x00004000 /* Is a video mem-to-mem device */#define V4L2_CAP_VIDEO_M2M 0x00008000#define V4L2_CAP_TUNER 0x00010000 /* has a tuner */#define V4L2_CAP_AUDIO 0x00020000 /* has audio support */#define V4L2_CAP_RADIO 0x00040000 /* is a radio device */#define V4L2_CAP_MODULATOR 0x00080000 /* has a modulator */#define V4L2_CAP_SDR_CAPTURE 0x00100000 /* Is a SDR capture device */#define V4L2_CAP_EXT_PIX_FORMAT 0x00200000 /* Supports the extended pixel format */#define V4L2_CAP_READWRITE 0x01000000 /* read/write systemcalls */#define V4L2_CAP_ASYNCIO 0x02000000 /* async I/O */#define V4L2_CAP_STREAMING 0x04000000 /* streaming I/O ioctls */#define V4L2_CAP_DEVICE_CAPS 0x80000000 /* sets device capabilities field */
3.获取USB摄像头输出图像的格式:
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index=0;
fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("Support format:\n");
while(ioctl(cameraFd, VIDIOC_ENUM_FMT, &fmtdesc) != -1)
{printf("\t%d.%s\n",fmtdesc.index+1,fmtdesc.description);fmtdesc.index++;
}
同第二步,相关结构体和宏定义可在linux/videodev2.h
中查看。我的USB摄像头输出格式为MJPEG和YUV 4:2:2(YUYV),选择不同的输出格式,会影响到后续对图像数据的处理。
4.获取并设置图像帧的具体属性(代码中称为format),包括帧的宽度和高度(即分辨率)、输出格式以及field,其中field取自枚举型v4l2_field
,由于我的摄像头的field值为1即没有field ( V4L2_FIELD_NONE
),我并不理解这里field的作用。
//获取当前摄像头的具体属性
struct v4l2_format fmt;
fmt.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(cameraFd, VIDIOC_G_FMT, &fmt);
printf("Current data format information:\n\twidth:%d\n\theight:%d\n\tformat:%#x ie. %c%c%c%c\n\tfield:%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.pixelformat,fmt.fmt.pix.pixelformat&0xff, (fmt.fmt.pix.pixelformat>>8)&0xff, (fmt.fmt.pix.pixelformat>>16)&0xff, (fmt.fmt.pix.pixelformat>>24)&0xff, fmt.fmt.pix.field);
如果获得的属性已经是我们想要的结果,则可以省略设置属性这一步骤,否则设置属性:
//设置摄像头的具体属性
fmt.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = ...;
fmt.fmt.pix.heifht = ...;
fmt.fmt.pix.pixelformat = ...;//输出格式需是第三步中查到的格式之一,即首先要求摄像头支持此种格式//格式对应的具体值在头文件linux/videodev2.h中可以查到
fmt.fmt.pix.field = ...;
ioctl(cameraFd, VIDIOC_S_FMT, &fmt);
注意,如果设置的属性是摄像头不支持的,则fmt结构体变量的相应成员的值不会发生改变。
5.向内核层申请缓冲区:缓冲区的作用是保存摄像头采集到的图像,他会和后面步骤提到的输入队列和输出队列配合将一帧帧图像数据送给用户。
struct v4l2_requestbuffers rqstbuf = {.count = 5,.memory = V4L2_MEMORY_MMAP,.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
};
ret = ioctl(cameraFd, VIDIOC_REQBUFS, &rqstbuf);
if(ret < 0) {printf("buffer's actual count is:%d\n", rqstbuf.count);perror("request buffers fail");return -1;
}else {printf("requst buffers success!\nbuffer's actual count is:%d\n", rqstbuf.count);
}
其中,
count为缓冲区的个数,每个缓冲区保存一帧图像,因此缓冲区的大小由第4步中图像帧的宽高和输出格式共同决定。
memory为第2步中提到的读取数据的方法,设置的值必须要求摄像头支持。
type只有两种,即捕获还是输出,对于摄像头来说当然是捕获。
ioctl执行完毕后,程序可能由于内存不足等原因没有申请到指定数量的缓冲区,此时需验证下实际申请到的缓冲区数量。
缓冲区的编号由buffer.index
进行维护。
6.将申请到的缓冲区映射到用户空间,并将所有缓冲区放入输入队列:
struct v4l2_buffer buffer = {.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,.memory = V4L2_MEMORY_MMAP,.index = 0,};unsigned char *video_buffer_ptr[rqstbuf.count];//定义用户空间的一段内存,//内核空间的缓冲区将会映射到此段内存for(int i=0;i<rqstbuf.count;i++) {buffer.index = i;ret = ioctl(cameraFd, VIDIOC_QUERYBUF, &buffer);if(ret<0) {perror("query address of buff failed");return -1;}printf("length of buff NO.%d is %d\n", i+1, buffer.length); video_buffer_ptr[i] = (unsigned char*) mmap(NULL, buffer.length, PROT_READ, MAP_SHARED, cameraFd, buffer.m.offset);if (video_buffer_ptr[i] == MAP_FAILED) { perror("mmap() failed");return -1; } //把所有缓存放入输入队列ret = ioctl(cameraFd, VIDIOC_QBUF, &buffer); if (ret<0) { perror("ioctl(VIDIOC_QBUF) failed");return -1; }}
通过定义一个指针数组unsigned char *video_buffer_ptr[rqstbuf.count];
,我们定义了用户空间的一段内存。mmap
函数负责将内核空间的缓冲区映射到此段内存。
映射完成后,V4L2通过两个队列进行视频流的管理。这两个队列对用户不可见,一个是输入队列,另一个是输出队列。对于捕获设备,输入队列满时将会转变为输出队列。
调用函数ioctl(cameraFd, VIDIOC_QBUF, &buffer);
可以将缓冲区加入输入队列队尾,调用函数ioctl(cameraFd, VIDIOC_DQBUF, &buffer);
从输出队列队首取出缓冲区,而缓冲区已被映射到用户空间,我们就可以从定义的指针指向的地址处开始读取图像数据了。
7.开启图像捕获:
int buffer_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(cameraFd, VIDIOC_STREAMON, &buffer_type);
if(ret<0) {perror("capture stream failed");return -1;
}else {printf("capture start\n");
}
8.从输出队列中取出缓冲区:
struct v4l2_buffer buf;
memset(&buf,0,sizeof(buf));
buf.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory=V4L2_MEMORY_MMAP;
buf.index=0;
do {ret = ioctl(cameraFd, VIDIOC_DQBUF, &buf);if(ret<0) {printf("\rwaiting for capturing...");}
}while(ret<0);
printf("\ncapture successed\n");
注意驱动将输入队列填满需要一定时间,在这之前取出缓冲区必然是会失败的,而由于我是用非阻塞模式打开的设备节点,故代码中使用了do while来不停地尝试将输出队列队头的缓冲区取出。
9.从用户空间读出图像数据并处理:
char* fileName = "/home/root/capture1.jpg";
FILE *fp;
fp = fopen(fileName, "w+");
for(long i=0;i<buf.length;i++) {int data = *(video_buffer_ptr[buf.index]+i);fputc(data, fp);
}
fclose(fp);
printf("saving file successed\n");
ioctl(cameraFd, VIDIOC_QBUF, &buf);//
本段代码所做的处理工作为将图像保存到本地。由于我选择的是以MJPEG格式输出的图像,故摄像头输出的数据是已经经过JPEG编码之后的数据,直接将数据写入.jpg文件即可,Windows和Ubuntu都支持JPEG解码,可以直接打开图片。
注意,每次读取数据后,需要将刚刚使用的缓冲区重新加入输入队列。这样形成一个循环,就可以获得视频流了。
10.完成收尾工作:取消映射、关闭图像捕获
ioctl(cameraFd, VIDIOC_STREAMOFF, &buffer_type);
for(int i=0;i<rqstbuf.count;i++) {munmap(video_buffer_ptr[i], buffer.length);
}
return 0;
最后,说明一下图像及视频编解码问题:
jpg是电脑上常用的图片格式,采用JPEG编码,由于我们的USB摄像头支持MJPEG输出格式,所以可以直接输出JPEG编码后的数据。但是,V4L2并不具有视频的编码能力,故单凭V4L2是无法输出.avi等视频格式的。要想输出视频文件,我们还需要其他专业的编解码库,如ffmpeg。