226 lines
8.4 KiB
HTML
226 lines
8.4 KiB
HTML
<!doctype html>
|
|
<meta charset=utf-8>
|
|
<meta name="timeout" content="long">
|
|
<title>Support for all stats defined in WebRTC Stats</title>
|
|
<script src=/resources/testharness.js></script>
|
|
<script src=/resources/testharnessreport.js></script>
|
|
<script src="../webrtc/RTCPeerConnection-helper.js"></script>
|
|
<script src="/resources/WebIDLParser.js"></script>
|
|
<script>
|
|
'use strict';
|
|
|
|
// inspired from similar test for MTI stats in ../webrtc/RTCPeerConnection-mandatory-getStats.https.html
|
|
|
|
// From https://w3c.github.io/webrtc-stats/webrtc-stats.html#rtcstatstype-str*
|
|
const dictionaryNames = {
|
|
"codec": "RTCCodecStats",
|
|
"inbound-rtp": "RTCInboundRtpStreamStats",
|
|
"outbound-rtp": "RTCOutboundRtpStreamStats",
|
|
"remote-inbound-rtp": "RTCRemoteInboundRtpStreamStats",
|
|
"remote-outbound-rtp": "RTCRemoteOutboundRtpStreamStats",
|
|
"csrc": "RTCRtpContributingSourceStats",
|
|
"peer-connection": "RTCPeerConnectionStats",
|
|
"data-channel": "RTCDataChannelStats",
|
|
"media-source": {
|
|
audio: "RTCAudioSourceStats",
|
|
video: "RTCVideoSourceStats"
|
|
},
|
|
"media-playout": "RTCAudioPlayoutStats",
|
|
"sender": {
|
|
audio: "RTCAudioSenderStats",
|
|
video: "RTCVideoSenderStats"
|
|
},
|
|
"receiver": {
|
|
audio: "RTCAudioReceiverStats",
|
|
video: "RTCVideoReceiverStats",
|
|
},
|
|
"transport": "RTCTransportStats",
|
|
"candidate-pair": "RTCIceCandidatePairStats",
|
|
"local-candidate": "RTCIceCandidateStats",
|
|
"remote-candidate": "RTCIceCandidateStats",
|
|
"certificate": "RTCCertificateStats",
|
|
};
|
|
|
|
function isPropertyTestable(type, property) {
|
|
// List of properties which are not testable by this test.
|
|
// When adding something to this list, please explain why.
|
|
const untestablePropertiesByType = {
|
|
'candidate-pair': [
|
|
'availableIncomingBitrate', // requires REMB, no TWCC.
|
|
],
|
|
'certificate': [
|
|
'issuerCertificateId', // we only use self-signed certificates.
|
|
],
|
|
'local-candidate': [
|
|
'url', // requires a STUN/TURN server.
|
|
'relayProtocol', // requires a TURN server.
|
|
'relatedAddress', // requires a STUN/TURN server.
|
|
'relatedPort', // requires a STUN/TURN server.
|
|
],
|
|
'remote-candidate': [
|
|
'url', // requires a STUN/TURN server.
|
|
'relayProtocol', // requires a TURN server.
|
|
'relatedAddress', // requires a STUN/TURN server.
|
|
'relatedPort', // requires a STUN/TURN server.
|
|
'tcpType', // requires ICE-TCP connection.
|
|
],
|
|
'outbound-rtp': [
|
|
'rid', // requires simulcast.
|
|
],
|
|
'inbound-rtp': [
|
|
'fecSsrc', // requires FlexFEC to be negotiated.
|
|
'fecBytesReceived', // requires FlexFEC to be negotiated.
|
|
],
|
|
'media-source': [
|
|
'echoReturnLoss', // requires gUM with an audio input device.
|
|
'echoReturnLossEnhancement', // requires gUM with an audio input device.
|
|
]
|
|
};
|
|
if (!untestablePropertiesByType[type]) {
|
|
return true;
|
|
}
|
|
return !untestablePropertiesByType[type].includes(property);
|
|
}
|
|
|
|
async function getAllStats(t, pc) {
|
|
// Try to obtain as many stats as possible, waiting up to 20 seconds for
|
|
// roundTripTime which can take several RTCP messages to calculate.
|
|
let stats;
|
|
for (let i = 0; i < 20; i++) {
|
|
stats = await pc.getStats();
|
|
const values = [...stats.values()];
|
|
const [remoteInboundAudio, remoteInboundVideo] =
|
|
["audio", "video"].map(kind =>
|
|
values.find(s => s.type == "remote-inbound-rtp" && s.kind == kind));
|
|
const [remoteOutboundAudio, remoteOutboundVideo] =
|
|
["audio", "video"].map(kind =>
|
|
values.find(s => s.type == "remote-outbound-rtp" && s.kind == kind));
|
|
// We expect both audio and video remote-inbound-rtp RTT.
|
|
const hasRemoteInbound =
|
|
remoteInboundAudio && "roundTripTime" in remoteInboundAudio &&
|
|
remoteInboundVideo && "roundTripTime" in remoteInboundVideo;
|
|
// Due to current implementation limitations, we don't put as hard
|
|
// requirements on remote-outbound-rtp as remote-inbound-rtp. It's enough if
|
|
// it is available for either kind and `roundTripTime` is not required. In
|
|
// Chromium, remote-outbound-rtp is only implemented for audio and
|
|
// `roundTripTime` is missing in this test, but awaiting for any
|
|
// remote-outbound-rtp avoids flaky failures.
|
|
const hasRemoteOutbound = remoteOutboundAudio || remoteOutboundVideo;
|
|
const hasMediaPlayout = values.find(({type}) => type == "media-playout") != undefined;
|
|
if (hasRemoteInbound && hasRemoteOutbound && hasMediaPlayout) {
|
|
return stats;
|
|
}
|
|
await new Promise(r => t.step_timeout(r, 1000));
|
|
}
|
|
return stats;
|
|
}
|
|
|
|
promise_test(async t => {
|
|
// load the IDL to know which members to be looking for
|
|
const idl = await fetch("/interfaces/webrtc-stats.idl").then(r => r.text());
|
|
// for RTCStats definition
|
|
const webrtcIdl = await fetch("/interfaces/webrtc.idl").then(r => r.text());
|
|
const astArray = WebIDL2.parse(idl + webrtcIdl);
|
|
|
|
let all = {};
|
|
for (let type in dictionaryNames) {
|
|
// TODO: make use of audio/video distinction
|
|
let dictionaries = dictionaryNames[type].audio ? Object.values(dictionaryNames[type]) : [dictionaryNames[type]];
|
|
all[type] = [];
|
|
let i = 0;
|
|
// Recursively collect members from inherited dictionaries
|
|
while (i < dictionaries.length) {
|
|
const dictName = dictionaries[i];
|
|
const dict = astArray.find(i => i.name === dictName && i.type === "dictionary");
|
|
if (dict && dict.members) {
|
|
all[type] = all[type].concat(dict.members.map(m => m.name));
|
|
if (dict.inheritance) {
|
|
dictionaries.push(dict.inheritance);
|
|
}
|
|
}
|
|
i++;
|
|
}
|
|
// Unique-ify
|
|
all[type] = [...new Set(all[type])];
|
|
}
|
|
|
|
const remaining = JSON.parse(JSON.stringify(all));
|
|
for (const type in remaining) {
|
|
remaining[type] = new Set(remaining[type]);
|
|
}
|
|
|
|
const pc1 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc1.close());
|
|
const pc2 = new RTCPeerConnection();
|
|
t.add_cleanup(() => pc2.close());
|
|
|
|
const dc1 = pc1.createDataChannel("dummy", {negotiated: true, id: 0});
|
|
const dc2 = pc2.createDataChannel("dummy", {negotiated: true, id: 0});
|
|
// Use a real gUM to ensure that all stats exposing hardware capabilities are
|
|
// also exposed.
|
|
const stream = await navigator.mediaDevices.getUserMedia(
|
|
{video: true, audio: true});
|
|
for (const track of stream.getTracks()) {
|
|
pc1.addTrack(track, stream);
|
|
pc2.addTrack(track, stream);
|
|
t.add_cleanup(() => track.stop());
|
|
}
|
|
|
|
// Do a non-trickle ICE handshake to ensure that TCP candidates are gathered.
|
|
await pc1.setLocalDescription();
|
|
await waitForIceGatheringState(pc1, ['complete']);
|
|
await pc2.setRemoteDescription(pc1.localDescription);
|
|
await pc2.setLocalDescription();
|
|
await waitForIceGatheringState(pc2, ['complete']);
|
|
await pc1.setRemoteDescription(pc2.localDescription);
|
|
// Await the DTLS handshake.
|
|
await Promise.all([
|
|
listenToConnected(pc1),
|
|
listenToConnected(pc2),
|
|
]);
|
|
const stats = await getAllStats(t, pc1);
|
|
|
|
// The focus of this test is that there are no dangling references,
|
|
// i.e. keys ending with `Id` as described in
|
|
// https://w3c.github.io/webrtc-stats/#guidelines-for-design-of-stats-objects
|
|
test(t => {
|
|
for (const stat of stats.values()) {
|
|
Object.keys(stat).forEach(key => {
|
|
if (!key.endsWith('Id')) return;
|
|
assert_true(stats.has(stat[key]), `${stat.type}.${key} can be resolved`);
|
|
});
|
|
}
|
|
}, 'All references resolve');
|
|
|
|
// The focus of this test is not API correctness, but rather to provide an
|
|
// accessible metric of implementation progress by dictionary member. We count
|
|
// whether we've seen each dictionary's members in getStats().
|
|
|
|
test(t => {
|
|
for (const stat of stats.values()) {
|
|
if (all[stat.type]) {
|
|
const memberNames = all[stat.type];
|
|
const remainingNames = remaining[stat.type];
|
|
assert_true(memberNames.length > 0, "Test error. No member found.");
|
|
for (const memberName of memberNames) {
|
|
if (memberName in stat) {
|
|
assert_not_equals(stat[memberName], undefined, "Not undefined");
|
|
remainingNames.delete(memberName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, "Validating stats");
|
|
|
|
for (const type in all) {
|
|
for (const memberName of all[type]) {
|
|
test(t => {
|
|
assert_implements_optional(isPropertyTestable(type, memberName),
|
|
`${type}.${memberName} marked as not testable.`);
|
|
assert_true(!remaining[type].has(memberName),
|
|
`Is ${memberName} present`);
|
|
}, `${type}'s ${memberName}`);
|
|
}
|
|
}
|
|
}, 'getStats succeeds');
|
|
</script>
|