diff options
Diffstat (limited to 'testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html')
-rw-r--r-- | testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html | 410 |
1 files changed, 410 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html new file mode 100644 index 0000000000..8062618dd6 --- /dev/null +++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html @@ -0,0 +1,410 @@ +<!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 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> |