最近想做一个视频推流拉流的小项目,需要使用V4L2驱动框架,先来学习学习
Video for Linuxtwo(Video4Linux2)简称V4L2,是V4L的改进版。V4L2是linux操作系统下用于采集图片、视频和音频数据的API接口,配合适当的视频采集设备和相应的驱动程序,可以实现图片、视频、音频等的采集。在远程会议、可视电话、视频监控系统和嵌入式多媒体终端中都有广泛的应用。
在Linux下,所有外设都被看成一种特殊的文件,成为“设备文件”,可以象访问普通文件一样对其进行读写。一般来说,采用V4L2驱动的摄像头设备文件是/dev/video0。V4L2支持两种方式来采集图像:内存映射方式(mmap)和直接读取方式(read)。V4L2在include/linux/videodev2.h文件中定义了一些重要的数据结构,在采集图像的过程中,就是通过对这些数据的操作来获得最终的图像数据。Linux系统V4L2的能力可在Linux内核编译阶段配置,默认情况下都有此开发接口。
一、工作流程
1、打开设备
int fd = open(“/dev/video0”, O_RDWR);
2、取得设备的属性信息,查看设备具有什么功能,比如是否具有视频输入,或者音频输入输出等
struct v4l2_capability VIDIOC_QUERYCAP
3、选择视频输入,一个视频设备可以有多个视频输入
struct v4l2_input VIDIOC_S_INPUT
4、设置视频的制式和帧格式,制式包括PAL,NTSC,帧的格式包括宽度和高度等
VIDIOC_S_STD, VIDIOC_S_FMT
struct v4l2_std_id struct v4l2_format
5、申请帧缓冲区
VIDIOC_REQBUFS
struct v4l2_requestbuffers
6、将申请到的内核空间帧缓冲区映射到用户空间
VIDIOC_QUERYBUF
struct v4l2_buffer
7、将申请到的缓冲区进入视频采集输入队列
VIDIOC_QBUF
struct v4l2_buffer
8、开始视频采集
VIDIOC_STREAMON
enum v4l2_buf_type
9、取出视频输出队列的帧缓冲数据,对数据处理
VIDIOC_DQBUF
struct v4l2_buffer
10、将帧缓冲区重新进入视频采集输入队列,这样便可循环采集帧数据
VIDIOC_QBUF
11、停止视频采集,解除映射
VIDIOC_STREAMOFF
12、关闭视频设备
close(fd)
二、V4L2API与数据结构
1、相关结构体
struct v4l2_requestbuffers //申请帧缓冲,对应命令VIDIOC_REQBUFS
struct v4l2_capability //视频设备的功能,对应命令VIDIOC_QUERYCAP
struct v4l2_input //视频输入信息,对应命令VIDIOC_ENUMINPUT
struct v4l2_standard //视频的制式,比如PAL,NTSC,对应命令VIDIOC_ENUMSTD
struct v4l2_format //帧的格式,对应命令VIDIOC_G_FMT、VIDIOC_S_FMT等
struct v4l2_buffer //驱动中的一帧图像缓存,对应命令VIDIOC_QUERYBUF
struct v4l2_crop //视频信号矩形边框
v4l2_std_id //视频制式
2、相关IOCTL接口命令
VIDIOC_REQBUFS //分配内存
VIDIOC_QUERYBUF //把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址
VIDIOC_QUERYCAP //查询驱动功能
VIDIOC_ENUM_FMT //获取当前驱动支持的视频格式
VIDIOC_S_FMT //设置当前驱动的频捕获格式
VIDIOC_G_FMT //读取当前驱动的频捕获格式
VIDIOC_TRY_FMT //验证当前驱动的显示格式
VIDIOC_CROPCAP //查询驱动的修剪能力
VIDIOC_S_CROP //设置视频信号的矩形边框
VIDIOC_G_CROP //读取视频信号的矩形边框
VIDIOC_QBUF //把数据从缓存中读取出来
VIDIOC_DQBUF //把数据放回缓存队列
VIDIOC_STREAMON //开始视频显示函数
VIDIOC_STREAMOFF //结束视频显示函数
VIDIOC_QUERYSTD //检查当前视频设备支持的标准,例如PAL或NTSC。
三、V4L2编程
一、对摄像头的相关配置
1. 设备的打开和关闭:
int fd = open(“/dev/video0”, O_RDWR);
close(fd);
2、查询设备属性:VIDIOC_QUERYCAP
相关函数
int ioctl(int fd, int request, struct v4l2_capability *argp);
相关结构体
structv4l2_capability
{
__u8 driver[16]; // 驱动名字
__u8 card[32]; // 设备名字
__u8bus_info[32]; // 设备在系统中的位置
__u32 version; // 驱动版本号
__u32capabilities; // 设备支持的操作
__u32reserved[4]; // 保留字段
};
capabilities 常用值:
V4L2_CAP_VIDEO_CAPTURE // 是否支持图像获取
capabilities域是一个位掩码用来描述驱动能做的不同的事情:
/* 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_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_READWRITE 0x01000000 /* read/write systemcalls */
#define V4L2_CAP_ASYNCIO 0x02000000 /* async I/O */
#define V4L2_CAP_STREAMING 0x04000000 /* streaming I/O ioctls */
查看设备信息
ret = ioctl(fd, VIDIOC_QUERYCAP, &argp);
if(ret == -1){
perror("ioctl");
exit(EXIT_FAILURE);
}
printf("driver_name:%s\ncard :%s\nbus_info:%s\nversion:%u\n",
argp.driver, argp.card, argp.bus_info, argp.version);
3、选择视频输入
相关结构体
struct v4l2_input {
__u32 index; /* Which input */
__u8 name[32]; /* Label */
__u32 type; /* Type of input */
__u32 audioset; /* Associated audios (bitfield) */
__u32 tuner; /* enum v4l2_tuner_type */
v4l2_std_id std;
__u32 status;
__u32 capabilities;
__u32 reserved[3];
};
index: 应用关注的输入的索引号; 这是惟一一个用户空间设定的字段。驱动要分配索引号给输入,从0开始,依次往上增加。应用想要知道所有可用的输入时,要调用VIDIOC_ENUMINPUT 控制,调用索引号从0开始,并开始递增。 一旦返回EINVAL,应用就知道,输入己经遍历结束了,只要有输入,输入索引号0就一定要存在的。
name[32]: /* 输入的名字,由驱动设定 */
type: 输入的类型 目前有两个值可选:V4L2_INPUT_TYPE_TUNER 和 V4L2_INPUT_TYPE_CAMERA
audioset: 描述那个音频输入可以与此视频输入相关联,音频输入与视频输入一样通过索引号枚举,但并非所有的音频和视频的组合都是可用的,这个字段是一个掩码,代表对于当前枚举出的视频而言,那些音频输入是可以与之关联的,如果没有音频输入可以与之关联,或者只有一个可选,那么就可以简单地把这个字段置为0
tuner: 如果输入是一个调谐器(type字段置为V4L2_INPUT_TYPE_TUNER),这个字段就是会包含一个相应的调谐设备的索引号
std: 描述设备支持哪个或哪些视频标准
status: 给出输入的状态,完整的标识符集可以再V4L2的文档中找到;简而言之,status中设置的每一位都代表一个问题,这些问题包括没有电源,没有信号,没有同频锁 等等
capabilities 设备支持的操作
选择视频输入 相关配置
struct v4l2_input input;
memset(&input, 0, sizeof(struct v4l2_input));
input.index = 0;
ret = ioctl(fd, VIDIOC_S_INPUT, &input);
if(ret == -1){
printf("VIDIOC_S_INPUT is error! LINE:%d\n",__LINE__);
exit(EXIT_FAILURE);
}
4、设置与查看帧格式
1、查看硬件所支持的格式
相关结构体
struct v4l2_fmtdesc
{
__u32 index; // 要查询的格式序号,应用程序设置
enum v4l2_buf_type type; // 帧类型,应用程序设置
__u32 flags; // 是否为压缩格式
__u8 description[32]; // 格式名称
__u32 pixelformat; // 格式
__u32 reserved[4]; // 保留
};
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,
V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8,
V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9,
V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE = 10,
V4L2_BUF_TYPE_SDR_CAPTURE = 11,
V4L2_BUF_TYPE_SDR_OUTPUT = 12,
V4L2_BUF_TYPE_META_CAPTURE = 13,
V4L2_BUF_TYPE_META_OUTPUT = 14,
/* Deprecated, do not use */
V4L2_BUF_TYPE_PRIVATE = 0x80,
};
VIDIOC_ENUM_FMT 命令枚举出所有支持的格式
在存储器中表示图像有很多种方法,市场几乎找不到可以处理所有V4l2所理解的视频格式的设备。驱动不应支持底层硬件不懂的视频格式。实际上在内核中进行格式转换是令人难以接受的。所以驱动必须可以应用选择一个硬件可以支持的格式。第一步就是简单的允许应用查询硬件所支持的格式。VIDIC_EMUM_FMT 就是为此目的而提供的。在驱动内部这个调用会转换为这样一个回调函数
int (vidioc_enum_fmt_cap)(struct file file, void private_data, struct v4l2_fmtdesc f);
这个回调函数要求视频捕捉设备描述其支持的格。应用会传递一个v4l2_fmdesc结构体
应用会设置index和type字段。index是用来区别格式的一个整形数;与V4l2所使用的其他索引一样,这个也是从0开始递增至最大允许值为止,应用可以通过一直递增索引值index直到返回EINVAL的方式枚举所有支持的格式。
type字段描述的是数据流类型;对于视频捕捉设备来说(摄像头)
就是V4L2_BUF_TYPE_VIDEO_CAPTURE
上述的回掉函数只作用于视频捕获设备;只有当type 字段的是值是 V4L2_BUF_TYPE_VIDEO_CAPTURE时才会调用。
查看当前摄像头支持的格式
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("fm:\n");
while(ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) != -1){
printf("%d.%s %c%c%c%c\n", fmtdesc.index + 1, fmtdesc.description,
fmtdesc.pixelformat & 0xFF,
(fmtdesc.pixelformat >> 8) & 0xFF,
(fmtdesc.pixelformat >> 16) & 0xFF,
(fmtdesc.pixelformat >> 24) & 0xFF);
fmtdesc.index++;
}
关于pixelformat的值 >>8 >> 16 >> 24 的原因
在头文件#include<linux/videodev2.h>中有定义
#define v4l2_fourcc(a, b, c, d)
((__u32)(a) | ((__u32)(b) << 8) | ((__u32)© << 16) | ((__u32)(d) << 24))
例如YUYV
#define V4L2_PIX_FMT_YUYV v4l2_fourcc(‘Y’, ‘U’, ‘Y’, ‘V’) /* 16 YUV 4:2:2
例如OV5640所支持的格式
2、查看或设置当前硬件的图像配置
应用可以通过调用VIDIOC_G_FMT知道硬件现在的配置如何。这种情况下传递的参数是一个v4l2_format 结构体:
struct v4l2_format
{
enum v4l2_buf_type type; //缓冲区的类型
union
{
struct v4l2_pix_format pix; //视频设备使用
struct v4l2_window win;
struct v4l2_vbi_format vbi;
struct v4l2_sliced_vbi_format sliced;
__u8 raw_data[200];
} fmt;
};
type描述的是缓冲区的类型;
对于视频捕获(和输出)设备,联合体中pix字段使我们关注的重点。
structv4l2_pix_format
{
__u32 width; // 帧宽,单位像素
__u32 height; // 帧高,单位像素
__u32 pixelformat; // 帧格式 rgb565 rgb888 yuv422 yuv420等等 相关参数在#include<linux/videodev2.h>
enum v4l2_field; //场格式
__u32 bytesperline; //表明缓冲区中有多少个字节用于表示图像中一行像素的所有像素值
//由于一个像素可能有多个字节表示,所以bytesperline可能是width值得若干倍
__u32 sizeimage; //图像大小
enum v4l2_colorspace colorspace; //色彩空间
__u32 priv;
};
驱动会用当前硬件的设置信息填充这个结构体并返回,这个调用通常不会失败,除非是硬件出现了非常严重的问题
enum v4l2_field {
V4L2_FIELD_ANY = 0,
/* driver can choose from none,
top, bottom, interlaced
depending on whatever it thinks
is approximate ...
V4L2_FIELD_NONE = 1, /* this device has no fields ...
V4L2_FIELD_TOP = 2, /* top field only
V4L2_FIELD_BOTTOM = 3, /* bottom field only
V4L2_FIELD_INTERLACED = 4, /* both fields interlaced
V4L2_FIELD_SEQ_TB = 5, /* both fields sequential into one
buffer, top-bottom order
V4L2_FIELD_SEQ_BT = 6, /* same as above + bottom-top order
V4L2_FIELD_ALTERNATE = 7, /* both fields alternating into
separate buffers
V4L2_FIELD_INTERLACED_TB = 8, /* both fields interlaced, top field
first and the top field is
transmitted first
V4L2_FIELD_INTERLACED_BT = 9, /* both fields interlaced, top field
first and the bottom field is
transmitted first
};
3、设置硬件的帧格式
这里设置帧格式有两个命令 VIDIOC_TRY_FMT和VIDIOC_S_FMT
多数应用都想最终对硬件进行配置以使其为应用提供一种符合其目的的格式。
改变视频格式有两个接口。第一个是VIDIOC_TRY_FMT调用,他再V4L2驱动中的转换为下面的回调函数: int (*vidioc_try_fmt_cap)(struct file *file, void *private_data,
struct v4l2_format *f);
int (*vidioc_try_fmt_video_output)(struct file *file, void *private_data,
struct v4l2_format f);
/ And so on for the other buffer types */
要处理这个调用,驱动会查看请求的视频格式,然后断定硬件是否支持这个格式。如果应用请求的格式是硬件不能支持的,就返回-EINVAL。
查看当前设备是否支持请求的帧格式
所以,例如,一个描述了一个不支持的fourcc编码或者请求了一个隔行扫描的视频,而设备只支持逐行扫描的就会失败。在另一方面,驱动可以调整size字段,以与硬件支持的图像大小相适应。普遍的做法是将大小调小。所以一个只能处理VGA分辨率的设备驱动会根据情况相应的调整width和height参数而成功返回。 v4l2_format 结构体会在调用后复制给用户空间;驱动应该更新这个结构体以反映改变的参数,这样应用才知道他真正得到的是什么。
VIDIOC_TRY_FMT这个处理对于驱动来说是可选的,但是不推荐忽略这个功能。如果提供了的话,这个函数可以再任何时候调用,甚至是设备工作的时候。他不可以对实质上的硬件参数做任何改变,只是让应用知道都可以做什么的一种方式
说白了这个命令就是用来查看设置的帧格式硬件是否支持
真正设置帧格式的命令是VIDIOC_S_FMT
与VIDIOC_TRY_FMT不同,这个调用是不能随便调用的,如果硬件正在工作,或者流缓冲器已经开辟了,改变格式会带来无尽的麻烦。
//设置帧格式 查看帧格式
struct v4l2_format fmt2;
memset(&fmt2, 0, sizeof(struct v4l2_format));
fmt2.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //摄像头 (视频捕获设备)
fmt2.fmt.pix.wi enumerator V4L2_BUF_TYPE_VIDEO_CAPTURE
fmt2.fmt.pix.he
fmt2.fmt.pix.pi Type: enum v4l2_buf_type
/*#define v4l2_ Value = 1
((__u32)(a) | ( ((__u32)(d) << 24))
*/ // In v4l2_buf_type
fmt2.fmt.pix.fi V4L2_BUF_TYPE_VIDEO_CAPTURE = 1
//设置帧格式 VIDIOC_S_FMT
if (ioctl(fd, VIDIOC_S_FMT, &fmt2) == -1){
perror("ioctl");
exit(EXIT_FAILURE);
}
fmt2.type = V4L2_BUF_TYPE_PRIVATE;
if (ioctl(fd, VIDIOC_S_FMT, &fmt2) == -1)
{
printf("VIDIOC_S_FMT is error-------------LINE:%d\n",__LINE__);
exit(EXIT_FAILURE);
}
fmt2.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
//查看设置的帧格式
if (ioctl(fd, VIDIOC_G_FMT, &fmt2) == -1)
{
perror("ioctl");
exit(EXIT_FAILURE);
}
printf("width:%d\nheight:%d\npixelformat:%c%c%c%c\n",
fmt2.fmt.pix.width, fmt2.fmt.pix.height,
fmt2.fmt.pix.pixelformat & 0xFF,
(fmt2.fmt.pix.pixelformat >> 8) & 0xFF,
(fmt2.fmt.pix.pixelformat >> 16) & 0xFF,
(fmt2.fmt.pix.pixelformat >> 24) & 0xFF
);
二、数据采集部分
设置完上面的相关属性 便进入了视频采集的部分
关于视频采集方式
操作系统一般把系统使用的内存划分为用户空间和内核空间,分别由应用程序管理和内核管理。应用程序可以直接访问内存的地址,而内核空间存放的是供内核访问的代码和数据,永不能直接访问。V4L2捕获的数据,最初是放在内核空间的,这意味着应用程序不能直接访问该内存,必须通过相应的手段来转换地址
视频采集方式一共有三种:
1、使用read()/write()方式;
在用用户空间和内核空间不断拷贝数据,占用了大量用户空间内存,效率较低;常用于静态图像的捕获
2、内存映射方式mmap
(memory-mapped buffers) (typeV4L2_MEMORY_MMAP)
将在内核空间分配的帧缓冲区映射到用户空间,这样便可直接操作帧缓冲区的数据
3、用户指针模式
(V4L2_MEMORY_USERPTR) 是在用户空间的应用中开辟的,很显然在这种情况下不需要mmap()调用的,但是驱动在有效支持用户空间缓冲区上的工作将会更难一些
1、向驱动申请帧缓存
相关结构体
/* M E M O R Y - M A P P I N G B U F F E R S
* M
*/ struct v4l2_requestbuffers {}
struct v4l2_requestbuffers {
__u32 count; //申请缓冲区的数量 一般不超过5个
__u32 type; /* enum v4l2_buf_type */
__u32 memory; /* enum v4l2_memory */
__u32 capabilities;
__u32 reserved[1];
};
enum v4l2_memory {
V4L2_MEMORY_MMAP = 1, // MMAP映射方式
V4L2_MEMORY_USERPTR = 2, //用户指针方式
V4L2_MEMORY_OVERLAY = 3,
V4L2_MEMORY_DMABUF = 4,
};
type 字段描述的是完成的I/O操作的类型。通常它的值要么是视频获得设备的V4L2_BUF_TYPE_VIDEO_CAPTURE ,要么是输出设备的V4L2_BUF_TYPE_VIDEO_OUTPUT.也有其它的类型,但在这里我们不予讨论。
如果应用想要使用内存映谢的缓冲区,它将会把memory字段置为 V4L2_MEMORY_MMAP,count置为它想要使用的缓冲区的数目。如果驱动不支持内存映射,它就该返回-EINVAL.否则它将在内部开辟请求的缓冲区并返回0.返回之后,应用就会认为缓冲区是存在的,所以任何可以失败的任务都在在这个阶段进行处理 (比如说内存开辟)
注意:驱动并不一定要开辟与请求的一样数目的缓冲区。在很多情况下,有一个最小值缓冲区数的有意义。如果应用请求的比最小值小,可能实际返回的要多于请求的。以笔者的经验,mplayer要用两个缓冲区,如果用户空间速度慢下来的话,这将很容易超支(因而丢失帧)。通过强制一个大一点的最小缓冲区数(可调整的模块参数),cafe_ccic驱动可以使流输入输出通道更加强壮。count 字段应设为方法返回前实际开辟的缓冲区数。
v4l2_requestbuffers结构中定义了缓存的数量,驱动会据此申请对应数量的视频缓存。多个缓存可以用于建立FIFO,来提高视频采集的效率。
控制命令VIDIOC_REQBUFS
ioctl(int fd, int request, struct v4l2_requsetbuffers *buffer)
申请缓冲区
//申请缓冲区
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(struct v4l2_requestbuffers));
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP; //使用内存映射缓冲区
//VIDIOC_REQUBUFS 申请4个视频数据的帧缓冲区
//每个帧缓冲区存放一帧视频数据,这些帧缓冲区在内核空间
if(ioctl(fd, VIDIOC_REQBUFS, &req) == -1){
perror("VIDIOC_REQBUFS");
exit(EXIT_FAILURE);
}
2、获取每个帧缓存的信息,并映射到用户空间
相关结构体 struct V4l2_buffer
struct v4l2_buffer {
__u32 index; //鉴别缓冲区的序号;他只在内存映射缓冲区中使用。与其他可以在V4L2接口中枚举的对象一样,内纯缓冲区的index从0开始一次递增
__u32 type; //描述缓冲区的类型,通常是
//V4L2_BUF_TYPE_VIDEO_CAPTURE或V4L2_BUF_TYPE_VIDEO_OUTPUT
__u32 bytesused; //缓存已使用的大小 单位是byte
__u32 flags; //为缓存当前状态(常见值有 V4L2_BUF_FLAG_MAPPED | V4L2_BUF_FLAG_QUEUED | V4L2_BUF_FLAG_DONE,分别代表当前缓存已经映射、缓存可以采集数据、缓存可以提取数据)
__u32 field;
struct timeval timestamp; //时间戳
struct v4l2_timecode timecode;
__u32 sequence; //缓存序号
/* memory location */
__u32 memory; //表示缓冲是内存映射还是用户空间模式
union {
__u32 offset; //当前缓存与内存区其实地址的偏移量
unsigned long userptr; //缓冲区的用户空间地址
struct v4l2_plane *planes;
__s32 fd;
} m;
__u32 length; //缓存大小
__u32 reserved2;
union {
__s32 request_fd;
__u32 reserved;
};
};
另外 VIDIOC_QBUF 和 VIDIOC_DQBUF 命令都采用结构 v4l2_buffer 与驱动通信:VIDIOC_QBUF 命令向驱动传递应用程序已经处理完的缓存,即将缓存加入空闲可捕获视频的队列,传递的主要参数为 index;VIDIOC_DQBUF 命令向驱动获取已经存放有视频数据的缓存,v4l2_buffer 的各个域几乎都会被更新,但主要的参数也是 index,应用程序会根据 index 确定可用数据的起始地址和范围。
这里接下来会介绍
将内核空间帧缓冲区映射到用户空间
typedef struct VideoBuffer{ //定义一个结构体来映射每个帧缓存
void * start;
size_t length;
}VideoBuffer;
VideoBuffer *buffer = (VideoBuffer *)calloc(req.count, sizeof(VideoBuffer)); //申请四个buffer大小的缓冲
区
if (buffer == NULL)
{
perror("buffer");
exit(EXIT_FAILURE);
}
struct v4l2_buffer buf;
int numBufs;
for (numBufs = 0; numBufs < req.count; numBufs++) //映射所有缓存
{
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = numBufs;
//VIDIOC_QUERYBUF 使用该方法查询帧缓冲区在内核空间的长度和偏移量地址 实际上就是驱动填充buf结构体
//一个缓冲区有三种基本状态 1、视频采集输入队列 2、视频采集输出队列 3、用户空间状态
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) //获取对应的index信息,此处主要利用length信息和offset>信息来完成对后面的mmap操作
{
printf("VIDIOC_QEQBUFS is error --------------LINE:%d\n",__LINE__);
exit(EXIT_FAILURE);
}
buffer[numBufs].length = buf.length; //帧长度
printf("----------%d\n",buf.m.offset);
//将申请到的内核空间帧缓冲区的地址映射到用户空间地址,这样便可直接操作处理帧缓冲区的数据
buffer[numBufs].start = mmap(NULL, //addr映射的起始地址 一般为NULL,由内核自动选择
buf.length, //被映射的内存长度
PROT_READ | PROT_WRITE, //标志映射后可读写
MAP_SHARED, //确定次内存映射能否被其他进程共享
fd, //文件描述符
buf.m.offset//根据偏移量来确定被映射的内存地址,成功返回被映射的内存地址
);
//buf.m.offset 内核空间帧缓冲区的偏移地址
if (buffer[numBufs].start == MAP_FAILED) //如果mmap映射内存失败MAP_FAILED ((void*)-1)
{
printf("mmap is error --------LINE:%d\n", __LINE__);
exit(EXIT_FAILURE);
}
//将缓冲区在视频采集输入队列进行排队(VIDIOC_QBUF) 缓冲区的一种状态 采集输入队列
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1){ //申请到的帧缓存加入到视频采集输入队列
printf("VIDIOC_QBUF is err ---------------------LINE:%d\n", __LINE__);
exit(EXIT_FAILURE);
}
printf("Frame buffer :%d address :0x%x length:%d\n",numBufs, (__u32)buffer[numBufs].start, buffer[numBufs].length);
}
3、开始视频采集
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
//打开设备视频流
if (ioctl(fd, VIDIOC_STREAMON, &type) == -1){
printf("VIDIOC_STREAMON is error --------------LINE:%d\n", __LINE__);
exit(EXIT_FAILURE);
}
前期初始化完成后,只是解决了一帧视频数据的格式和大小问题,而连续视频帧数据的采集需要用帧缓冲区队列的方式来解决,即要通过驱动程序在内存中申请几个帧缓冲区来存放视频数据。
应用程序通过API接口提供的方法(VIDIOC_REQBUFS)申请若干个视频数据的帧缓冲区,申请帧缓冲区数量一般不低于3个,每个帧缓冲区存放一帧视频数据,这些帧缓冲区在内核空间。
应用程序通过API接口提供的查询方法(VIDIOC_QUERYBUF)查询到帧缓冲区在内核空间的长度和偏移量地址。
应用程序再通过内存映射方法(mmap),将申请到的内核空间帧缓冲区的地址映射到用户空间地址,这样就可以直接处理帧缓冲区的数据。
(1)将帧缓冲区在视频输入队列排队,并启动视频采集
在驱动程序处理视频的过程中,定义了两个队列:视频采集输入队列(incoming queues)和视频采集输出队列(outgoing queues),前者是等待驱动存放视频数据的队列,后者是驱动程序已经放入了视频数据的队列。
应用程序需要将上述帧缓冲区在视频采集输入队列排队(VIDIOC_QBUF),然后可启动视频采集。
(2)循环往复,采集连续的视频数据
启动视频采集后,驱动程序开始采集一帧数据,把采集的数据放入视频采集输入队列的第一个帧缓冲区,一帧数据采集完成,也就是第一个帧缓冲区存满一帧数据后,驱动程序将该帧缓冲区移至视频采集输出队列,等待应用程序从输出队列取出。驱动程序接下来采集下一帧数据,放入第二个帧缓冲区,同样帧缓冲区存满下一帧数据后,被放入视频采集输出队列。
应用程序从视频采集输出队列中取出含有视频数据的帧缓冲区,处理帧缓冲区中的视频数据,如存储或压缩。
数据流动过程
缓冲区状态的切换过程
每一个帧缓冲区都有一个对应的状态标志变量,其中每一个比特代表一个状态
V4L2_BUF_FLAG_UNMAPPED 0B0000
V4L2_BUF_FLAG_MAPPED 0B0001
V4L2_BUF_FLAG_ENQUEUED 0B0010
V4L2_BUF_FLAG_DONE 0B0100
缓冲区的状态转化如图所示。
4、取出视频输出队列的帧数据
struct timeval tvptr;
tvptr.tv_usec = 0;
tvptr.tv_sec = 2;
fd_set fdread;
FD_ZERO(&fdread);
FD_SET(fd, &fdread);
ret = select(fd, &fdread, NULL, NULL, &tvptr);
if (ret == -1){
perror("EXIT_FAILURE");
exit(EXIT_FAILURE);
}
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = 0;
// 从视频输出队列中取出已含有采集数据的帧缓冲区
if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1){
perror("ioctl");
printf("VIDIOC_DQBUF is err --------------LINE:%d\n", __LINE__);
exit(EXIT_FAILURE);
}
5、处理帧缓冲区的视频数据
//将一帧数据写入文件
int fd_file = open("./out.yuv", O_RDWR | O_CREAT, 0777);
if (fd_file == -1)
{
perror("open");
exit(EXIT_FAILURE);
}
//用户空间 对缓冲区的内容进行操作 此时缓冲区既不在视频输入队列,也不在视频输出队列,而是处于用户空间
write(fd_file, buffer[buf.index].start, buffer[buf.index].length);
close(fd_file);
6、将刚刚处理完的缓冲区重新进入视频输入队列,这样便可循环采集
//将缓冲区设置为视频输入队列
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1){
printf("VIDIOC_QBUF ----------------LINE:%d\n",__LINE__);
exit(EXIT_FAILURE);
}
7、停止视频采集,解除映射
if (ioctl(fd, VIDIOC_STREAMOFF, &buf.type) == -1)
{
printf("VIDIOC_STREAMOFF is err ----------------LINE:%d\n", __LINE__);
exit(EXIT_FAILURE);
}
//解除映射
for (numBufs = 0; numBufs < 4; numBufs++)
{
munmap(buffer[numBufs].start, buffer[numBufs].length);
}
8、关闭视频设备
//关闭设备
close(fd);
源码也比较简单
关于V4L2采集视频的难点大概就是帧缓冲区的管理了
相关源代码
/*************************************************************************
> File Name: camer.c
> 作者:YJK
> Mail: 745506980@qq.com
> Created Time: 2020年11月18日 星期三 14时17分50秒
************************************************************************/
#include"camer.h"
#include <linux/i2c.h>
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
int fd;
int file_fd;
int frame_size;
static Video_Buffer * buffer = NULL;
FILE * file = NULL;
int ioctl_(int fd, int request, void *arg)
{
int ret = 0;
do{
ret = ioctl(fd, request, arg);
}while(ret == -1 && ret == EINTR);
}
int open_device(const char * device_name)
{
struct stat st;
if( -1 == stat( device_name, &st ) )
{
printf( "Cannot identify '%s'\n" , device_name );
return -1;
}
if ( !S_ISCHR( st.st_mode ) )
{
printf( "%s is no device\n" , device_name );
return -1;
}
fd = open(device_name, O_RDWR | O_NONBLOCK , 0);
if ( -1 == fd )
{
printf( "Cannot open '%s'\n" , device_name );
return -1;
}
return 0;
}
int init_device(void)
{
//查询设备信息
struct v4l2_capability cap;
if (ioctl_(fd, VIDIOC_QUERYCAP, &cap) == -1)
{
perror("VIDIOC_QUERYCAP");
return -1;
}
printf("---------------------LINE:%d\n", __LINE__);
printf("DriverName:%s\nCard Name:%s\nBus info:%s\nDriverVersion:%u.%u.%u\n",
cap.driver,cap.card,cap.bus_info,(cap.version>>16)&0xFF,(cap.version>>8)&0xFF,(cap.version)&0xFF);
//选择视频输入
struct v4l2_input input;
CLEAN(input);
input.index = 0;
if ( ioctl_(fd, VIDIOC_S_INPUT,&input) == -1){
printf("VIDIOC_S_INPUT IS ERROR! LINE:%d\n",__LINE__);
return -1;
}
//设置帧格式
struct v4l2_format fmt;
CLEAN(fmt);
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = WIDTH;
fmt.fmt.pix.height = HEIGHT;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
// fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (ioctl_(fd, VIDIOC_S_FMT, &fmt) == -1)
{
printf("VIDIOC_S_FMT IS ERROR! LINE:%d\n",__LINE__);
return -1;
}
fmt.type = V4L2_BUF_TYPE_PRIVATE;
if (ioctl_(fd, VIDIOC_S_FMT, &fmt) == -1){
printf("VIDIOC_S_FMT IS ERROR! LINE:%d\n", __LINE__);
return -1;
}
//查看帧格式
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if ( ioctl_(fd, VIDIOC_G_FMT, &fmt) == -1){
printf("VIDIOC_G_FMT IS ERROR! LINE:%d\n", __LINE__);
return -1;
}
printf("width:%d\nheight:%d\npixelformat:%c%c%c%c\n",
fmt.fmt.pix.width, fmt.fmt.pix.height,
fmt.fmt.pix.pixelformat & 0xFF,
(fmt.fmt.pix.pixelformat >> 8) & 0xFF,
(fmt.fmt.pix.pixelformat >> 16) & 0xFF,
(fmt.fmt.pix.pixelformat >> 24) & 0xFF
);
__u32 min = fmt.fmt.pix.width * 2;
if ( fmt.fmt.pix.bytesperline < min )
fmt.fmt.pix.bytesperline = min;
min = ( unsigned int )WIDTH * HEIGHT * 3 / 2;
if ( fmt.fmt.pix.sizeimage < min )
fmt.fmt.pix.sizeimage = min;
frame_size = fmt.fmt.pix.sizeimage;
printf("After Buggy driver paranoia\n");
printf(" >>fmt.fmt.pix.sizeimage = %d\n", fmt.fmt.pix.sizeimage);
printf(" >>fmt.fmt.pix.bytesperline = %d\n", fmt.fmt.pix.bytesperline);
printf("-#-#-#-#-#-#-#-#-#-#-#-#-#-\n");
printf("\n");
return 0;
}
int init_mmap()
{
//申请帧缓冲区
struct v4l2_requestbuffers req;
CLEAN(req);
req.count = 4;
req.memory = V4L2_MEMORY_MMAP; //使用内存映射缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
//申请4个帧缓冲区,在内核空间中
if ( ioctl_(fd, VIDIOC_REQBUFS, &req) == -1 )
{
printf("VIDIOC_REQBUFS IS ERROR! LINE:%d\n",__LINE__);
return -1;
}
//获取每个帧信息,并映射到用户空间
buffer = (Video_Buffer *)calloc(req.count, sizeof(Video_Buffer));
if (buffer == NULL){
printf("calloc is error! LINE:%d\n",__LINE__);
return -1;
}
struct v4l2_buffer buf;
int buf_index = 0;
for (buf_index = 0; buf_index < req.count; buf_index ++)
{
CLEAN(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.index = buf_index;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl_(fd, VIDIOC_QUERYBUF, &buf) == -1) //获取每个帧缓冲区的信息 如length和offset
{
printf("VIDIOC_QUERYBUF IS ERROR! LINE:%d\n",__LINE__);
return -1;
}
//将内核空间中的帧缓冲区映射到用户空间
buffer[buf_index].length = buf.length;
buffer[buf_index].start = mmap(NULL, //由内核分配映射的起始地址
buf.length,//长度
PROT_READ | PROT_WRITE, //可读写
MAP_SHARED,//可共享
fd,
buf.m.offset);
if (buffer[buf_index].start == MAP_FAILED){
printf("MAP_FAILED LINE:%d\n",__LINE__);
return -1;
}
//将帧缓冲区放入视频输入队列
if (ioctl_(fd, VIDIOC_QBUF, &buf) == -1)
{
printf("VIDIOC_QBUF IS ERROR! LINE:%d\n", __LINE__);
return -1;
}
printf("Frame buffer :%d address :0x%x length:%d\n",buf_index, (__u32)buffer[buf_index].start, buffer[buf_index].length);
}
}
void start_stream()
{
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl_(fd, VIDIOC_STREAMON, &type) == -1){
printf("VIDIOC_STREAMON IS ERROR! LINE:%d\n", __LINE__);
exit(EXIT_FAILURE);
}
}
void end_stream()
{
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl_(fd, VIDIOC_STREAMOFF, &type) == -1){
printf("VIDIOC_STREAMOFF IS ERROR! LINE:%d\n", __LINE__);
exit(EXIT_FAILURE);
}
}
static int read_frame()
{
struct v4l2_buffer buf;
int ret = 0;
CLEAN(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl_(fd, VIDIOC_DQBUF, &buf) == -1){
printf("VIDIOC_DQBUF! LINEL:%d\n", __LINE__);
return -1;
}
ret = write(file_fd, buffer[buf.index].start ,frame_size);
if (ret == -1)
{
printf("write is error !\n");
return -1;
}
if (ioctl_(fd, VIDIOC_QBUF, &buf) == -1){
printf("VIDIOC_QBUF! LINE:%d\n", __LINE__);
return -1;
}
return 0;
}
int open_file(const char * file_name)
{
file_fd = open(file_name, O_RDWR | O_CREAT, 0777);
if (file_fd == -1)
{
printf("open file is error! LINE:%d\n", __LINE__);
return -1;
}
// file = fopen(file_name, "wr+");
}
void close_mmap()
{
int i = 0;
for (i = 0; i < 4 ; i++)
{
munmap(buffer[i].start, buffer[i].length);
}
free(buffer);
}
void close_device()
{
close(fd);
close(file_fd);
}
int process_frame()
{
struct timeval tvptr;
int ret;
tvptr.tv_usec = 0; //等待50 us
tvptr.tv_sec = 2;
fd_set fdread;
FD_ZERO(&fdread);
FD_SET(fd, &fdread);
ret = select(fd + 1, &fdread, NULL, NULL, &tvptr);
if (ret == -1){
perror("EXIT_FAILURE");
exit(EXIT_FAILURE);
}
if (ret == 0){
printf("timeout! \n");
}
read_frame();
}
/*************************************************************************
> File Name: camer.h
> 作者:YJK
> Mail: 745506980@qq.com
> Created Time: 2020年11月18日 星期三 14时17分55秒
************************************************************************/
#ifndef __CAMER_H
#define __CAMER_H
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdlib.h>
#include<linux/videodev2.h>
#include<sys/ioctl.h>
#include<errno.h>
#include<string.h>
#include<assert.h>
#include<getopt.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<asm/types.h>
#include<linux/fb.h>
#define CLEAN(x) (memset(&(x), 0, sizeof(x)))
#define WIDTH 640
#define HEIGHT 480
typedef struct Video_Buffer{
void * start;
unsigned int length;
}Video_Buffer;
int ioctl_(int fd, int request, void *arg);
void sys_exit(const char *s);
int open_device(const char * device_name);
int open_file(const char * file_name);
void start_stream(void);
void end_stream(void);
int init_device(void);
int init_mmap(void);
static int read_frame(void);
int process_frame(void);
void close_mmap(void);
void close_device(void);
#endif
/*************************************************************************
> File Name: main.c
> 作者:YJK
> Mail: 745506980@qq.com
> Created Time: 2020年11月21日 星期六 18时46分19秒
************************************************************************/
#include<stdio.h>
#include"camer.h"
#define DEVICE_NAME "/dev/video0"
#define FILE_NAME "./out.yuv"
int main(int argc,char *argv[])
{
int ret = 0;
int i;
ret = open_device(DEVICE_NAME);
if (ret == -1)
exit(EXIT_FAILURE);
open_file(FILE_NAME);
init_device();
init_mmap();
start_stream();
for (i = 0 ; i < 600; i++)
{
process_frame();
printf("frame:%d\n",i);
}
end_stream();
close_mmap();
close_device();
return 0;
}
对于帧数据的处理暂时仅限于写入文件,后续学习H264编解码再做修改
后续来啦!!!