V4l2视频输出实现流程
Posted 逍遥說
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了V4l2视频输出实现流程相关的知识,希望对你有一定的参考价值。
实现功能
设备侧获取摄像头传感器的数据,通过UVC协议传给上位机。同时,上位机发送控制命令给设备侧。
参考源码:https://github.com/wlhe/uvc-gadget
1. 概念
UVC:是一种USB视频设备驱动。用来支持USB视频设备,凡是USB接口的摄像头都能够支持
V4L2:是Linux下视频采集和输出框架。用来统一接口,向应用层提供API
UVC和V4L2关系: V4L2就是用来管理UVC设备的并且能够提供视频相关的一些应用程序接口。在Linux系统上有很多的开源软件能够支持V4L2。常见的有FFmpeg、opencv、Skype、Mplayer等等。
2. 具体流程
2.1 打开video设备
Linux一切皆文件,首先打开视频数据要输出的设备文件,假如为/dev/video18
dev->fd = open("/dev/video18", O_RDWR | O_NONBLOCK);
以非阻塞的方式打开设备文件。启动时,驱动会先把缓存里初始化数据通过设备输出到上位机,然后等待视频数据填充缓存。
2.2 获取video设备的属性
struct v4l2_capability cap;
ret = ioctl(dev->fd, VIDIOC_QUERYCAP, &cap);
使用 VIDIOC_QUERYCAP 命令来获得当前设备的各个属性,查看设备对各项功能的支持程度,这里主要关注if(cap.capabilities & V4L2_CAP_VIDEO_OUTPUT) ,即这个设备是否具备 video output 的功能。
2.3 其他配置
2.3.1设备输出格式fmt查询
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0; //查询格式序号
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
ioctl(dev->fd, VIDIOC_ENUM_FMT, &fmtdesc);
用VIDIOC_ENUM_FMT来列举设备所支持的所有image格式
2.3.2 获取修剪能力cropcap,设置输出景象crop
获取:
struct v4l2_cropcap cropcap;
memset(&cropcap, 0, sizeof(cropcap));
cropcap.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
ioctl(dev->fd, VIDIOC_CROPCAP, &cropcap);//查询驱动的修剪能力
设置:
struct v4l2_crop crop;
crop.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
crop.c.top = g_display_top; // 0
crop.c.left = g_display_left; // 0
crop.c.width = g_display_width; // 显示宽度
crop.c.height = g_display_height; // 显示高度
ioctl(dev->fd, VIDIOC_S_CROP, &crop);
2.3.3 输出视频格式fmt设置
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
fmt.fmt.pix.width= g_in_width;
fmt.fmt.pix.height= g_in_height;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_UYVY;
fmt.fmt.pix.bytesperline = g_in_width;
fmt.fmt.pix.priv = 0;
fmt.fmt.pix.sizeimage = 0;
ioctl(dev->fd, VIDIOC_S_FMT, &fmt);
ioctl(dev->fd, VIDIOC_G_FMT, &fmt);
设置视频设备的视频数据格式,例如设置视频图像数据的长、宽,图像格式(JPEG、YUYV格式)
如果该视频设备驱动不支持你所设定的图像格式,视频驱动会重新修改struct v4l2_format结构体变量的值为该视频设备所支持的图像格式,所以在程序设计中,设定完所有的视频格式后,要获取实际的视频格式,要重新读取struct v4l2_format结构体变量
2.4 初始化并订阅UVC事件
2.4.1 初始化流控制 probe 和 commit 数据结构
uvc_streaming_control probe;
uvc_streaming_control commit;
数据结构uvc_streaming_control:
2.4.2 订阅事件
struct v4l2_event_subscription sub;
memset(&sub, 0, sizeof sub);
sub.type = UVC_EVENT_CONNECT;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
sub.type = UVC_EVENT_DISCONNECT;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
sub.type = UVC_EVENT_SETUP;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
sub.type = UVC_EVENT_DATA;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
sub.type = UVC_EVENT_STREAMON;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
sub.type = UVC_EVENT_STREAMOFF;
ioctl(dev->fd, VIDIOC_SUBSCRIBE_EVENT, &sub);
将UVC事件的connect、disconnect、setup、data、streamon、streamoff,通过VIDIOC_SUBSCRIBE_EVENT设置到驱动里(也可以说注册,订阅),上位机通过UVC事件与v4l2进行交互。
2.5 while循环等待事件触发
fd_set efds;
FD_ZERO(&efds);
FD_SET(dev->fd, &efds);
ret = select(dev->fd + 1, NULL, NULL, &efds, &tv);
阻塞,等待efds信号
struct v4l2_event v4l2_event;
struct uvc_event *uvc_event = (void *)&v4l2_event.u.data;
ret = ioctl(dev->fd, VIDIOC_DQEVENT, &v4l2_event);
当有订阅的事件到来时,触发efds信号,并获取事件信息。
2.6 主要事件处理
2.6.1 UVC_EVENT_SETUP
处理host请求的UVC_EVENT_SETUP事件:分标准USB命令和class类命令
USB_TYPE_STANDARD: 不处理,这部分驱动只需响应,不需要额外信息
USB_TYPE_CLASS: class类命令又分control和streaming,就是UVC协议相关的两个接口VC和VS。
1)VS接口处理(参考函数:uvc_events_process_streaming)
流控制命令:(参考 UVC 1.5 Class specification 4.3 节)
参数说明:
- bmRequestType 请求类型,参考标准USB协议
- bRequest 子类,定义在 Table A-8
- CS ,Control Selector ,定义在 Table A-16 ,例如是probe 还是 commit
- wIndex 高字节为0,低字节为接口号
- wLength 和 Data 和标准USB协议一样,为数据长度和数据
参数设置的过程需要主机和USB设备进行协商, 协商的过程大致如下图所示:
流程说明:
- Host 先将期望的设置发送给USB设备(PROBE)
- 设备将Host期望设置在自身能力范围之内进行修改,返回给Host(PROBE)
- Host 认为设置可行的话,Commit 提交(COMMIT)
- 设置接口的当前设置为某一个设置
2)VC接口处理(参考函数:uvc_events_process_control)
VC 接口内部有许多 unit 和 terminal 用来控制摄像头,比如我们可以通过 Process unit 设置白平衡、曝光等等。
参考结构定义:
struct uvc_camera_terminal camera_terminal;
struct uvc_processing_unit processing_unit;
2.6.2 UVC_EVENT_DATA
处理host请求的UVC_EVENT_DATA事件:传递参数信息
VS命令:probe和commit
VC命令:曝光、白平衡,亮度,对比度等等
根据请求,update本地的参数。
2.6.3 UVC_EVENT_STREAMON
处理host请求的UVC_EVENT_STREAMON事件:配置缓存空间,视频流传输启动
1)配置缓存空间
操作系统一般把系统使用的内存划分成用户空间和内核空间,分别由应用程序管理和操作系统管理。应用程序可以直接访问内存的地址,而内核空间存放的是供内核访问的代码和数据,用户不能直接访问。
V4l2缓存的数据,是存放在内核空间的,这意味着用户不能直接访问该段内存,必须通过某些手段来转换地址。主要采用内存映射方式和用户指针模式。
内存映射方式:把设备里的内存映射到应用程序中的内存空间,直接处理设备内存。
用户指针模式:内存片段由应用程序自己分配。这点需要在v4l2_requestbuffers里将memory字段设置成V4L2_MEMORY_USERPTR。
struct v4l2_requestbuffers
__u32 count;
__u32 type; /* enum v4l2_buf_type */
__u32 memory; /* enum v4l2_memory */
__u32 reserved[2];
;
count: 要申请的buffer的数量
memory:要么是 V4L2_MEMORY_MMAP,要么是V4L2_MEMORY_USERPTR
这里主要讲V4L2_MEMORY_USERPTR模式:
a. 向驱动申请视频流数据的帧缓冲区.
struct v4l2_requestbuffers rb;
memset(&rb, 0, sizeof rb);
rb.count = nbufs;
rb.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
rb.memory = V4L2_MEMORY_USERPTR;//V4L2_MEMORY_MMAP;
ret = ioctl(dev->fd, VIDIOC_REQBUFS, &rb);
b. 用户申请内存片段,并加入到输出缓存队列中
struct v4l2_buffer buf;
for (i = 0; i < dev->nbufs; ++i)
memset(&buf, 0, sizeof buf);
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
buf.memory = V4L2_MEMORY_USERPTR;
buf.length = MAX_BUFFER_SIZE;
buf.m.userptr = (unsigned long)dev->dummy_buf[i].start;//用户空间
ret = ioctl(dev->fd, VIDIOC_QBUF, &buf);
控制命令VIDIOC_QBUF逐一将buf投放到输出缓存队列尾部。申请若干个帧缓冲区,nbufs一般为不少于3个。
注意:测试验证用户申请的空间dev->dummy_buf[i].start要初始化为0,否则无法正常传输视频数据
v4l2_buffer 数据结构:
__u32 index; // 应用程序来设定,仅仅用来申明是哪个 buffer
__u32 type;
__u32 bytesused; //buffer 中已经使用的 byte 数,如果是 input stream 由 driver 来设 定,相反则由应用程序来设定
__u32 flags; // 定义了 buffer 的一些标志位,来表明这个 buffer 处在哪个队列,比如输入 队列或者输出队列 (V4L2_BUF_FLAG_QUEUED ,V4L2_BUF_FLAG_DONE) , 是否 关键帧等等
__u32 memory; //V4L2_MEOMORY_MMAP / V4L2_MEMORY_USERPTR / V4L2_MEMORY_OVERLAY
union m :
__u32 offset; // 当 memory 类型是 V4L2_MEOMORY_MMAP 的时候,主要用来表明 buffer 在 device momory 中相对起始位置的偏移,主要用在 mmap() 参数 中,对应用程序没有左右
unsigned long userptr; // 当 memory 类型是 V4L2_MEMORY_USERPTR 的时候,这是 一个指向虚拟内存中 buffer 的指针,由应用程序来设定。
__u32 length; //buffer 的 size
在驱动内部管理 着两个 buffer queues ,一个输入队列,一个输出队列。
对于 capture device 来说,当输入队列中的 buffer 被塞满数据以后会自动变为输出队列,等待调用 VIDIOC_DQBUF 将数据进行处理以后重新调用VIDIOC_QBUF 将 buffer 重新放进输入队列;
对于 output device 来说 buffer 被显示(或读走)后自动变为输出队列。等待调用 VIDIOC_DQBUF ,buffer被塞满数据后调用VIDIOC_QBUF 将 buffer 重新放进输入队列;
2)开始视频流数据的采集。
int type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
ioctl (fd_v4l, VIDIOC_STREAMON, &type)
2.6.4 UVC_EVENT_STREAMOFF
处理host的UVC_EVENT_STREAMOFF请求
a. 停止视频流输出
int type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
ioctl(dev->fd, VIDIOC_STREAMOFF, &type);
b. 释放内核缓存
nbufs = 0;//设置为0
memset(&rb, 0, sizeof rb);
rb.count = nbufs;
rb.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
rb.memory = V4L2_MEMORY_USERPTR;//V4L2_MEMORY_MMAP;
ret = ioctl(dev->fd, VIDIOC_REQBUFS, &rb);
2.7 视频数据输出
2.7.1 while循环等待写准备信号
a. 当有wfds信号到来时,表示可以对输出队列进行写操作
ret = select(dev->fd + 1, NULL, &wfds, NULL, &tv);//&wfds
if(ret > 0)
ret = uvc_video_process(dev);
b. 从视频缓冲区中的输出队列取得一个可写的缓冲区,并将视频数据copy到该缓冲区。
struct v4l2_buffer buf;
memset(&buf, 0, sizeof buf);
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
buf.memory = V4L2_MEMORY_USERPTR;//V4L2_MEMORY_MMAP;
buf.length = MAX_BUFFER_SIZE;
ret = ioctl(dev->fd, VIDIOC_DQBUF, &buf);
uvc_video_fill_buffer(dev, &buf);
c. 将该缓冲区重新投放到视频缓冲区的输入队列中,等待被上位机读取或显示。
ret = ioctl(dev->fd, VIDIOC_QBUF, &buf);
2.8 结束关闭视频设备
close(dev->fd);
free(dev->mem); //释放用户申请的内存
以上是关于V4l2视频输出实现流程的主要内容,如果未能解决你的问题,请参考以下文章
音视频编解码流程与如何使用 FFMPEG 命令进行音视频处理