RTCPeerConnection-getStats.https.html (14870B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <meta name="timeout" content="long"> 4 <title>RTCPeerConnection.prototype.getStats</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 // Test is based on the following editor draft: 12 // webrtc-pc 20171130 13 // webrtc-stats 20171122 14 15 // The following helper function is called from RTCPeerConnection-helper.js 16 // getTrackFromUserMedia 17 18 // The following helper function is called from RTCPeerConnection-helper.js 19 // exchangeIceCandidates 20 // exchangeOfferAnswer 21 22 /* 23 8.2. getStats 24 1. Let selectorArg be the method's first argument. 25 2. Let connection be the RTCPeerConnection object on which the method was invoked. 26 3. If selectorArg is null, let selector be null. 27 4. If selectorArg is a MediaStreamTrack let selector be an RTCRtpSender 28 or RTCRtpReceiver on connection which track member matches selectorArg. 29 If no such sender or receiver exists, or if more than one sender or 30 receiver fit this criteria, return a promise rejected with a newly 31 created InvalidAccessError. 32 5. Let p be a new promise. 33 6. Run the following steps in parallel: 34 1. Gather the stats indicated by selector according to the stats selection algorithm. 35 2. Resolve p with the resulting RTCStatsReport object, containing the gathered stats. 36 */ 37 promise_test(t => { 38 const pc = new RTCPeerConnection(); 39 t.add_cleanup(() => pc.close()); 40 return pc.getStats(); 41 }, 'getStats() with no argument should succeed'); 42 43 promise_test(t => { 44 const pc = new RTCPeerConnection(); 45 t.add_cleanup(() => pc.close()); 46 return pc.getStats(null); 47 }, 'getStats(null) should succeed'); 48 49 /* 50 8.2. getStats 51 4. If selectorArg is a MediaStreamTrack let selector be an RTCRtpSender 52 or RTCRtpReceiver on connection which track member matches selectorArg. 53 If no such sender or receiver exists, or if more than one sender or 54 receiver fit this criteria, return a promise rejected with a newly 55 created InvalidAccessError. 56 */ 57 promise_test(t => { 58 const pc = new RTCPeerConnection(); 59 t.add_cleanup(() => pc.close()); 60 return getTrackFromUserMedia('audio') 61 .then(([track, mediaStream]) => { 62 return promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(track)); 63 }); 64 }, 'getStats() with track not added to connection should reject with InvalidAccessError'); 65 66 promise_test(t => { 67 const pc = new RTCPeerConnection(); 68 t.add_cleanup(() => pc.close()); 69 return getTrackFromUserMedia('audio') 70 .then(([track, mediaStream]) => { 71 pc.addTrack(track, mediaStream); 72 return pc.getStats(track); 73 }); 74 }, 'getStats() with track added via addTrack should succeed'); 75 76 promise_test(async t => { 77 const pc = new RTCPeerConnection(); 78 t.add_cleanup(() => pc.close()); 79 80 const stream = await getNoiseStream({audio: true}); 81 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 82 const [track] = stream.getTracks(); 83 pc.addTransceiver(track); 84 85 return pc.getStats(track); 86 }, 'getStats() with track added via addTransceiver should succeed'); 87 88 promise_test(t => { 89 const pc = new RTCPeerConnection(); 90 t.add_cleanup(() => pc.close()); 91 const transceiver1 = pc.addTransceiver('audio'); 92 93 // Create another transceiver that resends what 94 // is being received, kind of like echo 95 const transceiver2 = pc.addTransceiver(transceiver1.receiver.track); 96 assert_equals(transceiver1.receiver.track, transceiver2.sender.track); 97 98 return promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(transceiver1.receiver.track)); 99 }, 'getStats() with track associated with both sender and receiver should reject with InvalidAccessError'); 100 101 /* 102 8.5. The stats selection algorithm 103 2. If selector is null, gather stats for the whole connection, add them to result, 104 return result, and abort these steps. 105 */ 106 promise_test(t => { 107 const pc = new RTCPeerConnection(); 108 t.add_cleanup(() => pc.close()); 109 return pc.getStats() 110 .then(statsReport => { 111 assert_true(!![...statsReport.values()].find(({type}) => type === 'peer-connection')); 112 }); 113 }, 'getStats() with no argument should return stats report containing peer-connection stats on an empty PC'); 114 115 promise_test(async t => { 116 const pc = createPeerConnectionWithCleanup(t); 117 const pc2 = createPeerConnectionWithCleanup(t); 118 const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio'); 119 pc.addTrack(sendtrack, mediaStream); 120 exchangeIceCandidates(pc, pc2); 121 await Promise.all([ 122 exchangeOfferAnswer(pc, pc2), 123 new Promise(r => pc2.ontrack = e => e.track.onunmute = r) 124 ]); 125 const statsReport = await pc.getStats(); 126 assert_true(!![...statsReport.values()].find(({type}) => type === 'peer-connection')); 127 assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp')); 128 }, 'getStats() track with stream returns peer-connection and outbound-rtp stats'); 129 130 promise_test(async t => { 131 const pc = createPeerConnectionWithCleanup(t); 132 const pc2 = createPeerConnectionWithCleanup(t); 133 const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio'); 134 pc.addTrack(sendtrack); 135 exchangeIceCandidates(pc, pc2); 136 await Promise.all([ 137 exchangeOfferAnswer(pc, pc2), 138 new Promise(r => pc2.ontrack = e => e.track.onunmute = r) 139 ]); 140 const statsReport = await pc.getStats(); 141 assert_true(!![...statsReport.values()].find(({type}) => type === 'peer-connection')); 142 assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp')); 143 }, 'getStats() track without stream returns peer-connection and outbound-rtp stats'); 144 145 promise_test(async t => { 146 const pc = createPeerConnectionWithCleanup(t); 147 const pc2 = createPeerConnectionWithCleanup(t); 148 const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio'); 149 pc.addTrack(sendtrack, mediaStream); 150 exchangeIceCandidates(pc, pc2); 151 await Promise.all([ 152 exchangeOfferAnswer(pc, pc2), 153 new Promise(r => pc2.ontrack = e => e.track.onunmute = r) 154 ]); 155 const statsReport = await pc.getStats(); 156 assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp')); 157 }, 'getStats() audio contains outbound-rtp stats'); 158 159 promise_test(async t => { 160 const pc = createPeerConnectionWithCleanup(t); 161 const pc2 = createPeerConnectionWithCleanup(t); 162 const [sendtrack, mediaStream] = await getTrackFromUserMedia('video'); 163 pc.addTrack(sendtrack, mediaStream); 164 exchangeIceCandidates(pc, pc2); 165 await Promise.all([ 166 exchangeOfferAnswer(pc, pc2), 167 new Promise(r => pc2.ontrack = e => e.track.onunmute = r) 168 ]); 169 const statsReport = await pc.getStats(); 170 assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp')); 171 }, 'getStats() video contains outbound-rtp stats'); 172 173 /* 174 8.5. The stats selection algorithm 175 3. If selector is an RTCRtpSender, gather stats for and add the following objects 176 to result: 177 - All RTCOutboundRtpStreamStats objects corresponding to selector. 178 - All stats objects referenced directly or indirectly by the RTCOutboundRtpStreamStats 179 objects added. 180 */ 181 promise_test(async t => { 182 const pc = createPeerConnectionWithCleanup(t); 183 const pc2 = createPeerConnectionWithCleanup(t); 184 185 let [sendtrack, mediaStream] = await getTrackFromUserMedia('audio'); 186 pc.addTrack(sendtrack, mediaStream); 187 exchangeIceCandidates(pc, pc2); 188 await Promise.all([ 189 exchangeOfferAnswer(pc, pc2), 190 new Promise(r => pc2.ontrack = e => e.track.onunmute = r) 191 ]); 192 const statsReport = await pc.getStats(sendtrack); 193 assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp')); 194 }, `getStats() on track associated with RTCRtpSender should return stats report containing outbound-rtp stats`); 195 196 /* 197 8.5. The stats selection algorithm 198 4. If selector is an RTCRtpReceiver, gather stats for and add the following objects 199 to result: 200 - All RTCInboundRtpStreamStats objects corresponding to selector. 201 - All stats objects referenced directly or indirectly by the RTCInboundRtpStreamStats 202 added. 203 */ 204 promise_test(async t => { 205 const pc = createPeerConnectionWithCleanup(t); 206 const pc2 = createPeerConnectionWithCleanup(t); 207 208 let [track, mediaStream] = await getTrackFromUserMedia('audio'); 209 pc.addTrack(track, mediaStream); 210 exchangeIceCandidates(pc, pc2); 211 await exchangeOfferAnswer(pc, pc2); 212 // Wait for unmute if the track is not already unmuted. 213 // According to spec, it should be muted when being created, but this 214 // is not what this test is testing, so allow it to be unmuted. 215 if (pc2.getReceivers()[0].track.muted) { 216 await new Promise(resolve => { 217 pc2.getReceivers()[0].track.addEventListener('unmute', resolve); 218 }); 219 } 220 const statsReport = await pc2.getStats(pc2.getReceivers()[0].track); 221 assert_true(!![...statsReport.values()].find(({type}) => type === 'inbound-rtp')); 222 }, `getStats() on track associated with RTCRtpReceiver should return stats report containing inbound-rtp stats`); 223 224 promise_test(async t => { 225 const pc = createPeerConnectionWithCleanup(t); 226 const pc2 = createPeerConnectionWithCleanup(t); 227 228 let [track, mediaStream] = await getTrackFromUserMedia('audio'); 229 pc.addTrack(track, mediaStream); 230 exchangeIceCandidates(pc, pc2); 231 await exchangeOfferAnswer(pc, pc2); 232 // Wait for unmute if the track is not already unmuted. 233 // According to spec, it should be muted when being created, but this 234 // is not what this test is testing, so allow it to be unmuted. 235 if (pc2.getReceivers()[0].track.muted) { 236 await new Promise(resolve => { 237 pc2.getReceivers()[0].track.addEventListener('unmute', resolve); 238 }); 239 } 240 const statsReport = await pc2.getStats(pc2.getReceivers()[0].track); 241 assert_true(!![...statsReport.values()].find(({type}) => type === 'inbound-rtp')); 242 }, `getStats() audio contains inbound-rtp stats`); 243 244 promise_test(async t => { 245 const pc = createPeerConnectionWithCleanup(t); 246 const pc2 = createPeerConnectionWithCleanup(t); 247 248 function check_no_candidate_pair_stats(statsReport) { 249 const candidatePairStats = [...(statsReport).values()].filter(({type}) => type === 'candidate-pair'); 250 assert_greater_than_equal(candidatePairStats.length, 0); 251 } 252 253 function check_candidate_pair_stats(statsReport, elapsed_time_ms) { 254 assert_true(!![...statsReport.values()].find(({type}) => type === 'candidate-pair')); 255 const candidatePairStats = [...(statsReport).values()].filter(({type}) => type === 'candidate-pair'); 256 assert_greater_than_equal(candidatePairStats.length, 1); 257 258 for (let pairStats of candidatePairStats) { 259 assert_not_equals(pairStats.responsesReceived, null, "responsesReceived should not be null"); 260 assert_not_equals(pairStats.totalRoundTripTime, null, "totalRoundTripTime should not be null"); 261 assert_not_equals(pairStats.currentRoundTripTime, null, "currentRoundTripTime should not be null"); 262 263 assert_greater_than_equal(pairStats.responsesReceived, 0); 264 assert_greater_than_equal(pairStats.totalRoundTripTime, 0); 265 assert_greater_than_equal(pairStats.currentRoundTripTime, 0); 266 267 if (pairStats.responsesReceived < 2) { 268 assert_equals(pairStats.totalRoundTripTime, pairStats.currentRoundTripTime); 269 } else { 270 assert_greater_than_equal(pairStats.totalRoundTripTime, pairStats.currentRoundTripTime); 271 } 272 assert_less_than_equal(pairStats.totalRoundTripTime*1000, elapsed_time_ms) 273 } 274 } 275 276 let [track, mediaStream] = await getTrackFromUserMedia('video'); 277 pc.addTrack(track, mediaStream); 278 exchangeIceCandidates(pc, pc2); 279 assert_equals(pc2.getReceivers().length, 0); 280 await exchangeOfferAnswer(pc, pc2); 281 282 assert_equals(pc.getSenders().length, 1); 283 assert_equals(pc2.getReceivers().length, 1); 284 const t0 = performance.now(); 285 286 check_no_candidate_pair_stats(await pc.getStats(pc.getSenders()[0].track)); 287 288 // Wait for unmute if the track is not already unmuted. 289 // According to spec, it should be muted when being created, but this 290 // is not what this test is testing, so allow it to be unmuted. 291 if (pc2.getReceivers()[0].track.muted) { 292 await new Promise(resolve => { 293 pc2.getReceivers()[0].track.addEventListener('unmute', resolve); 294 }); 295 } 296 // wait a bit longer for a few consent messages 297 await new Promise(r => t.step_timeout(r, 8000)); 298 299 const t1 = performance.now(); 300 301 check_candidate_pair_stats(await pc.getStats(pc.getSenders()[0].track), t1-t0); 302 }, `getStats() audio contains candidate-pair stats`); 303 304 promise_test(async t => { 305 const pc = new RTCPeerConnection(); 306 t.add_cleanup(() => pc.close()); 307 const [track, mediaStream] = await getTrackFromUserMedia('audio'); 308 pc.addTransceiver(track); 309 pc.addTransceiver(track); 310 await promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(track)); 311 }, `getStats(track) should not work if multiple senders have the same track`); 312 313 promise_test(async t => { 314 const kMinimumTimeElapsedBetweenGetStatsCallsMs = 500; 315 const pc = new RTCPeerConnection(); 316 t.add_cleanup(() => pc.close()); 317 const t0 = Math.floor(performance.now()); 318 const t0Stats = [...(await pc.getStats()).values()].find(({type}) => type === 'peer-connection'); 319 await new Promise( 320 r => t.step_timeout(r, kMinimumTimeElapsedBetweenGetStatsCallsMs)); 321 const t1Stats = [...(await pc.getStats()).values()].find(({type}) => type === 'peer-connection'); 322 const t1 = Math.ceil(performance.now()); 323 const maximumTimeElapsedBetweenGetStatsCallsMs = t1 - t0; 324 const deltaTimestampMs = t1Stats.timestamp - t0Stats.timestamp; 325 // The delta must be at least the time we waited between calls. 326 assert_greater_than_equal(deltaTimestampMs, 327 kMinimumTimeElapsedBetweenGetStatsCallsMs); 328 // The delta must be at most the time elapsed before the first getStats() 329 // call and after the second getStats() call. 330 assert_less_than_equal(deltaTimestampMs, 331 maximumTimeElapsedBetweenGetStatsCallsMs); 332 }, `RTCStats.timestamp increases with time passing`); 333 334 promise_test(async t => { 335 const pc1 = new RTCPeerConnection(); 336 pc1.close(); 337 await pc1.getStats(); 338 }, 'getStats succeeds on a closed peerconnection'); 339 340 </script>