/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
["inbound-rtp", "outbound-rtp", "media-source"].forEach(type => {
let s = statsExpectedByType[type];
s.optional = [...s.optional, ...s.localVideoOnly, ...s.localAudioOnly];
});
// // Checks that the fields in a report conform to the expectations in // statExpectedByType // function checkExpectedFields(report) {
report.forEach(stat => {
let expectations = statsExpectedByType[stat.type];
ok(expectations, "Stats type " + stat.type + " was expected"); // If the type is not expected or if it is flagged for skipping continue to // the next if (!expectations || expectations.skip) { return;
} // Check that all required fields exist
expectations.expected.forEach(field => {
ok(
field in stat, "Expected stat field " + stat.type + "." + field + " exists"
);
}); // Check that each field is either expected or optional
let allowed = [...expectations.expected, ...expectations.optional];
Object.keys(stat).forEach(field => {
ok(
allowed.includes(field), "Stat field " +
stat.type + "." +
field +
` is allowed. ${JSON.stringify(stat)}`
);
});
// // Ensure that unimplemented fields are not implemented // note: if a field is implemented it should be moved to expected or // optional. //
expectations.unimplemented.forEach(field => {
ok(
!Object.keys(stat).includes(field), "Unimplemented field " + stat.type + "." + field + " does not exist."
);
});
// // Ensure that all deprecated fields are not present //
expectations.deprecated.forEach(field => {
ok(
!Object.keys(stat).includes(field), "Deprecated field " + stat.type + "." + field + " does not exist."
);
});
});
}
function pedanticChecks(report) { // Check that report is only-maplike
[...report.keys()].forEach(key =>
is(
report[key],
undefined,
`Report is not dictionary like, it lacks a property for key ${key}`
)
);
report.forEach((statObj, mapKey) => {
info(`"${mapKey} = ${JSON.stringify(statObj, null, 2)}`);
}); // eslint-disable-next-line complexity
report.forEach((statObj, mapKey) => {
let tested = {}; // Record what fields get tested. // To access a field foo without marking it as tested use stat.inner.foo
let stat = new Proxy(statObj, {
get(stat, key) { if (key == "inner") { return stat;
}
tested[key] = true; return stat[key];
},
});
let expectations = statsExpectedByType[stat.type];
if (expectations.skip) { return;
}
// All stats share the following attributes inherited from RTCStats
is(stat.id, mapKey, stat.type + ".id is the same as the report key.");
// timestamp
ok(stat.timestamp >= 0, stat.type + ".timestamp is not less than 0"); // If the timebase for the timestamp is not properly set the timestamp // will appear relative to the year 1970; Bug 1495446 const date = new Date(stat.timestamp);
ok(
date.getFullYear() > 1970,
`${stat.type}.timestamp is relative to current time, date=${date}`
); // // RTCStreamStats attributes with common behavior // // inbound-rtp, outbound-rtp, remote-inbound-rtp, remote-outbound-rtp // inherit from RTCStreamStats if (
[ "inbound-rtp", "outbound-rtp", "remote-inbound-rtp", "remote-outbound-rtp",
].includes(stat.type)
) { const isRemote = stat.type.startsWith("remote-"); // // Common RTCStreamStats fields //
// SSRC
ok(stat.ssrc, stat.type + ".ssrc has a value");
// kind
ok(
["audio", "video"].includes(stat.kind),
stat.type + ".kind is 'audio' or 'video'"
);
// mediaType, renamed to kind but remains for backward compability.
ok(
["audio", "video"].includes(stat.mediaType),
stat.type + ".mediaType is 'audio' or 'video'"
);
// codecId
ok(stat.codecId, `${stat.type}.codecId has a value`);
ok(report.has(stat.codecId), `codecId ${stat.codecId} exists in report`);
is(
report.get(stat.codecId).type, "codec",
`codecId ${stat.codecId} in report is codec type`
);
is(
report.get(stat.codecId).mimeType.slice(0, 5),
stat.kind,
`codecId ${stat.codecId} in report is for a mimeType of the same ` +
`media type as the referencing rtp stream stat`
);
if (isRemote) { // local id if (stat.localId) {
ok(
report.has(stat.localId),
`localId ${stat.localId} exists in report.`
);
is(
report.get(stat.localId).ssrc,
stat.ssrc, "remote ssrc and local ssrc match."
);
is(
report.get(stat.localId).remoteId,
stat.id, "local object has remote object as it's own remote object."
);
}
} else { // remote id if (stat.remoteId) {
ok(
report.has(stat.remoteId),
`remoteId ${stat.remoteId} exists in report.`
);
is(
report.get(stat.remoteId).ssrc,
stat.ssrc, "remote ssrc and local ssrc match."
);
is(
report.get(stat.remoteId).localId,
stat.id, "remote object has local object as it's own local object."
);
}
}
// nackCount if (stat.nackCount) {
ok(
stat.nackCount >= 0,
`${stat.type}.nackCount is sane (${stat.kind}).`
);
}
if (!isRemote && stat.inner.kind == "video") { // firCount
ok(
stat.firCount >= 0 && stat.firCount < 100,
`${stat.type}.firCount is a sane number for a short ` +
`${stat.kind} test. value=${stat.firCount}`
);
// pliCount
ok(
stat.pliCount >= 0 && stat.pliCount < 200,
`${stat.type}.pliCount is a sane number for a short ` +
`${stat.kind} test. value=${stat.pliCount}`
);
// qpSum if (stat.qpSum !== undefined) {
ok(
stat.qpSum >= 0,
`${stat.type}.qpSum is at least 0 ` +
`${stat.kind} test. value=${stat.qpSum}`
);
}
} else {
is(
stat.qpSum,
undefined,
`${stat.type}.qpSum does not exist when stat.kind != video`
);
}
}
if (stat.type == "inbound-rtp") { // // Required fields //
// mid
ok(
parseInt(stat.mid) >= 0,
`${stat.type}.mid is a positive integer. value=${stat.mid}`
);
let inboundRtpMids = [];
report.forEach(r => { if (r.type == "inbound-rtp") {
inboundRtpMids.push(r.mid);
}
});
is(
inboundRtpMids.filter(mid => mid == stat.mid).length,
1,
`${stat.type}.mid is distinct. value=${
stat.mid
}, others=${JSON.stringify(inboundRtpMids)}`
);
// packetsReceived
ok(
stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5,
`${stat.type}.packetsReceived is a sane number for a short ` +
`${stat.kind} test. value=${stat.packetsReceived}`
);
// packetsDiscarded
ok(
stat.packetsDiscarded >= 0 && stat.packetsDiscarded < 100,
`${stat.type}.packetsDiscarded is sane number for a short test. ` +
`value=${stat.packetsDiscarded}`
); // bytesReceived
ok(
stat.bytesReceived >= 0 && stat.bytesReceived < 10 ** 9, // Not a magic number, just a guess
`${stat.type}.bytesReceived is a sane number for a short ` +
`${stat.kind} test. value=${stat.bytesReceived}`
);
// packetsLost
ok(
stat.packetsLost < 100,
`${stat.type}.packetsLost is a sane number for a short ` +
`${stat.kind} test. value=${stat.packetsLost}`
);
// This should be much lower for audio, TODO: Bug 1330575
let expectedJitter = stat.kind == "video" ? 0.5 : 1; // jitter
ok(
stat.jitter < expectedJitter,
`${stat.type}.jitter is sane number for a ${stat.kind} ` +
`local only test. value=${stat.jitter}`
);
// lastPacketReceivedTimestamp
ok(
stat.lastPacketReceivedTimestamp !== undefined,
`${stat.type}.lastPacketReceivedTimestamp has a value`
);
// headerBytesReceived
ok(
stat.headerBytesReceived >= 0 && stat.headerBytesReceived < 500000,
`${stat.type}.headerBytesReceived is sane for a short test. ` +
`value=${stat.headerBytesReceived}`
);
// Always missing from libwebrtc stats // estimatedPlayoutTimestamp // ok( // stat.estimatedPlayoutTimestamp !== undefined, // `${stat.type}.estimatedPlayoutTimestamp has a value` // );
// jitterBufferEmittedCount
ok(
stat.jitterBufferEmittedCount > 0,
`${stat.type}.jitterBufferEmittedCount is a sane number for a short ` +
`${stat.kind} test. value=${stat.jitterBufferEmittedCount}`
);
// jitterBufferDelay
let avgJitterBufferDelay =
stat.jitterBufferDelay / stat.jitterBufferEmittedCount;
ok(
avgJitterBufferDelay > 0 && avgJitterBufferDelay < 10,
`${stat.type}.jitterBufferDelay is a sane number for a short ` +
`${stat.kind} test. value=${stat.jitterBufferDelay}/${stat.jitterBufferEmittedCount}=${avgJitterBufferDelay}`
);
// // Optional fields //
// // Local audio only stats // if (stat.inner.kind != "audio") {
expectations.localAudioOnly.forEach(field => {
ok(
stat[field] === undefined,
`${stat.type} does not have field ${field}` +
` when kind is not 'audio'`
);
});
} else {
expectations.localAudioOnly.forEach(field => {
ok(
stat.inner[field] !== undefined,
stat.type + " has field " + field + " when kind is video"
);
}); // totalSamplesReceived
ok(
stat.totalSamplesReceived > 1000,
`${stat.type}.totalSamplesReceived is a sane number for a short ` +
`${stat.kind} test. value=${stat.totalSamplesReceived}`
);
// fecPacketsReceived
ok(
stat.fecPacketsReceived >= 0 && stat.fecPacketsReceived < 10 ** 5,
`${stat.type}.fecPacketsReceived is a sane number for a short ` +
`${stat.kind} test. value=${stat.fecPacketsReceived}`
);
// fecPacketsDiscarded
ok(
stat.fecPacketsDiscarded >= 0 && stat.fecPacketsDiscarded < 100,
`${stat.type}.fecPacketsDiscarded is sane number for a short test. ` +
`value=${stat.fecPacketsDiscarded}`
); // concealedSamples
ok(
stat.concealedSamples >= 0 &&
stat.concealedSamples <= stat.totalSamplesReceived,
`${stat.type}.concealedSamples is a sane number for a short ` +
`${stat.kind} test. value=${stat.concealedSamples}`
);
// silentConcealedSamples
ok(
stat.silentConcealedSamples >= 0 &&
stat.silentConcealedSamples <= stat.concealedSamples,
`${stat.type}.silentConcealedSamples is a sane number for a short ` +
`${stat.kind} test. value=${stat.silentConcealedSamples}`
);
// concealmentEvents
ok(
stat.concealmentEvents >= 0 &&
stat.concealmentEvents <= stat.packetsReceived,
`${stat.type}.concealmentEvents is a sane number for a short ` +
`${stat.kind} test. value=${stat.concealmentEvents}`
);
// insertedSamplesForDeceleration
ok(
stat.insertedSamplesForDeceleration >= 0 &&
stat.insertedSamplesForDeceleration <= stat.totalSamplesReceived,
`${stat.type}.insertedSamplesForDeceleration is a sane number for a short ` +
`${stat.kind} test. value=${stat.insertedSamplesForDeceleration}`
);
// removedSamplesForAcceleration
ok(
stat.removedSamplesForAcceleration >= 0 &&
stat.removedSamplesForAcceleration <= stat.totalSamplesReceived,
`${stat.type}.removedSamplesForAcceleration is a sane number for a short ` +
`${stat.kind} test. value=${stat.removedSamplesForAcceleration}`
);
// audioLevel
ok(
stat.audioLevel >= 0 && stat.audioLevel <= 128,
`${stat.type}.bytesReceived is a sane number for a short ` +
`${stat.kind} test. value=${stat.audioLevel}`
);
// totalAudioEnergy
ok(
stat.totalAudioEnergy >= 0 && stat.totalAudioEnergy <= 128,
`${stat.type}.totalAudioEnergy is a sane number for a short ` +
`${stat.kind} test. value=${stat.totalAudioEnergy}`
);
// totalSamplesDuration
ok(
stat.totalSamplesDuration >= 0 && stat.totalSamplesDuration <= 300,
`${stat.type}.totalSamplesDuration is a sane number for a short ` +
`${stat.kind} test. value=${stat.totalSamplesDuration}`
);
}
// // Local video only stats // if (stat.inner.kind != "video") {
expectations.localVideoOnly.forEach(field => {
ok(
stat[field] === undefined,
`${stat.type} does not have field ${field}` +
` when kind is not 'video'`
);
});
} else {
expectations.localVideoOnly.forEach(field => {
ok(
stat.inner[field] !== undefined,
stat.type + " has field " + field + " when kind is video"
);
}); // discardedPackets
ok(
stat.discardedPackets < 100,
`${stat.type}.discardedPackets is a sane number for a short test. ` +
`value=${stat.discardedPackets}`
); // framesPerSecond
ok(
stat.framesPerSecond > 0 && stat.framesPerSecond < 70,
`${stat.type}.framesPerSecond is a sane number for a short ` +
`${stat.kind} test. value=${stat.framesPerSecond}`
);
// framesDecoded
ok(
stat.framesDecoded > 0 && stat.framesDecoded < 1000000,
`${stat.type}.framesDecoded is a sane number for a short ` +
`${stat.kind} test. value=${stat.framesDecoded}`
);
// framesDropped
ok(
stat.framesDropped >= 0 && stat.framesDropped < 100,
`${stat.type}.framesDropped is a sane number for a short ` +
`${stat.kind} test. value=${stat.framesDropped}`
);
// frameWidth
ok(
stat.frameWidth > 0 && stat.frameWidth < 100000,
`${stat.type}.frameWidth is a sane number for a short ` +
`${stat.kind} test. value=${stat.frameWidth}`
);
// frameHeight
ok(
stat.frameHeight > 0 && stat.frameHeight < 100000,
`${stat.type}.frameHeight is a sane number for a short ` +
`${stat.kind} test. value=${stat.frameHeight}`
);
// totalDecodeTime
ok(
stat.totalDecodeTime >= 0 && stat.totalDecodeTime < 300,
`${stat.type}.totalDecodeTime is sane for a short test. ` +
`value=${stat.totalDecodeTime}`
);
// totalProcessingDelay
ok(
stat.totalProcessingDelay < 1000,
`${stat.type}.totalProcessingDelay is sane number for a short test ` +
`local only test. value=${stat.totalProcessingDelay}`
);
// totalInterFrameDelay
ok(
stat.totalInterFrameDelay >= 0 && stat.totalInterFrameDelay < 1000,
`${stat.type}.totalInterFrameDelay is sane for a short test. ` +
`value=${stat.totalInterFrameDelay}`
);
// totalSquaredInterFrameDelay
ok(
stat.totalSquaredInterFrameDelay >= 0 &&
stat.totalSquaredInterFrameDelay < 10000,
`${stat.type}.totalSquaredInterFrameDelay is sane for a short test. ` +
`value=${stat.totalSquaredInterFrameDelay}`
);
// framesReceived
ok(
stat.framesReceived >= 0 && stat.framesReceived < 100000,
`${stat.type}.framesReceived is a sane number for a short ` +
`${stat.kind} test. value=${stat.framesReceived}`
);
}
} elseif (stat.type == "remote-inbound-rtp") { // roundTripTime
ok(
stat.roundTripTime >= 0,
`${stat.type}.roundTripTime is sane with` +
`value of: ${stat.roundTripTime} (${stat.kind})`
); // // Required fields //
// packetsLost
ok(
stat.packetsLost < 100,
`${stat.type}.packetsLost is a sane number for a short ` +
`${stat.kind} test. value=${stat.packetsLost}`
);
// jitter
ok(
stat.jitter >= 0,
`${stat.type}.jitter is sane number (${stat.kind}). ` +
`value=${stat.jitter}`
);
// // Optional fields //
// packetsReceived if (stat.packetsReceived) {
ok(
stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5,
`${stat.type}.packetsReceived is a sane number for a short ` +
`${stat.kind} test. value=${stat.packetsReceived}`
);
}
// totalRoundTripTime
ok(
stat.totalRoundTripTime < 50000,
`${stat.type}.totalRoundTripTime is a sane number for a short ` +
`${stat.kind} test. value=${stat.totalRoundTripTime}`
);
// fractionLost
ok(
stat.fractionLost < 0.2,
`${stat.type}.fractionLost is a sane number for a short ` +
`${stat.kind} test. value=${stat.fractionLost}`
);
// roundTripTimeMeasurements
ok(
stat.roundTripTimeMeasurements >= 1 &&
stat.roundTripTimeMeasurements < 500,
`${stat.type}.roundTripTimeMeasurements is a sane number for a short ` +
`${stat.kind} test. value=${stat.roundTripTimeMeasurements}`
);
} elseif (stat.type == "outbound-rtp") { // // Required fields //
// packetsSent
ok(
stat.packetsSent > 0 && stat.packetsSent < 10000,
`${stat.type}.packetsSent is a sane number for a short ` +
`${stat.kind} test. value=${stat.packetsSent}`
);
// bytesSent const audio1Min = 16000 * 60; // 128kbps const video1Min = 250000 * 60; // 2Mbps
ok(
stat.bytesSent > 0 &&
stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min),
`${stat.type}.bytesSent is a sane number for a short ` +
`${stat.kind} test. value=${stat.bytesSent}`
);
// headerBytesSent
ok(
stat.headerBytesSent > 0 &&
stat.headerBytesSent < (stat.kind == "video" ? video1Min : audio1Min),
`${stat.type}.headerBytesSent is a sane number for a short ` +
`${stat.kind} test. value=${stat.headerBytesSent}`
);
// retransmittedPacketsSent
ok(
stat.retransmittedPacketsSent >= 0 &&
stat.retransmittedPacketsSent <
(stat.kind == "video" ? video1Min : audio1Min),
`${stat.type}.retransmittedPacketsSent is a sane number for a short ` +
`${stat.kind} test. value=${stat.retransmittedPacketsSent}`
);
// retransmittedBytesSent
ok(
stat.retransmittedBytesSent >= 0 &&
stat.retransmittedBytesSent <
(stat.kind == "video" ? video1Min : audio1Min),
`${stat.type}.retransmittedBytesSent is a sane number for a short ` +
`${stat.kind} test. value=${stat.retransmittedBytesSent}`
);
// // Optional fields //
// rid if (stat.kind == "audio") {
ok(
stat.rid === undefined,
`${stat.type}.rid" MUST NOT exist for audio. value=${stat.rid}`
);
} else {
let numSendVideoStreamsForMid = 0;
report.forEach(r => { if (
r.type == "outbound-rtp" &&
r.kind == "video" &&
r.mid == stat.mid
) {
numSendVideoStreamsForMid += 1;
}
}); if (numSendVideoStreamsForMid == 1) {
is(
stat.rid,
undefined,
`${stat.type}.rid" does not exist for singlecast video. value=${stat.rid}`
);
} else {
isnot(
stat.rid,
undefined,
`${stat.type}.rid" does exist for simulcast video. value=${stat.rid}`
);
}
}
// qpSum // This is supported for all of our vpx codecs and AV1 (on the encode // side, see bug 1519590) const mimeType = report.get(stat.codecId).mimeType; if (mimeType.includes("VP") || mimeType.includes("AV1")) {
ok(
stat.qpSum >= 0,
`${stat.type}.qpSum is a sane number (${stat.kind}) ` +
`for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}`
);
} elseif (mimeType.includes("H264")) { // OpenH264 encoder records QP so we check for either condition. if (!stat.qpSum && !("qpSum" in stat)) {
ok(
!stat.qpSum && !("qpSum" in stat),
`${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}`
);
} else {
ok(
stat.qpSum >= 0,
`${stat.type}.qpSum is a sane number (${stat.kind}) ` +
`for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}`
);
}
} else {
ok(
!stat.qpSum && !("qpSum" in stat),
`${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}`
);
}
// // Local video only stats // if (stat.inner.kind != "video") {
expectations.localVideoOnly.forEach(field => {
ok(
stat[field] === undefined,
`${stat.type} does not have field ` +
`${field} when kind is not 'video'`
);
});
} else {
expectations.localVideoOnly.forEach(field => {
ok(
stat.inner[field] !== undefined,
`${stat.type} has field ` +
`${field} when kind is video and isRemote is false`
);
});
// framesEncoded
ok(
stat.framesEncoded >= 0 && stat.framesEncoded < 100000,
`${stat.type}.framesEncoded is a sane number for a short ` +
`${stat.kind} test. value=${stat.framesEncoded}`
);
// frameWidth
ok(
stat.frameWidth >= 0 && stat.frameWidth < 100000,
`${stat.type}.frameWidth is a sane number for a short ` +
`${stat.kind} test. value=${stat.frameWidth}`
);
// frameHeight
ok(
stat.frameHeight >= 0 && stat.frameHeight < 100000,
`${stat.type}.frameHeight is a sane number for a short ` +
`${stat.kind} test. value=${stat.frameHeight}`
);
// framesPerSecond
ok(
stat.framesPerSecond >= 0 && stat.framesPerSecond < 60,
`${stat.type}.framesPerSecond is a sane number for a short ` +
`${stat.kind} test. value=${stat.framesPerSecond}`
);
// framesSent
ok(
stat.framesSent >= 0 && stat.framesSent < 100000,
`${stat.type}.framesSent is a sane number for a short ` +
`${stat.kind} test. value=${stat.framesSent}`
);
// hugeFramesSent
ok(
stat.hugeFramesSent >= 0 && stat.hugeFramesSent < 100000,
`${stat.type}.hugeFramesSent is a sane number for a short ` +
`${stat.kind} test. value=${stat.hugeFramesSent}`
);
// totalEncodeTime
ok(
stat.totalEncodeTime >= 0,
`${stat.type}.totalEncodeTime is a sane number for a short ` +
`${stat.kind} test. value=${stat.totalEncodeTime}`
);
// totalEncodedBytesTarget
ok(
stat.totalEncodedBytesTarget > 1000,
`${stat.type}.totalEncodedBytesTarget is a sane number for a short ` +
`${stat.kind} test. value=${stat.totalEncodedBytesTarget}`
);
}
} elseif (stat.type == "remote-outbound-rtp") { // // Required fields //
// packetsSent
ok(
stat.packetsSent > 0 && stat.packetsSent < 10000,
`${stat.type}.packetsSent is a sane number for a short ` +
`${stat.kind} test. value=${stat.packetsSent}`
);
// bytesSent const audio1Min = 16000 * 60; // 128kbps const video1Min = 250000 * 60; // 2Mbps
ok(
stat.bytesSent > 0 &&
stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min),
`${stat.type}.bytesSent is a sane number for a short ` +
`${stat.kind} test. value=${stat.bytesSent}`
);
ok(
stat.remoteTimestamp !== undefined,
`${stat.type}.remoteTimestamp ` + `is not undefined (${stat.kind})`
); const ageSeconds = (stat.timestamp - stat.remoteTimestamp) / 1000; // remoteTimestamp is exact (so it can be mapped to a packet), whereas // timestamp has reduced precision. It is possible that // remoteTimestamp occurs a millisecond into the future from // timestamp. We also subtract half a millisecond when reducing // precision on libwebrtc timestamps, to counteract the potential // rounding up that libwebrtc may do since it tends to round its // internal timestamps to whole milliseconds. In the worst case // remoteTimestamp may therefore occur 2 milliseconds ahead of // timestamp.
ok(
ageSeconds >= -0.002 && ageSeconds < 30,
`${stat.type}.remoteTimestamp is on the same timeline as ` +
`${stat.type}.timestamp, and no older than 30 seconds. ` +
`difference=${ageSeconds}s`
);
} elseif (stat.type == "media-source") { // trackIdentifier
is(typeof stat.trackIdentifier, "string");
isnot(stat.trackIdentifier, "");
// kind
is(typeof stat.kind, "string");
ok(stat.kind == "audio" || stat.kind == "video"); if (stat.inner.kind == "video") {
expectations.localVideoOnly.forEach(field => {
ok(
stat.inner[field] !== undefined,
`${stat.type} has field ` +
`${field} when kind is video and isRemote is false`
);
});
// frames
ok(
stat.frames >= 0 && stat.frames < 100000,
`${stat.type}.frames is a sane number for a short ` +
`${stat.kind} test. value=${stat.frames}`
);
// framesPerSecond
ok(
stat.framesPerSecond >= 0 && stat.framesPerSecond < 100,
`${stat.type}.framesPerSecond is a sane number for a short ` +
`${stat.kind} test. value=${stat.framesPerSecond}`
);
// width
ok(
stat.width >= 0 && stat.width < 1000000,
`${stat.type}.width is a sane number for a ` +
`${stat.kind} test. value=${stat.width}`
);
// height
ok(
stat.height >= 0 && stat.height < 1000000,
`${stat.type}.height is a sane number for a ` +
`${stat.kind} test. value=${stat.height}`
);
} else {
expectations.localVideoOnly.forEach(field => {
ok(
stat[field] === undefined,
`${stat.type} does not have field ` +
`${field} when kind is not 'video'`
);
});
}
} elseif (stat.type == "codec") { // // Required fields //
// mimeType & payloadType switch (stat.mimeType) { case"audio/opus":
is(stat.payloadType, 109, "codec.payloadType for opus"); break; case"video/VP8":
is(stat.payloadType, 120, "codec.payloadType for VP8"); break; case"video/VP9":
is(stat.payloadType, 121, "codec.payloadType for VP9"); break; case"video/H264":
ok(
stat.payloadType == 97 ||
stat.payloadType == 126 ||
stat.payloadType == 103 ||
stat.payloadType == 105,
`codec.payloadType for H264 was ${stat.payloadType}, exp. 97, 126, 103, or 105`
); break; case"video/AV1":
is(stat.payloadType, 99, "codec.payloadType for AV1"); break; default:
ok( false,
`Unexpected codec.mimeType ${stat.mimeType} for payloadType ` +
`${stat.payloadType}`
); break;
}
// transportId // (no transport stats yet)
ok(stat.transportId, "codec.transportId is set");
// clockRate if (stat.mimeType.startsWith("audio")) {
is(stat.clockRate, 48000, "codec.clockRate for audio/opus");
} elseif (stat.mimeType.startsWith("video")) {
is(stat.clockRate, 90000, "codec.clockRate for video");
}
// sdpFmtpLine // (not technically mandated by spec, but expected here) // AV1 has no required parameters, so don't require sdpFmtpLine for it. if (stat.mimeType != "video/AV1") {
ok(stat.sdpFmtpLine, "codec.sdp FmtpLine is set");
} const opusParams = [ "maxplaybackrate", "maxaveragebitrate", "usedtx", "stereo", "useinbandfec", "cbr", "ptime", "minptime", "maxptime",
]; const vpxParams = ["max-fs", "max-fr"]; const h264Params = [ "packetization-mode", "level-asymmetry-allowed", "profile-level-id", "max-fs", "max-cpb", "max-dpb", "max-br", "max-mbps",
]; // AV1 parameters: // https://aomediacodec.github.io/av1-rtp-spec/#721-mapping-of-media-subtype-parameters-to-sdp const av1Params = ["profile", "level-idx", "tier"]; // Check that the parameters are as expected. AV1 may have no parameters. for (const param of (stat.sdpFmtpLine || "").split(";")) { const [key, value] = param.split("="); if (stat.payloadType == 99) { // AV1 might not have any parameters, if it does make sure they are as expected. if (key) {
ok(
av1Params.includes(key),
`codec.sdpFmtpLine param ${key}=${value} for AV1`
);
}
} elseif (stat.payloadType == 109) {
ok(
opusParams.includes(key),
`codec.sdpFmtpLine param ${key}=${value} for opus`
);
} elseif (stat.payloadType == 120 || stat.payloadType == 121) {
ok(
vpxParams.includes(key),
`codec.sdpFmtpLine param ${key}=${value} for VPx`
);
} elseif (stat.payloadType == 97 || stat.payloadType == 126) {
ok(
h264Params.includes(key),
`codec.sdpFmtpLine param ${key}=${value} for H264`
); if (key == "packetization-mode") { if (stat.payloadType == 97) {
is(value, "0", "codec.sdpFmtpLine: H264 (97) packetization-mode");
} elseif (stat.payloadType == 126) {
is(
value, "1", "codec.sdpFmtpLine: H264 (126) packetization-mode"
);
}
} if (key == "profile-level-id") {
is(value, "42e01f", "codec.sdpFmtpLine: H264 profile-level-id");
}
}
}
// // Optional fields //
// codecType
ok(
!Object.keys(stat).includes("codecType") ||
stat.codecType == "encode" ||
stat.codecType == "decode", "codec.codecType (${codec.codecType}) is an expected value or absent"
);
let numRecvStreams = 0;
let numSendStreams = 0; const counts = { "inbound-rtp": 0, "outbound-rtp": 0, "remote-inbound-rtp": 0, "remote-outbound-rtp": 0,
}; const [kind] = stat.mimeType.split("/");
report.forEach(other => { if (other.type == "inbound-rtp" && other.kind == kind) {
numRecvStreams += 1;
} elseif (other.type == "outbound-rtp" && other.kind == kind) {
numSendStreams += 1;
} if (other.codecId == stat.id) {
counts[other.type] += 1;
}
}); const expectedCounts = {
encode: { "inbound-rtp": 0, "outbound-rtp": numSendStreams, "remote-inbound-rtp": numSendStreams, "remote-outbound-rtp": 0,
},
decode: { "inbound-rtp": numRecvStreams, "outbound-rtp": 0, "remote-inbound-rtp": 0, "remote-outbound-rtp": numRecvStreams,
},
absent: { "inbound-rtp": numRecvStreams, "outbound-rtp": numSendStreams, "remote-inbound-rtp": numSendStreams, "remote-outbound-rtp": numRecvStreams,
},
}; // Note that the logic above assumes at most one sender and at most one // receiver was used to generate this stats report. If more senders or // receivers are present, they'd be referring to not only this codec stat, // skewing `numSendStreams` and `numRecvStreams` above. // This could be fixed when we support `senderId` and `receiverId` in // RTCOutboundRtpStreamStats and RTCInboundRtpStreamStats respectively. for (const [key, value] of Object.entries(counts)) {
is(
value,
expectedCounts[stat.codecType || "absent"][key],
`codec.codecType ${stat.codecType || "absent"} ref from ${key} stat`
);
}
// channels if (stat.mimeType.startsWith("audio")) {
ok(stat.channels, "codec.channels should exist for audio"); if (stat.channels) { if (stat.sdpFmtpLine.includes("stereo=1")) {
is(stat.channels, 2, "codec.channels for stereo audio");
} else {
is(stat.channels, 1, "codec.channels for mono audio");
}
}
} else {
ok(!stat.channels, "codec.channels should not exist for video");
}
} elseif (stat.type == "candidate-pair") {
info("candidate-pair is: " + JSON.stringify(stat)); // // Required fields //
// transportId
ok(
stat.transportId,
`${stat.type}.transportId has a value. value=` +
`${stat.transportId} (${stat.kind})`
);
// localCandidateId
ok(
stat.localCandidateId,
`${stat.type}.localCandidateId has a value. value=` +
`${stat.localCandidateId} (${stat.kind})`
);
// remoteCandidateId
ok(
stat.remoteCandidateId,
`${stat.type}.remoteCandidateId has a value. value=` +
`${stat.remoteCandidateId} (${stat.kind})`
);
// priority
ok(
stat.priority,
`${stat.type}.priority has a value. value=` +
`${stat.priority} (${stat.kind})`
);
// state if (
stat.state == "succeeded" &&
stat.selected !== undefined &&
stat.selected
) {
info("candidate-pair state is succeeded and selected is true"); // nominated
ok(
stat.nominated,
`${stat.type}.nominated is true. value=${stat.nominated} ` +
`(${stat.kind})`
);
// bytesSent
ok(
stat.bytesSent > 100,
`${stat.type}.bytesSent is a sane number (>100) if media is flowing. ` +
`value=${stat.bytesSent}`
);
// bytesReceived
ok(
stat.bytesReceived > 100,
`${stat.type}.bytesReceived is a sane number (>100) if media is flowing. ` +
`value=${stat.bytesReceived}`
);
// lastPacketSentTimestamp
ok(
stat.lastPacketSentTimestamp,
`${stat.type}.lastPacketSentTimestamp has a value. value=` +
`${stat.lastPacketSentTimestamp} (${stat.kind})`
);
// // Ensure everything was tested //
[...expectations.expected, ...expectations.optional].forEach(field => {
ok(
Object.keys(tested).includes(field),
`${stat.type}.${field} was tested.`
);
});
});
}
function dumpStats(stats) { const dict = {}; for (const [k, v] of stats.entries()) {
dict[k] = v;
}
info(`Got stats: ${JSON.stringify(dict)}`);
}
async function waitForSyncedRtcp(pc) { // Ensures that RTCP is present
let ensureSyncedRtcp = async () => {
let report = await pc.getStats(); for (const v of report.values()) { if (v.type.endsWith("bound-rtp") && !(v.remoteId || v.localId)) {
info(`${v.id} is missing remoteId or localId: ${JSON.stringify(v)}`); returnnull;
} if (v.type == "remote-inbound-rtp" && v.roundTripTime === undefined) {
info(`${v.id} is missing roundTripTime: ${JSON.stringify(v)}`); returnnull;
}
} return report;
}; // Returns true if there is proof in aStats of rtcp flow for all remote stats // objects, compared to baseStats. const hasAllRtcpUpdated = (baseStats, stats) => {
let hasRtcpStats = false; for (const v of stats.values()) { if (v.type == "remote-outbound-rtp") {
hasRtcpStats = true; if (!v.remoteTimestamp) { // `remoteTimestamp` is 0 or not present. returnfalse;
} if (v.remoteTimestamp <= baseStats.get(v.id)?.remoteTimestamp) { // `remoteTimestamp` has not advanced further than the base stats, // i.e., no new sender report has been received. returnfalse;
}
} elseif (v.type == "remote-inbound-rtp") {
hasRtcpStats = true; // The ideal thing here would be to check `reportsReceived`, but it's // not yet implemented. if (!v.packetsReceived) { // `packetsReceived` is 0 or not present. returnfalse;
} if (v.packetsReceived <= baseStats.get(v.id)?.packetsReceived) { // `packetsReceived` has not advanced further than the base stats, // i.e., no new receiver report has been received. returnfalse;
}
}
} return hasRtcpStats;
};
let attempts = 0; const baseStats = await pc.getStats(); // Time-units are MS const waitPeriod = 100; const maxTime = 20000; for (let totalTime = maxTime; totalTime > 0; totalTime -= waitPeriod) { try {
let syncedStats = await ensureSyncedRtcp(); if (syncedStats && hasAllRtcpUpdated(baseStats, syncedStats)) {
dumpStats(syncedStats); return syncedStats;
}
} catch (e) {
info(e);
info(e.stack); throw e;
}
attempts += 1;
info(`waitForSyncedRtcp: no sync on attempt ${attempts}, retrying.`);
await wait(waitPeriod);
} throw Error( "Waiting for synced RTCP timed out after at least " + maxTime + "ms"
);
}
function checkSendCodecsMimeType(senderStats, mimeType, sdpFmtpLine = null) { const codecReports = senderStats.values().filter(s => s.type == "codec");
isnot(codecReports.length, 0, "Should have send codecs"); for (const c of codecReports) {
is(c.codecType, "encode", "Send codec is always encode");
is(c.mimeType, mimeType, "Mime type as expected"); if (sdpFmtpLine) {
is(c.sdpFmtpLine, sdpFmtpLine, "Sdp fmtp line as expected");
}
}
}
function checkSenderStats(senderStats, streamCount) { const outboundRtpReports = []; const remoteInboundRtpReports = []; for (const v of senderStats.values()) { if (v.type == "outbound-rtp") {
outboundRtpReports.push(v);
} elseif (v.type == "remote-inbound-rtp") {
remoteInboundRtpReports.push(v);
}
}
is(
outboundRtpReports.length,
streamCount,
`Sender with ${streamCount} simulcast streams has ${streamCount} outbound-rtp reports`
);
is(
remoteInboundRtpReports.length,
streamCount,
`Sender with ${streamCount} simulcast streams has ${streamCount} remote-inbound-rtp reports`
); for (const outboundRtpReport of outboundRtpReports) {
is(
outboundRtpReports.filter(r => r.ssrc == outboundRtpReport.ssrc).length,
1, "Simulcast send track SSRCs are distinct"
);
is(
outboundRtpReports.filter(r => r.mid == outboundRtpReport.mid).length,
streamCount, "Simulcast send track MIDs are identical"
); if (outboundRtpReport.kind == "video" && streamCount > 1) {
is(
outboundRtpReports.filter(r => r.rid == outboundRtpReport.rid).length,
1, "Simulcast send track RIDs are distinct"
);
} const remoteReports = remoteInboundRtpReports.filter(
r => r.id == outboundRtpReport.remoteId
);
is(
remoteReports.length,
1, "Simulcast send tracks have exactly one remote counterpart"
); const remoteInboundRtpReport = remoteReports[0];
is(
outboundRtpReport.ssrc,
remoteInboundRtpReport.ssrc, "SSRC matches for outbound-rtp and remote-inbound-rtp"
);
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.