RTCPeerConnection-mandatory-getStats.https.html (8235B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <meta name="timeout" content="long"> 4 <title>Mandatory-to-implement stats compliance (a subset of webrtc-stats)</title> 5 <script src=/resources/testharness.js></script> 6 <script src=/resources/testharnessreport.js></script> 7 <script src="RTCPeerConnection-helper.js"></script> 8 <script> 9 'use strict'; 10 11 // From https://w3c.github.io/webrtc-pc/#mandatory-to-implement-stats 12 13 const mandatory = { 14 RTCRtpStreamStats: [ 15 "ssrc", 16 "kind", 17 "transportId", 18 "codecId", 19 ], 20 RTCReceivedRtpStreamStats: [ 21 "packetsReceived", 22 "packetsLost", 23 "jitter", 24 ], 25 RTCInboundRtpStreamStats: [ 26 "trackIdentifier", 27 "remoteId", 28 "framesDecoded", 29 "framesDropped", 30 "nackCount", 31 "framesReceived", 32 "bytesReceived", 33 "totalAudioEnergy", 34 "totalSamplesDuration", 35 "packetsDiscarded", 36 ], 37 RTCRemoteInboundRtpStreamStats: [ 38 "localId", 39 "roundTripTime", 40 ], 41 RTCSentRtpStreamStats: [ 42 "packetsSent", 43 "bytesSent" 44 ], 45 RTCOutboundRtpStreamStats: [ 46 "remoteId", 47 "framesEncoded", 48 "nackCount", 49 "framesSent" 50 ], 51 RTCRemoteOutboundRtpStreamStats: [ 52 "localId", 53 "remoteTimestamp", 54 ], 55 RTCPeerConnectionStats: [ 56 "dataChannelsOpened", 57 "dataChannelsClosed", 58 ], 59 RTCDataChannelStats: [ 60 "label", 61 "protocol", 62 "dataChannelIdentifier", 63 "state", 64 "messagesSent", 65 "bytesSent", 66 "messagesReceived", 67 "bytesReceived", 68 ], 69 RTCMediaSourceStats: [ 70 "trackIdentifier", 71 "kind" 72 ], 73 RTCAudioSourceStats: [ 74 "totalAudioEnergy", 75 "totalSamplesDuration" 76 ], 77 RTCVideoSourceStats: [ 78 "width", 79 "height", 80 "framesPerSecond" 81 ], 82 RTCCodecStats: [ 83 "payloadType", 84 /* codecType is part of MTI but is not systematically set 85 per https://www.w3.org/TR/webrtc-stats/#dom-rtccodecstats-codectype 86 If the dictionary member is not present, it means that 87 this media format can be both encoded and decoded. */ 88 // "codecType", 89 "mimeType", 90 "clockRate", 91 "channels", 92 "sdpFmtpLine", 93 ], 94 RTCTransportStats: [ 95 "bytesSent", 96 "bytesReceived", 97 "selectedCandidatePairId", 98 "localCertificateId", 99 "remoteCertificateId", 100 ], 101 RTCIceCandidatePairStats: [ 102 "transportId", 103 "localCandidateId", 104 "remoteCandidateId", 105 "state", 106 "nominated", 107 "bytesSent", 108 "bytesReceived", 109 "totalRoundTripTime", 110 "responsesReceived", 111 "currentRoundTripTime" 112 ], 113 RTCIceCandidateStats: [ 114 "address", 115 "port", 116 "protocol", 117 "candidateType", 118 /* url requires a STUN or TURN server so is not testable. */ 119 // "url", 120 ], 121 RTCCertificateStats: [ 122 "fingerprint", 123 "fingerprintAlgorithm", 124 "base64Certificate", 125 /* issuerCertificateId is part of MTI but is not systematically set 126 per https://www.w3.org/TR/webrtc-stats/#dom-rtccertificatestats-issuercertificateid 127 If the current certificate is at the end of the chain 128 (i.e. a self-signed certificate), this will not be set. */ 129 // "issuerCertificateId", 130 ], 131 }; 132 133 // From https://w3c.github.io/webrtc-stats/webrtc-stats.html#rtcstatstype-str* 134 135 const dictionaryNames = { 136 "codec": "RTCCodecStats", 137 "inbound-rtp": "RTCInboundRtpStreamStats", 138 "outbound-rtp": "RTCOutboundRtpStreamStats", 139 "remote-inbound-rtp": "RTCRemoteInboundRtpStreamStats", 140 "remote-outbound-rtp": "RTCRemoteOutboundRtpStreamStats", 141 "csrc": "RTCRtpContributingSourceStats", 142 "peer-connection": "RTCPeerConnectionStats", 143 "data-channel": "RTCDataChannelStats", 144 "media-source": { 145 audio: "RTCAudioSourceStats", 146 video: "RTCVideoSourceStats" 147 }, 148 "track": { 149 video: "RTCSenderVideoTrackAttachmentStats", 150 audio: "RTCSenderAudioTrackAttachmentStats" 151 }, 152 "sender": { 153 audio: "RTCAudioSenderStats", 154 video: "RTCVideoSenderStats" 155 }, 156 "receiver": { 157 audio: "RTCAudioReceiverStats", 158 video: "RTCVideoReceiverStats", 159 }, 160 "transport": "RTCTransportStats", 161 "candidate-pair": "RTCIceCandidatePairStats", 162 "local-candidate": "RTCIceCandidateStats", 163 "remote-candidate": "RTCIceCandidateStats", 164 "certificate": "RTCCertificateStats", 165 }; 166 167 // From https://w3c.github.io/webrtc-stats/webrtc-stats.html (webidl) 168 169 const parents = { 170 RTCVideoSourceStats: "RTCMediaSourceStats", 171 RTCAudioSourceStats: "RTCMediaSourceStats", 172 RTCReceivedRtpStreamStats: "RTCRtpStreamStats", 173 RTCInboundRtpStreamStats: "RTCReceivedRtpStreamStats", 174 RTCRemoteInboundRtpStreamStats: "RTCReceivedRtpStreamStats", 175 RTCSentRtpStreamStats: "RTCRtpStreamStats", 176 RTCOutboundRtpStreamStats: "RTCSentRtpStreamStats", 177 RTCRemoteOutboundRtpStreamStats : "RTCSentRtpStreamStats", 178 }; 179 180 const remaining = JSON.parse(JSON.stringify(mandatory)); 181 for (const dictName in remaining) { 182 remaining[dictName] = new Set(remaining[dictName]); 183 } 184 185 async function getAllStats(t, pc) { 186 // Try to obtain as many stats as possible, waiting up to 20 seconds for 187 // roundTripTime of RTCRemoteInboundRtpStreamStats and 188 // remoteTimestamp of RTCRemoteOutboundRtpStreamStats which can take 189 // several RTCP messages to calculate. 190 let stats; 191 let remoteInboundFound = false; 192 let remoteOutboundFound = false; 193 for (let i = 0; i < 20; i++) { 194 stats = await pc.getStats(); 195 const values = [...stats.values()]; 196 const [remoteInboundAudio, remoteInboundVideo] = ["audio", "video"].map( 197 kind => values.find(s => 198 s.type == "remote-inbound-rtp" && s.kind == kind)); 199 if (remoteInboundAudio && "roundTripTime" in remoteInboundAudio && 200 remoteInboundVideo && "roundTripTime" in remoteInboundVideo) { 201 remoteInboundFound = true; 202 } 203 const [remoteOutboundAudio, remoteOutboundVideo] = ["audio", "video"].map( 204 kind => values.find(s => 205 s.type == "remote-outbound-rtp" && s.kind == kind)); 206 if (remoteOutboundAudio && "remoteTimestamp" in remoteOutboundAudio && 207 remoteOutboundVideo && "remoteTimestamp" in remoteOutboundVideo) { 208 remoteOutboundFound = true; 209 } 210 if (remoteInboundFound && remoteOutboundFound) { 211 return stats; 212 } 213 await new Promise(r => t.step_timeout(r, 1000)); 214 } 215 return stats; 216 } 217 218 promise_test(async t => { 219 const pc1 = new RTCPeerConnection(); 220 t.add_cleanup(() => pc1.close()); 221 const pc2 = new RTCPeerConnection(); 222 t.add_cleanup(() => pc2.close()); 223 224 const dc1 = pc1.createDataChannel("dummy", {negotiated: true, id: 0}); 225 const dc2 = pc2.createDataChannel("dummy", {negotiated: true, id: 0}); 226 227 const stream = await getNoiseStream({video: true, audio:true}); 228 for (const track of stream.getTracks()) { 229 pc1.addTrack(track, stream); 230 pc2.addTrack(track, stream); 231 t.add_cleanup(() => track.stop()); 232 } 233 exchangeIceCandidates(pc1, pc2); 234 await exchangeOfferAnswer(pc1, pc2); 235 const stats = await getAllStats(t, pc1); 236 237 // The focus of this test is not API correctness, but rather to provide an 238 // accessible metric of implementation progress by dictionary member. We count 239 // whether we've seen each dictionary's mandatory members in getStats(). 240 241 test(t => { 242 for (const stat of stats.values()) { 243 let dictName = dictionaryNames[stat.type]; 244 if (!dictName) continue; 245 if (typeof dictName == "object") { 246 dictName = dictName[stat.kind]; 247 } 248 249 assert_equals(typeof dictName, "string", "Test error. String."); 250 if (dictName && mandatory[dictName]) { 251 do { 252 const memberNames = mandatory[dictName]; 253 const remainingNames = remaining[dictName]; 254 assert_true(memberNames.length > 0, "Test error. Parent not found."); 255 for (const memberName of memberNames) { 256 if (memberName in stat) { 257 assert_not_equals(stat[memberName], undefined, "Not undefined"); 258 remainingNames.delete(memberName); 259 } 260 } 261 dictName = parents[dictName]; 262 } while (dictName); 263 } 264 } 265 }, "Validating stats"); 266 267 for (const dictName in mandatory) { 268 for (const memberName of mandatory[dictName]) { 269 test(t => { 270 assert_true(!remaining[dictName].has(memberName), 271 `Is ${memberName} present`); 272 }, `${dictName}'s ${memberName}`); 273 } 274 } 275 }, 'getStats succeeds'); 276 277 </script>