c++ && OpenCV的多线程实时视频传输(TCP on Windows)

项目场景:

  在无线局域网里采用TCP协议传输海康威视网络视频:

     上一篇文章中采用UDP协议传输网络视频,由于事先不知道图像字节长度,导致每次传输视频之前都需要根据图像大小更改UDP接收缓冲区,同时,上一篇文章中涉及到的只是在局域网中传输USB摄像头视频,如何快速解码网络摄像头并且高质量传输。这里我用到了多线程对快速解码这一要求进行了响应,采用TCP协议,在传输图像字节之前,先传输图像字节长度,在接收端根据发送端发送的长度信息,实时new一个字节数组作为缓冲区对图像字节数据进行保存。


问题描述

1、服务器端从接收缓冲区接收图像字节时,将图像队列复制给字节数组,如果采用在循环中进行赋值,那么当队列长度很大时 , 耗时很长,请看代码:

以下为未优化之前的代码

char send_char[SIZE] = { 0, };
        int index = 0;
        bool flag = false;
        for (int i = 0; i < len_encoder / SIZE + 1; ++i) {
            for (int k = 0; k < SIZE; ++k) {
                if (index >= data_encode.size()) {
                    flag = true;
                    break;
                }
                send_char[k] = data_encode[index++];
            }
            send(m_server, send_char, SIZE, 0);
        }


原因分析:

在循环中将data_encode队列中的每一个元素赋值为send_char,如果这个循环很大,是很耗时的.


解决方案:

memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。

memcpy是内存拷贝,当一段连续的空间很长时,采用内存拷贝效率高,而采用在循环中对数组元素进行单个赋值 效率很低。采用memcpy拷贝时 是一次性将源src 拷贝到目标dst

 以下为优化后的代码

char *send_b = new char[data_encode.size()];
memcpy(send_b, &data_encode[0], data_encode.size());

优化后 , 代码的执行速度比优化前至少加快了10倍  

 问题描述

2、采用opencv软解码海康威视摄像头视频流时,如果解码速度太慢,这样的状态持续一段时间后,会导致解码模块出bug,报类似于以下错误:

[h264 @ 000000000ef76940] cabac decode of qscale diff failed at 84 17 [h264 @ 000000000ef76940] error while decoding MB 84 17, bytestream 507ffmpeg

原因分析:

这种错误,我在网上查了一下,是由于解码模块在相邻两帧解码速度太慢导致的错误

之前代码逻辑是在一个线程里 解码视频 + 发送视频 ,这个过程太耗时,超过了40ms,久而久之,码流得不到解析,就会报错


解决方案:

那么这里可以采用多线程:一个线程解码视频,一个线程发送视频

以下为未优化之前的代码 

while (m_cap.read(frame)) {
		imencode(".jpg", frame, data_encode, params);  // 对图像进行压缩
		int len_encoder = data_encode.size();
		_itoa_s(len_encoder, frames_cnt, 10);
		send(m_server, frames_cnt, 10, 0);
		_itoa_s(SIZE, frames_cnt, 10);
		send(m_server, frames_cnt, 10, 0);
		// 发送
		char send_char[SIZE] = { 0, };
		int index = 0;
		bool flag = false;
		char *send_b = new char[data_encode.size()];
		for (int i = 0; i<data_encode.size(); i++)
		{
			//data_encode.size()数据装换成字符数组
			send_b[i] = data_encode[i];
		}
		int iSend = send(m_server, send_b, data_encode.size(), 0);
		delete[]send_b;

		data_encode.clear();
		++j;
	}

以下为优化后的代码

myMutex.lock();
		if (queueInput.empty()) {//如果队列中没有数据 说明队列为空 此时应该等待生产者向队列中输入数据
			myMutex.unlock();//释放锁
			Sleep(3);//睡眠三秒钟  把锁让给生产者
			continue;
		}
		else {
			frame = queueInput.front();//从队列中取出图像矩阵
			queueInput.pop();
			myMutex.unlock();//释放锁
		}
		imencode(".jpg", frame, data_encode, params);  // 对图像进行压缩
		int len_encoder = data_encode.size();//获取图像编码后的字节长度 方便后续通过TCP传输时  接收端知道此次传输的字节大小
		_itoa_s(len_encoder, frames_cnt, 10);// 
		send(m_server, 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(m_server, send_b, data_encode.size(), 0);//将图像字节数据传输到服务器端
		delete[]send_b;//销毁对象
		data_encode.clear();//将队列清空  方便下一次进行图像矩阵接收
		++j;

优化后 , 代码的执行速度比优化前至少加快了10倍  

 问题描述

3、在局域网中进行数据传输时,假如客户端传输100个字节长度的数据,在服务器端可能是先接收到53个字节,然后再接收剩下的47个字节的数据。如果我把客户端和服务器端都放在一个终端上运行,则不会出现这种情况。由于前期是在一个终端(也就是客户端和服务器端都在一台终端上)上面进行代码开发,代码没有出现问题,但是将代码移植到局域网中,出现了上述所说的现象:

原因分析:

这种现象,可能是由于网络传输导致。既然不能避免,那么在服务器端接收数据时,就进行数据长度校验,客户端首先会向服务器发送一个待接收的数据长度值,服务器端按照这个长度值进行数据接收


解决方案:

在服务器端接收数据时,就进行数据长度校验,客户端首先会向服务器发送一个待接收的数据长度值,服务器端按照这个长度值进行数据接收

以下为解决方案代码 

while (count > 0)//这里只能写count > 0 如果写count >= 0 那么while循环会陷入一个死循环
		{
			//在网络通信中  recv 函数一次性接收到的字节数可能小于等于设定的SIZE大小,这时可能需要多次recv
			int iRet = recv(m_accept, 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;

下面贴出客户端代码

// tcp_video_client.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "stdafx.h"
#include "opencv2opencv.hpp"
#include "opencv2imgprocimgproc.hpp"
#include<WinSock2.h>
#include<iostream>
#include<mutex>
#include<thread>
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"opencv_world340.lib")
std::mutex myMutex;
std::queue<cv::Mat> queueInput;//存储图像的队列

void get_online_video()
{
	//海康威视子码流拉流地址  用户名 admin 密码abc.1234 需要修改为对应的用户名和密码
	std::string  url = "rtsp://admin:[email protected]:554/h264/ch1/sub/av_stream";
	cv::VideoCapture cap(url);
	cv::Mat frame;//保存抽帧的图像矩阵
	while (1)
	{
		cap >> frame;
		myMutex.lock();
		if (queueInput.size() > 3) {
			queueInput.pop();
		}
		else {
			queueInput.push(frame);
		}
		myMutex.unlock();
	}
}

int send_online_video()
{
	WORD w_req = MAKEWORD(2, 2);//版本号
	WSADATA wsadata;
	int err;
	err = WSAStartup(w_req, &wsadata);
	if (err != 0) {
		std::cout << "初始化套接字库失败!" << std::endl;
		return false;
	}
	else {
		std::cout << "初始化套接字库成功!" << std::endl;
	}

	//检测版本号
	if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) {
		std::cout << "套接字库版本号不符!" << std::endl;
		WSACleanup();
		return false;
	}
	else {
		std::cout << "套接字库版本正确!" << std::endl;
	}

	SOCKADDR_IN server_addr;
	SOCKADDR_IN accept_addr;

	//填充服务端信息
	server_addr.sin_family = AF_INET;  // 用来定义那种地址族,AF_INET:IPV4
	std::string m_ip = "192.168.0.111";
	server_addr.sin_addr.S_un.S_addr = inet_addr(m_ip.c_str());  // 保存ip地址,htonl将一个无符号长整型转换为TCP/IP协议网络的大端
	// INADDR_ANY表示一个服务器上的所有网卡
	server_addr.sin_port = htons(7777);  // 端口号

	//创建套接字
	SOCKET m_server = socket(AF_INET, SOCK_STREAM, 0);
	if (connect(m_server, (SOCKADDR*)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
		std::cout << "服务器连接失败!" << std::endl;
		WSACleanup();
		return false;
	}
	else {
		std::cout << "服务器连接成功!" << std::endl;
	}

	cv::Mat frame;
	std::vector<uchar> data_encode;//保存从网络传输数据解码后的数据
	std::vector<int> params;  // 压缩参数
	params.resize(3, 0);
	params[0] = cv::IMWRITE_JPEG_QUALITY; // 无损压缩
	params[1] = 30;//压缩的质量参数 该值越大 压缩后的图像质量越好
	char frames_cnt[10] = { 0, };
	std::cout << "开始发送" << std::endl;
	int j = 0;
	while (1) {
		/* 这里采用多线程 从队列中存取数据 主要是防止单线程解码网络视频速度太慢导致的网络拥塞*/
		myMutex.lock();
		if (queueInput.empty()) {//如果队列中没有数据 说明队列为空 此时应该等待生产者向队列中输入数据
			myMutex.unlock();//释放锁
			Sleep(3);//睡眠三秒钟  把锁让给生产者
			continue;
		}
		else {
			frame = queueInput.front();//从队列中取出图像矩阵
			queueInput.pop();
			myMutex.unlock();//释放锁
		}
		imencode(".jpg", frame, data_encode, params);  // 对图像进行压缩
		int len_encoder = data_encode.size();//获取图像编码后的字节长度 方便后续通过TCP传输时  接收端知道此次传输的字节大小
		_itoa_s(len_encoder, frames_cnt, 10);// 
		send(m_server, 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(m_server, send_b, data_encode.size(), 0);//将图像字节数据传输到服务器端
		delete[]send_b;//销毁对象
		data_encode.clear();//将队列清空  方便下一次进行图像矩阵接收
		++j;
	}
	std::cout << "发送完成";
	closesocket(m_server);//关闭发送端套接字
	WSACleanup();//释放初始化Ws2_32.dll所分配的资源。
}

int main()
{
	std::thread Get(get_online_video);
	std::thread Send(send_online_video);
	Get.join();
	Send.join();
	return 0;
}

下面贴出服务器端代码

bool Server::receive_data() {
	Mat frame;
	vector<uchar> data_decode;
	std::vector<int> params;  // 压缩参数
	params.resize(3, 0);
	params[0] = IMWRITE_JPEG_QUALITY; // 无损压缩
	params[1] = 50;
	cv::namedWindow("Server", cv::WINDOW_NORMAL);
	char frams_cnt[10] = { 0, };
	// 解析总帧数
	int count = atoi(frams_cnt);
	int idx = 0;
	while (1) {
		// 解析图片字节长度
		int irecv = recv(m_accept, frams_cnt, 10, 0);
		int cnt = atoi(frams_cnt);
		
		data_decode.resize(cnt);//将队列大小重置为图片字节长度
		int index = 0;//表示接收数据长度计量
		count = cnt;//表示的是要从接收缓冲区接收字节的数量
		char *recv_char = new char[cnt];//新建一个字节数组 数组长度为图片字节长度
		while (count > 0)//这里只能写count > 0 如果写count >= 0 那么while循环会陷入一个死循环
		{
			//在网络通信中  recv 函数一次性接收到的字节数可能小于等于设定的SIZE大小,这时可能需要多次recv
			int iRet = recv(m_accept, 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())
			{
				imshow("Server", frame);
				waitKey(1);
				data_decode.clear();
			}
			else
			{
				std::cout << "####################################   " << std::endl;
				data_decode.clear();
				continue;
			}
		}
		catch (const char *msg)
		{
			data_decode.clear();
			continue;
		}
		
	}
	cout << "接受完成";
	return true;
}

以上为客户端和服务器端核心代码

完成代码可见github链接

https://github.com/linxizi/TCP_Online_Video.git

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>