V4l2视频输出实现流程(转)

本文详细介绍了如何利用UVC协议在Linux环境下,通过V4L2接口实现摄像头传感器数据的实时传输,并控制设备接受上位机指令,包括设备打开、属性配置、事件监听和数据处理等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实现功能

设备侧获取摄像头传感器的数据,通过UVC协议传给上位机。同时,上位机发送控制命令给设备侧

参考源码https://github.com/wlhe/uvc-gadget

1. 概念

UVC:是一种USB视频设备驱动。用来支持USB视频设备,凡是USB接口的摄像头都能够支持。

V4L2:是Linux下视频采集和输出框架。用来统一接口,向应用层提供API

UVC和V4L2关系 V4L2就是用来管理UVC设备的并且能够提供视频相关的一些应用程序接口。在Linux系统上有很多的开源软件能够支持V4L2。常见的有FFmpeg(我用这个)、opencvSkypeMplayer等等。

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 命令来获得设备的各个属性对各项功能的支持程度,这里主要关注 V4L2_CAP_VIDEO_OUTPUT ,即 video output 的功能。

2.3 其他配置

2.3.1 查询设备输出格式fmtdes

struct v4l2_fmtdesc fmtdes;
fmtdes.index = 0;    // 查询格式序号
fmtdes.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
ioctl(dev->fd, VIDIOC_ENUM_FMT, &fmtdes);

使用 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事件connectdisconnectsetupdatastreamonstreamoff,通过 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命令 (USB_TYPE_STANDARD): 不处理。这部分驱动只需响应,不需要额外信息

class类命令 (USB_TYPE_CLASS): 又分 controlstreaming,就是UVC协议相关的两个接口 VCVS

1)VS接口处理(参考函数:uvc_events_process_streaming)

流控制命令:(参考 UVC 1.5 Class specification 4.3 节)

参数说明

bmRequestType: 请求类型。参考标准USB协议。
bRequest: 子类。定义在Table A-8。
CS(Control Selector): 是 probe 还是 commit。定义在 Table A-16。
wIndex: 高字节为0,低字节为接口号
wLengthData: 数据长度和数据。和标准USB协议一样。
 

参数设置的过程需要主机和USB设备进行协商协商过程大致如下图所示:

流程说明

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


2)VC接口处理(参考函数:uvc_events_process_control)

struct uvc_camera_terminal camera_terminal;
struct uvc_processing_unit processing_unit;

VC接口内部有许多 unit terminal 用来控制摄像头。比如,我们可以通过 Process 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缓存的数据,存放在内核空间。这意味着用户不能直接访问该段内存,必须通过某些手段来转换地址。主要采用内存映射方式用户指针模式

内存映射方式 (mmap):把设备里的内存映射到应用程序中的内存空间,直接处理设备内存

用户指针模式 (userptr):内存片段由应用程序自己分配。这点需要在 struct v4l2_requestbuffers 里将 memory 字段设置成 V4L2_MEMORY_USERPTR

v4l2_requestbuffers 结构体成员:

struct v4l2_requestbuffers {
    __u32 count;
    __u32 type;      // enum v4l2_buf_type
    __u32 memory;    // enum v4l2_memory
    __u32 reserved[2];
 
};

count: 要申请的buffer数量

memory: V4L2_MEMORY_MMAPV4L2_MEMORY_USERPTR

这里主要讲 V4L2_MEMORY_USERPTR 模式

a. 向驱动申请视频流数据的帧缓冲区:

struct v4l2_requestbuffers reqbufs;
memset(&reqbufs, 0, sizeof reqbufs);
reqbufs.count  = nbufs;
reqbufs.type   = V4L2_BUF_TYPE_VIDEO_OUTPUT;
reqbufs.memory = V4L2_MEMORY_USERPTR;
ret = ioctl(dev->fd, VIDIOC_REQBUFS, &reqbufs);

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 结构体成员:

struct 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 */
}

驱动内部管理着2个缓存队列,一个输入队列,一个输出队列

对于 capture 类型的设备 来说,当 输入队列中的 buffer 被塞满数据后 会自动变为输出队列,等待调用 VIDIOC_DQBUF 命令将数据进行处理以后,重新调用 VIDIOC_QBUF 命令,将 buffer 重新放进输入队列。

对于 output 类型的设备 来说,当 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(&reqbufs, 0, sizeof(reqbufs));
reqbufs.count  = nbufs; 
reqbufs.type   = V4L2_BUF_TYPE_VIDEO_OUTPUT; 
reqbufs.memory = V4L2_MEMORY_USERPTR;
ret = ioctl(dev->fd, VIDIOC_REQBUFS, &reqbufs);


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

————————————————
原文链接:https://blog.youkuaiyun.com/h1527820835/article/details/124366709

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值