blob: 881b17987ed7d8599f4c233d0ca2ba4849eddc1e [file] [log] [blame]
/**
* Copyright 2016 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
/**
* Constructs an RTCStats dictionary by merging the parent RTCStats object with
* the dictionary of RTCStats members defined for this dictionary.
*/
function RTCStats(parent, membersObject) {
if (parent != null)
Object.assign(this, parent);
Object.assign(this, membersObject);
}
const Presence = {
MANDATORY: true,
OPTIONAL: false,
};
/**
* According to spec, multiple stats dictionaries can have the same
* "RTCStats.type". For example, "track" refers to any of
* RTCSenderVideoTrackAttachmentStats, RTCSenderAudioTrackAttachmentStats,
* RTCReceiverVideoTrackAttachmentStats and
* RTCReceiverAudioTrackAttachmentStats. Inspection is needed to determine which
* dictionary applies to the object; for simplicity, this class merges all of
* the associated dictionaries into a single dictionary.
*/
class MergedRTCStats {
constructor(presence, type, stats) {
this.presence_ = presence;
this.type_ = type;
this.stats_ = stats;
}
presence() {
return this.presence_;
}
type() {
return this.type_;
}
stats() {
return this.stats_;
}
merge(presence, stats) {
if (presence)
this.presence_ = true;
Object.assign(this.stats_, stats);
}
}
/**
* Maps "RTCStats.type" values to MergedRTCStats. These are descriptions of
* whitelisted (allowed to be exposed to the web) RTCStats-derived dictionaries,
* see RTCStats definitions below.
* @private
*/
const gStatsWhitelist = new Map();
function addRTCStatsToWhitelist(presence, type, stats) {
mergedStats = gStatsWhitelist.get(type);
if (!mergedStats) {
gStatsWhitelist.set(type, new MergedRTCStats(presence, type, stats));
} else {
mergedStats.merge(presence, stats);
}
}
/**
* RTCRtpStreamStats
* https://w3c.github.io/webrtc-stats/#streamstats-dict*
* @private
*/
let kRTCRtpStreamStats = new RTCStats(null, {
ssrc: 'number',
isRemote: 'boolean', // Obsolete, type reveals if "remote-" or not.
kind: 'string',
mediaType: 'string', // Obsolete, replaced by |kind|.
transportId: 'string',
codecId: 'string',
});
/**
* RTCReceivedRtpStreamStats
* https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats
* @private
*/
let kRTCReceivedRtpStreamStats = new RTCStats(kRTCRtpStreamStats, {
packetsReceived: 'number',
packetsLost: 'number',
jitter: 'number',
packetsDiscarded: 'number',
packetsRepaired: 'number',
burstPacketsLost: 'number',
burstPacketsDiscarded: 'number',
burstLossCount: 'number',
burstDiscardCount: 'number',
burstLossRate: 'number',
burstDiscardRate: 'number',
gapLossRate: 'number',
gapDiscardRate: 'number',
});
/*
* RTCInboundRTPStreamStats
* https://w3c.github.io/webrtc-stats/#inboundrtpstats-dict*
* @private
*/
let kRTCInboundRtpStreamStats = new RTCStats(kRTCReceivedRtpStreamStats, {
trackId: 'string',
receiverId: 'string',
remoteId: 'string',
framesDecoded: 'number',
qpSum: 'number',
lastPacketReceivedTimestamp: 'number',
averageRtcpInterval: 'number',
fecPacketsReceived: 'number',
fecPacketsDiscarded: 'number',
bytesReceived: 'number',
packetsFailedDecryption: 'number',
packetsDuplicated: 'number',
perDscpPacketsReceived: 'object',
nackCount: 'number',
firCount: 'number',
pliCount: 'number',
sliCount: 'number',
fractionLost: 'number', // Obsolete, moved to RTCRemoteInboundRtpStreamStats.
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'inbound-rtp', kRTCInboundRtpStreamStats);
/*
* RTCRemoteInboundRtpStreamStats
* https://w3c.github.io/webrtc-stats/#remoteinboundrtpstats-dict*
* @private
*/
let kRTCRemoteInboundRtpStreamStats =
new RTCStats(kRTCReceivedRtpStreamStats, {
localId: 'string',
roundTripTime: 'number',
fractionLost: 'number',
});
// TODO(hbos): When remote-inbound-rtp is implemented, make presence MANDATORY.
addRTCStatsToWhitelist(
Presence.OPTIONAL, 'remote-inbound-rtp', kRTCRemoteInboundRtpStreamStats);
/**
* RTCSentRtpStreamStats
* https://w3c.github.io/webrtc-stats/#dom-rtcsentrtpstreamstats
* @private
*/
let kRTCSentRtpStreamStats = new RTCStats(kRTCRtpStreamStats, {
packetsSent: 'number',
packetsDiscardedOnSend: 'number',
fecPacketsSent: 'number',
bytesSent: 'number',
bytesDiscardedOnSend: 'number',
});
/*
* RTCOutboundRtpStreamStats
* https://w3c.github.io/webrtc-stats/#outboundrtpstats-dict*
* @private
*/
let kRTCOutboundRtpStreamStats = new RTCStats(kRTCSentRtpStreamStats, {
trackId: 'string',
mediaSourceId: 'string',
senderId: 'string',
remoteId: 'string',
lastPacketSentTimestamp: 'number',
retransmittedPacketsSent: 'number',
retransmittedBytesSent: 'number',
targetBitrate: 'number',
totalEncodedBytesTarget: 'number',
framesEncoded: 'number',
qpSum: 'number',
totalEncodeTime: 'number',
totalPacketSendDelay: 'number',
averageRtcpInterval: 'number',
qualityLimitationReason: 'string',
qualityLimitationDurations: 'object',
perDscpPacketsSent: 'object',
nackCount: 'number',
firCount: 'number',
pliCount: 'number',
sliCount: 'number',
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'outbound-rtp', kRTCOutboundRtpStreamStats);
/*
* RTCRemoteOutboundRtpStreamStats
* https://w3c.github.io/webrtc-stats/#dom-rtcremoteoutboundrtpstreamstats
* @private
*/
let kRTCRemoteOutboundRtpStreamStats = new RTCStats(kRTCSentRtpStreamStats, {
localId: 'string',
remoteTimestamp: 'number',
});
// TODO(hbos): When remote-outbound-rtp is implemented, make presence MANDATORY.
addRTCStatsToWhitelist(
Presence.OPTIONAL, 'remote-outbound-rtp', kRTCRemoteOutboundRtpStreamStats);
/**
* RTCMediaSourceStats
* https://w3c.github.io/webrtc-stats/#dom-rtcmediasourcestats
* @private
*/
const kRTCMediaSourceStats = new RTCStats(null, {
trackIdentifier: 'string',
kind: 'string',
});
/**
* RTCAudioSourceStats
* https://w3c.github.io/webrtc-stats/#dom-rtcaudiosourcestats
* @private
*/
const kRTCAudioSourceStats = new RTCStats(kRTCMediaSourceStats, {
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'media-source', kRTCAudioSourceStats);
/**
* RTCVideoSourceStats
* https://w3c.github.io/webrtc-stats/#dom-rtcvideosourcestats
* @private
*/
const kRTCVideoSourceStats = new RTCStats(kRTCMediaSourceStats, {
width: 'number',
height: 'number',
frames: 'number',
framesPerSecond: 'number',
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'media-source', kRTCVideoSourceStats);
/*
* RTCRtpContributingSourceStats
* https://w3c.github.io/webrtc-stats/#dom-rtcrtpcontributingsourcestats
* @private
*/
let kRTCRtpContributingSourceStats = new RTCStats(null, {
contributorSsrc: 'number',
inboundRtpStreamId: 'string',
packetsContributedTo: 'number',
audioLevel: 'number',
});
// TODO(hbos): When csrc is implemented, make presence MANDATORY.
addRTCStatsToWhitelist(
Presence.OPTIONAL, 'csrc', kRTCRtpContributingSourceStats);
/*
* RTCCodecStats
* https://w3c.github.io/webrtc-stats/#codec-dict*
* @private
*/
let kRTCCodecStats = new RTCStats(null, {
payloadType: 'number',
mimeType: 'string',
// TODO(hbos): As soon as |codec| has been renamed |mimeType| in the webrtc
// repo, remove this line. https://bugs.webrtc.org/7061
codec: 'string',
clockRate: 'number',
channels: 'number',
sdpFmtpLine: 'string',
implementation: 'string',
});
addRTCStatsToWhitelist(Presence.MANDATORY, 'codec', kRTCCodecStats);
/*
* RTCPeerConnectionStats
* https://w3c.github.io/webrtc-stats/#pcstats-dict*
* @private
*/
let kRTCPeerConnectionStats = new RTCStats(null, {
dataChannelsOpened: 'number',
dataChannelsClosed: 'number',
dataChannelsRequested: 'number',
dataChannelsAccepted: 'number',
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'peer-connection', kRTCPeerConnectionStats);
/*
* RTCMediaStreamStats
* https://w3c.github.io/webrtc-stats/#msstats-dict*
* @private
*/
let kRTCMediaStreamStats = new RTCStats(null, {
streamIdentifier: 'string',
trackIds: 'sequence_string',
});
addRTCStatsToWhitelist(Presence.MANDATORY, 'stream', kRTCMediaStreamStats);
/**
* RTCMediaHandlerStats
* https://w3c.github.io/webrtc-stats/#mststats-dict*
* @private
*/
let kRTCMediaHandlerStats = new RTCStats(null, {
trackIdentifier: 'string',
remoteSource: 'boolean',
ended: 'boolean',
kind: 'string',
priority: 'string',
detached: 'boolean', // Obsolete, detached stats should fire "onstatsended".
});
/*
* RTCVideoHandlerStats
* https://w3c.github.io/webrtc-stats/#dom-rtcvideohandlerstats
* @private
*/
let kRTCVideoHandlerStats = new RTCStats(kRTCMediaHandlerStats, {
frameWidth: 'number',
frameHeight: 'number',
framesPerSecond: 'number',
});
/*
* RTCVideoSenderStats
* https://w3c.github.io/webrtc-stats/#dom-rtcvideosenderstats
* @private
*/
let kRTCVideoSenderStats = new RTCStats(kRTCVideoHandlerStats, {
mediaSourceId: 'string',
framesCaptured: 'number',
framesSent: 'number',
hugeFramesSent: 'number',
keyFramesSent: 'number',
});
// TODO(hbos): When sender is implemented, make presence MANDATORY.
addRTCStatsToWhitelist(Presence.OPTIONAL, 'sender', kRTCVideoSenderStats);
/*
* RTCSenderVideoTrackAttachmentStats
* https://w3c.github.io/webrtc-stats/#dom-rtcsendervideotrackattachmentstats
* @private
*/
let kRTCSenderVideoTrackAttachmentStats = new RTCStats(kRTCVideoSenderStats, {
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'track', kRTCSenderVideoTrackAttachmentStats);
/*
* RTCVideoReceiverStats
* https://w3c.github.io/webrtc-stats/#dom-rtcvideoreceiverstats
* @private
*/
let kRTCVideoReceiverStats = new RTCStats(kRTCVideoHandlerStats, {
estimatedPlayoutTimestamp: 'number',
jitterBufferDelay: 'number',
jitterBufferEmittedCount: 'number',
framesReceived: 'number',
keyFramesReceived: 'number',
framesDecoded: 'number',
framesDropped: 'number',
partialFramesLost: 'number',
fullFramesLost: 'number',
});
// TODO(hbos): When receiver is implemented, make presence MANDATORY.
addRTCStatsToWhitelist(
Presence.OPTIONAL, 'receiver', kRTCVideoReceiverStats);
/*
* RTCReceiverVideoTrackAttachmentStats
* https://github.com/w3c/webrtc-stats/issues/424
* @private
*/
let kRTCReceiverVideoTrackAttachmentStats =
new RTCStats(kRTCVideoReceiverStats, {
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'track', kRTCReceiverVideoTrackAttachmentStats);
/*
* RTCAudioHandlerStats
* https://w3c.github.io/webrtc-stats/#dom-rtcaudiohandlerstats
* @private
*/
let kRTCAudioHandlerStats = new RTCStats(kRTCMediaHandlerStats, {
audioLevel: 'number',
totalAudioEnergy: 'number',
voiceActivityFlag: 'boolean',
totalSamplesDuration: 'number',
});
/*
* RTCAudioSenderStats
* https://w3c.github.io/webrtc-stats/#dom-rtcaudiosenderstats
* @private
*/
let kRTCAudioSenderStats = new RTCStats(kRTCAudioHandlerStats, {
mediaSourceId: 'string',
echoReturnLoss: 'number',
echoReturnLossEnhancement: 'number',
totalSamplesSent: 'number',
});
// TODO(hbos): When sender is implemented, make presence MANDATORY.
addRTCStatsToWhitelist(Presence.OPTIONAL, 'sender', kRTCAudioSenderStats);
/*
* RTCSenderAudioTrackAttachmentStats
* https://w3c.github.io/webrtc-stats/#dom-rtcsenderaudiotrackattachmentstats
* @private
*/
let kRTCSenderAudioTrackAttachmentStats = new RTCStats(kRTCAudioSenderStats, {
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'track', kRTCSenderAudioTrackAttachmentStats);
/*
* RTCAudioReceiverStats
* https://w3c.github.io/webrtc-stats/#dom-rtcaudioreceiverstats
* @private
*/
let kRTCAudioReceiverStats = new RTCStats(kRTCAudioHandlerStats, {
estimatedPlayoutTimestamp: 'number',
jitterBufferDelay: 'number',
jitterBufferEmittedCount: 'number',
totalSamplesReceived: 'number',
concealedSamples: 'number',
silentConcealedSamples: 'number',
concealmentEvents: 'number',
insertedSamplesForDeceleration: 'number',
removedSamplesForAcceleration: 'number',
});
// TODO(hbos): When receiver is implemented, make presence MANDATORY.
addRTCStatsToWhitelist(
Presence.OPTIONAL, 'receiver', kRTCAudioReceiverStats);
/*
* RTCReceiverAudioTrackAttachmentStats
* https://github.com/w3c/webrtc-stats/issues/424
* @private
*/
let kRTCReceiverAudioTrackAttachmentStats =
new RTCStats(kRTCAudioReceiverStats, {
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'track', kRTCReceiverAudioTrackAttachmentStats);
/*
* RTCDataChannelStats
* https://w3c.github.io/webrtc-stats/#dcstats-dict*
* @private
*/
let kRTCDataChannelStats = new RTCStats(null, {
label: 'string',
protocol: 'string',
datachannelid: 'number',
state: 'string',
messagesSent: 'number',
bytesSent: 'number',
messagesReceived: 'number',
bytesReceived: 'number',
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'data-channel', kRTCDataChannelStats);
/*
* RTCTransportStats
* https://w3c.github.io/webrtc-stats/#transportstats-dict*
* @private
*/
let kRTCTransportStats = new RTCStats(null, {
bytesSent: 'number',
bytesReceived: 'number',
rtcpTransportStatsId: 'string',
dtlsState: 'string',
selectedCandidatePairId: 'string',
localCertificateId: 'string',
remoteCertificateId: 'string',
});
addRTCStatsToWhitelist(Presence.MANDATORY, 'transport', kRTCTransportStats);
/*
* RTCIceCandidateStats
* https://w3c.github.io/webrtc-stats/#icecandidate-dict*
* @private
*/
let kRTCIceCandidateStats = new RTCStats(null, {
transportId: 'string',
isRemote: 'boolean',
networkType: 'string',
ip: 'string',
port: 'number',
protocol: 'string',
relayProtocol: 'string',
candidateType: 'string',
priority: 'number',
url: 'string',
deleted: 'boolean',
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'local-candidate', kRTCIceCandidateStats);
addRTCStatsToWhitelist(
Presence.MANDATORY, 'remote-candidate', kRTCIceCandidateStats);
/*
* RTCIceCandidatePairStats
* https://w3c.github.io/webrtc-stats/#candidatepair-dict*
* @private
*/
let kRTCIceCandidatePairStats = new RTCStats(null, {
transportId: 'string',
localCandidateId: 'string',
remoteCandidateId: 'string',
state: 'string',
priority: 'number',
nominated: 'boolean',
writable: 'boolean',
readable: 'boolean',
bytesSent: 'number',
bytesReceived: 'number',
totalRoundTripTime: 'number',
currentRoundTripTime: 'number',
availableOutgoingBitrate: 'number',
availableIncomingBitrate: 'number',
requestsReceived: 'number',
requestsSent: 'number',
responsesReceived: 'number',
responsesSent: 'number',
retransmissionsReceived: 'number',
retransmissionsSent: 'number',
consentRequestsReceived: 'number',
consentRequestsSent: 'number',
consentResponsesReceived: 'number',
consentResponsesSent: 'number',
});
addRTCStatsToWhitelist(
Presence.MANDATORY, 'candidate-pair', kRTCIceCandidatePairStats);
/*
* RTCCertificateStats
* https://w3c.github.io/webrtc-stats/#certificatestats-dict*
* @private
*/
let kRTCCertificateStats = new RTCStats(null, {
fingerprint: 'string',
fingerprintAlgorithm: 'string',
base64Certificate: 'string',
issuerCertificateId: 'string',
});
addRTCStatsToWhitelist(Presence.MANDATORY, 'certificate', kRTCCertificateStats);
// Public interface to tests. These are expected to be called with
// ExecuteJavascript invocations from the browser tests and will return answers
// through the DOM automation controller.
/**
* Verifies that the promise-based |RTCPeerConnection.getStats| returns stats,
* makes sure that all returned stats have the base RTCStats-members and that
* all stats are allowed by the whitelist.
*
* Returns "ok-" followed by JSON-stringified array of "RTCStats.type" values
* to the test, these being the different types of stats that was returned by
* this call to getStats.
*/
function verifyStatsGeneratedPromise() {
peerConnection_().getStats()
.then(function(report) {
if (report == null || report.size == 0)
throw new failTest('report is null or empty.');
let statsTypes = new Set();
let ids = new Set();
for (let stats of report.values()) {
verifyStatsIsWhitelisted_(stats);
statsTypes.add(stats.type);
if (ids.has(stats.id))
throw failTest('stats.id is not a unique identifier.');
ids.add(stats.id);
}
returnToTest('ok-' + JSON.stringify(Array.from(statsTypes.values())));
},
function(e) {
throw failTest('Promise was rejected: ' + e);
});
}
/**
* Gets the result of the promise-based |RTCPeerConnection.getStats| as a
* dictionary of RTCStats-dictionaries.
*
* Returns "ok-" followed by a JSON-stringified dictionary of dictionaries to
* the test.
*/
function getStatsReportDictionary() {
peerConnection_().getStats()
.then(function(report) {
if (report == null || report.size == 0)
throw new failTest('report is null or empty.');
let reportDictionary = {};
for (let stats of report.values()) {
reportDictionary[stats.id] = stats;
}
returnToTest('ok-' + JSON.stringify(reportDictionary));
},
function(e) {
throw failTest('Promise was rejected: ' + e);
});
}
/**
* Measures the performance of the promise-based |RTCPeerConnection.getStats|
* and returns the time it took in milliseconds as a double
* (DOMHighResTimeStamp, accurate to one thousandth of a millisecond).
* Verifies that all stats types of the whitelist were contained in the result.
*
* Returns "ok-" followed by a double on success.
*/
function measureGetStatsPerformance() {
let t0 = performance.now();
peerConnection_().getStats()
.then(function(report) {
let t1 = performance.now();
for (let stats of report.values()) {
verifyStatsIsWhitelisted_(stats);
}
for (let mandatoryType of mandatoryStatsTypes()) {
let mandatoryTypeExists = false;
for (let stats of report.values()) {
if (stats.type == mandatoryType) {
mandatoryTypeExists = true;
break;
}
}
if (!mandatoryTypeExists) {
returnToTest('Missing mandatory type: ' + mandatoryType);
}
}
returnToTest('ok-' + (t1 - t0));
},
function(e) {
throw failTest('Promise was rejected: ' + e);
});
}
/**
* Returns a complete list of mandatory "RTCStats.type" values from the
* whitelist as a JSON-stringified array of strings to the test.
*/
function getMandatoryStatsTypes() {
returnToTest(JSON.stringify(Array.from(mandatoryStatsTypes())));
}
// Internals.
/** Gets a set of all mandatory stats types. */
function mandatoryStatsTypes() {
const mandatoryTypes = new Set();
for (let whitelistedStats of gStatsWhitelist.values()) {
if (whitelistedStats.presence() == Presence.MANDATORY)
mandatoryTypes.add(whitelistedStats.type());
}
return mandatoryTypes;
}
/**
* Checks if |stats| correctly maps to a a whitelisted RTCStats-derived
* dictionary, throwing |failTest| if it doesn't. See |gStatsWhitelist|.
*
* The "RTCStats.type" must map to a known dictionary description. Every member
* is optional, but if present it must be present in the whitelisted dictionary
* description and its type must match.
* @private
*/
function verifyStatsIsWhitelisted_(stats) {
if (stats == null)
throw failTest('stats is null or undefined: ' + stats);
if (typeof(stats.id) !== 'string')
throw failTest('stats.id is not a string:' + stats.id);
if (typeof(stats.timestamp) !== 'number' || !isFinite(stats.timestamp) ||
stats.timestamp <= 0) {
throw failTest('stats.timestamp is not a positive finite number: ' +
stats.timestamp);
}
if (typeof(stats.type) !== 'string')
throw failTest('stats.type is not a string: ' + stats.type);
let whitelistedStats = gStatsWhitelist.get(stats.type);
if (whitelistedStats == null)
throw failTest('stats.type is not a whitelisted type: ' + stats.type);
whitelistedStats = whitelistedStats.stats();
for (let propertyName in stats) {
if (propertyName === 'id' || propertyName === 'timestamp' ||
propertyName === 'type') {
continue;
}
if (!whitelistedStats.hasOwnProperty(propertyName)) {
throw failTest(
stats.type + '.' + propertyName + ' is not a whitelisted ' +
'member: ' + stats[propertyName]);
}
if (whitelistedStats[propertyName] === 'any')
continue;
if (!whitelistedStats[propertyName].startsWith('sequence_')) {
if (typeof(stats[propertyName]) !== whitelistedStats[propertyName]) {
throw failTest('stats.' + propertyName + ' should have a different ' +
'type according to the whitelist: ' + stats[propertyName] + ' vs ' +
whitelistedStats[propertyName]);
}
} else {
if (!Array.isArray(stats[propertyName])) {
throw failTest('stats.' + propertyName + ' should have a different ' +
'type according to the whitelist (should be an array): ' +
JSON.stringify(stats[propertyName]) + ' vs ' +
whitelistedStats[propertyName]);
}
let elementType = whitelistedStats[propertyName].substring(9);
for (let element in stats[propertyName]) {
if (typeof(element) !== elementType) {
throw failTest('stats.' + propertyName + ' should have a different ' +
'type according to the whitelist (an element of the array has ' +
'the incorrect type): ' + JSON.stringify(stats[propertyName]) +
' vs ' + whitelistedStats[propertyName]);
}
}
}
}
}