如何借助Multipeer Connectivity实现多设备同时传输摄像头帧
解决方案:多节点Multipeer Connectivity摄像头帧传输问题
针对多节点连接主机时,主机停止接收部分节点帧的问题,从以下核心方向排查修复:
1. 修复全局Semaphore的阻塞问题
你的代码中使用了全局streamingSemaphore,若初始值为1,会导致同一时间仅能处理一个节点的帧,多节点场景下其他帧会被阻塞在wait()调用处,最终表现为停止接收。
修改方案:
移除全局Semaphore,每个节点的帧处理由各自独立的receivingQueue负责,无需全局并发控制:
private func setPreviewLayerContents(_ data: Data, from peer: MCPeerID) { guard let view = getPeerView(using: peer) else { return } view.receivingQueue.async { [unowned self] in guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else { return } DispatchQueue.main.async { view.setPreviewImage(image) } } }
若必须限制并发数,应为每个节点单独创建Semaphore,而非全局共享。
2. 优化视频帧的传输方式
避免JSON编码二进制图片数据
JSON编码大体积图片Data会额外增加数据量、降低传输效率,改用直接传输二进制数据+自定义格式携带timestamp:
节点发送端修改:
static func sendVideoPreviewFrameCommand( using session: MCSession, to hostPeer: MCPeerID, imageData: Data, timestamp: String) throws { guard let timestampData = timestamp.data(using: .utf8) else { return } var combinedData = Data() // 先写入timestamp的长度,方便主机解析 let timestampLength = UInt32(timestampData.count).bigEndian combinedData.append(Data(bytes: ×tampLength, count: MemoryLayout<UInt32>.size)) combinedData.append(timestampData) combinedData.append(imageData) // 视频帧为实时数据,改用.unreliable模式提升传输效率(避免重传阻塞) try session.send(combinedData, toPeers: [hostPeer], with: .unreliable) }
主机接收端解析:
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { guard data.count >= MemoryLayout<UInt32>.size else { return } // 解析timestamp长度和内容 let timestampLengthData = data.prefix(MemoryLayout<UInt32>.size) var timestampLength: UInt32 = 0 timestampLengthData.copyBytes(to: ×tampLength, count: MemoryLayout<UInt32>.size) timestampLength = timestampLength.bigEndian let timestampData = data.dropFirst(MemoryLayout<UInt32>.size).prefix(Int(timestampLength)) let imageData = data.dropFirst(MemoryLayout<UInt32>.size + Int(timestampLength)) guard let timestamp = String(data: Data(timestampData), encoding: .utf8) else { return } setPreviewLayerContents(imageData, from: peerID) }
压缩图片数据
对摄像头帧进行压缩,减少单帧数据体积,避免传输带宽被占满:
// 节点端将CGImage转为压缩后的JPEG Data guard let uiImage = UIImage(cgImage: yourCGImage) else { return } if let compressedData = uiImage.jpegData(compressionQuality: 0.5) { try? sendVideoPreviewFrameCommand(using: session, to: hostPeer, imageData: compressedData, timestamp: timestamp) }
3. 确保发送目标PeerID正确
节点每次发送时,确认toPeers数组仅包含当前连接的主机PeerID,避免发送至其他节点或旧的无效PeerID。建议在节点端维护一个实时更新的hostPeer变量,确保发送目标准确。
4. 配置独立的接收队列
确保每个节点对应的receivingQueue是独立的并行队列,避免不同节点的帧处理互相阻塞:
// 初始化PeerView时创建独立队列 receivingQueue = DispatchQueue(label: "com.yourapp.receive.\(peerID.displayName)", attributes: .concurrent)
5. 排查会话连接状态
在主机和节点端分别监听连接状态变化,确保所有节点保持connected状态:
// 主机端监听连接状态 func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { switch state { case .connected: print("节点 \(peerID.displayName) 已连接") case .connecting: print("节点 \(peerID.displayName) 连接中") case .notConnected: print("节点 \(peerID.displayName) 已断开") @unknown default: break } } // 节点端监听发送失败 func session(_ session: MCSession, didFailToSendData data: Data, toPeer peerID: MCPeerID, error: Error?) { print("发送帧至 \(peerID.displayName) 失败: \(error?.localizedDescription ?? "未知错误")") }
内容的提问来源于stack exchange,提问作者cleanrun
相关产品推荐
相关产品推荐

