基于WebRTC实现多人视频通话的多RTCPeerConnection管理问题
WebRTC多人通话多PeerConnection实现方案
核心实现思路
- 每个远端参会用户对应独立的
RTCPeerConnection实例,不能复用同一个实例,因为单个RTCPeerConnection只能维护一对一的媒体传输通道 - 用字典结构存储所有PeerConnection实例,key用远端用户的唯一ID(可以用Firebase Auth的uid或者房间内分配的唯一用户标识),value就是对应的
RTCPeerConnection对象 - 本地媒体流只需要创建一次,主动添加到所有新建的PeerConnection实例中,不需要重复采集本地音视频
- 每个PeerConnection对应独立的远端轨道渲染视图,收到远端流后绑定到对应UI控件上
关键代码实现
1. 存储结构改造
替换原有单PeerConnection变量,改用字典存储多实例:
// key为远端用户唯一ID,value为对应的PeerConnection实例 var peerConnections: [String: RTCPeerConnection] = [:] // 存储每个远端用户对应的远程媒体轨道,方便渲染/销毁 var remoteStreams: [String: RTCMediaStream] = [:] // 提前创建好的本地媒体流,只初始化一次 var localStream: RTCMediaStream!
2. 新用户加入时的PeerConnection处理
收到新远端用户的Offer信令时,优先判断是否存在对应用户的PeerConnection,不存在则新建:
// 收到远端用户offer信令时调用,remoteUid为远端用户唯一标识 func handleRemoteOffer(offer: RTCSessionDescription, remoteUid: String) { // 如果不存在对应用户的PeerConnection则新建 if peerConnections[remoteUid] == nil { let config = RTCConfiguration() config.iceServers = [RTCIceServer(urlStrings: ["stun:stun.l.google.com:19302"])] config.sdpSemantics = .unifiedPlan // 初始化PeerConnection guard let newPeer = factory.peerConnection(with: config, constraints: defaultConstraints, delegate: self) else { LOG("创建PeerConnection失败,用户ID:\(remoteUid)") return } // 将本地流添加到新的PeerConnection中 newPeer.add(localStream.videoTracks[0], streamIds: [localStream.streamId]) newPeer.add(localStream.audioTracks[0], streamIds: [localStream.streamId]) peerConnections[remoteUid] = newPeer } // 获取对应用户的PeerConnection设置远端描述 guard let peer = peerConnections[remoteUid] else { return } peer.setRemoteDescription(offer) { error in if let error = error { LOG("setRemoteDescription失败,用户ID:\(remoteUid) 错误:\(error.localizedDescription)") return } LOG("setRemoteDescription成功,用户ID:\(remoteUid)") // 生成Answer回传给远端用户 self.makeAnswer(for: remoteUid) } }
3. Answer生成逻辑改造
指定对应远端用户的PeerConnection生成应答:
func makeAnswer(for remoteUid: String) { guard let peer = peerConnections[remoteUid] else { return } let answerConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: ["OfferToReceiveAudio": "true", "OfferToReceiveVideo": "true"]) peer.answer(for: answerConstraints) { sdp, error in if let error = error { LOG("生成Answer失败,用户ID:\(remoteUid) 错误:\(error.localizedDescription)") return } guard let sdp = sdp else { return } peer.setLocalDescription(sdp) { error in if let error = error { LOG("setLocalDescription失败,用户ID:\(remoteUid) 错误:\(error.localizedDescription)") return } // 把Answer通过Firebase信令发送给对应的远端用户 self.sendAnswer(sdp: sdp, to: remoteUid) } } }
4. 远端流回调处理
区分不同用户的PeerConnection回调,绑定对应渲染视图:
func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) { // 找到对应当前PeerConnection的用户ID guard let remoteUid = peerConnections.first(where: { $0.value == peerConnection })?.key else { return } remoteStreams[remoteUid] = stream // 主线程更新UI,将远端流绑定到对应的渲染视图上 DispatchQueue.main.async { let renderView = self.getRenderView(for: remoteUid) // 自行实现获取对应远端用户的渲染视图方法 stream.videoTracks.first?.add(renderView) } }
5. 用户退出销毁逻辑
用户离开房间时及时销毁对应资源避免内存泄漏:
func handleUserLeave(remoteUid: String) { // 销毁对应PeerConnection peerConnections[remoteUid]?.close() peerConnections.removeValue(forKey: remoteUid) // 移除远端流和对应的渲染视图 remoteStreams.removeValue(forKey: remoteUid) removeRenderView(for: remoteUid) // 自行实现移除对应渲染视图的方法 }
注意事项
- 信令收发必须携带发送方的用户唯一ID,才能正确匹配到对应的PeerConnection实例
- 采用Unified Plan的SDP语义,兼容性更好,适配多轨道场景
- 4人通话场景下本地会同时维护3个PeerConnection实例,设备性能消耗会比1对1场景高,建议做低性能设备的适配降级
内容的提问来源于stack exchange,提问作者Jatin Garg
相关产品推荐
相关产品推荐

