| <!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> |