V4L2(Video for Linux Two),是Linux内核提供的一组API,用于与视频设备(如网络摄像头、电视卡、USB摄像头等)交互,支持视频的采集和输出。
一、基本实现流程
1.打开设备
在linux系统中,摄像头以文件的形式存在,一般是在/dev文件夹下,通常以‘video+数字’作为文件名。所以开启摄像头设备其实就是打开相应的设备文件。
fd = open("/dev/video0", O_RDWR | O_NONBLOCK, 0);
2.设置视频的数据格式
通过ioctl系统调用接口设置要采集视频的数据格式,一般至少需要包括视频的宽高,像素格式。ioctl函数出现错误时返回-1,可以据此对它做错误处理。
struct v4l2_format 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_ANY;
ioctl(fd, VIDIOC_S_FMT, &fmt);
需要额外注意的时,这时的设置未必一定有效,而未生效时,则由系统自动设置无法生效的属性,而且此时没有错误提示。所以在完成设置用最好再查看一下有没有生效。
ioctl(fd, VIDIOC_G_FMT, &fmt;
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);
3.请求并分配缓冲区
在设备采集之前需要为采集到的数据分配缓冲区,内核和驱动会负责填充这些缓冲区。但是这些缓冲区数据是保存在内核空间中,可以使用内存映射数据映射到用户空间。
typedef struct BufferSt
{
void *start;
unsigned int length;
} BufferSt; //用于管理地址映射的用户空间地址
struct v4l2_requestbuffers req;
struct v4l2_buffer buf;
req.count = 4;
req.memory = V4L2_MEMORY_MMAP;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == ioctl(fd, VIDIOC_REQBUFS, &req))
{
perror("VIDIOC_REQBUFS ERROR");
close(fd);
exit(-1);
}
buffer = (BufferSt *)calloc(req.count, sizeof(BufferSt));
for (int buf_index = 0; buf_index < req.count; buf_index++)
{
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.index = buf_index;
buf.memory = V4L2_MEMORY_MMAP;
if (-1 == ioctl(fd, VIDIOC_QUERYBUF, &buf))
{
perror("VIDIOC_QUERYBUF error\n");
goto error;
}
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)
{
perror("Mapping buffer\n");
goto error;
}
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1)
{
perror("VIDIOC_QBUF error\n");
goto error;
}
}
4.开启流采集
分配完内存后,就可以开始采集视频数据。开启流的方式非常简单,使用ioctl函数设置VIDIOC_STREAMON即可。
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
5.读数据
前面已经完成了内存地址映射,所以当采集到数据后可以直接从用户空间取出数据。为了能够知道什么时候去取数据,可以使用io复用监听是否有可读事件的发生。
struct pollfd pfd = {fd, POLL_IN};
while (1)
{
ret = poll(&pfd, 1, -1);
if (ret < 0)
{
perror("Polling for frame\n");
break;
}
for (unsigned int i = 0; i < req.count; ++i)
{
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ret = ioctl(fd, VIDIOC_DQBUF, &buf); // 出队列获取数据
if (ret != -1)
fwrite(buffer[buf.index].start, 1, buf.length, outfp); //将数据写入文件
ioctl(fd, VIDIOC_QBUF, &buf);
}
}
6.关闭流采集
完成采集工作之后需要手动关闭流,并释放缓冲区资源,同时需要注意文件符的关闭和内存清理等工作。
ioctl(fd, VIDIOC_STREAMOFF, &type);
ioctl(fd, VIDIOC_REQBUFS, &buf); // 释放所有缓冲区
二、完整代码
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/videodev2.h>
#include <linux/fb.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <poll.h>
#include <signal.h>
#define WIDTH 640
#define HEIGHT 480
#define CLEAR(x) memset(&(x), 0, sizeof(x))
typedef struct BufferSt
{
void *start;
unsigned int length;
} BufferSt;
void alarm_handler(int signum)
{
printf("Alarm signal received!\n");
exit(0);
}
int main()
{
int fd = -1;
struct v4l2_format fmt;
struct v4l2_buffer buf;
enum v4l2_buf_type type;
int ret;
BufferSt *buffer = nullptr;
FILE *outfp;
struct sigaction sa;
sa.sa_handler = alarm_handler;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGALRM, &sa, NULL) == -1)
{
perror("sigaction");
return 1;
}
// 打开视频设备
fd = open("/dev/video0", O_RDWR | O_NONBLOCK, 0);
struct pollfd pfd = {fd, POLL_IN};
alarm(10);
if (fd < 0)
{
perror("Opening video device");
return 1;
}
// 设置视频采集的格式
CLEAR(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_ANY;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1)
{
perror("VIDIOC_S_FMT ERROR");
close(fd);
exit(-1);
}
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);
struct v4l2_requestbuffers req;
CLEAR(req);
req.count = 4;
req.memory = V4L2_MEMORY_MMAP;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (-1 == ioctl(fd, VIDIOC_REQBUFS, &req))
{
perror("VIDIOC_REQBUFS ERROR");
close(fd);
exit(-1);
}
buffer = (BufferSt *)calloc(req.count, sizeof(BufferSt));
for (int buf_index = 0; buf_index < req.count; buf_index++)
{
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.index = buf_index;
buf.memory = V4L2_MEMORY_MMAP;
if (-1 == ioctl(fd, VIDIOC_QUERYBUF, &buf))
{
perror("VIDIOC_QUERYBUF error\n");
goto error;
}
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)
{
perror("Mapping buffer\n");
goto error;
}
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1)
{
perror("VIDIOC_QBUF error\n");
goto error;
}
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
// 打开文件准备写入YUV数据
outfp = fopen("output.yuv", "wb");
if (!outfp)
{
perror("Opening output file\n");
goto error;
}
// 使用poll等待视频数据
while (1)
{
ret = poll(&pfd, 1, -1);
if (ret < 0)
{
perror("Polling for frame\n");
break;
}
CLEAR(buf);
for (unsigned int i = 0; i < req.count; ++i)
{
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ret = ioctl(fd, VIDIOC_DQBUF, &buf); // 出队列获取数据
if (ret != -1)
fwrite(buffer[buf.index].start, 1, buf.length, outfp);
ioctl(fd, VIDIOC_QBUF, &buf);
}
}
error:
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
if (outfp)
{
fclose(outfp); // 关闭输出文件
outfp = nullptr;
}
for (unsigned int i = 0; i < req.count; ++i)
{
munmap(buffer[i].start, buf.length);
}
if (fd > 0)
{
close(fd);
fd = -1;
}
ioctl(fd, VIDIOC_STREAMOFF, &type);
ioctl(fd, VIDIOC_REQBUFS, &buf); // 释放所有缓冲区
return 0;
}