0.自己写客服端(流程)
(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个字节的数据
}
1、现象演示
(1)在开发板上运行mjpeg-streamer服务器
在串口上运行以下指令
(2)进入虚拟机,让虚拟机中的ubuntu系统连接上wifi热点(点右上角的那个标志)S3C2440
(3)让虚拟机进入文本模式
按住ctrl+alt+F1,然后输入用户名和密码
(3)进入到mjpeg-streamer客户端的路径,运行客户端,跟的参数是开发板的IP地址,运行后可以看到摄像头采集到的数据
2、客户端程序编写
新建video_recv(视频接收)目录,有video_recv.c文件,video_recv_manager.c文件,在include目录下写头文件video_recv_manager.h 并修改相应的Makefile,修改顶层目录的Makefile,将编译出来的结果改名为mjpg_streamer_client
3、video_recv_manager.h
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);//链接到mjpg-streamer服务器上(socket文件句柄的指针,IP地址字符串)
int (*DisConnect_To_Server)(int *SocketClient);//断开链接(socket文件句柄的指针)
int (*Init)(int *SocketClient);//初始化函数
int (*GetFormat)(void);//获得视频格式
int (*Get_Video)(int *SocketClient, PT_VideoBuf ptVideoBuf);//获得视频函数(socket文件句柄的指针,PT_VideoBuf 结构体)
struct VideoRecv *ptNext; //用于多个结构体的联系
}T_VideoRecv, *PT_VideoRecv;
int VideoRecvInit(void);
void ShowVideoRecv(void);
int RegisterVideoRecv(PT_VideoRecv ptVideoRecv);
int Video_Recv_Init(void);
PT_VideoRecv GetVideoRecv(char *pcName);
4、实现video_recv_manager.c
结构体指针PT_VideoRecv
staticPT_VideoRecv g_ptVideoRecvHead = NULL;
/**********************************************************************
* 函数名称: RegisterVideoConvert
* 功能描述: 注册"字体模块", 所谓字体模块就是取出字符位图的方法
* 输入参数: ptVideoConvert - 一个结构体,内含"取出字符位图"的操作函数
* 输出参数: 无
* 返 回 值: 0 - 成功, 其他值 - 失败
***********************************************************************/
int RegisterVideoRecv(PT_VideoRecvptVideoRecv)
{
PT_VideoRecv ptTmp;
//如果头部为空,让头部指向我们传入的参数,PT_VideoRecv的ptNext( VideoRecv结构体指针)为空。否则插入尾部
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;
}
/**********************************************************************
* 函数名称: ShowVideoConvert
* 功能描述: 显示本程序能支持的"字体模块",打印名字
* 输入参数: 无
* 输出参数: 无
* 返 回 值: 无
***********************************************************************/
void ShowVideoRecv(void)
{
int i = 0;
PT_VideoRecv ptTmp = g_ptVideoRecvHead;
while (ptTmp)
{
printf("%02d %s\n", i++, ptTmp->name);
ptTmp = ptTmp->ptNext;
}
}
/**********************************************************************
* 函数名称: GetVideoConvert
* 功能描述: 根据名字取出指定的"字体模块",根据传入的名字匹配
* 输入参数: pcName - 名字
* 输出参数: 无
* 返 回 值: NULL - 失败,没有指定的模块,
* 非NULL - 字体模块的PT_VideoRecv结构体指针
***********************************************************************/
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;
}
/**********************************************************************
* 函数名称: FontsInit
* 功能描述: 调用各个字体模块的初始化函数
* 输入参数: 无
* 输出参数: 无
* 返 回 值: 0 - 成功, 其他值 - 失败
***********************************************************************/
int VideoRecvInit(void)
{
int iError;
iError = Video_Recv_Init();
return iError;
}
5、video_recv.c
链接到服务器
static int connect_to_server(int *SocketClient, const char *ip)
{
int iRet;
定义结构体sockaddr_in
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 */设置端口号 SERVER_PORT定义为8888(mjpg-streamer服务器的端口号默认为8080)
//tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
设置服务器的IP地址,也就是传入的IP参数
if (0 == inet_aton(ip, &tSocketServerAddr.sin_addr))
{
DBG_PRINTF("invalid server_ip\n");
return -1;
}
memset(tSocketServerAddr.sin_zero, 0, 8);
调用connect函数建立连接,变量iRet接收返回值
iRet = connect(*SocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
DBG_PRINTF("connect error!\n");
return -1;
}
return 0;
}
//和服务器断开(直接调用close)
static int disconnect_to_server(int *SocketClient)
{
close(*SocketClient);
return 0;
}
初始化函数
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';//iRecvLen是接收到的字符长度,数组最后一位写入 '\0'结束字符。打印接受到的头部大小
printf("http header: %s\n", ucRecvBuf);
}
return 0;
}
获得视频格式
static int getformat(void)
{
/* 直接返回视频的格式 */
return V4L2_PIX_FMT_MJPEG;
}
/**********************************************************************
* 函数名称:getFileLen
* 功能描述: 获得文件大小
* 输入参数: socketClient-socket句柄指针,FreeBuf-保存视频数据,FreeLen-保存
* 输出参数: 无
* 返 回 值: 文件的大小(视频长度)
***********************************************************************/
socket编程一次最多只能接受1500字节,这是由硬件决定的
static long int getFileLen(int *SocketClient, char *FreeBuf, int *FreeLen)
{
int iRecvLen;
long int videolen;
char ucRecvBuf[1024];
char *plen, *buffp;
while(1)
{
//ucRecvBuf保存接收到的数据
iRecvLen = recv(*SocketClient, ucRecvBuf, 1024, 0);
if (iRecvLen <= 0)
{
close(*SocketClient);
return -1;
}
/* 解析ucRecvBuf,判断接收到的数据是否是报文 */
通过字符串查找函数strstr,在ucRecvBuf中查找是否有"Length:"字符串。如果找不到返回值为NULL,如果找到plen指向字符串"Length:"开始的地址,也就是指向L的地址。看下面可知:后面的%d就是frame_size(帧大小)。
plen = strstr(ucRecvBuf, "Length:");
if(NULL != plen)
{
在字符串中找到':'这个字符,执行完第一条语句,plen指向:的地址。
plen = strchr(plen, ':');
plen++;//plen指向%d
videolen = atol(plen); //atol函数将字符串转换成整数,从而表达文件有多大。videolen是一张图片的大小
printf("the Video Len %ld\n", videolen);
}
当接收到的数据中包含报文的时候就退出,在报文中找特征,长度%d后面还跟\r\n\r\n这样的字符串,buffp不为0时表明找到字符串\r\n\r\n,跳出循环
buffp = strstr(ucRecvBuf, "\r\n\r\n");
if(buffp != NULL)
break;
}
虽然得到videolen的大小,但是在寻找头部信息的时候,每次接收的都是1024字节,也就是1K的数据,但从mjpg-streamer的源代码中发现,报文只是一个字符串,远远小于1K,1K的其他数据是什么数据呢,\r\n\r\n后面跟的马上是一帧图片的数据。所以1K数据中有可能包含我们的视频数据。所以还要将视频数据给扣出来。
buffp += 4;//+4是因为\r\n\r\n后面跟的是视频数据,buffp 指向视频数据,
*FreeLen = 1024 - (buffp - ucRecvBuf);//1k数据-(视频数据地址-接收到的数据的首地址)得到1K数据中有多少数据是视频数据
memcpy(FreeBuf, buffp, *FreeLen);//将视频数据拷贝到FreeBuf当中
return videolen;
}
接收剩余数据(socket句柄,buf的地址(指针的指针),接收剩余数据的大小)
static long int http_recv(int *SocketClient, char **lpbuff, long int size)
{
int iRecvLen = 0, RecvLen = 0;
char ucRecvBuf[BUFFER_SIZE];
硬件决定对于socket编程,一次最多只能接收1500字节数据,但是size可能是好几K的大小。所以要分次接收,在while里面有接收数据函数,如果接收的数据小于0,直接退出循环, BUFFER_SIZE定义为1024字节。将接收到的数据保存到ucRecvBuf,一次性接收多少字节(如果大小比1024字节大的话,就接收1024字节,否则只接收size这么大)
while(size > 0)
{
iRecvLen = recv(*SocketClient, ucRecvBuf, (size > BUFFER_SIZE)? BUFFER_SIZE: size, 0);
if (iRecvLen <= 0)
break;
RecvLen是总共接收多少字节数据,而iRecvLen是一次性接收多少视频数据
size做一次自减操作
RecvLen += iRecvLen;
size -= iRecvLen;
先给lpbuff分配缓存,如果不为空,说明之前已经分配了空间,就需要重新分配空间(利用realloc函数,第一个参数是需要重新分配的地址,第二个参数是大小。
if(*lpbuff == NULL)
{
*lpbuff = (char *)malloc(RecvLen);
if(*lpbuff == NULL)
return -1;
}
else
{
*lpbuff = (char *)realloc(*lpbuff, RecvLen);
if(*lpbuff == NULL)
return -1;
}
将获得的视频数据从ucRecvBuf拷贝到缓存(*lpbuff+总长度-这次接收的长度)当中,拷贝这一次接收的长度
memcpy(*lpbuff+RecvLen-iRecvLen, ucRecvBuf, iRecvLen);
}
最后返回总长度
return RecvLen;
}
获得视频数据
/**********************************************************************
* 函数名称:get_video
* 功能描述: 获得一帧视频数据
* 输入参数: socketClient-socket句柄指针 ,ptVideoBuf是视频缓存结构体
PT_VideoBuf变量(视频数据保存到此结构体的成员中)
* 输出参数: 无
* 返 回 值: 文件的大小(视频长度)
***********************************************************************/
static int get_video(int *SocketClient, PT_VideoBuf ptVideoBuf)
{
long int video_len, iRecvLen;
int FirstLen = 0;
char tmpbuf[1024];
FreeBuffer 存放剩余的视频数据
char *FreeBuffer = NULL;
设置以下两个成员变量,iTotalBytes是总共接收多少字节(等于视频长度),
//ptVideoBuf->tPixelDatas.iTotalBytes
//ptVideoBuf->tPixelDatas.aucPixelDatas
while(1)
{
获得文件大小(socket句柄,存储视频数据的缓冲期,FirstLen已经接收到的字节数据)
video_len = getFileLen(SocketClient, tmpbuf, &FirstLen);
对FreeBuffer分配空间,socket句柄SocketClient,地址&FreeBuffer,接收的视频数据长度 video_len - FirstLen(总的视频数据长度-已接收到的视频数据长度),返回值是实际接收的字节数
iRecvLen = http_recv(SocketClient, &FreeBuffer, video_len - FirstLen);
//接收剩余的视频数据,FreeBuffer用于存放剩余的视频数据。
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;
}
/* 构造 */g_tVideoRecv结构体
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);
}
6、main.c函数
如果参数不为2打印出调试信息
if (argc != 2)
{
printf("Usage:\n");
printf("%s
return -1;
}
/* 一系列的初始化 */
/* 注册显示设备 */
DisplayInit();
/* 可能可支持多个显示设备: 选择和初始化指定的显示设备 */
SelectAndInitDefaultDispDev("crt");
GetDispResolution(&iLcdWidth, &iLcdHeigt, &iLcdBpp);
GetVideoBufForDisplay(&tFrameBuf);
iPixelFormatOfDisp = tFrameBuf.iPixelFormat;
初始化
VideoRecvInit();
显示出有多少种视频获取通道
ShowVideoRecv();
获得视频数据
g_ptVideoRecvOpr = GetVideoRecv("http");
获取视频格式
iPixelFormatOfVideo = g_ptVideoRecvOpr->GetFormat();
图片格式转换相关的函数
VideoConvertInit();
ptVideoConvert = GetVideoConvertForFormats(iPixelFormatOfVideo, iPixelFormatOfDisp);
if (NULL == ptVideoConvert)
{
DBG_PRINTF("can not support this format convert\n");
return -1;
}
/* 启动摄像头设备 */ 链接函数(第二个参数是IP地址),返回值小于0表明链接失败,打印调试信息
if(g_ptVideoRecvOpr->Connect_To_Server(&iSocketClient, argv[1]) < 0)
{
DBG_PRINTF("can not Connect_To_Server\n");
return -1;
}
当链接到服务器后,执行初始化函数
if(g_ptVideoRecvOpr->Init(&iSocketClient) < 0)
{
DBG_PRINTF("can not Init\n");
return -1;
}
将video_buf清0,用于获取一帧数据
memset(&tVideoBuf, 0, sizeof(tVideoBuf));
分配缓存(30000字节)
tVideoBuf.tPixelDatas.aucPixelDatas = malloc(30000);
memset(&tConvertBuf, 0, sizeof(tConvertBuf));
tConvertBuf.iPixelFormat = iPixelFormatOfDisp;
tConvertBuf.tPixelDatas.iBpp = iLcdBpp;
if( pthread_mutex_init(&tVideoBuf.db, NULL) != 0 ) /* 初始化 global.db 成员 */
{
return -1;
}
if( pthread_cond_init(&tVideoBuf.db_update, NULL) != 0 ) /* 初始化 global.db_update(条件变量) 成员 */
{
DBG_PRINTF("could not initialize condition variable\n");
return -1;
}
/* 创建获取摄像头数据的线程 */
pthread_create(&RecvVideo_Id, NULL, &RecvVideoThread, &tVideoBuf);
while (1)
{
pthread_cond_wait(&tVideoBuf.db_update, &tVideoBuf.db);
ptVideoBufCur = &tVideoBuf;
if (iPixelFormatOfVideo != iPixelFormatOfDisp)
{
/* 转换为RGB */
iError = ptVideoConvert->Convert(&tVideoBuf, &tConvertBuf);
DBG_PRINTF("Convert %s, ret = %d\n", ptVideoConvert->name, iError);
if (iError)
{
DBG_PRINTF("Convert for %s error!\n", argv[1]);
continue;
}
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;
}