视频监控—mjpg-streamer客户端的编写
- 硬件平台:韦东山嵌入式Linxu开发板(S3C2440.v3)
- 软件平台:运行于VMware Workstation 12 Player下UbuntuLTS16.04_x64 系统
- 开发环境:arm-linux-gcc-4.3.2工具链、linux-3.4.2内核(开发版根文件系统)
- 源码仓库:https://gitee.com/d_1254436976/Embedded-Linux-Phase-3
目录
-
视频监控—mjpg-streamer客户端的编写 - 一、前言
- 二、服务器端的协议与数据流
- 1、协议
- 2、数据流
- 三、框架
- 1、程序框架
- 2、Makefile框架
- 四、数据流向
- 五、程序的编写
- 1、视频接收管理者头文件
- 2、具体的视频接收模块文件
- 3、主函数的调用流程
一、前言
在上篇博客已经分析过了关于mjpg_streamer的调用过程,下面我们来根据这个调用过程,自己写一个客户端,通过连接开发板的WIFI,在虚拟机上显示摄像头的数据。
二、服务器端的协议与数据流
1、协议
在客户端接接收视频数据时:
- 客户端需要发送
"GET /?action=stream"
字符串给服务器端,使服务器知道需要发送视频流数据给客户端。 - 接着,客户端需要发送少于2字节的数据,提醒服务器端不需要提供用户密码登陆功能
- 随后,服务器发送一串提示信息给客户端,表示服务器已经接收到上述客户端的要求
- 之后,服务器开始发送视频流数据
4.1 服务器发送字符串,告诉客户端这一帧数据的格式与大小
4.2 随后,服务器开始发送一帧数据给客户端
4.3 最后,服务器端发送"boundarydonotcross"
字符串,表示一帧数据发送完毕
2、数据流
mjpg_streamer从摄像头中接收数据时(输入插件):
- 支持摄像头输出数据的格式为yuv或mjpg
- 通过ioctl函数读取出输出数据后
2.1 若为mjpg格式,则直接拷贝到仓库
2.2 若为yuv格式,则先把yuv颜色空间转换成rgb颜色空间,最后通过libjpeg-turbo
把rgb数据压缩成jpeg格式的数据
2.3 把压缩后的jpeg数据拷贝到仓库中
mjpg_streamer从仓库中取出数据时(输出插件):
- 从仓库中把数据取出,通过socket把数据发送给客户端
所以最终客户端接收到的视频数据是一帧一帧的jpeg格式的图片
三、框架
1、程序框架
借助之前的【2.5 视频监控—在LCD上显示摄像头图像】的框架进行修改,完成通过客户端连接,借助mjpg_streamer在虚拟机上动态显示摄像头的数据信息:
对于上述完成主要功能的5个部分:display显示部分、debug调试信息输出部分、render渲染部分、video_recv视频接收部分、convert格式转换部分
- video_recv视频接收部分:负责从服务器端获得摄像头的原始数据;
- convert格式转换部分:负责对于摄像头的原始数据,进行格式转换,对于不同的摄像头有不同的数据格式,需要包含多个文件来支持多种转换方式;
- render渲染部分:负责对转换得到的数据格式,进行压缩、合并成可以在LDC上显示的数据;
- display显示部分:合并后的数据显示在虚拟机上;
- debug调试部分:设置打印等级和打印通道,通过打印等级来控制程序打印的调试信息、错误信息、警告信息等,通过打印通道设置来控制程序打印的输出的流向是标准输出还是网络打印输出。
2、Makefile框架
分为如下3部分:
- 顶层目录的Makefile:定义obj-y来指定根目录下要编进程序去的文件、子目录外,主要是定义工具链、编译参数、链接参数。
- 顶层目录的Makefile.build:把某个目录及它的所有子目录中、需要编进程序去的文件都编译出来,打包为
built-in.o
。 - 各级子目录的Makefile:把当前目录下的
.c
文件编进程序里。
四、数据流向
如图所示:视频数据总的流向为:摄像头—>mjpg_streamer服务器—>客户端
mjpg_streamer服务器:
- 在输入插件中,mjpg_streamer服务器通过ioctl函数,读出摄像头的数据,并存储到“仓库”中;
- 在输出插件中,从仓库中取出数据,通过socket协议发出视频数据给客户端;
客户端:
- 在 video视频设备部分,客户端把mjpg_streamer服务器接受到的数据存储在
VideoBuf
中,这些数据都是jpeg格式,需要convert部分转换格式; - 在convert格式转换部分,会根据video视频设备部分传来的数据格式
videobuf
,调用适合的转换文件来把原始的摄像头数据转换成rgb格式,存储在convertbuf
中。 - 在display显示部分中,对
convertbuf
的数据进行最后的排版与处理,最终显示在虚拟机上。
五、程序的编写
1、视频接收管理者头文件
同样的,需要设如下的管理者,进行对模块的管理。
/******************************************************************************** Copyleft (c) 2021 Kcode** @file video_recv_manager.h* @brief 视频数据管理者头文件,向下支持各种视频数据,向上提供接口* @author K* @version 0.0.1* @date 2021-07-26* @license MulanPSL-1.0** 文件修改历史:* <时间> | <版本> | <作者> | <描述>* 2021-08-11 | v0.0.1 | Kcode | 视频数据管理者头文件* -----------------------------------------------------------------------------******************************************************************************/#include <pthread.h>
#include "pic_operation.h"#ifndef _VIDEO_MANAGER_H
#define _VIDEO_MANAGER_H/*!* 存储视频数据*/
typedef struct VideoBuf {T_PIXELDATAS pixel_data; /**< 借用T_PixelDatas */int pixel_format; /**< 像素格式 *//* signal fresh frames */pthread_mutex_t db;pthread_cond_t db_update;
}T_VIDEOBUF, *PT_VIDEOBUF;/*!* 视频数据处理结构体*/
typedef struct VideoRecv {char *name; /**< 设备名 *//* 初始化设备 */int (*Init)(int *SocketClient);/* 连接服务器 */int (*ConnectToServer)(int *SocketClient, const char *ip);/* 断开服务器 */int (*DisConnectToServer)(int *SocketClient);/* 获得视频格式 */int (*GetFormat)(void);/* 获取video数据 */int (*GetVideo)(int *SocketClient, PT_VIDEOBUF ptVideoBuf);struct VideoRecv *ptNext;
}T_VIDEORECV, *PT_VIDEORECV;/*!* @brief 初始化函数,把对支持的各个设备注册进链表进行统一管理* @param [in] 无* @return 无*/
int video_recv_init(void);/*!* @brief 显示所支持的设备* @param [in] 无* @return 无 */
void ShowVideoOpr(void);/*!* @brief 注册函数* @param ptVideoRecv[in] 要注册的设备结构体结点* @return 0:成功 -1:失败*/
int RegisterVideoRecv(PT_VIDEORECV ptVideoRecv);int VideoRecvInit(void);void ShowVideoRecv(void);PT_VIDEORECV GetVideoRecv(char *pName);#endif /* _VIDEO_MANAGER_H */
2、具体的视频接收模块文件
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/videodev2.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>#include "config.h"
#include "video_recv_manager.h"
#include "debug_manager.h"#define BUFFER_SIZE 1024 /**< 服务器从客户端中接收的最大数据字节数 *//*!* @brief 与服务器建立连接* @param[in] socket_client socket的句柄* @param[in] ip 服务器的ip* @return int 成功:0,失败:-1*/
static int connect_to_server(int *socket_client, const char *ip)
{int ret;struct sockaddr_in socket_server_addr;*socket_client = socket(AF_INET, SOCK_STREAM, 0);socket_server_addr.sin_family = AF_INET;socket_server_addr.sin_port = htons(SERVER_PORT); /* host to net, short *///socket_server_addr.sin_addr.s_addr = INADDR_ANY;if (0 == inet_aton(ip, &socket_server_addr.sin_addr)){DBG_PRINTF("invalid server_ip\n");return -1;}memset(socket_server_addr.sin_zero, 0, 8);ret = connect(*socket_client, (const struct sockaddr *)&socket_server_addr, sizeof(struct sockaddr)); if (-1 == ret){DBG_PRINTF("connect error!\n");return -1;}return 0;
}/*!* @brief 断开与服务器的连接* @param[in] socket_client socket的句柄* @return int 0*/
static int disconnect_to_server(int *socket_client)
{close(*socket_client);return 0;
}/*!* @brief 发送报文给服务器端,告诉其需要发送的数据类型与要求* @param[in] socket_client socket的句柄* @return int 成功:总接收到的数据长度,失败:-1*/
static int init(int *socket_client)
{char send_buf[100];int send_len;int recv_len;char recv_buf[1000];/*!* 发请求类型字符串:"GET /?action=stream\n"表示需要客户端接收视频流数据 */memset(send_buf, 0x0, 100);strcpy(send_buf, "GET /?action=stream\n");send_len = send(*socket_client, send_buf, strlen(send_buf), 0);if (send_len <= 0){close(*socket_client);return -1;}/*!* 如果我们不使用密码功能!则只需发送任意长度为小于2字节的字符串 */memset(send_buf, 0x0, 100);strcpy(send_buf, "f\n");send_len = send(*socket_client, send_buf, strlen(send_buf), 0);if (send_len <= 0){close(*socket_client);return -1;}/*!* 将从服务器端接收一次报文* 由于之前已经做好准备工作,所以此时接收的信息是服务器端已ok*//* 接收客户端发来的数据并显示出来 */recv_len = recv(*socket_client, recv_buf, 999, 0);if (recv_len <= 0){close(*socket_client);return -1;}else{recv_buf[recv_len] = '\0';printf("http header: %s\n", recv_buf);}return 0;
}/*!* @brief 返回视频数据的格式* @return int V4L2_PIX_FMT_MJPEG*/
static int getformat(void)
{/* 直接返回视频的格式 */return V4L2_PIX_FMT_MJPEG;
}/*!* @brief 解析服务器端发送的报文,获取一帧视频数据的大小* @param[in] socket_client socket的句柄* @param[out] free_buf 存储服务器端发送的一帧视频数据的信息* @param[out] free_len free_buf中剩余内存大小* @return int 成功:一帧视频数据的大小*/
static long int get_file_len(int *socket_client, char *free_buf, int *free_len)
{int recv_len;long int videolen;char recv_buf[1024];char *plen, *buffp;while(1){/*!* 从服务器接收数据(将要接收到的一帧视频数据大小)*/recv_len = recv(*socket_client, recv_buf, 1024, 0);if (recv_len <= 0){close(*socket_client);return -1;}/*!* 解析recv_buf,判断接收到的数据是否是报文 */plen = strstr(recv_buf, "Length:");if(NULL != plen){plen = strchr(plen, ':');plen++;videolen = atol(plen);printf("the Video Len %ld\n", videolen);}/*!* 解析完毕的标志*/buffp = strstr(recv_buf, "\r\n\r\n");if(buffp != NULL)break;}buffp += 4;*free_len = 1024 - (buffp - recv_buf);memcpy(free_buf, buffp, *free_len);return videolen;
}/*!* @brief 从http客户端接收一帧视频数据数据* @param[in] socket_client socket的句柄* @param[out] lpbuff 存储接收到数据的地址* @param[in] size 需要接收到的数据长度 * @return long 成功:总接收到的数据长度,失败:-1*/
static long int http_recv(int *socket_client, char **lpbuff, long int size)
{int recv_len = 0; /**< 一次从客户端接收到数据的长度 */int recv_sum = 0; /**< 总共从客户端接收到数据的长度 */char recv_buf[BUFFER_SIZE]; /**< 存储接收到的数据 *//*!* 分次接收数据,最多接收BUFFER_SIZE大小字节的数据*/while(size > 0){/*!* 调用recv从客户端接收数据* 大小:(size > BUFFER_SIZE)? BUFFER_SIZE: size* 数据存储到:recv_buf[BUFFER_SIZE]*/recv_len = recv(*socket_client, recv_buf, (size > BUFFER_SIZE)? BUFFER_SIZE: size, 0);if (recv_len <= 0)break;recv_sum += recv_len; /* 实际接收的字节数 */size -= recv_len; /* 剩余需要接收的字节数 *//*!* 判断传入的lpbuff是否为空* 空则分配内存,不空则扩大内存*/if(*lpbuff == NULL){*lpbuff = (char *)malloc(recv_sum);if(*lpbuff == NULL)return -1;}else{*lpbuff = (char *)realloc(*lpbuff, recv_sum);if(*lpbuff == NULL)return -1;}/*!* 根据偏移值计算出内存地址,拷贝数据*/memcpy(((*lpbuff) + recv_sum - recv_len), recv_buf, recv_len);}return recv_sum;
}/*!* @brief 获取一帧视频数据* @param[in] socket_client socket的句柄* @param[in] video_buf 存储一帧数据的地址,需在函数外分配* @return 0 - 成功缩放,-1 - 不支持缩放*/
static int get_video(int *socket_client, PT_VIDEOBUF video_buf)
{long int video_len, recv_len;int first_len = 0;char tmpbuf[1024];char *free_buffer = NULL;if (video_buf->pixel_data.PixelDatas == NULL){DebugPrint(APP_ERR"please check that video_buf->pixel_data.PixelDatas == NULL\n");return -1;}/*!* 获取一帧视频数据*/while(1){/* 解析服务器的报文,获取一帧视频数据的大小 */video_len = get_file_len(socket_client, tmpbuf, &first_len); /* 解析服务器的数据,获取已接收的视频数据的大小 */recv_len = http_recv(socket_client, &free_buffer, video_len - first_len);/* 原子操作 */pthread_mutex_lock(&video_buf->db);/* 将两次接收到的视频数据组装成一帧数据 */memcpy(video_buf->pixel_data.PixelDatas, tmpbuf, first_len);memcpy(video_buf->pixel_data.PixelDatas + first_len, free_buffer, recv_len);video_buf->pixel_data.TotalBytes = video_len;/* 发出一个数据更新的信号,通知输出通道来取数据 */pthread_cond_broadcast(&video_buf->db_update);/* 原子操作结束 */pthread_mutex_unlock(&video_buf->db); }return 0;
}/* 构造 */
static T_VIDEORECV s_video_recv = {.name = "http",.ConnectToServer = connect_to_server,.DisConnectToServer = disconnect_to_server,.Init = init,.GetFormat = getformat,.GetVideo = get_video,
};/* 注册 */
int video_recv_init(void)
{return RegisterVideoRecv(&s_video_recv);
}
3、主函数的调用流程
-
初始化调试系统
-
注册显示模块
-
选择显示设备
-
获取显示屏参数
-
获取显示屏显存
-
获取显示器格式
-
注册视频数据接收模块
-
显示视频获取通道
-
获取视频获取操作函函数
-
获取视频数据格式
-
注册转换模块
-
获取支持格式的转换处理结构体
-
与服务器端建立连接
-
发送报文给服务器,告诉需要其所发送的数据类型与要求
-
清除video_buf的数据,并为其分配存储一帧数据的内存
-
清除convert_buf的数据,并设置其显示格式与bpp
-
初始化 video_buf.db 成员与video_buf.db_update(条件变量),用于线程管理
-
创建获取视频数据的线程
-
在while(1)中:
19.1 等待视频数据的更新,当接收到视频数据时,video_buf.db_update就会变换,通知主线程执行以下操作
19.2 接收完一帧数据后,调用libjpeg_turbo转换为RGB格式
19.3 调整数据位置,居中显示在虚拟机中(刷新) -
等待线程结束,以便回收它的资源
/******************************************************************************** Copyleft (c) 2021 Kcode** @file main.c* @brief 配合多个模块,通过连接mjpg_streamer服务器端,在虚拟机显示摄像头数据* @author K* @version 0.0.1* @date 2021-07-26* @license MulanPSL-1.0** 文件修改历史:* <时间> | <版本> | <作者> | <描述>* 2021-08-12 | v0.0.1 | Kcode | 主程序* -----------------------------------------------------------------------------******************************************************************************/#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>#include "config.h"
#include "disp_manager.h"
#include "debug_manager.h"
#include "pic_operation.h"
#include "render.h"
#include "convert_manager.h"
#include "video_recv_manager.h"PT_VIDEORECV s_video_recv_opr;
int socket_client; /**< socket套接字 *//*!* @brief 客户端接收线程函数* @return long 成功:总接收到的数据长度,失败:-1*/
void* recv_video_thread(void *data)
{if(s_video_recv_opr->GetVideo(&socket_client, (PT_VIDEOBUF)data) < 0){ DBG_PRINTF("can not Get_Video\n");}return data;
}int main(int argc, char **argv)
{int error;int video_pixel_format;int display_pixel_format;int lcd_width;int lcd_height;int lcd_bpp;int topleft_x;int topleft_y;T_VIDEOBUF video_buf; T_VIDEOBUF convert_buf;T_VIDEOBUF framebuf;PT_VIDEOBUF cur_video_buf;PT_VIDEOCONVERTOPR video_convert_opr; /**< 存储一帧数据的信息 */pthread_t recv_video_Id;/*!* 初始化调试系统*/error = DebugInit();if (error) {printf(APP_ERR"DebugInit error! File:%s Line:%d\n", __FILE__, __LINE__);return -1;}error = InitDebugChanel();if (error) {printf(APP_ERR"InitDebugChanel error! File:%s Line:%d\n", __FILE__, __LINE__);return -1;}/*!* 操作信息提示:./mjpg_streamer_client 192.168.7.0*/if (argc != 2){DebugPrint(APP_NOTICE"Usage:\n");DebugPrint(APP_NOTICE"%s <ip>\n", argv[0]);return -1;}/*!* 注册显示模块*/error = DisplayInit();if (error){DebugPrint(APP_ERR"DisplayInit err\n");return -1;}/* 选择显示设备 */SelectAndInitDefaultDispDev("crt");/* 获取显示屏参数 */GetDispResolution(&lcd_width, &lcd_height, &lcd_bpp);/* 获取显示屏显存 */GetVideoBufForDisplay(&framebuf);/* 获取显示器格式 */display_pixel_format = framebuf.pixel_format;/*!* 注册视频数据接收模块 */error = VideoRecvInit();if (error){DebugPrint(APP_ERR"VideoInit err\n");return -1;} /*!* 显示视频获取通道*/ShowVideoRecv();/* 获取视频获取操作函数 */s_video_recv_opr = GetVideoRecv("http");/* 获取视频数据格式 */video_pixel_format = s_video_recv_opr->GetFormat();/*!* 注册转换模块*/error = VideoConvertInit();if (error){DebugPrint(APP_ERR"VideoConvertInit err\n");return -1;}/* 获取支持格式的转换处理结构体 */video_convert_opr = GetVIdeoConvertForFormats(video_pixel_format,display_pixel_format);if (video_convert_opr == NULL){DebugPrint(APP_ERR"Can not support this format convert\n");return -1;}/*!* 与服务器端建立连接*/if(s_video_recv_opr->ConnectToServer(&socket_client, argv[1]) < 0){ DebugPrint(APP_ERR"Can not Connect_To_Server\n");return -1;}/*!* 发送报文给服务器,告诉需要其所发送的数据类型与要求*/if(s_video_recv_opr->Init(&socket_client) < 0){DebugPrint(APP_ERR"Can not Init\n");return -1;}/*!* 清除video_buf的数据,并为其分配存储一帧数据的内存*/memset(&video_buf, 0, sizeof(T_VIDEOBUF));video_buf.pixel_data.PixelDatas = (unsigned char *)malloc(30000);/*!* 清除convert_buf的数据,并设置其显示格式与bpp*/memset(&convert_buf, 0, sizeof(T_VIDEOBUF));convert_buf.pixel_format = display_pixel_format;convert_buf.pixel_data.bpp = lcd_bpp;/* 初始化 video_buf.db 成员 */if(pthread_mutex_init(&video_buf.db, NULL) != 0) {return -1;}/* 初始化 video_buf.db_update(条件变量) 成员 */if(pthread_cond_init(&video_buf.db_update, NULL) != 0) {DBG_PRINTF("could not initialize condition variable\n");return -1;}/*!* 创建获取视频数据的线程 */pthread_create(&recv_video_Id, NULL, &recv_video_thread, &video_buf);/*!* 处理摄像头数据* 如果没有按键输入,则循环显示,否则退出*/while(1){/* 等待数据的更新 */pthread_cond_wait(&video_buf.db_update, &video_buf.db);cur_video_buf = &video_buf;/*!* 转换为RGB */if (video_pixel_format != display_pixel_format){error = video_convert_opr->Convert(&video_buf, &convert_buf);DebugPrint(APP_ERR"Convert is begin\n");if (error){DebugPrint(APP_ERR"Convert for %s err\n", argv[1]);/*! * 由于网络的问题可能会出现一帧的数据非jpeg数据,* 使用continue可忽略这种情况 */continue; }cur_video_buf = &convert_buf;}/* 居中显示,计算此时的左上角坐标 */topleft_x = (lcd_width - cur_video_buf->pixel_data.width) / 2;topleft_y = (lcd_height - cur_video_buf->pixel_data.height) / 2;PicMerge(topleft_x, topleft_y, &cur_video_buf->pixel_data, &framebuf.pixel_data);/*!* 把framebuffer的数据刷到虚拟机,显示 */FlushPixelDatasToDev(&framebuf.pixel_data); }pthread_detach(recv_video_Id); // 等待线程结束,以便回收它的资源return 0;}