前端 WebRTC Stats 各浏览器数据及适配
1. WebRTC 及 Stats 简介
WebRTC(Web Real-Time Communication) 是一项让浏览器和移动设备通过简单的API进行实时通信的技术。它广泛应用于视频通话、语音聊天和数据传输等实时应用场景。为了确保实时通信的质量,开发者通常会使用 WebRTC 提供的统计数据(Stats)来监控、分析和优化连接的性能。
WebRTC Stats 是 WebRTC 提供的一组详细的指标数据,可以帮助我们评估实时通信的质量。这些统计信息包括音视频的码率、丢包率、延迟、抖动、帧率等,可以为我们提供全面的网络连接质量和媒体传输状况的数据支持。
浏览器通过不同的 API 暴露这些统计数据,因此不同浏览器的 WebRTC Stats 实现有一些差异,特别是不同版本的浏览器在统计字段上也会有所更新。因此,了解各浏览器的 Stats 格式并进行适配,是开发高质量 WebRTC 应用的重要步骤。
2. 各个浏览器的 Stats 原始数据
2.1 Chrome
Chrome 的 WebRTC 实现最为完整,也是许多 WebRTC 开发的主要调试工具。其 getStats() API 提供了丰富的统计数据。以下是 Chrome 浏览器的一些常见 Stats 数据字段:
-
inbound-rtp: 入站 RTP 流的统计数据,包括丢包率、延迟、抖动等。
-
outbound-rtp: 出站 RTP 流的统计数据,通常用于分析发送数据的质量。
-
candidate-pair: ICE 候选对信息,用于分析 WebRTC 连接的候选路径及其优劣。
-
remote-candidate/local-candidate: 远程/本地 ICE 候选者的信息。
新版 Chrome (v96+):
随着 Chrome 的版本更新,WebRTC Stats 数据的结构有所调整。新版 Chrome 中对部分字段进行了重命名,增加了更多的网络统计信息,尤其是网络状况分析(例如增加了丢包率预测功能)。主要变化包括:
-
引入了 totalRoundTripTime 用来更加精准地衡量 RTCP 报文往返时间。
-
新增了关于 SVC(Scalable Video Coding)相关的统计项。
- 新版本 Stats 数据示例(注:数据来自浏览器版本 Mac 129.0.6668.71)
[
{
"id": "AP",
"timestamp": 1727401120702.777,
"type": "media-playout",
"kind": "audio",
"synthesizedSamplesDuration": 0,
"synthesizedSamplesEvents": 0,
"totalPlayoutDelay": 8073.20178,
"totalSamplesCount": 367353,
"totalSamplesDuration": 8.33
},
{
"id": "CF43:96:FE:BD:BF:A4:08:A3:BA:0A:B0:BB:DC:9A:EF:B1:43:F3:4E:40",
"timestamp": 1727401120702.777,
"type": "certificate",
"base64Certificate": "MIIBnzCCAQigAwIBAgIEH20M8zANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDDAl3c3J0Yy5uZXQwHhcNMjQwOTI2MjAyNTQ2WhcNMjUwOTI2MjAyNTQ2WjAUMRIwEAYDVQQDDAl3c3J0Yy5uZXQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMpipgS8lMcklbmrt3vhhnX+j1P76exTnjWukH/tXNeHKZ20+8C2DjIzRkrWTulvBGi987vt9Q1UyFeUAs9pG5T7WN1UrElBZw9L5NecsbLTG5igxu3ynPE5MKSOOXSB4t3yo3EXws+pDz67Isq6vVweB+ICSB1WVT/YxpkHUqDdAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAYczbDpZXjvlr5B+90KYRuHnrdofFXSQHxMQ3uy14AvYNBRIW3bFf3Y1GtTITaqnlGaYRm16PlltySKE4gQxdP0ahqixYjCxVrSTQ2QwYXfcGCuOblGEUXq2U+jMzo7pt/WkeWRnbzknMEuiDZ+PGY8+mD4XNsF+eP9D6phbxIa0=",
"fingerprint": "43:96:FE:BD:BF:A4:08:A3:BA:0A:B0:BB:DC:9A:EF:B1:43:F3:4E:40",
"fingerprintAlgorithm": "sha-1"
},
{
"id": "CFCE:02:0C:4F:12:A3:53:2B:6A:B6:F4:B8:78:C7:DC:D7:9C:B3:A7:C1:3E:C2:2B:48:BD:94:2B:9C:DA:52:BC:35",
"timestamp": 1727401120702.777,
"type": "certificate",
"base64Certificate": "MIIBFjCBvKADAgECAghk09klkHIPojAKBggqhkjOPQQDAjARMQ8wDQYDVQQDDAZXZWJSVEMwHhcNMjQwOTI2MDEzODM0WhcNMjQxMDI3MDEzODM0WjARMQ8wDQYDVQQDDAZXZWJSVEMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARDXeUiumrGnDDCh7Fo3mUcnTLig/acbA6Obn1ATt55Ipb3AcgCK63mu14VeVq/4TntjKNN8dMrSkagcMJZGV4/MAoGCCqGSM49BAMCA0kAMEYCIQDyMMuS+Agp6WWuwEFy+9z7hSqbcvuwBgXbJZCC4jagJAIhAOybx9a5zzHZxGJDsZSd4+ttDZbddjwAm164x9Ink1Jb",
"fingerprint": "CE:02:0C:4F:12:A3:53:2B:6A:B6:F4:B8:78:C7:DC:D7:9C:B3:A7:C1:3E:C2:2B:48:BD:94:2B:9C:DA:52:BC:35",
"fingerprintAlgorithm": "sha-256"
},
{
"id": "CIT01_108_level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
"timestamp": 1727401120702.777,
"type": "codec",
"clockRate": 90000,
"mimeType": "video/H264",
"payloadType": 108,
"sdpFmtpLine": "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
"transportId": "T01"
},
{
"id": "CIT01_111_minptime=10;stereo=1;useinbandfec=1",
"timestamp": 1727401120702.777,
"type": "codec",
"channels": 2,
"clockRate": 48000,
"mimeType": "audio/opus",
"payloadType": 111,
"sdpFmtpLine": "minptime=10;stereo=1;useinbandfec=1",
"transportId": "T01"
},
{
"id": "CPVgNtU6tj_nrA8FDQ7",
"timestamp": 1727401120702.777,
"type": "candidate-pair",
"availableIncomingBitrate": 883696,
"availableOutgoingBitrate": 300000,
"bytesDiscardedOnSend": 0,
"bytesReceived": 1080356,
"bytesSent": 1572,
"consentRequestsSent": 5,
"currentRoundTripTime": 0.017,
"lastPacketReceivedTimestamp": 1727401120692,
"lastPacketSentTimestamp": 1727401120701,
"localCandidateId": "IVgNtU6tj",
"nominated": true,
"packetsDiscardedOnSend": 0,
"packetsReceived": 1347,
"packetsSent": 16,
"priority": 7926369428764100000,
"remoteCandidateId": "InrA8FDQ7",
"requestsReceived": 0,
"requestsSent": 6,
"responsesReceived": 6,
"responsesSent": 0,
"state": "succeeded",
"totalRoundTripTime": 0.101,
"transportId": "T01",
"writable": true
},
{
"id": "I6lTzyVHX",
"timestamp": 1727401120702.777,
"type": "local-candidate",
"address": "",
"candidateType": "host",
"foundation": "2032006369",
"ip": "",
"isRemote": false,
"networkType": "unknown",
"port": 61297,
"priority": 2113937151,
"protocol": "udp",
"transportId": "T01",
"usernameFragment": "ZIbK"
},
{
"id": "IDhnF7mGP",
"timestamp": 1727401120702.777,
"type": "local-candidate",
"address": "",
"candidateType": "host",
"foundation": "871967914",
"ip": "",
"isRemote": false,
"networkType": "unknown",
"port": 56782,
"priority": 2113929727,
"protocol": "udp",
"transportId": "T01",
"usernameFragment": "ZIbK"
},
{
"id": "IT01A371045484",
"timestamp": 1727401120702.777,
"type": "inbound-rtp",
"codecId": "CIT01_111_minptime=10;stereo=1;useinbandfec=1",
"kind": "audio",
"mediaType": "audio",
"ssrc": 371045484,
"transportId": "T01",
"jitter": 0.02,
"packetsLost": 17,
"packetsReceived": 424,
"audioLevel": 0.20187994018372143,
"bytesReceived": 110568,
"concealedSamples": 13941,
"concealmentEvents": 3,
"fecPacketsDiscarded": 0,
"fecPacketsReceived": 0,
"headerBytesReceived": 5088,
"insertedSamplesForDeceleration": 0,
"jitterBufferDelay": 588652.8,
"jitterBufferEmittedCount": 327360,
"jitterBufferMinimumDelay": 36806.4,
"jitterBufferTargetDelay": 36883.2,
"lastPacketReceivedTimestamp": 1727401120692.054,
"mid": "0",
"packetsDiscarded": 0,
"playoutId": "AP",
"removedSamplesForAcceleration": 37876,
"silentConcealedSamples": 11880,
"totalAudioEnergy": 0.13451713600243226,
"totalProcessingDelay": 587198.44416,
"totalSamplesDuration": 6.2799999999999105,
"totalSamplesReceived": 301440,
"trackIdentifier": "5ce99502-0cf8-450a-8b31-4ac8379cb394"
},
{
"id": "IT01V730841480",
"timestamp": 1727401120702.777,
"type": "inbound-rtp",
"codecId": "CIT01_108_level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
"kind": "video",
"mediaType": "video",
"ssrc": 730841480,
"transportId": "T01",
"jitter": 0.021,
"packetsLost": 0,
"packetsReceived": 921,
"bytesReceived": 938669,
"firCount": 0,
"frameHeight": 720,
"frameWidth": 1280,
"framesAssembledFromMultiplePackets": 173,
"framesDecoded": 188,
"framesDropped": 0,
"framesPerSecond": 21,
"framesReceived": 188,
"freezeCount": 0,
"headerBytesReceived": 11052,
"jitterBufferDelay": 1.5556219999999998,
"jitterBufferEmittedCount": 188,
"jitterBufferMinimumDelay": 8.519053999999999,
"jitterBufferTargetDelay": 8.519053999999999,
"keyFramesDecoded": 5,
"lastPacketReceivedTimestamp": 1727401120690.82,
"mid": "1",
"nackCount": 3,
"pauseCount": 0,
"pliCount": 0,
"totalAssemblyTime": 0.175033,
"totalDecodeTime": 0.5236,
"totalFreezesDuration": 0,
"totalInterFrameDelay": 5.996,
"totalPausesDuration": 0,
"totalProcessingDelay": 2.093067,
"totalSquaredInterFrameDelay": 0.41580599999999973,
"trackIdentifier": "14a84e86-40c4-4db7-80c0-9d979574de61"
},
{
"id": "IVgNtU6tj",
"timestamp": 1727401120702.777,
"type": "local-candidate",
"address": "",
"candidateType": "prflx",
"foundation": "1536824329",
"ip": "",
"isRemote": false,
"networkType": "unknown",
"port": 61297,
"priority": 1845501695,
"protocol": "udp",
"transportId": "T01",
"usernameFragment": "ZIbK"
},
{
"id": "InrA8FDQ7",
"timestamp": 1727401120702.777,
"type": "remote-candidate",
"address": "61.182.142.106",
"candidateType": "host",
"foundation": "1",
"ip": "61.182.142.106",
"isRemote": true,
"port": 16000,
"priority": 2013266431,
"protocol": "udp",
"transportId": "T01",
"usernameFragment": "3D0D_80521_66F60C9A_712_175"
},
{
"id": "P",
"timestamp": 1727401120702.777,
"type": "peer-connection",
"dataChannelsClosed": 0,
"dataChannelsOpened": 0
},
{
"id": "T01",
"timestamp": 1727401120702.777,
"type": "transport",
"bytesReceived": 1080356,
"bytesSent": 1572,
"dtlsCipher": "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"dtlsRole": "client",
"dtlsState": "connected",
"iceLocalUsernameFragment": "ZIbK",
"iceRole": "controlling",
"iceState": "connected",
"localCertificateId": "CFCE:02:0C:4F:12:A3:53:2B:6A:B6:F4:B8:78:C7:DC:D7:9C:B3:A7:C1:3E:C2:2B:48:BD:94:2B:9C:DA:52:BC:35",
"packetsReceived": 1347,
"packetsSent": 16,
"remoteCertificateId": "CF43:96:FE:BD:BF:A4:08:A3:BA:0A:B0:BB:DC:9A:EF:B1:43:F3:4E:40",
"selectedCandidatePairChanges": 1,
"selectedCandidatePairId": "CPVgNtU6tj_nrA8FDQ7",
"srtpCipher": "AES_CM_128_HMAC_SHA1_80",
"tlsVersion": "FEFD"
}
]
- 旧版本 Stats 数据示例(注:数据来自浏览器版本 Mac 66.0.3359.181)
[
{
"id": "RTCCertificate_26:A7:E3:EE:D1:6B:FD:DD:D2:AE:E9:42:7B:99:A6:12:59:73:FF:33",
"timestamp": 1727403044443.757,
"type": "certificate",
"fingerprint": "26:A7:E3:EE:D1:6B:FD:DD:D2:AE:E9:42:7B:99:A6:12:59:73:FF:33",
"fingerprintAlgorithm": "sha-1",
"base64Certificate": "MIIBnzCCAQigAwIBAgIEDpwPGzANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDDAl3c3J0Yy5uZXQwHhcNMjQwOTI2MjAyNTQ1WhcNMjUwOTI2MjAyNTQ1WjAUMRIwEAYDVQQDDAl3c3J0Yy5uZXQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMNMdScj78fxKYTb4IL6nsHlhjd6nTInBmNpPDi+Y6YMQ5Ak37VF+uffM2GC4w8B+66ZFLC5ghik+FuMJo1OOWtiYw4qosaIeX4MlAuZblRw0Rbl6YT4XC1RMvnsKytMGfHAH/o32nDle5xyyDhUwH7Z0dH7HMEVKtDjyqa7Me0NAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEADcjvHm/82INUWMSwaF9gGRyjKimiTC092TEldd6+t2/VRtJyDJS/6ufs93MnYupg4lgWmG8+8abISaScA3N1c3dKsH20easx/StjO5lof2a0eaR5u7x7ebyh+koa7smVjlHRSmdT4/XKYEiJgabcEal9oxqrZnrkovW14o2sqdA="
},
{
"id": "RTCCertificate_54:0D:09:82:96:09:A2:16:88:CD:1C:04:3A:60:28:24:F0:57:BF:90:17:47:DA:54:A3:5E:E1:16:45:14:49:71",
"timestamp": 1727403044443.757,
"type": "certificate",
"fingerprint": "54:0D:09:82:96:09:A2:16:88:CD:1C:04:3A:60:28:24:F0:57:BF:90:17:47:DA:54:A3:5E:E1:16:45:14:49:71",
"fingerprintAlgorithm": "sha-256",
"base64Certificate": "MIIBFTCBvaADAgECAgkA6k2QoqVLAGkwCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTI0MDkyNjAyMTAyNVoXDTI0MTAyNzAyMTAyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+tI+LMWxz9j57gu/55vNAuZWA5vXZJBykGu1Hxe+ZFUrcjPEDawhaO1NUrSKSdq7rShzbPqRyFMryXipGEBlRDAKBggqhkjOPQQDAgNHADBEAiB483H4mumr7QBSnU6HUdMZVTziN5tQ5pYvB3jJ57Oz4AIgIeqOy2UZ3tOzPEIrlDfVQtgzytKQfW+5AjWxoCtNWdE="
},
{
"id": "RTCCodec_audio_Inbound_0",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 0,
"mimeType": "audio/PCMU",
"clockRate": 8000
},
{
"id": "RTCCodec_audio_Inbound_103",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 103,
"mimeType": "audio/ISAC",
"clockRate": 16000
},
{
"id": "RTCCodec_audio_Inbound_104",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 104,
"mimeType": "audio/ISAC",
"clockRate": 32000
},
{
"id": "RTCCodec_audio_Inbound_110",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 110,
"mimeType": "audio/telephone-event",
"clockRate": 48000
},
{
"id": "RTCCodec_audio_Inbound_111",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 111,
"mimeType": "audio/opus",
"clockRate": 48000
},
{
"id": "RTCCodec_audio_Inbound_112",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 112,
"mimeType": "audio/telephone-event",
"clockRate": 32000
},
{
"id": "RTCCodec_audio_Inbound_113",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 113,
"mimeType": "audio/telephone-event",
"clockRate": 16000
},
{
"id": "RTCCodec_audio_Inbound_126",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 126,
"mimeType": "audio/telephone-event",
"clockRate": 8000
},
{
"id": "RTCCodec_audio_Inbound_8",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 8,
"mimeType": "audio/PCMA",
"clockRate": 8000
},
{
"id": "RTCCodec_audio_Inbound_9",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 9,
"mimeType": "audio/G722",
"clockRate": 8000
},
{
"id": "RTCCodec_audio_Outbound_111",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 111,
"mimeType": "audio/opus",
"clockRate": 48000
},
{
"id": "RTCCodec_video_Inbound_100",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 100,
"mimeType": "video/H264",
"clockRate": 90000
},
{
"id": "RTCCodec_video_Inbound_101",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 101,
"mimeType": "video/rtx",
"clockRate": 90000
},
{
"id": "RTCCodec_video_Inbound_102",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 102,
"mimeType": "video/H264",
"clockRate": 90000
},
{
"id": "RTCCodec_video_Inbound_123",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 123,
"mimeType": "video/rtx",
"clockRate": 90000
},
{
"id": "RTCCodec_video_Inbound_124",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 124,
"mimeType": "video/rtx",
"clockRate": 90000
},
{
"id": "RTCCodec_video_Inbound_125",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 125,
"mimeType": "video/ulpfec",
"clockRate": 90000
},
{
"id": "RTCCodec_video_Inbound_127",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 127,
"mimeType": "video/red",
"clockRate": 90000
},
{
"id": "RTCCodec_video_Inbound_96",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 96,
"mimeType": "video/VP8",
"clockRate": 90000
},
{
"id": "RTCCodec_video_Inbound_97",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 97,
"mimeType": "video/rtx",
"clockRate": 90000
},
{
"id": "RTCCodec_video_Inbound_98",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 98,
"mimeType": "video/VP9",
"clockRate": 90000
},
{
"id": "RTCCodec_video_Inbound_99",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 99,
"mimeType": "video/rtx",
"clockRate": 90000
},
{
"id": "RTCCodec_video_Outbound_102",
"timestamp": 1727403044443.757,
"type": "codec",
"payloadType": 102,
"mimeType": "video/H264",
"clockRate": 90000
},
{
"id": "RTCIceCandidatePair_AjuJCZ7+_IW154alq",
"timestamp": 1727403044443.757,
"type": "candidate-pair",
"transportId": "RTCTransport_audio_1",
"localCandidateId": "RTCIceCandidate_AjuJCZ7+",
"remoteCandidateId": "RTCIceCandidate_IW154alq",
"state": "succeeded",
"priority": 7926369428764100000,
"nominated": true,
"writable": true,
"bytesSent": 7282,
"bytesReceived": 2215418,
"totalRoundTripTime": 0.161,
"currentRoundTripTime": 0.013,
"availableOutgoingBitrate": 300000,
"availableIncomingBitrate": 1432511,
"requestsReceived": 0,
"requestsSent": 1,
"responsesReceived": 11,
"responsesSent": 0,
"consentRequestsSent": 10
},
{
"id": "RTCIceCandidate_AjuJCZ7+",
"timestamp": 1727403044443.757,
"type": "local-candidate",
"transportId": "RTCTransport_audio_1",
"isRemote": false,
"networkType": "unknown",
"ip": "220.249.52.114",
"port": 56338,
"protocol": "udp",
"candidateType": "prflx",
"priority": 1845501695,
"deleted": false
},
{
"id": "RTCIceCandidate_IW154alq",
"timestamp": 1727403044443.757,
"type": "remote-candidate",
"transportId": "RTCTransport_audio_1",
"isRemote": true,
"ip": "61.182.142.106",
"port": 16000,
"protocol": "udp",
"candidateType": "host",
"priority": 2013266431,
"deleted": false
},
{
"id": "RTCInboundRTPAudioStream_447985672",
"timestamp": 1727403044443.757,
"type": "inbound-rtp",
"ssrc": 447985672,
"isRemote": false,
"mediaType": "audio",
"trackId": "RTCMediaStreamTrack_receiver_11",
"transportId": "RTCTransport_audio_1",
"codecId": "RTCCodec_audio_Inbound_111",
"packetsReceived": 925,
"bytesReceived": 250173,
"packetsLost": 0,
"jitter": 0.023,
"fractionLost": 0
},
{
"id": "RTCInboundRTPVideoStream_751354887",
"timestamp": 1727403044443.757,
"type": "inbound-rtp",
"ssrc": 751354887,
"isRemote": false,
"mediaType": "video",
"trackId": "RTCMediaStreamTrack_receiver_12",
"transportId": "RTCTransport_audio_1",
"codecId": "RTCCodec_video_Inbound_102",
"firCount": 0,
"pliCount": 26,
"nackCount": 3,
"packetsReceived": 1891,
"bytesReceived": 1934115,
"packetsLost": 0,
"fractionLost": 0,
"framesDecoded": 39
},
{
"id": "RTCMediaStreamTrack_receiver_11",
"timestamp": 1727403044443.757,
"type": "track",
"trackIdentifier": "wsrtca0",
"remoteSource": true,
"ended": false,
"detached": false,
"kind": "audio",
"jitterBufferDelay": 50755.2,
"audioLevel": 0.16251106295968504,
"totalAudioEnergy": 0.43269636448123144,
"totalSamplesReceived": 888960,
"totalSamplesDuration": 18.580000000000105,
"concealedSamples": 5862,
"concealmentEvents": 7
},
{
"id": "RTCMediaStreamTrack_receiver_12",
"timestamp": 1727403044443.757,
"type": "track",
"trackIdentifier": "wsrtcv0",
"remoteSource": true,
"ended": false,
"detached": false,
"kind": "video",
"frameWidth": 1280,
"frameHeight": 720,
"framesReceived": 346,
"framesDecoded": 39,
"framesDropped": 85
},
{
"id": "RTCMediaStream_wsrtc",
"timestamp": 1727403044443.757,
"type": "stream",
"streamIdentifier": "wsrtc",
"trackIds": [
"RTCMediaStreamTrack_receiver_11",
"RTCMediaStreamTrack_receiver_12"
]
},
{
"id": "RTCPeerConnection",
"timestamp": 1727403044443.757,
"type": "peer-connection",
"dataChannelsOpened": 0,
"dataChannelsClosed": 0
},
{
"id": "RTCTransport_audio_1",
"timestamp": 1727403044443.757,
"type": "transport",
"bytesSent": 7282,
"bytesReceived": 2215418,
"dtlsState": "connected",
"selectedCandidatePairId": "RTCIceCandidatePair_AjuJCZ7+_IW154alq",
"localCertificateId": "RTCCertificate_54:0D:09:82:96:09:A2:16:88:CD:1C:04:3A:60:28:24:F0:57:BF:90:17:47:DA:54:A3:5E:E1:16:45:14:49:71",
"remoteCertificateId": "RTCCertificate_26:A7:E3:EE:D1:6B:FD:DD:D2:AE:E9:42:7B:99:A6:12:59:73:FF:33"
}
]
2.2 Firefox
Firefox 也提供了丰富的 WebRTC Stats,但其数据格式和 Chrome 略有不同。
-
inbound-rtp/outbound-rtp: 类似 Chrome 的字段,用于衡量媒体流的入站和出站质量。
-
iceCandidatePair: 提供连接的 ICE 候选对数据,帮助分析连接状况。
-
track: Firefox 特有的字段,用于获取媒体轨道的质量数据,如编码率和丢包率。
新版 Firefox (v91+):
Firefox 在新版本中开始支持更多的网络层面的统计数据,比如增加了关于 TURN 和 STUN 服务器性能的统计。另外,Firefox 的 Stats 在编码/解码方面也有更加详细的数据,如丢帧和延迟统计。
2.3 Safari
Safari 对 WebRTC 的支持较 Chrome 和 Firefox 来说起步较晚,但也已经提供了基本的 WebRTC Stats 数据。
-
inbound-rtp/outbound-rtp: 与其他浏览器类似,包含入站和出站流的基本数据。
-
candidate-pair: 提供候选对数据,帮助分析连接路径。
Safari 在 WebRTC Stats 上的数据相对简化,没有提供非常详细的统计数据,不过新版 Safari 正在逐步增加更多细节,尤其是在 iOS 平台的 Safari 上,对于 WebRTC 的支持还在逐渐改进。
新版 Safari (v15+):
新版 Safari 开始支持更多的连接状态数据以及媒体轨道的细节数据,如音视频轨道的帧率、码率等。这使得 Safari 在监控 WebRTC 连接质量上变得更加可靠。
2.4 Microsoft Edge
Microsoft Edge 使用 Chromium 内核,因此其 WebRTC Stats 与 Chrome 几乎完全一致。Edge 会继承 Chrome 的绝大多数统计字段,主要区别在于 Edge 的部分实现可能会有一些定制化的扩展字段。
3. 统一的适配
为了让 WebRTC 应用在所有浏览器中都能稳定运行,开发者需要对不同浏览器的 Stats 数据进行适配。主要的适配工作包括:
3.1 标准化 Stats 获取
通过 RTCPeerConnection.getStats() 方法,可以获取到不同浏览器的 WebRTC Stats 数据。为了确保跨浏览器的兼容性,我们可以通过一个统一的封装函数来适配各浏览器不同的字段格式。例如,可以创建一个通用的适配层,将所有浏览器的 Stats 数据标准化为统一的格式。
function getStandardizedStats(peerConnection) {
return new Promise((resolve, reject) => {
peerConnection.getStats(null).then(stats => {
let standardizedStats = {};
stats.forEach(report => {
// 针对不同类型的报告进行处理
if (report.type === 'inbound-rtp') {
standardizedStats.inbound = {
packetsLost: report.packetsLost,
jitter: report.jitter,
roundTripTime: report.roundTripTime || report.totalRoundTripTime,
bytesReceived: report.bytesReceived,
};
} else if (report.type === 'outbound-rtp') {
standardizedStats.outbound = {
bytesSent: report.bytesSent,
packetsSent: report.packetsSent,
};
}
});
resolve(standardizedStats);
}).catch(reject);
});
}
3.2 处理不同浏览器字段差异
为了应对不同浏览器在 WebRTC Stats 字段上的差异,可以创建一份映射表,将不同浏览器的字段映射为统一的字段。例如,Chrome 中的 totalRoundTripTime 在 Firefox 中可能叫做 roundTripTime,需要在适配层做相应的转换。
3.3 统一格式输出
无论是 Chrome、Firefox 还是 Safari,经过适配层处理后,所有浏览器的 WebRTC Stats 都可以输出成统一的格式。开发者只需基于这个标准化格式进行性能分析和问题排查,从而避免在不同浏览器中编写不同的处理逻辑。
4. 总结
WebRTC Stats 是监控实时通信质量的重要工具。各个浏览器在实现上存在一定差异,尤其是在不同版本的浏览器中,WebRTC Stats 的字段和功能有所不同。通过统一的适配层,开发者可以简化跨浏览器的兼容性处理,确保 WebRTC 应用在所有主流浏览器中都能顺利运行。
借助 WebRTC Stats,开发者可以实时监控视频、音频流的质量,优化用户体验,并快速定位网络连接或媒体传输中的问题。
评论区