使用UDP协议传输视频流!(分片、缓存)

背景

最近在开发工作中遇到需要两台本地设备之间进行视频流的传输的情况。但是团队一来没有这方面的专业人才,二来视频流的传续数据量很大,针对TCP和UDP的具体选择也不明确。

本文是在上诉背景之下进行的研究和开发工作。

目录

背景

UDP和TCP协议的选择

Socket-UDP协议代码详解

UDP协议发送端

UDP协议接收端


UDP和TCP协议的选择

视频流更加适合UDP协议的传输!

|----------------|-------|-------|
| 序号 | UDP协议 | TCP协议 |
| 是否需要握手 | 不需要握手 | 需要握手 |
| 是否确保数据帧传输准确性 | 不确保 | 确保 |
| 是否确保数据帧传输顺序一致性 | 不确保 | 确保 |
| 速度 | 相对高速 | 相对低速 |

对于一些需要准确传输的信息,则选择TCP协议

对于一些需要高速传输和不在意准确性的数据,选择UDP协议。

显然实时视频流就是一个典型的适合UDP协议的数据。

  • 实时视频流不在意数据是否完整传输(因为传输错误的帧马上就是过去式了,接着显示新的帧)
  • 实时视频流不在意帧是否顺序一致(少数的几帧在短暂的时间戳内顺序不一致无伤大雅)
  • 但实时视频流需要帧高速

Socket-UDP协议代码详解

UDP协议发送端

是否常常遇到问题:

OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小?

简单,发送端仅需要分片发送即可,当然接收端也需要想要调整。

python 复制代码
import cv2
import socket
import time
import struct
import numpy as np
import logging

# 配置摄像头和UDP传输参数
FPS_INTERVAL = 0.1  # 每隔0.1秒计算一次帧率
UDP_IP = "127.0.0.1"  # 目标接收端IP
UDP_PORT = 12345  # 目标接收端端口
MAX_UDP_SIZE = 1024  # 每个数据包最大传输大小,调整为1024字节

# 设置日志
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger()

# 创建UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 打开摄像头
cap = cv2.VideoCapture(1)
if not cap.isOpened():
    logger.error("无法打开摄像头,请检查设备连接")
    exit(1)

cap.set(cv2.CAP_PROP_FRAME_WIDTH, 4000)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 3000)
cap.set(cv2.CAP_PROP_FPS, 60)

# 初始化一些变量
frame_count = 0
last_time = time.time()

while True:
    try:
        ret, frame = cap.read()
        if not ret:
            logger.warning("无法读取摄像头帧")
            continue  # 如果读取失败,跳过本次循环

        # 定义新的大小(宽度,高度)
        new_dims = (1280, 960)  # 新的宽度和高度
        # 使用cv2.resize()调整图像大小
        frame = cv2.resize(frame, new_dims, interpolation=cv2.INTER_LINEAR)

        # 将帧转换为JPEG格式
        ret, jpeg = cv2.imencode('.jpg', frame)
        if ret:
            # 将JPEG图像数据转为字节流
            data = jpeg.tobytes()
            data_len = len(data)

            # 发送帧数据的总长度
            try:
                sock.sendto(struct.pack("L", data_len), (UDP_IP, UDP_PORT))  # 发送数据长度
            except socket.error as e:
                logger.error(f"发送数据长度失败: {e}")
                continue  # 如果发送失败,跳过本次循环

            # 分片发送数据
            for i in range(0, data_len, MAX_UDP_SIZE):
                packet = data[i:i+MAX_UDP_SIZE]
                try:
                    sock.sendto(packet, (UDP_IP, UDP_PORT))  # 发送数据片段
                except socket.error as e:
                    logger.error(f"发送数据片段失败: {e}")
                    continue  # 如果发送失败,跳过本次循环
        
        # 计算帧率:每帧计算一次
        current_time = time.time()
        frame_time = current_time - last_time  # 计算当前帧的时间差
        fps = 1.0 / frame_time if frame_time > 0 else 0  # 帧率 = 1 / 帧间隔

        # 更新上次帧的时间
        last_time = current_time

        # 在左上角显示帧率
        cv2.putText(frame, f"client-FPS: {fps:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)

        # 显示视频流
        cv2.imshow('Camera', frame)

    except (cv2.error, socket.error) as e:
        logger.error(f"发生异常: {e}")
        # 如果发生异常,等待一段时间重试
        time.sleep(2)
        continue

    # 按'q'退出
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 释放资源
cap.release()
cv2.destroyAllWindows()

UDP协议接收端

在遇到缓存问题的时候,接收端除了增设分片接受以外,还需要进行缓冲区大小的设定,这里推荐为5MB。当然还需要try except之后清空所有的缓冲区!

python 复制代码
import cv2
import socket
import struct
import numpy as np
import time
import logging

# 配置UDP接收参数
UDP_IP = "127.0.0.1"  # 本地IP
UDP_PORT = 12345  # 端口号

# 创建UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))

# 增加接收缓冲区的大小
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1048576 * 5)  # 设置接收缓冲区大小为5MB

# 用于接收数据的缓冲区
buffer = b''

# 设置日志记录
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
logging.basicConfig(filename='udp_server.log', level=logging.DEBUG, format=LOG_FORMAT)

last_time = time.time()

def clear_socket_buffer():
    """
    清空socket的接收缓冲区,丢弃所有未处理的数据。
    """
    while True:
        # 尝试读取一部分数据
        sock.settimeout(0.1)  # 设置一个短暂的超时避免阻塞
        try:
            data = sock.recv(4096)  # 尝试读取最大4KB的数据
            if not data:
                break
        except socket.timeout:
            break  # 如果超时,退出循环

while True:
    try:
        # 接收数据长度(最多接收4字节)
        data_len, addr = sock.recvfrom(4)
        if not data_len:
            continue
        
        data_len = struct.unpack("L", data_len)[0]
        
        # 接收图像数据(分片)
        buffer = b''  # 清空之前的缓冲区
        while len(buffer) < data_len:
            packet, addr = sock.recvfrom(1450)  # 每次接收一个片段
            buffer += packet  # 将接收到的数据片段拼接到缓冲区

        # 确保接收到完整数据
        if len(buffer) == data_len:
            # 解码图像
            nparr = np.frombuffer(buffer, np.uint8)
            frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

            if frame is not None:
                # 计算并显示帧率
                fps = 1 / (time.time() - last_time) if (time.time() - last_time) > 0 else 0
                last_time = time.time()

                # 在左上角显示帧率
                cv2.putText(frame, f"Server-FPS: {fps:.2f}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
                
                # 显示接收到的图像
                cv2.imshow('Received Video Stream', frame)

            else:
                logging.warning("接收到的图像无法解码!")
                continue  # 如果解码失败,跳过本次循环

        else:
            logging.error(f"接收到的数据包大小不匹配: 期望 {data_len} 字节, 实际 {len(buffer)} 字节")
            continue  # 如果数据不完整,跳过本次循环

    except socket.timeout:
        logging.warning("接收超时,等待下一帧数据...")
        continue  # 如果超时,继续等待

    except Exception as e:
        clear_socket_buffer()
        logging.error(f"发生异常: {e}")
        time.sleep(1)  # 如果发生异常,休眠2秒后继续尝试

    # 按 'q' 键退出
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 释放资源
cv2.destroyAllWindows()
sock.close()
logging.info("服务端退出,释放资源")

其实直接拿去用即可!

相关推荐
Chef_Chen2 分钟前
从0开始机器学习--Day17--神经网络反向传播作业
python·神经网络·机器学习
FeelTouch Labs17 分钟前
Netty实现WebSocket Server是否开启压缩深度分析
网络·websocket·网络协议
千澜空22 分钟前
celery在django项目中实现并发任务和定时任务
python·django·celery·定时任务·异步任务
斯凯利.瑞恩29 分钟前
Python决策树、随机森林、朴素贝叶斯、KNN(K-最近邻居)分类分析银行拉新活动挖掘潜在贷款客户附数据代码
python·决策树·随机森林
yannan201903131 小时前
【算法】(Python)动态规划
python·算法·动态规划
蒙娜丽宁1 小时前
《Python OpenCV从菜鸟到高手》——零基础进阶,开启图像处理与计算机视觉的大门!
python·opencv·计算机视觉
光芒再现dev1 小时前
已解决,部署GPTSoVITS报错‘AsyncRequest‘ object has no attribute ‘_json_response_data‘
运维·python·gpt·语言模型·自然语言处理
好喜欢吃红柚子1 小时前
万字长文解读空间、通道注意力机制机制和超详细代码逐行分析(SE,CBAM,SGE,CA,ECA,TA)
人工智能·pytorch·python·计算机视觉·cnn
小馒头学python1 小时前
机器学习是什么?AIGC又是什么?机器学习与AIGC未来科技的双引擎
人工智能·python·机器学习
神奇夜光杯1 小时前
Python酷库之旅-第三方库Pandas(202)
开发语言·人工智能·python·excel·pandas·标准库及第三方库·学习与成长