14、PeerConnection对象:生命周期管理、状态机设计、事件回调机制

聊到WebRTC,PeerConnection绝对是绕不开的核心。我个人习惯把它比作「视频通话的发动机」——没有它,音视频数据根本跑不起来。今天我们就来拆解这个对象,看看它到底怎么生、怎么活、怎么死。

14.1 生命周期:从出生到销毁

PeerConnection的生命周期,说白了就三个阶段:创建、使用、销毁。但每个阶段都有不少坑,我踩过,所以得跟你好好说说。

14.1.1 创建阶段

创建PeerConnection,你需要一个配置对象。这个配置决定了你的连接走什么网络、用什么协议。

const config = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: 'turn:turn.example.com:3478',
      username: 'user',
      credential: 'pass'
    }
  ],
  iceTransportPolicy: 'all', // 或 'relay'
  bundlePolicy: 'max-bundle',
  rtcpMuxPolicy: 'require'
};

const pc = new RTCPeerConnection(config);

嗯,这里要注意:配置一旦传入,就不能改了。我曾经有个项目,想在中途切换STUN服务器,结果发现根本不行——只能重建连接。所以,创建前一定要想清楚你的网络场景。

14.1.2 使用阶段

创建完之后,PeerConnection就进入「活跃期」。这个阶段主要做三件事:

  • 添加媒体流:通过addTrack()addTransceiver()把本地音视频塞进去
  • 交换SDP:创建Offer/Answer,完成信令协商
  • 收集ICE候选:找到双方都能通的网络路径

你想想看,这三件事其实是串行的。必须先加流,再协商,最后才能通。顺序错了,连接就起不来。

14.1.3 销毁阶段

用完了一定要关掉。这不是废话,我见过太多人忘了关连接,导致内存泄漏。

// 正确的关闭方式
pc.close();
pc = null;

⚠️ 注意:close()之后,这个PeerConnection就不能再用了。想重新连接?必须new一个新的。

14.2 状态机设计:三个核心状态机

PeerConnection内部其实跑着三个独立的状态机。理解它们,你就能预判连接的行为。

14.2.1 ICE连接状态

这个状态机描述的是网络连通性。我习惯把它叫做「网络健康检查器」。

状态 含义 常见触发
new 刚创建,还没开始收集 new RTCPeerConnection()
checking 正在收集候选、做连通性检查 setLocalDescription() 之后
connected 至少一条路径通了 STUN/TURN 打洞成功
completed 所有候选都检查完了 连通性检查结束
failed 所有路径都试过了,都不通 网络环境太差,或TURN服务器挂了
disconnected 曾经通,但现在断了 网络波动、超时
closed 连接被主动关闭 pc.close()

💡 我个人习惯监听oniceconnectionstatechange事件,一旦状态变成failed,就触发重连逻辑。别等到用户抱怨「画面卡住了」才处理。

14.2.2 信令状态

这个状态机管的是SDP交换的进度。说白了就是「你俩商量好了没」。

状态 含义
stable 没有进行中的协商
have-local-offer 本地创建了Offer,等远端应答
have-remote-offer 收到了远端的Offer,等本地应答
have-local-pranswer 本地创建了临时应答
have-remote-pranswer 收到了远端的临时应答
closed 连接关闭

为什么会需要临时应答?嗯,这跟音视频的「提前预览」有关。有些场景下,你还没决定好最终参数,但想先让对方看到画面——这时候就用pranswer。

14.2.3 连接状态

这个状态是ICE状态和信令状态的「综合体检报告」。它告诉你整个连接是否健康。

pc.onconnectionstatechange = (event) => {
  console.log('连接状态变了:', pc.connectionState);
  // 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed'
};

我个人习惯只看这个状态。因为它把ICE和信令的信息合并了,更直观。

14.3 事件回调机制:别错过关键信号

PeerConnection是事件驱动的。你不监听事件,就等于闭着眼睛开车。

14.3.1 必须监听的事件

  • ontrack:远端媒体流到了。这是你渲染对方画面的入口。
  • onicecandidate:本地发现了新的ICE候选。你得通过信令通道发给对方。
  • oniceconnectionstatechange:网络状态变了。重连、降级都靠它。
  • ondatachannel:有人要建数据通道。用于文本聊天或文件传输。
pc.ontrack = (event) => {
  const video = document.createElement('video');
  video.srcObject = event.streams[0];
  video.autoplay = true;
  document.body.appendChild(video);
};

pc.onicecandidate = (event) => {
  if (event.candidate) {
    // 通过信令通道发送给远端
    signalingChannel.send({
      type: 'candidate',
      candidate: event.candidate
    });
  }
};

🔑 核心原则:事件处理函数里不要做耗时操作。我曾经在ontrack里做视频转码,结果UI线程卡死,画面一帧都出不来。正确的做法是把数据扔给Worker处理。

14.3.2 容易忽略的事件

  • onnegotiationneeded:需要重新协商了。比如你中途加了新流,或者改了编码参数。
  • onicecandidateerror:ICE收集出错了。比如STUN服务器超时,或者TURN认证失败。

我记得有一次线上事故,用户反馈「连不上」。查了半天,发现是TURN服务器的密码过期了。但代码里只监听了onicecandidate,没监听onicecandidateerror,所以错误被静默吞掉了。从那以后,我每个项目都会加上这个监听。

14.4 状态机流转图

下面这张图展示了三个状态机之间的关系。我画的时候特意把「用户操作」和「网络事件」分开,方便你理解触发条件。

PeerConnection 状态机流转图 用户操作 网络事件 核心状态机 new checking / connecting connected failed / disconnected closed new RTCPeerConnection() setLocalDescription() ICE 候选交换完成 pc.close() STUN/TURN 超时 网络断开 所有候选失败

14.5 避坑指南

最后,分享几个我踩过的坑,希望能帮你省点时间。

  1. 不要重复创建PeerConnection:每次new都是一次资源分配。如果频繁重建,内存会暴涨。我建议用一个连接池来管理。
  2. 状态变化是异步的:别指望setLocalDescription()之后立刻就能拿到ICE候选。得等事件回调。
  3. close()之后别再用:这个我前面提过,但还是要强调。closed状态的PeerConnection,所有方法都会抛异常。
  4. 事件监听要早注册:最好在new完之后就注册所有监听。否则可能漏掉早期的事件,比如第一个ICE候选。

💡 我个人习惯封装一个PeerConnectionManager类,把创建、监听、销毁都包进去。这样业务代码只需要调用connect()和disconnect(),不用关心内部状态机。

PeerConnection的状态机设计其实很优雅。它把复杂的网络协商过程抽象成了几个清晰的状态,让开发者能专注于业务逻辑。理解了这些,你写出来的WebRTC应用就会稳定得多。


公众号:蓝海资料掘金营,微信deep3321