mjpg-streamer是一个开源的视频服务器,通过摄像头采集数据,放到内存中,再通过socket把视频数据发送出去,最终在web端显示视频数据。mjpg-streamer把采集数据、socket发送数据封装成了两个动态库,一个称作输入插件,一个称作输出插件。
如果我们想做一些跟视频传输相关的项目,完全可以利用mjpg-streamer作为视频数据来源,而不用再关心底层驱动如何实现,驱动视频数据如何读取。mjpg-streamer自带压缩算法,可以把采集的原始数据压缩成jpg格式的图像数据,从而方便传输。
本文就来简单的分析下,如何自己实现客户端程序。
关于mjpg-streamer的流程介绍网上有很多,我们只是分析下它是如何输出数据的,因为只有知道如何输出数据,才能写对应的代码去接收数据。
主要代码集中在 httpd.c 文件中。
mjpg-streamer可以同时处理多个客户端(web)的连接,对于每个连接到服务器的客户端,mjpg-streamer都会创建单独的线程来处理对应的客户端。
客户端想要获取服务器的视频数据,先要向服务器发起请求,简单点理解,就是先得告诉服务器,是要获取视频流,还是获取一张图片。
向服务器发送字符串 "GET /?action=stream"
为了保证数据的安全性,mjpg-streamer加上了用户名和密码,当然,是在启动服务器的时候,通过参数来决定是否要加验证。
从代码的 898 行可以看出,如果客户端发送的数据小于 2 个字节,就是跳出循环,代码继续向下走。所以得到了第二步:
向服务器发送任意小于两字节的字符串。
接着程序走到了 929 行,开始调用函数 send_stream。
send_streamer主要是向客户端返回数据,只要搞清楚返回哪些数据,那么我们的客户端程序基本就写出来了。
首先返回头部信息,这一部分没有什么有用的信息,所以我们直接接收后忽略就好。
接下来进入 376 行开始死循环。
分别向客户端发送了三个数据:
头部信息:包含了一帧数据的大小;
帧数据:我们真正想要得到的数据;
尾部信息。
虽然是分为三次发送,但是因为使用的是TCP协议,传输的过程中可能会分包或者粘包,所以接收数据的时候,并不是接收三次那么简单。有可能第一次收到的数据既包含了头部信息,又包含了帧数据(粘包);有可能第二次接收数据的时候,只收到了部分帧数据(分包)。
总结一下,如果想实现客户端,需要完成下面的步骤:
循环接收的时候,最好能够根据头部信息和尾部信息来确定帧数据,做到万无一失。
最后附上C语言的实现代码(跟语言、平台没有关系)。
int main()
{
int video_sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in video_addr;
memset(&video_addr, 0, sizeof(video_addr));
video_addr.sin_family = AF_INET;
video_addr.sin_port = htons(8080);
video_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(video_sockfd, (struct sockaddr *)&video_addr, sizeof(video_addr));
printf("connect to mjpg-streamer success\n");
char *buf = (char *)malloc(102400);
memset(buf, 0, 102400);
strcpy(buf, "GET /?action=stream\n");
send(video_sockfd, buf, strlen(buf), 0);
send(video_sockfd, "f\n", 2, 0);
memset(buf, 0, 102400);
recv(video_sockfd, buf, BUFLEN, 0);
int recv_size, pic_length = 0, p = 0;
char *begin, *end;
char cont_len[10] = {0};
char *pic_data = (char *)malloc(102400);
while (1)
{
memset(buf, 0, 102400);
recv_size = recv(video_sockfd, buf, 74, 0);
if (strstr(buf, "Content-Type"))
{
begin = strstr(buf, "Content-Length");
end = strstr(buf, "X-Timestamp");
memcpy(cont_len, begin + 16, end - 2 - begin - 16);
pic_length = atoi(cont_len);
printf("recv head Content-Length = %d %d\n", atoi(cont_len), recv_size);
memset(cont_len, 0, 10);
}
else
{
continue;
}
while (1)
{
memset(buf, 0, 102400);
recv_size = recv(video_sockfd, buf, pic_length, 0);
if (recv_size == pic_length)
{
memcpy(pic_data + p, buf, recv_size);
p += recv_size;
//处理图片数据
p = 0;
memset(pic_data, 0, 102400);
pic_length = 0;
break;
}
else
{
memcpy(pic_data + p, buf, recv_size);
pic_length = pic_length - recv_size;
p += recv_size;
}
}
recv(video_sockfd, buf, 24, 0);
}
}
详细视频教程 智能WiFi摄像头项目实战 扫码访问