一、为啥弱网下WebRTC容易断?

平时用微信语音或者抖音直播,在地铁、地下室这种信号差的地方,经常会出现通话突然断了、画面卡住的情况,其实这和WebRTC里的一个核心机制有关——ICE。简单说,ICE就像两个通话端之间的“信号快递员”,负责互相交换各自的网络地址、端口这些“找路信息”,确保双方能顺利连上。弱网环境下,这些找路信息要么传丢了,要么延迟太高,快递员找不到对方,就会触发连接断开。原生的WebRTC遇到这种情况,会直接硬重启ICE,也就是让快递员重新出发,但这段重启的时间里,通话是断的,用户会看到黑块、听不到声音,体验特别差。

1.1 弱网下的“假断开”问题

很多时候用户觉得“断网了”,其实只是短时间的网络波动,比如电梯里的信号闪了一下,ICE只是临时收不到对方的信息,根本不需要真的重启。原生策略不会区分“临时波动”和“真的失联”,一检测到ICE状态异常就直接重启,反而会因为频繁触发增加网络负担,甚至导致更严重的断连。

1.2 原生ICE重启的缺陷

原生的ICE restart是“硬重启”,相当于直接让整个连接断开,重新走一遍找流程,耗时大概1-3秒,这段时间用户的音视频是完全中断的,而且重启太频繁会导致网络资源浪费,极端情况下会触发浏览器的安全限制,阻止ICE再次尝试。

二、优化ICE Restart的具体做法

解决ICE重启的核心思路是:先判断是不是真的需要重启,再用“软重启”代替硬重启,避免不必要的断开

2.1 先“探路”,再动手

在触发ICE重启之前,必须先检测当前网络的真实状态,不能一卡就动。这里可以用浏览器自带的网络API,判断当前是不是真的弱网:

// 检测当前网络是否为需要处理的弱网
function isNeedHandleWeakNet() {
  // 兼容不同浏览器的网络API前缀
  const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
  if (!connection) return false; // 不支持API的情况,按默认不处理
  // 弱网标准:网络类型是2G级,或者延迟超过500ms
  const isWeakType = connection.effectiveType.includes('2g');
  const isHighLatency = connection.rtt > 500;
  // 再加一个判断:连续2秒以上的弱网才处理,避免临时波动
  return isWeakType || isHighLatency;
}
// 每2秒检测一次网络,避免频繁检测增加性能消耗
setInterval(() => {
  if (isNeedHandleWeakNet()) {
    // 后面的ICE重启代码放这里
  }
}, 2000);

这个检测逻辑能帮我们过滤掉大部分临时的网络波动,只有真的弱网时才会触发ICE处理。

2.2 ICE软重启的正确姿势

原生的restartIce方法其实可以用,但要加冷却时间,避免短时间内多次重启。软重启不会完全断开连接,只是让ICE重新收集候选路径,重启耗时大概几百毫秒,对用户几乎无感知:

// 记录上次ICE重启的时间,设置冷却5秒(避免频繁重启)
let lastRestartTime = 0;
const RESTART_COOLDOWN = 5000;
// 监听ICE连接状态变化,触发软重启
peerConnection.oniceconnectionstatechange = () => {
  const currentState = peerConnection.iceConnectionState;
  const now = Date.now();
  // 只有状态是failed,且冷却时间过了,才触发软重启
  if (currentState === 'failed' && (now - lastRestartTime > RESTART_COOLDOWN)) {
    lastRestartTime = now;
    // 软重启ICE,比硬重启耗时短,对业务影响小
    peerConnection.restartIce();
    console.log('触发ICE软重启,时间:', new Date().toLocaleTimeString());
  }
};

这里的冷却时间是关键,5秒的设置能平衡恢复速度和避免频繁触发的问题。

2.3 ICE重启的避坑点

  • 不能在ICE正在收集候选路径时重启,会导致冲突,触发异常状态;
  • 如果是因为用户切换了WiFi/4G这种手动网络变更,不需要重启ICE,浏览器会自动处理;
  • 要在重启前给用户一个低打扰的提示,比如“正在恢复”的小动画,而不是静默重启让用户摸不着头脑。

三、重连平滑性的实践

光优化ICE重启还不够,还要让用户感觉不到重连的过程,这部分是提升体验的核心。

3.1 重连时的过渡界面

很多时候ICE重启的几百毫秒里,用户会看到黑块,我们可以做一个轻量的加载遮罩,代替黑块,同时给用户反馈:

<!-- 重连加载遮罩,默认隐藏 -->
<div id="reconnectMask" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:9999; text-align:center; line-height:100vh; color:#fff;">
  <div style="display:inline-block; vertical-align:middle;">
    <!-- 加载动画 -->
    <div style="border:4px solid #fff; border-top:4px solid #007aff; border-radius:50%; width:30px; height:30px; animation: spin 1s linear infinite; margin:0 auto 10px;"></div>
    <span>网络有点卡,正在恢复...</span>
  </div>
</div>
<!-- 加载动画的CSS -->
<style>
  @keyframes spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
</style>

然后在JS里绑定ICE状态,当状态变成failed时显示遮罩,变成connected时隐藏遮罩,整个过程用户只会看到一个小动画,不会有黑块的突兀感。

3.2 丢帧不丢体验

重连时视频可能会丢几帧,我们可以让接收端缓存最近的3-5帧,继续显示,直到新的视频帧过来,这样不会有画面跳变;音频可以缓冲100ms,避免出现断音的情况,不用等完整的缓冲再播放。

3.3 降低用户感知的细节

  • 重连时不要弹出大的提示,用底部小 Toast 或者小动画就够;
  • 如果重连时间超过2秒,再弹出“网络不稳定”的提示,2秒内的短暂重连用户感知不到;
  • 后台悄悄做重连,如果用户切换到后台,不用暂停ICE,等回到前台后自动恢复,减少前台的卡顿。

四、实战踩坑总结

4.1 不能瞎重启

很多开发者一遇到ICE状态异常就重启,其实大部分情况是临时波动,只有连续5秒以上弱网才需要处理,避免因为频繁重启导致网络资源耗尽,触发浏览器的限制。

4.2 跨端适配问题

iOS的Safari和安卓的Chrome对WebRTC的API支持有差异,比如navigator.connection.effectiveType在iOS14以下的版本不支持,要做兼容处理,否则会导致网络检测失效。

4.3 日志留痕很重要

每次ICE重启的时间、原因、结果,都要存在LocalStorage里,遇到线上问题可以排查,比如是不是某个特定场景下频繁触发重启,优化策略。