V4L2(Video for Linux 2)是Linux为视频设备所提供的内核驱动和应用程序统一接口体系. 有了它,应用程序就可以通过调用设备接口控制函数ioctl(),来实现对视频设备的各种操控.通过一个摄像头设备的操控程序实例,我们可以学习到应用V4L2系统的步骤和重点.
1. 调用一个摄像头设备的控制函数ioctl(),主要涉及使用下面的一些控制代码(IOCT CODE):
VIDIOC_QUERYCAP 查询设备驱动相关信息及支持的各项功能,返回struct v4l2_queryctrl, 包括驱动名称,硬件名称, bus信息,及是否具备视频捕获/数据流控/音频支持/RDS输入输出等等各项功能.
VIDIOC_QUERYCTRL 查询设备支持可调节的各种控制变量,返回struct v4l2_control, 比如亮度,对比度,饱和度,色度,白平衡(白点位置)等等
VIDIOC_G_CTRL 获取指定控制变量的数值,返回struct v4l2_control, 比如亮度的最小值/最大值/默认值/当前值等.
VIDIOC_S_CTRL 设定指定控制变量的数值 (关联 struct v4l2_control)
VIDIOC_G_FMT 获取数据流格式, 对视频数据流而言, 返回struct v4l2_pix_format, 包含了图像尺寸,图像格式(yuyv/mjpeg...),每行大小bytesperline, 色彩空间colorspace等.
VIDIOC_S_FMT 设定指定数据流格式 (关联 struct v4l2_format )
VIDIOC_G_PARM 获取模式参数, 对视频捕获而言, 返回struct v4l2_captureparm, 主要有帧率FPS等信息.
VIDIOC_S_PARM 设定功能模式参数 (关联struct v4l2_captureparm)
VIDIOC_ENUM_FMT 枚举指定模式下支持的各种数据格式,返回struct v4l2_fmtdesc, 比如摄像头设备在V4L2_BUF_TYPE_VIDEO_CAPTURE模式下. 支持MJPEG和YUV4:2:2图像格式.
VIDIOC_REQBUFS 申请帧缓存,可指定帧缓存的数量和内存映射形式等 (关联 struct v4l2_buffer)
VIDIOC_QUERYBUF 获取帧缓存相关信息,返回struct v4l2_buffer, 包括帧缓存的编号,类型,时间戳,地址,大小等.
VIDIOC_DQBUF 将当前帧缓存从工作队列出取出, 返回struct v4l2_buffer, 包括帧缓存的编号,类型,时间戳,地址,大小等.
VIDIOC_QBUF 将当前帧缓存放入工作队列,以供摄像头写入数据.(关联struct v4l2_buffer)
2. 应用程序操作摄像头设备的基本流程:
2.1 打开摄像头设备文件
2.2 获取设备驱动所支持的各种能力(capability)
比如: 是否支持视频捕获(video capture); 是否支持数据流控(video streaming I/O ioctls)等.
2.3 查询和设置摄像头的各项控制变量: 比如亮度,对比度,饱和度,色度等等.
2.4 配置视频数据流格式: 模式及相关参数. 比如视频捕捉模式下的画面尺寸,图像格式(MJPEG, YUYV等)
2.5 设置视频捕获模式参数,比如帧率FPS.
2.6 向内核申请视频帧缓存.
2.7 启动设备,开始视频捕获.
2.8 读取帧缓存内数据,对数据进行处理/保存/显示等.
3. 读取摄像头数据并进行显示的程序实例源码: (部分用到EGi库)
/*-------------------------------------------------------------------------------------
操作指定摄像头设备,采集视频数据并将它实时显示在LCD上.支持YUYV和MJPEG格式.
Usage example:
./test_usbcam -d /dev/video0 -s 320x240 -r 30 (mjpg)
./test_usbcam -d /dev/video1 -r 20 -f yuyv -v (yuyv)
Reference:
超群天晴 https://www.cnblogs.com/surpassal/archive/2012/12/19/zed_webcam_lab1.html
JWH SMITH http://jwhsmith.net/2014/12/capturing-a-webcam-stream-using-v4l2
Midas Zhou
-------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <getopt.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <malloc.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <asm/types.h>
#include <linux/videodev2.h> /* include <linux/v4l2-controls.h>, <linux/v4l2-common.h> */
#include <egi_input.h>
#include <egi_fbdev.h>
#include <egi_image.h>
#include <egi_bjp.h>
#include <egi_FTsymbol.h>
#include <egi_timer.h>
/* 申请的帧缓存页数 */
#define REQ_BUFF_NUMBER 5 /* required buffers to be allocated. */
/* 用来记录帧缓存的地址和大小 */
struct buffer {
void * start;
size_t length;
};
struct buffer *buffers;
unsigned long bufindex;
/* 记录保存摄像头的控制变量和对应数值 */
struct v4l2_control vctrl_params[] =
{
{ V4L2_CID_BRIGHTNESS, 0 },
{ V4L2_CID_CONTRAST, 0 },
{ V4L2_CID_SATURATION, 0 },
{ V4L2_CID_SHARPNESS, 0 },
{ V4L2_CID_HUE, 0 },
{ V4L2_CID_GAMMA, 0 },
};
/* 显示标题和时间戳 */
void draw_marks(void);
/*---------------------------
Print help
-----------------------------*/
void print_help(const char* cmd)
{
printf("Usage: %s [-hd:s:r:f:v] \n", cmd);
printf(" -h help \n");
printf(" -d: Video device, default: '/dev/video0' \n");
printf(" -s: Image size, default: '320x240' \n");
printf(" -r: Frame rate, default: 10fps \n");
printf(" -f: Pixel format, default: V4L2_PIX_FMT_MJPEG \n");
printf(" 'yuyv' or 'mjpeg' \n");
printf(" -v: Reverse upside down, default: false. for YUYV only.\n");
}
/*-----------------------------
MAIN
------------------------------*/
int main (int argc,char ** argv)
{
char *dev_name = "/dev/video0";
int fd_dev; /* Video device */
fd_set fds;
unsigned char *rgb24=NULL;
struct v4l2_queryctrl queryctrl={0};
struct v4l2_control vctrl;
struct v4l2_capability cap;
struct v4l2_format fmt;
struct v4l2_fmtdesc fmtdesc;
struct v4l2_requestbuffers req;
struct v4l2_buffer bufferinfo;
struct v4l2_streamparm streamparm;
enum v4l2_buf_type type;
unsigned int i;
int opt;
char *pt;
/* 默认参数 */
/* Defaut params: pxiformat, image size and frame rate */
int pixelformat=V4L2_PIX_FMT_MJPEG;
int width=320;
int height=240;
int fps=10;
bool reverse=false;
/* 程序选项 */
/* Parse input option */
while( (opt=getopt(argc,argv,"hd:s:r:f:v"))!=-1 ) {
switch(opt) {
case 'h':
print_help(argv[0]);
exit(0);
break;
case 'd': /* Video device name */
dev_name=optarg;
break;
case 's': /* Image size */
width=atoi(optarg);
if((pt=strstr(optarg,"x"))!=NULL)
height=atoi(pt+1);
else
height=atoi(strstr(optarg,"X"));
break;
case 'r': /* Frame rate */
fps=atoi(optarg);
printf("Input fps=%d\n", fps);
break;
case 'f':
if(strstr(optarg, "yuyv"))
pixelformat=V4L2_PIX_FMT_YUYV;
break;
case 'v':
reverse=true;
break;
}
}
/* Init sys FBDEV 初始化FB显示设备 */
if( init_fbdev(&gv_fb_dev) )
return -1;
/* Load FT derived sympg_ascii 加载ASCII字体 */
if(FTsymbol_load_allpages() !=0) {
return -1;
}
/* 设置显示模式: 是否直接操作FB映像数据, 设置横竖屏 */
/* Set FB mode */
fb_set_directFB(&gv_fb_dev, false);
fb_position_rotate(&gv_fb_dev, 0);
/* 打开摄像头设备文件 */
/* Open video device */
fd_dev = open (dev_name, O_RDWR);
if(fd_dev<0) {
printf("Fail to open '%s', Err'%s'\n", dev_name, strerror(errno));
return -1;
}
/* 获取设备驱动支持的操作 */
/* Get video driver capacity */
if( ioctl (fd_dev, VIDIOC_QUERYCAP, &cap) !=0) {
printf("Fail to ioctl VIDIOC_QUERYCAP, Err'%s'\n", strerror(errno));
return -1;
}
printf("Driver Name:%s\n Card Name:%s\n Bus info:%s Capibilities:0x%04X\n",cap.driver,cap.card,cap.bus_info, cap.capabilities);
if( !(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) ) { /* 是否支持视频捕获 */
printf("The video device does NOT supports capture.\n");
return -1;
}
if( !(cap.capabilities & V4L2_CAP_STREAMING) ) { /* 是否支持数据流控 streaming I/O ioctls */
printf("The video device does NOT support streaming!\n");
return -1;
}
/* 在这里查询/设置摄像头的各项控制变量: 亮度,对比度,饱和度,色度等等. */
/* Set contrast/brightness/saturation/hue/sharpness/gamma/...
* Default usually is OK.
*/
printf("\n\t\t--- Control params ---\n");
for(i=0; i<sizeof(vctrl_params)/sizeof(struct v4l2_control); i++) {
/* Query Contro 获取设备支持的各项控制变量及其数值 */
vctrl.id=vctrl_params[i].id;
queryctrl.id=vctrl_params[i].id;
if( ioctl( fd_dev, VIDIOC_QUERYCTRL, &queryctrl) !=0 ) {
printf("Fail to ioctl VIDIOC_QUERYCTRL for control ID=%d, Err'%s'\n", i, strerror(errno));
continue;
}
if( ioctl( fd_dev, VIDIOC_G_CTRL, &vctrl) !=0 ) {
printf("Fail to ioctl VIDIOC_G_CTRL for control ID=%d, Err'%s'\n", i, strerror(errno));
continue;
}
else {
printf("%s: min=%d max=%d default=%d value=%d\n",
queryctrl.name, queryctrl.minimum, queryctrl.maximum, queryctrl.default_value,
vctrl.value);
}
/* 将亮度设置到其值域的80%, 其他也可以类似设置. */
/* Change brightness to 80% of its range. */
if(vctrl.id==V4L2_CID_BRIGHTNESS) {
vctrl.value=queryctrl.minimum+(queryctrl.maximum-queryctrl.minimum)*80/100;
ioctl( fd_dev, VIDIOC_S_CTRL, &vctrl);
}
}
/* 设置视频数据流格式: 模式及相关参数 */
/* Set frame format */
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; /* 视频捕捉模式 */
fmt.fmt.pix.width = width; /* 视频尺寸 */
fmt.fmt.pix.height = height;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; /* 隔行交替 */
fmt.fmt.pix.pixelformat = pixelformat; /* MJPEG or YUYV */
if( ioctl (fd_dev, VIDIOC_S_FMT, &fmt) !=0) {
printf("Fail to ioctl VIDIOC_S_FMT, Err'%s'\n", strerror(errno));
return -2;
}
if( ioctl( fd_dev, VIDIOC_G_FMT, &fmt) !=0 ) {
printf("Fail to ioctl VIDIOC_G_FMT, Err'%s'\n", strerror(errno));
return -2;
}
else {
/* 打印实际设置生效的参数 */
printf("\n\t--- Effective params ---\n");
printf("fmt.type: %d\n", fmt.type);
printf("pixelformat: %c%c%c%c\n",fmt.fmt.pix.pixelformat & 0xFF, (fmt.fmt.pix.pixelformat>>8) & 0xFF,
(fmt.fmt.pix.pixelformat>>16) & 0xFF, (fmt.fmt.pix.pixelformat>>24) & 0xFF);
printf("Width&Height: %d&%d\n",fmt.fmt.pix.width, fmt.fmt.pix.height);
printf("Bytesperline: %d\n", fmt.fmt.pix.bytesperline);
printf("Pixfield: %d (1-no field)\n", fmt.fmt.pix.field);
printf("Colorspace: %d\n", fmt.fmt.pix.colorspace);
}
/* 获取生效的实际画面尺寸 */
/* Confirm width and height */
width=fmt.fmt.pix.width;
height=fmt.fmt.pix.height;
/* 设置流控参数FPS */
/* Set stream params: frame rate */
memset(&streamparm, 0, sizeof(streamparm));
streamparm.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
streamparm.parm.capture.timeperframe.denominator=fps;
streamparm.parm.capture.timeperframe.numerator=1;
if( ioctl( fd_dev, VIDIOC_S_PARM, &streamparm) !=0) {
printf("Fail to ioctl VIDIOC_S_PARM, Err'%s'\n", strerror(errno));
return -3;
}
if( ioctl( fd_dev, VIDIOC_G_PARM, &streamparm) !=0) {
printf("Fail to ioctl VIDIOC_G_PARM, Err'%s'\n", strerror(errno));
return -3;
}
else {
printf("Frame rate: %d/%d fps\n", streamparm.parm.capture.timeperframe.denominator,
streamparm.parm.capture.timeperframe.numerator);
}
/* 枚举支持的视频格式 */
/* Enumerate formats supported by the vide device */
fmtdesc.index=0;
fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("The device supports following formats for video captrue:\n");
while( ioctl( fd_dev, VIDIOC_ENUM_FMT, &fmtdesc)==0 ) {
printf("\t%d.%s\n", fmtdesc.index+1, fmtdesc.description);
fmtdesc.index++;
}
/* 申请帧缓存 */
/* Allocate buffers */
req.count = REQ_BUFF_NUMBER;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if( ioctl (fd_dev, VIDIOC_REQBUFS, &req) !=0) {
printf("Fail to ioctl VIDIOC_REQBUFS, Err'%s'\n", strerror(errno));
return -4;
}
/* 实际申请到的帧缓存页数 Actual buffer number allocated */
printf("Req. buffer ret/req:\t%d/%d \n", req.count, REQ_BUFF_NUMBER);
/* 获取帧缓存的大小和地址,并将其保存到buffers结构中 */
/* Allocate buffer info */
buffers = calloc (req.count, sizeof (*buffers));
/* Get buffer info */
memset(&bufferinfo, 0, sizeof(bufferinfo));
for (bufindex = 0; bufindex < req.count; ++bufindex)
{
bufferinfo.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
bufferinfo.memory = V4L2_MEMORY_MMAP;
bufferinfo.index = bufindex;
if( ioctl (fd_dev, VIDIOC_QUERYBUF, &bufferinfo) !=0) {
printf("Fail to ioctl VIDIOC_QUERYBUF, Err'%s'\n", strerror(errno));
return -4;
}
buffers[bufindex].length = bufferinfo.length;
/* Mmap to user space. the device MUST support V4L2_CAP_STREAMING mode! */
buffers[bufindex].start = mmap (NULL, bufferinfo.length, PROT_READ | PROT_WRITE,
MAP_SHARED, fd_dev, bufferinfo.m.offset);
if(buffers[bufindex].start == MAP_FAILED) {
printf("Fail to mmap buffer, Err'%s'\n", strerror(errno));
return -4;
}
/* Clear it */
memset(buffers[bufindex].start, 0, bufferinfo.length);
}
/* 启动捕获视频,注意:有些设备必须将帧缓存先放入队列后才能启动! */
/* Start to capture video. some devices may need to queue the buffer at first! */
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if( ioctl (fd_dev, VIDIOC_STREAMON, &type) !=0) {
printf("Fail to ioctl VIDIOC_STREAMON, Err'%s'\n", strerror(errno));
return -5;
}
/* 将帧缓存放入工作队列,以供摄像头写入数据. */
/* Queue buffer after turn on video capture! */
for (i = 0; i < REQ_BUFF_NUMBER; ++i) {
bufferinfo.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
bufferinfo.memory = V4L2_MEMORY_MMAP;
bufferinfo.index = i;
if( ioctl (fd_dev, VIDIOC_QBUF, &bufferinfo) !=0) {
printf("Fail to ioctl VIDIOC_QBUF, Err'%s'\n", strerror(errno));
return -4;
}
}
/* Reset index, from which it starts the loop. */
bufferinfo.index=0;
/* 循环例程: 从摄像头读取视频数据,将其输出到FB显示出来. */
/* ---------- Loop reading video frames and display --------*/
/* 1. V4L2_PIX_FMT_YUYV格式读取和显示循环例程序 */
if( pixelformat==V4L2_PIX_FMT_YUYV )
{
printf("Output format: YUYV, reverse=%s\n", reverse?"Yes":"No");
/* 为RBG888数据申请内存 */
rgb24=calloc(1, width*height*3);
if(rgb24==NULL) {
printf("Fail to calloc rgb24!\n");
goto END_FUNC;
}
/* 循环: 帧缓存出列-->读数据转码成RGB888--->帧缓存入列--->显示图像 */
for(;;) {
/* 等待数据 */
FD_ZERO (&fds);
FD_SET (fd_dev, &fds);
select(fd_dev + 1, &fds, NULL, NULL, NULL);
/* 将当前帧缓存从工作队列出取出 */
/* Dequeue the buffer */
bufferinfo.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
bufferinfo.memory = V4L2_MEMORY_MMAP;
bufferinfo.index++;
if(bufferinfo.index==REQ_BUFF_NUMBER)
bufferinfo.index=0;
if( ioctl(fd_dev, VIDIOC_DQBUF, &bufferinfo) !=0)
printf("Fail to ioctl VIDIOC_DQBUF, Err'%s'\n", strerror(errno));
else {
/* 将当前缓存里的数据转化成RGB888格式放入到rbg24中 */
/* Convert YUYV to RGB888 */
egi_color_YUYV2RGB888(buffers[bufferinfo.index].start, rgb24, width, height, reverse);
/* 将当前帧缓存放入工作队列,以供摄像头写入数据.*/
/* Queue the buffer */
if( ioctl(fd_dev, VIDIOC_QBUF, &bufferinfo) !=0)
printf("Fail to ioctl VIDIOC_QBUF, Err'%s'\n", strerror(errno));
/* 显示图像 */
/* DirectFB write */
egi_imgbuf_showRBG888(rgb24, width, height, &gv_fb_dev, 0, 0);
draw_marks(); /* 加上标题时间戳 */
fb_render(&gv_fb_dev); /* 刷新Framebuffer */
}
}
} /* End V4L2_PIX_FMT_YUYV */
/* 2. V4L2_PIX_FMT_MJPEG格式读取和显示循环例程序 */
else
{
printf("Output format: MJPEG\n");
/* 循环: 帧缓存出列-->解码显示JPG图像--->帧缓存入列 */
for(;;) {
/* 等待数据 */
FD_ZERO (&fds);
FD_SET (fd_dev, &fds);
select(fd_dev + 1, &fds, NULL, NULL, NULL);
/* 将当前帧缓存从工作队列出取出 */
/* Dequeue the buffer */
bufferinfo.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
bufferinfo.memory = V4L2_MEMORY_MMAP;
bufferinfo.index++;
if(bufferinfo.index==REQ_BUFF_NUMBER)
bufferinfo.index=0;
if( ioctl(fd_dev, VIDIOC_DQBUF, &bufferinfo) !=0)
printf("Fail to ioctl VIDIOC_DQBUF, Err'%s'\n", strerror(errno));
else {
/* 直接解码显示缓存中的JPG图像数据 */
show_jpg(NULL, buffers[bufferinfo.index].start, buffers[bufferinfo.index].length, &gv_fb_dev, 0, 0, 0);
draw_marks(); /* 加上标题时间戳 */
fb_render(&gv_fb_dev); /* 刷新Framebuffer */
}
/* 将当前帧缓存放入工作队列,以供摄像头写入数据.*/
/* Queue the buffer */
if( ioctl(fd_dev, VIDIOC_QBUF, &bufferinfo) !=0)
printf("Fail to ioctl VIDIOC_QBUF, Err'%s'\n", strerror(errno));
}
} /* End V4L2_PIX_FMT_MJPEG */
END_FUNC:
/* 关闭视频流 */
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if( ioctl(fd_dev, VIDIOC_STREAMOFF, &type) !=0)
printf("Fail to ioctl VIDIOC_DQBUF, Err'%s'\n", strerror(errno));
/* 解除内存映射 */
for (i = 0; i < REQ_BUFF_NUMBER; ++i)
munmap (buffers[i].start, buffers[i].length);
/* 释放申请的内存 */
free(buffers);
free(rgb24);
/* 关闭设备文件 */
close (fd_dev);
/* 释放字符映像 */
symbol_release_allpages();
return 0;
}
/*------------------------------------------
显示标题和时间戳
Put titles and time stamp on the FB image.
------------------------------------------*/
void draw_marks(void)
{
char strtmp[32];
/* 标题 */
symbol_string_writeFB(&gv_fb_dev, &sympg_ascii,
WEGI_COLOR_BLUE, -1, /* fontcolor, int transpcolor */
10, 5, /* int x0, int y0 */
"EGi: A Linux Player", 255); /* string, opaque */
/* 时间戳 */
tm_get_strdaytime(strtmp);
symbol_string_writeFB(&gv_fb_dev, &sympg_ascii,
WEGI_COLOR_ORANGE, -1,
320/2-20, 240-20,
strtmp, 255);
}