V4l2 Capture Sequence

来源:互联网 发布:淘宝店铺页头怎么改 编辑:程序博客网 时间:2024/05/23 10:55


v4l2操作实际上就是open()设备,close()设备,以及中间过程的ioctl()操作。对于ioctl的调用,要注意对errno的判断,如果调用被其他信号中断,即errno等于EINTR的时候,要重新调用。

Videocapture device的实际功能就是采集视频信号,并将数字化的图像保存在memory中,现在几乎上所有的相关设备都能采集25/30/s。在下面的讨论中,我只列举出一些和camera密切相关的一些属性和方法。

1open_device

打开设备一般都是使用open()打开/dev下的video设备文件,比如说/dev/video1,打开之前首先要对相应的设备文件进行检查,比如说使用stat()获得文件属性,并判断是否为字符设备文件。

驱动通过主设备号810255之间的次设备号来注册devicenote系统管理员通过设备的主次设备号在/dev目录下创建相应的字符设备文件。应用程序不能通过设备的主次设备号来打开设备,而必须通过适当的devicename,即/dev目录下的设备文件来打开设备。

v4l2支持一个设备文件可以被多次打开,却只允许其中一个应用程序与设备进行数据交换,其他应用程序只能用来设定一些设备参数,对设备进行一些控制。

//======相关spechttp://v4l2spec.bytesex.org/spec/c174.htm#OPEN

2init_device

对设备进行初始化是一个很复杂的过程,其中要进行一系列参数的协商,其中重要的包括v4l2_capability,v4l2_cropcap, v4l2_format等等。

首先使用VIDIOC_QUERYCAP命令来获得当前设备的各个属性,查看设备对各项功能的支持程度:

intioctl(int fd, int request, struct v4l2_capability *argp);

所有的v4l2驱动都必须支持VIDIOC_QUERYCAP,而且在打开设备以后,这个ioctl必须是被首先调用的。

v4l2_capability的各项参数可以查API,其中比较重要的是下面的成员变量:

_u32 capabilities

这个32位无符号整型定义了当前设备对一些关键属性的支持:

V4L2_CAP_VIDEO_CAPTURE 0x00000001

//这个设备支持videocapture的接口,即这个设备具备videocapture的功能

V4L2_CAP_VIDEO_OUTPUT0x00000002

//这个设备支持videooutput的接口,即这个设备具备videooutput的功能

V4L2_CAP_VIDEO_OVERLAY0x00000004

//这个设备支持videooverlay的接口,即这个设备具备videooverlay的功能,在这个功能下会将采集到的imag方在视频设备的meomory中保存,并直接在屏幕上显示,而不需要经过其他的处理。

V4L2_CAP_VIDEO_OUTPUT_OVERLAY0x00000200

//这个设备支持videooutput overlay(又名On-ScreenDisplay),这是一个实验性的功能,spec说明他在将来可能会改变,如果打开这个功能必须将videooverlay功能给关闭,反之亦然

V4L2_CAP_READWRITE0x01000000

//这个设备是否支持read()write()I/O操作函数

V4L2_CAP_STREAMING0x04000000

//这个设备是否支持streamingI/O操作函数

在实际操作过程中,可以将取得的capabilites与这些宏进行与远算来判断设备是否支持相应的功能。

除了VIDIOC_QUERYCAP之外,设备其他属性的获得可以通过其他的命令,比如说VIDIOC_ENUMINPUTVIDIOC_ENUMOUTPUT可以枚举出设备的输入输出物理连接。

//==============相关spechttp://v4l2spec.bytesex.org/spec/x282.htm

 

获得devicecapability以后,可以根据应用程序的功能要求对设备参数进行一系列的设置,这些参数又分为两部分,一个是usercontrl,还一个是extendedcontrol,下面先来讲对usercontrl的一些参数进行设置。

Usercontrol参数包含一个ID,以及相应的Type,下面对各个type进行简单的列举:

ID Type

V4L2_CID_BASE

//第一个预定义的ID,实际等于V4L2_CID_BRIGHTNESS,因为V4L2_CID_BRIGHTNESS是第一个预定义的ID

V4L2_CID_USER_BASE

//实际上等同于V4L2_CID_BASE

V4L2_CID_BRIGHTNESS integer

//图片的亮度,或者说黑色位准

V4L2_CID_AUTO_WHITE_BALANCE boolean

//camera的自动白平衡

V4L2_CID_EXPOSURE integer

//camera的爆光时间

V4L2_CID_LASTP1

//最后一个预定义的ID,实际等于上一个ID1

V4L2_CID_PRIVATE_BASE

//第一个driver定义的一般controlID

可以通过VIDIOC_QUERYCTRLVIDIOC_QUERYMENUioctls来枚举出有效的controlID,及其属性,比如说ID值,类型,是否有效,是否可修改,最大值,最小值,步长等等,主要的数据结构是v4l2_queryctrlv4l2_querymenu,他们的结构可以参考spec。另外可以通过V4L2_CID_BASEV4L2_CID_LASTP1可以枚举出所有的预定义controlID,可以通过V4L2_CID_PRIVATE_BASE来枚举出所有的驱动定义的controlIDMenu实际上是同一个ID可能具有多个选项的目录。

intioctl(int fd, int request,structv4l2_queryctrl*argp);

intioctl(int fd, int request, struct v4l2_querymenu *argp);

获得usercontrol ID以后,可以对其中可以修改的ID按照应用程序的要求进行修改VIDIOC_G_CTRL,VIDIOC_S_CTRL

intioctl(int fd, int request, struct v4l2_control *argp);

v4l2_control的结构比较简单,就是相应的ID及其value

//==========相关spechttp://v4l2spec.bytesex.org/spec/x542.htm

 

除了usercontrol之外还有一个就是扩展控制,扩展控制可以同时原子的对多个ID进行control,相关命令是三个:VIDIOC_G_EXT_CTRLS,VIDIOC_S_EXT_CTRLSVIDIOC_TRY_EXT_CTRLS

intioctl(int fd, int request, struct v4l2_ext_controls *argp);

其中最重要的是v4l2_ext_controls这个数据结构,它包含几个内容:

__u32 ctrl_class

//现在spec中只定义了两种类型的classV4L2_CTRL_CLASS_USERV4L2_CTRL_CLASS_MPEG

__u32 count

//ctrl数组中的control,即v4l2_ext_control的个数

structv4l2_ext_control * controls

//control数组,v4l2_ext_control包含要设定的ID,以及value

应用程序可以使用V4L2_CTRL_FLAG_NEXT_CTRL来对扩展control进行枚举,V4L2_CTRL_FLAG_NEXT_CTRL返回下一个ID更高的controlID

structv4l2_queryctrl qctrl;

qctrl.id =V4L2_CTRL_FLAG_NEXT_CTRL;

while (0== ioctl (fd, VIDIOC_QUERYCTRL, &qctrl)) {

/*... */

qctrl.id|= V4L2_CTRL_FLAG_NEXT_CTRL;

}

要枚举指定的controlclass中的control可以使用下面的方法:

qctrl.id =V4L2_CTRL_CLASS_MPEG | V4L2_CTRL_FLAG_NEXT_CTRL;

while (0== ioctl (fd, VIDIOC_QUERYCTRL, &qctrl)) {

if(V4L2_CTRL_ID2CLASS (qctrl.id) != V4L2_CTRL_CLASS_MPEG)

break;

/*... */

qctrl.id|= V4L2_CTRL_FLAG_NEXT_CTRL;

}

当然前提是驱动必须提供对V4L2_CTRL_FLAG_NEXT_CTRL的支持。

应用程序可以为创建一个控制面板,其中包含一系列控制,每个controlclass用一个V4L2_CTRL_TYPE_CTRL_CLASS类型开始,当使用VIDIOC_QUERYCTRL的时候将返回这个controlclassname,下面我们来看cameracontrol class的一些control

ID Type

V4L2_CID_CAMERA_CLASS class

//cameraclass的描述符,当调用VIDIOC_QUERYCTRL的时候将返回一个对这个class的描述

V4L2_CID_EXPOSURE_AUTO integer

//自动爆光

V4L2_CID_FOCUS_AUTO boolean

//自动对焦

//==============相关spechttp://v4l2spec.bytesex.org/spec/x802.htm

 

对各种控制参数进行设置以后,下面要进行的就是要获得设备对ImageCroppingScaling的支持,即对图像的取景范围以及图片的比例缩放的支持。ImageCrop的功能通俗一点讲就是他对camera镜头能捕捉的图像,截取一个范围来保存,而这个要截取的范围就是最终保存下来的图像。

对于一个视频捕捉或者视频直接播放的设备来说,源是视频信号,而croppingioctl决定视频信号的哪部分被采样,目标则是应用程序或者屏幕上读到的图片。对于视频输出设备来说,输入是应用程序传入的图片,而输出则是视频流,croppingioctl此时则决定图片的哪部分会被插入视频信号,所有的视频捕捉和视频输出设备都必须支持VIDIOC_CROPCAPioctl:

Intioctl(int fd, int request, struct v4l2_cropcap *argp)

其中数据结构v4l2_cropcap的几个要重要的成员变量是下面这些:

enumv4l2_buf_type type

//数据流的类型,在VIDIOC_CROPCAP这个控制中只有V4L2_BUF_TYPE_CAPTURE,V4L2_BUF_TYPE_OUTPUT,V4L2_BUF_TYPE_OVERLAY以及驱动定义的一些一般类型V4L2_BUF_TYPE_PRIVATE是有用的

structv4l2_rect bounds

//这是camera的镜头能捕捉到的窗口大小的局限,在应用程序设置窗口参数的时候要注意,不能超过这个长宽限制

structv4l2_rect defrect

//定义了默认的窗口大小,包括起点的位置以及长宽的大小,大小以像素为单位

structv4l2_fract pixelaspect

//定义了图片的宽高比

应用程序可以使用VIDIOC_G_CROPVIDIOC_S_CROP来获得对这些窗口参数并对其进行设置,也就是所谓的ScalingAdjustments,因为硬件可能在这些窗口参数设置上具有很多限制,当需要对窗口参数进行设置的时候,驱动会按照自身的规律在应用程序要求和设备限制上决定一个平衡值,一般应用程序应该先使用VIDIOC_CROPCAP来获得硬件限制,并使设定的参数在bound范围以内:

intioctl(int fd, int request, struct v4l2_crop *argp);

intioctl(int fd, int request, const struct v4l2_crop *argp);

//====相关spechttp://v4l2spec.bytesex.org/spec/x1904.htm

设置好取景窗口参数以后,下面要进行的设置就是对图形格式的协商,这个DataFormat的协商通过VIDIOC_G_FMTVIDIOC_S_FMT来实现。另外VIDIOC_TRY_FMT的功能等同与VIDIOC_S_FMT,唯一的不同就是他不会改变驱动的状态,它在任何时候都可以被调用,主要用来获得硬件的限制,从而对参数进行协商。如果驱动需要与应用程序交换数据,则必须支持VIDIOC_G_FMTVIDIOC_S_FMTVIDIOC_TRY_FMT是可选的,但是是强烈推荐实现的。

Intioctl(int fd, int requeset, struct v4l2_format *argp) ;

前面讲过,虽然一个设备文件可以支持多打开,但是只允许一个能与驱动进行数据交换,因此在设备的初始化过程中对VIDIOC_S_FMTioctl的调用是一个转折点,第一个调用VIDIOC_S_FMTioctl的文件描述符会打开一个逻辑的流,如果此时其他的文件描述符对设备进行的操作有可能破坏这个流的时候是会被禁止的,比如说如果另外一个应用程序想修改videostandard,只有对流拥有所有权的文件描述符才能修改这方面的属性。再比如当overlay已经开始的时候,videocapture就会被限制在和overlay相同的croppingimagesize

一般来说只允许同一个文件描述符拥有一个逻辑流,唯一的例外是videocapturevideooverlay可以使用同一个文件描述符。

下面来看看v4l2_format这个数据结构,它包含几个重要内容:

enum v4l2_buf_type type

//buf的类型,比如说V4L2_BUF_TYPE_VIDEO_CAPTURE

union fmt

struct v4l2_pix_format

//used for video capture and output

struct v4l2_window

//used for video overlay

…………

其中最重要的是union中的两个结构体,v4l2_windowoverlayinterface的内容,将在overlay中再讨论,先看一下v4l2_pix_format的结构:

__u32 width

__u32 height

//分别是image的宽度和高度,以像素为单位,应用程序可以设置这些参数,驱动会返回一个最靠近这些参数的值,为什么是最靠近的值呢,因为图像格式以及硬件限制的原因,可能应用程序要求的值无法得到满足。[在这里普及一个基础知识YUV格式有两种存储方式,一种就是将其3个分量存在同一个数组中,然后几个像素组成一个宏块,这种方式叫packed;另外一种就是3个分量分别存放在不同的数组中,这种方式叫做planar。]

__u32 pixelformat

//这就是图像格式了,可以是RGB,也可以是YUV,还可以是压缩格式MPEG或者JPEG,这个值是通过一个4字母宏来计算出来的:#definev4l2_fourcc(a,b,c,d)(((__u32)(a)<<0) | ((__u32)(b)<<8)|((__u32)(c)<<16) | ((__u32)(a)<<24)),具体格式的标准宏可以参照spec

enum v4l2_field field

//这个定义了视频信号的场的顺序,比如视频信号可能是顺序扫描的,也可能是隔行扫描的。一般将场分为top场和bottom场,一个videocamera不会在一个时间内暴光一个整帧,而是将其分成场分别传输。所有的videocaptureoutput装置都必须指定其场的传输顺序,即是top场在前还是bottom场在时间上和空间上的顺序。具体的可以从参考spec关于FieldOrder的描述,一般采用的是V4L2_FIILED_INTERLACED,在这个模式下image包含交叉存取的帧,场的顺序由当前的视频标准来决定。

__u32 bytesperline

//即每行像素所占的byte数,应用程序和驱动都可以设置这个参数,但驱动可以忽略应用程序的参数,而返回一个硬件要求的参数,应用程序可以设置这个参数为0来让驱动返回一个默认值。Image在内存中还是按照每行像素这样来存储的,每一行像素后面都有一个衬垫来代表该行像素的结束。

__u32 sizeimage

//要保存一个完整的Image需要的buffer空间,单位是byte,由驱动来设定,是保存一个图像所需要的最大byte数,而不是图像被压缩的byte数。

如果驱动需要与应用程序交换imagedata则必须支持VIDIOC_ENUM_FMT来列出所有驱动支持的FMT格式

 

实际上,crop是对取景进行限制,而fmt则是对最终保存下来的图片属性进行设置,如果取景后的图片和要求的图像属性有冲突,就要将取景后的图片进行相应的调整,比如放大,缩小等等

看下面的例子:

Resettingthe cropping parameters

(Avideo capture device is assumed; change V4L2_BUF_TYPE_VIDEO_CAPTUREfor other devices.)

structv4l2_cropcap cropcap;

structv4l2_crop crop;

memset(&cropcap, 0, sizeof (cropcap));

cropcap.type= V4L2_BUF_TYPE_VIDEO_CAPTURE;

if(-1 == ioctl (fd, VIDIOC_CROPCAP, &cropcap)) {

perror("VIDIOC_CROPCAP");

exit(EXIT_FAILURE);

}

memset(&crop, 0, sizeof (crop));

crop.type= V4L2_BUF_TYPE_VIDEO_CAPTURE;

crop.c= cropcap.defrect;

 

/*Ignore if cropping is not supported (EINVAL). */

if(-1 == ioctl (fd, VIDIOC_S_CROP, &crop)

&&errno != EINVAL) {

perror("VIDIOC_S_CROP");

exit(EXIT_FAILURE);

}

Simpledownscaling

(Avideo capture device is assumed.)

structv4l2_cropcap cropcap;

structv4l2_format format;

reset_cropping_parameters();

/*Scale down to 1/4 size of full picture. */

memset(&format, 0, sizeof (format)); /* defaults */

format.type= V4L2_BUF_TYPE_VIDEO_CAPTURE;

format.fmt.pix.width= cropcap.defrect.width >> 1;

format.fmt.pix.height= cropcap.defrect.height >> 1;

format.fmt.pix.pixelformat= V4L2_PIX_FMT_YUYV;

if(-1 == ioctl (fd, VIDIOC_S_FMT, &format)) {

perror("VIDIOC_S_FORMAT");

exit(EXIT_FAILURE);

}

/*We could check the actual image size now, the actual scaling factor

orif the driver can scale at all. */

另外还有一个可选的选项,就是如果采用read/write模式,还可以通过设置流参数属性来优化capture的性能,在这里就不讨论了,具体的可以去参照spec

 

完成这一系列参数的初始化以后,最后一个要协商的就是I/0模式的选择:主要分为两种,一种是Read/Write,这也是打开videodevice之后默认选择的I/O方法,其他的方法如果使用必须经过协商;还有一个就是stream,其中stream中根据实现方式的不同又可以分为MemoryMappingUserPointersDriver可以决定是否支持对I/Oswitch,这不是必须的,如果driver不支持,则只有通过open/closedevice来实现I/0的切换。

首先来看Read/Write,如果VIDIOC_QUERYCAP调用返回的v4l2_capability参数中,V4L2_CAP_READWRITE被设置成真了的话,就说明支持Read/WriteI/O。这是最简单最原始的方法,它需要进行数据的拷贝(而不是像memorymap那样只需要进行指针的交换),而且不会交换元数据(比如说帧计数器和时间戳之类的可用于识别帧丢失和进行帧同步),虽然它是最原始的方法,但因为其简单,所以对于简单的应用程序比如只需要capture静态图像是很有用的

如果使用Read/Write方法支持的话,必须同时支持另外两个函数select()poll(),这两个函数用来进行I/0的多路复用。

对于streaming它有两种方式,driver对两种方式的支持要使用VIDIOC_REQBUFS来确定:

intioctl(int fd, int request, struct v4l2_requestbuffers *argp);

对于memorymapped方式,Memorymapped buffers是通过VIDIOC_REQBUFSdevicememory中申请的,而且必须在map进应用程序虚拟地址空间之前就申请好。而对于UserpointersUserbuffers是在应用程序自己开辟的,只是通过VIDIOC_REQBUFS将驱动转化到userpointerI/O模式下。这两种方式都不会拷贝数据,而只是buffer指针的交互。

首先来看一下v4l2_requestbuffers这个数据结构:

__u32 count

//要申请的buffer的数量,只有当memory被设置成V4L2_MEMORY_MMAP的时候才会设置这个参数

enum v4l2_buf_type type

enum v4l2_memory memory

//要么是V4L2_MEMORY_MMAP,要么是V4L2_MEMORY_USERPTR

对于memorymapped模式,要在devicememory下申请buffer,应用程序必须初始化上面的3个参数,驱动最后返回的buffer的个数可能等于count,也可能少于或者多于count,少于可能是因为内存不足,多于则可能是驱动为更好地完成相应功能增加的buffer。如果driver不支持memorymapped调用这个ioctl就会返回EINVAL

因为memorymap模式下分配的是实实在在的物理内存,不是虚拟内存,所以使用完以后一定要使用munmap()释放。

应用程序可以重新调用VIDICO_REQBUFS来改变buffer的个数,但前提是必须先释放已经mappedbuffer,可以先munmap,然后设置参数count0来释放所有的buffer

对于Userpointer I/O,应用程序只需设置上面的typememory类型就可以了。

申请好buffer后在进行memorymapped之前,首先要使用VIDIOC_QUERYBUF来获得分配的buffer信息,以传给函数mmap()来进行map

intioctl(int fd, int request, struct v4l2_buffer *argp);

VIDIOC_QUERYBUFmemorymapped这种模式下使用的方法,在Userpointer模式下不需要使用这个函数,在调用之前应用程序需要设定v4l2_buffer中的两个参数,一个是buffer类型,另外一个是indexnumber(有效值从0到申请的buffer数目减1),调用这个ioctl会将相应buffer中的flagV4L2_BUF_FLAG_MAPPED,V4L2_BUF_FLAG_QUEUEDV4L2_BUF_FLAG_DONE设置为有效。下面我们来仔细看看v4l2_buffer这个数据结构:

__u32 index

//应用程序来设定,仅仅用来申明是哪个buffer

enum v4l2_buf_type type

__u32 bytesused

//buffer中已经使用的byte数,如果是inputstreamdriver来设定,相反则由应用程序来设定

__u32 flags

//定义了buffer的一些标志位,来表明这个buffer处在哪个队列,比如输入队列或者输出队列(V4L2_BUF_FLAG_QUEUED V4L2_BUF_FLAG_DONE),是否关键帧等等,具体可以参照spec

enum v4l2_memory memory

//V4L2_MEOMORY_MMAPV4L2_MEMORY_USERPTRV4L2_MEMORY_OVERLAY

union m

__u32 offset

//memory类型是V4L2_MEOMORY_MMAP的时候,主要用来表明bufferdevicemomory中相对起始位置的偏移,主要用在mmap()参数中,对应用程序没有左右

unsigned long userptr

//memory类型是V4L2_MEMORY_USERPTR的时候,这是一个指向虚拟内存中buffer的指针,由应用程序来设定。

__u32 length

//buffersize

driver内部管理着两个bufferqueues,一个输入队列,一个输出队列。对于capturedevice来说,当输入队列中的buffer被塞满数据以后会自动变为输出队列,等待调用VIDIOC_DQBUF将数据进行处理以后重新调用VIDIOC_QBUFbuffer重新放进输入队列;对于outputdevice来说buffer被显示以后自动变为输出队列。

刚初始化的所有map过的buffer开始都处于dequeced的状态,由driver来管理对应用程序是不可访问的。对于capture应用程序来说,首先是通过VIDIOC_QBUF将所有map过的buffer加入队列,然后通过VIDIOC_STREAMON开始capture,并进入readloop,在这里应用程序会等待直到有一个buffer被填满可以从队列中dequeued,当数据使用完后再enqueue进输入队列;对于output应用程序来说,首先应用程序会buffer装满数据然后enqueued,当足够的buffer进入队列以后就调用VIDIOC_STREAMON将数据输出。

有两种方法来阻塞应用程序的执行,直到有buffer能被dequeued,默认的是当调用VIDIOC_DQBUF的时候会被阻塞,直到有数据在outgoingqueue,但是如果打开设备文件的时候使用了O_NONBLOCK,则当调用VIDIOC_DQBUF而又没有数据可读的时候就会立即返回。另外一种方法是调用selectpoll来对文件描述符进行监听是否有数据可读。

VIDIOC_STREAMONVIDIOC_STREAMOFF两个ioctl用来开始和停止capturing或者output,而且VIDIOC_STREAMOFF会删除输入和输出队列中的所有buffer

因此drvier如果要实现memorymapping I/O必须支持VIDIOC_REQBUFS,VIDIOC_QUERYBUF, VIDIOC_QBUF, VIDIOC_DQBUF, VIDIOC_STREAMONVIDIOC_STREAMOFFioctl, the mmap(), munmap(), select()poll()函数

 

UserPointers是一种综合了Read/Writememorymappded优势的I/O方法,buffer是由应用程序自己申请的,可以是在虚拟内存或者共享内存中。在captureoutput方面基本来说和memorymapped方式是相同的,在这里只提一下它申请内存的方式。

Userpointer方式下,申请的内存也memorypage size为单位对齐,而且buffersize也有一定限制,例示代码中是这样计算buffersize的,暂时还不知道这样分配buffersize的依据是什么,先简单地这样用就好了:

page_size= getpagesize ();

buffer_size= (buffer_size + page_size - 1) & ~(page_size – 1);

buffers[n_buffers].start= memalign (/* boundary */ page_size,

buffer_size);

 

3start_capturing

经过上面的一系列的数据协商已经buffer的分配以后就可以调用VIDIOC_QBUFbuffer全部加入输入队列中,并调用VIDIOC_STREAM0N开始捕获数据了:

intioctl(int fd, int request, struct v4l2_buffer *argp);

//VIDIOC_QBUF VIDIOC_DQBUF

intioctl(int fd, int request, const int *argp);

//VIDIOC_STREAM0N VIDIOC_STREAMOFFint参数是buffer类型)

 

4mainloop

开始捕获数据以后就会进入一个主循环,可以使用select或者poll来监听文件描述符的状态,一旦有数据可读,就调用函数来读取数据。

 

5read_frame

读取数据根据I/O方式的不同而不同:

Read/Write方式直接从文件描述符中读一个帧大小的数据;

Memorymapped方式下先从输出队列中dequeued一个buffer,然后对帧数据进行处理,处理完成以后再放入输入队列。

Userpointer方式下也是首先从输出队列中dequeued一个buffer,然后对这个buffer进行判断,看是否是应用程序开始申请的buffer,然后再对这个buffer进行处理,最后放入输入队列。

 

6stop_capturing / uninit_device / close device

最后就是捕捉以及资源释放并关闭device