/* eslint-disable no-fallthrough */
const MockRTCStatsReport = require('./mockrtcstatsreport');
const ERROR_PEER_CONNECTION_NULL = 'PeerConnection is null';
const ERROR_WEB_RTC_UNSUPPORTED = 'WebRTC statistics are unsupported';
const SIGNED_SHORT = 32767;
// (rrowland) Only needed to detect Chrome so we can force using legacy stats until standard
// stats are fixed in Chrome.
let isChrome = false;
if (typeof window !== 'undefined') {
const isCriOS = !!window.navigator.userAgent.match('CriOS');
const isElectron = !!window.navigator.userAgent.match('Electron');
const isGoogle = typeof window.chrome !== 'undefined'
&& window.navigator.vendor === 'Google Inc.'
&& window.navigator.userAgent.indexOf('OPR') === -1
&& window.navigator.userAgent.indexOf('Edge') === -1;
isChrome = isCriOS || isElectron || isGoogle;
}
/**
* @typedef {Object} StatsOptions
* Used for testing to inject and extract methods.
* @property {function} [createRTCSample] - Method for parsing an RTCStatsReport
*/
/**
* Collects any WebRTC statistics for the given {@link PeerConnection}
* @param {PeerConnection} peerConnection - Target connection.
* @param {StatsOptions} options - List of custom options.
* @return {Promise<RTCSample>} Universally-formatted version of RTC stats.
*/
function getStatistics(peerConnection, options) {
options = Object.assign({
createRTCSample
}, options);
if (!peerConnection) {
return Promise.reject(new Error(ERROR_PEER_CONNECTION_NULL));
}
if (typeof peerConnection.getStats !== 'function') {
return Promise.reject(new Error(ERROR_WEB_RTC_UNSUPPORTED));
}
// (rrowland) Force using legacy stats on Chrome until audioLevel of the outbound
// audio track is no longer constantly zero.
if (isChrome) {
return new Promise((resolve, reject) => peerConnection.getStats(resolve, reject)).then(MockRTCStatsReport.fromRTCStatsResponse)
.then(options.createRTCSample);
}
let promise;
try {
promise = peerConnection.getStats();
} catch (e) {
promise = new Promise((resolve, reject) => peerConnection.getStats(resolve, reject)).then(MockRTCStatsReport.fromRTCStatsResponse);
}
return promise.then(options.createRTCSample);
}
/**
* @typedef {Object} RTCSample - A sample containing relevant WebRTC stats information.
* @property {Number} [timestamp]
* @property {String} [codecName] - MimeType name of the codec being used by the outbound audio stream
* @property {Number} [rtt] - Round trip time
* @property {Number} [jitter]
* @property {Number} [packetsSent]
* @property {Number} [packetsLost]
* @property {Number} [packetsReceived]
* @property {Number} [bytesReceived]
* @property {Number} [bytesSent]
* @property {Number} [localAddress]
* @property {Number} [remoteAddress]
* @property {Number} [audioInputLevel] - Between 0 and 32767
* @property {Number} [audioOutputLevel] - Between 0 and 32767
*/
function RTCSample() { }
/**
* Create an RTCSample object from an RTCStatsReport
* @private
* @param {RTCStatsReport} statsReport
* @returns {RTCSample}
*/
function createRTCSample(statsReport) {
let activeTransportId = null;
const sample = new RTCSample();
let fallbackTimestamp;
Array.from(statsReport.values()).forEach(stats => {
// Firefox hack -- Firefox doesn't have dashes in type names
const type = stats.type.replace('-', '');
fallbackTimestamp = fallbackTimestamp || stats.timestamp;
switch (type) {
case 'inboundrtp':
sample.timestamp = sample.timestamp || stats.timestamp;
sample.jitter = stats.jitter * 1000;
sample.packetsLost = stats.packetsLost;
sample.packetsReceived = stats.packetsReceived;
sample.bytesReceived = stats.bytesReceived;
const inboundTrack = statsReport.get(stats.trackId);
if (inboundTrack) {
sample.audioOutputLevel = inboundTrack.audioLevel * SIGNED_SHORT;
}
break;
case 'outboundrtp':
sample.timestamp = stats.timestamp;
sample.packetsSent = stats.packetsSent;
sample.bytesSent = stats.bytesSent;
if (stats.codecId && statsReport.get(stats.codecId)) {
const mimeType = statsReport.get(stats.codecId).mimeType;
sample.codecName = mimeType && mimeType.match(/(.*\/)?(.*)/)[2];
}
const outboundTrack = statsReport.get(stats.trackId);
if (outboundTrack) {
sample.audioInputLevel = outboundTrack.audioLevel * SIGNED_SHORT;
}
break;
case 'transport':
if (stats.dtlsState === 'connected') {
activeTransportId = stats.id;
}
break;
}
});
if (!sample.timestamp) {
sample.timestamp = fallbackTimestamp;
}
const activeTransport = statsReport.get(activeTransportId);
if (!activeTransport) { return sample; }
const selectedCandidatePair = statsReport.get(activeTransport.selectedCandidatePairId);
if (!selectedCandidatePair) { return sample; }
const localCandidate = statsReport.get(selectedCandidatePair.localCandidateId);
const remoteCandidate = statsReport.get(selectedCandidatePair.remoteCandidateId);
Object.assign(sample, {
localAddress: localCandidate && localCandidate.ip,
remoteAddress: remoteCandidate && remoteCandidate.ip,
rtt: selectedCandidatePair && (selectedCandidatePair.currentRoundTripTime * 1000)
});
return sample;
}
module.exports = getStatistics;