一、V4L2
1.1 介绍
V4L2是video for Linux 2的缩写,是一套Linux内核视频设备的驱动框架,该驱动框架为应用层提供一套统一的操作接口(一系列的ioctl)。
V4L2在设计时,是要支持很多广泛的设备的,它们之中只有一部分在本质上是真正的视频设备,可以支持多种设备,它可以有以下几种接口。
- video capture interface:视频捕获接口,这种接口应用于摄像头,V4L2在最初设计的时候就是应用于这种功能;
- video output interface:视频输出接口,将静止图像或图像序列编码为模拟视频信号,通过此接口,应用程序可以控制编码过程并将图像从用户空间移动到驱动程序;
- video overlay interface:视频直接传输接口,可以将采集到的视频数据直接传输到显示设备,不需要cpu参与,这种方式的显示图像的效率比其他方式高得多;
1.2 V4L2应用程序框架
在linux下,一切设备皆是是文件,可以像访问普通文件一样对其进行读写。一般来说,采用V4L2驱动的摄像头设备文件是/dev/video0。
V4L2支持两种方式来采集图像:内存映射方式(mmap)和直接读取方式(read)。
V4L2在include/linux/videodev.h文件中定义了一些重要的数据结构,在采集图像的过程中,就是通过对这些数据的操作来获取最终的图像数据,Linux系统V4L2的能力可在Linux内核编译阶段配置,默认情况下都有此开发接口。
而摄像头所用的主要是capature了,视频的捕获,具体linux的调用可以参考下图:
1.3 V4L2采集视频步骤
V4L2支持内存映射方式(mmap)和直接读取方式(read)来采集数据,前者一般用于连续视频数据的采集,后者常用于静态图片数据的采集,本文重点讨论内存映射方式的视频采集。应用程序通过V4L2接口采集视频数据分为以下几个步骤:
- 打开视频设备文件;
- 查询设备驱动的功能;
- 枚举输入设备,并设置输入设备;
- 获取视频采集设备支持的视频格式;
- 设置视频设备的视频数据格式,通过V4L2接口设置视频图像的采集窗口、采集的点阵大小和格式等;
- 申请若干视频采集的帧缓冲区,并将这些帧缓冲区从内核空间映射到用户空间,便于应用程序读取/处理视频数据;
- 将申请到的帧缓冲区在视频采集输入队列排队,并启动视频采集;
- 驱动开始视频数据的采集,应用程序从视频采集输出队列取出帧缓冲区;
- 这里我们拿到帧缓冲区的图像数据,我们就可以进行一些想要的操作,比如保存图片,实时显示等;
- 将帧缓冲区重新放入视频采集输入队列,循环往复采集连续的视频数据;
- 停止视频采集;
二、ioctl API介绍
其中V4L2大多数操作都是通过应用层调用ioctl实现的,可以将这些ioctl分为若干类。
2.1 查询设备的功能
由于V4L2涵盖了各种各样的设备,因此并非API的所有方面都适用于所有类型的设备,在使用V4L2设备时,必须调用此API,获得设备支持的功能(capture、output、overlay…)
ID | 描述 |
---|---|
VIDIOC_QUERYCAP | 查询设备功能 |
参数类型为V4L2的能力描述类型struct v4l2_capability:
struct v4l2_capability {
__u8 driver[16]; /* i.e. "bttv" */ //驱动名称,
__u8 card[32]; /* i.e. "Hauppauge WinTV" */ //
__u8 bus_info[32]; /* "PCI:" + pci_name(pci_dev) */ //PCI总线信息
__u32 version; /* should use KERNEL_VERSION() */
__u32 capabilities; /* Device capabilities */ //设备能力
__u32 reserved[4];
};
返回值说明: 执行成功时,函数返回值为 0;
函数执行成功后,struct v4l2_capability 结构体变量中的返回当前视频设备所支持的功能;例如支持视频捕获功能V4L2_CAP_VIDEO_CAPTURE、 V4L2_CAP_STREAMING等。
使用举例:
/* 获取驱动信息:获取设备具有的能力 */
int v4l2_querycap(int fd, struct v4l2_capability* cap)
{
if (ioctl(fd, VIDIOC_QUERYCAP, cap) < 0)
{
printf("ERR(%s):VIDIOC_QUERYCAP failed\n", __func__);
return -1;
}
return 0;
}
执行完VIDIOC_QUERYCAP命令后,cap变量中包含了该视频设备的能力信息:其中最重要的是capabilities字段,这个字段标记着V4L2设备的功能,capabilities有以下部分标记位:
ID | 描述符 |
---|---|
V4L2_CAP_VIDEO_CAPTURE | 设备支持捕获功能 |
V4L2_CAP_VIDEO_OUTPUT | 设备支持输出功能 |
V4L2_CAP_VIDEO_OVERLAY | 设备支持预览功能 |
V4L2_CAP_STREAMING | 设备支持流读写 |
V4L2_CAP_READWRITE | 设备支持read、write方式读写 |
程序中通过检查cap中的设备能力信息来判断设备是否支持某项功能。
// 查询设备驱动的功能
ret = v4l2_querycap(fd, &cap);
if(ret < 0)
goto err;
printf("Driver Name:%s\n Card Name:%s\n Bus info:%s\n\n",cap.driver,cap.card,cap.bus_info);
if(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)
printf("dev support capture\n");
if(cap.capabilities & V4L2_CAP_VIDEO_OUTPUT)
printf("dev support output\n");
if(cap.capabilities & V4L2_CAP_VIDEO_OVERLAY)
printf("dev support overlay\n");
if(cap.capabilities & V4L2_CAP_STREAMING)
printf("dev support streaming\n");
if(cap.capabilities & V4L2_CAP_READWRITE)
printf("dev support read write\n");
这里我们需要检查一下是否是为视频采集设备V4L2_CAP_VIDEO_CAPTURE以及是否支持流IO操作V4L2_CAP_STREAMING。
2.2 应用优先级
当多个应用程序共享设备时,可能需要为它们分配不同的优先级。视频录制应用程序可以例如阻止其他应用程序改变视频控制或切换当前的电视频道。
另一个目标是允许在后台工作的低优先级应用程序,这些应用程序可以被用户控制的应用程序抢占,并在以后自动重新获得对设备的控制
ID | 描述 |
---|---|
VIDIOC_G_PRIORITY | 获取优先级 |
VIDIOC_S_PRIORITY | 设置优先级 |
2.3 输入和输出设备
ID | 描述 |
---|---|
VIDIOC_ENUMINPUT | 枚举视频输入设备 |
VIDIOC_G_INPUT | 获取当前的视频输入设备 |
VIDIOC_S_INPUT | 设置视频输入设备 |
VIDIOC_ENUMOUTPUT | 枚举视频输出设备 |
VIDIOC_G_OUTPUT | 获取当前视频输出设备 |
VIDIOC_S_OUTPUT | 设置视频输出设备 |
VIDIOC_ENUMAUDIO | 枚举音频输入设备 |
VIDIOC_G_AUDIO | 获取当前音频输入设备 |
VIDIOC_S_AUDIO | 设置音频输入设备 |
VIDIOC_ENUMAUDOUT | 枚举音频输出设备 |
VIDIOC_G_OUTPUT | 获取音频输出设备 |
VIDIOC_S_AUDOUT | 设置音频输出设备 |
2.3.1 VIDIOC_ENUMINPUT
一个设备可能有多个输入,比如:在芯片上,摄像头控制器和摄像头接口是分离的,需要选择哪一个摄像头接口作为摄像头控制器的输入源。
当然,并不是所有的设备都需要设置输入,比如:uvc摄像头,一般只有一个输入,默认就会选择,不需要设置。
视频捕获的应用首先要通过VIDIOC_ENUMINPUT命令来枚举所有可用的输入。在V4L2层,这个调用会转换成调用一个驱动中对应的回调函数:
int (*vidioc_enum_input)(struct file *file, void *private_data, struct v4l2_input *input);
在这个调用中,file 对就的是打开的视频设备。private_data是驱动的私有字段,input字段是真正的传递的信息:
struct v4l2_input {
__u32 index; /* Which input */
__u8 name[32]; /* Label */
__u32 type; /* Type of input */
__u32 audioset; /* Associated audios (bitfield) */
__u32 tuner; /* Associated tuner */
v4l2_std_id std;
__u32 status;
__u32 reserved[4];
};
它有如下几个值得关注的字段:
- index:应用关注的输入的索引号;这是唯一一个用户空间设定的字段。驱动要分配索引号给输入,从0开始,依次往上增加。应用想要知道所有可用的输入时,要调用VIDIOC_ENUMINPUT 控制,调用索引号从0开始,并开始递增。 一旦返回EINVAL,应用就知道,输入己经遍历结束了,只要有输入,输入索引号0就一定要存在的;
- name::输入的名字,由驱动设定。简单起见,可以设为”Camera”,诸如此类;如果卡上有多个输入,名称就要与接口的打印相符合;
- type:输入的类型,现在只有两个值可选:V4L2_INPUT_TYPE_TUNER和V4L2_INPUT_TYPE_CAMERA;
- audioset:描述哪个音频输入可以与些视频输入相关联音频输入与视频输入一样通过索引号枚举 ,但并非所以的音频与视频的组合都是可用的,这个字段是一个掩码,代表对于当前枚举出的视频而言,哪些音频输入是可以与 之关联的.如果没有音频输入可以与之关联,或是只有一个可选,那么就可以简单地把这个字段置0;
- tuner: 如果输入是一个调谐器 (type字段置为V4L2_INPUT_TYPE_TUNER),,这个字段就是会包含一个相应的调谐设备的索引号;
- std: 描述设备支持哪个或哪些视频标准.;
- status::给出输入的状态.,简而言之,status中设置的每一位都代表一个问题。这些问题包括没有电源,没有信号,没有同频锁等;
- reserved:保留字段,驱动应该将其置0。
通常驱动会设置上面所以的字段,并返回0。如果索引值超出支持的输入范围,应该返回-EINVAL,这个调用里可能出现的错误不多。
应用程序使用举例:
int v4l2_enuminput(int fd, int index, char* name)
{
struct v4l2_input input;
int found = 0;
input.index = 0;
while(!ioctl(fd, VIDIOC_ENUMINPUT, &input))
{
printf("input:%s : std: 0x%08x\n", input.name, input.std);
if(input.index == index)
{
found = 1;
strcpy(name, input.name);
}
++input.index;
}
if(!found)
{
printf("%s:can't find input dev\n", __func__);
return -1;
}
return 0;
}
执行完该函数,就可以获取输入的名字:
ret = v4l2_enuminput(fd, 0, name);
if(ret < 0)
goto err;
printf("input device name:%s\n", name);
2.3.2 VIDIOC_S_INPUT
当应用想改变当前的输入时,驱动会收到一个对回调函数vidioc_s_input()的调用。
int (*vidioc_s_input) (struct file *file, void *private_data, unsigned int index);
其中index用来确定那个输入是应用想要的,驱动要对硬件进行设置,选择那个输入并返回0。也有可能要返回-EINVAL(索引号不正确时) 或-EIO(硬件有问题).,即使只有一路输入,驱动也要实现这个回调函数。
应用程序使用举例:
int v4l2_s_input(int fd, int index)
{
struct v4l2_input input;
// 指定输入编号
input.index = index;
if (ioctl(fd, VIDIOC_S_INPUT, &input) < 0)
{
printf("ERR(%s):VIDIOC_S_INPUT failed\n", __func__);
return -1;
}
return 0;
}
确定了输入编号后,执行如下代码,设定输入为0:
ret = v4l2_s_input(fd, 0);
if(ret < 0)
goto err;
2.4 视频标准
ID | 描述 |
---|---|
VIDIOC_ENUMSTD | 枚举设备支持的所有标准 |
VIDIOC_G_STD | 获取当前正在使用的标准 |
VIDIOC_S_STD | 设置视频标准 |
VIDIOC_QUERYSTD | 有的设备支持自动侦测输入源的视频标准,此ioctl获取检测到的标准 |
2.5 控制属性
ID | 描述 |
---|---|
VIDIOC_QUERYCTRL | 查询指定的control详细信息 |
VIDIOC_QUERYMENU | 查询menu |
VIDIOC_G_CTRL | 获取设备指定control的当前信息 |
VIDIOC_S_CTRL | 设置设备指定的control |
VIDIOC_STREAMON
|
开始视频采集 |
VIDIOC_STREAMOFF
|
结束视频采集 |
2.5.1 VIDIOC_STREAMON
VIDIOC_STREAMON用于启动视频采集命令,应用程序调用VIDIOC_STREAMON启动视频采集命令后,视频设备驱动程序开始采集视频数据,并把采集到的视频数据保存到视频驱动的视频缓冲区中。
参数类型为V4L2的视频缓冲区类型 enum v4l2_buf_type ;
enum v4l2_buf_type {
V4L2_BUF_TYPE_VIDEO_CAPTURE = 1,
V4L2_BUF_TYPE_VIDEO_OUTPUT = 2,
V4L2_BUF_TYPE_VIDEO_OVERLAY = 3,
V4L2_BUF_TYPE_VBI_CAPTURE = 4,
V4L2_BUF_TYPE_VBI_OUTPUT = 5,
V4L2_BUF_TYPE_SLICED_VBI_CAPTURE = 6,
V4L2_BUF_TYPE_SLICED_VBI_OUTPUT = 7,
#if 1
/* Experimental */
V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8,
#endif
V4L2_BUF_TYPE_PRIVATE = 0x80,
};
返回值说明: 执行成功时,函数返回值为 0;函数执行成功后,视频设备驱动程序开始采集视频数据,此时应用程序一般通过调用select函数来判断一帧视频数据是否采集完成。当视频设备驱动完成一帧视频数据采集并保存到视频缓冲区中时,select函数返回,应用程序接着可以读取视频数据;否则select函数阻塞直到视频数据采集完成。
应用程序使用举例:
/* 开始采集 */
int v4l2_streamon(int fd)
{
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
// 启动视频采集命令,应用程序调用VIDIOC_STREAMON启动视频采集命令后,视频设备驱动程序开始采集视频数据,
// 并把采集到的视频数据保存到视频驱动的视频缓冲区中
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0)
{
printf("ERR(%s):VIDIOC_STREAMON failed\n", __func__);
return -1;
}
// 视频设备驱动程序开始采集视频数据,此时应用程序一般通过调用select函数来判断一帧视频数据是否采集完成
// 当视频设备驱动完成一帧视频数据采集并保存到视频缓冲区中时,select函数返回,应用程序接着可以读取视频数据
// 否则select函数阻塞直到视频数据采集完成
if(v4l2_poll(fd) < 0)
return -1;
return 0;
}
/* 等待一帧数据采集完成 */
int v4l2_poll(int fd)
{
int ret;
struct pollfd poll_fds[1];
poll_fds[0].fd = fd;
poll_fds[0].events = POLLIN;
// 等待一帧数据采集完成
ret = poll(poll_fds, 1, 10000);
if (ret < 0)
{
printf("ERR(%s):poll error\n", __func__);
return -1;
}
if (ret == 0)
{
printf("ERR(%s):No data in 10 secs..\n", __func__);
return -1;
}
return 0;
}
当视频缓冲都如队列后,执行如下代码,开始视频图像采集:
ret = v4l2_streamon(fd);
if(ret < 0)
goto err;
2.5.2 VIDIOC_STREAMOFF
/* 停止视频采集 */
int v4l2_streamoff(int fd)
{
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
// 停止视频采集命令,应用程序调用VIDIOC_ STREAMOFF停止视频采集命令后,视频设备驱动程序不在采集视频数据
if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0)
{
printf("ERR(%s):VIDIOC_STREAMOFF failed\n", __func__);
return -1;
}
return 0;
}
完成视频采集后,一般执行如下代码,停止视频采集工作:
ret = v4l2_streamoff(fd);
if(ret < 0)
goto err;
2.6 图像格式
图像由多种格式YUV和RGB还有压缩格式等等,其中每种格式又分有多种格式,比如RGB:RGB565、RGB888…
所以在使用设备时,需要对格式进行设置
ID | 描述 |
---|---|
VIDIOC_ENUM_FMT | 枚举设备支持的图像格式 |
VIDIOC_G_FMT | 获取当前设备的图像格式 |
VIDIOC_S_FMT | 设置图像格式 |
VIDIOC_TRY_FMT | 测试设备是否支持此格式 |
2.6.1 VIDIOC_ENUM_FMT
VIDIOC_ENUM_FMT用于 获取当前视频设备支持的视频格式 。参数类型为V4L2的视频格式描述符类型 struct v4l2_fmtdesc:
struct v4l2_fmtdesc {
__u32 index; /* Format number */
enum v4l2_buf_type type; /* buffer type */
__u32 flags;
__u8 description[32]; /* Description string */
__u32 pixelformat; /* Format fourcc */
__u32 reserved[4];
};
返回值说明: 执行成功时,函数返回值为0;struct v4l2_fmtdesc 结构体中的.pixelformat和 description 成员返回当前视频设备所支持的视频格式;这里很关键,因为不同的摄像头可能支持的格式不一样。V4L2可以支持的格式很多,/usr/include/linux/videodev2.h文件中可以看到,比如:
V4L2_PIX_FMT_RGB565
V4L2_PIX_FMT_RGB32
V4L2_PIX_FMT_YUYV
V4L2_PIX_FMT_UYVY
V4L2_PIX_FMT_VYUY
V4L2_PIX_FMT_YVYU
V4L2_PIX_FMT_YUV422P
V4L2_PIX_FMT_NV12
V4L2_PIX_FMT_NV12T
V4L2_PIX_FMT_NV21
V4L2_PIX_FMT_NV16
V4L2_PIX_FMT_NV61
V4L2_PIX_FMT_YUV420
V4L2_PIX_FMT_JPEG
应用程序使用举例:
int v4l2_enum_fmt(int fd,unsigned int fmt,enum v4l2_buf_type type)
{
struct v4l2_fmtdesc fmt_desc;
int found = 0;
memset(&fmt_desc, 0, sizeof(fmt_desc));
fmt_desc.type = type;
fmt_desc.index = 0;
while (!ioctl(fd, VIDIOC_ENUM_FMT, &fmt_desc))
{
printf("pixelformat = %c%c%c%c, description = %s\n",
fmt_desc.pixelformat & 0xFF, (fmt_desc.pixelformat >> 8) & 0xFF, (fmt_desc.pixelformat >> 16) & 0xFF,
(fmt_desc.pixelformat >> 24) & 0xFF, fmt_desc.description);
if (fmt_desc.pixelformat == fmt)
{
found = 1;
}
fmt_desc.index++;
}
if (!found)
{
printf("%s:unsupported pixel format\n", __func__);
return -1;
}
return 0;
}
我采用的usb摄像头只支持V4L2_PIX_FMT_YUYV,一般的USB摄像头都会支持YUYV,有些还支持其他的格式。这里我们判断一下我们的摄像头是否支持V4L2_PIX_FMT_YUYV:
ret = v4l2_enum_fmt(fd,V4L2_PIX_FMT_YUYV, V4L2_BUF_TYPE_VIDEO_CAPTURE);
if(ret < 0)
goto err;
2.6.2 VIDIOC_S_FMT
既然我使用的摄像头只支持V4L2_PIX_FMT_YUYV,因此我们需要通过命令VIDIOC_S_FMT设置图像格式,参数类型为V4L2的视频数据格式类型 struct v4l2_format:
struct v4l2_format {
enum v4l2_buf_type type; //数据流类型,必须永远是V4L2_BUF_TYPE_VIDEO_CAPTURE
union {
struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
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 */
__u8 raw_data[200]; /* user-defined */
} fmt;
};
v4l2_format中的fmt是一个union,其中哪个成员有效取决于type的取值,一般较常用的是取类型type为 V4L2_BUF_TYPE_VIDEO_CAPTURE,此时pix生效。该成员的详细内部细节如下:
struct v4l2_pix_format {
__u32 width; // 宽,必须是16的倍数
__u32 height; // 高,必须是16的倍数
__u32 pixelformat; // 视频数据存储类型,例如是YUV4:2:2还是RGB
enum v4l2_field field;
__u32 bytesperline;
__u32 sizeimage;
enum v4l2_colorspace colorspace;
__u32 priv;
};
这里我们需要