WebRTC 调试: 抓包、RTCStats、webrtc-internals

本文是 WebRTC 系列专栏的第二十六篇,将深入探讨 WebRTC 的调试工具和方法,包括 Chrome webrtc-internals、RTP/RTCP 抓包分析以及性能指标解读。


目录

  1. 调试概述
  2. webrtc-internals
  3. RTCStats API
  4. 抓包分析
  5. 常见问题诊断
  6. 监控系统搭建
  7. 总结

1. 调试概述

1.1 WebRTC 调试挑战

WebRTC 调试难点:

1. 实时性
   - 问题转瞬即逝
   - 难以复现

2. 复杂性
   - 多协议栈 (ICE/DTLS/SRTP/RTP/RTCP)
   - 多组件交互

3. 网络依赖
   - NAT 穿透问题
   - 网络波动影响

4. 端到端
   - 发送端/接收端/服务器
   - 需要多点协同调试

1.2 调试工具概览

工具用途适用场景
webrtc-internals浏览器内置调试快速诊断
RTCStats API程序化获取统计监控系统
Wireshark网络抓包协议分析
Chrome DevTools网络/控制台信令调试

2. webrtc-internals

2.1 访问方式

Chrome: chrome://webrtc-internals
Firefox: about:webrtc
Edge: edge://webrtc-internals

功能:
- 查看所有 PeerConnection
- 实时统计图表
- 事件日志
- 导出调试信息

2.2 界面解读

webrtc-internals 主要区域:

+------------------------------------------------------------------+
|  Create Dump  |  Download the PeerConnection updates              |
+------------------------------------------------------------------+
|                                                                  |
|  PeerConnection (id=1)                                           |
|  +------------------------------------------------------------+  |
|  | url: https://example.com                                   |  |
|  | iceConnectionState: connected                              |  |
|  | iceGatheringState: complete                                |  |
|  | signalingState: stable                                     |  |
|  +------------------------------------------------------------+  |
|                                                                  |
|  [Stats Tables]  [Stats Graphs]  [Event Log]                     |
|                                                                  |
+------------------------------------------------------------------+

2.3 关键指标

重要统计指标:

1. 连接状态
   - iceConnectionState
   - iceGatheringState
   - signalingState
   - connectionState

2. 视频发送
   - bytesSent: 发送字节数
   - packetsSent: 发送包数
   - framesEncoded: 编码帧数
   - framesSent: 发送帧数
   - qualityLimitationReason: 质量限制原因

3. 视频接收
   - bytesReceived: 接收字节数
   - packetsReceived: 接收包数
   - packetsLost: 丢包数
   - framesDecoded: 解码帧数
   - framesDropped: 丢弃帧数
   - jitter: 抖动

4. 音频
   - audioLevel: 音量级别
   - totalAudioEnergy: 总音频能量
   - echoReturnLoss: 回声损耗

2.4 图表分析

常用图表:

1. Bitrate (码率)
   - 发送/接收码率变化
   - 识别带宽问题

2. Packets (包)
   - 发送/接收/丢失包数
   - 识别丢包问题

3. Frame Rate (帧率)
   - 编码/发送/接收/解码帧率
   - 识别性能问题

4. RTT (往返时延)
   - 网络延迟变化
   - 识别网络问题

2.5 导出调试信息

// 在 webrtc-internals 页面
// 点击 "Create Dump" 按钮
// 或 "Download the PeerConnection updates and stats data"

// 导出格式为 JSON,包含:
{
    "getUserMedia": [...],
    "PeerConnections": [
        {
            "pid": 12345,
            "url": "https://example.com",
            "rtcConfiguration": {...},
            "constraints": {...},
            "stats": {...},
            "updateLog": [...]
        }
    ]
}

3. RTCStats API

3.1 获取统计信息

// 获取所有统计
const stats = await pc.getStats();

stats.forEach(report => {
    console.log(`${report.type}: ${report.id}`);
    console.log(report);
});

// 获取特定 sender/receiver 的统计
const sender = pc.getSenders()[0];
const senderStats = await sender.getStats();

const receiver = pc.getReceivers()[0];
const receiverStats = await receiver.getStats();

3.2 统计报告类型

// 主要报告类型
const reportTypes = {
    // 入站 RTP 流
    'inbound-rtp': {
        kind: 'video',           // 'audio' 或 'video'
        packetsReceived: 1000,
        bytesReceived: 500000,
        packetsLost: 5,
        jitter: 0.01,
        framesDecoded: 300,
        framesDropped: 2,
        frameWidth: 1280,
        frameHeight: 720
    },
    
    // 出站 RTP 流
    'outbound-rtp': {
        kind: 'video',
        packetsSent: 1000,
        bytesSent: 500000,
        framesEncoded: 300,
        framesSent: 300,
        qualityLimitationReason: 'none', // 'cpu', 'bandwidth', 'other'
        qualityLimitationDurations: {
            none: 10000,
            cpu: 0,
            bandwidth: 500,
            other: 0
        }
    },
    
    // 远端入站 RTP (从 RTCP RR 获取)
    'remote-inbound-rtp': {
        packetsLost: 5,
        jitter: 0.01,
        roundTripTime: 0.05
    },
    
    // ICE 候选对
    'candidate-pair': {
        state: 'succeeded',
        localCandidateId: 'xxx',
        remoteCandidateId: 'yyy',
        bytesSent: 1000000,
        bytesReceived: 1000000,
        currentRoundTripTime: 0.05,
        availableOutgoingBitrate: 2000000
    },
    
    // 本地候选
    'local-candidate': {
        candidateType: 'host', // 'srflx', 'relay'
        protocol: 'udp',
        address: '192.168.1.100',
        port: 50000
    },
    
    // 媒体源
    'media-source': {
        kind: 'video',
        width: 1280,
        height: 720,
        framesPerSecond: 30
    }
};

3.3 统计监控类

class WebRTCStatsMonitor {
    constructor(pc, interval = 1000) {
        this.pc = pc;
        this.interval = interval;
        this.previousStats = null;
        this.callbacks = [];
    }
    
    start() {
        this.timer = setInterval(() => this.collect(), this.interval);
    }
    
    stop() {
        clearInterval(this.timer);
    }
    
    onStats(callback) {
        this.callbacks.push(callback);
    }
    
    async collect() {
        const stats = await this.pc.getStats();
        const report = this.processStats(stats);
        
        this.callbacks.forEach(cb => cb(report));
        this.previousStats = stats;
    }
    
    processStats(stats) {
        const report = {
            timestamp: Date.now(),
            video: { send: {}, recv: {} },
            audio: { send: {}, recv: {} },
            connection: {}
        };
        
        stats.forEach(s => {
            if (s.type === 'outbound-rtp') {
                const target = s.kind === 'video' ? report.video.send : report.audio.send;
                target.bitrate = this.calculateBitrate(s, 'bytesSent');
                target.packetsSent = s.packetsSent;
                target.framesEncoded = s.framesEncoded;
                target.frameWidth = s.frameWidth;
                target.frameHeight = s.frameHeight;
                target.qualityLimitationReason = s.qualityLimitationReason;
            }
            
            if (s.type === 'inbound-rtp') {
                const target = s.kind === 'video' ? report.video.recv : report.audio.recv;
                target.bitrate = this.calculateBitrate(s, 'bytesReceived');
                target.packetsReceived = s.packetsReceived;
                target.packetsLost = s.packetsLost;
                target.jitter = s.jitter;
                target.framesDecoded = s.framesDecoded;
                target.framesDropped = s.framesDropped;
            }
            
            if (s.type === 'candidate-pair' && s.state === 'succeeded') {
                report.connection.rtt = s.currentRoundTripTime * 1000;
                report.connection.availableBandwidth = s.availableOutgoingBitrate;
            }
        });
        
        return report;
    }
    
    calculateBitrate(current, field) {
        if (!this.previousStats) return 0;
        
        let previous = null;
        this.previousStats.forEach(s => {
            if (s.id === current.id) previous = s;
        });
        
        if (!previous) return 0;
        
        const bytesDiff = current[field] - previous[field];
        const timeDiff = (current.timestamp - previous.timestamp) / 1000;
        
        return Math.round((bytesDiff * 8) / timeDiff);
    }
}

// 使用示例
const monitor = new WebRTCStatsMonitor(pc);
monitor.onStats(report => {
    console.log('Video send bitrate:', report.video.send.bitrate);
    console.log('Video recv bitrate:', report.video.recv.bitrate);
    console.log('RTT:', report.connection.rtt);
});
monitor.start();

3.4 丢包率计算

class PacketLossCalculator {
    constructor() {
        this.history = [];
    }
    
    update(packetsReceived, packetsLost) {
        this.history.push({
            timestamp: Date.now(),
            received: packetsReceived,
            lost: packetsLost
        });
        
        // 保留最近 60 秒
        const cutoff = Date.now() - 60000;
        this.history = this.history.filter(h => h.timestamp > cutoff);
    }
    
    // 计算瞬时丢包率
    getInstantLossRate() {
        if (this.history.length < 2) return 0;
        
        const latest = this.history[this.history.length - 1];
        const previous = this.history[this.history.length - 2];
        
        const receivedDiff = latest.received - previous.received;
        const lostDiff = latest.lost - previous.lost;
        
        if (receivedDiff + lostDiff === 0) return 0;
        return lostDiff / (receivedDiff + lostDiff);
    }
    
    // 计算平均丢包率
    getAverageLossRate() {
        if (this.history.length < 2) return 0;
        
        const first = this.history[0];
        const last = this.history[this.history.length - 1];
        
        const totalReceived = last.received - first.received;
        const totalLost = last.lost - first.lost;
        
        if (totalReceived + totalLost === 0) return 0;
        return totalLost / (totalReceived + totalLost);
    }
}

4. 抓包分析

4.1 Wireshark 配置

Wireshark 抓取 WebRTC 流量:

1. 捕获过滤器
   udp port 3478 or udp portrange 10000-60000

2. 显示过滤器
   stun || dtls || rtp || rtcp

3. 解码设置
   - 右键 UDP 包 -> Decode As -> RTP
   - 或设置 RTP 端口范围

4.2 STUN 包分析

STUN Binding Request:

Frame: 96 bytes
    STUN Message Type: Binding Request (0x0001)
    Message Length: 68
    Message Cookie: 0x2112a442
    Message Transaction ID: xxx
    Attributes:
        USERNAME: user:pass
        ICE-CONTROLLING: xxx
        PRIORITY: xxx
        MESSAGE-INTEGRITY: xxx
        FINGERPRINT: xxx

分析要点:
- 检查 USERNAME 格式
- 检查 MESSAGE-INTEGRITY
- 观察请求/响应配对

4.3 DTLS 握手分析

DTLS 握手流程:

1. ClientHello
   - 版本: DTLS 1.2
   - 随机数
   - 密码套件列表
   - 扩展 (use_srtp)

2. HelloVerifyRequest
   - Cookie

3. ClientHello (带 Cookie)

4. ServerHello
   - 选择的密码套件
   - 选择的 SRTP 配置

5. Certificate
   - 服务器证书

6. ServerKeyExchange
   - ECDHE 参数

7. CertificateRequest

8. ServerHelloDone

9. Certificate (客户端)

10. ClientKeyExchange

11. CertificateVerify

12. ChangeCipherSpec

13. Finished

分析要点:
- 检查证书是否正确
- 检查密码套件协商
- 检查 SRTP 配置

4.4 RTP 包分析

RTP 包结构:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X|  CC   |M|     PT      |       sequence number         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           timestamp                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           synchronization source (SSRC) identifier            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Wireshark 显示:
Real-Time Transport Protocol
    Version: 2
    Padding: False
    Extension: True
    Contributing source identifiers count: 0
    Marker: False
    Payload type: VP8 (96)
    Sequence number: 12345
    Timestamp: 1234567890
    Synchronization Source identifier: 0x12345678

分析要点:
- 序列号是否连续
- 时间戳增量是否正确
- Marker 位是否正确标记帧边界

4.5 RTCP 包分析

RTCP Sender Report:

Sender Report
    Version: 2
    Padding: False
    Reception report count: 1
    Packet type: Sender Report (200)
    Length: 12
    Sender SSRC: 0x12345678
    NTP timestamp: xxx
    RTP timestamp: xxx
    Sender's packet count: 1000
    Sender's octet count: 500000
    
    Source 1
        SSRC: 0x87654321
        Fraction lost: 0
        Cumulative lost: 5
        Extended highest sequence number: 12345
        Interarrival jitter: 100
        Last SR timestamp: xxx
        Delay since last SR: xxx

分析要点:
- 丢包数 (Cumulative lost)
- 丢包率 (Fraction lost)
- 抖动 (Jitter)
- RTT 计算 (通过 LSR 和 DLSR)

5. 常见问题诊断

5.1 连接失败

// 诊断连接失败
function diagnoseConnectionFailure(pc) {
    // 1. 检查 ICE 状态
    console.log('ICE Connection State:', pc.iceConnectionState);
    console.log('ICE Gathering State:', pc.iceGatheringState);
    
    // 2. 检查候选收集
    pc.getStats().then(stats => {
        let localCandidates = [];
        let remoteCandidates = [];
        
        stats.forEach(report => {
            if (report.type === 'local-candidate') {
                localCandidates.push({
                    type: report.candidateType,
                    protocol: report.protocol,
                    address: report.address
                });
            }
            if (report.type === 'remote-candidate') {
                remoteCandidates.push({
                    type: report.candidateType,
                    protocol: report.protocol,
                    address: report.address
                });
            }
        });
        
        console.log('Local candidates:', localCandidates);
        console.log('Remote candidates:', remoteCandidates);
        
        // 3. 检查是否有 relay 候选
        const hasRelay = localCandidates.some(c => c.type === 'relay');
        if (!hasRelay) {
            console.warn('No relay candidates - TURN may not be configured');
        }
    });
}

// 常见原因:
// 1. ICE 服务器配置错误
// 2. 防火墙阻止 UDP
// 3. 对称 NAT 无法穿透
// 4. TURN 服务器不可用

5.2 视频卡顿

// 诊断视频卡顿
async function diagnoseVideoStutter(pc) {
    const stats = await pc.getStats();
    
    stats.forEach(report => {
        if (report.type === 'inbound-rtp' && report.kind === 'video') {
            // 检查丢包
            const lossRate = report.packetsLost / 
                (report.packetsReceived + report.packetsLost);
            console.log('Packet loss rate:', (lossRate * 100).toFixed(2) + '%');
            
            // 检查丢帧
            const dropRate = report.framesDropped / 
                (report.framesDecoded + report.framesDropped);
            console.log('Frame drop rate:', (dropRate * 100).toFixed(2) + '%');
            
            // 检查抖动
            console.log('Jitter:', report.jitter * 1000, 'ms');
            
            // 检查解码延迟
            if (report.totalDecodeTime && report.framesDecoded) {
                const avgDecodeTime = report.totalDecodeTime / report.framesDecoded;
                console.log('Avg decode time:', avgDecodeTime * 1000, 'ms');
            }
        }
        
        if (report.type === 'candidate-pair' && report.state === 'succeeded') {
            console.log('RTT:', report.currentRoundTripTime * 1000, 'ms');
            console.log('Available bandwidth:', 
                report.availableOutgoingBitrate / 1000, 'kbps');
        }
    });
}

// 卡顿原因:
// 1. 网络丢包 > 5%
// 2. 抖动 > 50ms
// 3. 带宽不足
// 4. CPU 解码能力不足

5.3 音频问题

// 诊断音频问题
async function diagnoseAudioIssues(pc) {
    const stats = await pc.getStats();
    
    stats.forEach(report => {
        if (report.type === 'inbound-rtp' && report.kind === 'audio') {
            console.log('Audio packets received:', report.packetsReceived);
            console.log('Audio packets lost:', report.packetsLost);
            console.log('Audio jitter:', report.jitter * 1000, 'ms');
            
            // 检查音频级别
            if (report.audioLevel !== undefined) {
                console.log('Audio level:', report.audioLevel);
                if (report.audioLevel < 0.01) {
                    console.warn('Audio level very low - possible mute or no input');
                }
            }
        }
        
        if (report.type === 'media-source' && report.kind === 'audio') {
            console.log('Audio source level:', report.audioLevel);
            console.log('Echo return loss:', report.echoReturnLoss);
        }
    });
}

// 音频问题原因:
// 1. 麦克风未授权或静音
// 2. 回声消除失效
// 3. 音频丢包
// 4. 采样率不匹配

6. 监控系统搭建

6.1 数据收集

class WebRTCMetricsCollector {
    constructor(pc, endpoint) {
        this.pc = pc;
        this.endpoint = endpoint;
        this.sessionId = this.generateSessionId();
    }
    
    generateSessionId() {
        return Math.random().toString(36).substring(2, 15);
    }
    
    async collect() {
        const stats = await this.pc.getStats();
        const metrics = {
            sessionId: this.sessionId,
            timestamp: Date.now(),
            video: {},
            audio: {},
            connection: {}
        };
        
        stats.forEach(report => {
            this.processReport(report, metrics);
        });
        
        return metrics;
    }
    
    processReport(report, metrics) {
        switch (report.type) {
            case 'outbound-rtp':
                if (report.kind === 'video') {
                    metrics.video.send = {
                        bitrate: this.calculateBitrate(report),
                        frameRate: report.framesPerSecond,
                        width: report.frameWidth,
                        height: report.frameHeight,
                        qualityLimitation: report.qualityLimitationReason
                    };
                }
                break;
                
            case 'inbound-rtp':
                if (report.kind === 'video') {
                    metrics.video.recv = {
                        bitrate: this.calculateBitrate(report),
                        packetsLost: report.packetsLost,
                        jitter: report.jitter,
                        framesDropped: report.framesDropped
                    };
                }
                break;
                
            case 'candidate-pair':
                if (report.state === 'succeeded') {
                    metrics.connection = {
                        rtt: report.currentRoundTripTime,
                        availableBandwidth: report.availableOutgoingBitrate,
                        localType: report.localCandidateType,
                        remoteType: report.remoteCandidateType
                    };
                }
                break;
        }
    }
    
    async send(metrics) {
        try {
            await fetch(this.endpoint, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(metrics)
            });
        } catch (error) {
            console.error('Failed to send metrics:', error);
        }
    }
    
    start(interval = 5000) {
        this.timer = setInterval(async () => {
            const metrics = await this.collect();
            await this.send(metrics);
        }, interval);
    }
    
    stop() {
        clearInterval(this.timer);
    }
}

6.2 服务端存储

// metrics-server.js
const express = require('express');
const app = express();

app.use(express.json());

// 存储指标 (实际应使用时序数据库如 InfluxDB)
const metricsStore = [];

app.post('/metrics', (req, res) => {
    const metrics = req.body;
    metrics.receivedAt = Date.now();
    metricsStore.push(metrics);
    
    // 保留最近 1 小时
    const cutoff = Date.now() - 3600000;
    while (metricsStore.length > 0 && metricsStore[0].receivedAt < cutoff) {
        metricsStore.shift();
    }
    
    res.json({ status: 'ok' });
});

// 查询接口
app.get('/metrics/:sessionId', (req, res) => {
    const { sessionId } = req.params;
    const sessionMetrics = metricsStore.filter(m => m.sessionId === sessionId);
    res.json(sessionMetrics);
});

// 聚合统计
app.get('/stats/summary', (req, res) => {
    const summary = {
        totalSessions: new Set(metricsStore.map(m => m.sessionId)).size,
        avgRtt: 0,
        avgPacketLoss: 0,
        avgBitrate: 0
    };
    
    // 计算平均值...
    
    res.json(summary);
});

app.listen(3001);

7. 总结

7.1 调试工具对比

工具优点缺点适用场景
webrtc-internals无需代码,实时仅限浏览器快速诊断
RTCStats API可编程,灵活需要开发监控系统
Wireshark协议级分析学习曲线高深度调试

7.2 关键指标阈值

健康指标参考值:

丢包率: < 2%
抖动: < 30ms
RTT: < 150ms
帧率: >= 24fps
解码延迟: < 50ms

7.3 下一篇预告

在下一篇文章中,我们将探讨 WebRTC 质量优化策略。


参考资料

  1. RTCStats Identifiers
  2. Chrome webrtc-internals
  3. Wireshark WebRTC

Logo

邀请您加入社区

更多推荐