You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何借助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: &timestampLength, 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: &timestampLength, 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 17:07:25