19、SDP交换:创建Offer/Answer、设置本地/远程描述、ICE候选者收集
聊到WebRTC,绕不开的一个核心环节就是SDP交换。说白了,这就是两个浏览器之间互相“递名片”的过程——你得告诉对方你能支持什么格式、用什么编码、网络情况怎么样。我刚开始接触WebRTC时,总觉得SDP就是一堆看不懂的字符串,后来踩了不少坑才明白,这玩意儿是整个连接建立的基石。
什么是SDP?
SDP全称是Session Description Protocol,会话描述协议。它不负责传输数据,只负责描述“我想怎么跟你通信”。
一个典型的SDP里包含这些信息:
- 媒体类型:音频还是视频?
- 编解码器:用H264还是VP8?
- 网络信息:IP地址、端口号
- 传输协议:RTP还是SRTP?
- 带宽信息:我能承受多少码率?
嗯,这里要注意:SDP本身只是一个文本格式的协议,WebRTC用它来协商双方的媒体能力。你想想看,如果两个人各说各的方言,那肯定没法交流。SDP就是帮你们统一“语言”的。
核心要点:SDP交换是WebRTC连接建立的“握手”阶段,没有这个步骤,后续的音视频数据根本传不过去。
创建Offer/Answer
整个SDP交换流程分为两步:发起方创建Offer,接收方返回Answer。我习惯把这个过程比作打电话——你先说“喂,我能说中文和英文”,对方回答“好的,那咱们说中文吧”。
创建Offer
在WebRTC中,创建Offer是通过RTCPeerConnection对象完成的。代码其实很简单:
const pc = new RTCPeerConnection(configuration);
// 创建Offer
pc.createOffer()
.then(offer => {
return pc.setLocalDescription(offer);
})
.then(() => {
// 此时localDescription已经设置好了
// 可以把offer通过信令服务器发送给对方
signalingServer.send(JSON.stringify({
type: 'offer',
sdp: pc.localDescription.sdp
}));
})
.catch(err => {
console.error('创建Offer失败:', err);
});
我在项目中遇到过一个问题:createOffer必须在添加了媒体轨道之后调用,否则生成的SDP里没有媒体信息,对方根本不知道你要传什么。所以记得先调用addTrack或者addTransceiver。
创建Answer
接收方收到Offer后,需要创建Answer:
// 收到对方的Offer
signalingServer.onmessage = async (event) => {
const data = JSON.parse(event.data);
if (data.type === 'offer') {
// 设置远程描述
await pc.setRemoteDescription(new RTCSessionDescription(data));
// 创建Answer
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
// 把Answer发回去
signalingServer.send(JSON.stringify({
type: 'answer',
sdp: pc.localDescription.sdp
}));
}
};
这里有个坑:setRemoteDescription必须在createAnswer之前调用。我曾经因为顺序搞反,折腾了半天才发现是这个问题。
设置本地/远程描述
设置描述其实就是把SDP信息告诉RTCPeerConnection对象。它有两个方法:
setLocalDescription():设置本地的SDP(Offer或Answer)setRemoteDescription():设置对方的SDP
为什么要分本地和远程?因为WebRTC需要知道“我能做什么”和“对方能做什么”,然后才能找到共同点。说白了,就是双方各退一步,找个都能接受的方案。
小技巧:设置描述后,可以通过pc.localDescription和pc.remoteDescription查看当前的状态。调试时我经常打印这两个属性,看看SDP里到底写了什么。
ICE候选者收集
SDP交换完成后,接下来就是ICE候选者收集。ICE全称是Interactive Connectivity Establishment,交互式连接建立。它的作用是帮双方找到一条可以通信的网络路径。
为什么会需要这个?因为现实网络环境很复杂——有NAT、有防火墙、有代理服务器。你想想看,如果两个设备都在不同的内网里,它们怎么直接通信?ICE就是来解决这个问题的。
ICE候选者类型
| 类型 | 说明 | 优先级 |
|---|---|---|
| host | 本机IP地址,比如192.168.1.100 | 最高 |
| srflx | 经过NAT映射后的公网IP | 中等 |
| relay | 通过TURN服务器中转 | 最低 |
ICE收集的过程是这样的:
- 先收集本机所有网络接口的IP地址(host类型)
- 通过STUN服务器获取公网IP(srflx类型)
- 如果前两种都不行,就用TURN服务器中转(relay类型)
我曾经在一个客户的现场遇到过这种情况:他们的网络策略非常严格,host和srflx都连不上,最后只能靠relay。还好提前配置了TURN服务器,不然项目就黄了。
监听ICE候选者事件
在代码里,ICE候选者是通过事件回调来获取的:
pc.onicecandidate = (event) => {
if (event.candidate) {
// 把候选者信息发送给对方
signalingServer.send(JSON.stringify({
type: 'candidate',
candidate: event.candidate
}));
} else {
// 候选者收集完成
console.log('所有ICE候选者已收集完毕');
}
};
注意这个event.candidate为null的情况——它表示收集完成了。我刚开始写的时候没处理这个,结果一直收不到完成信号,排查了好久才发现。
接收对方的候选者
收到对方发来的候选者后,需要添加到RTCPeerConnection中:
signalingServer.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'candidate') {
const candidate = new RTCIceCandidate(data.candidate);
pc.addIceCandidate(candidate)
.then(() => {
console.log('成功添加ICE候选者');
})
.catch(err => {
console.error('添加ICE候选者失败:', err);
});
}
};
注意事项:添加ICE候选者必须在setRemoteDescription之后进行。如果顺序反了,WebRTC会报错。另外,如果对方发来的候选者已经过期或者不匹配,addIceCandidate也会失败。
完整的SDP交换流程
我把整个流程画了一张图,方便你理解:
从图上可以看出来,整个流程是双向的。发起方先出手,接收方回应,然后双方同时收集ICE候选者并交换。等所有候选者都交换完毕,WebRTC就会自动选择最优路径建立连接。
实际项目中的注意事项
我在多个项目中总结了几点经验,分享给你:
- 信令通道要可靠:SDP和ICE候选者都是通过信令通道传输的,如果信令通道不稳定,整个连接过程就会失败。我建议用WebSocket,别用HTTP轮询。
- ICE重启:如果网络环境发生变化(比如从WiFi切到4G),可以调用
pc.restartIce()重新收集候选者。 - 超时处理:ICE收集不是无限等待的,一般设置一个30秒的超时。如果超时还没连上,就提示用户检查网络。
- 日志记录:开发阶段一定要把SDP和ICE候选者的内容打印出来,方便排查问题。我曾经靠这个找到了一个编解码器不匹配的bug。
调试技巧:Chrome浏览器里输入chrome://webrtc-internals,可以看到所有WebRTC连接的详细信息,包括SDP内容、ICE状态、网络延迟等。这个工具救过我很多次。
好了,SDP交换和ICE候选者收集就讲到这里。记住一句话:SDP是“谈什么”,ICE是“怎么谈”。两者配合好了,P2P连接才能顺利建立。