UDP 实时视频传输(三)

UDP 实时视频传输

  • 采用组播组的形式
  • 视频编/解码核心是 OpenCV

上一章提供了几种解决办法,这一章给出分片发送的详细案例。

分片发送

将数据拆分为多个小数据包,在接收端重新组装。
发送端代码

MAX_UDP_PACKET_SIZE = 65000  # 实际可用的最大 UDP 数据包大小

def send_frame(sock, frame, address):
	frame_size = len(frame)
	for i in range(0, frame_size, MAX_UDP_PACKET_SIZE):
		sock.sendto(frame[i:i+MAX_UDP_PACKET_SIZE], address)

# 示例
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 50]
_, buffer = cv2.imencode('.jpg', frame, encode_param)
send_frame(sock, buffer.tobytes(), (MULTICAST_GROUP, MULTICAST_PORT))

接收端代码
在接收端收集所有分片后重组帧:

buffer = b""
while self.socket.hasPendingDatagrams():
	datagram, _, _ = self.socket.readDatagram(self.socket.pendingDatagramSize())
	buffer += datagram

# 解码完整帧
frame = cv2.imdecode(np.frombuffer(buffer, dtype=np.uint8), cv2.IMREAD_COLOR)

分片后发现视频仍然是存在卡顿或显示不全。
:是因为分片数据未正确重组导致的。在 UDP 中,数据包可能乱序或丢失,因此需要额外的机制来确保分片的完整性和顺序。以下是一个完整的分片发送与接收方案:

分片发送添加头信息

1. 发送端:分片并添加头信息

每个分片数据需要附加元数据,用于表示:

  • 当前分片的序号。
  • 总分片数。
  • 帧ID,用于区分不同帧。

示例实现:

import cv2
import socket
import struct

# 配置
MULTICAST_GROUP = "224.1.1.1"
MULTICAST_PORT = 12345
MAX_UDP_PACKET_SIZE = 65000

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1)

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)
cap.set(cv2.CAP_PROP_FPS, 15)

frame_id = 0  # 用于标识帧

while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    # 压缩帧
    encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 50]
    _, buffer = cv2.imencode('.jpg', frame, encode_param)
    buffer = buffer.tobytes()

    # 分片
    frame_size = len(buffer)
    num_chunks = (frame_size // MAX_UDP_PACKET_SIZE) + 1

    for i in range(num_chunks):
        # 当前分片的数据
        start = i * MAX_UDP_PACKET_SIZE
        end = start + MAX_UDP_PACKET_SIZE
        chunk_data = buffer[start:end]

        # 分片头部信息:帧 ID (4字节), 总分片数 (2字节), 当前分片序号 (2字节)
        header = struct.pack("!IHH", frame_id, num_chunks, i)

        # 发送分片
        sock.sendto(header + chunk_data, (MULTICAST_GROUP, MULTICAST_PORT))

    frame_id += 1  # 帧 ID 递增

cap.release()
sock.close()

2. 接收端:重组分片

接收端需要:

  • 根据帧 ID 将分片归类。
  • 根据总分片数和序号确认数据是否完整。
  • 将完整帧解码并显示

示例实现:

from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
from PyQt5.QtNetwork import QUdpSocket, QHostAddress
from PyQt5.QtGui import QPixmap, QImage
import numpy as np
import cv2
import struct

class MulticastReceiver(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("UDP 组播视频接收")
        self.resize(640, 480)

        self.MULTICAST_GROUP = "224.1.1.1"
        self.MULTICAST_PORT = 12345

        # 创建 UDP 套接字
        self.socket = QUdpSocket(self)
        self.socket.bind(QHostAddress.AnyIPv4, self.MULTICAST_PORT)
        self.socket.joinMulticastGroup(QHostAddress(self.MULTICAST_GROUP))
        self.socket.readyRead.connect(self.receive_data)

        # 创建界面
        self.label = QLabel(self)
        self.label.setScaledContents(True)
        layout = QVBoxLayout(self)
        layout.addWidget(self.label)

        # 帧缓冲区
        self.frames = {}  # 存储当前正在接收的帧:{frame_id: {chunk_index: data}}
        self.frame_sizes = {}  # 存储每帧的分片总数:{frame_id: total_chunks}

    def receive_data(self):
        while self.socket.hasPendingDatagrams():
            datagram, _, _ = self.socket.readDatagram(self.socket.pendingDatagramSize())
            
            # 解析分片头部
            header = datagram[:8]
            frame_id, total_chunks, chunk_index = struct.unpack("!IHH", header)

            # 分片数据
            chunk_data = datagram[8:]

            # 初始化缓冲区
            if frame_id not in self.frames:
                self.frames[frame_id] = {}
                self.frame_sizes[frame_id] = total_chunks

            # 存储分片
            self.frames[frame_id][chunk_index] = chunk_data

            # 检查是否完整
            if len(self.frames[frame_id]) == self.frame_sizes[frame_id]:
                # 重组帧
                complete_frame = b"".join(self.frames[frame_id][i] for i in range(self.frame_sizes[frame_id]))
                
                # 解码帧
                frame = cv2.imdecode(np.frombuffer(complete_frame, dtype=np.uint8), cv2.IMREAD_COLOR)
                if frame is not None:
                    height, width, channel = frame.shape
                    bytes_per_line = channel * width
                    qimg = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped()
                    self.label.setPixmap(QPixmap.fromImage(qimg))

                # 移除已处理帧
                del self.frames[frame_id]
                del self.frame_sizes[frame_id]

if __name__ == "__main__":
    app = QApplication([])
    receiver = MulticastReceiver()
    receiver.show()
    app.exec()

关键点说明

  1. 分片头部设计:
  • 帧 ID(!I,4 字节):用于区分不同的帧。
  • 总分片数(!H,2 字节):确保可以接收完整帧。
  • 当前分片序号(!H,2 字节):用于重组。
  1. 接收缓冲区:
  • self.frames:存储接收到的分片。
  • self.frame_sizes:存储每帧的总分片数,用于判断完整性。
  1. 丢包处理:
  • 如果某些分片未到达,可设置超时机制,丢弃该帧。
  • 改用更可靠的传输协议(如 RTP 或 TCP)时可避免此问题。
  1. 性能优化:
  • 降低分辨率、帧率和压缩质量。
  • 增加接收缓冲区大小,确保网络不会因拥塞丢包。

分片完整性测试方法

发送端:确保分片逻辑正确

  • 确认发送的 frame_idtotal_chunkschunk_index 是正确的。
  • 在发送时打印调试信息:
print(f"Sending frame {frame_id}, chunk {i}/{num_chunks}")

接收端:验证接收到的分片

  • 打印接收端的分片信息:
print(f"Received frame {frame_id}, chunk {chunk_index}/{total_chunks}")
  • 检查是否有分片缺失:
    • 对比接收到的 chunk_indextotal_chunks
    • 如果有分片丢失,记录丢失的索引用于进一步调试。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值