280 lines
12 KiB
HTML
280 lines
12 KiB
HTML
<!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>
|