summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html')
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html422
1 files changed, 422 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..4889bcf4dd
--- /dev/null
+++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-getStats.https.html
@@ -0,0 +1,422 @@
+<!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 [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();
+ 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 [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();
+ 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 [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_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 [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_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 Promise.all([
+ exchangeOfferAnswer(pc, pc2),
+ new Promise(r => pc2.ontrack = e => e.track.onunmute = r)
+ ]);
+ 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 [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 stats = await pc.getStats(sendtrack);
+ 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>