从深圳回来已经20多天了,除了完善毕业设计程序和论文,其他时间都去玩游戏了。真的是最后的一段时间能够无忧无虑在校园里挥霍自己的青春了。今天完成的答辩,比想象的要简单,一直以来想把我现在的这个流媒体的东西用文字记录下来,但是都去玩了。但是今天开始还是把这些东西都记录下来。其实整个项目最开始接触的是socket编程,用socket写一个很简单的机遇POP3协议的邮件发送程序都觉得沾沾自喜。现在看来但是确实还是很幼稚的。。。
其实V4L2就是LINUX下的一套API,我刚刚开始接触的时候觉得好难,完全就TMD看不懂啊。。。反正就是各种不靠谱,其实现在看来这些东西不难,其实很简单。只是当时没有决心去做而已。其实大多数的初学者都有我这样的想法,看着这些不熟悉的东西都会很烦躁,沉不住气不想去看。但是事实是多看看多GOOGLE查查基本就能理解了。下面是API的代码解析:
1)打开一个视频设备:
fd = open("/dev/video0", O_RDWR/*|O_NONBLOCK*/, 0);
要从摄像头中回去到图像首先当然要打开一个摄像头,在LINUX中对摄像头的操作是对相应的设备文件进行操作实现的。在LINUX的根文件系统中/dev目录有很多设备文件,其中摄像头对应的是viode0,使用open函数打开,O_RDWR表示读写,O_NONBLOCK表示非阻塞,屏蔽掉表示阻塞方式(使用非阻塞方式调用视频设备,即使尚未捕获到视频信息,驱动依旧会把缓存里面的东西返回给应用程序,如果使用阻塞方式调用视频设备,正好相反,在没捕获到视频设备之前,程序会阻塞在OPEN函数所在的位置)open函数返回一个文件描述符fd,以后的程序中就是fd进行操作。
2)ioctl()函数
具体的ioctl()函数是啥玩意儿我也不知道,百度百科里面说是一种获得设备信息和向设备发送控制参数的手段。那么在我们的代码中就是通过这个函数来和摄像头进行“交互”。比如我要知道这个摄像头是什么型号,有多大的视野,我要获取多大的图像。。。等等,具体设置在后文中会有详细解释。ioctl()函数有3个参数,第一个是前面提到的文件描述符fd,就是我们要操作摄像头。第二个参数是命令,第三个参数是一个结构体,根据第二个参数的不同使用不同的结构体。
a)在这个程序中ioclt函数用到的命令:
VIDIOC_QUERYCAP //查询设备功能信息
VIDIOC_CROPCAP //查询驱动的修剪能力
VIDIOC_S_CROP //设置视频信号的矩形边框
VIDIOC_S_FMT //设置当前驱动的频捕获格式
VIDIOC_REQBUFS //分配内存
VIDIOC_QUERYBUF //把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址
VIDIOC_QBUF //把数据从缓存中读取出来
VIDIOC_STREAMON //开始视频流的获取
VIDIOC_STREAMOFF //结束视频流的获取
VIDIOC_DQBUF //把数据放回缓存队列
此处可戳这里:http://www.cnblogs.com/xmphoenix/archive/2011/08/20/2147064.html(反正这个V4L2比我写得好)
b)程序中用到的结构体
struct v4l2_capability cap //返回当前视频设备所支持的功能;
struct v4l2_cropcap cropcap //设置设备的捕捉能力参数;
struct v4l2_crop crop //设置窗口的捕捉能力参数;
struct v4l2_format fmt //设置帧的格式,比如宽度,高度等;
struct v4l2_requestbuffers req //向驱动申请帧缓冲的请求,里面包含申请的个数;
struct v4l2_buffer buf //代表驱动中的一帧;
(以上所有的命令和结构体都是在程序中出现的,但是绝对不是完整的,还有很多没有没有一一列举出来)
3)V4L2操作流程
a.打开设备文件:
fd = open("/dev/video0", O_RDWR/*|O_NONBLOCK*/, 0);
使用open函数打开"/dev/video0"这个设备文件可以分为阻塞方式和非阻塞方式,如果使用非阻塞方式调用视频设备,即使尚未捕获到视频信息,驱动依旧会把缓存(DQBUFF)里面的东西返回给应用程序,如果使用阻塞方式调用视频设备,正好相反,在没捕获到视频设备之前,程序会阻塞在OPEN函数所在的位置。
b.查询视频设备支持的功能:
struct v4l2_capability cap; ioctl(fd, VIDIOC_QUERYCAP, &cap);
利用ioctl()函数来对打开的设备文件而获得的句柄fd进行读写,第二个参数VIDIOC_QUERYCAP是查询设备属性命令,获取到的设备信息保存到 struct v4l2_capability声明的结构体中,通过判断结构体中capabilities成员的值来判断设备文件是不是支持视频捕捉(V4L2_CAP_VIDEO_CAPTURE)等操作。
c.设置设备和窗口参数:
struct v4l2_cropcap cropcap; struct v4l2_crop crop; ioctl(fd, VIDIOC_CROPCAP, &cropcap); ioctl(fd, VIDIOC_S_CROP, &crop);
同样利用ioctl()函数,VIDIOC_CROPCAP用于查询设备的窗口属性,包括最大窗口左上角坐标和宽高、默认窗口左上角坐标和宽高等属性,根据查询到的值再使用VIDIOC_S_CROP命令和struct v4l2_crop声明的结构体来设置我们的窗口。
d.设置获取帧的格式:
struct v4l2_format fmt; ioctl(fd, VIDIOC_S_FMT, &fmt);
先是为声明的结构体变量fmt即帧的属性赋值,包括帧类型、宽、高、帧的数据存储类型(YUV\RGB)等,然后是ioctl()函数和对应的命令VIDIOC_S_FMT进行设置。
e.向驱动申请帧缓冲和地址映射:
struct v4l2_requestbuffers req; struct v4l2_buffer buf; buffers = calloc(req.count, sizeof(*buffers)); ioctl(fd, VIDIOC_REQBUFS, &req); ioctl(fd, VIDIOC_QUERYBUF, &buf); mmap(NULL, // start anywhere buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); ioctl(fd, VIDIOC_QBUF, &buf);
首先是用VIDIOC_REQBUFS命令向驱动申请帧(一般不超过5个),在结构体struct v4l2_requestbuffers有我们需要设置的参数,然后在我们的计算机内存中定义帧空间,用VIDIOC_QUERYBUF命令查询设备中的帧信息,并把查询到的信息保存到struct v4l2_buffer定义的结构体中,使用mmap()函数将设备中的帧的缓存地址一一映射成计算机内存中的绝对地址,然后使用VIDIOC_QBUF命令把这些帧放到缓存中,这样我们就可以方便对这些帧进行读取。
f.采集和处理数据:
enum v4l2_buf_type type; struct v4l2_buffer queue_buf; ioctl(fd, VIDIOC_STREAMON, &type); ioctl(fd, VIDIOC_DQBUF, &queue_buf); ioctl(fd, VIDIOC_QBUF, &queue_buf);
V4L2有一个数据缓存,存放了前面已经申请数量的缓存数据。数据缓存采用FIFO的方式,当应用程序调用缓存数据时,缓存队列将最先采集到的视频数据送出,并重新回到缓存队列中等待接收数据。这个过程中需要用到两个命令VIDIOC_DQBUF和VIDIOC_QBUF来送去和放入缓存。当然在这之前我们需要使用VIDIOC_STREAMON来打开视频流。
全部代码如下,我现在发现我封装得特别傻逼,但是还是写下来,有时间再改改,文件是video.c
#include "video.h" static struct buffer * buffers = NULL; static unsigned int n_buffers = 0; extern findex; extern fd; int open_device() { fd = open("/dev/video0", O_RDWR/*|O_NONBLOCK*/, 0); if(-1 == fd) { printf("open error\n"); return -1; } return 0; } int close_device() { if(-1 == close(fd)) { printf("close error\n"); return -1; } return 0; } int init_device() { struct v4l2_capability cap; struct v4l2_cropcap cropcap; struct v4l2_crop crop; struct v4l2_format fmt; if(-1 == ioctl(fd, VIDIOC_QUERYCAP, &cap)) { printf("querycap error\n"); return -1; } printf("**************************************************\n"); printf("Driver Name:%s\nCard Name:%s\nBus info:%s\nDriver Version:%u.%u.%u\nCapabilities:%u \n",cap.driver,cap.card,cap.bus_info,(cap.version>>16)&0xff, (cap.version>>8)&0xff,cap.version&0xff,cap.capabilities); printf("**************************************************\n"); if(!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { printf("Capture error\n"); return -1; } if(!(cap.capabilities & V4L2_CAP_STREAMING)) { printf("Streaming error\n"); return -1; } CLEAR(cropcap); cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if(0 == ioctl(fd, VIDIOC_CROPCAP, &cropcap)) { CLEAR(crop); crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; crop.c = cropcap.defrect; if(-1 == ioctl(fd, VIDIOC_S_CROP, &crop)) { switch (errno) { case EINVAL: printf("not support crop\n"); } printf("can't set VIDIOC_S_CROP\n"); //return -1; } } CLEAR(fmt); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 640; fmt.fmt.pix.height = 480; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; if(-1 == ioctl(fd, VIDIOC_S_FMT, &fmt)) { printf("VIDIOC_S_FMT error\n"); return -1; } return 0; } int init_mmap() { struct v4l2_requestbuffers req; CLEAR(req); req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if(-1 == ioctl(fd, VIDIOC_REQBUFS, &req)) { printf("VIDIOC_REQBUFS\n"); return -1; } if(req.count < 2) { printf("Insufficient buffer memory\n"); return -1; } buffers = calloc(req.count, sizeof(*buffers)); if(!buffers) { printf("out of memory\n"); return -1; } for(n_buffers = 0; n_buffers < req.count; ++n_buffers) { struct v4l2_buffer buf; CLEAR(buf); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = n_buffers; if(-1 == ioctl(fd, VIDIOC_QUERYBUF, &buf)) { printf("VIDIOC_QUERYBUF\n"); return -1; } buffers[n_buffers].length = buf.length; buffers[n_buffers].start = mmap(NULL, // start anywhere buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if(MAP_FAILED == buffers[n_buffers].start) { printf("mmap error\n"); return -1; } } return 0; } int start_capturing() { unsigned int i; 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; buf.index = i; // fprintf(stderr, "n_buffers: %d\n", i); if(-1 == ioctl(fd, VIDIOC_QBUF, &buf)) { printf("VIDIOC_QBUF error\n"); return -1; } } enum v4l2_buf_type type; type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if(-1 == ioctl(fd, VIDIOC_STREAMON, &type)) { printf("VIDIOC_STREAMON error\n"); return -1; } return 0; } int stop_capturing() { enum v4l2_buf_type type; type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if(-1 == ioctl(fd, VIDIOC_STREAMOFF, &type)) { printf(" VIDIOC_STREAMOFF error\n"); return -1; } return 0; } int uninit_mmap() { unsigned int i; for(i = 0; i < n_buffers; ++i) { if(-1 == munmap(buffers[i].start, buffers[i].length)) { printf("munmap error\n"); return -1; } } free(buffers); return 0; } int get_frame(void **frame_buf, size_t* len) { struct v4l2_buffer queue_buf; CLEAR(queue_buf); queue_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; queue_buf.memory = V4L2_MEMORY_MMAP; if(-1 == ioctl(fd, VIDIOC_DQBUF, &queue_buf)) { printf("VIDIOC_DQBUF error\n"); return -1; } printf("run to here\n"); *frame_buf = buffers[queue_buf.index].start; *len = buffers[queue_buf.index].length; findex = queue_buf.index; return 0; } int unget_frame() { if(findex != -1) { struct v4l2_buffer queue_buf; CLEAR(queue_buf); queue_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; queue_buf.memory = V4L2_MEMORY_MMAP; queue_buf.index = findex; if(-1 == ioctl(fd, VIDIOC_QBUF, &queue_buf)) { printf("VIDIOC_QBUF error\n"); return -1; } return 0; } return -1; }
这些代码我忘记当初是在那抄的了!!!!
总结:get_frame的第一个参数是一个双重指针是由于C语言的值传递,在这个函数中获取到了每一帧的开始地址和每一帧的大小,那么就是获取到一帧图像啦!