一.V4L2简介
V4L2(Video for Linux 2):Linux内核中视频设备中的驱动框架,对于应用层它提供了一系列的API接口,同时对于硬件层,它适配大部分的视频设备,因此通过调用V4L2的接口函数可以适配大部分的视频设备。
二、操作流程
1.打开设备
当把摄像头插入到电脑后,执行ls /dev/vi* 可看到/dev目录下出现摄像头的video节点。这里出现了两个设备节点:dev/video0、dev/video1,是因为一个是图像/视频采集,一个是metadata采集。
使用open函数打开摄像头节点
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char**argv)
{
if(argc != 2)
{
printf("%s </dev/video0,1...>\n", argv[0]);
return -1;
}
int fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("打开设备失败");
return -1;
}
close(fd);
return 0;
}
2.获取支持格式和功能
使用ioctl函数int ioctl(int fd, unsigned long request, ...)获取摄像头支持的格式,这里ioctl的参数可以在头文件videodev2.h中找到(路径为/usr/include/linux/)。对应操作命令如下表,这里需要获取摄像头支持的格式,所以操作命令为VIDIOC_ENUM_FMT,对应的结构体是struct v4l2_fmtdesc。另建议在source insight下下载linux源码,然后建立该工程,在该工程下写代码,便于查看对应结构体。
关于v4l2_fmtdesc结构体
struct v4l2_fmtdesc {
__u32 index; /* Format number */
__u32 type; /* enum v4l2_buf_type */
__u32 flags;
__u8 description[32]; /* Description string */
__u32 pixelformat; /* Format fourcc */
__u32 reserved[4];
};
该结构体定义如上,因为v4l2不单单针对摄像头,所以在使用前需要对type成员进行初始化,v4l2_buf_type这个枚举总共有13个成员,这里选择1,即视频抓取。
在用代码读取过程中时,因为支持多种格式,所以用while循环读取支持的格式,
关于v4l2_capability结构体
struct v4l2_capability
{
u8 driver[16]; // 驱动名字
u8 card[32]; // 设备名字
u8 bus_info[32]; // 设备在系统中的位置
u32 version;// 驱动版本号
u32 capabilities;// 设备支持的操作
u32 reserved[4]; // 保留字段
};
除了用 v4l2_fmtdesc结构体获取像素格式,还可以通过v4l2_capability结构体来获取设备的功能,主要看capabilities成员,其是否支持视频捕获(V4L2_CAP_VIDEO_CAPTURE)、以及是否支持流读写(V4L2_CAP_STREAMING)。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
int main(int argc, char**argv)
{
if(argc != 2)
{
printf("%s </dev/video0,1...>\n", argv[0]);
return -1;
}
//打开摄像头设备
int fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("打开设备失败");
return -1;
}
//获取摄像头支持格式,使用ioctl函数int ioctl(int fd, unsigned long request, ...);
struct v4l2_fmtdesc v4fmt;
struct v4l2_capability cap;
v4fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //选择视频抓取
int i = 0;
while(1)
{
v4fmt.index = i;
i++;
int ret = ioctl(fd, VIDIOC_ENUM_FMT, &v4fmt);
if(ret < 0)
{
perror("获取格式失败");
break;
}
printf("index = %d\n", v4fmt.index);
printf("flags = %d\n", v4fmt.flags);
printf("descrrption = %s\n", v4fmt.description);
unsigned char *p = (unsigned char*)&v4fmt.pixelformat;
printf("pixelformat = %c%c%c%c\n", p[0],p[1],p[2],p[3]);
printf("reserved = %d\n", v4fmt.reserved[0]);
}
int ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
if(ret < 0)
perror("获取功能失败");
printf("drivers:%s\n", cap.driver);//读取驱动名字
if(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)
printf("%s 支持视频捕获\n", argv[1]);
if(cap.capabilities & V4L2_CAP_STREAMING)
printf("%s 支持流读写\n", argv[1]);
close(fd);
return 0;
}
运行结果
运行结果说明我的摄像头支持视频捕获,同时支持流读写支持两种像素格式YUYV和MJPG,读不到video1支持的像素格式,该摄像头的两个设备节点仅video0用于视频采集。关于YUYV和MJPG可以看这篇博文。
https://blog.csdn.net/u014260892/article/details/51883723
3.配置摄像头
在视频采集之前需要设置视频采集格式,定义v4l2_format结构体变量,然后通过结构体v4l2_pix_format来设置采集的高、宽以及像素格式(YUYV),设置之后,可以采用打印的方式来查看是否设置成功。
struct v4l2_format {
__u32 type;
union {
struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
struct v4l2_pix_format_mplane pix_mp; /* V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE */
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_CAPTURE */
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 */
};
测试程序如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
int main(int argc, char**argv)
{
if(argc != 2)
{
printf("%s </dev/video0,1...>\n", argv[0]);
return -1;
}
//打开摄像头设备
int fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("打开设备失败");
return -1;
}
//设置摄像头采集格式
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //选择视频抓取
vfmt.fmt.pix.width = 640;//设置宽,不能随意设置
vfmt.fmt.pix.height = 480;//设置高
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;//设置视频采集格式
int ret = ioctl(fd, VIDIOC_S_FMT, &vfmt);// VIDIOC_S_FMT:设置捕获格式
if(ret < 0)
{
perror("设置采集格式错误");
}
memset(&vfmt, 0, sizeof(vfmt));
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_G_FMT, &vfmt);
if(ret < 0)
{
perror("读取采集格式失败");
}
printf("width = %d\n", vfmt.fmt.pix.width);
printf("width = %d\n", vfmt.fmt.pix.height);
unsigned char *p = (unsigned char*)&vfmt.fmt.pix.pixelformat;
printf("pixelformat = %c%c%c%c\n", p[0],p[1],p[2],p[3]);
close(fd);
return 0;
}
运行结果和设置一样
4.向内核申请帧缓冲队列并映射
V4L2读取数据时有两种方式,第一种是用read读取(调用read函数),第二种是用流(streaming)读取,在第二步上已经获取到我的设备支持流读写,为了提高效率采用流读写,流读写就是在内核中维护一个缓存队列,然后再映射到用户空间,应用层直接读取队列中的数据。
步骤为:申请缓冲区->逐个查询申请到的缓冲区->逐个映射->逐个放入队列中
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <sys/mman.h>
int main(int argc, char**argv)
{
if(argc != 2)
{
printf("%s </dev/video0,1...>\n", argv[0]);
return -1;
}
//打开摄像头设备
int fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("打开设备失败");
return -1;
}
//设置摄像头采集格式
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //选择视频抓取
vfmt.fmt.pix.width = 640;//设置宽,不能随意设置
vfmt.fmt.pix.height = 480;//设置高
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;//设置视频采集格式
int ret = ioctl(fd, VIDIOC_S_FMT, &vfmt);// VIDIOC_S_FMT:设置捕获格式
if(ret < 0)
{
perror("设置采集格式错误");
}
memset(&vfmt, 0, sizeof(vfmt));
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_G_FMT, &vfmt);
if(ret < 0)
{
perror("读取采集格式失败");
}
printf("width = %d\n", vfmt.fmt.pix.width);
printf("width = %d\n", vfmt.fmt.pix.height);
unsigned char *p = (unsigned char*)&vfmt.fmt.pix.pixelformat;
printf("pixelformat = %c%c%c%c\n", p[0],p[1],p[2],p[3]);
//申请缓冲队列
struct v4l2_requestbuffers reqbuffer;
reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuffer.count = 4; //申请4个缓冲区
reqbuffer.memory = V4L2_MEMORY_MMAP; //采用内存映射的方式
ret = ioctl(fd, VIDIOC_REQBUFS, &reqbuffer);
if(ret < 0)
{
perror("申请缓冲队列失败");
}
//映射,映射之前需要查询缓存信息->每个缓冲区逐个映射->将缓冲区放入队列
struct v4l2_buffer mapbuffer;
unsigned char *mmpaddr[4];//用于存储映射后的首地址
mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//初始化type
for(int i = 0; i < 4; i++)
{
mapbuffer.index = i;
ret = ioctl(fd, VIDIOC_QUERYBUF, &mapbuffer); //查询缓存信息
if(ret < 0)
perror("查询缓存队列失败");
mmpaddr[i] = (unsigned char *)mmap(NULL, mapbuffer.length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, mapbuffer.m.offset);//mapbuffer.m.offset映射文件的偏移量
//放入队列
ret = ioctl(fd, VIDIOC_QBUF, &mapbuffer);
if(ret < 0)
perror("放入队列失败");
}
close(fd);
return 0;
}
5.采集视频
做完前面的设置就可以进行采集数据,打开设备->读取数据->关闭设备->释放映射。
读取数据的本质就是从上一个步骤中映射的队列中取出数据,取出数据后需要将该缓冲区放入队列中,以便于再去采集下一帧数据。
为了便于查看,在设置采集格式时,选择MJPEG格式,用fopen函数建立一个1.jpg文件,用fwrite函数保存读到的一帧数据。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <sys/mman.h>
int main(int argc, char**argv)
{
if(argc != 2)
{
printf("%s </dev/video0,1...>\n", argv[0]);
return -1;
}
//1.打开摄像头设备
int fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("打开设备失败");
return -1;
}
//2.设置摄像头采集格式
struct v4l2_format vfmt;
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //选择视频抓取
vfmt.fmt.pix.width = 640;//设置宽,不能随意设置
vfmt.fmt.pix.height = 480;//设置高
vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;//设置视频采集像素格式
int ret = ioctl(fd, VIDIOC_S_FMT, &vfmt);// VIDIOC_S_FMT:设置捕获格式
if(ret < 0)
{
perror("设置采集格式错误");
}
memset(&vfmt, 0, sizeof(vfmt));
vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_G_FMT, &vfmt);
if(ret < 0)
{
perror("读取采集格式失败");
}
printf("width = %d\n", vfmt.fmt.pix.width);
printf("width = %d\n", vfmt.fmt.pix.height);
unsigned char *p = (unsigned char*)&vfmt.fmt.pix.pixelformat;
printf("pixelformat = %c%c%c%c\n", p[0],p[1],p[2],p[3]);
//4.申请缓冲队列
struct v4l2_requestbuffers reqbuffer;
reqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuffer.count = 4; //申请4个缓冲区
reqbuffer.memory = V4L2_MEMORY_MMAP; //采用内存映射的方式
ret = ioctl(fd, VIDIOC_REQBUFS, &reqbuffer);
if(ret < 0)
{
perror("申请缓冲队列失败");
}
//映射,映射之前需要查询缓存信息->每个缓冲区逐个映射->将缓冲区放入队列
struct v4l2_buffer mapbuffer;
unsigned char *mmpaddr[4];//用于存储映射后的首地址
unsigned int addr_length[4];//存储映射后空间的大小
mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//初始化type
for(int i = 0; i < 4; i++)
{
mapbuffer.index = i;
ret = ioctl(fd, VIDIOC_QUERYBUF, &mapbuffer); //查询缓存信息
if(ret < 0)
perror("查询缓存队列失败");
mmpaddr[i] = (unsigned char *)mmap(NULL, mapbuffer.length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, mapbuffer.m.offset);//mapbuffer.m.offset映射文件的偏移量
addr_length[i] = mapbuffer.length;
//放入队列
ret = ioctl(fd, VIDIOC_QBUF, &mapbuffer);
if(ret < 0)
perror("放入队列失败");
}
//打开设备
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_STREAMON, &type);
if(ret < 0)
perror("打开设备失败");
//从队列中提取一帧数据
struct v4l2_buffer readbuffer;
readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_DQBUF, &readbuffer);//从缓冲队列获取一帧数据(出队列)
//出队列后得到缓存的索引index,得到对应缓存映射的地址mmpaddr[readbuffer.index]
if(ret < 0)
perror("获取数据失败");
FILE *file = fopen("1.jpg", "w+");//建立文件用于保存一帧数据
fwrite(mmpaddr[readbuffer.index], readbuffer.length, 1, file);
fclose(file);
//读取数据后将缓冲区放入队列
ret = ioctl(fd, VIDIOC_QBUF, &readbuffer);
if(ret < 0)
perror("放入队列失败");
//关闭设备
ret = ioctl(fd, VIDIOC_STREAMOFF, &type);
if(ret < 0)
perror("关闭设备失败");
//取消映射
for(int i = 0; i < 4; i++)
munmap(mmpaddr[i], addr_length[i]);
close(fd);
return 0;
}
运行程序,目录下顺利得到jpg文件,但是打开时报 (Not a JPEG file: starts with 0xe0 0xc1)错误
三、采集到的jpg图片报(Not a JPEG file: starts with 0xe0 0xc1)错误
如上所示,顺利采集到1.jpg图片,但是测试好几次均报该错误,在仔细检查设置无误后,猜测可能是还未采集到数据就已经读取出来了,所以在队列中获取一帧数据的前面加了sleep(5)延迟5s采集,但仅有一次成功,可以读出但采集的数据有损坏,而且修改sleep的数值,每次的错误文件的两个头码还不相同。同时在申请缓存之前将查询缓存信息之前现将结构体清0,然后再映射,发现还是有该问题。另外尝试了加poll函数等待读取,同样无法读取。
于是我将采集格式变为yuyv格式,发现程序阻塞至ret = ioctl(fd, VIDIOC_DQBUF, &readbuffer),查找相关帖子发现是虚拟机中USB兼容性问题,将设置中USB兼容性改为USB3.1阻塞消失,同时jpg格式也顺利采集到数据。1.jpg文件也可以顺利打开。