目录

什么是V4L2?

V4L2编程流程

1.打开摄像头设备

2.查询设备的属性或功能

3.设置合适的采样方式

4.如果支持STREAM则设置缓冲队列属性

5.内存映射

6.开启视频采集

7.帧缓冲出队、对采集的数据进行处理(保存为图片,或者通过网络协议发送)

8.停止采集,释放映射,关闭设备


什么是V4L2?

V4L2,即 Video for linux two ,是 Linux 内核中视频类设备的一套驱动框架,为视频类设备驱动开发和应用层提供了一套统一的接口规范使用 V4L2 设备驱动框架注册的设备会在 Linux 系统/dev/目录下生成对应的设备节点文件,设备节点的名称通常为 videoX(X为0、1、2…)

V4L2是Linux视频处理模块的最新标准代码,包括对视频输入设备的处理,如高频(即、电视信号输入端子)或摄像头,还包括视频处理输出装置。一般来说,最常见的是使用V4L2来处理相机数据采集的问题。我们通常使用的相机实际上是一个图像传感器,将捕捉到的光线通过视频芯片处理后,编码成JPG/MJPGYUV格式输出。我们可以很容易地通过V4L2与第一台摄像机设备“通信”,如设置或获取它们的工作参数。

V4L2编程流程

在内核中,摄像头捕捉到的视频数据,我们可以使用一个队列来存储。我们做的工作大致是这样的:首先配置摄像头的相关参数,可以正常工作,然后申请一个号码的内核视频缓存,送他们到队列,像三个空盘子在传送带上。然后我们还需要三个内核缓存区域通过mmap函数映射到用户空间,
这样我们就可以操作相机数据在用户层,然后我们可以启动相机开始数据采集,每一帧捕获数据我们可以做一个团队操作,读取数据,然后再阅读内核缓存的数据小组,依次循环。

V4L2接口编程步骤:

  1. 首先是打开摄像头设备;
  2. 查询设备的属性或功能;
  3. 设置设备的参数,譬如像素格式、 帧大小、 帧率;
  4. 申请帧缓冲、 内存映射(.把内核的缓冲区队列映射到用户空间);
  5. 帧缓冲入队;
  6. 开启视频采集;
  7. 帧缓冲出队、对采集的数据进行处理;
  8. 处理完后,再次将帧缓冲入队,往复;
  9. 结束采集,释放映射,关闭设备。

1.打开摄像头设备

视频类设备对应的设备节点为/dev/videoX(X为0、1、2…)可以使用命令 ls /dev/video* 查看视频类设备对应的设备节点,在ARM板上插上USB摄像头之后可以看到多了一个设备节点。

javacv 一个usb摄像头跟另一个usb麦克风录制视频 usb摄像头录像_数据

//1. 打开摄像头
    int fd = open("/dev/video0", O_RDWR);
    if(-1 == fd)
    {
        perror("open");//打开失败
        return -1;
    }

2.查询设备的属性或功能

1.提取摄像头的能力,查看设备是否为视频采集设备以及支持那些功能

#if 0
/***结构体里是摄像头支持的一些功能****/
struct v4l2_capability { 
    __u8 driver[16]; /* i.e. "bttv" */ 
    __u8 card[32]; /* i.e. "Hauppauge WinTV" */ 
    __u8 bus_info[32]; /* "PCI:" + pci_name(pci_dev) */ 
    __u32 version; /* should use KERNEL_VERSION() */ 
    __u32 capabilities; /* Device capabilities */ 
    __u32 reserved[4]; 
}; 

#endif

    struct v4l2_capability cap = {0};
    //2. 提取摄像头的能力(linux v4l2规范)
    int ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
    if (-1 == ret)
    {
        perror("ioctl");
        return -1;
    }  
    printf("driver name: %s   device name: %s device location: %s\n", cap.driver, cap.card, cap.bus_info);
    printf("version: %u.%u.%u\n", (cap.version >> 16) & 0xff, (cap.version >> 8) & 0xff, cap.version & 0xff);

    if(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)
        printf("v4l2 dev support capture\n");

    if(cap.capabilities & V4L2_CAP_VIDEO_OUTPUT)
        printf("v4l2 dev support output\n");

    if(cap.capabilities & V4L2_CAP_VIDEO_OVERLAY)
        printf("v4l2 dev support overlay\n");

    if(cap.capabilities & V4L2_CAP_STREAMING)
        printf("v4l2 dev support streaming\n");

    if(cap.capabilities & V4L2_CAP_READWRITE)
        printf("v4l2 dev support read write\n");

2.查看摄像头所支持的所有像素格式,先看一下v4l2_fmtdesc结构体里面包含了那些成员

struct v4l2_fmtdesc {
	__u32 index; /* index 就是一个编号 */
	__u32 type; /* enum v4l2_buf_type */
	__u32 flags;
	__u8 description[32]; /* description 字段是一个简单地描述性字符串 */
	__u32 pixelformat; /* pixelformat 字段则是对应的像素格式编号 */
	__u32 reserved[4];
};

代码查看设备支持那些格式如下:

struct v4l2_fmtdesc fmtdesc;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmtdesc.index = 0;
    //支持哪些图片格式
    while (!ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc))
    {
        printf("fmt:%s\n", fmtdesc.description);
        fmtdesc.index++;
    }

3.查看摄像头所支持的分辨率,先看一下v4l2_frmsizeenum结构体

struct v4l2_frmsizeenum {
	__u32 index; /* Frame size number */
	__u32 pixel_format; /* 像素格式 */
	__u32 type; /* type */
	union { /* Frame size */
		struct v4l2_frmsize_discrete discrete;
		struct v4l2_frmsize_stepwise stepwise;
	};
	__u32 reserved[2]; /* Reserved space for future use */
};

struct v4l2_frmsize_discrete {
	__u32 width; /* Frame width [pixel] */
	__u32 height; /* Frame height [pixel] */
};

我们要枚举出摄像头 MJPEG 像素格式所支持的所有分辨率:

struct v4l2_frmsizeenum frmsize;
frmsize.index = 0;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

printf("MJPEG格式支持所有分辨率如下:\n");
frmsize.pixel_format = V4L2_PIX_FMT_MJPEG;
while(ioctl(fd,VIDIOC_ENUM_FRAMESIZES,&frmsize) == 0){
    printf("frame_size<%d*%d>\n",frmsize.discrete.width,frmsize.discrete.height);
    frmsize.index++;
}

4.查看摄像头所支持的视频采集帧率,先看一下v4l2_frmivalenum结构体

struct v4l2_frmivalenum {
	__u32 index; /* Frame format index */
	__u32 pixel_format;/* Pixel format */
	__u32 width; /* Frame width */
	__u32 height; /* Frame height */
	__u32 type; /* type */
	union { /* Frame interval */
	struct v4l2_fract discrete;
	struct v4l2_frmival_stepwise stepwise;
	};
	__u32 reserved[2]; /* Reserved space for future use */
};

struct v4l2_fract {
	__u32 numerator; //分子
	__u32 denominator; //分母
};

比如我们要枚举出摄像头 MJPEG 格式下640*480分辨率所支持的帧数:

struct v4l2_frmivalenum frmival;
    frmival.index = 0;
    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    frmival.pixel_format = V4L2_PIX_FMT_MJPEG;
    frmival.width = 640;
    frmival.height = 480;
    while(ioctl(fd,VIDIOC_ENUM_FRAMEINTERVALS,&frmival) == 0){
        printf("frame_interval under frame_size <%d*%d> support %dfps\n",frmival.width,frmival.height,frmival.discrete.denominator / frmival.discrete.numerator);
        frmival.index++;
    }

3.设置合适的采样方式

首先要定义结构体v4l2_format来保存采集格式信息,使用VIDIOC_S_FMT指令设置格式,最后用VIDIOC_G_FMT指令查看相关参数是否生效,我们先来看看结构体v4l2_format

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 */ 
        struct v4l2_meta_format meta; /* V4L2_BUF_TYPE_META_CAPTURE */ 
        __u8 raw_data[200]; /* user-defined */ 
    } fmt;
 };

代码实现:

//3. 设置合适的采样方式
    struct v4l2_format v4l2_fmt;
    memset(&v4l2_fmt, 0, sizeof(struct v4l2_format));
    v4l2_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 
    v4l2_fmt.fmt.pix.width = W; //宽度
    v4l2_fmt.fmt.pix.height = H; //高度
    v4l2_fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; //像素格式M-JPEG
    v4l2_fmt.fmt.pix.field = V4L2_FIELD_ANY;

     // 检查设置参数是否生效
    if (ioctl(fd, VIDIOC_S_FMT, &v4l2_fmt) < 0)
    {
        printf("ERR(%s):VIDIOC_S_FMT failed\n", __func__);
        return -1;
    }

4.如果支持STREAM则设置缓冲队列属性

申请帧缓冲顾名思义就是申请用于存储一帧图像数据的缓冲区, 使 VIDIOC_REQBUFS 指令可申请帧缓冲其中struct v4l2_requestbuffers 结构体描述了申请帧缓冲的信息。我们先看看结构体v4l2_buffer

struct v4l2_buffer { 
    __u32 index; //缓冲区编号 
    __u32 type; 
    __u32 bytesused; //图像大小 
    __u32 flags; 
    __u32 field; 
    struct timeval timestamp; 
    struct v4l2_timecode timecode; 
    __u32 sequence; /* memory location */ 
    __u32 memory; 
    union { 
        __u32 offset; 
        unsigned long userptr; 
        struct v4l2_plane *planes;
        __s32 fd; 
    } m; 
    __u32 length;
    __u32 reserved2; 
    union { 
        __s32 request_fd; 
        __u32 reserved; 
    }; 
};

代码实现:

void* addr[4]; //缓存地址, 假设定为系统缓存4帧 
unsigned int size[4];
struct v4l2_requestbuffers req; 
req.count = 4; //缓存数量 
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 
req.memory = V4L2_MEMORY_MMAP; 
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) 
{ 
    printf("ERR(%s):VIDIOC_REQBUFS failed\n", __func__); 
    return -1; 
}

5.内存映射

把内核的缓冲区队列映射到用户空间

int i = 0;
    for(;i<4; i++)
    {
        struct v4l2_buffer v4l2_buffer;
        memset(&v4l2_buffer, 0, sizeof(struct v4l2_buffer));
        v4l2_buffer.index = i; //想要查询的缓存
        v4l2_buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        v4l2_buffer.memory = V4L2_MEMORY_MMAP;

        /* 查询缓存信息 */
        int ret = ioctl(fd, VIDIOC_QUERYBUF, &v4l2_buffer);
        if(ret < 0)
        {
            printf("Unable to query buffer.\n");
            return -1;
        }

        /* 得到图像缓冲位置 */
        addr[i] = mmap(NULL /* start anywhere */ ,
                v4l2_buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED,
                fd, v4l2_buffer.m.offset);
        size[i]=v4l2_buffer.length;

        if (ioctl(fd, VIDIOC_QBUF, &v4l2_buffer) < 0)
        {
            printf("ERR(%s):VIDIOC_QBUF failed\n", __func__);
            return -1;
        }
    }

6.开启视频采集

//5. 开启采样流
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(fd, VIDIOC_STREAMON, &type) < 0)
    {
        printf("ERR(%s):VIDIOC_STREAMON failed\n", __func__);
        return -1;
    }

7.帧缓冲出队、对采集的数据进行处理(保存为图片,或者通过网络协议发送)

这里我们使用了IO多路复用,可以同时对多个设备进行操作。

//6. 等待采样成功并提取图像(select)
    struct pollfd poll_fds[1];
    poll_fds[0].fd = fd;
    poll_fds[0].events = POLLIN; //关注可读
    poll(poll_fds, 1, 10000);//等待有图像准备好

    //6.2 提取采样内容
    struct v4l2_buffer buffer;
    buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buffer.memory = V4L2_MEMORY_MMAP;
    if (ioctl(fd, VIDIOC_DQBUF, &buffer) < 0)
    {
        printf("ERR(%s):VIDIOC_DQBUF failed, dropped frame\n", __func__);
        return -1;
    }
    //buffer.index: 成功拍摄出画面的下标
    //buffer.bytesused: 成功拍下来图片的大小
    //addr[buffer.index]: 图片存储的内存位置
    printf("ready: %u %u\n", buffer.index, buffer.bytesused);
    //假如实现视频监控: 你应该将内存图片通过网络发走!(这里使用了UDP协议)
#if 1
    int ret=sendto(Socket_fd, addr[buffer.index], buffer.bytesused, 0, (struct sockaddr *)&Phone_ipaddr, sizeof(Phone_ipaddr));
    if(ret<0)
    {
        perror("sendto");
    }
    printf("%d\n",ret);
#endif
    //假设实现照相机: 你应该将内存图片保存到文件中!
#if 0
    int fdjpg = open("1.jpg", O_CREAT|O_WRONLY, 0666);
    if(-1 == fdjpg)
    {
        perror("open  1.jpg");
    }
    else
    {
        //内存图片保存到文件中
        write(fdjpg, addr[buffer.index], buffer.bytesused);
    }
    close(fdjpg);
    break;
#endif	
    //6.3 标识图像已经取走,读取数据并处理完之后要再次入队
    if (ioctl(fd, VIDIOC_QBUF, &buffer) < 0)
    {
        printf("ERR(%s):VIDIOC_QBUF failed\n", __func__);//入队失败
        return -1;
    }
}

8.停止采集,释放映射,关闭设备

if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0)
    {
        printf("ERR(%s):VIDIOC_STREAMOFF failed\n", __func__);
        return -1;
    }
 // 释放映射
    for(int i=0;i<4;i++){
        munmap(add[i],size[i]);
    }   
	close(fd);