一、项目思路
实时监控系统/直播系统,通过V4L2接口从摄像头采集YUYV格式的视频;用H264对视频数据进行编码;然后通过RTMP协议发送至支持了rtmp的nginx流媒体服务器;用客户端使用VLC从服务器拉流显示。
V4L2----->H264----->RTMP------>nginx------>VLC
V4L2:linux下摄像头操作的框架(主要用来采集图像视频)
YUYV:图像格式
H264:音视频编码格式(图像的压缩算法)
RTMP:用来传输视频、音频流数据
nginx:反向代理服务器
VLC:一款开源的跨平台多媒体播放器和服务器
二、配置环境
参考:
Ubuntu16.04使用笔记本电脑摄像头操作方式_ubuntu16.04找不到摄像头图标-CSDN博客
三、相关技术
3.1、V4L2
V4L2(Video for Linux 2)是用于Linux操作系统的视频捕获框架,允许开发者访问和控制视频设备,如摄像头、摄像机和其他视频输入设备。
应用程序通过V4L2框架,对摄像头进行操作,如 设置摄像头的频率、图像参数、查看摄像头支持的配置等等;在使用V4L2框架采集摄像头时,主要步骤 1、打开设备;2、对设备进行配置;3、设置数据采集方式;4、处理数据;5、关闭设备。
摄像头属性相关结构体:
struct v4l2_format {
__u32 type; // 数据流类型(例如:V4L2_BUF_TYPE_VIDEO_CAPTURE)
union {
struct v4l2_pix_format pix; // 像素格式
// 更多格式类型可以通过 union 扩展
} fmt;
};
struct v4l2_pix_format {
__u32 width; // 图像宽度
__u32 height; // 图像高度
__u32 pixelformat; // 像素格式(例如:V4L2_PIX_FMT_YUV420)
// 更多参数可以根据不同的像素格式类型进行扩展
};
struct v4l2_capability {
char driver[16]; // 驱动程序名称
char card[32]; // 设备名称或描述
char bus_info[32]; // 连接总线信息
__u32 version; // V4L2 规范的版本号
__u32 capabilities; // 设备支持的能力标志
};
/*ioctl 查看支持的驱动*/
ret = ioctl(fd, VIDIOC_QUERYCAP, &capability);
if (ret == -1) {
perror("camera->init");
close(fd);
return -1;
}
3.2、YUV
YUV 是一种将彩色图像分离为亮度(Y)和色度(UV)信息的图像编码格式。常用有YUV422格式和YUV420格式。与RGB相比YUV422的大小是其2/3,而YUV420是其1/2。在YUV422格式中,按照U、V分量在时空上的排列顺序不同,可以将他们分为YUYV、YVYU、UYVY、VYUY四种不同的排列方式;在YUV420格式中,又分为I420(YU12)、YV12、NV12、NV21。
四、源代码
1.cam.h
#ifndef __CAM_H__
#define __CAM_H__
#define CAMERA_USB "/dev/video0"//填写自己video0文件路径
int camera_init(char *devpath, unsigned int *width, unsigned int *height, unsigned int *size, unsigned int *ismjpeg);
int camera_start(int fd);
int camera_dqbuf(int fd, void **buf, unsigned int *size, unsigned int *index);
int camera_eqbuf(int fd, unsigned int index);
int camera_stop(int fd);
int camera_exit(int fd);
#endif
2.cam.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include "cam.h"
/*
函数功能:用于进行设备和文件的输入/输出控制操作,可以向设备驱动程序发送特定的命令以及相关参数
函数名:int ioctl(int fd, unsigned long request, ...);
函数参数:fd 设备文件的文件描述符 request 表示要执行的 ioctl 命令或操作。
*/
#define REQBUFS_COUNT 4
struct cam_buf {
void *start;
size_t length;
};
struct v4l2_requestbuffers reqbufs;//用于请求视频缓冲区
struct cam_buf bufs[REQBUFS_COUNT];//用于存储视频缓冲区的信息
/*
功能: 初始化摄像头
函数名:camera_init
参数:devpath 摄像头的路径名 width采集图像的宽度 height采集图像的高度 size图像大小 ismjpeg图像的格式(1表示支持jpeg格式,非1表示其它格式)
返回值:成功返回摄像头文件描述符,失败返回-1
*/
int camera_init(char *devpath, unsigned int *width, unsigned int *height, unsigned int *size, unsigned int *ismjpeg)
{
int i;
int fd = -1;
int ret;
struct v4l2_buffer vbuf;//用于表示视频缓冲区的信息
struct v4l2_format format;//用于配置视频设备的格式
struct v4l2_capability capability;//获取视频设备相关信息
/*open 打开设备文件*/
if((fd = open(devpath, O_RDWR)) == -1){
perror("open:");
close(fd);
return -1;
}
/*ioctl 查看支持的驱动*/
ret = ioctl(fd, VIDIOC_QUERYCAP, &capability);//VIDIOC_QUERYCAP摄像头设备的基本信息
if (ret == -1) {
perror("camera->init");
close(fd);
return -1;
}
/*判断设备是否支持视频采集*/
if(!(capability.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "camera->init: device can not support V4L2_CAP_VIDEO_CAPTURE\n");
close(fd);
return -1;
}
/*判断设备是否支持视频流采集,*/
if(!(capability.capabilities & V4L2_CAP_STREAMING)) {
fprintf(stderr, "camera->init: device can not support V4L2_CAP_STREAMING\n");
close(fd);
return -1;
}
/*V4L2_CAP_VIDEO_CAPTURE,则可以使用它来捕获视频帧;V4L2_CAP_STREAMING,则可以配置它以进行实时视频流捕获。*/
if (*ismjpeg != 1){
/*设置捕获的视频格式 YUYV*/
memset(&format, 0, sizeof(format));//视频设备的格式format清空
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//视频捕获相关的参数
format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; //原始图像格式V4L2_PIX_FMT_YUYV
format.fmt.pix.width = *width;// 填充所需的宽度
format.fmt.pix.height = *height; // 填充所需的高度
format.fmt.pix.field = V4L2_FIELD_ANY;//表示图像字段可以是任何类型
ret = ioctl(fd, VIDIOC_S_FMT, &format); //通过ioctl函数将设置信息设置到驱动程序
if(ret == -1) {
perror("camera init");
close(fd);
return -1;
} else {
*ismjpeg = 0;
fprintf(stdout, "camera->init: picture format is yuyv\n");
goto get_fmt;
}
} else {
/*设置捕获的视频格式 MYJPEG*/
memset(&format, 0, sizeof(format));
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//永远都是这个类型
format.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;//设置采集图片的格式V4L2_PIX_FMT_MJPEG
format.fmt.pix.width = *width;
format.fmt.pix.height = *height;
format.fmt.pix.field = V4L2_FIELD_ANY; //设置图片一行一行的采集
ret = ioctl(fd, VIDIOC_S_FMT, &format); //ioctl 是设置生效
if(ret == -1){
perror("camera init");
close(fd);
return -1;
}else {
fprintf(stdout, "camera->init: picture format is mjpeg\n");
*ismjpeg = 1;
goto get_fmt;
}
}
//获取摄像头属性信息
get_fmt:
ret = ioctl(fd, VIDIOC_G_FMT, &format);
if (ret == -1) {
perror("camera init");
close(fd);
return -1;
}
/*向驱动申请缓存*/
memset(&reqbufs, 0, sizeof(struct v4l2_requestbuffers));//清空请求视频缓冲区
reqbufs.count = REQBUFS_COUNT; //缓存区个数
reqbufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbufs.memory = V4L2_MEMORY_MMAP; //设置操作申请缓存的方式:映射 MMAP
ret = ioctl(fd, VIDIOC_REQBUFS, &reqbufs);//用于分配4个内存映射类型的视频捕获缓冲区
if (ret == -1) {
perror("camera init");
close(fd);
return -1;
}
/*循环映射并入队*/
for (i = 0; i < reqbufs.count; i++){
/*真正获取缓存的地址大小*/
memset(&vbuf, 0, sizeof(struct v4l2_buffer));
vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vbuf.memory = V4L2_MEMORY_MMAP;//设置操作申请缓存的方式:映射 MMAP
vbuf.index = i;//查询的缓冲区的索引号
ret = ioctl(fd, VIDIOC_QUERYBUF, &vbuf);
if (ret == -1) {
perror("camera init");
close(fd);
return -1;
}
/*映射缓存到用户空间,通过mmap讲内核的缓存地址映射到用户空间,并切和文件描述符fd相关联*/
bufs[i].length = vbuf.length;
bufs[i].start = mmap(NULL, vbuf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, vbuf.m.offset);
if (bufs[i].start == MAP_FAILED) {
perror("camera init");
close(fd);
return -1;
}
/*每次映射都会入队,放入缓冲队列*/
vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vbuf.memory = V4L2_MEMORY_MMAP;
ret = ioctl(fd, VIDIOC_QBUF, &vbuf);
if (ret == -1) {
perror("camera init");
close(fd);
return -1;
}
}
/*VIDIOC_QUERYBUF 用于查询缓冲区的属性和状态,而 VIDIOC_QBUF 用于将缓冲区添加到捕获队列中*/
/*返回真正设置成功的宽.高.大小*/
*width = format.fmt.pix.width;
*height = format.fmt.pix.height;
*size = bufs[0].length;
return fd;
}
/*
函数功能:启动摄像头视频捕获
函数名:camera_start
函数参数:fd 摄像头文件描述符
*/
int camera_start(int fd)
{
int ret;
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
/*ioctl控制摄像头开始采集*/
ret = ioctl(fd, VIDIOC_STREAMON, &type);//VIDIOC_STREAMON 启动视频捕获流程
if (ret == -1) {
perror("camera->start");
return -1;
}
fprintf(stdout, "camera->start: start capture\n");
return 0;
}
/*
函数功能:摄像头数据出队
函数名:camera_dqbuf
函数参数:fd 摄像头文件描述符 buf 输出的图像数据 size输出图像数据大小 index 输出的哪一片缓冲队列
*/
int camera_dqbuf(int fd, void **buf, unsigned int *size, unsigned int *index)
{
int ret;
fd_set fds;
struct timeval timeout; //为IO多路复用设置时间的
struct v4l2_buffer vbuf;
while (1) {
FD_ZERO(&fds);
FD_SET(fd, &fds); //将摄像头添加到监听队列
timeout.tv_sec = 2;
timeout.tv_usec = 0;
ret = select(fd + 1, &fds, NULL, NULL, &timeout); //使用select机制,保证fd有图片的时候才能出对
if (ret == -1) {
perror("camera->dbytesusedqbuf");
if (errno == EINTR)
continue;
else
return -1;
} else if (ret == 0) {
fprintf(stderr, "camera->dqbuf: dequeue buffer timeout\n");
return -1;
} else {
vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vbuf.memory = V4L2_MEMORY_MMAP;
ret = ioctl(fd, VIDIOC_DQBUF, &vbuf); //出队,也就是从用户空间取出图片
if (ret == -1) {
perror("camera->dqbuf");
return -1;
}
*buf = bufs[vbuf.index].start;
*size = vbuf.bytesused;
*index = vbuf.index; //当前读走的缓冲区下标
return 0;
}
}
}
/*
函数功能:摄像头数据入队
函数名称:camera_eqbuf
函数参数:fd 摄像头文件描述符 index 下一帧数据入队缓冲区下标
*/
int camera_eqbuf(int fd, unsigned int index)
{
int ret;
struct v4l2_buffer vbuf;
vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vbuf.memory = V4L2_MEMORY_MMAP;
vbuf.index = index;
ret = ioctl(fd, VIDIOC_QBUF, &vbuf); //入队
if (ret == -1) {
perror("camera->eqbuf");
return -1;
}
return 0;
}
/*
函数功能:停止摄像头采集图片
函数名称:camera_stop
函数参数:fd 文件描述符
函数返回值:成功返回0,失败返回-1
*/
int camera_stop(int fd)
{
int ret;
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd, VIDIOC_STREAMOFF, &type);
if (ret == -1) {
perror("camera->stop");
return -1;
}
fprintf(stdout, "camera->stop: stop capture\n");
return 0;
}
/*
函数功能:关闭摄像头
函数名称:camera_exit
函数参数:fd 摄像头文件描述符
函数返回值:成功返回0,失败返回-1
注:退出之前需要将内核中的数据清空(出队),解除映射关系
*/
int camera_exit(int fd)
{
int i;
int ret;
struct v4l2_buffer vbuf;
vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vbuf.memory = V4L2_MEMORY_MMAP;
for (i = 0; i < reqbufs.count; i++) {
ret = ioctl(fd, VIDIOC_DQBUF, &vbuf);
if (ret == -1)
break;
}
for (i = 0; i < reqbufs.count; i++)
munmap(bufs[i].start, bufs[i].length);
fprintf(stdout, "camera->exit: camera exit\n");
return close(fd);
}
3.main.c
#include <stdio.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "cam.h"
int main()
{
unsigned int width = 640;
unsigned int height = 480;
unsigned int size; //存储图片大小的变量,由出队函数赋值
unsigned int ismjpeg = 1;
unsigned int index = -1;
//保存图片内容的地址。
char *jpg_buf = NULL;
//初始化摄像头
int fd = camera_init(CAMERA_USB,&width,&height,&size,&ismjpeg);
if (fd < 0){
return -1;
}
//开始采集
if (camera_start(fd) < 0){
return -1;
}
jpg_buf = (char *)malloc(width * height);
if (jpg_buf == NULL){
printf("malloc failed\n");
return -1;
}
int file_fd = -1;
for(int i = 0; i<100;i++)
{
//出队
camera_dqbuf(fd, &jpg_buf, &size, &index);
//入队
camera_eqbuf(fd,index);
file_fd= open("1.jpeg", O_CREAT | O_RDWR, 0640);
if (file_fd < 0){
perror("open file");
return -1;
}
printf( "write: %d\n", write(file_fd,jpg_buf, size));
}
close(file_fd);
camera_stop(fd);
camera_exit(fd);
}
五、测试
edu@edu:~/Nginx/code/V4L2$ gcc *.c
edu@edu:~/Nginx/code/V4L2$ ./a.out
目前项目已开源至 gitee:流媒体项目: 通过V4L2接口从摄像头采集YUYV格式的视频;用H264对视频数据进行编码;然后通过RTMP协议发送至支持了rtmp的nginx流媒体服务器;用客户端使用VLC从服务器拉流显示。