MJPG-streamer是一款免费基于IP地址的视频流服务器,它的输入插件从摄像头读取视频数据,这个输入插件产生视频数据并将视频数据复制到内存中,它有多个输出插件将这些视频数据经过处理,其中最重要的输出插件是网站服务器插件,它将视频数据传送到用户浏览器中,MJPG-streamer的工作就是将其中的一个输入插件和多个输出插件绑定在一起,所有的工作都是通过它的各个插件完成的。
(1).发送一个请求字符串
“GET /?action=snapshot\n”
"GET /?action=stream\n"
“GET /?action=command\n”
(2).再发送一次字符串
如果我们不使用密码功能!则只需发送任意长度为小于2字节的字符串,比如:“f\n”
如果发送的请求是:“GET /?action=snapshot\n”
(3).需要接收一次字符串(是服务器发过来的报文)
(4).接收一帧图片
如果发送的请求是:"GET /?action=stream\n"
(3).需要接收一次字符串(是服务器发过来的报文)
while(1)
{
(4).再接收一次报文,解析它,得到一帧图片的大小(size)
(5).接收size个字节的数据
}
服务器报文:
sprintf(buffer,“Content-Type: image/jjpeg\r\n”
“Content-Length: %d\r\n”
“\r\n”,frame_size);
电脑或安卓端的客户端
video_recv_manager.c
static PT_VideoRecv g_ptVideoRecvHead = NULL; //链表头
//注册
int RegisterVideoRecv(PT_VideoRecv ptVideoRecv){
PT_VideoRecv ptTmp;
if (!g_ptVideoRecvHead){
g_ptVideoRecvHead = ptVideoRecv;
ptVideoRecv->ptNext = NULL;
}
else{
ptTmp = g_ptVideoRecvHead;
while (ptTmp->ptNext)
ptTmp = ptTmp->ptNext;
ptTmp->ptNext = ptVideoRecv;
ptVideoRecv->ptNext = NULL;
}
return 0;
}
//根据名字取出指定的"获取视频模块"
PT_VideoRecv GetVideoRecv(char *pcName){
PT_VideoRecv ptTmp = g_ptVideoRecvHead;
while (ptTmp){
if (strcmp(ptTmp->name, pcName) == 0)
return ptTmp;
ptTmp = ptTmp->ptNext;
}
return NULL;
}
//调用各模块的初始化函数
int VideoRecvInit(void){
return Video_Recv_Init();
}
video_recv.c
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 VideoRecv {
char *name;
int (*Connect_To_Server)(int *SocketClient, const char *ip); //连接到服务器
int (*DisConnect_To_Server)(int *SocketClient); //断开服务器
int (*Init)(int *SocketClient); //客户端初始化
int (*GetFormat)(void); //获取视频格式
int (*Get_Video)(int *SocketClient, PT_VideoBuf ptVideoBuf); //获取视频
struct VideoRecv *ptNext;
}T_VideoRecv, *PT_VideoRecv;
分配设置注册VideoRecv结构体
/* 构造 */
static T_VideoRecv g_tVideoRecv = {
.name = "http",
.Connect_To_Server = connect_to_server, //连接到服务器
.DisConnect_To_Server = disconnect_to_server, //断开服务器
.Init = init, //初始化
.GetFormat = getformat, //获取视频格式
.Get_Video = get_video, //获取视频
};
/* 注册 */
int Video_Recv_Init(void){
return RegisterVideoRecv(&g_tVideoRecv);
}
连接到服务器
断开服务器
获得视频的格式
static int connect_to_server(int *SocketClient, const char *ip){
struct sockaddr_in tSocketServerAddr;
*SocketClient = socket(AF_INET, SOCK_STREAM, 0);
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
//tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
inet_aton(ip, &tSocketServerAddr.sin_addr);
memset(tSocketServerAddr.sin_zero, 0, 8);
connect(*SocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
return 0;
}
static int disconnect_to_server(int *SocketClient){
close(*SocketClient);
return 0;
}
static int getformat(void){
return V4L2_PIX_FMT_MJPEG; /* 直接返回视频的格式 */
}
初始化函数
1.发送一个请求字符串:“GET /?action=stream\n”
2.再发送一次字符串(不使用密码)
3.需要接收一次字符串(是服务器发过来的报文)
static int init(int *SocketClient){
char ucSendBuf[100]; //发送缓冲区
int iSendLen;
int iRecvLen;
unsigned char ucRecvBuf[1000]; //接收缓冲区
/* 发请求类型字符串 */
memset(ucSendBuf, 0x0, 100);
strcpy(ucSendBuf, "GET /?action=stream\n");
iSendLen = send(*SocketClient, ucSendBuf, strlen(ucSendBuf), 0);
if (iSendLen <= 0){
close(*SocketClient);
return -1;
}
/* 如果我们不使用密码功能!则只需发送任意长度为小于2字节的字符串 */
memset(ucSendBuf, 0x0, 100);
strcpy(ucSendBuf, "f\n");
iSendLen = send(*SocketClient, ucSendBuf, strlen(ucSendBuf), 0);
if (iSendLen <= 0){
close(*SocketClient);
return -1;
}
/* 将从服务器端接收一次报文 */
/* 接收客户端发来的数据并显示出来 */
iRecvLen = recv(*SocketClient, ucRecvBuf, 999, 0);
if (iRecvLen <= 0){
close(*SocketClient);
return -1;
}
else{
ucRecvBuf[iRecvLen] = '\0';
printf("http header: %s\n", ucRecvBuf);
}
return 0;
}
获取视频get_video
3.需要接收一次字符串(是服务器发过来的报文)
while(1)
{
4.再接收一次报文,解析它,得到一帧图片的大小(size)
5.接收size个字节的数据
}
获取一帧数据要上锁,原子操作。
static int get_video(int *SocketClient, PT_VideoBuf ptVideoBuf){
long int video_len, iRecvLen; //一帧视频长度,接收视频长度
int FirstLen = 0; //第1次获取视频大小长度
char tmpbuf[1024];
char *FreeBuffer = NULL; //剩余视频缓冲区
while(1){
video_len = getFileLen(SocketClient, tmpbuf, &FirstLen); //获取一帧视频大小
iRecvLen = http_recv(SocketClient, &FreeBuffer, video_len - FirstLen); //获取剩余视频数据
pthread_mutex_lock(&ptVideoBuf->db);
/* 将两次接收到的视频数据组装成一帧数据 */
memcpy(ptVideoBuf->tPixelDatas.aucPixelDatas, tmpbuf, FirstLen); //第一次获得视频
memcpy(ptVideoBuf->tPixelDatas.aucPixelDatas+FirstLen, FreeBuffer, iRecvLen); //第二次获得视频
ptVideoBuf->tPixelDatas.iTotalBytes = video_len;
pthread_cond_broadcast(&ptVideoBuf->db_update); // 发出一个数据更新的信号,通知发送通道来取数据
pthread_mutex_unlock( &ptVideoBuf->db ); // 原子操作结束
}
return 0;
}
获得一帧视频大小,参数:客户端,视频数据大小和长度(输出参数)
通过字符串判断是否为报文,/r/n/r/n后面就是一帧视频的数据,要保存在缓冲区。
服务器报文:
sprintf(buffer,“Content-Type: image/jjpeg\r\n”
“Content-Length: %d\r\n”
“\r\n”,frame_size);
static long int getFileLen(int *SocketClient, char *FreeBuf, int *FreeLen){
int iRecvLen;
long int videolen; //视频长度
char ucRecvBuf[1024]; //接收缓冲区
char *plen, *buffp;
while(1){
iRecvLen = recv(*SocketClient, ucRecvBuf, 1024, 0); //接收1k数据(一次最多1024)
/* 解析ucRecvBuf,判断接收到的数据是否是报文 */
plen = strstr(ucRecvBuf, "Length:"); //字符串查找报文
if(NULL != plen){
plen = strchr(plen, ':'); //字符查找
plen++;
videolen = atol(plen); //字符串转整数,获得视频长度
printf("the Video Len %ld\n", videolen);
}
buffp = strstr(ucRecvBuf, "\r\n\r\n");
if(buffp != NULL)
break;
}
buffp += 4; //指向视频数据
*FreeLen = 1024 - (buffp - ucRecvBuf); //视频数据长度
memcpy(FreeBuf, buffp, *FreeLen); //保存视频数据
return videolen;
}
视频接收函数,参数:socket句柄,剩余视频缓冲区地址,获取大小
static long int http_recv(int *SocketClient, char **lpbuff, long int size){
int iRecvLen = 0, RecvLen = 0; //单次接收大小,总接收大小
char ucRecvBuf[BUFFER_SIZE]; //接收缓冲区
while(size > 0){ //1次最大1024,多次接收,size可能很大
iRecvLen = recv(*SocketClient, ucRecvBuf, (size > BUFFER_SIZE)? BUFFER_SIZE: size, 0);
if (iRecvLen <= 0)
break;
RecvLen += iRecvLen;
size -= iRecvLen;
if(*lpbuff == NULL){ //如果缓冲区为空,分配空间
*lpbuff = (char *)malloc(RecvLen);
if(*lpbuff == NULL)
return -1;
}
else{ //如果缓冲区不为空,之前已分配,重新分配
*lpbuff = (char *)realloc(*lpbuff, RecvLen);
if(*lpbuff == NULL)
return -1;
}
memcpy(*lpbuff+RecvLen-iRecvLen, ucRecvBuf, iRecvLen); //视频数据拷贝进缓存
}
return RecvLen; //返回接收总长度
}
main函数
PT_VideoRecv g_ptVideoRecvOpr;
int iSocketClient;
//获取摄像头数据的线程:获取一帧视频
void *RecvVideoThread(void *data){
g_ptVideoRecvOpr->Get_Video(&iSocketClient, (PT_VideoBuf)data);
return data;
}
/* video2lcd </dev/video0,1,...> */
int main(int argc, char **argv){
if (argc != 2){
printf("Usage:\n");
printf("%s <ip>\n", argv[0]);
return -1;
}
/* 一系列的初始化 */
DisplayInit(); /* 注册显示设备 */
/* 可能可支持多个显示设备: 选择和初始化指定的显示设备 */
SelectAndInitDefaultDispDev("crt");
GetDispResolution(&iLcdWidth, &iLcdHeigt, &iLcdBpp);
GetVideoBufForDisplay(&tFrameBuf);
iPixelFormatOfDisp = tFrameBuf.iPixelFormat;
VideoRecvInit(); //视频接收模块初始化
g_ptVideoRecvOpr = GetVideoRecv("http");
iPixelFormatOfVideo = g_ptVideoRecvOpr->GetFormat();
VideoConvertInit();
ptVideoConvert = GetVideoConvertForFormats(iPixelFormatOfVideo, iPixelFormatOfDisp);
/* 启动摄像头设备 */
g_ptVideoRecvOpr->Connect_To_Server(&iSocketClient, argv[1]); //连接到服务器
g_ptVideoRecvOpr->Init(&iSocketClient); //视频接收模块初始化
memset(&tVideoBuf, 0, sizeof(tVideoBuf));
tVideoBuf.tPixelDatas.aucPixelDatas = malloc(30000);
memset(&tConvertBuf, 0, sizeof(tConvertBuf));
tConvertBuf.iPixelFormat = iPixelFormatOfDisp;
tConvertBuf.tPixelDatas.iBpp = iLcdBpp;
pthread_mutex_init(&tVideoBuf.db, NULL); /* 初始化 global.db(自旋锁) 成员 */
pthread_cond_init(&tVideoBuf.db_update, NULL); /* 初始化 global.db_update(条件变量) 成员 */
/* 创建获取摄像头数据的线程 */
pthread_create(&RecvVideo_Id, NULL, &RecvVideoThread, &tVideoBuf);
while (1){
pthread_cond_wait(&tVideoBuf.db_update, &tVideoBuf.db); //等待数据更新
ptVideoBufCur = &tVideoBuf;
if (iPixelFormatOfVideo != iPixelFormatOfDisp){ //格式转换
/* 转换为RGB */
ptVideoConvert->Convert(&tVideoBuf, &tConvertBuf);
DBG_PRINTF("Convert %s, ret = %d\n", ptVideoConvert->name, iError);
ptVideoBufCur = &tConvertBuf;
}
/* 合并进framebuffer */
/* 接着算出居中显示时左上角坐标 */
iTopLeftX = (iLcdWidth - ptVideoBufCur->tPixelDatas.iWidth) / 2;
iTopLeftY = (iLcdHeigt - ptVideoBufCur->tPixelDatas.iHeight) / 2;
PicMerge(iTopLeftX, iTopLeftY, &ptVideoBufCur->tPixelDatas, &tFrameBuf.tPixelDatas);
FlushPixelDatasToDev(&tFrameBuf.tPixelDatas); /* 把framebuffer的数据刷到LCD上, 显示 */
}
pthread_detach(RecvVideo_Id); // 等待线程结束,以便回收它的资源
return 0;
}
在主函数中创建接收视频线程RecvVideoThread,调用Get_Video函数获取一帧数据。
get_video函数中:
3.需要接收一次字符串(是服务器发过来的报文)
while(1){
4.再接收一次报文,解析它,得到一帧图片的大小(size)
5.接收size个字节的数据
pthread_mutex_lock(&ptVideoBuf->db); 上锁
memcpy(ptVideoBuf->tPixelDatas.aucPixelDatas, tmpbuf, FirstLen); //第一次获得视频
memcpy(ptVideoBuf->tPixelDatas.aucPixelDatas+FirstLen, FreeBuffer, iRecvLen); //第二次获得视频
pthread_cond_broadcast(&ptVideoBuf->db_update); // 发出一个数据更新的信号,通知发送通道来取数据
pthread_mutex_unlock( &ptVideoBuf->db ); 解锁
}
主函数:
while (1){
pthread_cond_wait(&tVideoBuf.db_update, &tVideoBuf.db); //等待数据更新
ptVideoBufCur = &tVideoBuf
转换为RGB
合并进framebuffer
PicMerge(iTopLeftX, iTopLeftY, &ptVideoBufCur->tPixelDatas, &tFrameBuf.tPixelDatas);
FlushPixelDatasToDev(&tFrameBuf.tPixelDatas); /* 把framebuffer的数据刷到LCD上, 显示 */
}