mjpg-streamer学习笔记9---自己写客户端

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服务器

在串口上运行以下指令

mjpg-streamer学习笔记9---自己写客户端_第1张图片

(2)进入虚拟机,让虚拟机中的ubuntu系统连接上wifi热点(点右上角的那个标志)S3C2440

mjpg-streamer学习笔记9---自己写客户端_第2张图片

(3)让虚拟机进入文本模式

按住ctrl+alt+F1,然后输入用户名和密码

mjpg-streamer学习笔记9---自己写客户端_第3张图片

(3)进入到mjpeg-streamer客户端的路径,运行客户端,跟的参数是开发板的IP地址,运行后可以看到摄像头采集到的数据

mjpg-streamer学习笔记9---自己写客户端_第4张图片

2、客户端程序编写

新建video_recv(视频接收)目录,有video_recv.c文件,video_recv_manager.c文件,在include目录下写头文件video_recv_manager.h 并修改相应的Makefile,修改顶层目录的Makefile,将编译出来的结果改名为mjpg_streamer_client

mjpg-streamer学习笔记9---自己写客户端_第5张图片

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 \n", argv[0]);
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;
}

/**********************************************************************
 * 函数名称:getFileLen
 * 功能描述:  获得文件大小
 * 输入参数: socketClient-socket句柄指针,FreeBuf-保存视频数据,FreeLen-保存
 * 输出参数: 无
 * 返 回 值: 文件的大小(视频长度)
 ***********************************************************************/

你可能感兴趣的:(mjpg-streamer)