自己写精简版mjpg-streamer(含工程)

前言

从去年九月就有想过把mjpg-streamer的http推流精简化了,结果一直拖到现在,而且到现在也只是简单的实现了,工程仍存在bug,那不管怎么样,这个demo终归还是实现了。这个工程可以说是mjpg-streamer的极度精简版,可以将Linux下使用摄像头+WIFI传输图像,简单来说就是一个简单的无线图传。在树莓派ZERO W下测试,在网络好的情况下,达到30帧也是没问题的。

这是工程的网址:https://github.com/Hectoor/Simple-version-of-mjpg-streamer

工程结构

要想实现无线图传,主要分为三个要点,一个是实现图像的获取,另一个是实现网络的传输,最后是要考虑图像获取和网络传输如何结合起来。下面的图大致地描述了整个工程的思路,主要是参考了mjpg-streamer的思路。
在这里插入图片描述
一步一步来,先从获取图像开始。

采集图像

由于主控核心板使用Linux系统平台,故本设计采集摄像头数据使用Linux下的视频采集框架V4L2(Linux for Video 2)。在Linux系统平台下,一切皆为文件,视频设备属于设备文件,可对其进行读写操作。V4L2框架提供部分摄像头的底层驱动,主要针对USB摄像头,通常情况下只需要摄像头支持UVC(USB VIDEO CAMERA)协议,即可直接通过调用V4L2的软件接口(API)进行采集。

这里有个注意的地方,如果你使用的是树莓派专用摄像头,可能会发现摄像头打开不了,即/dev下没有设备。可以参考这篇博文:树莓派无法打开摄像头问题

当时碰到一个难题,就是mjpeg保存成一张图片,使用电脑打开是看不到的。也就是说,除了把MJPEG提取出来,还需要转换成JPEG才能在电脑上显示出来,这让我使用了YUYV格式,再用libjpeg转换成JPEG,然后再进行推流。显然,这个速度是比直接采集MJPEG慢很多的,在树莓派ZERO W上跑每秒最多只有5帧,这个效果是很差的。

但是这周我又研究了一下Linux下的V4L2(Video For Linux 2),发现直接提取MJPEG并保存为JPEG格式就可以直接在电脑上显示了。这意味着我之前的代码有问题,提取出来的数据不正确。

MJPEG和JPEG的区别

在电脑能直接显示MJPEG数据的JPEG格式的图像,但这并不意味着这两种格式(MJPEG和JPEG)就没有区别,关于这个区别,在谷歌也没有很多的资料,以下是谷歌出来的区别:

1、JPEG是单页文件格式。Motion JPEG是静态照片的JPEG标准的动态视频改编。MJPEG将视频流视为一系列静态照片,单独压缩每个帧,不使用帧间压缩。

2、使用MJPG像素格式,帧率更高(约30 fps),每帧的字节6,7,8,9(从0开始索引)为’J’,‘F’,‘I’,‘F’ 。如果我使用JPEG,帧速率较低(约6 fps),相同的字节为’E’,‘x’,‘i’,‘f’

如上所述,我个人认为JPEG和MJPEG的差距只是在图像信息上有一些区别,但是在图像数据是没有太大的差异的,当然我也不清楚我这样理解是否正确,希望有知道的小伙伴能指正我的错误。

要写一个mjpg-streamer精简版,首先得获取到摄像头图像的数据。

循环获取图像结构

驱动摄像头并获得一帧图像数据有十一个步骤,分别是:打开设备、检查是否是摄像头设备、设置参数、检查参数是否设置正确、请求缓冲区存储图像数据、配置内存映射、打开摄像头输入开关、从缓冲区取出一帧图像数据、关闭摄像头输入开关、撤销映射、关闭摄像头设备。

在本系统中,仅仅采集一张图片是不够的。进行实时视频传输,则需要不断地进行图像采集、读取和传输。根据上述流程,当打开视频数据输入之后,摄像头模块不断采集数据存至缓冲区,每个缓冲区设置四块,分别代表四张图像,当使用VIDIOC_QBUF接口时,缓存采集到的最新的四张图像数据到四块缓冲块里;当用VIDIOC_DQBUF接口时,将缓冲区中的其中一块图像数据取出。使用了四次VIDIOC_DQBUF指令后,缓存区数据为空,需再次使用VIDIOC_QBUF将最新采集到的图像数据填入缓冲区。
在这里插入图片描述

获取单张图像并保存demo

为了体现效果,这里提供了获取单张图像并保存起来的demo,请读者自行尝试,以保证V4L2是否跑通。直接上代码,里面都有详细注释的。

/*
 *  name:camera.c
 *  author:Hector Cheung
 *  Time: 4, 9, 2019
 */
#include <stdio.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <linux/videodev2.h>
#include<sys/ioctl.h>
#include <setjmp.h>
#include <unistd.h>

#define CAM_DEV "/dev/video0"
#define   WIDTH   640                       // 图片的宽度
#define   HEIGHT  480                       // 图片的高度
#define  NB_BUFFER  4	//memory block number
struct pic_data 
{
	unsigned char *tmpbuffer[NB_BUFFER];
	unsigned int tmpbytesused[NB_BUFFER];
}pic;


int cam_fd;

/*
Description.:Initalize V4L2 driver.
*/
int v4l2_init(void)
{
	int i;
	int ret = 0;
	
	// 1、Open camera device
	if((cam_fd = open(CAM_DEV,O_RDWR)) == -1)
	{
		perror("ERROR opening V4L interface.");
		return -1;
	}
	
	// 2、Judge if the device is a camera device 
	struct v4l2_capability cam_cap;
	if(ioctl(cam_fd,VIDIOC_QUERYCAP,&cam_cap) == -1)
	{
		perror("Error opening device %s: unable to query device.");
		return -1;
	}
	if((cam_cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == 0) 
    {
		perror("ERROR video capture not supported.");
        return -1;
    }

	// 3、Setting output parameter.
	struct v4l2_format v4l2_fmt;
	v4l2_fmt.type = V4L2_CAP_VIDEO_CAPTURE;
	v4l2_fmt.fmt.pix.width = WIDTH;
	v4l2_fmt.fmt.pix.height = HEIGHT;
	v4l2_fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
	if (ioctl (cam_fd, VIDIOC_S_FMT, &v4l2_fmt) == -1) 
	{	
		perror("ERROR camera VIDIOC_S_FMT Failed.");
		return -1;
	}
	// 4、Check whether the parameters are set successfully 
	if (ioctl (cam_fd, VIDIOC_G_FMT, &v4l2_fmt) == -1) 
	{
		perror("ERROR camera VIDIOC_G_FMT Failed.");
		return -1;
	}
	if (v4l2_fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG)
	{
		printf("Set VIDIOC_S_FMT sucessful\n");
	}
	// 5、Require buffer to store image data
	struct v4l2_requestbuffers v4l2_req;
	v4l2_req.count = NB_BUFFER;
	v4l2_req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	v4l2_req.memory = V4L2_MEMORY_MMAP;
	if (ioctl (cam_fd, VIDIOC_REQBUFS, &v4l2_req) == -1) 
	{
		perror("ERROR camera VIDIOC_REQBUFS Failed.");
		return -1;
	} 
	// 6、Start memory map
	struct v4l2_buffer v4l2_buf;
	v4l2_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	v4l2_buf.memory = V4L2_MEMORY_MMAP;
	  for(i = 0; i < NB_BUFFER; i++) 
   {
		v4l2_buf.index = i;
		if(ioctl(cam_fd, VIDIOC_QUERYBUF, &v4l2_buf) < 0)
		{
			perror("Unable to query buffer.");
			return -1;
		}
		
		pic.tmpbuffer[i] = mmap(NULL, v4l2_buf.length, PROT_READ, MAP_SHARED, cam_fd, v4l2_buf.m.offset);
		if(pic.tmpbuffer[i] == MAP_FAILED)
		{
			 perror("Unable to map buffer.");
			 return -1;
		}
        if(ioctl(cam_fd, VIDIOC_QBUF, &v4l2_buf) < 0)
		{
			perror("Unable to queue buffer.");
			return -1;
		}
   }
	//7、Open stream input 
	int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
   	if(ioctl(cam_fd, VIDIOC_STREAMON, &type) < 0)
	{
		perror("Unable to start capture.");
		return -1;
	}
	return 0;
}

/*
Description.:Get a jpeg image and save.
*/
int v4l2Grab(void)
{
	//8、Get a image 
	struct v4l2_buffer buff;
	buff.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	buff.memory = V4L2_MEMORY_MMAP;
	if(ioctl(cam_fd, VIDIOC_DQBUF, &buff) < 0)
	{
		printf("camera VIDIOC_DQBUF	Failed.\n");
		return -1;
	}
	
	pic.tmpbytesused[buff.index] = buff.bytesused;
	printf("size : %d\n",pic.tmpbytesused[buff.index]);			  
	
	//9、Save image.
	int jpg_fd = open("1.jpeg",O_RDWR|O_CREAT,00700);
	if(jpg_fd == -1)
   	{
		printf("open ipg Failed!\n ");
		return -1;		
   	}
	int writesize =  write(jpg_fd, pic.tmpbuffer[buff.index], pic.tmpbytesused[buff.index]);
	printf("Write successfully size : %d\n",writesize);
	close(jpg_fd);
/*
	FILE *fp = NULL;
	fp = fopen("test.txt", "w");
	if(fp != NULL)
	{
		fwrite(pic[0].tmpbuffer, 1, pic[0].tmpbytesused, fp);
		sync();
		fclose(fp);
	}
*/	
	//10、Queue the buffers.
	if(ioctl(cam_fd, VIDIOC_QBUF, &buff) < 0)
	{
		printf("camera VIDIOC_QBUF Failed.");
		return -1;
	}
	return 0;
}

/*
Description.:Release resource
*/
int v4l2_close(void)
{
	// Remove mmap.
	int i;
	for(i=0; i<NB_BUFFER; i++)
		munmap(pic.tmpbuffer[i],pic.tmpbytesused[i]);
	close(cam_fd);
	return 0;
}

/*
Description.:main
*/
int main(int argc, char* argv[])
{
	v4l2_init();			
	v4l2Grab();		
	v4l2_close();			
	return 0;
}

运行后可以直接在电脑上打开。
在这里插入图片描述

网络传输

在Linux系统下通用的网络编程是通过socket套接字接口来实现的。socket套接字是一种特殊的I/O接口,也是一种文件描述符,可以对其进行读写。其中,嵌入式设备充当服务器的角色,Android智能手机/PC充当客户端的角色。服务器(即嵌入式设备)需四个步骤进行初始化。
在这里插入图片描述

发布了42 篇原创文章 · 获赞 39 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/Hanghang_/article/details/89161427