| <!doctype html> | |
| <meta charset=utf-8> | |
| <meta name="timeout" content="long"> | |
| <title>RTCPeerConnection.prototype.getStats</title> | |
| <script src="/resources/testharness.js"></script> | |
| <script src="/resources/testharnessreport.js"></script> | |
| <script src="RTCPeerConnection-helper.js"></script> | |
| <script> | |
| 'use strict'; | |
| // Test is based on the following editor draft: | |
| // webrtc-pc 20171130 | |
| // webrtc-stats 20171122 | |
| // The following helper function is called from RTCPeerConnection-helper.js | |
| // getTrackFromUserMedia | |
| // The following helper function is called from RTCPeerConnection-helper.js | |
| // exchangeIceCandidates | |
| // exchangeOfferAnswer | |
| /* | |
| 8.2. getStats | |
| 1. Let selectorArg be the method's first argument. | |
| 2. Let connection be the RTCPeerConnection object on which the method was invoked. | |
| 3. If selectorArg is null, let selector be null. | |
| 4. If selectorArg is a MediaStreamTrack let selector be an RTCRtpSender | |
| or RTCRtpReceiver on connection which track member matches selectorArg. | |
| If no such sender or receiver exists, or if more than one sender or | |
| receiver fit this criteria, return a promise rejected with a newly | |
| created InvalidAccessError. | |
| 5. Let p be a new promise. | |
| 6. Run the following steps in parallel: | |
| 1. Gather the stats indicated by selector according to the stats selection algorithm. | |
| 2. Resolve p with the resulting RTCStatsReport object, containing the gathered stats. | |
| */ | |
| promise_test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| return pc.getStats(); | |
| }, 'getStats() with no argument should succeed'); | |
| promise_test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| return pc.getStats(null); | |
| }, 'getStats(null) should succeed'); | |
| /* | |
| 8.2. getStats | |
| 4. If selectorArg is a MediaStreamTrack let selector be an RTCRtpSender | |
| or RTCRtpReceiver on connection which track member matches selectorArg. | |
| If no such sender or receiver exists, or if more than one sender or | |
| receiver fit this criteria, return a promise rejected with a newly | |
| created InvalidAccessError. | |
| */ | |
| promise_test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| return getTrackFromUserMedia('audio') | |
| .then(([track, mediaStream]) => { | |
| return promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(track)); | |
| }); | |
| }, 'getStats() with track not added to connection should reject with InvalidAccessError'); | |
| promise_test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| return getTrackFromUserMedia('audio') | |
| .then(([track, mediaStream]) => { | |
| pc.addTrack(track, mediaStream); | |
| return pc.getStats(track); | |
| }); | |
| }, 'getStats() with track added via addTrack should succeed'); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| pc.addTransceiver(track); | |
| return pc.getStats(track); | |
| }, 'getStats() with track added via addTransceiver should succeed'); | |
| promise_test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const transceiver1 = pc.addTransceiver('audio'); | |
| // Create another transceiver that resends what | |
| // is being received, kind of like echo | |
| const transceiver2 = pc.addTransceiver(transceiver1.receiver.track); | |
| assert_equals(transceiver1.receiver.track, transceiver2.sender.track); | |
| return promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(transceiver1.receiver.track)); | |
| }, 'getStats() with track associated with both sender and receiver should reject with InvalidAccessError'); | |
| /* | |
| 8.5. The stats selection algorithm | |
| 2. If selector is null, gather stats for the whole connection, add them to result, | |
| return result, and abort these steps. | |
| */ | |
| promise_test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| return pc.getStats() | |
| .then(statsReport => { | |
| assert_true(!![...statsReport.values()].find(({type}) => type === 'peer-connection')); | |
| }); | |
| }, 'getStats() with no argument should return stats report containing peer-connection stats on an empty PC'); | |
| promise_test(async t => { | |
| const pc = createPeerConnectionWithCleanup(t); | |
| const pc2 = createPeerConnectionWithCleanup(t); | |
| const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio'); | |
| pc.addTrack(sendtrack, mediaStream); | |
| exchangeIceCandidates(pc, pc2); | |
| await Promise.all([ | |
| exchangeOfferAnswer(pc, pc2), | |
| new Promise(r => pc2.ontrack = e => e.track.onunmute = r) | |
| ]); | |
| const statsReport = await pc.getStats(); | |
| assert_true(!![...statsReport.values()].find(({type}) => type === 'peer-connection')); | |
| assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp')); | |
| }, 'getStats() track with stream returns peer-connection and outbound-rtp stats'); | |
| promise_test(async t => { | |
| const pc = createPeerConnectionWithCleanup(t); | |
| const pc2 = createPeerConnectionWithCleanup(t); | |
| const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio'); | |
| pc.addTrack(sendtrack); | |
| exchangeIceCandidates(pc, pc2); | |
| await Promise.all([ | |
| exchangeOfferAnswer(pc, pc2), | |
| new Promise(r => pc2.ontrack = e => e.track.onunmute = r) | |
| ]); | |
| const statsReport = await pc.getStats(); | |
| assert_true(!![...statsReport.values()].find(({type}) => type === 'peer-connection')); | |
| assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp')); | |
| }, 'getStats() track without stream returns peer-connection and outbound-rtp stats'); | |
| promise_test(async t => { | |
| const pc = createPeerConnectionWithCleanup(t); | |
| const pc2 = createPeerConnectionWithCleanup(t); | |
| const [sendtrack, mediaStream] = await getTrackFromUserMedia('audio'); | |
| pc.addTrack(sendtrack, mediaStream); | |
| exchangeIceCandidates(pc, pc2); | |
| await Promise.all([ | |
| exchangeOfferAnswer(pc, pc2), | |
| new Promise(r => pc2.ontrack = e => e.track.onunmute = r) | |
| ]); | |
| const statsReport = await pc.getStats(); | |
| assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp')); | |
| }, 'getStats() audio contains outbound-rtp stats'); | |
| promise_test(async t => { | |
| const pc = createPeerConnectionWithCleanup(t); | |
| const pc2 = createPeerConnectionWithCleanup(t); | |
| const [sendtrack, mediaStream] = await getTrackFromUserMedia('video'); | |
| pc.addTrack(sendtrack, mediaStream); | |
| exchangeIceCandidates(pc, pc2); | |
| await Promise.all([ | |
| exchangeOfferAnswer(pc, pc2), | |
| new Promise(r => pc2.ontrack = e => e.track.onunmute = r) | |
| ]); | |
| const statsReport = await pc.getStats(); | |
| assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp')); | |
| }, 'getStats() video contains outbound-rtp stats'); | |
| /* | |
| 8.5. The stats selection algorithm | |
| 3. If selector is an RTCRtpSender, gather stats for and add the following objects | |
| to result: | |
| - All RTCOutboundRtpStreamStats objects corresponding to selector. | |
| - All stats objects referenced directly or indirectly by the RTCOutboundRtpStreamStats | |
| objects added. | |
| */ | |
| promise_test(async t => { | |
| const pc = createPeerConnectionWithCleanup(t); | |
| const pc2 = createPeerConnectionWithCleanup(t); | |
| let [sendtrack, mediaStream] = await getTrackFromUserMedia('audio'); | |
| pc.addTrack(sendtrack, mediaStream); | |
| exchangeIceCandidates(pc, pc2); | |
| await Promise.all([ | |
| exchangeOfferAnswer(pc, pc2), | |
| new Promise(r => pc2.ontrack = e => e.track.onunmute = r) | |
| ]); | |
| const statsReport = await pc.getStats(sendtrack); | |
| assert_true(!![...statsReport.values()].find(({type}) => type === 'outbound-rtp')); | |
| }, `getStats() on track associated with RTCRtpSender should return stats report containing outbound-rtp stats`); | |
| /* | |
| 8.5. The stats selection algorithm | |
| 4. If selector is an RTCRtpReceiver, gather stats for and add the following objects | |
| to result: | |
| - All RTCInboundRtpStreamStats objects corresponding to selector. | |
| - All stats objects referenced directly or indirectly by the RTCInboundRtpStreamStats | |
| added. | |
| */ | |
| promise_test(async t => { | |
| const pc = createPeerConnectionWithCleanup(t); | |
| const pc2 = createPeerConnectionWithCleanup(t); | |
| let [track, mediaStream] = await getTrackFromUserMedia('audio'); | |
| pc.addTrack(track, mediaStream); | |
| exchangeIceCandidates(pc, pc2); | |
| await exchangeOfferAnswer(pc, pc2); | |
| // Wait for unmute if the track is not already unmuted. | |
| // According to spec, it should be muted when being created, but this | |
| // is not what this test is testing, so allow it to be unmuted. | |
| if (pc2.getReceivers()[0].track.muted) { | |
| await new Promise(resolve => { | |
| pc2.getReceivers()[0].track.addEventListener('unmute', resolve); | |
| }); | |
| } | |
| const statsReport = await pc2.getStats(pc2.getReceivers()[0].track); | |
| assert_true(!![...statsReport.values()].find(({type}) => type === 'inbound-rtp')); | |
| }, `getStats() on track associated with RTCRtpReceiver should return stats report containing inbound-rtp stats`); | |
| promise_test(async t => { | |
| const pc = createPeerConnectionWithCleanup(t); | |
| const pc2 = createPeerConnectionWithCleanup(t); | |
| let [track, mediaStream] = await getTrackFromUserMedia('audio'); | |
| pc.addTrack(track, mediaStream); | |
| exchangeIceCandidates(pc, pc2); | |
| await exchangeOfferAnswer(pc, pc2); | |
| // Wait for unmute if the track is not already unmuted. | |
| // According to spec, it should be muted when being created, but this | |
| // is not what this test is testing, so allow it to be unmuted. | |
| if (pc2.getReceivers()[0].track.muted) { | |
| await new Promise(resolve => { | |
| pc2.getReceivers()[0].track.addEventListener('unmute', resolve); | |
| }); | |
| } | |
| const statsReport = await pc2.getStats(pc2.getReceivers()[0].track); | |
| assert_true(!![...statsReport.values()].find(({type}) => type === 'inbound-rtp')); | |
| }, `getStats() audio contains inbound-rtp stats`); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const [track, mediaStream] = await getTrackFromUserMedia('audio'); | |
| pc.addTransceiver(track); | |
| pc.addTransceiver(track); | |
| await promise_rejects_dom(t, 'InvalidAccessError', pc.getStats(track)); | |
| }, `getStats(track) should not work if multiple senders have the same track`); | |
| promise_test(async t => { | |
| const kMinimumTimeElapsedBetweenGetStatsCallsMs = 500; | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const t0 = Math.floor(performance.now()); | |
| const t0Stats = [...(await pc.getStats()).values()].find(({type}) => type === 'peer-connection'); | |
| await new Promise( | |
| r => t.step_timeout(r, kMinimumTimeElapsedBetweenGetStatsCallsMs)); | |
| const t1Stats = [...(await pc.getStats()).values()].find(({type}) => type === 'peer-connection'); | |
| const t1 = Math.ceil(performance.now()); | |
| const maximumTimeElapsedBetweenGetStatsCallsMs = t1 - t0; | |
| const deltaTimestampMs = t1Stats.timestamp - t0Stats.timestamp; | |
| // The delta must be at least the time we waited between calls. | |
| assert_greater_than_equal(deltaTimestampMs, | |
| kMinimumTimeElapsedBetweenGetStatsCallsMs); | |
| // The delta must be at most the time elapsed before the first getStats() | |
| // call and after the second getStats() call. | |
| assert_less_than_equal(deltaTimestampMs, | |
| maximumTimeElapsedBetweenGetStatsCallsMs); | |
| }, `RTCStats.timestamp increases with time passing`); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| pc1.close(); | |
| await pc1.getStats(); | |
| }, 'getStats succeeds on a closed peerconnection'); | |
| </script> |