| <!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 |
| // doSignalingHandshake |
| |
| /* |
| 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(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 navigator.mediaDevices.getUserMedia({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()); |
| return getTrackFromUserMedia('audio') |
| .then(([track, mediaStream]) => { |
| // addTransceiver allows adding same track multiple times |
| const transceiver1 = pc.addTransceiver(track); |
| const transceiver2 = pc.addTransceiver(track); |
| |
| assert_not_equals(transceiver1, transceiver2); |
| assert_not_equals(transceiver1.sender, transceiver2.sender); |
| assert_equals(transceiver1.sender.track, transceiver2.sender.track); |
| |
| return promise_rejects(t, 'InvalidAccessError', pc.getStats(track)); |
| }); |
| }, `getStats() with track associated with more than one sender should reject with InvalidAccessError`); |
| |
| 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(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(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| return getTrackFromUserMedia('audio') |
| .then(([track, mediaStream]) => { |
| pc.addTrack(track, mediaStream); |
| return pc.getStats(); |
| }) |
| .then(statsReport => { |
| validateStatsReport(statsReport); |
| assert_stats_report_has_stats(statsReport, ['peer-connection']); |
| assert_stats_report_has_stats(statsReport, ['outbound-rtp']); |
| }); |
| }, 'getStats() with no argument should return stats report containing peer-connection stats and outbound-track-stats'); |
| |
| promise_test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| return getTrackFromUserMedia('audio') |
| .then(([track, mediaStream]) => { |
| pc.addTrack(track); |
| return pc.getStats(); |
| }) |
| .then(statsReport => { |
| validateStatsReport(statsReport); |
| assert_stats_report_has_stats(statsReport, ['peer-connection']); |
| assert_stats_report_has_stats(statsReport, ['outbound-rtp']); |
| }); |
| }, 'getStats() with no argument should return stats for no-stream tracks'); |
| |
| /* |
| 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 doSignalingHandshake(pc, pc2); |
| await listenToIceConnected(pc); |
| const stats = await pc.getStats(track); |
| validateStatsReport(stats); |
| assert_stats_report_has_stats(stats, ['outbound-rtp']); |
| }, `getStats() on track associated with RtpSender 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 doSignalingHandshake(pc, pc2); |
| await new Promise(resolve => { |
| pc2.getReceivers()[0].track.addEventListener('unmute', resolve); |
| }); |
| const stats = await pc2.getStats(track); |
| validateStatsReport(stats); |
| assert_stats_report_has_stats(stats, ['inbound-rtp']); |
| }, `getStats() on track associated with RtpReceiver should return stats report containing inbound-rtp 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", |
| "peer-connection", |
| "data-channel", |
| "stream", |
| "track", |
| "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'); |
| |
| return 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'); |
| |
| 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 mediaStreamStats = findStatsFromReport(statsReport, |
| stats => { |
| return stats.type === 'stream' && |
| stats.streamIdentifier === mediaStream.id; |
| }, |
| 'Expect media stream stats to be found'); |
| |
| assert_true(mediaStreamStats.trackIds.include(audioTrackStats.id)); |
| assert_true(mediaStreamStats.trackIds.include(videoTrackStats.id)); |
| } |
| |
| const onConnected = t.step_func(() => { |
| // Wait a while for the peer connections to collect stats |
| t.step_timeout(() => { |
| Promise.all([ |
| 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); |
| doSignalingHandshake(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`); |
| |
| </script> |