Source: rtc/stats.js

/* 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;