转自:http://blog.csdn.net/jansonzhe/article/details/47334671
在我的上一篇文章基于V4L2驱动程序的USB摄像头Android(JNI)的编写(一)中,我详细介绍了如何配置V4L2驱动程序的采集环境,那么在这篇文章中,我将详细分析V4L2采集视频的过程。
一、向驱动程序申请缓冲帧
缓冲帧,顾名思义就是在Linux驱动程序中用于临时存放数据的“容器”,在V4L2驱动程序中,也就是存放我们的视频流数据。而什么又是向驱动程序申请缓冲帧呢,因为在V4L2驱动程序中,其配备的缓冲帧的个数并不是固定的,每一次我们使用的时候都要根据我们的需求向V4L2驱动程序申请,这也体现了V4L2的强大以及设计的合理性,保证了不必要的资源浪费。下面介绍如何向V4L2驱动程序申请缓冲帧。
使用命令:VIDIOC_REQBUFS
struct v4l2_requestbuffers req;
//设定v4l2_requestbuffers的参数信息
CLEAR (req);
req.count = 4; //缓存数量
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //数据流类型,这个参数不能改变
req.memory = V4L2_MEMORY_MMAP;//使用内存映射方式。
//申请缓冲数,使用VIDIOC_REQBUFS,注意上面的req.count虽然赋值为4,但是并不一定申请之后,得到的缓冲数也为4
if (-1 == xioctl (fd, VIDIOC_REQBUFS, &req)) {
if (EINVAL == errno) {
LOGE("%s does not support memory mapping", dev_name);
return ERROR_LOCAL;
} else {
return errnoexit ("VIDIOC_REQBUFS");
}
}
//如果申请得到的缓冲4小于2的话,则表示没有充分的内存(内核空间,具有实际的物理内存)
if (req.count < 2) {
LOGE("Insufficient buffer memory on %s", dev_name);
return ERROR_LOCAL;
}
//定义buffer类型结构体
struct buffer {
void * start;
size_t length;
};
//定义buffer类型的结构体指针
struct buffer * buffers = NULL;
//根据申请到的缓存数,在用户空间也分配req.count个buffer指针用于存放内核空间的映射。
buffers = calloc (req.count, sizeof (*buffers));
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
对于上面的程序,我想着重介绍的是v4l2_requestbuffers结构体,该结构体定义了一些设置缓冲帧的信息,其完整的结构体定义如下:
struct v4l2_requestbuffers
{
__u32 count //申请缓冲帧的数量
__u32 type //数据流类型,这个参数一般是固定的
__u32 memory //采用内存的映射方式
__u32 reserved[2] //保留数据,一般不用。
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
在程序中,我们为什么还要在用户空间里面定义一个buffer类型的结构体指针呢,并且申请的指针数量和内核空间里面定义的缓冲帧数一样,这是应用了我们Linux内核映射的一种方法。采用的内存映射的原因是因为由于视频流数据比较大,如果我们还采用以往的那种copy_from_user或者copy_to_user的方式的话,那么这样不仅耗费内核空间的额内存,而且还耗费用户空间的内存,同时还大大降低了驱动程序的性能。而采用内存映射的话,只要我们在用户空间将内存空间缓冲帧的物理地址映射到我们的用户空间就可以了,这样在用户空间我们就直接使用内核缓冲帧的数据就可以了。
二、将缓冲帧映射到用户空间
上一步中,我们已经在内核空间中申请了缓冲帧,同时还在用户空间中定义了相等数量的缓冲帧指针,那么这一步我们当然就要完成将内核中的缓冲帧映射到用户空间的工作啦。
这一步中,首先我们要使用VIDIOC_QUERYBUF命令获取缓冲帧的相关信息。然后获取之后,我们再对每一个缓冲帧进行逐一映射。主要代码如下:
for (n_buffers = 0; n_buffers < req.count; ++n_buffers) {
struct v4l2_buffer buf;
CLEAR (buf);
//初始化一下v4l2_buffer
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
//完成对内核缓冲帧的编号,使之与用户空间的缓冲帧指针对应。
buf.index = n_buffers;
//获取缓存buf的地址以及其他信息。
if (-1 == xioctl (fd, VIDIOC_QUERYBUF, &buf))
return errnoexit ("VIDIOC_QUERYBUF");
buffers[n_buffers].length = buf.length;
buffers[n_buffers].start =
mmap (NULL ,
buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, buf.m.offset);
//mmap系统调用的最终目的是将,设备或文件映射到用户进程的虚拟地址空间,实现用户进程对文件的直接读写
if (MAP_FAILED == buffers[n_buffers].start) //如果映射失败
return errnoexit ("mmap");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
这一步中我们还使用了一个非常重要的结构体v4l2_buffer,关于此结构体的介绍可以查看官方文档。现在我们的初始化工作,到这里也就基本结束了。下一步将开始捕获视频流数据。
三、捕获视频流
捕获视频流主要分两个步骤进行,第一步是将我们在内核空间申请的缓冲帧逐一放入接收视频流数据的队列当中,采用的命令是:VIDIOC_QBUF,第二步是开启捕获视频数据,采用的命令是:VIDIOC_STREAMON,一般在写代码的时候,会将这两部分写在一起。
unsigned int i;
enum v4l2_buf_type type;
//只有在捕获视频流之前,应该要将buf放入队列,这样只要队列里面有buf,设备才将数据放入到buf中,其中将buf放入队列的函数是VIDIOC_QBUF
for (i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf;
CLEAR (buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP; //这里选择的是mmap的形式,即内存映射方式。
//将带有编号的缓冲帧放入接收视频数据队列。
if (-1 == xioctl (fd, VIDIOC_QBUF, &buf))
return errnoexit ("VIDIOC_QBUF");
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
//开启捕获视频数据
if (-1 == xioctl (fd, VIDIOC_STREAMON, &type))
return errnoexit ("VIDIOC_STREAMON");
return SUCCESS_LOCAL;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
四、读取缓冲帧数据
经过上面一步,这个时候应该视频流数据被逐一捕获并放入在接收数据队列的缓冲帧当中了,现在我们的主要工作就是要从接收队列的缓冲帧当中一帧帧读取出数据,然后将读取的数据进行处理。当然读取完了之后,我们还要重新将缓冲帧放入接收数据的队列当中。读取数据使用的命令是:VIDIOC_DQBUF,将缓冲帧放入接收数据队列的还是我们上一步使用的命令:VIDIOC_QBUF。
struct v4l2_buffer buf;
unsigned int i;
//读取缓存,即出队列
CLEAR (buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (-1 == xioctl (fd, VIDIOC_DQBUF, &buf)) {
switch (errno) {
case EAGAIN:
return 0;
case EIO:
default:
return errnoexit ("VIDIOC_DQBUF");
}
}
//一个条件判断语句,如果判断不成功,则结束程序。此时的n_buffers已经是4了
assert (buf.index < n_buffers);
//将读出的缓存进行处理,将数据起始地址作为参数,传递给处理函数
processimage (buffers[buf.index].start);
//重新放入缓存队列,这一步是必须的,因为有时候你并不知道是否是属于输出设备还是输入设备
if (-1 == xioctl (fd, VIDIOC_QBUF, &buf))
return errnoexit ("VIDIOC_QBUF");
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
整个视频流数据的接收过程可以用下面的图表示
程序中的processimage函数是用于处理视频流数据的。由于我们在基于V4L2驱动程序的USB摄像头Android(JNI)的编写(一)中的设置采集视频帧的格式、宽、高、大小这一步中,将采集的视频流数据的储存格式设置为YUYV格式,因此在我们的processimage函数中将对此进行处理。