在上篇部落格已經分析過了關於mjpg_streamer的呼叫過程,下面 下麪我們來根據這個呼叫過程,自己寫一個用戶端,通過連線開發板的WIFI,在虛擬機器上顯示攝像頭的數據。
在用戶端接接收視訊數據時:
"GET /?action=stream"
字串給伺服器端,使伺服器知道需要發送視訊流數據給用戶端。"boundarydonotcross"
字串,表示一幀數據發送完畢mjpg_streamer從攝像頭中接收數據時(輸入外掛):
libjpeg-turbo
把rgb數據壓縮成jpeg格式的數據mjpg_streamer從倉庫中取出數據時(輸出外掛):
所以最終用戶端接收到的視訊數據是一幀一幀的jpeg格式的圖片
藉助之前的【2.5 視訊監控—在LCD上顯示攝像頭影象】的框架進行修改,完成通過用戶端連線,藉助mjpg_streamer在虛擬機器上動態顯示攝像頭的數據資訊:
對於上述完成主要功能的5個部分:display顯示部分、debug偵錯資訊輸出部分、render渲染部分、video_recv視訊接收部分、convert格式轉換部分
分爲如下3部分:
built-in.o
。.c
檔案編進程式裡。
如圖所示:視訊數據總的流向爲:攝像頭—>mjpg_streamer伺服器—>用戶端
mjpg_streamer伺服器:
用戶端:
VideoBuf
中,這些數據都是jpeg格式,需要convert部分轉換格式;videobuf
,呼叫適合的轉換檔案來把原始的攝像頭數據轉換成rgb格式,儲存在convertbuf
中。convertbuf
的數據進行最後的排版與處理,最終顯示在虛擬機器上。同樣的,需要設如下的管理者,進行對模組的管理。
/*******************************************************************************
* 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 */
#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);
}
初始化偵錯系統
註冊顯示模組
選擇顯示裝置
獲取顯示屏參數
獲取顯示屏視訊記憶體
獲取顯示器格式
註冊視訊數據接收模組
顯示視訊獲取通道
獲取視訊獲取操作函函數
獲取視訊數據格式
註冊轉換模組
獲取支援格式的轉換處理結構體
與伺服器端建立連線
發送報文給伺服器,告訴需要其所發送的數據型別與要求
清除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;
}