1548 字
8 分钟
WebRTC 接收端创建 RTCPeerConnection 的两种方式与最佳实践

在搭建 WebRTC 应用时,开发者经常会遇到一个看似简单、实则影响架构的问题:接收端(callee)应该在收到对方的 SDP offer 之后再创建 RTCPeerConnection,还是在信令通道建立时就提前创建好?

这两种做法都能跑通,但在资源占用、连接速度、状态管理等方面差异不小。本文先对比两种方式的核心差异,再补充几个实际开发中容易踩坑的细节,最后给出场景化的选型建议。


方式 1:收到 SDP 后再创建 RTCPeerConnection(按需创建)#

流程#

  1. 接收端通过信令通道收到 offer
  2. 此时才 new RTCPeerConnection(config)
  3. 调用 setRemoteDescription(offer)
  4. 创建 answersetLocalDescription(answer),并通过信令通道发回。

示例代码#

let peerConnection;
const pendingCandidates = []; // 用于 buffer 提前到达的 candidate
async function handleOffer(offer) {
peerConnection = new RTCPeerConnection(config);
peerConnection.onicecandidate = event => {
if (event.candidate) {
sendToSignalingServer({ type: "candidate", candidate: event.candidate });
}
};
peerConnection.ontrack = event => {
// 把远端的媒体流挂到 video 元素上
remoteVideo.srcObject = event.streams[0];
};
peerConnection.onconnectionstatechange = () => {
if (["failed", "disconnected", "closed"].includes(peerConnection.connectionState)) {
cleanup();
}
};
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
// 处理在 setRemoteDescription 之前到达的 ICE candidate
for (const c of pendingCandidates) {
await peerConnection.addIceCandidate(c);
}
pendingCandidates.length = 0;
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
sendToSignalingServer({ type: "answer", answer: peerConnection.localDescription });
}
async function handleRemoteCandidate(candidate) {
if (peerConnection && peerConnection.remoteDescription) {
await peerConnection.addIceCandidate(candidate);
} else {
pendingCandidates.push(candidate);
}
}

优点#

  • 资源高效:没有通话时不占用 ICE agent、STUN/TURN 连接等资源。
  • 适应性强:可以根据 offer 内容动态决定 iceServersbundlePolicy 等配置(例如不同的房间走不同的 TURN)。
  • 清理逻辑简单:没有”空连接”需要管理。

缺点#

  • 首次建连略慢:从收到 offer 到开始 ICE 收集存在一小段串行延迟。
  • 需要 buffer ICE candidate:信令服务器可能先转发对端的 candidate 再转发 offer,必须妥善处理时序。

方式 2:提前创建 RTCPeerConnection,等待 SDP#

流程#

  1. 信令通道(WebSocket / SSE)一建立,立刻 new RTCPeerConnection(config)
  2. 收到 offer 后直接 setRemoteDescriptioncreateAnswersetLocalDescription

示例代码#

const peerConnection = new RTCPeerConnection(config);
peerConnection.onicecandidate = event => {
if (event.candidate) {
sendToSignalingServer({ type: "candidate", candidate: event.candidate });
}
};
peerConnection.ontrack = event => {
remoteVideo.srcObject = event.streams[0];
};
async function handleOffer(offer) {
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
sendToSignalingServer({ type: "answer", answer: peerConnection.localDescription });
}

优点#

  • 建连更快:ICE 收集可以与信令并行(虽然在没有 remote description 之前作用有限)。
  • 适合长连接复用:在房间型应用、SFU 客户端、远程协作工具中,连接生命周期较长,预先创建更自然。

缺点#

  • 资源闲置:即使没人来连,RTCPeerConnection 也会持有内部状态。如果配置了 TURN,TURN 分配甚至会在 setLocalDescription 后产生流量。
  • 状态管理更复杂:需要超时清理、失败重建、避免在错误状态下复用。
  • 配置不够灵活:连接参数必须在 offer 到达前就确定。

容易被忽略的几个技术细节#

不管选哪种方式,下面这些点都值得在代码里显式处理。

1. ICE candidate 的时序问题#

WebRTC 的 trickle ICE 机制下,candidate 可以在 SDP 之后陆续到达,但信令服务器并不保证 offer 一定先于 candidate 送达接收端。如果接收端在没有 remote description 的情况下调用 addIceCandidate,浏览器会抛错。

解决办法就是上面示例里的 buffer 模式:先攒着,等 setRemoteDescription 完成再统一 addIceCandidate

2. ontrackaddTrack / addTransceiver#

原始笔记的代码只展示了协商,没有处理媒体本身。实际开发中:

  • 接收端需要 peerConnection.ontrack = ... 来拿到远端流。
  • 如果接收端也要发送本地流(双向通话),需要 peerConnection.addTrack(track, stream) 或显式 addTransceiver,并且要在 createAnswer 之前完成。

3. 连接状态监听与清理#

peerConnection.onconnectionstatechange = () => {
console.log("connection state:", peerConnection.connectionState);
};
peerConnection.oniceconnectionstatechange = () => {
console.log("ice state:", peerConnection.iceConnectionState);
};

当状态变为 faileddisconnected 时,根据业务决定是触发 ICE restart 还是直接 peerConnection.close()。否则连接会一直挂着,浏览器内存和网络资源得不到释放。

4. Perfect Negotiation 模式#

如果你的场景中双方都可能主动发起 offer(例如随时切换摄像头、添加 screen share 触发 renegotiation),强烈建议采用 W3C 推荐的 Perfect Negotiation 模式:

  • 给一端标记 polite,另一端标记 impolite
  • 通过 signalingStatemakingOffer 标志检测 glare(双方同时发 offer)。
  • polite 端在冲突时回滚自己的 offer,impolite 端忽略对方的 offer。

这个模式天然兼容方式 2(预先创建),并且能优雅处理任意时刻的 renegotiation。

5. TURN 凭证与配置时效性#

如果你用的是带时效性的 TURN 凭证(例如 Twilio、Coturn 的 REST API 模式),方式 2 提前创建连接的话,凭证在等待 offer 到达时可能已经过期。这种场景下方式 1 更安全,或者在方式 2 中实现凭证刷新逻辑。


怎么选?场景化建议#

场景推荐方式理由
1 对 1 视频通话(短时、按需)方式 1资源按需占用,配置灵活
房间型应用 / 多人会议客户端方式 2长连接复用,预先准备 ICE
SFU / MCU 客户端方式 2连接长期存在,需要快速接入新流
云游戏 / 远程桌面方式 2对建连延迟敏感,连接长期持有
客服/在线咨询入口方式 1大部分访客不会真的发起通话,避免无谓资源消耗
移动端弱网/省电场景方式 1减少后台 ICE/TURN 流量与电量消耗

总结#

  • **方式 1(按需创建)**是大多数 1 对 1 通话的合理默认值,资源利用率高、配置灵活,缺点是首次建连略慢、需要 buffer ICE candidate。
  • **方式 2(提前创建)**适合长连接、对建连延迟敏感、或需要随时 renegotiation 的场景,但要付出额外的状态管理成本。
  • 不管选哪种,都要把 ICE candidate buffer、ontrack 处理、连接状态清理 这三件事做对。
  • 如果应用涉及双向 renegotiation,直接套用 Perfect Negotiation 模式可以省掉很多边界情况的代码。

架构选型没有银弹,关键是把每种方式背后的资源/延迟权衡想清楚,再结合业务的连接生命周期做决定。

WebRTC 接收端创建 RTCPeerConnection 的两种方式与最佳实践
https://blog.cuixu.cn/posts/frontend/webrtc-peerconnection-creation-timing/
作者
崔旭
发布于
2022-09-20
许可协议
CC BY-NC-SA 4.0