Linux V4L2应用编程常用结构体介绍
本文将会介绍V4L2中常用的结构体,包括主要成员的介绍和使用例程。读者需要注意的是,除了关注结构体的介绍外,还需要着重关注结构体在用于ioctl
函数时对应的参数。
1. v4l2_capability
该结构体用于获得设备所支持的能力,其结构体成员如下:
struct v4l2_capability {__u8 driver[16]; // 驱动名称__u8 card[32]; // 设备名称__u8 bus_info[32]; // 设备总线信息__u32 version; // 驱动版本号__u32 capabilities; // 设备支持的功能__u32 device_caps; // 设备特定的功能__u32 reserved[3]; // 保留字段
};
capabilities
:表示设备支持的功能,例如V4L2_BUF_TYPE_VIDEO_CAPTURE
表示支持视频捕获,V4L2_CAP_STREAMING
表示支持流式传输
该结构体常用于摄像头应用程序的开头,以确定后续操作的支持情况。
该结构体的使用例程为:
struct v4l2_capability cap;
memset(&cap,0,sizeof(struct v4l2_capability));
ret = ioctl(fd,VIDIOC_QUERYCAP,&cap); //使用VIDIOC_QUERYCAP查询设备支持的能力
if(ret != 0) //如果失败则打印错误信息并退出
{printf("capability query failed!\n");return -1;
}
//判断是否支持指定的功能
if(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) //用于确保该设备是个捕获设备printf("Video capture supported\n");
if(cap.capabilities & V4L2_CAP_STREAMING) //用于确保可以实现mmap内存映射printf("Streaming supported\n");
2. v4l2_fmtdesc
该结构体用于描述视频设备支持的图像格式,其成员如下:
struct v4l2_fmtdesc {__u32 index; __u32 type; __u32 flags;__u8 description[32]; /*格式的描述字符串,一般为人类可读的格式名称*/__u32 pixelformat; /*格式的四字符代码,难以阅读*/__u32 reserved[4];
};
index
:用于查询指定格式的索引,一般用于for循环遍历,从0开始递增,直到没有更多格式返回。type
:指定缓冲区的类型,这些类型保存在v4l2_buf_type
枚举中,例如V4L2_BUF_TYPE_VIDEO_CAPTURE
表示这是一个捕获设备pixelformat
:像素格式的四字符代码,例如V4L2_PIX_FMT_RGB332
。可以使用ioctl
函数获得(基于index
)
下面给出该结构体的使用例程:
struct v4l2_fmtdesc fmt;
memset(&fmt,0,sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //指定为捕获设备for(fmt.index = 0;;fmt.index++)
{if(ioctl(fd,VIDIOC_ENUM_FMT,&fmt) == -1) //根据index的值获得支持格式的类型,相当于去访问支持格式数组{printf("No more format support.\n");break;}printf("Format %d: %s (0x%08x)\n", fmt.index, fmt.description, fmt.pixelformat); //打印出支持格式的描述和像素格式
}
名称介绍:
像素格式:描述图像中像素的排列方式和颜色编码方式,常见的像素格式包括RGB、YUV以及JEPG等等
缓冲区类型:描述视频数据的储存方式,如单平面缓冲区、多平面缓冲区等。
3. v4l2_frmsizeenum
上面我们学习了如何获得支持的像素格式,除了像素格式,图像参数还有一个重要的参数就是帧大小(即每一帧的大小),也可以称之为分辨率。我们可以使用该结构体来枚举视频设备支持的帧大小(frm_size_enum)。其成员如下:
struct v4l2_frmsizeenum {__u32 index; // 帧大小索引__u32 pixel_format; // 像素格式(FourCC)__u32 type; // 帧大小类型union {struct v4l2_frmsize_discrete discrete; // 离散帧大小,描述了帧大小的宽度和高度struct v4l2_frmsize_stepwise stepwise; // 步进帧大小,描述了帧大小的最小宽度、最大宽度、步长,以及最小高度、最大高度、步长。};__u32 reserved[2]; // 保留字段
};
index
:一个像素格式往往会支持多个帧大小,该成员就是用来遍历所有支持的帧大小,和v4l2_fmtdesc
结构体的index是类似的pixel_format
:像素格式,需要我们手动指定。可以通过v4l2_fmtdesc
结构体获得。只有支持指定像素格式的帧大小才会被枚举。type
:帧大小的类型,由底层返回。例如V4L2_FRMSIZE_TYPE_DISCRETE
表示离散帧大小,V4L2_FRMSIZE_TYPE_STEPWISE
表示步进帧大小。下面的union联合体会根据上面type的不同使用不同的结构体保存结果。
该结构体的使用例程为:
for(fmtdesc.index = 0;;fmtdesc.index++)
{if(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc) != 0){printf("can not get format information.\n");break;}memset(&fsenum,0,sizeof(struct v4l2_frmsizeenum)); // 先清空帧大小描述fsenum.pixel_format = fmtdesc.pixelformat;//一个格式可能支持多种帧大小,所以需要枚举帧大小for(fsenum.index = 0;;fsenum.index++){if(ioctl(fd,VIDIOC_ENUM_FRAMESIZES,&fsenum) != 0) //枚举这种格式所支持的帧大小,帧大小也可以称为分辨率break;printf("format %s,%d,framesize %d: %d x %d\n",fmtdesc.description,fmtdesc.pixelformat,fsenum.index,fsenum.discrete.width,fsenum.discrete.height); //打印帧大小信息}
}
//输出结果示例:
//format YUYV 4:2:2,1448695129,framesize 0: 640 x 480
//format YUYV 4:2:2,1448695129,framesize 1: 320 x 240
//can not get format information.
4. v4l2_format
该结构体用于描述视频设备当前设置的图像格式。需要和上述v4l2_fmtdesc
区分的是,v4l2_fmtdesc
保存的是摄像头设备支持的格式,而v4l2_format
保存的是当前设置的格式(即支持格式中的一个)对应的更加详细的信息。其成员如下:
struct v4l2_format {__u32 type; // 缓冲区类型union {struct v4l2_pix_format pix; // 单平面图像格式struct v4l2_pix_format_mplane pix_mp; // 多平面图像格式struct v4l2_window win; // 视频叠加窗口struct v4l2_vbi_format vbi; // 垂直消隐间隔(VBI)数据格式struct v4l2_sliced_vbi_format sliced; // 切片 VBI 数据格式struct v4l2_sdr_format sdr; // 软件定义无线电(SDR)格式__u8 raw_data[200]; // 原始数据,用于未来扩展} fmt;
};
fmt
:该成员为一个联合体,根据所使用type的不同,使用联合体中不同的子结构体来描述信息
我们可以使用ioctl获得当前格式的详细信息,也可以设置当前格式的信息
ioctl(fd, VIDIOC_G_FMT, &fmt); //获得当前格式的详细信息
ioctl(fd, VIDIOC_S_FMT, &fmt); //设置当前格式的部分信息,需要判断返回值看是否设置成功
下面给出该结构体的使用例程(包括设置视频格式):
//获得当前格式信息
struct v4l2_format currfmt;
memset(&currfmt,0,sizeof(currfmt));
currfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_G_FMT, &currfmt) == -1)
{perror("Failed to get format");close(fd);return 1;
}
/*后面就是打印currfmt中的详细格式信息了,例如分辨率、像素格式等信息*///设置当前格式信息
struct v4l2_format fmt;
memset(&fmt,0,sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1024;
fmt.fmt.pix.height = 768;
fmt.fmt.pix.pixelformat = V4L2_PIX_LMT_MJPEG;
fmt.fmt.pix.field = V4L2_FIELD_ANY;
ret = ioctl(fd, VIDIOC_S_FMT, &fmt);
if(ret != 0)
{printf("format set failed!\n");return -1;
}
需要注意的是当设置的格式或分辨率不被硬件设备支持时,硬件设备就会返回实际设置的分辨率和格式,所以fmt取了址。
5. v4l2_queryctrl
该结构体用于查询视频设备支持的控件信息的结构体,可以获得控件的详细信息,其成员如下:
struct v4l2_queryctrl {__u32 id; // 控件 ID__u32 type; // 控件类型__u8 name[32]; // 控件名称__s32 minimum; // 控件的最小值__s32 maximum; // 控件的最大值__s32 step; // 控件的步长__s32 default_value; // 控件的默认值__u32 flags; // 控件的标志位__u32 reserved[2]; // 保留字段
};
id
:指定要查询的控件,每个控件都有唯一的标识符,例如V4L2_CID_BRIGHTNESS
代表亮度。还有对比度、饱和度、色调等。该成员需要用户手动指定。type
:控件的类型,例如V4L2_CTRL_TYPE_INTEGER
表示整型控件。
下面给出该结构体的使用例程:
struct v4l2_queryctrl queryctrl;
memset(&queryctrl, 0, sizeof(queryctrl));
queryctrl.id = V4L2_CID_BRIGHTNESS; // 查询亮度控件if(ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl) == -1) //使用的命令为VIDIOC_QUERYCTRL
{printf("Brightness control is not supported!\n");return -1;
}
/*查询成功,打印空间的详细信息*/
然而可以看到v4l2_queryctrl
结构体并没有该控件当前的值,例如当前的亮度为多少,只保存了控件的基础信息。
6. v4l2_control
该结构体弥补了上述结构体的不足,可以实现设置或获取视频设备控件值,其成员为:
struct v4l2_control {__u32 id; // 控件 ID__s32 value; // 控件的值
};
我们在使用时,只需要指定控件的id,就可以获得或设置对应控件的值了,例如:
struct v4l2_control control;
memset(&control, 0, sizeof(control));
control.id = V4L2_CID_BRIGHTNESS; // 获取亮度控件的值
//获得当前亮度值
ioctl(fd, VIDIOC_G_CTRL, &control);
//设置当前亮度值
control.value = 50;
ioctl(fd, VIDIOC_S_CTRL, &control);
7. v4l2_requestbuffers
该结构体用于请求视频设备的缓冲区,其定义了要申请缓冲区的数量以及内存的映射方式,这些可以由用户手动指定。而每一个缓冲区的大小主要是由当前摄像头的视频格式(如分辨率和像素格式)所决定的。该结构体的成员如下:
struct v4l2_requestbuffers {__u32 count; // 请求的缓冲区数量__u32 type; // 缓冲区类型__u32 memory; // 内存映射方式__u32 reserved[2]; // 保留字段,通常设置为0
};
count
:请求的缓冲区数量,实际申请的数量可能会由于硬件的限制而小于所设定的数值type
:缓冲区的类型,一般设置为V4L2_BUF_TYPE_VIDEO_CAPTURE
表示捕获设备memory
:内存映射方式。例如V4L2_MEMORY_MMAP
表示使用内存映射方式(mmap),视频缓冲区由内核分配;V4L2_MEMORY_USERPTR
表示使用用户空间指针,视频缓冲区由APP分配,然后给内核使用;V4L2_MEMORY_DMABUF
表示使用DMA缓冲区。一般常使用第一个参数(内存映射方式)
该结构体的使用例程为:
struct v4l2_requestbuffers buffer;memset(&buffer,0,sizeof(struct v4l2_requestbuffers)); //使用前先置零
buffer.count = 32;
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
if(ioctl(fd,VIDIOC_REQBUFS,&buffer) != 0) //使用的命令为VIDIOC_REQBUFS
{printf("request buffer failed!\n");return -1;
}
else printf("real request buffer size is %d\n",buffer.count); //申请成功后,打印实际申请的缓冲区数量
当使用v4l2_requestbuffers
申请缓冲区后,这些缓冲区是在底层的驱动程序中。对于这些申请的缓冲区信息,应用程序应该如何访问呢?
8. v4l2_buffer
上面我提出了应用程序如何访问底层驱动的缓冲区信息的问题,而这里要介绍的v4l2_buffer
就是用于解决上述问题。(注意是信息,而不是数据)
该结构体用于一个具体的视频数据缓冲区。它包含了缓冲区的状态、索引、长度、时间戳等信息。该结构体成员如下:
struct v4l2_buffer {__u32 index; // 缓冲区索引__u32 type; // 缓冲区类型__u32 bytesused; // 缓冲区中实际使用的字节数__u32 flags; // 缓冲区的标志位__u32 field; // 场序(逐行、隔行等)struct timeval timestamp; // 时间戳struct v4l2_timecode timecode; // 时间码__u32 sequence; // 序列号__u32 memory; // 内存映射方式union {__u32 offset; // 内存映射偏移量(V4L2_MEMORY_MMAP)unsigned long userptr; // 用户空间指针(V4L2_MEMORY_USERPTR)struct v4l2_plane *planes; // 多平面缓冲区的平面信息__s32 fd; // DMA 缓冲区文件描述符(V4L2_MEMORY_DMABUF)} m;__u32 length; // 缓冲区的长度__u32 reserved2; // 保留字段__u32 reserved; // 保留字段
};
index
:缓冲区的索引。前面我们使用v4l2_requestbuffers
结构体申请了多个缓冲区,可以理解为一个数组,这个index
就是用于访问对应的”数组“元素。在获得底层缓冲区数据时,该参数也可以由底层返回。即当我们手动设置时是想访问具体缓冲区的信息,而底层返回时是告知哪一个缓冲区有视频数据,可以进行读取了。type
和memory
:和上面的v4l2_requestbuffers
是一样的,这里需要用户手动指明。type
需要和v4l2_requestbuffers
中设置保保持一致。
可以看到v4l2_requestbuffers
和v4l2_buffer
都是和缓冲区有关的结构体,那么它们各自的定位都是什么呢?前者是用于告知底层我要申请多大的缓冲区,然后底层去尽可能申请对应大小的缓冲区;而对于申请的缓冲区,上层应用程序可能想访问缓冲区的信息,此时就需要使用后者来获得对应缓冲区的信息。
下面给出两个该结构体的用例:
访问申请的缓冲区并进行mmap映射,并将缓冲区放入输入队列
//接上面v4l2_requestbuffers的例程
struct v4l2_buffer buf;
void *bufs[32];for(i = 0;i < buffer.count;i++)
{memset(&buf,0,sizeof(struct v4l2_buffer));buf.index = i; //index手动设置时,此时用于获得对应缓冲区索引的信息buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //type和memory是什么情况下都需要用户手动设置的buf.memory = V4L2_MEMORY_MMAP;if(ioctl(fd,VIDIOC_QUERYBUF,&buf) != 0) //此时的命令为VIDIOC_QUERYBUF,即查询buf{printf("can't query buf.index %d info!\n",buf.index);return -1;}bufs[i] = mmap(NULL,buf.length,PROT_READ | PROT_WRITE,MAP_SHARED,fd,buf.m.offset); //mmap映射if(bufs[i] == MAP_FAILED){printf("mmap failed!\n");return -1;}
}/*把buffer放入输入队列,表示该缓冲区可以存放视频数据*/
for(i = 0;i < buffer.count;i++)
{memset(&buf,0,sizeof(struct v4l2_buffer));buf.index = i;buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;if(ioctl(fd,VIDIOC_QBUF,&buf) != 0) //使用的命令为VIDIOC_QBUF,表示将缓冲区加入队列,等待设备填充数据{printf("failed put buffer to queue\n");return -1;}
}
从输出队列获得获得缓冲区信息并通过mmap的数组获得图像数据
/*该程序接上面的程序,以下一般嵌套在一个查询函数中,例如poll,当有数据时执行下面的代码*/
memset(&buf,0,sizeof(struct v4l2_buffer));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
//这里并没有手动设置index,而是由底层返回
//从输出队列获得保存视频数据的缓冲区信息
//这里buf用于获得图像数据对应的缓冲区索引和图像数据的大小等信息,其本身并不包含图像数据
if(ioctl(fd,VIDIOC_DQBUF,&buf) != 0) //此时的命令为VIDIOC_DQBUF,从队列中取出缓冲区
{printf("dequeue buf failed!\n");return -1;
}
sprintf(filename,"./picfile/video_raw_data%04d.jpg",file_cnt++); //创建一个文件用于保存图像数据
fd_file = open(filename,O_RDWR | O_CREAT,0666);
if(fd_file < 0)
{printf("%s create failed!\n",filename);return -1;
}
//根据索引获得对应缓冲区的数据
write(fd_file,bufs[buf.index],buf.bytesused); //bytesused表示一帧数据有多大
close(fd_file);/*把buffer再次放入队列*/
if(ioctl(fd,VIDIOC_QBUF,&buf) != 0)
{printf("failed put buffer to queue\n");return -1;
}
在上面的例程中,可以看到引入了队列的概念,包括输入队列和输出队列。首先,需要明白的是这里的队列是不完全同于C++中的queue
,C++的队列保存的是数据,而V4L2中的队列保存的是状态!相同的是二者都是FIFO(先入先出)的逻辑。
我们可以从队列保存的结构体元素,即v4l2_buffer
来理解。前面已经强调了这个结构体保存的是缓冲区的信息,包括缓冲区的索引(关键)、类型以及映射方式等信息,而并不是数据。当将缓冲区的信息放入输入队列时,此时表示这个缓冲区可以用于存储视频数据;当缓冲区填充完数据后,其信息就会被转移到输出队列,表示可以去读对应缓冲区的信息了。简单来说就是通过使用输入、输出队列来判断哪个缓冲区可以写数据,哪个缓冲区可以读数据。
上面的描述还是有些抽象,下面我用一个例子来介绍一下,为了方便介绍,这里我仅假设队列中保存的是缓冲区的索引(实际上还有其他更多信息)。
例如我们申请了一个大小为4的缓冲区,一开始我们将所有的缓冲区放入输入队列,此时两个队列的信息为:
input_queue = {0,1,2,3}; //输入队列,保存可以可用缓冲区的索引(这里简化了)
output_queue = {}; //输出队列,一开始啥也没有
当摄像头有视频数据传来时,此时就会从输入队列中取出最先进来的元素,即索引0。此时底层知道了可以去第0个缓冲区写入视频数据,并在相应位置完成数据的写入。随后,由于缓冲区0已经被写入了数据,此时将索引0放入输出队列,表示可以去第0和缓冲区读取视频数据了。
input_queue = {1,2,3}; //输入队列
output_queue = {0}; //输出队列,保存了缓冲区0的索引
之后应用程序通过VIDIOC_DQBUF
指令从输出队列中获得了索引0(还有其他更多信息),此时应用程序就知道可以去第0个缓冲区读取视频数据
input_queue = {1,2,3}; //输入队列
output_queue = {}; //输出队列,由于应用程序已经将0给取出,此时输出队列又为空了
应用程序处理完第0个缓冲区后,此时第0个缓冲区又可以用于保存新的视频数据了,所以我们还需要再将该缓冲区的索引放入输入队列
input_queue = {1,2,3,0}; //输入队列,由于先入先出,此时0位于列的尾部
output_queue = {}; //输出队列
除了上面的例子外,还可以通过下面这个更贴近生活、生动形象的例子来再理解一下: