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;
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
});
}
};
14.3.2 容易忽略的事件
- onnegotiationneeded:需要重新协商了。比如你中途加了新流,或者改了编码参数。
- onicecandidateerror:ICE收集出错了。比如STUN服务器超时,或者TURN认证失败。
我记得有一次线上事故,用户反馈「连不上」。查了半天,发现是TURN服务器的密码过期了。但代码里只监听了onicecandidate,没监听onicecandidateerror,所以错误被静默吞掉了。从那以后,我每个项目都会加上这个监听。
14.4 状态机流转图
下面这张图展示了三个状态机之间的关系。我画的时候特意把「用户操作」和「网络事件」分开,方便你理解触发条件。
14.5 避坑指南
最后,分享几个我踩过的坑,希望能帮你省点时间。
- 不要重复创建PeerConnection:每次new都是一次资源分配。如果频繁重建,内存会暴涨。我建议用一个连接池来管理。
- 状态变化是异步的:别指望setLocalDescription()之后立刻就能拿到ICE候选。得等事件回调。
- close()之后别再用:这个我前面提过,但还是要强调。closed状态的PeerConnection,所有方法都会抛异常。
- 事件监听要早注册:最好在new完之后就注册所有监听。否则可能漏掉早期的事件,比如第一个ICE候选。
PeerConnection的状态机设计其实很优雅。它把复杂的网络协商过程抽象成了几个清晰的状态,让开发者能专注于业务逻辑。理解了这些,你写出来的WebRTC应用就会稳定得多。