文章目录
一、什么是V4L2二、V4l2的基本流程三、v4l2框架整理四、V4l2的buf管理五、V4L2的整体流程参考笔记:
https://blog.csdn.net/yhg20090519/article/details/78366179https://blog.csdn.net/yhg20090519/article/details/78496936https://www.cnblogs.com/vedic/p/10763838.htmlhttps://blog.csdn.net/kuangzuxiaoN/article/details/77048509
一、什么是V4L2
相机驱动层位于HAL Moudle与硬件层之间,借助linux内核驱动框架,以文件节点的方式暴露接口给用户空间,让HAL Module通过标准的文件访问接口,从而能够将请求顺利地下发到内核中,而在内核中,为了更好的支持视频流的操作,早先提出了v4l视频处理框架,但是由于操作复杂,并且代码无法进行较好的重构,难以维护等原因,之后便衍生出了v4l2框架。
按照v4l2标准,它将一个数据流设备抽象成一个videoX节点,从属的子设备都对应着各自的v4l2_subdev实现,并且通过media controller进行统一管理,整个流程复杂但高效,同时代码的扩展性也较高。
所以我们可以看到对应的相机节点再dev目录下
mvk_8qxp:/ # ls /dev/video*/dev/vidie0 /dev/video1 /dev/video3 /dev/video4
这也就是针对我们调试相机的时候可以看到的一些基础信息
二、V4l2的基本流程
1. 打开video设备
在需要进行视频数据流的操作之前,首先要通过标准的字符设备操作接口open方法来打开一个video设备,并且将返回的字符句柄存在本地,之后的一系列操作都是基于该句柄,而在打开的过程中,会去给每一个子设备的上电,并完成各自的一系列初始化操作。
2. 查看并设置设备
在打开设备获取其文件句柄之后,就需要查询设备的属性,该动作主要通过ioctl传入VIDIOC_QUERYCAP参数来完成,其中该系列属性通过v4l2_capability结构体来表达,除此之外,还可以通过传入VIDIOC_ENUM_FMT来枚举支持的数据格式,通过传入VIDIOC_G_FMT/VIDIOC_S_FMT来分别获取和获取当前的数据格式,通过传入VIDIOC_G_PARM/VIDIOC_S_PARM来分别获取和设置参数。
3. 申请帧缓冲区
完成设备的配置之后,便可以开始向设备申请多个用于盛装图像数据的帧缓冲区,该动作通过调用ioctl并且传入VIDIOC_REQBUFS命令来完成,最后将缓冲区通过mmap方式映射到用户空间
4. 将帧缓冲区入队
申请好帧缓冲区之后,通过调用ioctl方法传入VIDIOC_QBUF命令来将帧缓冲区加入到v4l2 框架中的缓冲区队列中,静等硬件模块将图像数据填充到缓冲区中
5. 开启数据流
将所有的缓冲区都加入队列中之后便可以调用ioctl并且传入VIDIOC_STREAMON命令,来通知整个框架开始进行数据传输,其中大致包括了通知各个子设备开始进行工作,最终将数据填充到V4L2框架中的缓冲区队列中。
6. 将帧缓冲区出队
一旦数据流开始进行流转了,我们就可以通过调用ioctl下发VIDIOC_DQBUF命令来获取帧缓冲区,并且将缓冲区的图像数据取出,进行预览、拍照或者录像的处理,处理完成之后,需要将此次缓冲区再次放入V4L2框架中的队列中等待下次的图像数据的填充。
整个采集图像数据的流程现在看来还是比较简单的,接口的控制逻辑很清晰,主要原因是为了提供给用户的接口简单而且抽象,这样方便用户进行集成开发,其中的大部分复杂的业务处理都被V4L2很好的封装了,接下来我们来详细了解下V4L2框架内部是如何表达以及如何运转的。
对应的函数流程可以用这个图来表示:
其实可以看出,V4L2的整个流程都是用ioctl完成的,关于ioctl,我们后面说。
三、v4l2框架整理
v4l2_device 充当父类,通过链表把所有注册到其下的子设备管理起来,这些设备可以是GRABBER也可以是RADIO或者VBI的。
这就是里面一些属性,有一些没有看明白名,所以这里就没有详解。
// kernel_imx/include/media/v4l2-device.hstruct v4l2_device { struct device *dev;//成功创建设备的指针 struct media_device *mdev;//创建成功的媒体设备的指针 struct list_head subdevs;//头节点,用于遍历后面的跟踪子设备 spinlock_t lock;//自旋锁,可以使驱动程序已经这个结构嵌入到一个更大的结构(这个还没看懂怎么用的) char name[V4L2_DEVICE_NAME_SIZE]; //设备名 void (*notify)(struct v4l2_subdev *sd, unsigned int notification, void *arg);//函数指针,报告一些子设备调用的回调函数 struct v4l2_ctrl_handler *ctrl_handler;//控制处理数据结构 struct v4l2_prio_state prio;//优先级状态 struct kref ref;//计数 void (*release)(struct v4l2_device *v4l2_dev);//释放v4l2设备函数指针};
v4l2_subdev结构体包含了对设备操作的ops和ctrls,这部分代码和硬件相关,需要驱动工程师根据硬件实现控制上下电、读取ID、饱和度、对比度和视频数据流打开关闭等接口函数。
这个结构体代表每一个子设备在初始化的时候都要挂载在v4l2_device上,将其统一管理。
// kernel_imx/include/media/v4l2-subdev.hstruct v4l2_subdev {#if defined(CONFIG_MEDIA_CONTROLLER) struct media_entity entity;#endif struct list_head list; struct module *owner;//模块拥有者 bool owner_v4l2_dev; u32 flags; struct v4l2_device *v4l2_dev;//指向父设备 const struct v4l2_subdev_ops *ops;//提供一些控制v4l2设备的接口 const struct v4l2_subdev_internal_ops *internal_ops;//向V4L2框架提供的接口函数 struct v4l2_ctrl_handler *ctrl_handler;//subdev控制接口 char name[V4L2_SUBDEV_NAME_SIZE];//子设备名 u32 grp_id; void *dev_priv; void *host_priv; struct video_device *devnode;//video_device 设备节点 struct device *dev; struct fwnode_handle *fwnode; struct list_head async_list; struct v4l2_async_subdev *asd; struct v4l2_async_notifier *notifier; struct v4l2_async_notifier *subdev_notifier; struct v4l2_subdev_platform_data *pdata;};
下面两个结构体是
const struct v4l2_subdev_ops *ops;//提供一些控制v4l2设备的接口 const struct v4l2_subdev_internal_ops *internal_ops;//向V4L2框架提供的接口函数
这两个属性的结构体:
struct v4l2_subdev_ops { const struct v4l2_subdev_core_ops *core;//视频设备通用的操作:初始化、加载FW、上电和RESET等 const struct v4l2_subdev_tuner_ops *tuner;//tuner特有的操作 const struct v4l2_subdev_audio_ops *audio;//audio特有的操作 const struct v4l2_subdev_video_ops *video;//视频设备的特有操作:裁剪图像、开关视频流等 const struct v4l2_subdev_vbi_ops *vbi; const struct v4l2_subdev_ir_ops *ir; const struct v4l2_subdev_sensor_ops *sensor; const struct v4l2_subdev_pad_ops *pad;};/** * struct v4l2_subdev_internal_ops - V4L2 subdev internal ops * * @registered: called when this subdev is registered. When called the v4l2_dev * field is set to the correct v4l2_device. * * @unregistered: called when this subdev is unregistered. When called the * v4l2_dev field is still set to the correct v4l2_device. * * @open: called when the subdev device node is opened by an application. * * @close: called when the subdev device node is closed. Please note that * it is possible for @close to be called after @unregistered! * * @release: called when the last user of the subdev device is gone. This * happens after the @unregistered callback and when the last open * filehandle to the v4l-subdevX device node was closed. If no device * node was created for this sub-device, then the @release callback * is called right after the @unregistered callback. * The @release callback is typically used to free the memory containing * the v4l2_subdev structure. It is almost certainly required for any * sub-device that sets the V4L2_SUBDEV_FL_HAS_DEVNODE flag. * * .. note:: * Never call this from drivers, only the v4l2 framework can call * these ops. */struct v4l2_subdev_internal_ops { /* 当subdev注册时被调用,读取IC的ID来进行识别 */ int (*registered)(struct v4l2_subdev *sd); void (*unregistered)(struct v4l2_subdev *sd); /* 当设备节点被打开时调用,通常会给设备上电和设置视频捕捉FMT */ int (*open)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh); int (*close)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh); /* 释放对应的subdev */ void (*release)(struct v4l2_subdev *sd);};
调用 video_device 结构体进行设备的注册
// kernel_imx/include/media/v4l2-dev.h struct video_device{#if defined(CONFIG_MEDIA_CONTROLLER)struct media_entity entity;struct media_intf_devnode *intf_devnode;struct media_pipeline pipe;#endifconst struct v4l2_file_operations *fops;// 设备描述符号u32 device_caps;/* sysfs */struct device dev;// v4l 设备struct cdev *cdev;// 字符设备struct v4l2_device *v4l2_dev;// v4l2_device 父设备 struct device *dev_parent;// 父设备struct v4l2_ctrl_handler *ctrl_handler;// 设备节点的控制处理结构,可能为NULLstruct vb2_queue *queue;struct v4l2_prio_state *prio;/* device info */char name[32];enum vfl_devnode_type vfl_type;enum vfl_devnode_direction vfl_dir;int minor;u16 num;unsigned long flags;int index;/* V4L2 file handles */spinlock_tfh_lock;struct list_headfh_list;int dev_debug;v4l2_std_id tvnorms;/* callbacks *//* ioctl回调函数集,提供file_operations中的ioctl调用 */void (*release)(struct video_device *vdev);// 释放函数const struct v4l2_ioctl_ops *ioctl_ops;DECLARE_BITMAP(valid_ioctls, BASE_VIDIOC_PRIVATE);struct mutex *lock;ANDROID_KABI_RESERVE(1);ANDROID_KABI_RESERVE(2);};
v4l2_fh是用来保存子设备的特有操作方法,也就是下面要分析到的v4l2_ctrl_handler,内核提供一组v4l2_fh的操作方法,通常在打开设备节点时进行v4l2_fh注册。
// kernel_imx/include/media/v4l2-fh.h/** * struct v4l2_fh - Describes a V4L2 file handler * * @list: list of file handlers * @vdev: pointer to &struct video_device * @ctrl_handler: pointer to &struct v4l2_ctrl_handler * @prio: priority of the file handler, as defined by &enum v4l2_priority * * @wait: event' s wait queue * @subscribe_lock: serialise changes to the subscribed list; guarantee that * the add and del event callbacks are orderly called * @subscribed: list of subscribed events * @available: list of events waiting to be dequeued * @navailable: number of available events at @available list * @sequence: event sequence number * * @m2m_ctx: pointer to &struct v4l2_m2m_ctx */struct v4l2_fh { struct list_head list; struct video_device *vdev; struct v4l2_ctrl_handler *ctrl_handler; enum v4l2_priority prio; /* Events */ wait_queue_head_t wait; struct mutex subscribe_lock; struct list_head subscribed; struct list_head available; unsigned int navailable; u32 sequence; struct v4l2_m2m_ctx *m2m_ctx;};/*这个头文件中还包含了如何去初始化子设备等、添加子设备、删除子设备*/void v4l2_fh_init(struct v4l2_fh *fh, struct video_device *vdev);/** * v4l2_fh_add - Add the fh to the list of file handles on a video_device. * * @fh: pointer to &struct v4l2_fh * * .. note:: * The @fh file handle must be initialised first. */void v4l2_fh_add(struct v4l2_fh *fh);/** * v4l2_fh_open - Ancillary routine that can be used as the open\(\) op * of v4l2_file_operations. * * @filp: pointer to struct file * * It allocates a v4l2_fh and inits and adds it to the &struct video_device * associated with the file pointer. */int v4l2_fh_open(struct file *filp);/** * v4l2_fh_del - Remove file handle from the list of file handles. * * @fh: pointer to &struct v4l2_fh * * On error filp->private_data will be %NULL, otherwise it will point to * the &struct v4l2_fh. * * .. note::
v4l2_ctrl_handler是用于保存子设备控制方法集的结构体,对于视频设备这些ctrls包括设置亮度、饱和度、对比度和清晰度等,用链表的方式来保存ctrls,可以通过v4l2_ctrl_new_std函数向链表添加ctrls。
其实下面的注释已经有对应的说明了
// kernel_imx/include/media/v4l2-ctrls.hstruct v4l2_ctrl *v4l2_ctrl_new_std(struct v4l2_ctrl_handler *hdl, const struct v4l2_ctrl_ops *ops, u32 id, s64 min, s64 max, u64 step, s64 def);/** * v4l2_ctrl_new_std_menu() - Allocate and initialize a new standard V4L2 * menu control. * * @hdl: The control handler. 已经初始化的v4l2_ctrl_handler的结构体 * @ops: The control ops.已经设置好的v4l2_ctrl_ops结构体 * @id: The control ID.对应的ID通过;ioctl传递的参数,也就是序列号 * @max: The control's maximum value.操作范围的最大值和最小值 * @mask: The control's skip mask for menu controls. This makes it * easy to skip menu items that are not valid. If bit X is set, * then menu item X is skipped. Of course, this only works for * menus with <= 64 menu items. There are no menus that come * close to that number, so this is OK. Should we ever need more, * then this will have to be extended to a bit array. * @def: The control's default value. * * Same as v4l2_ctrl_new_std(), but @min is set to 0 and the @mask value * determines which menu items are to be skipped. * * If @id refers to a non-menu control, then this function will return NULL. */
上面基本就是V4L2的框架图的一些源码解释,至于在代码中如何实现,还是需要根据代码去查看逻辑。主要是从注册设备去跟踪代码吧,因为工作中暂时没这些方面的需求,所以我这里也知识做一个简单的介绍。
四、V4l2的buf管理
因为我们可以通过v4l2的相关命令进行摄像头的数据流的抓取,在这过程中就涉及到一些文件的读写。
v4l2有三种:使用read/write方式;内存映射方式(mmap)和用户指针模式(USERPTR)。
read和write 是基本帧IO访问方式,通过read读取每一帧数据,数据需要在内核和用户之间拷贝,这种方式访问速度可能会非常慢;
内存映射缓冲区 (V4L2_MEMORY_MMAP): 是在内核空间开辟缓冲区,应用通过mmap()系统调用映射到用户地址空间。这些缓冲区可以是大而连续DMA缓冲区、通过vmalloc()创建的虚拟缓冲区,或者直接在设备的IO内存中开辟的缓冲区(如果硬件支持);
用户空间缓冲区(V4L2_MEMORY_USERPTR) 是用户空间的应用中开辟缓冲区,用户与内核空间之间交换缓冲区指针。很明显,在这种情况下是不需要mmap()调用的,但驱动为有效的支持用户空间缓冲区,其工作将也会更困难。 Camera sensor捕捉到图像数据通过并口或MIPI传输到CAMIF(camera interface),CAMIF可以对图像数据进行调整(翻转、裁剪和格式转换等)。然后DMA控制器设置DMA通道请求AHB将图像数据传到分配好的DMA缓冲区。待图像数据传输到DMA缓冲区之后,mmap操作把缓冲区映射到用户空间,应用就可以直接访问缓冲区的数据。而为了使设备支持流IO这种方式,v4l2需要实现对video buffer的管理,即实现:
/* vb2_queue代表一个videobuffer队列,vb2_buffer是这个队列中的成员,vb2_mem_ops是缓冲内存的操作函数集,vb2_ops用来管理队列 */struct vb2_queue { enum v4l2_buf_type type; //buffer类型 unsigned int io_modes; //访问IO的方式:mmap、userptr etc const struct vb2_ops *ops; //buffer队列操作函数集合 const struct vb2_mem_ops *mem_ops; //buffer memory操作集合 struct vb2_buffer *bufs[VIDEO_MAX_FRAME]; //代表每个frame buffer unsignedint num_buffers; //分配的buffer个数 ..........};/* vb2_mem_ops包含了内存映射缓冲区、用户空间缓冲区的内存操作方法 */struct vb2_mem_ops { void *(*alloc)(void *alloc_ctx, unsignedlong size); //分配视频缓存 void (*put)(void *buf_priv); //释放视频缓存 /* 获取用户空间视频缓冲区指针 */ void *(*get_userptr)(void *alloc_ctx, unsigned long vaddr, unsignedlong size, int write); void (*put_userptr)(void *buf_priv); //释放用户空间视频缓冲区指针 /* 用于缓存同步 */ void (*prepare)(void *buf_priv); void (*finish)(void *buf_priv); /* 缓存虚拟地址 & 物理地址 */ void *(*vaddr)(void *buf_priv); void *(*cookie)(void *buf_priv); unsignedint (*num_users)(void *buf_priv); //返回当期在用户空间的buffer数 int (*mmap)(void *buf_priv, structvm_area_struct *vma); //把缓冲区映射到用户空间 ..............};/* mem_ops由kernel自身实现并提供了三种类型的视频缓存区操作方法:连续的DMA缓冲区、集散的DMA缓冲区以及vmalloc创建的缓冲区,分别由videobuf2-dma-contig.c、videobuf2-dma-sg.c和videobuf-vmalloc.c文件实现,可以根据实际情况来使用。*//* vb2_ops是用来管理buffer队列的函数集合,包括队列和缓冲区初始化等 */struct vb2_ops { //队列初始化 int(*queue_setup)(struct vb2_queue *q, const struct v4l2_format *fmt, unsigned int *num_buffers, unsigned int*num_planes, unsigned int sizes[], void *alloc_ctxs[]); //释放和获取设备操作锁 void(*wait_prepare)(struct vb2_queue *q); void(*wait_finish)(struct vb2_queue *q); //对buffer的操作 int(*buf_init)(struct vb2_buffer *vb); int(*buf_prepare)(struct vb2_buffer *vb); int(*buf_finish)(struct vb2_buffer *vb); void(*buf_cleanup)(struct vb2_buffer *vb); //开始/停止视频流 int(*start_streaming)(struct vb2_queue *q, unsigned int count); int(*stop_streaming)(struct vb2_queue *q); //把VB传递给驱动,以填充frame数据 void(*buf_queue)(struct vb2_buffer *vb);};
//kernel_imx/drivers/media/v4l2-core/v4l2-compat-ioctl32.c struct v4l2_buffer32 { __u32 index;// buffer的序号 __u32 type; /* enum v4l2_buf_type 类型*/ __u32 bytesused;// 已经使用的byte数 __u32 flags; __u32 field; /* enum v4l2_field */ struct { compat_s64 tv_sec; compat_s64 tv_usec; } timestamp;// 时间戳,代表帧捕获的时间 struct v4l2_timecode timecode; __u32 sequence; /* memory location */ __u32 memory; /* enum v4l2_memory */ union { __u32 offset; compat_long_t userptr; compat_caddr_t planes; __s32 fd; } m; __u32 length;// 缓冲区大小,单位byte __u32 reserved2; __s32 request_fd;};
五、V4L2的整体流程
因为我这里自己跟踪流程的时候我已经跟蒙了,所以这里我就后续学会了再补充,这里我是根据设备注册开始跟踪的
// kernel_imx/drivers/media/v4l2-core/v4l2-dev.c int __video_register_device(struct video_device *vdev, enum vfl_devnode_type type, int nr, int warn_if_nr_in_use, struct module *owner){int i = 0;int ret;int minor_offset = 0;int minor_cnt = VIDEO_NUM_DEVICES;const char *name_base;/* A minor value of -1 marks this video device as never having been registered */vdev->minor = -1;/* the release callback MUST be present */if (WARN_ON(!vdev->release))return -EINVAL;/* the v4l2_dev pointer MUST be present */if (WARN_ON(!vdev->v4l2_dev))return -EINVAL;/* the device_caps field MUST be set for all but subdevs */if (WARN_ON(type != VFL_TYPE_SUBDEV && !vdev->device_caps))return -EINVAL;/* v4l2_fh support */spin_lock_init(&vdev->fh_lock);INIT_LIST_HEAD(&vdev->fh_list);/* Part 1: check device type */switch (type) {case VFL_TYPE_VIDEO:name_base = "video";break;case VFL_TYPE_VBI:name_base = "vbi";break;case VFL_TYPE_RADIO:name_base = "radio";break;case VFL_TYPE_SUBDEV:name_base = "v4l-subdev";break;case VFL_TYPE_SDR:/* Use device name 'swradio' because 'sdr' was already taken. */name_base = "swradio";break;case VFL_TYPE_TOUCH:name_base = "v4l-touch";break;default:pr_err("%s called with unknown type: %d\n", __func__, type);return -EINVAL;}vdev->vfl_type = type;vdev->cdev = NULL;if (vdev->dev_parent == NULL)vdev->dev_parent = vdev->v4l2_dev->dev;if (vdev->ctrl_handler == NULL)vdev->ctrl_handler = vdev->v4l2_dev->ctrl_handler;/* If the prio state pointer is NULL, then use the v4l2_device prio state. */if (vdev->prio == NULL)vdev->prio = &vdev->v4l2_dev->prio;/* Part 2: find a free minor, device node number and device index. */#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES/* Keep the ranges for the first four types for historical * reasons. * Newer devices (not yet in place) should use the range * of 128-191 and just pick the first free minor there * (new style). */switch (type) {case VFL_TYPE_VIDEO:minor_offset = 0;minor_cnt = 64;break;case VFL_TYPE_RADIO:minor_offset = 64;minor_cnt = 64;break;case VFL_TYPE_VBI:minor_offset = 224;minor_cnt = 32;break;default:minor_offset = 128;minor_cnt = 64;break;}#endif/* Pick a device node number */mutex_lock(&videodev_lock);nr = devnode_find(vdev, nr == -1 ? 0 : nr, minor_cnt);if (nr == minor_cnt)nr = devnode_find(vdev, 0, minor_cnt);if (nr == minor_cnt) {pr_err("could not get a free device node number\n");mutex_unlock(&videodev_lock);return -ENFILE;}#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES/* 1-on-1 mapping of device node number to minor number */i = nr;#else/* The device node number and minor numbers are independent, so we just find the first free minor number. */for (i = 0; i < VIDEO_NUM_DEVICES; i++)if (video_devices[i] == NULL)break;if (i == VIDEO_NUM_DEVICES) {mutex_unlock(&videodev_lock);pr_err("could not get a free minor\n");return -ENFILE;}#endifvdev->minor = i + minor_offset;vdev->num = nr;/* Should not happen since we thought this minor was free */if (WARN_ON(video_devices[vdev->minor])) {mutex_unlock(&videodev_lock);pr_err("video_device not empty!\n");return -ENFILE;}devnode_set(vdev);vdev->index = get_index(vdev);video_devices[vdev->minor] = vdev;mutex_unlock(&videodev_lock);if (vdev->ioctl_ops)determine_valid_ioctls(vdev);/* Part 3: Initialize the character device */vdev->cdev = cdev_alloc();if (vdev->cdev == NULL) {ret = -ENOMEM;goto cleanup;}vdev->cdev->ops = &v4l2_fops;vdev->cdev->owner = owner;ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);if (ret < 0) {pr_err("%s: cdev_add failed\n", __func__);kfree(vdev->cdev);vdev->cdev = NULL;goto cleanup;}/* Part 4: register the device with sysfs */vdev->dev.class = &video_class;vdev->dev.devt = MKDEV(VIDEO_MAJOR, vdev->minor);vdev->dev.parent = vdev->dev_parent;dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);ret = device_register(&vdev->dev);if (ret < 0) {pr_err("%s: device_register failed\n", __func__);goto cleanup;}/* Register the release callback that will be called when the last reference to the device goes away. */vdev->dev.release = v4l2_device_release;if (nr != -1 && nr != vdev->num && warn_if_nr_in_use)pr_warn("%s: requested %s%d, got %s\n", __func__,name_base, nr, video_device_node_name(vdev));/* Increase v4l2_device refcount */v4l2_device_get(vdev->v4l2_dev);/* Part 5: Register the entity. */ret = video_register_media_controller(vdev);/* Part 6: Activate this minor. The char device can now be used. */set_bit(V4L2_FL_REGISTERED, &vdev->flags);return 0;cleanup:mutex_lock(&videodev_lock);if (vdev->cdev)cdev_del(vdev->cdev);video_devices[vdev->minor] = NULL;devnode_clear(vdev);mutex_unlock(&videodev_lock);/* Mark this video device as never having been registered. */vdev->minor = -1;return ret;}
因为v4l2的框架还是有点多,我也没有对应的项目,所以这里就先写到这里。。
为完待续…