class="hide-preCode-box">
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
可以从capabilities 成员获得很多信息,如是否是capture设备 ,使用内存映射还是直接读的方式获取图像数据。
/* 判断是否是视频采集设备 */
if (!(V4L2_CAP_VIDEO_CAPTURE & vcap.capabilities)) {
fprintf(stderr, "Error: No capture video device!\n");
return -1;
}
#define V4L2_CAP_VIDEO_CAPTURE 0x00000001 /* Is a video capture device */
#define V4L2_CAP_VIDEO_OUTPUT 0x00000002 /* Is a video output device */
#define V4L2_CAP_READWRITE 0x01000000 /* read/write systemcalls */
#define V4L2_CAP_ASYNCIO 0x02000000 /* async I/O */
#define V4L2_CAP_STREAMING 0x04000000 /* streaming I/O ioctls */
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
3、设置设备参数
在设置格式之前,要先枚举出所有的格式,看一看是否支持要设置的格式,然后再进一步设置 。
//枚举出摄像头支持的所有像素格式:VIDIOC_ENUM_FMT
ioctl(int fd, VIDIOC_ENUM_FMT, struct v4l2_fmtdesc *fmtdesc);
//枚举摄像头所支持的所有视频采集分辨率:VIDIOC_ENUM_FRAMESIZES
ioctl(int fd, VIDIOC_ENUM_FRAMESIZES, struct v4l2_frmsizeenum *frmsize);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
设置图像格式需要用到struct v4l2_format 结构体,该结构体描述每帧图像的具体格式,包括帧类型以及图像的长、宽等信息。
//设置设备参数
ioctl(int fd, VIDIOC_G_FMT, struct v4l2_format *fmt);
/**
* struct v4l2_format - stream data format
* @type: enum v4l2_buf_type; type of the data stream
* @pix: definition of an image format
* @pix_mp: definition of a multiplanar image format
* @win: definition of an overlaid image
* @vbi: raw VBI capture or output parameters
* @sliced: sliced VBI capture or output parameters
* @raw_data: placeholder for future extensions and custom formats
*/
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;
};
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">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
使用摄像头设备时,type设置成V4L2_BUF_TYPE_VIDEO_CAPTURE ,使用struct v4l2_pix_format来描述摄像头属性。
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 800;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
if (0 > ioctl(fd, VIDIOC_S_FMT, &fmt)) { //设置格式
perror("ioctl error");
return -1;
}
struct v4l2_pix_format {
__u32 width; //视频帧的宽度(单位:像素)
__u32 height; //视频帧的高度(单位:像素)
__u32 pixelformat; //像素格式
__u32 field; /* enum v4l2_field */
__u32 bytesperline; /* for padding, zero if unused */
__u32 sizeimage;
__u32 colorspace; /* enum v4l2_colorspace */
__u32 priv; /* private data, depends on pixelformat */
__u32 flags; /* format flags (V4L2_PIX_FMT_FLAG_*) */
union {
/* enum v4l2_ycbcr_encoding */
__u32 ycbcr_enc;
/* enum v4l2_hsv_encoding */
__u32 hsv_enc;
};
__u32 quantization; /* enum v4l2_quantization */
__u32 xfer_func; /* enum v4l2_xfer_func */
};
VIDIOC_S_FMT: 设置当前驱动的视频捕获格式;
VIDIOC_G_FMT: 读取当前驱动的视频捕获格式;
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">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 30 31 32 33
设置帧率,需要传入struct v4l2_streamparm结构体
ioctl(int fd, VIDIOC_S_PARM, struct v4l2_streamparm *streamparm);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
4、申请帧缓存
读取摄像头数据的方式有两种,一种是 read 方式(对应设备功能返回的 V4L2_CAP_READWRITE );另一种则是 streaming 方式 (使用mmap,对应设备功能返回的 V4L2_CAP_STREAMING )
//申请帧缓存
ioctl(int fd, VIDIOC_REQBUFS, struct v4l2_requestbuffers *reqbuf);
struct v4l2_requestbuffers {
__u32 count; /* 帧缓存区的数量 */
__u32 type; /* enum v4l2_buf_type 缓冲帧数据格式*/
__u32 memory; /* enum v4l2_memory 是内存映射还是用户指针方式*/
__u32 capabilities;
__u32 reserved[1];
};
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
例子:
memset(&tV4l2ReqBuffs, 0, sizeof(struct v4l2_requestbuffers));
tV4l2ReqBuffs.count = 3;
tV4l2ReqBuffs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //缓冲帧数据格式
tV4l2ReqBuffs.memory = V4L2_MEMORY_MMAP; //内存映射的方式
iError = ioctl(Fd, VIDIOC_REQBUFS, &tV4l2ReqBuffs);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
使用 VIDIOC_REQBUFS 指令申请帧缓冲, 该缓冲区实质上是由内核所维护的,应用程序不能直接读
取该缓冲区的数据,我们需要将其映射到用户空间 中
//需要查询帧缓冲的信息
ioctl(int fd, VIDIOC_QUERYBUF, struct v4l2_buffer *buf);
//申请帧缓冲后、需要查询帧缓冲的信息用于mmap参数,调用 mmap()将帧缓冲映射到用户地址空间
iError = ioctl(iFd, VIDIOC_QUERYBUF, &tV4l2Buf);
ptVideoDevice->pucVideBuf[i] = mmap(0 /* start anywhere */ ,
tV4l2Buf.length, PROT_READ, MAP_SHARED, iFd,
tV4l2Buf.m.offset);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
5、入队,开始采集
将申请的缓冲帧依次放入缓冲帧输入队列,等待被图像采集设备依次填满
ioctl(int fd, VIDIOC_QBUF, struct v4l2_buffer *buf);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
ioctl ( int fd, VIDIOC_STREAMON, int * type) ;
ioctl ( int fd, VIDIOC_STREAMOFF, int * type) ;
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
6、出队
ioctl(int fd, VIDIOC_DQBUF, struct v4l2_buffer *buf);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
帧缓冲出队之后,接下来便可读取数据了,然后对数据进行处理, 比如对数据进行转化,如RGB888转RGB565,将转换后的数据刷到FrameBuffer上进行显示。处理完后再入队继续采集。
7、关闭采集
停止采集图像数据,首先使用VIDIOC_STREAMOFF命令,关闭捕获图像数据。同时要注意取消内存映射和关闭句柄,防止不必要的内存泄漏。
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_STREAMOFF, &type)) {
perror("ioctl error");
return -1;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
六、实现效果
使用USB摄像头实现效果
参考资料
data-report-view="{"mod":"1585297308_001","spm":"1001.2101.3001.6548","dest":"https://blog.csdn.net/m0_61737429/article/details/129773764","extend1":"pc","ab":"new"}">>
id="article_content" class="article_content clearfix">
id="content_views" class="markdown_views prism-atom-one-dark">
class="toc">
Linux--V4L2 应用编程
Linux–V4L2应用编程
一、V4L2简介
V4L全称是Video for Linux,是Linux内核 中标准的关于视频驱动程序,目前使用比较多的版本是Video for Linux 2,简称V4L2 。它为Linux下的视频驱动提供了统一的接口,使得应用程序可以使用统一的API操作不同的视频设备。从内核空间到用户空间,主要的数据流和控制类均由V4L2驱动程序的框架来定义。 V4L2支持三类设备:视频输入输出设备 、VBI设备 和radio设备 ,分别会在/dev目录下产生video*、radio*和vbi设备节点。
在Linux中,摄像头方面的标准化程度比较高,这个标准就是V4L2驱动程序,这也是业界比较公认的方式。
二、V4L2整体框架
三、V4L2视频采集过程
重要的一步是分配帧缓冲区 ,并将分配的帧缓冲区从内核空间映射到用户空间 ,然后将申请到的帧缓冲区在视频采集输入队列排队,剩下的就是等待视频数据的到来。 当启动视频采集后,驱动程序开始采集一帧图像数据,会把采集的图像数据放入视频采集输入队列的第一个帧缓冲区,一阵图像数据就算采集完成了。第一个帧缓冲区存满一帧图像数据后,驱动程序将该帧缓冲区移至视频采集输出队列,等待应用程序从输出队列取出,应用程序取出图像数据可以对图像数据进行处理或存储操作,然后将帧该缓冲区放入视频采集输入队列的尾部。驱动程序接下来采集下一帧数据,放入第二个缓冲区,同样的帧缓冲区存满一帧数据后,驱动程序将该缓冲区移至视频采集输出队列,应用程序将该帧缓冲区的图像数据取出后又将该帧缓冲区放入视频输入队列尾部,这样循环往复就实现了循环采集。流程如下图所示:
四、V4L2应用层主要接口
V4L2应用层大多数用ioctl来实现对设备的IO操作。其中V4L2提供了很多ioctl宏来和应用层交互。主要定义在uapi/linux/videodev2.h文件中。 在应用程序代码中,需要包含头文件 linux/videodev2.h 。
在进行V4L2开发中,常用的命令标识符如下:
(1) VIDIOC_REQBUFS: 分配内存;
(2) VIDIOC_QUERYBUF: 把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址;
(3) VIDIOC_QUERYCAP: 查询驱动功能;
(4) VIDIOC_ENUM_FMT: 获取当前驱动支持的视频格式;
(5) VIDIOC_S_FMT: 设置当前驱动的视频捕获格式;
(6) VIDIOC_G_FMT: 读取当前驱动的视频捕获格式;
(7) VIDIOC_TRY_FMT: 验证当前驱动的显示格式;
(8) VIDIOC_CROPCAP: 查询驱动的修剪功能;
(9) VIDIOC_S_CROP: 设置视频信号的边框;
(10)VIDIOC_G_CROP: 读取视频信号的边框;
(11)VIDIOC_QBUF: 把数据从缓存中读取出来;
(12)VIDIOC_DQBUF: 把数据放回缓存队列;
(13)VIDIOC_STREAMON: 开始视频显示函数;
(14)VIDIOC_STREAMOFF:结束视频显示函数;
(15)VIDIOC_QUERYSTD: 检查当前视频设备支持的标准,例如PAL或NTSC;
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">
五、V4L2应用编程流程
V4L2 设备驱动框架向应用层提供了一套统一、标准的接口规范,应用程序按照该接口规范来进行应用 编程。 多数采用ioctl来实现。
1、打开视频文件设备
视频类设备对应的设备节点为/dev/videoX, X 为数字编号,通常从 0 开始 ,使用open打开节点, 应用程序能够使用阻塞模式 或非阻塞模式 打开视频设备,如果使用非阻塞模式调用视频设备,即使尚未捕获到信息,驱动依旧会把缓存(DQBUFF)里的东西返回给应用程序。
fd = open ( "/dev/video0" , O_RDWR) ;
fd = open ( "/dev/video0" , O_RDWR | O_NOBLOCK) ;
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
2、查询属性、功能
查询设备的属性, 使用的指令为 VIDIOC_QUERYCAP ,需要传入一个表示属性的结构体, 如下所示:
//查询设备的属性
ioctl(int fd, VIDIOC_QUERYCAP, struct v4l2_capability *cap);
/**
* struct v4l2_capability - Describes V4L2 device caps returned by VIDIOC_QUERYCAP
*
* @driver: name of the driver module (e.g. "bttv")
* @card: name of the card (e.g. "Hauppauge WinTV")
* @bus_info: name of the bus (e.g. "PCI:" + pci_name(pci_dev) )
* @version: KERNEL_VERSION
* @capabilities: capabilities of the physical device as a whole
* @device_caps: capabilities accessed via this particular device (node)
* @reserved: reserved fields for future extensions
*/
struct v4l2_capability {
__u8 driver[16]; /* 驱动名字 */
__u8 card[32]; /* 设备名字 */
__u8 bus_info[32]; /* 总线名字 */
__u32 version; /* 版本信息 */
__u32 capabilities; /* 功能 */
__u32 device_caps; /* 通过特定设备(节点)访问的能力 */
__u32 reserved[3]; /* 保留 */
};
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
可以从capabilities 成员获得很多信息,如是否是capture设备 ,使用内存映射还是直接读的方式获取图像数据。
/* 判断是否是视频采集设备 */
if (!(V4L2_CAP_VIDEO_CAPTURE & vcap.capabilities)) {
fprintf(stderr, "Error: No capture video device!\n");
return -1;
}
#define V4L2_CAP_VIDEO_CAPTURE 0x00000001 /* Is a video capture device */
#define V4L2_CAP_VIDEO_OUTPUT 0x00000002 /* Is a video output device */
#define V4L2_CAP_READWRITE 0x01000000 /* read/write systemcalls */
#define V4L2_CAP_ASYNCIO 0x02000000 /* async I/O */
#define V4L2_CAP_STREAMING 0x04000000 /* streaming I/O ioctls */
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
3、设置设备参数
在设置格式之前,要先枚举出所有的格式,看一看是否支持要设置的格式,然后再进一步设置 。
//枚举出摄像头支持的所有像素格式:VIDIOC_ENUM_FMT
ioctl(int fd, VIDIOC_ENUM_FMT, struct v4l2_fmtdesc *fmtdesc);
//枚举摄像头所支持的所有视频采集分辨率:VIDIOC_ENUM_FRAMESIZES
ioctl(int fd, VIDIOC_ENUM_FRAMESIZES, struct v4l2_frmsizeenum *frmsize);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
设置图像格式需要用到struct v4l2_format 结构体,该结构体描述每帧图像的具体格式,包括帧类型以及图像的长、宽等信息。
//设置设备参数
ioctl(int fd, VIDIOC_G_FMT, struct v4l2_format *fmt);
/**
* struct v4l2_format - stream data format
* @type: enum v4l2_buf_type; type of the data stream
* @pix: definition of an image format
* @pix_mp: definition of a multiplanar image format
* @win: definition of an overlaid image
* @vbi: raw VBI capture or output parameters
* @sliced: sliced VBI capture or output parameters
* @raw_data: placeholder for future extensions and custom formats
*/
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;
};
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">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
使用摄像头设备时,type设置成V4L2_BUF_TYPE_VIDEO_CAPTURE ,使用struct v4l2_pix_format来描述摄像头属性。
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 800;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
if (0 > ioctl(fd, VIDIOC_S_FMT, &fmt)) { //设置格式
perror("ioctl error");
return -1;
}
struct v4l2_pix_format {
__u32 width; //视频帧的宽度(单位:像素)
__u32 height; //视频帧的高度(单位:像素)
__u32 pixelformat; //像素格式
__u32 field; /* enum v4l2_field */
__u32 bytesperline; /* for padding, zero if unused */
__u32 sizeimage;
__u32 colorspace; /* enum v4l2_colorspace */
__u32 priv; /* private data, depends on pixelformat */
__u32 flags; /* format flags (V4L2_PIX_FMT_FLAG_*) */
union {
/* enum v4l2_ycbcr_encoding */
__u32 ycbcr_enc;
/* enum v4l2_hsv_encoding */
__u32 hsv_enc;
};
__u32 quantization; /* enum v4l2_quantization */
__u32 xfer_func; /* enum v4l2_xfer_func */
};
VIDIOC_S_FMT: 设置当前驱动的视频捕获格式;
VIDIOC_G_FMT: 读取当前驱动的视频捕获格式;
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="hide-preCode-box">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 30 31 32 33
设置帧率,需要传入struct v4l2_streamparm结构体
ioctl(int fd, VIDIOC_S_PARM, struct v4l2_streamparm *streamparm);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
4、申请帧缓存
读取摄像头数据的方式有两种,一种是 read 方式(对应设备功能返回的 V4L2_CAP_READWRITE );另一种则是 streaming 方式 (使用mmap,对应设备功能返回的 V4L2_CAP_STREAMING )
//申请帧缓存
ioctl(int fd, VIDIOC_REQBUFS, struct v4l2_requestbuffers *reqbuf);
struct v4l2_requestbuffers {
__u32 count; /* 帧缓存区的数量 */
__u32 type; /* enum v4l2_buf_type 缓冲帧数据格式*/
__u32 memory; /* enum v4l2_memory 是内存映射还是用户指针方式*/
__u32 capabilities;
__u32 reserved[1];
};
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
例子:
memset(&tV4l2ReqBuffs, 0, sizeof(struct v4l2_requestbuffers));
tV4l2ReqBuffs.count = 3;
tV4l2ReqBuffs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //缓冲帧数据格式
tV4l2ReqBuffs.memory = V4L2_MEMORY_MMAP; //内存映射的方式
iError = ioctl(Fd, VIDIOC_REQBUFS, &tV4l2ReqBuffs);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
使用 VIDIOC_REQBUFS 指令申请帧缓冲, 该缓冲区实质上是由内核所维护的,应用程序不能直接读
取该缓冲区的数据,我们需要将其映射到用户空间 中
//需要查询帧缓冲的信息
ioctl(int fd, VIDIOC_QUERYBUF, struct v4l2_buffer *buf);
//申请帧缓冲后、需要查询帧缓冲的信息用于mmap参数,调用 mmap()将帧缓冲映射到用户地址空间
iError = ioctl(iFd, VIDIOC_QUERYBUF, &tV4l2Buf);
ptVideoDevice->pucVideBuf[i] = mmap(0 /* start anywhere */ ,
tV4l2Buf.length, PROT_READ, MAP_SHARED, iFd,
tV4l2Buf.m.offset);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
5、入队,开始采集
将申请的缓冲帧依次放入缓冲帧输入队列,等待被图像采集设备依次填满
ioctl(int fd, VIDIOC_QBUF, struct v4l2_buffer *buf);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
ioctl ( int fd, VIDIOC_STREAMON, int * type) ;
ioctl ( int fd, VIDIOC_STREAMOFF, int * type) ;
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
6、出队
ioctl(int fd, VIDIOC_DQBUF, struct v4l2_buffer *buf);
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
帧缓冲出队之后,接下来便可读取数据了,然后对数据进行处理, 比如对数据进行转化,如RGB888转RGB565,将转换后的数据刷到FrameBuffer上进行显示。处理完后再入队继续采集。
7、关闭采集
停止采集图像数据,首先使用VIDIOC_STREAMOFF命令,关闭捕获图像数据。同时要注意取消内存映射和关闭句柄,防止不必要的内存泄漏。
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_STREAMOFF, &type)) {
perror("ioctl error");
return -1;
}
class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
六、实现效果
使用USB摄像头实现效果
参考资料
data-report-view="{"mod":"1585297308_001","spm":"1001.2101.3001.6548","dest":"https://blog.csdn.net/m0_61737429/article/details/129773764","extend1":"pc","ab":"new"}">>
评论记录:
回复评论: