V4L2是linux操作系统下用于采集图片、视频和音频数据的API接口,配合适当的视频采集设备和相应的驱动程序,可以实现图片、视频、音频等的采集。在远程会议、可视电话、视频监控系统和嵌入式多媒体终端中都有广泛的应用。
重要函数介绍:ioctl函数
ioctl函数主要用于控制设备
1 #include <sys/ioctl.h>
2 int ioctl(int fd, unsigned long request, ...);
3 参数: fd: 打开设备的文件描述符
4 unsigned long request: 给驱动的命令
5 第三个参数是变参:根据第二个参数来定 比如:request是获取摄像头格式命令, 第三个
参数就是存储格式的结构体
6 返回值:成功返回0, 失败‐1
V4L2采集视频显示流程
1. 打开设备,关闭设备
注: video0 查询到当前系统摄像头设备为video0 如果是开发板(video0‐video6, 自己新添
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <linux/videodev2.h>
int main(void)
{
//1.打开设备
int fd = open("/dev/video0", O_RDWR);
if(fd < 0)
{
perror("open fail");
return ‐1;
}
//2.关闭设备
close(fd);
return 0;
}
struct v4l2_fmtdesc
{
__u32 index; // 要查询的格式序号,应用程序设置
enum v4l2_buf_type type; // 帧类型,应用程序设置
__u32 flags; // 是否为压缩格式
__u8 description[32]; // 格式名称
__u32 pixelformat; // 像素格式
__u32 reserved[4]; // 保留
};
enum v4l2_buf_type {
V4L2_BUF_TYPE_VIDEO_CAPTURE = 1,
V4L2_BUF_TYPE_VIDEO_OUTPUT = 2,
V4L2_BUF_TYPE_VIDEO_OVERLAY = 3,
V4L2_BUF_TYPE_VBI_CAPTURE = 4,
V4L2_BUF_TYPE_VBI_OUTPUT = 5,
V4L2_BUF_TYPE_SLICED_VBI_CAPTURE = 6,
V4L2_BUF_TYPE_SLICED_VBI_OUTPUT = 7,
V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8,
V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9,
V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE = 10,
V4L2_BUF_TYPE_SDR_CAPTURE = 11,
V4L2_BUF_TYPE_SDR_OUTPUT = 12,
V4L2_BUF_TYPE_META_CAPTURE = 13,
V4L2_BUF_TYPE_META_OUTPUT = 14,
/* Deprecated, do not use */
V4L2_BUF_TYPE_PRIVATE = 0x80,
};
应用会设置index和type字段。index是用来区别格式的一个整形数;与V4l2所使用的其他索引一样,这个也是从0开始递增至最大允许值为止,应用可以通过一直递增索引值index直到返回EINVAL的方式枚举所有支持的格式。
type字段描述的是数据流类型;对于视频捕捉设备来说(摄像头)就是V4L2_BUF_TYPE_VIDEO_CAPTURE
//获取摄像头支持的格式
struct v4l2_fmtdesc fmt;
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
int i=0;
for(;;i++)
{
fmt.index = i;
int ret = ioctl(fd, VIDIOC_ENUM_FMT, &fmt);
if(ret < 0)
{
break;
}
printf("%s--%s\n", fmt.description, (char*)(&fmt.pixelformat));
}
3、设置摄像头采集格式,获取当前采集格式
//设置采集格式
struct v4l2_format vfmat;
vfmat.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vfmat.fmt.pix.width = 300;//宽
vfmat.fmt.pix.height = 300;//高
vfmat.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; //采集格式
int ret = ioctl(fd, VIDIOC_S_FMT, &vfmat);
if(ret < 0)
{
perror("set fail");
}
//获取当前采集的格式
memset(&vfmat, 0, sizeof(vfmat));//清空
vfmat.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_G_FMT, &vfmat);
if(ret < 0)
{
perror("get fail");
}
printf("当前采集格式:%d:%d--%s\n",vfmat.fmt.pix.width,
vfmat.fmt.pix.height,
(char *)(&vfmat.fmt.pix.pixelformat));
其中
设置采集格式:
struct v4l2_format vfmat;
:定义一个v4l2_format
结构体变量vfmat
,用于设置采集格式。struct v4l2_format { __u32 type; //V4L2_BUF_TYPE_VIDEO_CAPTURE union { struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */ struct v4l2_pix_format_mplane pix_mp; /* V4L2_BUF_TYPE_VIDEO_CAPTURE_MPL ANE */ struct v4l2_window win; /* V4L2_BUF_TYPE_VIDEO_OVERLAY */ struct v4l2_vbi_format vbi; /* V4L2_BUF_TYPE_VBI_CAPTURE */ struct v4l2_sliced_vbi_format sliced; /* V4L2_BUF_TYPE_SLICED_VBI_CAPTUR E */ struct v4l2_sdr_format sdr; /* V4L2_BUF_TYPE_SDR_CAPTURE */ __u8 raw_data[200]; /* user‐defined */ } fmt; }; struct v4l2_pix_format { __u32 width; //采集宽 __u32 height; //采集高 __u32 pixelformat; //采集格式 __u32 field; /* enum v4l2_field */ __u32 bytesperline; /* for padding, zero if unused */ __u32 sizeimage; __u32 colorspace; /* enum v4l2_colorspace */ __u32 priv; /* private data, depends on pixelformat */ __u32 flags; /* format flags (V4L2_PIX_FMT_FLAG_*) */ __u32 ycbcr_enc; /* enum v4l2_ycbcr_encoding */ __u32 quantization; /* enum v4l2_quantization */ __u32 xfer_func; /* enum v4l2_xfer_func */ };
vfmat.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
:设置数据流类型为视频采集。vfmat.fmt.pix.width = 300;
:设置视频采集的宽度为300像素。vfmat.fmt.pix.height = 300;
:设置视频采集的高度为300像素。vfmat.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
:设置视频采集的像素格式为 YUYV,这是一种常见的格式,每4个字节表示两个像素点,其中 Y 表示亮度信息,U 和 V 表示色度信息。int ret = ioctl(fd, VIDIOC_S_FMT, &vfmat);
:通过ioctl
系统调用将设置好的采集格式应用到视频设备。-
memset(&vfmat, 0, sizeof(vfmat));
:清空vfmat
结构体
清空 vfmat
结构体是为了确保结构体中的字段都被正确初始化。在这段代码中,vfmat
结构体在设置采集格式之前被使用,然后在获取当前采集格式之前被清空。
结构体在声明后,它的字段中可能包含一些随机的数据。为了确保在设置采集格式之前的字段值都是有效的、预期的值,需要将结构体清空,使其字段都初始化为零或空。
在这里,使用 memset(&vfmat, 0, sizeof(vfmat))
将 vfmat
结构体的所有字节都设置为零,从而清空了结构体中的所有字段。这样做可以确保之前的设置不会影响到当前的设置,同时也可以避免在获取当前采集格式时读取到未初始化的值。
清空结构体是一种良好的编程实践,可以确保结构体中的字段始终处于可控状态。
4. 申请内核缓冲区队列,把缓冲区队列映射到用户空间
1 struct v4l2_requestbuffers {
2 __u32 count; //申请缓冲区个数
3 __u32 type; /* enum v4l2_buf_type */
4 __u32 memory; /* enum v4l2_memory */ ‐‐设置为映射
5 __u32 reserved[2];
6 };
1 struct v4l2_buffer {
2 __u32 index;
3 __u32 type;
4 __u32 bytesused;
5 __u32 flags;
6 __u32 field;
7 struct timeval timestamp;
8 struct v4l2_timecode timecode;
9 __u32 sequence;
10
11 /* memory location */
12 __u32 memory;
13 union {
14 __u32 offset;
15 unsigned long userptr;
16 struct v4l2_plane *planes;
17 __s32 fd;
18 } m;
19 __u32 length;
20 __u32 reserved2;
21 __u32 reserved;
22 };
//重点关注index和length
将申请到的帧缓冲映射到用户空间,这样就可以直接操作采集到的帧了,而不必去复制。
1 //申请缓冲区队列4
2 struct v4l2_requestbuffers requestbuf;
3 requestbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
4 requestbuf.count = 4;//申请4个缓冲区
5 requestbuf.memory = V4L2_MEMORY_MMAP;//映射空间
6 ret = ioctl(fd, VIDIOC_REQBUFS, &requestbuf);
7 if(ret < 0)
8 {
9 perror("request fail");
10 }
11
12 unsigned char *mmp[4]; int size[4];//用户空间首地址
13 for(int i=0 ; i<requestbuf.count; i++)
14 {
15 //1.获取某一个缓冲区
16 struct v4l2_buffer vbuf;
17 vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
18 vbuf.index = i;
19 ret = ioctl(fd, VIDIOC_QUERYBUF, &vbuf);//从内核中查询一个空间做映射
20 if(ret < 0)
21 {
22 perror("QUERYBUF fail");
23 }
24 //2.映射
25 mmp[i] = mmap(NULL, vbuf.length, PROT_READ|PROT_WRITE, MAP_SHARED, fd,
vbuf.m.offset);
26 size[i] = vbuf.length;
27 //3.告诉驱动映射完毕
28 ret = ioctl(fd, VIDIOC_QBUF, &vbuf);
29 if(ret < 0)
30 {
31 perror("QBUF fail");
32 }
33 }
34

1 #define VIDIOC_STREAMON _IOW('V', 18, int)
2 #define VIDIOC_STREAMOFF _IOW('V', 19, int)
1 //开始采集
2 int val = V4L2_BUF_TYPE_VIDEO_CAPTURE;
3 ret = ioctl(fd,VIDIOC_STREAMON, &val);
4 if(ret < 0)
5 {
6 perror("start fail");
7 }
8 {
9 //采集代码
10 }
11
12 //停止采集
13 ret = ioctl(fd,VIDIOC_STREAMOFF, &val);
14 if(ret < 0)
15 {
16 perror("stop fail");
17 }
1 //采集一帧数据
2 struct v4l2_buffer readbuffer;
3 readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//先初始化
4 ret = ioctl(fd, VIDIOC_DQBUF, &readbuffer);
5 if(ret < 0)
6 {
7 perror("read‐start fail");
8 }
9 //根据readbuffer结构体中的index获取用户空间映射空间, length获取数据长度
10 FILE *file = fopen("my.jpg", "w+");
11 fwrite(mmp[readbuffer.index], readbuffer.length, 1, file);
12 fclose(file);
13
14 ret = ioctl(fd, VIDIOC_QBUF, &readbuffer);//通知驱动内核使用完毕
15 if(ret < 0)
16 {
17 perror("read‐end fail");
18 }
1 //释放映射
2 for(int i=0; i<4; i++)
3 munmap(mmp[i], size[i]);
4 close(fd)
注意:
操作系统一般把系统使用的内存划分成用户空间和内核空间,分别由应用程序管理和操作系统管理。应用程序可以直接访问内存的地址,而内核空间存放的是供内核访问的代码和数据,用户不能直接访问。
v4l2捕获的数据,最初是存放在内核空间的,这意味着用户不能直接访问该段内存,必须通过某些手段来转换地址。
一共有三种视频采集方式:使用read、write方式;内存映射方式和用户指针模式。
read、write方式:在用户空间和内核空间不断拷贝数据,占用了大量用户内存空间,效率不高。
内存映射方式:把设备里的内存映射到应用程序中的内存控件,直接处理设备内存,这是一种有效的方式。上面的mmap函数就是使用这种方式。
用户指针模式:内存片段由应用程序自己分配。这点需要在v4l2_requestbuffers里将memory字段设置成V4L2_MEMORY_USERPTR。
处理采集数据
V4L2有一个数据缓存,存放req.count数量的缓存数据。数据缓存采用FIFO的方式,当应用程序调用缓存数据时,缓存队列将最先采集到的视频数据缓存送出,并重新采集一张视频数据。