第一感觉是首先得了解v4l2协议,它的功能、以及与之对应的实现逻辑,还有与硬件、操作系统的交互等内容。再试着根据功能逻辑和软硬件交互关系,借助硬件设备,实现基础的功能,如查询设备信息、帧类型等。然后,一步一步按照逻辑功能实现步骤,完成代码的编写。
一开始并不了解YUV格式文件,在代码中也仅仅是把硬件设备中采集的单帧数据写入到 file.yuv 文件中,并试着使用pYUV软件打开该文件,结果肯定打开失败。此处,考虑的问题是可能是打开的方式不对(格式选择、帧率等),或者是文件数据类型不对(并不是YUV格式),最后才发现,就是文件数据类型不对,原因是USB协议不支持也就是下面要求下载virtualbox扩展增强包的原因。而且,打开的方式也不对,要求VGA-YUV422-interleaved(后面补充)。
后面就可以实现USB摄像头采集YUV格式视频,保存并播放。下面将详细介绍整个流程。
1、系统环境
操作系统:ubuntu16.04LTS
编辑器:Vim 7.3.429
编译器:gcc 4.6.3
2、下载virtualbox扩展增强包(与USB摄像头有关)
说明:首先,要确认你的USB摄像头使用的什么版的USB协议,1.1或2.0或3.0;virtualbox16.04LTS仅支持USB1.1,所以如果你的USB摄像头是USB2.0或USB3.0就需要安装扩展增强包;当然,如果你的virtualbox一开始就支持USB2.0或USB3.0,也就无需执行这一步骤。
virtualbox管理器->设置->USB设备->USB控制器(确认是否支持USB2.0/3.0)
下载方式(https://jingyan.baidu.com/article/22a299b5e0198a9e19376a38.html)
3、驱动一般会自动安装,如果驱动未安装,也无法实现功能
说明:这一步意在确认USB硬件是否正常,先在主机上确认(排除硬件设备出现的问题),再在ubuntu系统上使用相关软件确认(排除ubuntu上驱动的问题),若两步都能够成功显示图像,则表明在硬件、驱动、接口等方面都没有问题,最后才进行软件编程。
此处,考虑的较为繁杂,一般的设备都不会出现问题,最可能遇见的问题就是上面说的USB协议不一致的问题。所以,如果软件方面确认无误,此步可跳过。
1、所用到软件
主机(Windows):MiniVCap(USB检测)、pYUV(YUV格式文件打开)
虚拟机(ubuntu):cheese/mplayer/camorama(USB检测)
2、确认USB摄像头在主机上可用
插入摄像头USB接口,打开MiniCap,若图像显示成功,则确认无误。
3、确认USB摄像头在ubuntu系统可用
安装相关的软件(任选其一)
sudo apt-get install cheese;
sudo apt-get install mplayer;
sudo apt-get install camorama;
插入摄像头USB接口,打开执行任一软件,若图像显示成功,则确认无误。
cheese;
mplayer;
camorama;
v4l2,全称video for Linux 2,是 linux 里一套标准的音频、视频数据采集的API接口,它支持 uvc 标准的摄像头。
UVC,全称为:USB video class 或USB video device class。是Microsoft与另外几家设备厂商联合推出的为USB视频捕获设备定义的协议标准。符合UVC规格的硬件设备在不需要安装任何的驱动程序下即可在主机中正常使用。
一般,在Linux系统下,一切皆文件,对设备的操作都是通过对设备文件的读写来实现的。一般,我们的USB摄像头的设备文件为 /dev/video0 ,对摄像头数据的采集也就是对设备文件进行读取。
由于对设备文件的读取是在内核的内存区域完成的,用户内存空间需要通过内核内存空间来获取相关采集数据,这就需要把内核内存空间的数据放到用户内存空间中,v4l2提供的方式有三种:内存映射(mmap)、直接读取(read)、用户指针,本文使用内存映射的方式。
1、ioctl函数的使用
对于设备文件的内核操作都是通过ioctl函数来实现,包括设备相关信息的读取和设置、设备缓冲区的申请和映射等操作。
#include
int ioctl(int fd , int request , ...)
fd:文件指针
request:操作方式
...:与操作方式对应的参数
ioctl函数,在本次功能实现中扮演着重要的角色,其使用方式的具体实现将在下面的内容进行详细说明。
2、数据结构的声明
在实现功能的过程中,有着不同保存临时数据的数据结构体,它们都在 /usr/include/linux/videodev.h 内有声明
//device capabilities 设备能力(存放设备信息)
//使用:查看设备信息 driver、card、bus_info、version
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) */
__u32 version; /* should use KERNEL_VERSION() */
__u32 capabilities; /* Device capabilities */
__u32 reserved[4];
};
//video format 视频格式(存放视频信息,可设置)
//使用:type(V4L2_BUF_TYPE_VIDEO_CAPTURE)、fmt(如下)
struct v4l2_format {
enum v4l2_buf_type type;
union {
struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
struct v4l2_pix_format_mplane pix_mp; /* V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE */
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;
};
//video image format 单帧格式(存放单帧信息,可设置)
//使用:width(680)、height(480)、pixelformat(V4L2_PIX_FMT_YUYV)、field(V4L2_FIELD_INTERLACED)
struct v4l2_pix_format {
__u32 width;
__u32 height;
__u32 pixelformat;
enum v4l2_field field;
__u32 bytesperline; /* for padding, zero if unused */
__u32 sizeimage;
enum v4l2_colorspace colorspace;
__u32 priv; /* private data, depends on pixelformat */
};
//format enumeration 帧格式枚举(存放设备支持的格式,用于查看设备支持的格式)
//使用:type(V4L2_BUF_TYPE_VIDEO_CAPTURE)
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];
};
//memeory-mapping buffers 请求内存映射的缓冲结构体(请求内核分配缓冲区)
//使用:count(4)、type(V4L2_BUF_TYPE_VIDEO_CAPTURE)、memory(V4L2_MEMORY_MMAP)
struct v4l2_requestbuffers {
__u32 count;
enum v4l2_buf_type type;
enum v4l2_memory memory;
__u32 reserved[2];
};
//memeory buffer information 内核空间帧信息(用于获取内核帧的长度和地址,便于映射)
//使用:type(V4L2_BUF_TYPE_VIDEO_CAPTURE)、memory(V4L2_MEMORY_MMAP)
struct v4l2_buffer {
__u32 index;
enum v4l2_buf_type type;
__u32 bytesused;
__u32 flags;
enum v4l2_field field;
struct timeval timestamp;
struct v4l2_timecode timecode;
__u32 sequence;
/* memory location */
enum v4l2_memory memory;
union {
__u32 offset;
unsigned long userptr;
struct v4l2_plane *planes;
} m;
__u32 length;
__u32 input;
__u32 reserved;
};
3、请求类型request与相关参数的实现
合理使用ioctl函数的request参数就可以实现对设备I/O通道进行管理,具体在 /usr/include/linux/videodev.h 有声明。
//ioctl codes for video devices 函数ioctl的request设置说明
#define VIDIOC_QUERYCAP _IOR('V', 0, struct v4l2_capability)//获取设备能力
#define VIDIOC_ENUM_FMT _IOWR('V', 2, struct v4l2_fmtdesc)//获取设备支持的帧格式
#define VIDIOC_G_FMT _IOWR('V', 4, struct v4l2_format)//获取当前设备帧格式
#define VIDIOC_S_FMT _IOWR('V', 5, struct v4l2_format)//设置当前设备帧格式
#define VIDIOC_REQBUFS _IOWR('V', 8, struct v4l2_requestbuffers)//申请内核空间缓冲区
#define VIDIOC_QUERYBUF _IOWR('V', 9, struct v4l2_buffer)//获取内核空间单帧缓冲区信息
#define VIDIOC_QBUF _IOWR('V', 15, struct v4l2_buffer)//将单帧缓冲区放入采集入队队列
#define VIDIOC_DQBUF _IOWR('V', 17, struct v4l2_buffer)//从采集出队队列取出单帧缓冲区
#define VIDIOC_STREAMON _IOW('V', 18, int)//开始视频流采集
#define VIDIOC_STREAMOFF _IOW('V', 19, int)//关闭视频流采集
后面的结构体数据类型(struct v4l2_*)、基本数据类型(int)就是ioctl函数参数的 ...,与request对应。
4、映射函数(mmap、munmap)和自定义用户内存空间缓冲区(VideoBuffer)
#include
//将内核内存映射到用户内存
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
//addr:一般为NULL,让系统自动选择地址
//length:映射到内存中的内容大小
//prot:映射区域的保护方式。包括PROT_EXEC(执行)、PROT_READ(读)、PROT_WRITE(写)、PROT_NONE(无操作)
//flags:映射区域的特性。此处用到了MAP_SHARED
//fd:映射文件的文件描述符
//offeset:文件映射的偏移量。
//取消内存映射
int munmap(void *addr, size_t length);
//addr:需要取消映射的用户内存首地址
//length:取消映射的大小
//自定义的用户内存缓冲区,用于和内核内存映射
typedef struct VideoBuffer
{
void * start; //首地址指针
size_t length; //内容长度
}VideoBuffer;
说明:在本次功能实现过程中,我们使用到三种内存缓冲区的结构体,分别是 struct v4l2_requestbuffers、struct v4l2_buffer、struct VideoBuffer,此处做一个辨析。
1、打开设备文件
#include
int fd = open("/dev/video0",O_RDWR,0);
2、查看设备信息
void query_cap(int fd)
{
struct v4l2_capability cap;
ioctl(fd,VIDIOC_QUERYCAP,&cap);
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);
}
3、查看设备支持的帧结构
void enum_fmt(int fd)
{
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index=0;
fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("Supportformat:/n");
while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)!=-1)
{
printf("/t%d.%s/n",fmtdesc.index+1,fmtdesc.description);
fmtdesc.index++;
}
}
4、查看或设置当前格式
void get_fmt(int fd)
{
struct v4l2_format fmt;
fmt.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd,VIDIOC_G_FMT,&fmt);
printf(“Currentdata format information:\n\twidth:%d\n\theight:%d\n”,fmt.fmt.width,fmt.fmt.height);
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index=0;
fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)!=-1)
{
if(fmtdesc.pixelformat& fmt.fmt.pixelformat)
{
printf(“\tformat:%s\n”,fmtdesc.description);
break;
}
fmtdesc.index++;
}
}
void set_fmt(int fd)
{
struct v4l2_format fmt;
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_INTERLACED;
if(ioctl(fd,VIDIOC_S_FMT,&fmt) == -1) return -1;
}
5、申请内核内存缓冲区(4帧)
struct v4l2_requestbuffers req;
req.count=4;
req.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory=V4L2_MEMORY_MMAP;
ioctl(fd,VIDIOC_REQBUFS,&req);
6、获取内存内核的地址、长度,并映射到用户内存空间 ,将其放入采集入队队列
VideoBuffer * buffers = calloc(req.count,sizeof(VideoBuffer));
struct v4l2_buffer buf;
for(int 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;
// 读取缓存
if(ioctl(fd,VIDIOC_QUERYBUF,&buf) == -1) return -1;
buffers[numBufs].length = buf.length;
// 转换成相对地址
buffers[numBufs].start = mmap(NULL,buf.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,buf.m.offset);
if(buffers[numBufs].start == MAP_FAILED) return -1;
// 放入缓存队列
if(ioctl(fd,VIDIOC_QBUF,&buf) == -1) return -1;
}
7、开启视频流采集
enum v4l2_buf_type type;
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl (fd,VIDIOC_STREAMON, &type);
8、将帧缓冲区,取出队列,读取数据(数据处理),放入队列
while(1)
{
struct v4l2_buffer buf;
memset(&buf,0,sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
// 从缓冲区取出一个缓冲帧
ioctl (fd,VIDIOC_DQBUF, &buf);
// 图像处理
process_image(buffers[buf.index].start,buffers[buf.index].length);
// 将取出的缓冲帧放回缓冲区
ioctl (fd, VIDIOC_QBUF,&buf);
}
9、图像处理函数的实现
void process_image(void* start,size_t length)
{
int fd = open("video.yuv",O_RDWR|O_CREAT|O_APPEND,0777);
write(fd,(char*)start,length);
close(fd);
}
10、关闭视频流
ioctl (fd,VIDIOC_STREAMOFF, &type);
11、关闭映射
for(int i=0;i
12、关闭文件
close(fd);
源代码:等代码
扩展:YUV格式数据量较大,格式可以转RGB格式,便于对数据进行压缩,另外还可编码H.264。