简单视频监控项目的设计与实现(二)
之前简单分析了mjpg-streamer的源码,了解了数据收发的基本流程,现在我们可以编写客户端接收程序了。
我们知道server_thread线程会监听客户端连接,一旦有客户端连接,便创建一个子线程client_thread来处理。子线程中会读取客户端发来的一串字符串,解析并判断是什么请求(“GET /?action=snapshot”,“GET /?action=stream”,“GET /?action=command”),保存下来,然后在从客户端读取一串数据,判断是否有用户名和密码,若有用户名和密码则还要进行解码匹配。最后根据请求类型执行不同的动作。
我们这次实现的是视频监控,传输的是视频流,所以请求类型应该是流类型的,即"GET /?action=stream"。
这里将客户端实现的主要代码简单分析下
1、首先,建立连接
connect:
...
#define SERV_PORT 8080
static int MjpegConnect(int *sockfd,const char* ip)
{
int ret;
struct sockaddr_in servaddr;
*sockfd = socket(AF_INET,SOCK_STREAM,0);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
ret = inet_aton(ip, &servaddr.sin_addr);
if(ret == 0)
{
printf("fail to ip->addr.s_addr\n");
return -1;
}
memset(servaddr.sin_zero, 0, 8);
ret = connect(*sockfd,(const struct sockaddr *)&servaddr, sizeof(struct sockaddr));
if(ret < 0)
{
printf("connect failed\n");
return -1;
}
return 0;
}
2、发送请求
static int MjpegInit(int *sockfd)
{
//向服务器发送GET请求报文,请求获取图像
char buf[100];
unsigned char recvbuf[1000];
int recvlen;
int sendlen;
memset(buf,0x0,100);
strcpy(buf,"GET /?action=stream\n");
//ssize_t send(int sockfd, const void *buf, size_t len, int flags); flags为0时,与write是同等的
sendlen = send(*sockfd,buf,strlen(buf),0);
if(sendlen <= 0)
{
close(*sockfd);
return -1;
}
//如果我们不使用密码功能!则只需发送任意长度为小于2字节的字符串,如"f\n"
memset(buf,0x0,100);
strcpy(buf,"f\n");
sendlen = send(*sockfd,buf,strlen(buf),0);
if(sendlen <= 0)
{
close(*sockfd);
return -1;
}
//从服务器接收回应报文 200 OK
//ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
recvlen = recv(*sockfd,recvbuf,999,0);
if(recvlen <= 0)
{
close(*sockfd);
return -1;
}else
{
recvbuf[recvlen] = '\0';
printf("http header: %s\n", recvbuf);
}
return 0;
}
3、获取一帧图像数据
注意,服务器会发送一段带有头部信息的数据,头部信息告诉客户端本次发送的一帧图片的大小,所以在接受时需要将头部信息读取后去除。
typedef struct VideoBuf {
T_PixelDatas tPixelDatas;
int iPixelFormat; //像素格式
/* signal fresh frames */
pthread_mutex_t db;
pthread_cond_t db_update;
}T_VideoBuf, *PT_VideoBuf;
/* 图片的象素数据 */
typedef struct PixelDatas {
int iWidth; /* 宽度: 一行有多少个象素 */
int iHeight; /* 高度: 一列有多少个象素 */
int iBpp; /* 一个象素用多少位来表示 */
int iLineBytes; /* 一行数据有多少字节 */
int iTotalBytes; /* 所有字节数 */
unsigned char *aucPixelDatas; /* 象素数据存储的地方 */
}T_PixelDatas, *PT_PixelDatas;
static int MjpegGetFrames(int *sockfd, PT_VideoBuf ptVideoBuf)
{
//获取图像数据
//服务器会先发送一个头部信息buffer,告诉客户端即将发送图片的格式和大小,然后紧接着发出一帧图片数据
//Content-Type: image/jpeg\r\n Content-Length: %d\r\n\r\n"
//需要将头部信息后的图像数据取出
long int videolen, irecvlen;
int flen = 0;
char tmpbuf[1024];
char *fbuf = NULL;
while(1)
{
videolen = get_head_len(sockfd,tmpbuf,&flen);
irecvlen = get_for_one_frame(sockfd,&fbuf,videolen - flen);
//将两次获得的数据封装成一个
//原子操作
pthread_mutex_lock(&ptVideoBuf->db);
memcpy(ptVideoBuf->tPixelDatas.aucPixelDatas,tmpbuf,flen);
memcpy(ptVideoBuf->tPixelDatas.aucPixelDatas+flen,fbuf,irecvlen);
ptVideoBuf->tPixelDatas.iTotalBytes = videolen;
pthread_cond_broadcast(&ptVideoBuf->db_update); //唤醒阻塞在条件变量上的线程
pthread_mutex_unlock( &ptVideoBuf->db ); // 原子操作结束
}
return 0;
}
//获取一帧数据的长度,保存头部信息后的图像数据
static long int get_head_len(int *sockfd,char *freebuf,int *freelen)
{
int recvlen;
char recvbuf[1024];
char* pos,*buff;
long int videolen;
while(1)
{
recvlen = recv(*sockfd,recvbuf,1024,0);
if(recvlen <= 0)
{
close(*sockfd);
return -1;
}
//解析数据信息,判断是否是报文,获取一帧长度
//printf("recvbuf is %s\n",recvbuf);
pos = strstr(recvbuf,"Length:");
if(pos != NULL)
{
pos = strchr(pos,':');
pos++;
videolen = atol(pos);
printf("data length is %ld\n",videolen);
}
//跳出循环
buff = strstr(recvbuf,"\r\n\r\n");
if(buff != NULL)
break;
}
buff += 4;
*freelen = 1024 - (buff - recvbuf);
//printf("free size is %d\n",*freelen);
//拷贝剩余数据
memcpy(freebuf,buff,*freelen);
return videolen;
}
//数据组合
static long int get_for_one_frame(int *sockfd, char **pbuff, long int size)
{
int irecvLen = 0, recvLen = 0;
char recvbuf[1024];
//如果一帧剩余数据size > 1024,则反复接受,直到接收到全部数据
while(size > 0)
{
irecvLen = recv(*sockfd, recvbuf, (size > 1024)? 1024: size, 0);
if (irecvLen <= 0)
break;
//recvLen最终等于size
recvLen += irecvLen;
size -= irecvLen;
if(*pbuff == NULL)
{
*pbuff = (char *)malloc(recvLen);
if(*pbuff == NULL)
return -1;
}
else
{
//若pbuff不为空,则重新分配recvLen大的空间
*pbuff = (char *)realloc(*pbuff, recvLen);
if(*pbuff == NULL)
return -1;
}
memcpy(*pbuff+recvLen-irecvLen, recvbuf, irecvLen);
}
return recvLen;
}
4、将图像显示
先要将获得的MJPEG格式的图像转换成RGB格式,然后再通过svgalib库进行显示。
转换函数就不多说了,网上有很多资源。
图像的显示也很简单,就是利用svgalib库提供的函数将转换后的像素数据写入到创建的虚拟屏幕的显存中即可。
static int CRTShowPage(PT_PixelDatas ptPixelDatas)
{
int x, y;
unsigned int *pdwColor = (unsigned int *)ptPixelDatas->aucPixelDatas;
unsigned int dwColor;
unsigned int dwRed, dwGreen, dwBlue;
if (ptPixelDatas->iBpp != 32)
{
return -1;
}
//g_tCRTOpr.iYres,g_tCRTOpr.iXres是屏幕的分辨率
for (y = 0; y < g_tCRTOpr.iYres; y++)
{
for (x = 0; x < g_tCRTOpr.iXres; x++)
{
/* 0x00RRGGBB */
dwColor = *pdwColor++;
dwRed = (dwColor >> 16) & 0xff;
dwGreen = (dwColor >> 8) & 0xff;
dwBlue = (dwColor >> 0) & 0xff;
/*
* #include <vgagl.h>
* void gl_setpixelrgb(int x ,int y ,int r ,int g ,int b );
* gl_setpixelrgb在位置(x,y)上绘制单个像素
*/
gl_setpixelrgb(x, y, dwRed, dwGreen, dwBlue);
}
}
/*
* #include <vgagl.h>
* void gl_copyscreen(GraphicsContext * gc );
* 将当前图形上下文内容(屏幕数据)复制到指定的图形上下文(例如,物理屏幕)。假定上下文大小相同。
*/
gl_copyscreen(physicalscreen);
return 0;
}
5、显示效果
结语
这个视频监控项目还是比较简单的,蛮适合用来练练手,从整个项目做的过程中感受最深的主要是锻炼了分析问题解决问题的能力吧,还有培养面向对象的思想,韦老师的项目基本上都是按照面向对象的思想来分析的,这一点还是很不错的。
不足之处就是视频的显示还是有点卡顿的,依赖的vga库目前只能在32位虚拟机上测试,目前正在学QT,后面想用QT写一个客户端来显示画面。我也阅读了许多关于视频监控的文章,有一个思路就是在开发板上利用UVC摄像头采集到原始数据后,进行H264编码压缩,然后通过RTP协议发送到远程客户端,在客户端上实现数据的解码和显示。编码解码可以通过FFMPEG实现,客户端可以用QT编写。这个思路后面在仔细研究研究。
由于是初学,难免有些错误,如有错误还请各路大神指正,谢谢。
本文地址:https://blog.csdn.net/ChenNightZ/article/details/108183331