7、RTCPeerConnection:创建连接、添加轨道、创建Offer/Answer、ICE候选处理
好,咱们今天来聊聊 WebRTC 里最核心的一个对象——RTCPeerConnection。说实话,我刚开始接触 WebRTC 的时候,觉得这玩意儿就是个黑盒。你给它一些配置,它就能把音视频传过去。但实际用起来,里面的门道可不少。
这一章,我会带你手把手把整个连接流程走一遍。从创建连接,到添加轨道,再到交换 SDP 和 ICE 候选。嗯,咱们一步步来。
7.1 创建 RTCPeerConnection
创建连接的第一步,就是实例化一个 RTCPeerConnection 对象。你想想看,它就像是你和远端之间的一条虚拟网线。没有它,啥都传不了。
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:your-turn-server.com:3478',
username: 'user',
credential: 'pass'
}
]
};
const pc = new RTCPeerConnection(config);
这里有个坑,我一开始就踩过。ICE 服务器配置里,STUN 服务器是必须的,但 TURN 服务器不是。不过,如果你的用户可能处在对称 NAT 后面,那 TURN 就是救命稻草。我个人习惯是,至少配一个 Google 的公共 STUN 服务器,再配一个自己的 TURN 服务器兜底。
7.2 添加轨道
连接建好了,接下来要往里塞数据。说白了,就是把你本地的音视频轨道加进去。
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
这里有个细节,addTrack 的第二个参数是流对象。为什么需要传流?因为远端收到轨道后,需要知道这些轨道属于哪个流,才能正确渲染。我在项目中遇到过,有人把同一个流里的视频和音频轨道分开添加,结果远端播放器只显示了画面,没有声音。排查了半天,才发现是流对象没传对。
另外,如果你要做屏幕共享,那就用 getDisplayMedia 替代 getUserMedia。代码几乎一样,只是用户会看到一个选择窗口。
7.3 创建 Offer 和 Answer
轨道加好了,接下来就是发起方创建 Offer,接收方回复 Answer。这个过程,说白了就是双方交换各自的媒体能力。
// 发起方
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 通过信令服务器发送 offer 给远端
// 接收方
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// 通过信令服务器发送 answer 给发起方
// 发起方收到 answer
await pc.setRemoteDescription(answer);
嗯,这里要注意顺序。必须先 setLocalDescription,再发送。为什么?因为 setLocalDescription 会触发 ICE 候选的收集。如果你先发送了 Offer,再设置本地描述,ICE 候选可能还没生成,远端就收不到候选了。
createOffer 之后直接发送 Offer。一定要等 setLocalDescription 完成。我曾经因为这个问题,调试了整整一个下午。
7.4 ICE 候选处理
最后一步,也是最容易出问题的一步——ICE 候选交换。ICE 候选,说白了就是告诉对方,你有哪些可能的网络路径可以连接。
pc.onicecandidate = (event) => {
if (event.candidate) {
// 通过信令服务器发送 candidate 给远端
signalingServer.send({
type: 'candidate',
candidate: event.candidate
});
}
};
// 远端收到 candidate
pc.addIceCandidate(new RTCIceCandidate(candidate));
这里有个常见的误解:是不是必须把所有候选都收集完,才能开始连接?其实不是。ICE 协议是渐进式的,每收集到一个候选,就会尝试连接。所以,你收到一个候选,就立即添加一个,不用等全部收齐。
我记得有一次,用户反馈连接特别慢。排查后发现,是信令服务器做了缓存,等所有候选都收齐了才一次性发送。改成立即转发后,连接速度明显提升。
7.5 完整流程示意图
下面这张图,把整个流程串起来了。你可以对照着看,每一步在做什么。
7.6 状态监听与调试
连接建立后,别忘了监听状态变化。这能帮你快速定位问题。
pc.oniceconnectionstatechange = () => {
console.log('ICE 状态:', pc.iceConnectionState);
// 可能的值: new, checking, connected, completed, failed, disconnected, closed
};
pc.onconnectionstatechange = () => {
console.log('连接状态:', pc.connectionState);
// 可能的值: new, connecting, connected, disconnected, failed, closed
};
我个人习惯,在 failed 或 disconnected 状态时,尝试重新创建连接。不过要注意,不要频繁重试,否则会浪费带宽。一般等 2-3 秒再重试比较合理。
- 创建连接时,STUN 服务器必配,TURN 服务器按需配
- 添加轨道时,确保流对象正确传递
- Offer/Answer 交换,必须先 setLocalDescription 再发送
- ICE 候选要立即转发,不要缓存
- 监听状态变化,及时处理异常
好了,这一章的内容就到这里。RTCPeerConnection 是整个 WebRTC 的基石,理解了它,后面的路就好走了。如果你在实际项目中遇到问题,不妨回头看看这几个步骤,八成能找到原因。