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()
关键点说明
- 分片头部设计:
- 帧 ID(
!I
,4 字节):用于区分不同的帧。 - 总分片数(
!H
,2 字节):确保可以接收完整帧。 - 当前分片序号(
!H
,2 字节):用于重组。
- 接收缓冲区:
self.frames
:存储接收到的分片。self.frame_sizes
:存储每帧的总分片数,用于判断完整性。
- 丢包处理:
- 如果某些分片未到达,可设置超时机制,丢弃该帧。
- 改用更可靠的传输协议(如 RTP 或 TCP)时可避免此问题。
- 性能优化:
- 降低分辨率、帧率和压缩质量。
- 增加接收缓冲区大小,确保网络不会因拥塞丢包。
分片完整性测试方法
发送端:确保分片逻辑正确
- 确认发送的
frame_id
、total_chunks
和chunk_index
是正确的。 - 在发送时打印调试信息:
print(f"Sending frame {frame_id}, chunk {i}/{num_chunks}")
接收端:验证接收到的分片
- 打印接收端的分片信息:
print(f"Received frame {frame_id}, chunk {chunk_index}/{total_chunks}")
- 检查是否有分片缺失:
- 对比接收到的
chunk_index
和total_chunks
。 - 如果有分片丢失,记录丢失的索引用于进一步调试。
- 对比接收到的