peer-connection/helpers/statistics/index.js

import Skylink from '../../../index';
import SessionDescription from '../../../session-description';
import logger from '../../../logger';
import messages from '../../../messages';
import {
  GET_CONNECTION_STATUS_STATE, DATA_CHANNEL_TYPE, TAGS,
} from '../../../constants';
import { dispatchEvent } from '../../../utils/skylinkEventManager';
import { getConnectionStatusStateChange } from '../../../skylink-events/peer-events';
import parsers from './parsers/index';
import { isEmptyObj } from '../../../utils/helpers';

/**
 * @classdesc This class is used to fetch the statistics for a RTCPeerConnection
 * @class
 * @private
 */
class PeerConnectionStatistics {
  constructor(roomKey, peerId) {
    /**
     * The current skylink state of the room
     * @type {SkylinkState}
     */
    this.roomState = Skylink.getSkylinkState(roomKey);
    /**
     * Current RTCPeerConnection based on the peerId
     * @type {RTCPeerConnection}
     */
    this.peerConnection = this.roomState.peerConnections[peerId] || null;
    this.peerConnStatus = this.roomState.peerConnStatus[peerId] || null;
    this.dataChannel = this.roomState.dataChannels[peerId] || null;
    this.peerId = peerId;
    this.roomKey = roomKey;
    this.output = {
      peerId,
      raw: {},
      connection: {},
      audio: {
        sending: {},
        receiving: {},
      },
      video: {
        sending: {},
        receiving: {},
      },
      selectedCandidatePair: {
        id: null,
        local: {},
        remote: {},
        // consentResponses: {}, TODO: remove
        consentRequests: {},
        responses: {},
        requests: {},
      },
      certificate: {},
    };
    this.beSilentOnLogs = Skylink.getInitOptions().beSilentOnStatsLogs;
    this.isAutoBwStats = false;
    this.bandwidth = null;
  }

  /**
   * Helper function for getting RTC Connection Statistics
   * @returns {Promise<statistics>}
   */
  getConnectionStatus() {
    return this.getStatistics(false, false);
  }

  getStatsSuccess(promiseResolve, promiseReject, stats) {
    const { AdapterJS } = window;
    const { peerBandwidth, peerStats } = this.roomState;
    // TODO: Need to do full implementation of success function
    if (typeof stats.forEach === 'function') {
      stats.forEach((item, prop) => {
        this.output.raw[prop] = item;
      });
    } else {
      this.output.raw = stats;
    }

    const edgeTracksKind = {
      remote: {},
      local: {},
    };

    try {
      if (isEmptyObj(peerStats)) {
        logger.log.DEBUG([this.peerId, TAGS.STATS_MODULE, null, messages.STATS_MODULE.STATS_DISCARDED]);
        return;
      }
      // Polyfill for Plugin missing "mediaType" stats item
      const rawOutput = Object.keys(this.output.raw);
      for (let i = 0; i < rawOutput.length; i += 1) {
        try {
          if (rawOutput[i].indexOf('ssrc_') === 0 && !this.output.raw[rawOutput[i]].mediaType) {
            this.output.raw[rawOutput[i]].mediaType = this.output.raw[rawOutput[i]].audioInputLevel || this.output.raw[rawOutput[i]].audioOutputLevel ? 'audio' : 'video';

            // Polyfill for Edge 15.x missing "mediaType" stats item
          } else if (AdapterJS.webrtcDetectedBrowser === 'edge' && !this.output.raw[rawOutput[i]].mediaType
            && ['inboundrtp', 'outboundrtp'].indexOf(this.output.raw[rawOutput[i]].type) > -1) {
            const trackItem = this.output.raw[this.output.raw[rawOutput[i]].mediaTrackId] || {};
            this.output.raw[rawOutput[i]].mediaType = edgeTracksKind[this.output.raw[rawOutput[i]].isRemote ? 'remote' : 'local'][trackItem.trackIdentifier] || '';
          }

          // Parse DTLS certificates and ciphers used
          parsers.parseCertificates(this.output, rawOutput[i]);
          parsers.parseSelectedCandidatePair(this.roomState, this.output, rawOutput[i], this.peerConnection, this.peerId, this.isAutoBwStats);
          parsers.parseCodecs(this.output, rawOutput[i]);
          parsers.parseAudio(this.roomState, this.output, rawOutput[i], this.peerConnection, this.peerId, this.isAutoBwStats);
          parsers.parseVideo(this.roomState, this.output, rawOutput[i], this.peerConnection, this.peerId, this.isAutoBwStats);
          parsers.parseVideoE2EDelay(this.roomState, this.output, rawOutput[i], this.peerConnection, this.peerId, this.beSilentOnLogs);

          if (this.isAutoBwStats && !peerBandwidth[this.peerId][rawOutput[i]]) {
            peerBandwidth[this.peerId][rawOutput[i]] = this.output.raw[rawOutput[i]];
          } else if (!this.isAutoBwStats && !peerStats[this.peerId][rawOutput[i]]) {
            peerStats[this.peerId][rawOutput[i]] = this.output.raw[rawOutput[i]];
          }
        } catch (err) {
          logger.log.DEBUG([this.peerId, TAGS.STATS_MODULE, null, messages.STATS_MODULE.ERRORS.PARSE_FAILED], err);
          break;
        }
      }
    } catch (err) {
      this.getStatsFailure(promiseReject, messages.STATS_MODULE.ERRORS.PARSE_FAILED, err);
    }

    dispatchEvent(getConnectionStatusStateChange({
      state: GET_CONNECTION_STATUS_STATE.RETRIEVE_SUCCESS,
      peerId: this.peerId,
      stats: this.output,
    }));

    promiseResolve(this.output);
  }

  getStatsFailure(promiseReject, errorMsg, error) {
    const errMsg = errorMsg || messages.STATS_MODULE.RETRIEVE_STATS_FAILED;

    if (!this.beSilentOnLogs) {
      logger.log.ERROR([this.peerId, TAGS.STATS_MODULE, null, errMsg], error);
      dispatchEvent(getConnectionStatusStateChange({
        state: GET_CONNECTION_STATUS_STATE.RETRIEVE_ERROR,
        peerId: this.peerId,
        error,
      }));
    }
    promiseReject(error);
  }

  /**
   * Fetch webRTC stats of a RTCPeerConnection
   * @param beSilentOnLogs
   * @param isAutoBwStats
   * @return {Promise<statistics>}
   * @fires getConnectionStatusStateChange
   */
  // eslint-disable-next-line consistent-return
  getStatistics(beSilentOnLogs = false, isAutoBwStats = false) {
    const { STATS_MODULE } = messages;
    return new Promise((resolve, reject) => {
      if (!this.roomState.peerStats[this.peerId] && !isAutoBwStats) {
        logger.log.WARN(STATS_MODULE.NOT_INITIATED);
        resolve(null);
      } else {
        this.beSilentOnLogs = beSilentOnLogs;
        this.isAutoBwStats = isAutoBwStats;

        try {
          this.gatherRTCPeerConnectionDetails();
          this.gatherSDPIceCandidates();
          this.gatherSDPCodecs();
          this.gatherCertificateDetails();
          this.gatherSSRCDetails();
          this.gatherRTCDataChannelDetails();
        } catch (err) {
          logger.log.WARN([this.peerId, TAGS.STATS_MODULE, null, messages.STATS_MODULE.ERRORS.PARSE_FAILED], err);
        }

        if (typeof this.peerConnection.getStats !== 'function') {
          this.getStatsFailure(reject, messages.PEER_CONNECTION.getstats_api_not_available);
        }

        dispatchEvent(getConnectionStatusStateChange({
          state: GET_CONNECTION_STATUS_STATE.RETRIEVING,
          peerId: this.peerId,
        }));

        this.peerConnection.getStats()
          .then((stats) => { this.getStatsSuccess(resolve, reject, stats); })
          .catch((error) => {
            if (error.message === messages.STATS_MODULE.ERRORS.STATS_IS_NULL) {
              logger.log.WARN([this.peerId, TAGS.STATS_MODULE, null, messages.STATS_MODULE.ERRORS.RETRIEVE_STATS_FAILED], error.message);
              return;
            }
            this.getStatsFailure(reject, null, error);
          });
      }
    });
  }

  /**
   * Formats output object with RTCPeerConnection details
   * @private
   */
  gatherRTCPeerConnectionDetails() {
    const { peerConnection } = this;
    this.output.connection.iceConnectionState = peerConnection.iceConnectionState;
    this.output.connection.iceGatheringState = peerConnection.iceGatheringState;
    this.output.connection.signalingState = peerConnection.signalingState;

    this.output.connection.remoteDescription = {
      type: (peerConnection.remoteDescription && peerConnection.remoteDescription.type) || '',
      sdp: (peerConnection.remoteDescription && peerConnection.remoteDescription.sdp) || '',
    };

    this.output.connection.localDescription = {
      type: (peerConnection.localDescription && peerConnection.localDescription.type) || '',
      sdp: (peerConnection.localDescription && peerConnection.localDescription.sdp) || '',
    };

    this.output.connection.constraints = this.peerConnStatus ? this.peerConnStatus.constraints : null;
    this.output.connection.optional = this.peerConnStatus ? this.peerConnStatus.optional : null;
    this.output.connection.sdpConstraints = this.peerConnStatus ? this.peerConnStatus.sdpConstraints : null;
  }

  /**
   * Formats output object with Ice Candidate details
   * @private
   */
  gatherSDPIceCandidates() {
    const { peerConnection, beSilentOnLogs } = this;
    this.output.connection.candidates = {
      sending: SessionDescription.getSDPICECandidates(this.peerId, peerConnection.localDescription, beSilentOnLogs),
      receiving: SessionDescription.getSDPICECandidates(this.peerId, peerConnection.remoteDescription, beSilentOnLogs),
    };
  }

  /**
   * Formats output object with SDP codecs
   * @private
   */
  gatherSDPCodecs() {
    const { peerConnection, beSilentOnLogs } = this;
    this.output.audio.sending.codec = SessionDescription.getSDPSelectedCodec(this.peerId, peerConnection.remoteDescription, 'audio', beSilentOnLogs);
    this.output.video.sending.codec = SessionDescription.getSDPSelectedCodec(this.peerId, peerConnection.remoteDescription, 'video', beSilentOnLogs);
    this.output.audio.receiving.codec = SessionDescription.getSDPSelectedCodec(this.peerId, peerConnection.localDescription, 'audio', beSilentOnLogs);
    this.output.video.receiving.codec = SessionDescription.getSDPSelectedCodec(this.peerId, peerConnection.localDescription, 'video', beSilentOnLogs);
  }

  /**
   * Formats output object with SDP certificate details
   * @private
   */
  gatherCertificateDetails() {
    const { peerConnection, beSilentOnLogs } = this;
    this.output.certificate.local = SessionDescription.getSDPFingerprint(this.peerId, peerConnection.localDescription, beSilentOnLogs);
    this.output.certificate.remote = SessionDescription.getSDPFingerprint(this.peerId, peerConnection.remoteDescription, beSilentOnLogs);
  }

  /**
   * Formats output object with audio and video ssrc details
   * @private
   */
  gatherSSRCDetails() {
    const { peerConnection, beSilentOnLogs } = this;
    const inboundSSRCs = SessionDescription.getSDPMediaSSRC(this.peerId, peerConnection.remoteDescription, beSilentOnLogs);
    const outboundSSRCs = SessionDescription.getSDPMediaSSRC(this.peerId, peerConnection.localDescription, beSilentOnLogs);
    this.output.audio.receiving.ssrc = inboundSSRCs.audio;
    this.output.video.receiving.ssrc = inboundSSRCs.video;
    this.output.audio.sending.ssrc = outboundSSRCs.audio;
    this.output.video.sending.ssrc = outboundSSRCs.video;
  }

  /**
   * Formats output object with RTCDataChannel details
   * @private
   */
  gatherRTCDataChannelDetails() {
    const { dataChannel } = this;
    if (dataChannel) {
      const dcKeys = Object.keys(dataChannel);

      this.output.connection.dataChannels = {};

      dcKeys.forEach((prop) => {
        const channel = dataChannel[prop];
        this.output.connection.dataChannels[channel.channel.label] = {
          label: channel.channel.label,
          readyState: channel.channel.readyState,
          channelType: DATA_CHANNEL_TYPE[prop === 'main' ? 'MESSAGING' : 'DATA'],
          currentTransferId: channel.transferId || null,
          currentStreamId: channel.streamId || null,
        };
      });
    }
  }
}

export default PeerConnectionStatistics;