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设备进行协商, 协商的过程大致如下图所示:

流程说明:

  1. Host 先将期望的设置发送给USB设备(PROBE)
  2. 设备将Host期望设置在自身能力范围之内进行修改,返回给Host(PROBE)
  3. Host 认为设置可行的话,Commit 提交(COMMIT)
  4. 设置接口的当前设置为某一个设置

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); //释放用户申请的内存

猜你喜欢

转载自blog.csdn.net/h1527820835/article/details/124366709