277 lines
8 KiB
HTML
277 lines
8 KiB
HTML
<!doctype html>
|
|
<meta charset=utf-8>
|
|
<meta name="timeout" content="long">
|
|
<title>Mandatory-to-implement stats compliance (a subset of webrtc-stats)</title>
|
|
<script src=/resources/testharness.js></script>
|
|
<script src=/resources/testharnessreport.js></script>
|
|
<script src="RTCPeerConnection-helper.js"></script>
|
|
<script>
|
|
'use strict';
|
|
|
|
// From https://w3c.github.io/webrtc-pc/#mandatory-to-implement-stats
|
|
|
|
const mandatory = {
|
|
RTCRtpStreamStats: [
|
|
"ssrc",
|
|
"kind",
|
|
"transportId",
|
|
"codecId",
|
|
],
|
|
RTCReceivedRtpStreamStats: [
|
|
"packetsReceived",
|
|
"packetsLost",
|
|
"jitter",
|
|
],
|
|
RTCInboundRtpStreamStats: [
|
|
"trackIdentifier",
|
|
"remoteId",
|
|
"framesDecoded",
|
|
"framesDropped",
|
|
"nackCount",
|
|
"framesReceived",
|
|
"bytesReceived",
|
|
"totalAudioEnergy",
|
|
"totalSamplesDuration",
|
|
"packetsDiscarded",
|
|
],
|
|
RTCRemoteInboundRtpStreamStats: [
|
|
"localId",
|
|
"roundTripTime",
|
|
],
|
|
RTCSentRtpStreamStats: [
|
|
"packetsSent",
|
|
"bytesSent"
|
|
],
|
|
RTCOutboundRtpStreamStats: [
|
|
"remoteId",
|
|
"framesEncoded",
|
|
"nackCount",
|
|
"framesSent"
|
|
],
|
|
RTCRemoteOutboundRtpStreamStats: [
|
|
"localId",
|
|
"remoteTimestamp",
|
|
],
|
|
RTCPeerConnectionStats: [
|
|
"dataChannelsOpened",
|
|
"dataChannelsClosed",
|
|
],
|
|
RTCDataChannelStats: [
|
|
"label",
|
|
"protocol",
|
|
"dataChannelIdentifier",
|
|
"state",
|
|
"messagesSent",
|
|
"bytesSent",
|
|
"messagesReceived",
|
|
"bytesReceived",
|
|
],
|
|
RTCMediaSourceStats: [
|
|
"trackIdentifier",
|
|
"kind"
|
|
],
|
|
RTCAudioSourceStats: [
|
|
"totalAudioEnergy",
|
|
"totalSamplesDuration"
|
|
],
|
|
RTCVideoSourceStats: [
|
|
"width",
|
|
"height",
|
|
"framesPerSecond"
|
|
],
|
|
RTCCodecStats: [
|
|
"payloadType",
|
|
/* codecType is part of MTI but is not systematically set
|
|
per https://www.w3.org/TR/webrtc-stats/#dom-rtccodecstats-codectype
|
|
If the dictionary member is not present, it means that
|
|
this media format can be both encoded and decoded. */
|
|
// "codecType",
|
|
"mimeType",
|
|
"clockRate",
|
|
"channels",
|
|
"sdpFmtpLine",
|
|
],
|
|
RTCTransportStats: [
|
|
"bytesSent",
|
|
"bytesReceived",
|
|
"selectedCandidatePairId",
|
|
"localCertificateId",
|
|
"remoteCertificateId",
|
|
],
|
|
RTCIceCandidatePairStats: [
|
|
"transportId",
|
|
"localCandidateId",
|
|
"remoteCandidateId",
|
|
"state",
|
|
"nominated",
|
|
"bytesSent",
|
|
"bytesReceived",
|
|
"totalRoundTripTime",
|
|
"responsesReceived",
|
|
"currentRoundTripTime"
|
|
],
|
|
RTCIceCandidateStats: [
|
|
"address",
|
|
"port",
|
|
"protocol",
|
|
"candidateType",
|
|
/* url requires a STUN or TURN server so is not testable. */
|
|
// "url",
|
|
],
|
|
RTCCertificateStats: [
|
|
"fingerprint",
|
|
"fingerprintAlgorithm",
|
|
"base64Certificate",
|
|
/* issuerCertificateId is part of MTI but is not systematically set
|
|
per https://www.w3.org/TR/webrtc-stats/#dom-rtccertificatestats-issuercertificateid
|
|
If the current certificate is at the end of the chain
|
|
(i.e. a self-signed certificate), this will not be set. */
|
|
// "issuerCertificateId",
|
|
],
|
|
};
|
|
|
|
// 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"
|
|
},
|
|
"track": {
|
|
video: "RTCSenderVideoTrackAttachmentStats",
|
|
audio: "RTCSenderAudioTrackAttachmentStats"
|
|
},
|
|
"sender": {
|
|
audio: "RTCAudioSenderStats",
|
|
video: "RTCVideoSenderStats"
|
|
},
|
|
"receiver": {
|
|
audio: "RTCAudioReceiverStats",
|
|
video: "RTCVideoReceiverStats",
|
|
},
|
|
"transport": "RTCTransportStats",
|
|
"candidate-pair": "RTCIceCandidatePairStats",
|
|
"local-candidate": "RTCIceCandidateStats",
|
|
"remote-candidate": "RTCIceCandidateStats",
|
|
"certificate": "RTCCertificateStats",
|
|
};
|
|
|
|
// From https://w3c.github.io/webrtc-stats/webrtc-stats.html (webidl)
|
|
|
|
const parents = {
|
|
RTCVideoSourceStats: "RTCMediaSourceStats",
|
|
RTCAudioSourceStats: "RTCMediaSourceStats",
|
|
RTCReceivedRtpStreamStats: "RTCRtpStreamStats",
|
|
RTCInboundRtpStreamStats: "RTCReceivedRtpStreamStats",
|
|
RTCRemoteInboundRtpStreamStats: "RTCReceivedRtpStreamStats",
|
|
RTCSentRtpStreamStats: "RTCRtpStreamStats",
|
|
RTCOutboundRtpStreamStats: "RTCSentRtpStreamStats",
|
|
RTCRemoteOutboundRtpStreamStats : "RTCSentRtpStreamStats",
|
|
};
|
|
|
|
const remaining = JSON.parse(JSON.stringify(mandatory));
|
|
for (const dictName in remaining) {
|
|
remaining[dictName] = new Set(remaining[dictName]);
|
|
}
|
|
|
|
async function getAllStats(t, pc) {
|
|
// Try to obtain as many stats as possible, waiting up to 20 seconds for
|
|
// roundTripTime of RTCRemoteInboundRtpStreamStats and
|
|
// remoteTimestamp of RTCRemoteOutboundRtpStreamStats which can take
|
|
// several RTCP messages to calculate.
|
|
let stats;
|
|
let remoteInboundFound = false;
|
|
let remoteOutboundFound = false;
|
|
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));
|
|
if (remoteInboundAudio && "roundTripTime" in remoteInboundAudio &&
|
|
remoteInboundVideo && "roundTripTime" in remoteInboundVideo) {
|
|
remoteInboundFound = true;
|
|
}
|
|
const [remoteOutboundAudio, remoteOutboundVideo] = ["audio", "video"].map(
|
|
kind => values.find(s =>
|
|
s.type == "remote-outbound-rtp" && s.kind == kind));
|
|
if (remoteOutboundAudio && "remoteTimestamp" in remoteOutboundAudio &&
|
|
remoteOutboundVideo && "remoteTimestamp" in remoteOutboundVideo) {
|
|
remoteOutboundFound = true;
|
|
}
|
|
if (remoteInboundFound && remoteOutboundFound) {
|
|
return stats;
|
|
}
|
|
await new Promise(r => t.step_timeout(r, 1000));
|
|
}
|
|
return stats;
|
|
}
|
|
|
|
promise_test(async t => {
|
|
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});
|
|
|
|
const stream = await getNoiseStream({video: true, audio:true});
|
|
for (const track of stream.getTracks()) {
|
|
pc1.addTrack(track, stream);
|
|
pc2.addTrack(track, stream);
|
|
t.add_cleanup(() => track.stop());
|
|
}
|
|
exchangeIceCandidates(pc1, pc2);
|
|
await exchangeOfferAnswer(pc1, pc2);
|
|
const stats = await getAllStats(t, pc1);
|
|
|
|
// 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 mandatory members in getStats().
|
|
|
|
test(t => {
|
|
for (const stat of stats.values()) {
|
|
let dictName = dictionaryNames[stat.type];
|
|
if (!dictName) continue;
|
|
if (typeof dictName == "object") {
|
|
dictName = dictName[stat.kind];
|
|
}
|
|
|
|
assert_equals(typeof dictName, "string", "Test error. String.");
|
|
if (dictName && mandatory[dictName]) {
|
|
do {
|
|
const memberNames = mandatory[dictName];
|
|
const remainingNames = remaining[dictName];
|
|
assert_true(memberNames.length > 0, "Test error. Parent not found.");
|
|
for (const memberName of memberNames) {
|
|
if (memberName in stat) {
|
|
assert_not_equals(stat[memberName], undefined, "Not undefined");
|
|
remainingNames.delete(memberName);
|
|
}
|
|
}
|
|
dictName = parents[dictName];
|
|
} while (dictName);
|
|
}
|
|
}
|
|
}, "Validating stats");
|
|
|
|
for (const dictName in mandatory) {
|
|
for (const memberName of mandatory[dictName]) {
|
|
test(t => {
|
|
assert_true(!remaining[dictName].has(memberName),
|
|
`Is ${memberName} present`);
|
|
}, `${dictName}'s ${memberName}`);
|
|
}
|
|
}
|
|
}, 'getStats succeeds');
|
|
|
|
</script>
|