4、RTCPeerConnection详解:核心对象、ICE框架与Candidate、Offer/Answer模型
各位同学,今天我们来啃一块硬骨头——RTCPeerConnection。说实话,我刚接触WebRTC的时候,最头疼的就是这个对象。它太庞大了,像个瑞士军刀,什么功能都往里塞。但你别怕,我们把它拆开来看,其实就三块:连接管理、网络穿透、媒体协商。
我个人习惯把RTCPeerConnection比作一个「电话交换机」。你拨号、等待对方接听、建立通话,每一步都有对应的API。咱们今天就把这三块讲透。
4.1 RTCPeerConnection对象:你的连接管家
先看一个最简单的创建方式:
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
});
嗯,这里要注意。这个构造函数的参数叫RTCConfiguration,最常用的就是iceServers。我在项目中遇到过,有人只配了一个STUN服务器,结果在复杂网络环境下死活连不上。后来加了TURN服务器才搞定。
RTCPeerConnection身上挂着一堆事件,我列几个你一定会用到的:
| 事件名 | 触发时机 | 我常用的场景 |
|---|---|---|
icecandidate |
ICE代理发现新候选地址时 | 收集Candidate并通过信令发送给对方 |
track |
远端添加了媒体轨道时 | 把收到的视频流挂到video标签上 |
connectionstatechange |
连接状态变化时 | 显示"已连接/已断开"状态提示 |
datachannel |
收到数据通道请求时 | 建立非媒体数据传输 |
你想想看,这些事件其实就是整个连接的生命周期。从创建到销毁,每一步都有回调让你干预。
4.2 ICE框架与Candidate:网络穿透的幕后英雄
为什么需要ICE?说白了,你的电脑在局域网里,对方的电脑可能在另一个局域网里。中间隔着NAT、防火墙,怎么找到对方?ICE就是干这个的。
ICE的全称是Interactive Connectivity Establishment,交互式连接建立。它背后有三个协议:
- STUN:帮你发现自己的公网IP和端口。我习惯叫它「照镜子」。
- TURN:如果P2P打不通,就通过服务器中转。这是保底方案。
- Candidate:每一个可能的连接地址,就是一个Candidate。
一个Candidate长这样:
candidate:842163049 1 udp 16777215 192.168.1.100 54321 typ host
我来拆解一下:
842163049:Foundation,用来去重1:组件ID,1表示RTP,2表示RTCPudp:传输协议16777215:优先级,越高越优先192.168.1.100:IP地址54321:端口typ host:类型,host是本地地址,srflx是反射地址,relay是中继地址
我曾经在一个项目中,发现Candidate一直收集不全。排查了半天,原来是STUN服务器地址写错了。嗯,这种低级错误我也犯过。
核心要点:ICE框架会收集三种Candidate:host(本地)、srflx(公网映射)、relay(中继)。优先级从高到低排列,优先尝试P2P直连。
ICE的状态机也很重要。它从new开始,经过checking、connected,最终到completed或failed。我建议你在开发时,把iceConnectionState的变化打印出来,调试起来会轻松很多。
4.3 Offer/Answer模型与SDP交换
这是WebRTC最核心的机制。说白了,就是双方坐下来商量:「我用什么编码器?你支持什么分辨率?咱们怎么传数据?」
流程很简单:
- 发起方创建Offer(SDP)
- 发起方设置本地描述
- 发起方通过信令把Offer发给对方
- 接收方设置远端描述
- 接收方创建Answer(SDP)
- 接收方设置本地描述
- 接收方把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 给对方
// 发起方
await pc.setRemoteDescription(answer);
你可能会问:SDP里面到底写了什么?我截取一段给你看:
v=0
o=- 123456 2 IN IP4 0.0.0.0
s=-
t=0 0
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104
a=rtpmap:111 opus/48000/2
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
这里面包含了媒体类型(audio/video)、编码器(opus/ISAC)、传输协议等信息。你不需要手写SDP,浏览器会帮你生成。但你要能看懂,因为调试时经常要检查SDP内容。
小技巧:在Chrome开发者工具里,输入pc.localDescription.sdp就能看到当前生成的SDP。我调试时经常这么干。
4.4 完整流程的SVG图解
下面这张图,把整个流程串起来了。我建议你多看几遍,把每个步骤对应到代码上。
关键点:Offer/Answer交换的是SDP,ICE Candidate交换的是网络地址。两者是并行的,但SDP交换必须先完成,ICE才能开始真正的连接尝试。
4.5 避坑指南
我做了这么多年WebRTC开发,踩过的坑能写一本书。这里挑几个最常见的:
坑1:setLocalDescription和setRemoteDescription的顺序不能乱。必须先设置本地描述,再设置远端描述。我曾经在代码里把顺序写反了,结果ICE一直卡在checking状态,排查了整整一天。
坑2:ICE Candidate的收集是异步的。你调用createOffer之后,Candidate不会立刻全部出现。要监听icecandidate事件,等它触发时再发送。我见过有人直接在createOffer后面取candidate,结果取到的是空的。
建议:开发阶段把pc.onicecandidate和pc.oniceconnectionstatechange都打上日志。这样你能看到Candidate的收集进度和ICE状态变化,调试起来一目了然。
好了,RTCPeerConnection的核心内容就这些。你把它理解成「连接管家+网络探路者+协商主持人」三个角色,就很好记了。代码写起来其实就那么几行,关键是理解背后的原理。
公众号:蓝海资料掘金营,微信deep3321