视觉媒体通信——视频网络传输

0 引言

        TCP是一种可靠的、面向连接的传输协议,它提供了流控制、拥塞控制和可靠的数据传输机制,适用于各种应用场景,特别是对数据传输的可靠性要求较高的场景,如文件传输、Web浏览和电子邮件等。但TCP协议的开销较大、时延较高、不适用于低宽带高延迟网络。

        UDP(用户数据报协议)是一种面向无连接的传输层协议。与TCP相比,UDP更加轻量级,不提供可靠性和拥塞控制机制,但具有较低的延迟和较少的开销。由于其简单性和低延迟的特点,UDP常用于需要快速传输和实时性较高的数据,但对数据丢失和乱序具有一定容忍度的场景,在使用UDP时,应用程序需要自行处理数据的可靠性和丢失恢复等问题。

        本次基于以上两种协议,来实现视频网络传输。易知,在每一种协议下,都需要有收发两端,才可以完成传输。此外,本次实现视频的实时传输,也即直播的简易版。本质是对摄像头捕捉到的每一帧图像进行传输,在建立连接的前提下,摄像头每捕捉一帧画面,就对其进行压缩编码,然后传输。

1 实验目标

        编程实现视频网络传输:利用socket接口,实现压缩视频的网络传输,尝试传输层协议tcp和udp时视频传输质量(时延、卡顿等)。

2 实验内容

2.1 TCP协议

2.1.1 Client端

        TCP协议下的客户端算法流程如图2-1-1所示。具体如下:

图2-1-1 TCP协议下Client端算法流程框图

        (1)使用函数socket()创建一个socket。

        (2)使用函数setsockopt()设置socket属性。

        (3)使用函数bind()绑定IP地址、端口等信息到socket上。

        (4)设置要连接的对方的IP地址和端口等属性。

        (5)使用函数connect()连接服务器。

        (6)设置摄像头参数并开启摄像头。

        (7)将摄像头捕获到的图像进行压缩处理并发送。

        (8)释放空间并关闭网络连接。

2.1.2 Server端

        TCP协议下的服务器端算法流程如图2-1-2所示。具体如下:

图2-1-2 TCP协议下Server端算法流程框图

        (1)使用函数socket()创建一个socket。

        (2)使用函数setsockopt()设置socket属性。

        (3)使用函数bind()绑定IP地址、端口等信息到socket上。

        (4)使函数listen()开启监听。

        (5)使用函数accept()接收客户端上来的连接,。

        (6)接收数据,将收到的数据解压缩并显示。

        (7)关闭网络连接。

        (8)关闭监听。

2.2 UDP协议

2.2.1 Client端

         UDP协议下的客户端算法流程如图2-2-1所示。具体流程如下:

图2-2-1 UDP协议下Client端算法流程框图

        (1)引入所需的头文件。

        (2)使用 WSAStartup() 函数初始化 Winsock 库。

        (3)创建套接字 sclient,使用 socket() 函数。

        (4)设置服务器地址 serAddr,包括服务器的 IP 地址和端口号。

        (5)创建 VideoCapture 对象 capture,用于打开摄像头并捕获视频帧。

        (6)设置摄像头参数,如分辨率和视频大小。

        (7)创建一个大小为 MAX_BUFFER_SIZE 的字符数组 sendData,用于存储图像数据。

        (8)创建名为 "Client" 的窗口,用于显示捕获的视频帧。

        (9)在循环中,读取摄像头的每一帧图像数据,并将每帧图像进行90JPEG压缩处理。

        (10)显示视频帧,并等待按键。

        (11)当按下 ESC 键时,退出循环。

        (12)使用 sendto() 函数将 sendData 数组中的数据通过 UDP 发送给服务器。

        (13)释放摄像头资源,释放内存,关闭窗口,关闭套接字,并清理 Winsock 库。

2.2.2 Server端

        UDP协议下的服务器端算法流程如图2-2-2所示。具体如下:

图2-2-2 UDP协议下Server端算法流程框图

        (1)初始化 Winsock 库:通过调用 WSAStartup 函数初始化 Winsock 库。

        (2)创建套接字:使用 socket 函数创建一个 UDP 套接字。

        (3)绑定 IP 地址和端口号:通过设置 sockaddr_in 结构体的相关字段,使用 bind 函数将套接字绑定到指定的 IP 地址和端口号。

        (4)创建接收缓冲区和图像对象:定义一个接收缓冲区用于接收来自客户端的图像数据,并创建一个Mat 对象用于存储接收到的图像。

        (5)进入循环等待:通过一个无限循环,服务器端不断等待来自客户端的图像数据。

        (6)接收数据:使用 recvfrom 函数从客户端接收图像数据,并将数据存储在接收缓冲区中。

        (7)解析图像数据:将接收到的图像数据解压缩并存储。

        (8)显示图像:使用 cv::imshow 函数显示接收到的图像。

        (9)继续等待下一次数据接收:回到循环的开头,继续等待下一次来自客户端的图像数据。

        (10)清理资源:在退出循环后,释放所使用的资源,包括关闭套接字和清理 Winsock 库。

3 实验结果

3.1 TCP协议

图3-1-1 TCP协议下收(右)发(左)端视频情况

图3-1-2 TCP协议下收(右)发(左)端视频质量

        由上述结果可知,对视频进行JPEG压缩并传输,发送端摄像头捕获的帧率为30FPS,且发送的时延大致稳定在0.06ms;接收端视频的帧率约为14FPS,接收处理时延约为0.15ms且不稳定。而且在实际的过程中,肉眼能够明显看到接收端视频的卡顿,效果较差。我看到接收端处理时延会突然出现8ms跳变,因此我怀疑是解压缩解压缩的问题。

3.2 UDP协议

图3-2-1 UDP协议下收(右)发(左)端视频情况

图3-2-2 UDP协议下收(右)发(左)端视频质量

        由上述结果可知,对视频进行JPEG压缩并传输,发送端摄像头捕获的帧率为30FPS,且发送的时延平均在0.05ms;接收端视频的帧率约为13FPS,接收处理时延约为8ms,没有较大波动。在实际的过程中,肉眼能够明显看到接收端视频的不太流畅,但是不会出现明显卡顿,是由于帧率较低且处理时延较稳定的原因。

4 代码部分

4.1 TCP协议

4.1.1 Client端

//进行JPEG压缩后传输
#include <WINSOCK2.H>
#include <iostream>
#include <stdio.h>
#include <cv.h>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <vector>
#include <chrono>

#pragma  comment(lib,"ws2_32.lib")

using namespace cv;

int main(int argc, char* argv[])
{
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA data;
	if (WSAStartup(sockVersion, &data) != 0)
	{
		return 0;
	}

	SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sclient == INVALID_SOCKET)
	{
		printf("invalid socket !\n");
		return 0;
	}

	sockaddr_in serAddr;
	serAddr.sin_family = AF_INET;
	serAddr.sin_port = htons(8888);
	serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //服务器的IP地址,可以是:①连接外网后分配的②手动设置的
	if (connect(sclient, (sockaddr*)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
	{
		printf("connect error !\n");
		closesocket(sclient);
		return 0;
	}
	//摄像头
	VideoCapture capture(0);
	//摄像头参数设置
	capture.set(CV_CAP_PROP_FRAME_WIDTH, 640);//宽度
	capture.set(CV_CAP_PROP_FRAME_HEIGHT, 480);//高度

	//视频大小
	Size S = Size((int)capture.get(CV_CAP_PROP_FRAME_WIDTH),
		(int)capture.get(CV_CAP_PROP_FRAME_HEIGHT));
	//初始化压缩
	std::vector<int> compressionParams;
	compressionParams.push_back(CV_IMWRITE_JPEG_QUALITY);//JPEG压缩
	compressionParams.push_back(90); // 压缩质量,0 - 100
 
 // 变量用于计算时延
std::chrono::steady_clock::time_point startTime1, endTime1;
 
	int i, j;
	int key;
	char* sendData = new char[1000000];

	namedWindow("Client", CV_WINDOW_AUTOSIZE);
	Mat frame, gray;
	while (capture.read(frame))
	{

		// 压缩图像到frame
		std::vector<uchar> compressedData;
		imencode(".jpg", frame, compressedData, compressionParams);
      // 发送数据前记录时间戳
      startTime1 = std::chrono::steady_clock::now();
		// 发送数据
		send(sclient, reinterpret_cast<const char*>(compressedData.data()), compressedData.size(), 0);
      // 发送数据后记录时间戳
      endTime1 = std::chrono::steady_clock::now();

      // 计算时延
      std::chrono::duration<double> timeDiff = endTime1 - startTime1;
      printf("发送时延: %.2f ms\n", timeDiff.count() * 1000);

	  //摄像头捕获的FPS
	  int fps = capture.get(CV_CAP_PROP_FPS);
	  printf("current fps : %d \n", fps);

		imshow("Client", frame);
		cvWaitKey(30);//如果服务端收到的视频比较卡,此处延时适当改大一点
		send(sclient, sendData, 1000000, 0);
		//等待按键
		key = cvWaitKey(30);
		//按ESC键直接退出
		if (key == 27) {
			return 0;
		}

	}
	capture.release();
	delete[] sendData;
	cvDestroyWindow("Client");
	closesocket(sclient);
	WSACleanup();
	return 0;
}

4.1.2 Server端

//进行JPEG压缩后传输
#include <stdio.h>
#include <winsock2.h>
#include <cv.h>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <vector>
#include <chrono>

#pragma comment(lib,"ws2_32.lib")

int main(void)
{
	//初始化WSA
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsaData;
	if (WSAStartup(sockVersion, &wsaData) != 0)
	{
		return 0;
	}
	//创建套接字
	SOCKET slisten = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (slisten == INVALID_SOCKET)
	{
		printf("socket error !");
		return 0;
	}
	//绑定IP和端口
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	sin.sin_port = htons(8888);//端口8888
	sin.sin_addr.S_un.S_addr = INADDR_ANY;
	if (bind(slisten, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		printf("bind error !");
	}

	//开始监听
	if (listen(slisten, 5) == SOCKET_ERROR)
	{
		printf("listen error !");
		return 0;
	}

	//循环接收数据
	SOCKET sClient;
	sockaddr_in remoteAddr;
	int nAddrlen = sizeof(remoteAddr);

	printf("等待连接...\n");
	do
	{
		sClient = accept(slisten, (SOCKADDR*)&remoteAddr, &nAddrlen);
	} while (sClient == INVALID_SOCKET);
	printf("接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));

	cv::Mat image_src(cv::Size(640, 480), CV_8UC3);
 
    // 变量用于计算帧率时延
    std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now();
    int frameCount = 0;
    double fps = 0.0;
    std::chrono::steady_clock::time_point startTime1, endTime1;

	char* revData = new char[1000000];

	int i, j;
	int ret;
	int key;
	cv::namedWindow("server", cv::WINDOW_AUTOSIZE);
	while (true)
	{
		// 接收数据
		ret = recv(sClient, revData, 1000000, 0);
     // 接收数据前记录时间戳
      startTime1 = std::chrono::steady_clock::now();
		if (ret > 0)
		{
			revData[ret] = 0x00;
			// 创建向量存储接收数据
			std::vector<uchar> receivedData(revData, revData + ret);

			// 解码收到的压缩图像
			cv::Mat receivedImage = cv::imdecode(receivedData, cv::IMREAD_COLOR);

			// Display the received image
			//cvWaitKey(1);
			if (!receivedImage.empty())
			{
				cv::imshow("server", receivedImage);
			}
			
          // 接收数据后记录时间戳
          endTime1 = std::chrono::steady_clock::now();

          // 计算接收时延
          std::chrono::duration<double> timeDiff = endTime1 - startTime1;
          printf("接收处理时延: %.2f ms\n", timeDiff.count() * 1000);
          
			// 重置
			ret = 0;
			//等待按键
			key = cvWaitKey(30);
			//按ESC键直接退出
			if (key == 27) {
				return 0;
			}
		}
		// 计算帧率
		frameCount++;
		std::chrono::steady_clock::time_point endTime = std::chrono::steady_clock::now();
		std::chrono::duration<double> elapsedTime = endTime - startTime;
		if (elapsedTime.count() >= 1.0)
		{
			fps = frameCount / elapsedTime.count();
			printf("帧率: %.2f\n", fps);
			frameCount = 0;
			startTime = endTime;
		}
		//等待按键
		key = cvWaitKey(30);
		//按ESC键直接退出
		if (key == 27) {
			return 0;
		}
	}
	cvDestroyWindow("server");
	delete[] revData;
	closesocket(slisten);
	WSACleanup();
	return 0;
}

4.2 UDP协议

4.2.1 Client端

#include <WINSOCK2.H>
#include <iostream>
#include <stdio.h>
#include <cv.h>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <chrono>

#pragma comment(lib,"ws2_32.lib")

#define MAX_BUFFER_SIZE 1000000

using namespace cv;

int main()
{
    WORD sockVersion = MAKEWORD(2, 2);
    WSADATA data;
    if (WSAStartup(sockVersion, &data) != 0)
    {
        return 0;
    }

    // 创建套接字
    SOCKET sclient = socket(AF_INET, SOCK_DGRAM, 0);
    if (sclient == -1)
    {
        printf("invalid socket !\n");
        return 0;
    }

    // 服务器地址
    sockaddr_in serAddr;
    serAddr.sin_family = AF_INET;
    serAddr.sin_port = htons(8888);
    serAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 服务器的IP地址,可以根据实际情况修改

    // 摄像头
    VideoCapture capture(0);
    // 摄像头参数设置
    // 分辨率
    capture.set(CV_CAP_PROP_FRAME_WIDTH, 640);   // 宽度
    capture.set(CV_CAP_PROP_FRAME_HEIGHT, 480);  // 高度

    // 视频大小
    Size S = Size((int)capture.get(CV_CAP_PROP_FRAME_WIDTH),
        (int)capture.get(CV_CAP_PROP_FRAME_HEIGHT));

    std::vector<uchar> sendData;

    namedWindow("Client", CV_WINDOW_AUTOSIZE);
    Mat frame;
    int key;
    // 变量用于计算时延
    std::chrono::steady_clock::time_point startTime1, endTime1;

    while (capture.read(frame))
    {
        
        // 图像压缩
        std::vector<int> compression_params;
        compression_params.push_back(CV_IMWRITE_JPEG_QUALITY);  // JPEG压缩
        compression_params.push_back(90);  // 压缩质量,0 - 100

        // 压缩图像到sendData中
        imencode(".jpg", frame, sendData, compression_params);

        imshow("Client", frame);
        //等待按键
        key = cvWaitKey(30);
        //按ESC键直接退出
        if (key == 27) {
            break;
        }
        // 发送数据前记录时间戳
        startTime1 = std::chrono::steady_clock::now();
        
        //摄像头捕获的FPS
        int fps = capture.get(CV_CAP_PROP_FPS);
        printf("current fps : %d \n", fps);
        // 发送数据
        int sendResult = sendto(sclient, reinterpret_cast<char*>(sendData.data()), sendData.size(), 0, (struct sockaddr*)&serAddr, sizeof(serAddr));
        if (sendResult == -1)
        {
            printf("Failed to send data\n");
        }       

        // 发送数据后记录时间戳
        endTime1 = std::chrono::steady_clock::now();

        // 计算时延
        std::chrono::duration<double> timeDiff = endTime1 - startTime1;
        printf("发送时延: %.2f ms\n", timeDiff.count() * 1000);
    }

    capture.release();
    cvDestroyWindow("Client");
    closesocket(sclient);
    WSACleanup();
    return 0;
}

4.2.2 Server端

#include <stdio.h>
#include <winsock2.h>
#include <opencv2/opencv.hpp>
#include <cv.h>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <Ws2tcpip.h>
#include <chrono>

#pragma comment(lib, "ws2_32.lib")

#define MAX_BUFFER_SIZE 1000000

int main()
{
    // 初始化WSA
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("Failed to initialize winsock");
        return 0;
    }

    // 创建套接字
    SOCKET sServer = socket(AF_INET, SOCK_DGRAM, 0);
    if (sServer == INVALID_SOCKET)
    {
        printf("Failed to create socket");
        return 0;
    }

    // 绑定IP和端口
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8888);  // 端口8888
    serverAddr.sin_addr.S_un.S_addr = INADDR_ANY;
    if (bind(sServer, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1)
    {
        printf("Failed to bind socket");
        closesocket(sServer);
        return 0;
    }

    cv::Mat image_dst;
    std::vector<uchar> imageData;

    cv::namedWindow("Server", cv::WINDOW_NORMAL);

    printf("等待连接...\n");

    // 变量用于计算帧率时延
    std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now();
    int frameCount = 0;
    double fps = 0.0;
    std::chrono::steady_clock::time_point startTime1, endTime1;

    while (1)
    {
        // 接收数据前记录时间戳
        startTime1 = std::chrono::steady_clock::now();
        // 接收数据
        struct sockaddr_in clientAddr;
        socklen_t nLen = sizeof(clientAddr);
        char revData[MAX_BUFFER_SIZE];
        int ret = recvfrom(sServer, revData, MAX_BUFFER_SIZE, 0, (struct sockaddr*)&clientAddr, &nLen);
        

        if (ret > 0)
        {
            revData[ret] = '\0';

            // 将接收到的数据存储在imageData中
            imageData.assign(revData, revData + ret);

            // 解码并显示图像
            image_dst = cv::imdecode(imageData, cv::IMREAD_COLOR);
            if (!image_dst.empty())
            {
                cv::imshow("Server", image_dst);
            }
            // 接收数据后记录时间戳
            endTime1 = std::chrono::steady_clock::now();

            // 计算接收时延
            std::chrono::duration<double> timeDiff = endTime1 - startTime1;
            printf("接收处理时延: %.2f ms\n", timeDiff.count() * 1000);
            //等待按键
            int key = cvWaitKey(30);
            //按ESC键直接退出
            if (key == 27) {
                break;
            }
            // 计算帧率
            frameCount++;
            std::chrono::steady_clock::time_point endTime = std::chrono::steady_clock::now();
            std::chrono::duration<double> elapsedTime = endTime - startTime;
            if (elapsedTime.count() >= 1.0)
            {
                fps = frameCount / elapsedTime.count();
                printf("帧率: %.2f\n", fps);
                frameCount = 0;
                startTime = endTime;
            }       
        }
        //等待按键
        int key = cvWaitKey(30);
        //按ESC键直接退出
        if (key == 27) {
            break;
        }
    }
    cv::destroyWindow("Server");
    closesocket(sServer);
    WSACleanup();
    return 0;
}

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值