mirror of
https://github.com/servo/servo.git
synced 2025-07-02 21:13:39 +01:00
352 lines
13 KiB
HTML
352 lines
13 KiB
HTML
<!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);
|
|
coupleIceCandidates(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);
|
|
coupleIceCandidates(pc, pc2);
|
|
await doSignalingHandshake(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(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();
|
|
}
|
|
}));
|
|
|
|
|
|
coupleIceCandidates(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>
|