19、Rollback机制:SDP回滚的触发条件、createOffer失败后的恢复、setLocalDescription('rollback')
聊到WebRTC的媒体协商,很多人会把注意力放在createOffer、createAnswer、setLocalDescription、setRemoteDescription这几个核心API上。这没错,但有个机制经常被忽略——Rollback。
说白了,Rollback就是SDP协商过程中的“后悔药”。你想想看,网络环境那么复杂,信令服务器可能挂掉,用户可能突然切网络,甚至你写的代码里createOffer的参数传错了……这些情况都会导致协商走到一半卡住了。没有Rollback,整个PeerConnection的状态就乱了。
我个人习惯把Rollback理解成“事务回滚”。就像数据库里执行了一半的SQL发现报错,得把数据恢复到之前的状态。WebRTC的SDP协商也是类似的——SignalingState必须保持一致性。
19.1 什么时候需要Rollback?
先看触发条件。我总结下来,主要有三种场景:
- setRemoteDescription失败:比如你收到了对端的SDP,但格式不对或者缺少关键字段,setRemoteDescription抛异常了。这时候本地状态可能已经部分更新了,需要回滚。
- createOffer/createAnswer失败:这个比较隐蔽。我记得有一次在项目中,用户手机内存不足,createOffer直接返回了一个错误。嗯,这时候本地还没有设置任何SDP,但Promise reject了,你得决定下一步怎么走。
- 应用层主动取消协商:比如用户点了“取消呼叫”,或者信令超时了。你正在进行的协商还没完成,需要把状态拉回来。
你可能会问:“createOffer失败也要回滚?它都没设置过任何东西啊。” 对,严格来说createOffer失败本身不需要调用rollback,但后续的恢复流程需要。我后面会细说。
19.2 createOffer失败后的恢复策略
createOffer失败,最常见的原因是ICE代理状态异常或者编解码器配置冲突。我在一个跨平台项目中遇到过,Android端和iOS端的H.264 profile-level-id参数不匹配,createOffer直接挂了。
怎么恢复?我的建议是三步走:
- 检查PeerConnection状态:先确认signalingState是不是“stable”。如果是,说明还没开始协商,直接重试createOffer就行。
- 如果状态不是stable:说明之前有未完成的协商。这时候需要先调用setLocalDescription({type: 'rollback'}),把状态回滚到stable。
- 重新createOffer:回滚成功后,再重新调用createOffer,带上正确的参数。
这里有个坑——不要盲目重试。我曾经见过一个同事在createOffer失败后直接循环重试,结果把浏览器搞崩了。正确的做法是:先判断失败原因,如果是临时性错误(比如网络抖动),可以等几秒再试;如果是配置错误,得先修配置。
核心原则:createOffer失败后,不要直接setLocalDescription(offer),先检查signalingState。非stable状态必须先rollback。
19.3 setLocalDescription('rollback') 到底干了什么?
这个API是Chrome 58之后才稳定支持的。它的作用很简单:把signalingState回退到上一个稳定状态。
具体来说:
- 如果当前是“have-local-offer”,回滚到“stable”。
- 如果当前是“have-remote-offer”,回滚到“stable”。
- 如果当前是“stable”,调用rollback会抛异常——嗯,已经在稳定状态了,回滚个啥?
我画了一张图,帮你理解整个状态流转:
从图上可以看得很清楚:rollback只有两条路径,都是从非稳定状态回到stable。没有其他分支。
19.4 实战:一个完整的Rollback流程
光说理论不够,我写一段伪代码,展示实际项目中怎么用:
async function safeCreateOffer(pc) {
try {
// 第一步:检查当前状态
if (pc.signalingState !== 'stable') {
console.warn('当前状态不是stable,先回滚');
await pc.setLocalDescription({type: 'rollback'});
}
// 第二步:创建offer
const offer = await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
});
// 第三步:设置本地描述
await pc.setLocalDescription(offer);
return offer;
} catch (err) {
console.error('createOffer流程失败:', err);
// 第四步:异常恢复
if (pc.signalingState !== 'stable') {
try {
await pc.setLocalDescription({type: 'rollback'});
} catch (rollbackErr) {
// 如果rollback也失败了,只能重建PeerConnection
console.error('rollback也失败了,需要重建连接');
throw rollbackErr;
}
}
throw err;
}
}
个人经验:我在生产环境中见过最诡异的情况是——rollback调用成功了,但ICE代理状态没有跟着恢复。后来发现是浏览器bug,Chrome 74之前的一个已知问题。解决方案是rollback之后主动重启ICE:pc.restartIce()。
19.5 什么时候不要用Rollback?
嗯,这里要划重点。Rollback不是万能的,有些场景下它反而会添乱:
- ICE重启过程中:如果你正在做ICE restart,rollback会把ICE候选人也回滚掉,导致连接中断。ICE restart有自己的回滚机制,别混用。
- 已经建立了媒体流:如果媒体流已经在传输了,rollback不会关掉媒体流——它只影响SDP状态。但如果你期望的是“回滚到没媒体流的状态”,那得手动关掉Transceiver。
- 多次连续rollback:别在一个循环里反复rollback。我见过有人写了个重试逻辑,rollback -> createOffer -> 失败 -> rollback -> createOffer……最后浏览器内存爆了。每次rollback之后,最好加一个短暂的延迟。
警告:rollback不会重置Transceiver的direction属性。如果你之前设置了“sendrecv”,rollback之后还是“sendrecv”。需要手动调用transceiver.setDirection('inactive')来完全重置。
19.6 浏览器兼容性
最后聊一下兼容性。Rollback机制在主流浏览器上的支持情况:
| 浏览器 | 支持版本 | 备注 |
|---|---|---|
| Chrome | 58+ | 稳定支持,但早期版本有ICE状态残留bug |
| Firefox | 60+ | 支持良好,但rollback后需要重新添加ICE候选 |
| Safari | 12.1+ | 部分支持,某些场景下rollback不触发iceconnectionstatechange |
| Edge (Chromium) | 79+ | 与Chrome行为一致 |
我个人建议:不要完全依赖rollback。它是个好工具,但不是银弹。在关键的生产环境中,我通常会加一个“重建PeerConnection”的兜底逻辑。如果rollback两次都失败了,直接new RTCPeerConnection()重新来。虽然粗暴,但有效。
好了,关于Rollback机制就聊这么多。记住一句话:Rollback是SDP协商的安全网,但不是你的业务逻辑。该做状态校验就做校验,该加超时就加超时,别把所有希望都寄托在回滚上。