为了模拟视频推流场景中 多用户终端请求拉取服务器端的视频流,这里我在Windows平台下,采用TCP协议在服务器和客户端之间传输视频流,其中服务器端拉取视频流,并将视频流发送给多路客户端。这里的网络视频流我采用的是海康威视的网络视频流,rtsp://admin:[email protected]:554/h264/ch1/sub/av_stream
多线程下资源共享问题:
如果服务器对每个客户端都new一个拉流的对象,并分别拉取海康网络视频流,然后分别将图像帧发送给每个连接好的客户端,不仅进行了不必要的重复的工作,而且浪费的计算资源:
std::string url = "rtsp://admin:[email protected]:554/h264/ch1/sub/av_stream";
cv::VideoCapture cap1(url);
std::string url = "rtsp://admin:[email protected]:554/h264/ch1/sub/av_stream";
cv::VideoCapture cap2(url);
DWORD WINAPI VideoThread(LPVOID lpParameter)
{
//海康威视子码流拉流地址 用户名 admin 密码abc.1234 需要修改为对应的用户名和密码
std::string url = "rtsp://admin:[email protected]:554/h264/ch1/sub/av_stream";
cv::VideoCapture cap(url);
//cv::VideoCapture cap(0);
while (1)
{
WaitForSingleObject(hMutex, INFINITE);
cap >> cv_img;
ReleaseMutex(hMutex);
}
}
视频软解码是一个耗CPU的工作,如果定义多个对象去拉相同的视频流的话,那么必定浪费了不必要的计算资源:
定义一个拉流对象,并将解析后的图像帧作为一个全局变量,每个子线程取图像帧时,通过互斥锁进行排他性资源获取,这里我定义了两个线程,一个是拉流的子线程,另一个是发送TCP数据的子线程:
。以下为服务器端发送数据子线程通过互斥锁进行资源获取的代码
WaitForSingleObject(hMutex, INFINITE);
//if (cv_img.size().width < 0 || cv_img.size().height < 0) { continue; }
if (cv_img.empty()) { Sleep(5); ReleaseMutex(hMutex); continue; }
ReleaseMutex(hMutex);
以下为服务器端拉流子线程的工作逻辑代码
DWORD WINAPI VideoThread(LPVOID lpParameter)
{
//海康威视子码流拉流地址 用户名 admin 密码abc.1234 需要修改为对应的用户名和密码
std::string url = "rtsp://admin:[email protected]:554/h264/ch1/sub/av_stream";
cv::VideoCapture cap(url);
//cv::VideoCapture cap(0);
while (1)
{
WaitForSingleObject(hMutex, INFINITE);
cap >> cv_img;
ReleaseMutex(hMutex);
}
}
下面给出服务器端和客户端代码源码
chat_server.cpp
// chat_server.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include
#include
#include
#include
#include "opencv2\opencv.hpp"
#include "opencv2\imgproc\imgproc.hpp"
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
#pragma comment (lib, "opencv_world340.lib") //加载 ws2_32.dll
HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);
cv::Mat cv_img; //cv_图像
DWORD WINAPI VideoThread(LPVOID lpParameter)
{
//海康威视子码流拉流地址 用户名 admin 密码abc.1234 需要修改为对应的用户名和密码
std::string url = "rtsp://admin:[email protected]:554/h264/ch1/sub/av_stream";
cv::VideoCapture cap(url);
//cv::VideoCapture cap(0);
while (1)
{
WaitForSingleObject(hMutex, INFINITE);
cap >> cv_img;
ReleaseMutex(hMutex);
}
}
DWORD WINAPI ServerThread(LPVOID lpParameter)
{
std::vector params; // 压缩参数
params.resize(3, 0);
params[0] = CV_IMWRITE_JPEG_QUALITY; // 无损压缩
params[1] = 30;//压缩的质量参数 该值越大 压缩后的图像质量越好
SOCKET ClientSocket = *(SOCKET*)lpParameter;
char serverBuffer[100] = { 0 };//缓冲区
char frames_cnt[10] = { 0, };
std::vector data_encode;//用来从队列中提取编码后的数据
while (1)
{
//由于编码耗时,故将这部分操作放在取流线程做,减小主线程的处理时间
std::vector data_encode;//保存从网络传输数据解码后的数据
WaitForSingleObject(hMutex, INFINITE);
//if (cv_img.size().width < 0 || cv_img.size().height < 0) { continue; }
if (cv_img.empty()) { Sleep(5); ReleaseMutex(hMutex); continue; }
ReleaseMutex(hMutex);
cv::imencode(".jpg", cv_img, data_encode, params); // 对图像进行压缩
int len_encoder = data_encode.size();//获取图像编码后的字节长度 方便后续通过TCP传输时 接收端知道此次传输的字节大小
_itoa_s(len_encoder, frames_cnt, 10);//
send(ClientSocket, frames_cnt, 10, 0);//将图像字节长度 进行传输
// 发送
int index = 0;//标志实时接收图像字节的长度 方便程序中判断还有多少字节尚未接收到
char *send_b = new char[data_encode.size()];// 创建一个字节数组 开启大小为图像字节长度的字符数组空间
//这里是将data_encode首地址且长度为图片字节长度 通过内存拷贝复制到send_b数组中,相比于采用循环单个元素赋值,速度快了至少10倍
memcpy(send_b, &data_encode[0], data_encode.size());
int iSend = send(ClientSocket, send_b, data_encode.size(), 0);//将图像字节数据传输到服务器端
delete[]send_b;//销毁对象
data_encode.clear();//将队列清空 方便下一次进行图像矩阵接收
}
//关闭监听套接字
closesocket(ClientSocket);
return 0;
}
int main() {
//初始化 DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET servSockToListen = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//绑定套接字
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET; //使用IPv4地址
sockAddr.sin_addr.s_addr = htonl(INADDR_ANY); //具体的IP地址
sockAddr.sin_port = htons(9999); //端口
bind(servSockToListen, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
//设置监听状态
listen(servSockToListen, 20);
HANDLE vThread = CreateThread(NULL, 0, &VideoThread, NULL, 0, NULL);
CloseHandle(vThread);
while (1)
{
//接收客户端请求
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
//accept阻塞
SOCKET clntSock = accept(servSockToListen, (SOCKADDR*)&clntAddr, &nSize);
std::cout << "一个客户端已连接到服务器,socket是:" << clntSock << std::endl;
//为每一个连接创建一个线程
HANDLE sThread = CreateThread(NULL, 0, &ServerThread, &clntSock, 0, NULL);
CloseHandle(sThread);
}
//关闭监听套接字
closesocket(servSockToListen);
//终止 DLL 的使用
WSACleanup();
return 0;
}
chat_client.cpp
// chat_client.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include
#include
#include
#include
#include
#include "opencv2\opencv.hpp"
#include "opencv2\imgproc\imgproc.hpp"
#pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll
#pragma comment (lib, "opencv_world340.lib") //加载 ws2_32.dll
using namespace std;
int main() {
//初始化 DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET servSockToContect = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//绑定套接字
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.S_un.S_addr = inet_addr("192.168.0.110");
sockAddr.sin_port = htons(9999);
//connect阻塞
connect(servSockToContect, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
char sendBuffer[100] = { 0 };
char clientBuffer[100] = { 0 };
char frams_cnt[10] = { 0, };
std::vector data_decode;//保存从网络传输数据解码后的数据
cv::Mat frame;
while (1)
{
int irecv = recv(servSockToContect, frams_cnt, 10, 0);
int cnt = atoi(frams_cnt);
//std::cout << " cnt= " << cnt << std::endl;
data_decode.resize(cnt);//将队列大小重置为图片字节长度
int index = 0;//表示接收数据长度计量
int count = cnt;//表示的是要从接收缓冲区接收字节的数量
char *recv_char = new char[cnt];//新建一个字节数组 数组长度为图片字节长度
while (count > 0)//这里只能写count > 0 如果写count >= 0 那么while循环会陷入一个死循环
{
//在网络通信中 recv 函数一次性接收到的字节数可能小于等于设定的SIZE大小,这时可能需要多次recv
int iRet = recv(servSockToContect, recv_char, count, 0);
int tmp = 0;//用来保存当前接收的数据长度
for (int k = 0; k < iRet; k++)
{
tmp = k + 1;
index++;
if (index >= cnt) { break; }
}
memcpy(&data_decode[index - tmp], recv_char, tmp);//内存拷贝函数
if (!iRet) { return -1; }
count -= iRet;//更新余下需要从接收缓冲区接收的字节数量
}
delete[]recv_char;
try {
frame = cv::imdecode(data_decode, CV_LOAD_IMAGE_COLOR);
if (!frame.empty())
{
cv::imshow("Server", frame);
cv::waitKey(1);
data_decode.clear();
}
else
{
std::cout << "#################################### " << std::endl;
data_decode.clear();
continue;
}
}
catch (const char *msg)
{
data_decode.clear();
continue;
}
}
//关闭套接字
closesocket(servSockToContect);
//终止 DLL 的使用
WSACleanup();
return 0;
}
下面是服务器端向两个客户端传输视频效果图
这篇文章,承接上一篇和上上篇文章的构造思路,后续我会把该文章对应的Github源码链接附上来