18、重协商(Re-negotiation):触发重协商的场景、setLocalDescription的再次调用、动态添加/移除轨道
重协商,说白了就是让两个 PeerConnection 重新坐下来,再谈一次。嗯,这个场景在实际项目中太常见了。我记得刚做 WebRTC 那会儿,以为连接建立好就万事大吉了,结果产品经理过来说:「我们要支持动态开关摄像头。」好嘛,重协商就这么来了。
什么时候需要重协商?
触发重协商的场景其实就几类,我帮你梳理一下:
- 动态添加/移除轨道:比如用户中途打开麦克风、关闭摄像头。这是最常见的触发场景。
- 编解码器偏好变化:比如你想从 VP8 切到 H.264,或者调整码率。
- ICE 重新收集:网络切换了(WiFi 切 4G),需要重新收集候选者。
- DTMF 或数据通道:有些实现里,创建数据通道也会触发重协商。
核心原则:只要你想改变媒体流的结构或参数,大概率需要重协商。
你想想看,为什么不能直接改?因为 SDP 是双方协商的结果。你这边改了,对面不知道,那不就乱套了?所以必须走一轮新的 Offer/Answer 交换。
setLocalDescription 的再次调用
重协商的核心操作,就是再次调用 setLocalDescription。很多人以为这玩意儿只在初始化时用一次,其实不是的。
流程大概是这样的:
- 你调用
pc.addTrack()或pc.removeTrack()。 - 然后调用
pc.createOffer()生成新的 Offer。 - 再调用
pc.setLocalDescription(offer)设置本地描述。 - 通过信令把 Offer 发给远端。
- 远端调用
pc.setRemoteDescription(offer),然后pc.createAnswer(),再pc.setLocalDescription(answer)。 - 你这边收到 Answer 后,调用
pc.setRemoteDescription(answer)。
嗯,这里要注意:setLocalDescription 是可以多次调用的。每次调用都会替换掉之前的本地描述。我在项目中遇到过有人担心会「覆盖」掉什么,其实不会,它本来就是设计成可重入的。
避坑指南:千万不要在 setLocalDescription 还没完成(Promise 还没 resolve)时,又去调下一次。我曾经因为这个踩过坑,导致 SDP 状态混乱,浏览器直接报 InvalidStateError。
动态添加/移除轨道的正确姿势
动态添加轨道,代码其实很简单:
// 添加轨道
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const track = stream.getVideoTracks()[0];
const sender = pc.addTrack(track, stream);
// 触发重协商
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 然后通过信令发送 offer...
移除轨道也类似:
// 移除轨道
const senders = pc.getSenders();
for (const sender of senders) {
if (sender.track && sender.track.kind === 'video') {
pc.removeTrack(sender);
break;
}
}
// 触发重协商
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 发送 offer...
我个人习惯把重协商的逻辑封装成一个函数,比如 negotiate(),这样每次改完轨道直接调它就行,不用重复写 createOffer 和 setLocalDescription。
小技巧:可以用 pc.onnegotiationneeded 事件来监听是否需要重协商。但要注意,这个事件在某些浏览器上触发时机不太一致,我建议还是手动控制更靠谱。
重协商的完整流程图
下面这张图展示了重协商的完整流程,从添加轨道到 SDP 交换完成:
重协商的注意事项
重协商看着简单,但坑不少。我列几个常见的:
| 问题 | 说明 | 建议 |
|---|---|---|
| 并发重协商 | 如果多次触发重协商,可能导致 SDP 状态错乱 | 加一个锁或队列,串行执行 |
| 轨道状态不一致 | 添加轨道后还没协商完,远端可能收不到数据 | 等 ontrack 事件触发后再操作 |
| ICE 重启 | 重协商时可能触发 ICE 重启,导致短暂断流 | 评估业务场景,必要时用 iceRestart 参数 |
| 编解码器不匹配 | 新添加的轨道可能不支持远端已有的编解码器 | 提前检查 getCapabilities() |
重要提醒:重协商不是免费的。每次重协商都会产生信令开销,而且可能引起短暂的媒体中断。所以不要频繁触发,比如用户快速开关摄像头时,最好加个防抖。
实际项目中的经验
我在做一个多人视频会议项目时,遇到过一个问题:用户 A 打开摄像头,触发重协商,结果用户 B 那边也同时在操作麦克风,两边同时发 Offer,导致信令冲突。后来我们加了一个「协商锁」,同一时间只允许一个人发起重协商,问题就解决了。
还有一个坑是 addTrack 和 removeTrack 的调用顺序。如果你先 remove 再 add,中间可能会有一个短暂的无轨道状态。我建议先 add 再 remove,或者用 replaceTrack 来替换轨道,这样更平滑。
个人习惯:我一般会在 setLocalDescription 之后加一个超时处理。如果 5 秒内没收到远端的 Answer,就主动取消这次重协商,避免死等。
嗯,重协商这块内容其实不难,但细节很多。你只要记住:改轨道 -> 发 Offer -> 等 Answer,这个循环就对了。剩下的就是处理各种边界情况,比如网络断开、用户快速操作等等。
最后说一句,setLocalDescription 的再次调用,说白了就是告诉浏览器:「嘿,我的媒体流变了,帮我生成新的 SDP。」浏览器会帮你处理好一切,你只需要保证调用时机正确就行。
公众号:蓝海资料掘金营,微信 deep3321