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?

先看触发条件。我总结下来,主要有三种场景:

  1. setRemoteDescription失败:比如你收到了对端的SDP,但格式不对或者缺少关键字段,setRemoteDescription抛异常了。这时候本地状态可能已经部分更新了,需要回滚。
  2. createOffer/createAnswer失败:这个比较隐蔽。我记得有一次在项目中,用户手机内存不足,createOffer直接返回了一个错误。嗯,这时候本地还没有设置任何SDP,但Promise reject了,你得决定下一步怎么走。
  3. 应用层主动取消协商:比如用户点了“取消呼叫”,或者信令超时了。你正在进行的协商还没完成,需要把状态拉回来。

你可能会问:“createOffer失败也要回滚?它都没设置过任何东西啊。” 对,严格来说createOffer失败本身不需要调用rollback,但后续的恢复流程需要。我后面会细说。

19.2 createOffer失败后的恢复策略

createOffer失败,最常见的原因是ICE代理状态异常或者编解码器配置冲突。我在一个跨平台项目中遇到过,Android端和iOS端的H.264 profile-level-id参数不匹配,createOffer直接挂了。

怎么恢复?我的建议是三步走:

  1. 检查PeerConnection状态:先确认signalingState是不是“stable”。如果是,说明还没开始协商,直接重试createOffer就行。
  2. 如果状态不是stable:说明之前有未完成的协商。这时候需要先调用setLocalDescription({type: 'rollback'}),把状态回滚到stable。
  3. 重新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会抛异常——嗯,已经在稳定状态了,回滚个啥?

我画了一张图,帮你理解整个状态流转:

SignalingState 状态机与 Rollback 路径 stable (初始状态) have-local-offer (已设本地offer) have-remote-offer (已设远端offer) setLocalDescription(offer) setRemoteDescription(offer) rollback rollback 💡 rollback 只能从 have-local-offer 或 have-remote-offer 回到 stable 如果当前已经是 stable,调用 rollback 会抛出 InvalidStateError

从图上可以看得很清楚: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协商的安全网,但不是你的业务逻辑。该做状态校验就做校验,该加超时就加超时,别把所有希望都寄托在回滚上。

公众号:蓝海资料掘金营,微信 deep3321