supported-stats.https.html (8587B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <meta name="timeout" content="long"> 4 <title>Support for all stats defined in WebRTC Stats</title> 5 <script src=/resources/testharness.js></script> 6 <script src=/resources/testharnessreport.js></script> 7 <script src="../webrtc/RTCPeerConnection-helper.js"></script> 8 <script src="/resources/WebIDLParser.js"></script> 9 <script> 10 'use strict'; 11 12 // inspired from similar test for MTI stats in ../webrtc/RTCPeerConnection-mandatory-getStats.https.html 13 14 // From https://w3c.github.io/webrtc-stats/webrtc-stats.html#rtcstatstype-str* 15 const dictionaryNames = { 16 "codec": "RTCCodecStats", 17 "inbound-rtp": "RTCInboundRtpStreamStats", 18 "outbound-rtp": "RTCOutboundRtpStreamStats", 19 "remote-inbound-rtp": "RTCRemoteInboundRtpStreamStats", 20 "remote-outbound-rtp": "RTCRemoteOutboundRtpStreamStats", 21 "csrc": "RTCRtpContributingSourceStats", 22 "peer-connection": "RTCPeerConnectionStats", 23 "data-channel": "RTCDataChannelStats", 24 "media-source": { 25 audio: "RTCAudioSourceStats", 26 video: "RTCVideoSourceStats" 27 }, 28 "media-playout": "RTCAudioPlayoutStats", 29 "sender": { 30 audio: "RTCAudioSenderStats", 31 video: "RTCVideoSenderStats" 32 }, 33 "receiver": { 34 audio: "RTCAudioReceiverStats", 35 video: "RTCVideoReceiverStats", 36 }, 37 "transport": "RTCTransportStats", 38 "candidate-pair": "RTCIceCandidatePairStats", 39 "local-candidate": "RTCIceCandidateStats", 40 "remote-candidate": "RTCIceCandidateStats", 41 "certificate": "RTCCertificateStats", 42 }; 43 44 function isPropertyTestable(type, property) { 45 // List of properties which are not testable by this test. 46 // When adding something to this list, please explain why. 47 const untestablePropertiesByType = { 48 'candidate-pair': [ 49 'availableIncomingBitrate', // requires REMB, no TWCC. 50 ], 51 'certificate': [ 52 'issuerCertificateId', // we only use self-signed certificates. 53 ], 54 'local-candidate': [ 55 'url', // requires a STUN/TURN server. 56 'relayProtocol', // requires a TURN server. 57 'relatedAddress', // requires a STUN/TURN server. 58 'relatedPort', // requires a STUN/TURN server. 59 ], 60 'remote-candidate': [ 61 'url', // requires a STUN/TURN server. 62 'relayProtocol', // requires a TURN server. 63 'relatedAddress', // requires a STUN/TURN server. 64 'relatedPort', // requires a STUN/TURN server. 65 'tcpType', // requires ICE-TCP connection. 66 ], 67 'outbound-rtp': [ 68 'rid', // requires simulcast. 69 ], 70 'inbound-rtp': [ 71 'fecSsrc', // requires FlexFEC to be negotiated. 72 'fecBytesReceived', // requires FlexFEC to be negotiated. 73 ], 74 'media-source': [ 75 'echoReturnLoss', // requires gUM with an audio input device. 76 'echoReturnLossEnhancement', // requires gUM with an audio input device. 77 ] 78 }; 79 if (!untestablePropertiesByType[type]) { 80 return true; 81 } 82 return !untestablePropertiesByType[type].includes(property); 83 } 84 85 async function getAllStats(t, pc) { 86 // Try to obtain as many stats as possible, waiting up to 20 seconds for 87 // roundTripTime which can take several RTCP messages to calculate. 88 let stats; 89 for (let i = 0; i < 20; i++) { 90 stats = await pc.getStats(); 91 const values = [...stats.values()]; 92 const [remoteInboundAudio, remoteInboundVideo] = 93 ["audio", "video"].map(kind => 94 values.find(s => s.type == "remote-inbound-rtp" && s.kind == kind)); 95 const [remoteOutboundAudio, remoteOutboundVideo] = 96 ["audio", "video"].map(kind => 97 values.find(s => s.type == "remote-outbound-rtp" && s.kind == kind)); 98 // We expect both audio and video remote-inbound-rtp RTT. 99 const hasRemoteInbound = 100 remoteInboundAudio && "roundTripTime" in remoteInboundAudio && 101 remoteInboundVideo && "roundTripTime" in remoteInboundVideo; 102 // Due to current implementation limitations, we don't put as hard 103 // requirements on remote-outbound-rtp as remote-inbound-rtp. It's enough if 104 // it is available for either kind and `roundTripTime` is not required. In 105 // Chromium, remote-outbound-rtp is only implemented for audio and 106 // `roundTripTime` is missing in this test, but awaiting for any 107 // remote-outbound-rtp avoids flaky failures. 108 const hasRemoteOutbound = remoteOutboundAudio || remoteOutboundVideo; 109 const hasMediaPlayout = values.find(({type}) => type == "media-playout") != undefined; 110 if (hasRemoteInbound && hasRemoteOutbound && hasMediaPlayout) { 111 return stats; 112 } 113 await new Promise(r => t.step_timeout(r, 1000)); 114 } 115 return stats; 116 } 117 118 promise_test(async t => { 119 // load the IDL to know which members to be looking for 120 const idl = await fetch("/interfaces/webrtc-stats.idl").then(r => r.text()); 121 // for RTCStats definition 122 const webrtcIdl = await fetch("/interfaces/webrtc.idl").then(r => r.text()); 123 const astArray = WebIDL2.parse(idl + webrtcIdl); 124 125 let all = {}; 126 for (let type in dictionaryNames) { 127 // TODO: make use of audio/video distinction 128 let dictionaries = dictionaryNames[type].audio ? Object.values(dictionaryNames[type]) : [dictionaryNames[type]]; 129 all[type] = []; 130 let i = 0; 131 // Recursively collect members from inherited dictionaries 132 while (i < dictionaries.length) { 133 const dictName = dictionaries[i]; 134 const dict = astArray.find(i => i.name === dictName && i.type === "dictionary"); 135 if (dict && dict.members) { 136 all[type] = all[type].concat(dict.members.map(m => m.name)); 137 if (dict.inheritance) { 138 dictionaries.push(dict.inheritance); 139 } 140 } 141 i++; 142 } 143 // Unique-ify 144 all[type] = [...new Set(all[type])]; 145 } 146 147 const remaining = JSON.parse(JSON.stringify(all)); 148 for (const type in remaining) { 149 remaining[type] = new Set(remaining[type]); 150 } 151 152 const pc1 = new RTCPeerConnection(); 153 t.add_cleanup(() => pc1.close()); 154 const pc2 = new RTCPeerConnection(); 155 t.add_cleanup(() => pc2.close()); 156 157 const dc1 = pc1.createDataChannel("dummy", {negotiated: true, id: 0}); 158 const dc2 = pc2.createDataChannel("dummy", {negotiated: true, id: 0}); 159 // Use a real gUM to ensure that all stats exposing hardware capabilities are 160 // also exposed. 161 const stream = await navigator.mediaDevices.getUserMedia( 162 {video: true, audio: true}); 163 for (const track of stream.getTracks()) { 164 pc1.addTrack(track, stream); 165 pc2.addTrack(track, stream); 166 t.add_cleanup(() => track.stop()); 167 } 168 169 // Do a non-trickle ICE handshake to ensure that TCP candidates are gathered. 170 await pc1.setLocalDescription(); 171 await waitForIceGatheringState(pc1, ['complete']); 172 await pc2.setRemoteDescription(pc1.localDescription); 173 await pc2.setLocalDescription(); 174 await waitForIceGatheringState(pc2, ['complete']); 175 await pc1.setRemoteDescription(pc2.localDescription); 176 // Await the DTLS handshake. 177 await Promise.all([ 178 listenToConnected(pc1), 179 listenToConnected(pc2), 180 ]); 181 const stats = await getAllStats(t, pc1); 182 183 // The focus of this test is that there are no dangling references, 184 // i.e. keys ending with `Id` as described in 185 // https://w3c.github.io/webrtc-stats/#guidelines-for-design-of-stats-objects 186 test(t => { 187 for (const stat of stats.values()) { 188 Object.keys(stat).forEach(key => { 189 if (!key.endsWith('Id')) return; 190 assert_true(stats.has(stat[key]), `${stat.type}.${key} can be resolved`); 191 }); 192 } 193 }, 'All references resolve'); 194 195 // The focus of this test is not API correctness, but rather to provide an 196 // accessible metric of implementation progress by dictionary member. We count 197 // whether we've seen each dictionary's members in getStats(). 198 199 test(t => { 200 for (const stat of stats.values()) { 201 if (all[stat.type]) { 202 const memberNames = all[stat.type]; 203 const remainingNames = remaining[stat.type]; 204 assert_true(memberNames.length > 0, "Test error. No member found."); 205 for (const memberName of memberNames) { 206 if (memberName in stat) { 207 assert_not_equals(stat[memberName], undefined, "Not undefined"); 208 remainingNames.delete(memberName); 209 } 210 } 211 } 212 } 213 }, "Validating stats"); 214 215 for (const type in all) { 216 for (const memberName of all[type]) { 217 test(t => { 218 assert_implements_optional(isPropertyTestable(type, memberName), 219 `${type}.${memberName} marked as not testable.`); 220 assert_true(!remaining[type].has(memberName), 221 `Is ${memberName} present`); 222 }, `${type}'s ${memberName}`); 223 } 224 } 225 }, 'getStats succeeds'); 226 </script>