linux V4L2子系统——v4l2架构(7)之V4L2应用编程
备注:
1. Kernel版本:5.4
2. 使用工具:Source Insight 4.0
3. 参考博客:
(1)
Linux V4L2子系统-应用层访问video设备(四)
(2)
深入学习Linux摄像头(一)v4l2应用编程
文章目录
概述
V4L2子系统向上提供了很多访问Video设备的接口,应用程序可以通过系统调用访问Video设备。但由于Video设备千差万别,很少有设备驱动程序能支持所有的接口功能,因此在使用之前,需要了解设备驱动程序支持的功能。
v4l2 API介绍
详见:
linux V4L2子系统——v4l2的结构体(4)之ioctl
v4l2设备操作流程
V4L2支持多种接口:capture(捕获)、output(输出)、overlay(预览)等等这里讲解如何使用capture功能,下面讲解操作流程
打开设备文件
在Linux中,视频设备节点为/dev/videox,使用open函数将其打开。视频设备与其他设备一样可以视为一个文件,所以使用open打开文件。可以是阻塞打开,也可以是非阻塞打开,非阻塞打开,若没有数据,则会返回错误。
// 阻塞打开
fd = open(vmsg->dev, O_RDWR | O_NONBLOCK, 0);
if (fd == -1) {
sys_log_err("Cann't open '%s': %d, %s\n",
vmsg->dev, errno, strerror(errno));
return -ENODEV;
}
// 非阻塞打开
fd = open(vmsg->dev, O_RDWR);
if (fd == -1) {
sys_log_err("Cann't open '%s': %d, %s\n",
vmsg->dev, errno, strerror(errno));
return -ENODEV;
}
获取设备支持的功能
在使用Video设备之前,需要了解Video设备支持哪些功能,如是图像捕获设备还是图像输出设备、可对视频信号进行何种调制。支持VBI、是否具有音频功能等。可通过VIDIOC_QUERYCAP命令获取Video设备支持的功能。最终通过检查struct v4l2_capability中的capabilities变量获取设备支持的功能。
struct v4l2_capability cap;
......
ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
if (ret < 0) {
sys_log_err("%s is no V4L2 device %d-%d, %s\n",
vmsg->dev, ret, errno, strerror(errno));
goto error;
}
// 判断是否支持某些功能
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
sys_log_err("%s isn't video capture device\n", vmsg->dev);
ret = -ENODEV;
goto error;
}
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
sys_log_err("%s does not support streaming i/o\n", vmsg->dev);
ret = -ENODEV;
goto error;
}
......
设备的功能保存在 struct v4l2_capability 结构体中,capabilities 变量具体表示了设备具有的功能,功能由宏定义 V4L2_CAP_XXXX 表示。
// 源码:include/uapi/linux/videodev2.h
struct v4l2_capability {
__u8 driver[16]; // 驱动模块的名称,如"sun6i-video"
__u8 card[32]; // 品牌名称,如"sunxi WinTV"
__u8 bus_info[32]; // 总线名称,如"PCI:" + pci_name(pci_dev)
__u32 version; // 版本信息,KERNEL_VERSION
__u32 capabilities; // 设备整体的功能
__u32 device_caps;
__u32 reserved[3];
};
其中域 capabilities 代表设备支持的操作模式,常见的值有 V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING 表示是一个视频捕捉设备并且具有数据流控制模式;另外 driver 域需要和 struct video_device 中的 name 匹配。
如下:
// 源码:include/uapi/linux/videodev2.h
/* Values for 'capabilities' field */
#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_VIDEO_OVERLAY 0x00000004 /* Can do video overlay 支持预览功能 */
#define V4L2_CAP_VBI_CAPTURE 0x00000010 /* Is a raw VBI capture device */
#define V4L2_CAP_VBI_OUTPUT 0x00000020 /* Is a raw VBI output device */
#define V4L2_CAP_SLICED_VBI_CAPTURE 0x00000040 /* Is a sliced VBI capture device */
#define V4L2_CAP_SLICED_VBI_OUTPUT 0x00000080 /* Is a sliced VBI output device */
#define V4L2_CAP_RDS_CAPTURE 0x00000100 /* RDS data capture */
#define V4L2_CAP_VIDEO_OUTPUT_OVERLAY 0x00000200 /* Can do video output overlay */
#define V4L2_CAP_HW_FREQ_SEEK 0x00000400 /* Can do hardware frequency seek */
#define V4L2_CAP_RDS_OUTPUT 0x00000800 /* Is an RDS encoder */
/* Is a video capture device that supports multiplanar formats */
#define V4L2_CAP_VIDEO_CAPTURE_MPLANE 0x00001000
/* Is a video output device that supports multiplanar formats */
#define V4L2_CAP_VIDEO_OUTPUT_MPLANE 0x00002000
/* Is a video mem-to-mem device that supports multiplanar formats */
#define V4L2_CAP_VIDEO_M2M_MPLANE 0x00004000
/* Is a video mem-to-mem device */
#define V4L2_CAP_VIDEO_M2M 0x00008000
#define V4L2_CAP_TUNER 0x00010000 /* has a tuner */
#define V4L2_CAP_AUDIO 0x00020000 /* has audio support */
#define V4L2_CAP_RADIO 0x00040000 /* is a radio device */
#define V4L2_CAP_MODULATOR 0x00080000 /* has a modulator */
#define V4L2_CAP_SDR_CAPTURE 0x00100000 /* Is a SDR capture device */
#define V4L2_CAP_EXT_PIX_FORMAT 0x00200000 /* Supports the extended pixel format */
#define V4L2_CAP_SDR_OUTPUT 0x00400000 /* Is a SDR output device */
#define V4L2_CAP_META_CAPTURE 0x00800000 /* Is a metadata capture 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 */
#define V4L2_CAP_META_OUTPUT 0x08000000 /* Is a metadata output device */
#define V4L2_CAP_TOUCH 0x10000000 /* Is a touch device */
#define V4L2_CAP_DEVICE_CAPS 0x80000000 /* sets device capabilities field */
获取和设置像素格式
有些摄像头支持多个像素格式,有的摄像头只支持一种像素格式。因此在设置像素格式之前需要了解摄像头支持的像素格式,然后再进行设置。VIDIOC_ENUM_FMT 命令枚举设备支持的像素格式,VIDIOC_S_FMT 命令设置设备当前采用的像素格式。
- 枚举支持的像素格式
struct v4l2_fmtdesc fmtdesc = {0};
......
// 获取支持的像素格式
while (!ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc)) {
printf("fmt: %s\n", fmtdesc.description);
fmtdesc.index++;
}
......
- 设置像素格式
struct v4l2_format fmt = {0};
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_ANY;
ret = ioctl(fd, VIDIOC_S_FMT, &fmt);
if (ret < 0) {
sys_log_err("%s VIDIOC_S_FMT error %d-%d, %s\n",
vmsg->dev, ret, errno, strerror(errno));
memset(&fmt, 0, sizeof(fmt));
if (0 == ioctl(fd, VIDIOC_G_FMT, &fmt)) {
sys_log_err("%s support pixformat:\n", vmsg->dev);
video_show_format(&fmt, _LOG_ERR);
}
goto error;
}
- 获取像素格式
struct v4l2_format fmt = {0};
......
ret = ioctl(fd, VIDIOC_G_FMT, &fmt);
if (ret < 0) {
sys_log_err("%s VIDIOC_G_FMT error %d-%d, %s\n",
vmsg->dev, ret, errno, strerror(errno));
goto error;
}
设置缓存
v4l2设备读取数据的方式有两种,一种是 read 方式,一种是 streaming 方式,具体需要看其功能是支持 V4L2_CAP_READWRITE 还是V4L2_CAP_STREAMING
read 方式很容易理解,就是通过 read 函数读取,那么 streaming是什么意思呢?
streaming就是在内核空间中维护一个 缓存队列,然后将内存映射到用户空间,应用读取图像数据就是一个不断地 出队列 和 入队列 的过程,如下图所示
申请缓冲区
Video设备捕获的视频数据应该存放在预先分配的缓冲区中。使用 VIDIOC_REQBUFS 命令向内核申请缓冲区。在申请之前,需要设置申请的缓冲区数量 nr_bufs、缓冲区数据流类型type和缓冲区内存使用方式 memory。
struct v4l2_requestbuffers *req = NULL;
req = &vmsg->reqbuf;
memset(req, 0, sizeof(*req));
req->count = buf_cnt; //缓存数量
req->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req->memory = V4L2_MEMORY_MMAP; // 内存映射的方式,可提高效率,减少内存占用
ret = ioctl(fd, VIDIOC_REQBUFS, req);
if (ret) {
sys_log_err("%s request buffer failed %d-%d, %s\n",
vmsg->dev, ret, errno, strerror(errno));
return ret;
}
下面是struct v4l2_requestbuffers结构体的具体定义。
// 源码:include/uapi/linux/videodev2.h
/*
* M E M O R Y - M A P P I N G B U F F E R S
*/
struct v4l2_requestbuffers {
__u32 count; // 缓冲区数量
__u32 type; /* enum v4l2_buf_type 数据流类型,通常为V4L2_BUF_TYPE_VIDEO_CAPTURE */
__u32 memory; /* enum v4l2_memory 缓冲区内存使用方式,通常为V4L2_MEMORY_MMAP */
__u32 capabilities;
__u32 reserved[1];
};
enum v4l2_memory {
V4L2_MEMORY_MMAP = 1, // 内存映射
V4L2_MEMORY_USERPTR = 2, // 用户空间指针
V4L2_MEMORY_OVERLAY = 3, // 内存重叠
V4L2_MEMORY_DMABUF = 4, // DMABUF
};
映射缓存
为什么要映射缓存?
因为如果使用read方式读取的话,图像数据是从内核空间拷贝会应用空间,而一副图像的数据一般来讲是比较大的,所以效率会比较低。而如果使用映射的方式,讲内核空间的内存应用到用户空间,那么用户空间读取数据就想在操作内存一样,不需要经过内核空间到用户空间的拷贝,大大提高效率
映射缓存需要先查询缓存信息,然后再使用缓存信息进行映射,下面是一个例子
struct video_buff {
void* start;
size_t length;
};
......
struct video_buff *vbuff = NULL;
struct v4l2_buffer buf;
......
vbuff = (struct video_buff *)calloc(req->count, sizeof(struct video_buff));
if (NULL == vbuff) {
sys_log_err("%s request vbuff failed %d, %s\n",
vmsg->dev, errno, strerror(errno));
return -ENOMEM;
}
for (i = 0; i < req->count; i++) {
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ret = ioctl(fd, VIDIOC_QUERYBUF, &buf);
if (ret) {
sys_log_err("%s VIDIOC_QUERYBUF faild %d-%d, %s\r\n",
vmsg->dev, ret, errno, strerror(errno));
goto mmap_faild;
}
vbuff[i].length = buf.length;
vbuff[i].start = mmap(NULL,
buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_LOCKED,
fd,
buf.m.offset);
if (MAP_FAILED == vbuff[i].start) {
ret = (int)MAP_FAILED;
sys_log_err("%s map buff faild\r\n", vmsg->dev);
goto mmap_faild;
}
}
......
将所有的缓存放入队列
struct v4l2_buffer buf;
enum v4l2_buf_type type;
struct v4l2_requestbuffers *req = NULL;
......
req = &vmsg->reqbuf;
for (i = 0; i < req->count; i++) {
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ret = ioctl(vmsg->video_fd, VIDIOC_QBUF, &buf);
if (ret < 0) {
sys_log_err("%s VIDIOC_QBUF failed:%d-%d, %s\n",
vmsg->dev, ret, errno, strerror(errno));
goto error;
}
}
......
开始采集视频
经过前面的准备工作,现在可以使能设备,开始采集视频数据了。VIDIOC_STREAMON命令使能设备,开始采集视频。
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
......
ret = ioctl(vmsg->video_fd, VIDIOC_STREAMON, &type);
if (ret < 0) {
sys_log_err("%s VIDIOC_STREAMON START faild::%d-%d, %s\n",
vmsg->dev, ret, errno, strerror(errno));
goto error;
}
......
处理数据
struct v4l2_buffer v4l2_buf = {0};
for (;;) {
ret = ioctl(fd, VIDIOC_QBUF, &v4l2_buf); // 从环形队列中获取一个缓冲区
......
data = buf[v4l2_buf.index].start; // 缓冲区地址
length = buf[v4l2_buf.index].length // 缓冲区数据长度
......
ret = ioctl(fd, VIDIOC_QBUF, &v4l2_buf); // 将缓冲区放入环形队列中
......
}
停止采集视频
应用可使用VIDIOC_STREAMOFF命令停止视频采集,同时禁止设备。
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
......
ret = ioctl(vmsg->video_fd, VIDIOC_STREAMOFF, &type);
if (ret < 0) {
sys_log_err("%s VIDIOC_STREAMONVIDIOC_STREAMOFF STOP faild::%d-%d, %s\n",
vmsg->dev, ret, errno, strerror(errno));
goto error;
}
关闭设备
不再使用设备时,可使用close关闭设置,close调用后之前申请的缓冲区会被释放。