| <!doctype html> |
| <meta charset=utf-8> |
| <title>RTCPeerConnection.prototype.getStats</title> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="RTCPeerConnection-helper.js"></script> |
| <script src="dictionary-helper.js"></script> |
| <script src="RTCStats-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 RTCStats-helper.js |
| // validateStatsReport |
| // assert_stats_report_has_stats |
| |
| // 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 => { |
| validateStatsReport(statsReport); |
| assert_stats_report_has_stats(statsReport, ['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 [track, mediaStream] = await getTrackFromUserMedia('audio'); |
| pc.addTrack(track, mediaStream); |
| exchangeIceCandidates(pc, pc2); |
| await exchangeOfferAnswer(pc, pc2); |
| await listenToConnected(pc); |
| const statsReport = await pc.getStats(); |
| getRequiredStats(statsReport, 'peer-connection'); |
| getRequiredStats(statsReport, '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 [track, mediaStream] = await getTrackFromUserMedia('audio'); |
| pc.addTrack(track); |
| exchangeIceCandidates(pc, pc2); |
| await exchangeOfferAnswer(pc, pc2); |
| await listenToConnected(pc); |
| const statsReport = await pc.getStats(); |
| getRequiredStats(statsReport, 'peer-connection'); |
| getRequiredStats(statsReport, '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 [track, mediaStream] = await getTrackFromUserMedia('audio'); |
| pc.addTrack(track, mediaStream); |
| exchangeIceCandidates(pc, pc2); |
| await exchangeOfferAnswer(pc, pc2); |
| await listenToConnected(pc); |
| const statsReport = await pc.getStats(); |
| assert_stats_report_has_stats(statsReport, ['outbound-rtp']); |
| }, 'getStats() audio outbound-rtp contains all mandatory stats'); |
| |
| promise_test(async t => { |
| const pc = createPeerConnectionWithCleanup(t); |
| const pc2 = createPeerConnectionWithCleanup(t); |
| const [track, mediaStream] = await getTrackFromUserMedia('video'); |
| pc.addTrack(track, mediaStream); |
| exchangeIceCandidates(pc, pc2); |
| await exchangeOfferAnswer(pc, pc2); |
| await listenToConnected(pc); |
| const statsReport = await pc.getStats(); |
| assert_stats_report_has_stats(statsReport, ['outbound-rtp']); |
| }, 'getStats() video outbound-rtp contains all mandatory stats'); |
| |
| promise_test(async t => { |
| const pc = createPeerConnectionWithCleanup(t); |
| const pc2 = createPeerConnectionWithCleanup(t); |
| const [audioTrack, audioStream] = await getTrackFromUserMedia('audio'); |
| pc.addTrack(audioTrack, audioStream); |
| const [videoTrack, videoStream] = await getTrackFromUserMedia('video'); |
| pc.addTrack(videoTrack, videoStream); |
| exchangeIceCandidates(pc, pc2); |
| await exchangeOfferAnswer(pc, pc2); |
| await listenToConnected(pc); |
| const statsReport = await pc.getStats(); |
| validateStatsReport(statsReport); |
| }, 'getStats() audio and video validate all mandatory 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 [track, mediaStream] = await getTrackFromUserMedia('audio'); |
| pc.addTrack(track, mediaStream); |
| exchangeIceCandidates(pc, pc2); |
| await exchangeOfferAnswer(pc, pc2); |
| await listenToConnected(pc); |
| const stats = await pc.getStats(track); |
| getRequiredStats(stats, '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 stats = await pc2.getStats(pc2.getReceivers()[0].track); |
| getRequiredStats(stats, '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 stats = await pc2.getStats(pc2.getReceivers()[0].track); |
| getRequiredStats(stats, 'inbound-rtp'); |
| }, `getStats() inbound-rtp contains all mandatory stats`); |
| |
| /* |
| 8.6 Mandatory To Implement Stats |
| An implementation MUST support generating statistics of the following types |
| when the corresponding objects exist on a PeerConnection, with the attributes |
| that are listed when they are valid for that object. |
| */ |
| |
| const mandatoryStats = [ |
| "codec", |
| "inbound-rtp", |
| "outbound-rtp", |
| "remote-inbound-rtp", |
| "remote-outbound-rtp", |
| "media-source", |
| "peer-connection", |
| "data-channel", |
| "sender", |
| "receiver", |
| "transport", |
| "candidate-pair", |
| "local-candidate", |
| "remote-candidate", |
| "certificate" |
| ]; |
| |
| async_test(t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const dataChannel = pc1.createDataChannel('test-channel'); |
| |
| getNoiseStream({ |
| audio: true, |
| video: true |
| }) |
| .then(t.step_func(mediaStream => { |
| const tracks = mediaStream.getTracks(); |
| const [audioTrack] = mediaStream.getAudioTracks(); |
| const [videoTrack] = mediaStream.getVideoTracks(); |
| |
| for (const track of mediaStream.getTracks()) { |
| t.add_cleanup(() => track.stop()); |
| pc1.addTrack(track, mediaStream); |
| } |
| |
| const testStatsReport = (pc, statsReport) => { |
| validateStatsReport(statsReport); |
| assert_stats_report_has_stats(statsReport, mandatoryStats); |
| |
| const dataChannelStats = findStatsFromReport(statsReport, |
| stats => { |
| return stats.type === 'data-channel' && |
| stats.dataChannelIdentifier === dataChannel.id; |
| }, |
| 'Expect data channel stats to be found'); |
| |
| assert_equals(dataChannelStats.label, 'test-channel'); |
| |
| /* TODO track stats are obsolete - replace with sender/receiver? */ |
| const audioTrackStats = findStatsFromReport(statsReport, |
| stats => { |
| return stats.type === 'track' && |
| stats.trackIdentifier === audioTrack.id; |
| }, |
| 'Expect audio track stats to be found'); |
| |
| assert_equals(audioTrackStats.kind, 'audio'); |
| |
| const videoTrackStats = findStatsFromReport(statsReport, |
| stats => { |
| return stats.type === 'track' && |
| stats.trackIdentifier === videoTrack.id; |
| }, |
| 'Expect video track stats to be found'); |
| |
| assert_equals(videoTrackStats.kind, 'video'); |
| } |
| |
| const onConnected = t.step_func(() => { |
| // Wait a while for the peer connections to collect stats |
| t.step_timeout(() => { |
| Promise.all([ |
| /* TODO: for both pc1 and pc2 to expose all mandatory stats, they need to both send/receive tracks and data channels */ |
| pc1.getStats() |
| .then(statsReport => testStatsReport(pc1, statsReport)), |
| |
| pc2.getStats() |
| .then(statsReport => testStatsReport(pc2, statsReport)) |
| ]) |
| .then(t.step_func_done()) |
| .catch(t.step_func(err => { |
| assert_unreached(`test failed with error: ${err}`); |
| })); |
| }, 200) |
| }) |
| |
| let onTrackCount = 0 |
| let onDataChannelCalled = false |
| |
| pc2.addEventListener('track', t.step_func(() => { |
| onTrackCount++; |
| if (onTrackCount === 2 && onDataChannelCalled) { |
| onConnected(); |
| } |
| })); |
| |
| pc2.addEventListener('datachannel', t.step_func(() => { |
| onDataChannelCalled = true; |
| if (onTrackCount === 2) { |
| onConnected(); |
| } |
| })); |
| |
| |
| exchangeIceCandidates(pc1, pc2); |
| exchangeOfferAnswer(pc1, pc2); |
| })) |
| .catch(t.step_func(err => { |
| assert_unreached(`test failed with error: ${err}`); |
| })); |
| |
| }, `getStats() with connected peer connections having tracks and data channel should return all mandatory to implement 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 = getRequiredStats(await pc.getStats(), 'peer-connection'); |
| await new Promise( |
| r => t.step_timeout(r, kMinimumTimeElapsedBetweenGetStatsCallsMs)); |
| const t1Stats = getRequiredStats(await pc.getStats(), '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`); |
| |
| </script> |