跳到正文

WebRTC 软电话音视频排障与流媒体避坑指南 ​

本文档总结在开发与测试环境中进行 WebRTC / Verto 音视频通话时的标准网络连接路径、常见挂断故障(如媒体超时秒断)以及前端视频流渲染避坑规范(防黑屏卡死)。


1. 网络连接标准路径(推荐实践) ​

进行 WebRTC 软电话联调时,建议采用以下两条标准路径,避免在复杂的局域网二层路由中耗费调试成本:

路径 A:本机开发联调(最简极速) ​

  • 适用场景:在 Windows 11 开发机本机使用浏览器软电话(http://localhost:19913/softphone 或 19920)或桌面 MicroSIP 客户端。
  • 工作机制:依托开发机 %USERPROFILE%\.wslconfig 配置的镜像网络(networkingMode=Mirrored 与 hostAddressLoopback=true),Windows 与 WSL 内部共享同一回环网络。
  • 连接方式:SIP / WS 直接指向 127.0.0.1(WS 端口 5066,WSS 端口 7443),数据通过系统内核 Loopback 极速直达容器,完全不经过物理无线网卡协议栈,天然杜绝网络丢包与 ARP 异常。

路径 B:跨网与移动端真机联调(工业级标准) ​

  • 适用场景:局域网内其他设备(如手机移动端浏览器)、远程办公或跨网络环境真机联调。
  • 工作机制:采用现代 WebRTC 标准的 ICE + STUN/TURN (coturn) 架构,由部署在公网节点的 coturn 服务自动完成 NAT 穿透与媒体中继。
  • 连接方式:直接接入线上环境(wss://fs.wangijun.com,coturn 监听 3478,中继段 31000-31040/UDP),详见专题文档 softphone-webrtc-ice-turn-guide.md。在任何 4G/5G 蜂窝网、多层路由器或家庭 Wi-Fi 下均可秒级稳定建联,连通率 100%。

为什么不推荐局域网 Wi-Fi 直连 WSL 调试?

在家庭或办公无线 Wi-Fi(802.11 协议)下,由于 802.11 帧格式三地址限制,无线网卡不会将局域网对端设备的二层 ARP 响应镜像送入 WSL2 内部虚拟网卡,导致 WSL 内核的邻居状态卡在 FAILED 并触发 UDP EHOSTUNREACH 异常挂断。过去曾尝试在本地部署定时轮询注入静态 ARP 等补丁手段,但维护成本高且极易失效。当前项目推荐统一按上述两条标准路径运行,彻底规避局域网二层广播缺陷。


2. FreeSWITCH 拨号计划超时误区(排查呼叫秒断) ​

在排查呼叫接通后数秒内被意外切断(伴随 media_timeout)时,需重点核对 FreeSWITCH 拨号计划配置:

xml
<!-- 常见错误:media_timeout 在 FreeSWITCH 中单位为毫秒 (ms) -->
<action application="set" data="media_timeout=300"/>
  • 故障成因:部分配置误将 media_timeout 理解为秒,填入 300 实际上是 300 毫秒(0.3 秒)!在 WebRTC 初期媒体与 DTLS 握手协商过程中,微小的等待就会触发超时强制掐线。
  • 解决方案:在拨号计划中移除不必要的激进超时配置;若需保活防护,应采用以秒为单位的标准配置(如 rtp-timeout-sec=300)。

3. 前端 WebRTC 视频秒开与防黑屏避坑规范 ​

在音视频通话(如 apps/callflow-webpage 中的 Verto 视频电话组件)中,底层可能双向传输了数万个视频 RTP 数据包,但网页端依然黑屏无画面。这通常是由前端 MediaStream 状态管理和浏览器播放策略引起的,必须遵循以下加固规范:

3.1 维护单一持久的 MediaStream 实例 ​

在 WebRTC 的 peer.ontrack 回调中,绝不能每次收到 track 就重新执行 new MediaStream() 替换原有引用:

typescript
// use-verto-client.ts
// 声明唯一持久的聚合流实例
let remoteMediaStream: MediaStream | null = null;

peer.ontrack = event => {
  if (!remoteMediaStream) {
    remoteMediaStream = new MediaStream();
    remoteStream.value = remoteMediaStream; // 仅赋值一次
  }
  // 增量注入新到达的媒体轨道,严禁替换 MediaStream 对象引用
  if (!remoteMediaStream.getTracks().some(t => t.id === event.track.id)) {
    remoteMediaStream.addTrack(event.track);
  }

  const updateTracksState = () => {
    if (!remoteMediaStream) return;
    const tracks = remoteMediaStream.getTracks();
    hasRemoteVideo.value = tracks.some(t => t.kind === "video" && t.readyState === "live");
    hasRemoteAudio.value = tracks.some(t => t.kind === "audio" && t.readyState === "live");
  };

  updateTracksState();
  event.track.onunmute = () => updateTracksState();
  event.track.onmute = () => updateTracksState();
  event.track.onended = () => updateTracksState();
};

原理:Vue 的响应式 watch(remoteStream) 一旦感知到新对象,就会重新执行 video.srcObject = stream。HTML5 <video> 每次被赋予新的 srcObject 都会立即销毁已初始化的底层硬件解码器并将就绪状态重置为 HAVE_NOTHING,丢弃后续所有 P 帧,导致画面永久卡死在黑屏状态。

3.2 独立音频播放 + 视频元素显式 muted 绕过 Autoplay ​

在视频电话页面(如 verto-videophone.vue)中:

html
<!-- 1. 独立 audio 标签负责声音播放 -->
<audio ref="remoteAudioRef" autoplay class="hidden" />

<!-- 2. 大屏 video 必须显式声明 muted 属性 -->
<video
  ref="remoteVideoRef"
  class="remote-video-feed"
  autoplay
  playsinline
  muted
  webkit-playsinline="true"
  x5-playsinline="true"
  x5-video-player-type="h5-page"
  x5-video-player-fullscreen="false"
/>
typescript
// 3. 增加 srcObject 幂等防重判断,避免无谓重置解码器
watch(
  remoteStream,
  async stream => {
    if (remoteAudioRef.value && remoteAudioRef.value.srcObject !== (stream || null)) {
      remoteAudioRef.value.srcObject = stream || null;
      if (stream) remoteAudioRef.value.play().catch(() => {});
    }

    if (remoteVideoRef.value && remoteVideoRef.value.srcObject !== (stream || null)) {
      remoteVideoRef.value.srcObject = stream || null;
      if (stream) {
        try {
          await remoteVideoRef.value.play();
        } catch (err) {
          // 兜底保障:若浏览器拦截则静音起播
          remoteVideoRef.value.muted = true;
          await remoteVideoRef.value.play().catch(() => {});
        }
      }
    }
  },
  { immediate: true }
);

原理:现代浏览器对带有声音的媒体播放有着极严格的 Autoplay 自动播放拦截策略。通过独立 <audio> 播放声音,并将大屏 <video> 设为 muted,既避免了双通道声音混响,又可 100% 获得浏览器的静音自动起播豁免。


4. 常用诊断命令速查 ​

在 WSL 终端中执行以下命令可快速抓包与监控:

bash
# 1. 实时捕获两端双向音视频 UDP 数据流与信令交互
sudo tcpdump -i any host <对端设备 IP> -nn

# 2. 实时监控 FreeSWITCH 呼叫通道与 Verto 调试日志
docker exec -it fs-app fs_cli -x "show channels"
docker exec -it fs-app fs_cli -x "verto debug on"

文档与代码在同一仓库维护,现有 Markdown 是唯一内容源。