小言_互联网的博客

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

459人阅读  评论(0)

项目场景:

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

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


问题描述

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

以下为未优化之前的代码


  
  1. char send_char[SIZE] = { 0, };
  2.          int index = 0;
  3.          bool flag = false;
  4.          for ( int i = 0; i < len_encoder / SIZE + 1; ++i) {
  5.              for ( int k = 0; k < SIZE; ++k) {
  6.                  if (index >= data_encode.size()) {
  7.                     flag = true;
  8.                      break;
  9.                 }
  10.                 send_char[k] = data_encode[index++];
  11.             }
  12.             send(m_server, send_char, SIZE, 0);
  13.         }


原因分析:

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


解决方案:

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

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

 以下为优化后的代码


  
  1. char *send_b = new char[data_encode.size()];
  2. 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,久而久之,码流得不到解析,就会报错


解决方案:

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

以下为未优化之前的代码 


  
  1. while (m_cap.read(frame)) {
  2. imencode( ".jpg", frame, data_encode, params); // 对图像进行压缩
  3. int len_encoder = data_encode.size();
  4. _itoa_s(len_encoder, frames_cnt, 10);
  5. send(m_server, frames_cnt, 10, 0);
  6. _itoa_s(SIZE, frames_cnt, 10);
  7. send(m_server, frames_cnt, 10, 0);
  8. // 发送
  9. char send_char[SIZE] = { 0, };
  10. int index = 0;
  11. bool flag = false;
  12. char *send_b = new char[data_encode.size()];
  13. for ( int i = 0; i<data_encode.size(); i++)
  14. {
  15. //data_encode.size()数据装换成字符数组
  16. send_b[i] = data_encode[i];
  17. }
  18. int iSend = send(m_server, send_b, data_encode.size(), 0);
  19. delete[]send_b;
  20. data_encode.clear();
  21. ++j;
  22. }

以下为优化后的代码


  
  1. myMutex. lock();
  2. if (queueInput.empty()) { / /如果队列中没有数据 说明队列为空 此时应该等待生产者向队列中输入数据
  3. myMutex. unlock(); / /释放锁
  4. Sleep( 3); / /睡眠三秒钟 把锁让给生产者
  5. continue;
  6. }
  7. else {
  8. frame = queueInput.front(); / /从队列中取出图像矩阵
  9. queueInput.pop();
  10. myMutex. unlock(); / /释放锁
  11. }
  12. imencode( ".jpg", frame, data_encode, params); / / 对图像进行压缩
  13. int len_encoder = data_encode. size(); / /获取图像编码后的字节长度 方便后续通过TCP传输时 接收端知道此次传输的字节大小
  14. _itoa_s(len_encoder, frames_cnt, 10); / /
  15. send(m_server, frames_cnt, 10, 0); / /将图像字节长度 进行传输
  16. / / 发送
  17. int index = 0; / /标志实时接收图像字节的长度 方便程序中判断还有多少字节尚未接收到
  18. char * send_b = new char[ data_encode. size()]; / / 创建一个字节数组 开启大小为图像字节长度的字符数组空间
  19. / /这里是将 data_encode首地址且长度为图片字节长度 通过内存拷贝复制到 send_b数组中,相比于采用循环单个元素赋值,速度快了至少 10
  20. memcpy( send_b, & data_encode[ 0], data_encode. size());
  21. int iSend = send(m_server, send_b, data_encode. size(), 0); / /将图像字节数据传输到服务器端
  22. delete[] send_b; / /销毁对象
  23. data_encode.clear(); / /将队列清空 方便下一次进行图像矩阵接收
  24. + +j;

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

 问题描述

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

原因分析:

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


解决方案:

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

以下为解决方案代码 


  
  1. while (count > 0) //这里只能写count > 0 如果写count >= 0 那么while循环会陷入一个死循环
  2. {
  3. //在网络通信中 recv 函数一次性接收到的字节数可能小于等于设定的SIZE大小,这时可能需要多次recv
  4. int iRet = recv(m_accept, recv_char, count, 0);
  5. int tmp = 0; //用来保存当前接收的数据长度
  6. for ( int k = 0; k < iRet; k++)
  7. {
  8. tmp = k+ 1;
  9. index++;
  10. if (index >= cnt) { break; }
  11. }
  12. memcpy(&data_decode[index - tmp ], recv_char , tmp); //内存拷贝函数
  13. if (!iRet) { return -1; }
  14. count -= iRet; //更新余下需要从接收缓冲区接收的字节数量
  15. }
  16. delete[]recv_char;

下面贴出客户端代码


  
  1. / / tcp_video_client.cpp : 定义控制台应用程序的入口点。
  2. / /
  3. #include "stdafx.h"
  4. #include "stdafx.h"
  5. #include "opencv2\opencv.hpp"
  6. #include "opencv2\imgproc\imgproc.hpp"
  7. #include <WinSock 2.h >
  8. #include <iostream >
  9. #include <mutex >
  10. #include <thread >
  11. #pragma comment(lib, "ws2_32.lib")
  12. #pragma comment(lib, "opencv_world340.lib")
  13. std ::mutex myMutex;
  14. std ::queue <cv ::Mat > queueInput; / /存储图像的队列
  15. void get_online_video()
  16. {
  17. / /海康威视子码流拉流地址 用户名 admin 密码abc. 1234 需要修改为对应的用户名和密码
  18. std :: string url = "rtsp://admin:abc.1234@192.168.0.64:554/h264/ch1/sub/av_stream";
  19. cv ::VideoCapture cap(url);
  20. cv ::Mat frame; / /保存抽帧的图像矩阵
  21. while ( 1)
  22. {
  23. cap >> frame;
  24. myMutex. lock();
  25. if (queueInput. size() > 3) {
  26. queueInput.pop();
  27. }
  28. else {
  29. queueInput.push(frame);
  30. }
  31. myMutex. unlock();
  32. }
  33. }
  34. int send_online_video()
  35. {
  36. WORD w_req = MAKEWORD( 2, 2); / /版本号
  37. WSADATA wsadata;
  38. int err;
  39. err = WSAStartup(w_req, &wsadata);
  40. if (err ! = 0) {
  41. std ::cout < < "初始化套接字库失败!" < < std ::endl;
  42. return false;
  43. }
  44. else {
  45. std ::cout < < "初始化套接字库成功!" < < std ::endl;
  46. }
  47. / /检测版本号
  48. if (LOBYTE(wsadata.wVersion) ! = 2 || HIBYTE(wsadata.wHighVersion) ! = 2) {
  49. std ::cout < < "套接字库版本号不符!" < < std ::endl;
  50. WSACleanup();
  51. return false;
  52. }
  53. else {
  54. std ::cout < < "套接字库版本正确!" < < std ::endl;
  55. }
  56. SOCKADDR_ IN server_addr;
  57. SOCKADDR_ IN accept_addr;
  58. / /填充服务端信息
  59. server_addr.sin_family = AF_INET; / / 用来定义那种地址族,AF_INET:IPV 4
  60. std :: string m_ip = "192.168.0.111";
  61. server_addr.sin_addr.S_un.S_addr = inet_addr(m_ip.c_str()); / / 保存ip地址,htonl将一个无符号长整型转换为TCP /IP协议网络的大端
  62. / / INADDR_ ANY表示一个服务器上的所有网卡
  63. server_addr.sin_port = htons( 7777); / / 端口号
  64. / /创建套接字
  65. SOCKET m_server = socket(AF_INET, SOCK_STREAM, 0);
  66. if (connect(m_server, (SOCKADDR *) &server_addr, sizeof(SOCKADDR)) = = SOCKET_ ERROR) {
  67. std ::cout < < "服务器连接失败!" < < std ::endl;
  68. WSACleanup();
  69. return false;
  70. }
  71. else {
  72. std ::cout < < "服务器连接成功!" < < std ::endl;
  73. }
  74. cv ::Mat frame;
  75. std ::vector <uchar > data_encode; / /保存从网络传输数据解码后的数据
  76. std ::vector <int > params; / / 压缩参数
  77. params.resize( 3, 0);
  78. params[ 0] = cv ::IMWRITE_JPEG_QUALITY; / / 无损压缩
  79. params[ 1] = 30; / /压缩的质量参数 该值越大 压缩后的图像质量越好
  80. char frames_cnt[ 10] = { 0, };
  81. std ::cout < < "开始发送" < < std ::endl;
  82. int j = 0;
  83. while ( 1) {
  84. / * 这里采用多线程 从队列中存取数据 主要是防止单线程解码网络视频速度太慢导致的网络拥塞 * /
  85. myMutex. lock();
  86. if (queueInput.empty()) { / /如果队列中没有数据 说明队列为空 此时应该等待生产者向队列中输入数据
  87. myMutex. unlock(); / /释放锁
  88. Sleep( 3); / /睡眠三秒钟 把锁让给生产者
  89. continue;
  90. }
  91. else {
  92. frame = queueInput.front(); / /从队列中取出图像矩阵
  93. queueInput.pop();
  94. myMutex. unlock(); / /释放锁
  95. }
  96. imencode( ".jpg", frame, data_encode, params); / / 对图像进行压缩
  97. int len_encoder = data_encode. size(); / /获取图像编码后的字节长度 方便后续通过TCP传输时 接收端知道此次传输的字节大小
  98. _itoa_s(len_encoder, frames_cnt, 10); / /
  99. send(m_server, frames_cnt, 10, 0); / /将图像字节长度 进行传输
  100. / / 发送
  101. int index = 0; / /标志实时接收图像字节的长度 方便程序中判断还有多少字节尚未接收到
  102. char * send_b = new char[ data_encode. size()]; / / 创建一个字节数组 开启大小为图像字节长度的字符数组空间
  103. / /这里是将 data_encode首地址且长度为图片字节长度 通过内存拷贝复制到 send_b数组中,相比于采用循环单个元素赋值,速度快了至少 10
  104. memcpy( send_b, & data_encode[ 0], data_encode. size());
  105. int iSend = send(m_server, send_b, data_encode. size(), 0); / /将图像字节数据传输到服务器端
  106. delete[] send_b; / /销毁对象
  107. data_encode.clear(); / /将队列清空 方便下一次进行图像矩阵接收
  108. + +j;
  109. }
  110. std ::cout < < "发送完成";
  111. closesocket(m_server); / /关闭发送端套接字
  112. WSACleanup(); / /释放初始化Ws 2_ 32.dll所分配的资源。
  113. }
  114. int ma in()
  115. {
  116. std ::thread Get( get_online_video);
  117. std ::thread Send( send_online_video);
  118. Get.join();
  119. Send.join();
  120. return 0;
  121. }

下面贴出服务器端代码


  
  1. bool Server :: receive_dat a() {
  2. Mat frame;
  3. vector <uchar > data_decode;
  4. std ::vector <int > params; / / 压缩参数
  5. params.resize( 3, 0);
  6. params[ 0] = IMWRITE_JPEG_QUALITY; / / 无损压缩
  7. params[ 1] = 50;
  8. cv ::namedWindow( "Server", cv ::WINDOW_NORMAL);
  9. char frams_cnt[ 10] = { 0, };
  10. / / 解析总帧数
  11. int count = atoi(frams_cnt);
  12. int idx = 0;
  13. while ( 1) {
  14. / / 解析图片字节长度
  15. int irecv = recv(m_ accept, frams_cnt, 10, 0);
  16. int cnt = atoi(frams_cnt);
  17. data_decode.resize(cnt); / /将队列大小重置为图片字节长度
  18. int index = 0; / /表示接收数据长度计量
  19. count = cnt; / /表示的是要从接收缓冲区接收字节的数量
  20. char *recv_char = new char[cnt]; / /新建一个字节数组 数组长度为图片字节长度
  21. while ( count > 0) / /这里只能写 count > 0 如果写 count >= 0 那么while循环会陷入一个死循环
  22. {
  23. / /在网络通信中 recv 函数一次性接收到的字节数可能小于等于设定的 SIZE大小,这时可能需要多次recv
  24. int iRet = recv(m_ accept, recv_char, count, 0);
  25. int tmp = 0;
  26. for (int k = 0; k < iRet; k + +)
  27. {
  28. tmp = k + 1;
  29. index + +;
  30. if ( index >= cnt) { break; }
  31. }
  32. memcpy( & data_decode[ index - tmp ], recv_char , tmp);
  33. if (!iRet) { return - 1; }
  34. count - = iRet; / /更新余下需要从接收缓冲区接收的字节数量
  35. }
  36. delete[]recv_char;
  37. try {
  38. frame = cv ::imdecode( data_decode, CV_LOAD_IMAGE_COLOR);
  39. if (!frame.empty())
  40. {
  41. imshow( "Server", frame);
  42. waitKey( 1);
  43. data_decode.clear();
  44. }
  45. else
  46. {
  47. std ::cout < < "#################################### " < < std ::endl;
  48. data_decode.clear();
  49. continue;
  50. }
  51. }
  52. catch (const char *msg)
  53. {
  54. data_decode.clear();
  55. continue;
  56. }
  57. }
  58. cout < < "接受完成";
  59. return true;
  60. }

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

完成代码可见github链接

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


转载:https://blog.csdn.net/linxizi0622/article/details/128868324
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场