18、WebRTC调试与工具:从入门到实战
说实话,WebRTC 开发最让人头疼的不是写代码,而是出了问题你根本不知道问题出在哪。音视频通话嘛,涉及网络、编解码、浏览器、信令服务器……任何一个环节出岔子,画面就卡住或者黑屏了。
我自己刚入行那会儿,调试全靠 console.log,后来被一个诡异的丢包问题折磨了三天,才老老实实把 chrome://webrtc-internals 和 Wireshark 学透。今天我就把这些经验掰开揉碎讲给你听。
本章核心:掌握 WebRTC 调试的四大武器——浏览器内置工具、网络抓包、日志系统、性能监控。遇到问题别慌,按这个顺序排查。
18.1 chrome://webrtc-internals:浏览器自带的调试神器
你打开 Chrome,地址栏输入 chrome://webrtc-internals,回车。这就是 WebRTC 开发者的「黑匣子」。所有 PeerConnection 的状态、ICE 连接、RTP 流、丢包率、延迟……全在这里。
18.1.1 怎么看懂这些数据?
我个人习惯先看 ICE Connection State。如果状态不是 connected 或 completed,那说明网络穿透失败了。这时候我会切到 Stats 标签,看 candidate-pair 的 priority 和 writable 字段。
小技巧:在 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 解析。我一般这样做:
- 先抓包,过滤条件写
udp.port == 你的端口号 - 找到一条 RTP 包,右键 →
Decode As→ 选择 RTP - 然后 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%,建议开启 NACK 或 FEC。NACK 是请求重传,FEC 是前向纠错。我个人更推荐 NACK,因为 FEC 会占用额外带宽。
18.3 日志系统与常见错误排查
WebRTC 的日志系统其实挺完善的,只是很多人不知道怎么用。你可以在浏览器里开启 chrome://webrtc-logs,或者通过 RTCPeerConnection 的 oniceconnectionstatechange 事件来捕获状态变化。
18.3.1 常见错误及对策
| 错误类型 | 典型表现 | 排查方向 |
|---|---|---|
| ICE 连接失败 | 状态卡在 checking |
检查 STUN/TURN 服务器配置,防火墙是否放行 UDP |
| 媒体协商失败 | setRemoteDescription 报错 |
检查 SDP 中 codec 是否匹配,是否有 a=fmtp 冲突 |
| 编解码错误 | 黑屏但音频正常 | 查看 RTCRtpReceiver 的 codec 属性,确认解码器是否加载 |
| 带宽不足 | 视频模糊、频繁降分辨率 | 检查 googAvailableSendBandwidth 和 googTargetSendBitrate |
避坑指南:我曾经在 onicecandidate 事件里忘记把 candidate 发送给远端,结果 ICE 一直卡在 checking。排查了整整一天才发现是信令通道没传 candidate。记住:ICE 候选者必须通过信令服务器交换,浏览器不会自动帮你传。
18.3.2 如何优雅地打日志?
不要只打 console.log。我建议用 RTCPeerConnection 的 onconnectionstatechange 和 oniceconnectionstatechange 来记录状态机变化:
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 的 MediaStream 和 RTCPeerConnection 如果不及时释放,内存会越涨越高。我见过一个项目,每次通话结束后没有调用 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