18、WebRTC调试与工具:从入门到实战

说实话,WebRTC 开发最让人头疼的不是写代码,而是出了问题你根本不知道问题出在哪。音视频通话嘛,涉及网络、编解码、浏览器、信令服务器……任何一个环节出岔子,画面就卡住或者黑屏了。

我自己刚入行那会儿,调试全靠 console.log,后来被一个诡异的丢包问题折磨了三天,才老老实实把 chrome://webrtc-internals 和 Wireshark 学透。今天我就把这些经验掰开揉碎讲给你听。

本章核心:掌握 WebRTC 调试的四大武器——浏览器内置工具、网络抓包、日志系统、性能监控。遇到问题别慌,按这个顺序排查。

WebRTC 调试体系 chrome://webrtc-internals 实时 ICE / RTP / 统计 Wireshark 抓包 RTP/RTCP 包分析 日志系统 RTCError / 信令日志 性能监控 CPU / 内存 / 带宽 排查顺序:浏览器 → 抓包 → 日志 → 性能

18.1 chrome://webrtc-internals:浏览器自带的调试神器

你打开 Chrome,地址栏输入 chrome://webrtc-internals,回车。这就是 WebRTC 开发者的「黑匣子」。所有 PeerConnection 的状态、ICE 连接、RTP 流、丢包率、延迟……全在这里。

18.1.1 怎么看懂这些数据?

我个人习惯先看 ICE Connection State。如果状态不是 connectedcompleted,那说明网络穿透失败了。这时候我会切到 Stats 标签,看 candidate-pairprioritywritable 字段。

小技巧:在 webrtc-internals 页面按 Ctrl+F,搜索 googRtt 可以快速找到往返时延。如果 RTT 超过 300ms,用户会明显感觉到延迟。

还有一个我经常用的——googFrameRateReceived。如果这个值低于 15fps,说明接收端解码跟不上,或者网络丢包严重。嗯,这里要注意:不要只看平均值,要看曲线。突然掉帧往往意味着网络抖动。

18.1.2 实战:用 webrtc-internals 定位黑屏问题

我记得有一次,用户反馈视频通话偶尔黑屏。我打开 webrtc-internals,发现 framesDecoded 在某个时间点之后就不再增长了,但 packetsReceived 还在涨。这说明数据包到了,但解码器罢工了。

排查下来,是 codec 协商出了问题——对方发的是 H.264,但接收端只支持 VP8。解决方案很简单:在 SDP 协商时把 codec 优先级调一下。

// 设置 codec 优先级
const transceiver = pc.getTransceivers()[0];
const codecs = RTCRtpReceiver.getCapabilities('video').codecs;
const h264Codec = codecs.find(c => c.mimeType.includes('H264'));
if (h264Codec) {
  transceiver.setCodecPreferences([h264Codec]);
}

18.2 Wireshark 抓包分析 RTP/RTCP

浏览器工具能看应用层,但网络层的问题还得靠 Wireshark。说白了,Wireshark 就是网络世界的显微镜。

18.2.1 抓包前的准备

你需要在 Wireshark 里设置 RTP 解析。我一般这样做:

  1. 先抓包,过滤条件写 udp.port == 你的端口号
  2. 找到一条 RTP 包,右键 → Decode As → 选择 RTP
  3. 然后 Wireshark 会自动解析后续的 RTP 流

注意:如果 RTP 包被加密了(SRTP),Wireshark 默认看不到载荷。你需要导出 SDP 中的 a=crypto 字段,在 Wireshark 里设置解密密钥。具体操作:Edit → Preferences → Protocols → SRTP → 添加密钥。

18.2.2 从 RTP 包里能看出什么?

我重点看三个东西:

  • 序列号(Sequence Number):如果出现跳号,说明有丢包。连续丢 3 个以上,视频就会卡顿。
  • 时间戳(Timestamp):相邻包的间隔是否均匀。不均匀说明网络抖动大。
  • SSRC:每个流有唯一标识。如果同一个 SSRC 出现在两个不同的 IP 上,那可能是发生了 ICE 重启 或者 Simulcast 切换

我曾经遇到一个诡异的问题:视频流每隔 30 秒就卡一下。用 Wireshark 一看,发现每 30 秒就有一个 RTCP 的 BYE 包 发出来,然后马上又重建连接。查了半天,是信令服务器那边有个定时器在作怪。

18.2.3 RTCP 包:不只是报告

RTCP 包里有 Sender Report (SR)Receiver Report (RR)。SR 告诉对方「我发了多少包」,RR 告诉对方「我收到了多少包」。两者一对比,丢包率就出来了。

// 丢包率计算(伪代码)
丢包率 = (发送端总包数 - 接收端总包数) / 发送端总包数 * 100%

如果丢包率超过 5%,建议开启 NACKFEC。NACK 是请求重传,FEC 是前向纠错。我个人更推荐 NACK,因为 FEC 会占用额外带宽。

18.3 日志系统与常见错误排查

WebRTC 的日志系统其实挺完善的,只是很多人不知道怎么用。你可以在浏览器里开启 chrome://webrtc-logs,或者通过 RTCPeerConnectiononiceconnectionstatechange 事件来捕获状态变化。

18.3.1 常见错误及对策

错误类型 典型表现 排查方向
ICE 连接失败 状态卡在 checking 检查 STUN/TURN 服务器配置,防火墙是否放行 UDP
媒体协商失败 setRemoteDescription 报错 检查 SDP 中 codec 是否匹配,是否有 a=fmtp 冲突
编解码错误 黑屏但音频正常 查看 RTCRtpReceivercodec 属性,确认解码器是否加载
带宽不足 视频模糊、频繁降分辨率 检查 googAvailableSendBandwidthgoogTargetSendBitrate

避坑指南:我曾经在 onicecandidate 事件里忘记把 candidate 发送给远端,结果 ICE 一直卡在 checking。排查了整整一天才发现是信令通道没传 candidate。记住:ICE 候选者必须通过信令服务器交换,浏览器不会自动帮你传。

18.3.2 如何优雅地打日志?

不要只打 console.log。我建议用 RTCPeerConnectiononconnectionstatechangeoniceconnectionstatechange 来记录状态机变化:

pc.onconnectionstatechange = () => {
  console.log(`[WebRTC] 连接状态: ${pc.connectionState}`);
  if (pc.connectionState === 'failed') {
    // 触发重连逻辑
    restartIce();
  }
};

pc.oniceconnectionstatechange = () => {
  console.log(`[ICE] 状态: ${pc.iceConnectionState}`);
  if (pc.iceConnectionState === 'disconnected') {
    // 可能只是短暂断连,等几秒再判断
    setTimeout(() => {
      if (pc.iceConnectionState === 'disconnected') {
        console.warn('[ICE] 长时间断连,尝试重启');
      }
    }, 5000);
  }
};

18.4 性能监控:CPU、内存、带宽

WebRTC 是 CPU 密集型应用。编码、解码、渲染,每一步都在吃 CPU。如果 CPU 占用率超过 80%,视频就会开始掉帧。

18.4.1 浏览器端的性能监控

performance.getEntriesByType('resource') 可以看资源加载时间。但更直接的是用 RTCPeerConnection.getStats() 获取实时统计:

async function monitorPerformance(pc) {
  const stats = await pc.getStats();
  stats.forEach(report => {
    if (report.type === 'outbound-rtp' && report.kind === 'video') {
      console.log(`编码帧率: ${report.framesPerSecond}`);
      console.log(`目标码率: ${report.targetBitrate / 1000} kbps`);
      console.log(`编码 CPU 占用: ${report.encoderImplementation}`);
    }
    if (report.type === 'candidate-pair' && report.state === 'succeeded') {
      console.log(`当前 RTT: ${report.currentRoundTripTime * 1000} ms`);
      console.log(`可用带宽: ${report.availableOutgoingBitrate / 1000} kbps`);
    }
  });
}

// 每 2 秒采样一次
setInterval(() => monitorPerformance(pc), 2000);

18.4.2 带宽自适应:别让用户卡死

WebRTC 内置了带宽估计(GCC),但有时候它不够激进。我习惯手动设置一个 maxBitrate,防止编码器吃太多带宽:

const sender = pc.getSenders()[0];
const params = sender.getParameters();
params.encodings[0].maxBitrate = 500000; // 500 kbps
sender.setParameters(params);

你想想看,如果用户用的是 4G 网络,带宽只有 1Mbps,你非要推 2Mbps 的流,那画面肯定卡成幻灯片。不如主动降码率,至少保证流畅。

我的经验:在移动端,建议把视频码率控制在 300-800 kbps,音频用 32-64 kbps。这样即使网络波动,用户也不会完全断连。另外,别忘了监控 googNacksReceived——如果这个值飙升,说明你的码率设得太高了。

18.4.3 内存泄漏:一个容易被忽视的问题

WebRTC 的 MediaStreamRTCPeerConnection 如果不及时释放,内存会越涨越高。我见过一个项目,每次通话结束后没有调用 pc.close(),结果内存从 50MB 涨到 500MB,浏览器直接崩溃。

function cleanup(pc) {
  pc.getSenders().forEach(sender => {
    if (sender.track) {
      sender.track.stop();
    }
  });
  pc.close();
  pc = null;
}

嗯,这里要注意:track.stop() 只是停止采集,不会释放 PeerConnection。一定要显式调用 close()

小结

调试 WebRTC 其实没那么玄乎。记住这个顺序:先用 chrome://webrtc-internals 看应用层状态,再用 Wireshark 看网络层细节,日志系统帮你定位代码逻辑问题,性能监控确保资源不超标。把这四样工具用好,90% 的问题都能在半小时内定位。

我当年被那个 ICE 候选者没传的问题折磨时,要是早点学会看 webrtc-internals 的 candidate-pair 状态,也不至于熬夜到凌晨三点。工具就在那里,关键是你得知道怎么看、看什么。


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