text: import uuid from 'uuid';
  import bowser from 'bowser';
  import {StatelessWebexPlugin} from '@webex/webex-core';
  
  import StatsAnalyzer from '../statsAnalyzer';
  import LoggerProxy from '../common/logs/logger-proxy';
  import Trigger from '../common/events/trigger-proxy';
  import Roap from '../roap/index';
  import Media from '../media';
  import MediaProperties from '../media/properties';
  import MeetingStateMachine from '../meeting/state';
  import AudioStateMachine from '../meeting/audio';
  import VideoStateMachine from '../meeting/video';
  import LocusInfo from '../locus-info';
  import PeerConnectionManager from '../peer-connection-manager';
  import Metrics from '../metrics';
  import {trigger, mediaType, eventType} from '../metrics/config';
  import ReconnectionManager from '../reconnection-manager';
  import MeetingRequest from '../meeting/request';
  import Members from '../members/index';
  import MeetingUtil from '../meeting/util';
  import MediaUtil from '../media/util';
  import WebRTCStats from '../stats/index';
  import StatsMetrics from '../stats/metrics';
  import StatsUtil from '../stats/util';
  import ReconnectionError from '../common/errors/reconnection';
  import ReconnectInProgress from '../common/errors/reconnection-in-progress';
  import {
    _BUSY_,
    _CALL_,
    _INCOMING_,
    _JOIN_,
    CONNECTION_STATE,
    CONTENT,
    ENDED,
    EVENT_TRIGGERS,
    EVENT_TYPES,
    EVENTS,
    FLOOR_ACTION,
    FULL_STATE,
    LAYOUT_TYPES,
    LOCUSINFO,
    MEETING_REMOVED_REASON,
    MEETING_STATE_MACHINE,
    MEETING_STATE,
    MEETINGS,
    METRICS_OPERATIONAL_MEASURES,
    MQA_STATS,
    NETWORK_STATUS,
    ONLINE,
    PC_BAIL_TIMEOUT,
    QUALITY_LEVELS,
    RECORDING_STATE,
    ROAP_SEQ_PRE,
    SDP,
    SHARE_STOPPED_REASON,
    STATS,
    VIDEO_RESOLUTIONS,
    VIDEO
  } from '../constants';
  import ParameterError from '../common/errors/parameter';
  import MediaError from '../common/errors/media';
  import MQAProcessor from '../metrics/mqa-processor';
  
  import InMeetingActions from './in-meeting-actions';
  
  const logRequest = (request, {header = '', success = '', failure = ''}) => {
    LoggerProxy.logger.info(header);
  
    return request
      .then((arg) => {
        LoggerProxy.logger.info(success);
  
        return arg;
      })
      .catch((error) => {
        LoggerProxy.logger.error(failure, error);
        throw error;
      });
  };
  
  /**
   * MediaDirection
   * @typedef {Object} MediaDirection
   * @property {boolean} sendAudio
   * @property {boolean} receiveAudio
   * @property {boolean} sendVideo
   * @property {boolean} receiveVideo
   * @property {boolean} sendShare
   * @property {boolean} receiveShare
   * @property {boolean} isSharing
   */
  
  /**
    * AudioVideo
    * @typedef {Object} AudioVideo
    * @property {Object} audio
    * @property {String} audio.deviceId
    * @property {Object} video
    * @property {String} video.deviceId
    */
  
  /**
     * SharePreferences
     * @typedef {Object} SharePreferences
     * @property {Object} [shareConstraints]
     * @property {Boolean} [highFrameRate]
     */
  
  /**
   * JoinOptions
   * @typedef {Object} JoinOptions
   * @property {String} [resourceId]
   * @property {String} [pin]
   * @property {Boolean} [moderator]
   * @property {String|Object} [meetingQuality]
   * @property {String} [meetingQuality.local]
   * @property {String} [meetingQuality.remote]
   * @property {Boolean} [rejoin]
   */
  
  /**
   * SendOptions
   * @typedef {Object} SendOptions
   * @property {Boolean} sendAudio
   * @property {Boolean} sendVideo
   * @property {Boolean} sendShare
   */
  
  /**
   * Recording
   * @typedef {Object} Recording
   * @property {Object} state
   * @property {String} modifiedBy
   */
  
  /**
    * Meeting State Change Event
    * Emitted when ever there is a meeting state change
    * @event meeting:stateChange
    * @instance
    * @type {Object}
    * @property {String} currentState current state of the meeting
    * @property {String} previousState previous state of the meeting
    * @memberof Meeting
    */
  
  /**
    * Media Ready Event
    * Emitted when a stream is ready to be rendered
    * @event media:ready
    * @instance
    * @type {Object}
    * @property {MediaStream} stream the media stream
    * @property {String} type what type of stream, remote, local
    * @memberof Meeting
    */
  
  /**
    * Media Stopped Event
    * Emitted when a stream has stopped sending
    * @event media:stopped
    * @instance
    * @type {Object}
    * @property {String} type what type of stream, remote, local
    * @memberof Meeting
    */
  
  /**
   * Meeting Ringing Event
   * Emitted when this client should play a ringing sound, because this member is getting an incoming meeting
   * or sending out an incoming meeting
   * @event meeting:ringing
   * @instance
   * @type {Object}
   * @property {String} type // INCOMING or JOIN
   * @property {String} id
   * @memberof Meeting
   */
  
  /**
   * Meeting Ringing Stop Event
   * Emitted when this client should stop playing a ringing sound
   * @event meeting:ringingStop
   * @instance
   * @type {Object}
   * @property {Object} type
   * @property {Boolean} type.remoteAnswered
   * @property {Boolean} type.remoteDeclined
   * @property {String} id
   * @memberof Meeting
   */
  
  /**
   * Meeting Started Sharing Local Event
   * Emitted when this member starts sharing
   * @event meeting:startedSharingLocal
   * @instance
   * @type {Object}
   * @memberof Meeting
   */
  
  /**
   * Meeting Stopped Sharing Local Event
   * Emitted when this member stops sharing
   * @event meeting:stoppedSharingLocal
   * @instance
   * @type {Object}
   * @memberof Meeting
   */
  
  /**
   * Meeting Started Sharing Remote Event
   * Emitted when remote sharing starts
   * @event meeting:startedSharingRemote
   * @instance
   * @type {Object}
   * @memberof Meeting
   */
  
  /**
   * Meeting Stopped Sharing Remote Event
   * Emitted when remote screen sharing ends
   * @event meeting:stoppedSharingRemote
   * @instance
   * @type {Object}
   * @memberof Meeting
   */
  
  /**
   * Meeting Locked Event
   * Emitted when a meeting is locked
   * @event meeting:locked
   * @instance
   * @type {Object}
   * @property {Object} info
   * @memberof Meeting
   */
  
  /**
   * Meeting Unlocked Event
   * Emitted when a meeting is unlocked
   * @event meeting:unlocked
   * @instance
   * @type {Object}
   * @property {Object} info
   * @memberof Meeting
   */
  
  /**
   * Meeting Actions Update Event
   * Emitted when a user can take actions on a meeting such as lock, unlock, assign host
   * @event meeting:actionsUpdate
   * @instance
   * @type {Object}
   * @property {Boolean} canLock
   * @property {Boolean} canUnlock
   * @property {Boolean} canAssignHost
   * @memberof Meeting
   */
  
  /**
   * Meeting Muted By Others Event
   * Emitted when a member is muted by another member
   * @event meeting:self:mutedByOthers
   * @instance
   * @type {Object}
   * @property {Object} payload
   * @memberof Meeting
   */
  
  /**
   * Meeting Self Guest Admitted Event
   * Emitted when a member admitted to the meeting by another member
   * @event meeting:self:guestAdmitted
   * @instance
   * @type {Object}
   * @property {Object} payload
   * @memberof Meeting
   */
  
  /**
   * Meeting Self Lobby Waiting Event
   * Emitted when this member enters the lobby and is waiting for the webex meeting to begin
   * @event meeting:self:lobbyWaiting
   * @instance
   * @type {Object}
   * @property {Object} payload
   * @memberof Meeting
   */
  
  /**
    * Reconnection Starting Event
    * Emitted when reconnection of media to the active meeting was successful
    * @event meeting:reconnectionStarting
    * @instance
    * @memberof Meeting
    */
  
  /**
    * Reconnection Success Event
    * Emitted when reconnection of media to the active meeting was successful
    * @event meeting:reconnectionSuccess
    * @instance
    * @type {Object}
    * @property {Object} reconnect
    * @memberof Meeting
    */
  
  /**
    * Reconnection Failure Event
    * Emitted when reconnection of media to the active meeting was successful
    * @event meeting:reconnectionFailure
    * @instance
    * @type {Object}
    * @property {Error} error
    * @memberof Meeting
    */
  
  /**
    * Meeting High Packet Loss Event
    * Emitted when ever there is high packet loss detected
    * @event meeting:highPacketLoss
    * @type {Object}
    * @property {String} mediaType Type of media experiencing packet loss
    * @property {Number} packetLossRatio current packet loss ratio
    * @memberof Meeting
    */
  
  /**
   * @description Meeting is the crux of the plugin
   * @export
   * @class Meeting
   */
  export default class Meeting extends StatelessWebexPlugin {
    namespace = MEETINGS;
  
    /**
     * @param {Object} attrs
     * @param {Object} options
     * @constructor
     * @memberof Meeting
    */
    constructor(attrs, options) {
      super({}, options);
      /**
       * @instance
       * @type {Object}
       * @readonly
       * @private
       * @memberof Meeting
       */
      this.attrs = attrs;
      /**
       * @instance
       * @type {Object}
       * @readonly
       * @private
       * @memberof Meeting
       */
      this.options = options;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.id = uuid.v4();
      /**
       * Correlation ID used for network tracking of meeting join
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.correlationId = this.id;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.userId = attrs.userId;
      /**
       * Organization ID
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.orgId = attrs.orgId;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.resource = attrs.resource;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.deviceUrl = attrs.deviceUrl;
      /**
       * @description set you -1 as default values is 0 (used to idenfify if 1st roap request was sent)
       * @instance
       * @type {Number}
       * @readonly
       * @private
       * @memberof Meeting
       */
      this.roapSeq = ROAP_SEQ_PRE;
      /**
       * @instance
       * @type {Object}
       * @readonly
       * @public
       * @memberof Meeting
       */
      // TODO: needs to be defined as a class
      this.meetingInfo = {};
      /**
       * @instance
       * @type {Members}
       * @public
       * @memberof Meeting
       */
      this.members = new Members({locusUrl: (attrs.locus && attrs.locus.url)}, {parent: this.webex});
      /**
       * @instance
       * @type {Roap}
       * @readonly
       * @private
       * @memberof Meeting
       */
      this.roap = new Roap({}, {parent: this.webex});
      /**
       * created later
       * @instance
       * @type {ReconnectionManager}
       * @readonly
       * @private
       * @memberof Meeting
       */
      this.reconnectionManager = new ReconnectionManager(this);
      /**
       * created later
       * @instance
       * @type {AudioStateMachine}
       * @public
       * @memberof Meeting
       */
      this.audio = null;
      /**
       * created later
       * @instance
       * @type {VideoStateMachine}
       * @public
       * @memberof Meeting
       */
      this.video = null;
      /**
       * @instance
       * @type {MeetingStateMachine}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.meetingFiniteStateMachine = MeetingStateMachine.create(this);
      /**
       * @instance
       * @type {WebRTCStats}
       * @public
       * @memberof Meeting
       */
      this.stats = null;
      /**
       * @instance
       * @type {WebRTCStats}
       * @readonly
       * @private
       * @memberof Meeting
       */
      this.internalStats = null;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.conversationUrl = null;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.locusUrl = (attrs.locus && attrs.locus.url) || null;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.sipUri = null;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.partner = null;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.type = null;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.owner = null;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.hostId = null;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.policy = null;
      /**
       * @instance
       * @type {MeetingRequest}
       * @private
       * @memberof Meeting
       */
      this.meetingRequest = new MeetingRequest({}, options);
  
      /**
       * Passing only info as we send basic info for meeting added event
       * @instance
       * @type {MediaProperties}
       * @public
       * @memberof Meeting
       */
      this.mediaProperties = new MediaProperties();
      /**
       * @instance
       * @type {InMeetingActions}
       * @public
       * @memberof Meeting
      */
      this.inMeetingActions = new InMeetingActions();
      /**
       * @instance
       * @type {Boolean}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.isSharing = false;
      /**
       * @instance
       * @type {Array}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.callEvents = [];
      /**
       * There is a pending floor requested by the user
       * @instance
       * @type {floorGrantPending}
       * @private
       * @memberof Meeting
       */
      this.floorGrantPending = false;
      /**
       * @instance
       * @type {MediaMetrics}
       * @readonly
       * @private
       * @memberof Meeting
       */
      this.mediaQualityMetrics = null;
      /**
       * @instance
       * @type {StatsAnalyzer}
       * @private
       * @memberof Meeting
       */
      this.statsAnalyzer = null;
      /**
       * @instance
       * @type {MQAProcessor}
       * @readonly
       * @private
       * @memberof Meeting
       */
      this.mqaProcessor = null;
      /**
       * @instance
       * @type {String}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.networkStatus = null;
      /**
       * Passing only info as we send basic info for meeting added event
       * @instance
       * @type {MeetingRequest}
       * @private
       * @memberof Meeting
       */
      this.locusInfo = new LocusInfo(this.updateMeetingObject.bind(this), this.webex, this.id);
      // We had to add listeners first before setting up the locus instance
      /**
       * @instance
       * @type {Recording}
       * @readonly
       * @public
       * @memberof Meeting
       */
      this.recording = null;
  
      /**
       * Promise that exists if joining, and resolves upon method completion.
       * @instance
       * @type {Promise}
       * @private
       * @memberof Meeting
       */
      this.deferJoin = undefined;
  
      this.setUpLocusInfoListeners();
      this.locusInfo.init(attrs.locus ? attrs.locus : {});
    }
  
    /**
     * Proxy function for all the listener set ups
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setUpLocusInfoListeners() {
      // meeting update listeners
      this.setUpLocusInfoSelfListener();
      this.setUpLocusInfoMeetingListener();
      // members update listeners
      this.setUpLocusFullStateListener();
      this.setUpLocusUrlListener();
      this.setUpLocusHostListener();
      this.setUpLocusSelfListener();
      this.setUpLocusParticipantsListener();
      this.setupLocusControlsListener();
      this.setUpLocusMediaSharesListener();
      this.setUpLocusInfoMeetingInfoListener();
      this.setUpLocusInfoAssignHostListener();
      this.setUpLocusInfoMediaInactiveListener();
    }
  
  
    /**
     * Set up the locus info listener for meetings disconnected due to inactivity
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setUpLocusInfoMediaInactiveListener() {
      this.locusInfo.on(EVENTS.DISCONNECT_DUE_TO_INACTIVITY, (res) => {
        Metrics.sendOperationalMetric(
          METRICS_OPERATIONAL_MEASURES.DISCONNECT_DUE_TO_INACTIVITY,
          {
            correlation_id: this.correlationId,
            locus_id: this.locusId
          }
        );
  
        // Upload logs on media inactivity
        // Normally media should not be inactive
        Trigger.trigger(
          this,
          {
            file: 'meeting/index',
            function: 'setUpLocusInfoMediaInactiveListener'
          },
          EVENTS.REQUEST_UPLOAD_LOGS,
          this
        );
  
        // TODO: need to send metric for locus disconnect
        LoggerProxy.logger.error(`Meeting:index#setUpLocusInfoMediaInactiveListener --> Meeting disconnected due to inactivity: ${res.reason}`);
        this.reconnect();
      });
    }
  
    /**
     * Set up the locus info listener for assign host permissions on a meeting
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setUpLocusInfoAssignHostListener() {
      this.locusInfo.on(EVENTS.LOCUS_INFO_CAN_ASSIGN_HOST, (payload) => {
        Trigger.trigger(
          this,
          {
            file: 'meeting/index',
            function: 'setUpLocusInfoAssignHostListener'
          },
          EVENT_TRIGGERS.MEETING_ACTIONS_UPDATE,
          {
            canAssignHost: payload.canAssignHost,
            canLock: this.inMeetingActions.getCanLock(),
            canUnlock: this.inMeetingActions.getCanUnlock()
          }
        );
        this.inMeetingActions.setCanAssignHost(payload.canAssignHost);
      });
    }
  
    /**
     * Set up the internal locus info full state object listener
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setUpLocusFullStateListener() {
      this.locusInfo.on(LOCUSINFO.EVENTS.FULL_STATE_MEETING_STATE_CHANGE, (payload) => {
        Trigger.trigger(
          this,
          {
            file: 'meeting/index',
            function: 'setUpLocusFullStateListener'
          },
          EVENT_TRIGGERS.MEETING_STATE_CHANGE,
          {
            payload
          }
        );
      });
  
      this.locusInfo.on(LOCUSINFO.EVENTS.FULL_STATE_TYPE_UPDATE, (payload) => {
        this.members.locusFullStateTypeUpdate(payload);
      });
    }
  
    /**
     * get the metrics payload pre
     * @param {Object} options
     * @param {String} options.event
     * @param {String} options.trackingId
     * @param {Object} options.locus
     * @returns {Object}
     * @memberof Meeting
     */
    getAnalyzerMetricsPrePayload(options) {
      if (options) {
        const {event, trackingId, locus} = options;
  
        if (!event) {
          LoggerProxy.logger.error('Meeting:index#getAnalyzerMetricsPrePayload --> Error [Call Analyzer Event', event || '', `]: invalid identifers or event type! ${this.correlationId}`);
  
          return null;
        }
  
        const identifiers = {
          correlationId: this.correlationId,
          userId: this.userId,
          deviceId: this.deviceUrl,
          orgId: this.orgId
        };
  
        // If locus is passed take the value from there else use meeting object
        if (locus) {
          identifiers.locusId = locus.url.split('/').pop();
          identifiers.locusStartTime = locus.fullState.lastActive;
        }
        else if (this.locusUrl && this.locusInfo.fullState) {
          identifiers.locusId = this.locusUrl && this.locusUrl.split('/').pop();
          identifiers.locusStartTime = this.locusInfo.fullState && this.locusInfo.fullState.lastActive;
        }
  
        if (options.trackingId) {
          identifiers.trackingId = trackingId;
        }
  
        let payload = {};
  
        if (options.type === MQA_STATS.CA_TYPE) {
          payload = Metrics.initMediaPayload(options.event, identifiers, options);
        }
  
        else {
          payload = Metrics.initPayload(options.event, identifiers, options);
        }
  
        return payload;
      }
  
      return null;
    }
  
    /**
     * Send the metrics to call-analyzer dashboard
     * @param {Object} options
     * @param {String} options.event
     * @param {String} options.trackingId
     * @param {Object} options.locus
     * @returns {Promise}
     * @private
     * @memberof Meeting
     */
    sendCallAnalyzerMetrics(options) {
      const payload = this.getAnalyzerMetricsPrePayload(options);
  
      return this.webex.internal.metrics.submitCallDiagnosticEvents(payload);
    }
  
    /**
     * Send the metrics to Media Quality Analyzer dashboard
     * @param {Object} options
     * @param {String} options.event
     * @param {String} options.trackingId
     * @param {Object} options.locus
     * @returns {Promise}
     * @private
     * @memberof Meeting
     */
    sendMediaQualityAnalyzerMetrics(options) {
      options.type = MQA_STATS.CA_TYPE;
      options.clientType = this.config.metrics.clientType;
      options.subClientType = this.config.metrics.subClientType;
      const payload = this.getAnalyzerMetricsPrePayload(options);
  
      return this.webex.internal.metrics.submitCallDiagnosticEvents(payload);
    }
  
    /**
     * sets the network status on meeting object
     * @param {String} networkStatus
     * @private
     * @returns {undefined}
     * @memberof Meeting
     */
    setNetworkStatus(networkStatus) {
      if (networkStatus === NETWORK_STATUS.DISCONNECTED) {
        Trigger.trigger(
          this,
          {
            file: 'meeting/index',
            function: 'setNetworkStatus'
          },
          EVENT_TRIGGERS.MEETINGS_NETWORK_DISCONNECTED,
        );
      }
      else if (networkStatus === NETWORK_STATUS.CONNECTED && this.networkStatus === NETWORK_STATUS.DISCONNECTED) {
        Trigger.trigger(
          this,
          {
            file: 'meeting/index',
            function: 'setNetworkStatus'
          },
          EVENT_TRIGGERS.MEETINGS_NETWORK_CONNECTED,
        );
      }
  
      this.networkStatus = networkStatus;
    }
  
    /**
     * Set up the locus info self listener
     * update self value for members and updates the member
     * notifies consumer with members:self:update {activeSelfId endedSelfId}
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setUpLocusSelfListener() {
      this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_SELF, (payload) => {
        this.members.locusSelfUpdate(payload);
  
        // If user moved to a JOINED state and there is a pending floor grant trigger it
        if (this.floorGrantPending && payload.newSelf.state === MEETING_STATE.STATES.JOINED) {
          this.share()
            .then(() => { this.floorGrantPending = false; });
        }
      });
    }
  
    /**
     * Set up the locus info host listener
     * update host value for members and updates the member
     * notifies consumer with members:host:update: {activeHostId, endedHostId}
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setUpLocusHostListener() {
      this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_HOST, (payload) => {
        this.members.locusHostUpdate(payload);
      });
    }
  
    /**
     * Set up the locus info participants update listener
     * update members collection value for members
     * notifies consumer with members:update
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setUpLocusParticipantsListener() {
      this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_PARTICIPANTS, (payload) => {
        this.members.locusParticipantsUpdate(payload);
      });
    }
  
  
    /**
     * Set up the locus info recording update listener
     * update recording value for the meeting
     * notifies consumer with:
     *  meeting:recording:started
     *  meeting:recording:stopped
     *  meeting:recording:paused
     *  meeting:recording:resumed
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setupLocusControlsListener() {
      this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
        ({state, modifiedBy, lastModified}) => {
          let event;
  
          switch (state) {
            case RECORDING_STATE.RECORDING:
              event = EVENT_TRIGGERS.MEETING_STARTED_RECORDING;
              break;
            case RECORDING_STATE.IDLE:
              event = EVENT_TRIGGERS.MEETING_STOPPED_RECORDING;
              break;
            case RECORDING_STATE.PAUSED:
              event = EVENT_TRIGGERS.MEETING_PAUSED_RECORDING;
              break;
            case RECORDING_STATE.RESUMED:
              event = EVENT_TRIGGERS.MEETING_RESUMED_RECORDING;
              break;
            default:
              event = null;
              break;
          }
  
          // `RESUMED` state should be converted to `RECORDING` after triggering the event
          this.recording = {
            state: state === RECORDING_STATE.RESUMED ? RECORDING_STATE.RECORDING : state,
            modifiedBy,
            lastModified
          };
  
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'setupLocusControlsListener'
            },
            event,
            this.recording
          );
        });
    }
  
    /**
     * Set up the locus info media shares listener
     * update content sharing id value for members, and updates the member
     * notifies consumer with members:content:update {activeContentSharingId, endedContentSharingId}
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setUpLocusMediaSharesListener() {
      // Will get triggered on local and remote share
      this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES, (payload) => {
        const {contentId, disposition} = payload.current;
  
        // Check if screenshare is being taken over by a remote
        // If so, terminate the share and allow the remote to be on the floor
        // Terminating the share allows the user to screenshare again without any issues
        if (
          this.isSharing &&
          this.selfId !== contentId &&
          disposition !== FLOOR_ACTION.RELEASED
        ) {
          this.updateShare({
            sendShare: false,
            receiveShare: this.mediaProperties.mediaDirection.receiveShare
          }).finally(() => {
            this.isSharing = false;
  
            // Trigger remote share start event
            Trigger.trigger(
              this,
              {
                file: 'meetings/index',
                function: 'remoteShare'
              },
              EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE
            );
          });
        }
        // Check if remote started a share
        else if (
          this.selfId !== contentId &&
          disposition === FLOOR_ACTION.GRANTED
        ) {
          Trigger.trigger(
            this,
            {
              file: 'meetings/index',
              function: 'remoteShare'
            },
            EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE
          );
        }
        // check if remote stopped a share
        else if (
          this.selfId !== contentId &&
          disposition === FLOOR_ACTION.RELEASED
        ) {
          Trigger.trigger(
            this,
            {
              file: 'meetings/index',
              function: 'remoteShare'
            },
            EVENT_TRIGGERS.MEETING_STOPPED_SHARING_REMOTE
          );
        }
  
        this.members.locusMediaSharesUpdate(payload);
      });
    }
  
    /**
     * Set up the locus info url listener
     * update locus_url value for members
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setUpLocusUrlListener() {
      this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_URL, (payload) => {
        this.members.locusUrlUpdate(payload);
      });
    }
  
    /**
     * Set up the locus info meeting info listener
     * @returns {undefined}
     * @private
     * @memberof meeting
     */
    setUpLocusInfoMeetingInfoListener() {
      this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_LOCKED, (payload) => {
        if (payload) {
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'setUpLocusInfoMeetingInfoListener'
            },
            EVENT_TRIGGERS.MEETING_LOCKED,
            {
              payload
            }
          );
        }
      });
      this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_UNLOCKED, (payload) => {
        if (payload) {
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'setUpLocusInfoMeetingInfoListener'
            },
            EVENT_TRIGGERS.MEETING_UNLOCKED,
            {
              payload
            }
          );
        }
      });
      this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, (payload) => {
        if (payload && payload.info) {
          let newCanLock;
          let newCanUnlock;
  
          if (payload.self && !payload.self.moderator) {
            newCanLock = false;
            newCanUnlock = false;
          }
          else {
            newCanLock = MeetingUtil.canUserLock(payload.info.moderator, payload.info.policy);
            newCanUnlock = MeetingUtil.canUserUnlock(payload.info.moderator, payload.info.policy);
          }
  
          if (newCanLock && !this.inMeetingActions.canLock) {
            Trigger.trigger(
              this,
              {
                file: 'meeting/index',
                function: 'setUpLocusInfoMeetingInfoListener'
              },
              EVENT_TRIGGERS.MEETING_ACTIONS_UPDATE,
              {
                canLock: true,
                canUnlock: false,
                canAssignHost: this.inMeetingActions.getCanAssignHost()
              }
            );
          }
          if (newCanUnlock && !this.inMeetingActions.canUnlock || !newCanLock) {
            Trigger.trigger(
              this,
              {
                file: 'meeting/index',
                function: 'setUpLocusInfoMeetingInfoListener'
              },
              EVENT_TRIGGERS.MEETING_ACTIONS_UPDATE,
              {
                canLock: false,
                canUnlock: true,
                canAssignHost: this.inMeetingActions.getCanAssignHost()
              }
            );
          }
          this.inMeetingActions.setCanLock(newCanLock || false);
          this.inMeetingActions.setCanUnlock(newCanUnlock || false);
        }
      });
    }
  
    /**
     * Internal function to listen to the self object changes
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setUpLocusInfoSelfListener() {
      this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MUTED_BY_OTHERS, (payload) => {
        if (payload) {
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'setUpLocusInfoSelfListener'
            },
            EVENT_TRIGGERS.MEETING_SELF_MUTED_BY_OTHERS,
            {
              payload
            }
          );
        }
      });
      this.locusInfo.on(LOCUSINFO.EVENTS.SELF_UNADMITTED_GUEST, (payload) => {
        if (payload) {
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'setUpLocusInfoSelfListener'
            },
            EVENT_TRIGGERS.MEETING_SELF_LOBBY_WAITING,
            {
              payload
            }
          );
  
          Metrics.postEvent({
            event: eventType.LOBBY_ENTERED,
            meeting: this
          });
        }
      });
      this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
        if (payload) {
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'setUpLocusInfoSelfListener'
            },
            EVENT_TRIGGERS.MEETING_SELF_GUEST_ADMITTED,
            {
              payload
            }
          );
  
          Metrics.postEvent({
            event: eventType.LOBBY_EXITED,
            meeting: this
          });
        }
      });
  
      this.locusInfo.on(LOCUSINFO.EVENTS.MEDIA_INACTIVITY, () => {
        Metrics.sendOperationalMetric(
          METRICS_OPERATIONAL_MEASURES.MEETING_MEDIA_INACTIVE,
          {
            correlation_id: this.correlationId,
            locus_id: this.locusId
          }
        );
        this.reconnect();
      });
  
      // There is two stats for mute one is the actual media being sent or received
      // The second on is if the audio is muted, we need to tell the statsAnalyzer when
      // the audio is muted or the user is not willing to send media
      this.locusInfo.on(LOCUSINFO.EVENTS.MEDIA_STATUS_CHANGE, (status) => {
        if (this.statsAnalyzer) {
          this.statsAnalyzer.updateMediaStatus({
            actual: status,
            expected: {
              // We need to check what should be the actual direction of media
              sendAudio: this.mediaProperties.mediaDirection?.sendAudio && !this.audio?.isMuted(),
              sendVideo: this.mediaProperties.mediaDirection?.sendVideo && !this.video?.isMuted(),
              sendShare: this.mediaProperties.mediaDirection?.sendShare && this.isSharing,
              receiveAudio: this.mediaProperties.mediaDirection?.receiveAudio,
              receiveVideo: this.mediaProperties.mediaDirection?.receiveVideo,
              receiveShare: this.mediaProperties.mediaDirection?.receiveShare
            }
          });
        }
      });
    }
  
    /**
     * Add LocusInfo nested object listeners (from child to parent)
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setUpLocusInfoMeetingListener() {
      this.locusInfo.on(EVENTS.REMOTE_ANSWERED_DECLINED, (payload) => {
        this.meetingFiniteStateMachine.remote(payload);
      });
      this.locusInfo.on(EVENTS.DESTROY_MEETING, (payload) => {
        // if self state is NOT left
  
        // TODO: Handle sharing and wireless sharing when meeting end
        if (this.wirelessShare) {
          if (this.mediaProperties.shareTrack) {
            this.mediaProperties.shareTrack.onended = null;
            this.mediaProperties.shareTrack.stop();
          }
        }
        // when multiple WEB deviceType join with same user
        // and some of the devices are joined and some are left
        // when your own device is still connected you want to leave and destroy
        // else you want to just destroy
        // this looks odd because when it leaves it should destroy, but we get a
        // leave response and we should destroy it on the next event loop
        // the leave response gets parsed and we decide if we want to destroy the meeting
        // the first time we just leave it, the second time it comes it destroys it from the collection
        if (payload.shouldLeave) {
          // TODO:  We should do cleaning of meeting object if the shouldLeave: false because there might be meeting object which we are not cleaning
  
          this.leave({reason: payload.reason}).then(() => {
            LoggerProxy.logger.warn('Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. The meeting has been left, but has not been destroyed, you should see a later event for leave.');
          }).catch((error) => {
            LoggerProxy.logger.error(`Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. Issue with leave for meeting, meeting still in collection: ${this.meeting}, error: ${error}`);
          });
        }
        else {
          LoggerProxy.logger.info('Meeting:index#setUpLocusInfoMeetingListener --> MEETING_REMOVED_REASON', payload.reason);
          MeetingUtil.cleanUp(this);
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'setUpLocusInfoMeetingListener'
            },
            EVENTS.DESTROY_MEETING,
            {
              reason: payload.reason,
              meetingId: this.id
            }
          );
        }
      });
    }
  
    /**
     * Set meeting values rather than events
     * @param {Object} object
     * @returns {undefined}
     * @private
     * @memberof Meeting
     * // TODO: is this function necessary?
     */
    updateMeetingObject(object) {
      // Validate if these are valid meeting object property
      // TODO: add a check to make sure the value passed in the constructor
      // is not changed by any delta event
      if (object && Object.keys(object).length) {
        Object.keys(object).forEach((key) => {
          this[key] = object[key];
        });
      }
    }
  
    /**
     * Invite a guest to the call that isn't normally part of this call
     * @param {Object} invitee
     * @param {String} invitee.emailAddress
     * @param {String} invitee.email
     * @param {Boolean} [alertIfActive]
     * @returns {Promise} see #members.addMember
     * @public
     * @memberof Meeting
     */
    invite(invitee, alertIfActive = true) {
      return this.members.addMember(invitee, alertIfActive);
    }
  
    /**
     * Admit the guest(s) to the call once they are waiting
     * @param {Array} memberIds
     * @returns {Promise} see #members.admitMembers
     * @public
     * @memberof Meeting
     */
    admit(memberIds) {
      return this.members.admitMembers(memberIds);
    }
  
    /**
     * Remove the member from the meeting, boot them
     * @param {String} memberId
     * @returns {Promise} see #members.removeMember
     * @public
     * @memberof Meeting
     */
    remove(memberId) {
      return this.members.removeMember(memberId);
    }
  
    /**
     * Mute another member from the meeting
     * @param {String} memberId
     * @param {Boolean} mute
     * @returns {Promise} see #members.muteMember
     * @public
     * @memberof Meeting
     */
    mute(memberId, mute = true) {
      return this.members.muteMember(memberId, mute);
    }
  
    /**
     * Transfer the moderator role to another eligible member
     * @param {String} memberId
     * @param {Boolean} moderator
     * @returns {Promise} see #members.transferHostToMember
     * @public
     * @memberof Meeting
     */
    transfer(memberId, moderator = true) {
      return this.members.transferHostToMember(memberId, moderator);
    }
  
    /**
     * Reference to the Members object
     * @returns {Members}
     * @public
     * @memberof Meeting
     */
    getMembers() {
      return this.members;
    }
  
    /**
     * If this gets turned off mid meeting after attaching media, it will shut down the MQA metrics and they will
     * not restart unless the start function is called again
     * @returns {undefined}
     * @public
     * @memberof Meeting
     */
    cleanMQAInterval() {
      if (this.mqaInterval) {
        clearInterval(this.mqaInterval);
      }
    }
  
    /**
     * Automatically publishes media metrics data and on a config interval
     * uses the existing getStats data filter, so no new streams are created
     * but it is separate than history, so history will not be available
     * @returns {MediaMetrics}
     * @public
     * @memberof Meeting
     */
    startMediaQualityMetrics() {
      const automaticMetrics = new StatsMetrics({config: this.config});
  
      const stats = this.getStats(automaticMetrics.initialize(), true);
  
      this.mediaQualityMetrics = automaticMetrics;
  
      this.mediaQualityMetrics.setStats(stats);
  
      this.mqaInterval = setInterval(() => this.processMQAData(), this.config.metrics.mqaMetricsInterval);
  
      return automaticMetrics;
    }
  
    /**
     * @private
     * @returns {undefined}
     * @memberof Meeting
     */
    processMQAData() {
      if (!this.mqaProcessor) {
        this.mqaProcessor = new MQAProcessor();
      }
  
      MQA_STATS.DATA_PLACEMENTS.forEach((key) => {
        if (this.mediaQualityMetrics && this.mediaQualityMetrics.stats) {
          let sendRecvData;
  
          if (key.toLowerCase().endsWith(STATS.SENDER)) {
            sendRecvData = this.mediaQualityMetrics.stats.getSender(key);
          }
          else if (key.toLowerCase().endsWith(STATS.RECEIVER)) {
            sendRecvData = this.mediaQualityMetrics.stats.getReceiver(key);
          }
          let mqa;
  
          if (sendRecvData) {
            mqa = sendRecvData.getMQA();
          }
          if (mqa) {
            const interval = mqa.getSlice(this.config.metrics.mqaMetricsInterval / 1000); // milliseconds -> second based intervals
  
            this.mqaProcessor.process(key, interval);
          }
        }
      });
    }
  
    /**
     * Reference to the stats builder object
     * @param {Object} options - see #createStats
     * @param {Boolean} override - override the previous getStats
     * @returns {WebRTCStats}
     * @public
     * @memberof Meeting
     */
    getStats(options, override) {
      if (!this.stats) {
        return this.createStats(options);
      }
      if (override) {
        if (this.stats) {
          LoggerProxy.logger.log('Meeting:index#getStats --> Overriding the previous stats object without destroying first can result in memory leaks.');
        }
  
        return this.createStats(options);
      }
  
      return this.stats;
    }
  
    /**
     * write the stats builder object and assign to meeting property
     * @param {Object} options
     * @returns {WebRTCStats}
     * @public
     * @memberof Meeting
     */
    createStats(options = {}) {
      StatsUtil.generateOptions(options, STATS.CONFIG, this);
  
      options.config = STATS.CONFIG;
  
      this.stats = new WebRTCStats(this.attrs, this.options, options);
  
      return this.stats;
    }
  
    /**
     * if you have started a stats instance, here's how you can stop it
     * @returns {undefined}
     * @public
     * @memberof Meeting
     */
    stopStats() {
      if (this.stats) {
        this.stats.destroySenders();
        this.stats.destroyReceivers();
        this.stats = null;
      }
    }
  
    /**
     * Convenience function to tell whether a meeting is muted
     * @returns {Boolean} if meeting audio muted or not
     * @public
     * @memberof Meeting
     */
    isAudioMuted() {
      return this.audio && this.audio.isMuted();
    }
  
    /**
     * Convenience function to tell if the end user last changed the audio state
     * @returns {Boolean} if audio was manipulated by the end user
     * @public
     * @memberof Meeting
     */
    isAudioSelf() {
      return this.audio && this.audio.isSelf();
    }
  
    /**
     * Convenience function to tell whether video is muted
     * @returns {Boolean} if meeting video is muted or not
     * @public
     * @memberof Meeting
     */
    isVideoMuted() {
      return this.video && this.video.isMuted();
    }
  
    /**
     * Convenience function to tell whether the end user changed the video state
     * @returns {Boolean} if meeting video is muted or not
     * @public
     * @memberof Meeting
     */
    isVideoSelf() {
      return this.video && this.video.isSelf();
    }
  
    /**
     * Sets the meeting info on the class instance
     * @param {Object} meetingInfo
     * @param {Object} meetingInfo.body
     * @param {String} meetingInfo.body.conversationUrl
     * @param {String} meetingInfo.body.locusUrl
     * @param {String} meetingInfo.body.sipUri
     * @param {Object} meetingInfo.body.owner
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    parseMeetingInfo(meetingInfo) {
      // MeetingInfo will be undefined for 1:1 calls
      if (meetingInfo && meetingInfo.body && !(meetingInfo.errors && meetingInfo.errors.length > 0)) {
        this.conversationUrl = meetingInfo.body.conversationUrl || this.conversationUrl;
        this.locusUrl = meetingInfo.body.locusUrl || this.locusUrl;
        this.setSipUri(meetingInfo.body.sipMeetingUri || this.sipUri);
        this.owner = meetingInfo.body.owner || this.owner;
      }
    }
  
    /**
     * Sets the first locus info on the class instance
     * @param {Object} locus
     * @param {String} locus.url
     * @param {Array} locus.participants
     * @param {Object} locus.self
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    parseLocus(locus) {
      if (locus) {
        this.locusUrl = locus.url;
        // TODO: move this to parse participants module
        this.setLocus(locus);
  
        // check if we can extract this info from partner
        // Parsing of locus object must be finished at this state
        if (locus.participants && locus.self) {
          this.partner = MeetingUtil.getLocusPartner(locus.participants, locus.self);
        }
  
        // For webex meeting the sipUrl gets updated in info parser
        if (!this.sipUri && this.partner && this.type === _CALL_) {
          this.setSipUri(this.partner.person.sipUrl || this.partner.person.id);
        }
      }
    }
  
    /**
     * Sets the sip uri on the class instance
     * uses meeting info as precedence
     * @param {String} sipUri
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setSipUri(sipUri) {
      // This can be tel no, device id or a sip uri, user Id
      this.sipUri = sipUri;
    }
  
    /**
     * Set the roap seq on the class instance
     * @param {Number} seq
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setRoapSeq(seq) {
      if (seq >= 0) {
        this.roapSeq = seq;
      }
    }
  
    /**
     * Set the locus info the class instance
     * @param {Object} locus
     * @param {Array} locus.mediaConnections
     * @param {String} locus.locusUrl
     * @param {String} locus.locusId
     * @param {String} locus.mediaId
     * @param {Object} locus.host
     * @todo change name to genertic parser
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setLocus(locus) {
      const mtgLocus = locus.locus || locus;
  
      // LocusInfo object saves the locus object
      // this.locus = mtgLocus;
      this.mediaConnections = locus.mediaConnections;
      this.locusUrl = locus.locusUrl || locus.url;
      this.locusId = locus.locusId;
      this.selfId = locus.selfId;
      this.mediaId = locus.mediaId;
      this.hostId = mtgLocus.host ? mtgLocus.host.id : this.hostId;
      this.locusInfo.initialSetup(mtgLocus);
    }
  
    /**
     * Sets the remote stream on the class instance and emits and
     * event to developers
     * @param {Object} pc The remote stream peer connection
     * @returns {undefined}
     * @public
     * @memberof Meeting
     */
    setRemoteStream(pc) {
      if (!pc) {
        return;
      }
      // eslint-disable-next-line no-param-reassign
      pc.ontrack = (event) => {
        // eslint-disable-next-line no-warning-comments
        // TODO: It's possible for media to not be present
        // so we might need to either
        // A) wait until we have media flowing
        // B) trigger a second event when video is flowing
        LoggerProxy.logger.log(`Meeting:index#setRemoteStream --> ontrack event received for peerConnection: ${event}`);
  
        const MEDIA_ID = {
          AUDIO_TRACK: '0',
          VIDEO_TRACK: '1',
          SHARE_TRACK: '2'
        };
        let eventType = null;
        const mediaTrack = event.track;
        let trackMediaID = null;
  
  
        // In case of safari some time the transceiver is not present for specific os version
        // sdk tries to determine the transceive using the track id present
        if (event.transceiver && event.transceiver.mid) {
          trackMediaID = event.transceiver.mid;
        }
        else {
          const {audioTransceiver, videoTransceiver, shareTransceiver} = event.target;
  
          // audio kind indicates its a audio stream
          if (mediaTrack.id === audioTransceiver.receiver.track.id) {
            trackMediaID = '0';
          }
          else
          if (mediaTrack.id === videoTransceiver.receiver.track.id) {
            trackMediaID = '1';
          }
          else
          if (mediaTrack.id === shareTransceiver.receiver.track.id) {
            trackMediaID = '2';
          }
          else {
            trackMediaID = null;
            Metrics.sendOperationalMetric(
              METRICS_OPERATIONAL_MEASURES.MUTE_AUDIO_FAILURE,
              {
                correlation_id: this.correlationId,
                locus_id: this.locusUrl.split('/').pop()
              }
            );
          }
        }
  
  
        switch (trackMediaID) {
          case MEDIA_ID.AUDIO_TRACK:
            eventType = EVENT_TYPES.REMOTE_AUDIO;
            this.mediaProperties.setRemoteAudioTrack(mediaTrack);
            break;
          case MEDIA_ID.VIDEO_TRACK:
            eventType = EVENT_TYPES.REMOTE_VIDEO;
            this.mediaProperties.setRemoteVideoTrack(mediaTrack);
            break;
          case MEDIA_ID.SHARE_TRACK:
            if (event.track) {
              eventType = EVENT_TYPES.REMOTE_SHARE;
              this.mediaProperties.setRemoteShare(mediaTrack);
            }
            break;
          default: {
            LoggerProxy.logger.log('Meeting:index#setRemoteStream --> no matching media track id');
          }
        }
  
        // start stats here the stats are coming null if you dont receive streams
  
        this.statsAnalyzer.startAnalyzer(this.mediaProperties.peerConnection);
  
        if (eventType && mediaTrack) {
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'setRemoteStream:pc.ontrack'
            },
            EVENT_TRIGGERS.MEDIA_READY,
            {
              type: eventType,
              stream: MediaUtil.createMediaStream([mediaTrack])
            }
          );
        }
      };
    }
  
    /**
     * Removes remote audio and video stream on the class instance and triggers an event
     * to developers
     * @returns {undefined}
     * @public
     * @memberof Meeting
     * @deprecated after v1.89.3
     */
    unsetRemoteStream() {
      LoggerProxy.logger.warn('Meeting:index#unsetRemoteStream --> [DEPRECATION WARNING]: unsetRemoteStream has been deprecated after v1.89.3');
      this.mediaProperties.unsetRemoteMedia();
    }
  
    /**
     * Removes remote audio, video and share tracks from class instance's mediaProperties
     * @returns {undefined}
     */
    unsetRemoteTracks() {
      this.mediaProperties.unsetRemoteTracks();
    }
  
    /**
     * Removes the remote stream on the class instance and triggers an event
     * to developers
     * @returns {undefined}
     * @public
     * @memberof Meeting
     * @deprecated after v1.89.3
     */
    closeRemoteStream() {
      LoggerProxy.logger.warn('Meeting:index#closeRemoteStream --> [DEPRECATION WARNING]: closeRemoteStream has been deprecated after v1.89.3');
      this.closeRemoteTracks();
    }
  
    /**
     * Removes the remote tracks on the class instance and triggers an event
     * to developers
     * @returns {undefined}
     * @memberof Meeting
     */
    closeRemoteTracks() {
      const {
        remoteAudioTrack,
        remoteVideoTrack,
        remoteShare
      } = this.mediaProperties;
  
      /**
       * Triggers an event to the developer
       * @param {string} mediaType Type of media that was stopped
       * @returns {void}
       * @inner
       */
      const triggerMediaStoppedEvent = (mediaType) => {
        Trigger.trigger(
          this,
          {
            file: 'meeting/index',
            function: 'closeRemoteTracks'
          },
          EVENT_TRIGGERS.MEDIA_STOPPED,
          {
            type: mediaType
          }
        );
      };
  
      /**
       * Stops a media track and emits an event
       * @param {MediaStreamTrack} track Media track to stop
       * @param {string} type Media track type
       * @returns {Promise}
       * @inner
       */
      // eslint-disable-next-line arrow-body-style
      const stopTrack = (track, type) => {
        return Media.stopTracks(track)
          .then(() => {
            const isTrackStopped = track && track.readyState === ENDED;
            const isWrongReadyState = track && !isTrackStopped;
  
            if (isTrackStopped) {
              triggerMediaStoppedEvent(type);
            }
            else if (isWrongReadyState) {
              LoggerProxy.logger.warn(`Meeting:index#closeRemoteTracks --> Error: MediaStreamTrack.readyState is ${track.readyState} for ${type}`);
            }
          });
      };
  
      return Promise.all([
        stopTrack(remoteAudioTrack, EVENT_TYPES.REMOTE_AUDIO),
        stopTrack(remoteVideoTrack, EVENT_TYPES.REMOTE_VIDEO),
        stopTrack(remoteShare, EVENT_TYPES.REMOTE_SHARE)
      ]);
    }
  
    /**
     * Sets the local media stream on the class and emits an event to the developer
     * @param {Stream} localStream the local media stream
     * @returns {undefined}
     * @public
     * @memberof Meeting
     */
    setLocalTracks(localStream) {
      if (localStream) {
        const {audioTrack, videoTrack} = MeetingUtil.getTrack(localStream);
        let settings = null;
  
        if (audioTrack) {
          settings = audioTrack.getSettings();
          this.mediaProperties.setMediaSettings('audio', {
            echoCancellation: settings.echoCancellation,
            noiseSuppression: settings.noiseSuppression
          });
  
          LoggerProxy.logger.log('Meeting:index#setLocalTracks --> Audio settings.', JSON.stringify(this.mediaProperties.mediaSettings.audio));
          this.mediaProperties.setLocalAudioTrack(audioTrack);
        }
  
        if (videoTrack) {
          settings = videoTrack.getSettings();
          this.mediaProperties.setLocalVideoTrack(videoTrack);
          this.mediaProperties.setMediaSettings('video', {
            aspectRatio: settings.aspectRatio,
            frameRate: settings.frameRate,
            height: settings.height,
            width: settings.width
          });
          // store and save the selected video input device
          if (settings.deviceId) {
            this.mediaProperties.setVideoDeviceId(settings.deviceId);
          }
          LoggerProxy.logger.log('Meeting:index#setLocalTracks --> Video settings.', JSON.stringify(this.mediaProperties.mediaSettings.video));
        }
  
        Trigger.trigger(
          this,
          {
            file: 'meeting/index',
            function: 'setLocalTracks'
          },
          EVENT_TRIGGERS.MEDIA_READY,
          {
            type: EVENT_TYPES.LOCAL,
            stream: MediaUtil.createMediaStream([this.mediaProperties.audioTrack, this.mediaProperties.videoTrack])
          }
        );
      }
    }
  
    /**
     * Sets the local media stream on the class and emits an event to the developer
     * @param {Stream} localShare the local media stream
     * @returns {undefined}
     * @public
     * @memberof Meeting
     */
    setLocalShareTrack(localShare) {
      let settings = null;
  
      if (localShare) {
        this.mediaProperties.setLocalShareTrack(MeetingUtil.getTrack(localShare).videoTrack);
        const contentTracks = this.mediaProperties.shareTrack;
  
        if (contentTracks) {
          settings = contentTracks.getSettings();
          this.mediaProperties.setMediaSettings('screen', {
            aspectRatio: settings.aspectRatio,
            frameRate: settings.frameRate,
            height: settings.height,
            width: settings.width,
            displaySurface: settings.displaySurface,
            cursor: settings.cursor
          });
          LoggerProxy.logger.log('Meeting:index#setLocalTracks --> Screen settings.', JSON.stringify(this.mediaProperties.mediaSettings.screen));
        }
  
        contentTracks.onended = () => {
          if (this.wirelessShare) {
            this.leave({reason: MEETING_REMOVED_REASON.USER_ENDED_SHARE_STREAMS});
          }
          else {
            this.stopShare();
          }
        };
  
        Trigger.trigger(
          this,
          {
            file: 'meeting/index',
            function: 'setLocalShareTrack'
          },
          EVENT_TRIGGERS.MEDIA_READY,
          {
            type: EVENT_TYPES.LOCAL_SHARE,
            stream: localShare
          }
        );
      }
    }
  
    /**
     * Closes the local stream from the class and emits an event to the developer
     * @returns {undefined}
     * @event media:stopped
     * @public
     * @memberof Meeting
     */
    closeLocalStream() {
      const {audioTrack, videoTrack} = this.mediaProperties;
  
      return Media.stopTracks(audioTrack)
        .then(() => Media.stopTracks(videoTrack))
        .then(() => {
          const audioStopped = audioTrack && audioTrack.readyState === ENDED;
          const videoStopped = videoTrack && videoTrack.readyState === ENDED;
  
          if (audioStopped && videoStopped) {
            Trigger.trigger(
              this,
              {
                file: 'meeting/index',
                function: 'closeLocalStream'
              },
              EVENT_TRIGGERS.MEDIA_STOPPED, {
                type: EVENT_TYPES.LOCAL
              }
            );
          }
          else if (audioTrack || videoTrack) {
            LoggerProxy.logger.warn('Meeting:index#closeLocalStream --> Error: MediaStreamTrack.readyState is incorrect.');
          }
        });
    }
  
    /**
     * Closes the local stream from the class and emits an event to the developer
     * @returns {undefined}
     * @event media:stopped
     * @public
     * @memberof Meeting
     */
    closeLocalShare() {
      const track = this.mediaProperties.shareTrack;
  
      return Media.stopTracks(track).then(() => {
        if (track && track.readyState === ENDED) {
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'closeLocalShare'
            },
            EVENT_TRIGGERS.MEDIA_STOPPED, {
              type: EVENT_TYPES.LOCAL_SHARE
            }
          );
        }
        else if (track) {
          // Track exists but with wrong readyState
          LoggerProxy.logger.warn(`Meeting:index#closeLocalShare --> Error: MediaStreamTrack.readyState is ${track.readyState} for localShare`);
        }
      });
    }
  
    /**
     * Removes the local stream from the class and emits an event to the developer
     * @returns {undefined}
     * @public
     * @memberof Meeting
     */
    unsetLocalVideoTrack() {
      this.mediaProperties.unsetLocalVideoTrack();
    }
  
    /**
     * Removes the local share from the class and emits an event to the developer
     * @returns {undefined}
     * @public
     * @memberof Meeting
     */
    unsetLocalShareTrack() {
      this.mediaProperties.unsetLocalShareTrack();
    }
  
    /**
     * sets up reconnection logic
     * @returns {undefined}
     * @public
     * @memberof Meeting
     * @todo remove
     * // TODO: remove??
     */
    setReconnectListener() {
      // Client will have a socket manager and handle reconnecting to mercury, when we reconnect to mercury
      // if the meeting has active peer connections, it should try to reconnect.
      this.webex.internal.mercury.on(ONLINE, () => {
        LoggerProxy.logger.log('Meeting:index#setReconnectListener --> Web socket online');
      });
    }
  
    /**
     * Close the peer connections and remove them from the class. Triggers an event
     * when each is closed.
     * @returns {Promise} returns a resolved promise with an array of closed peer connections
     * @public
     * @memberof Meeting
     */
    closePeerConnections() {
      return PeerConnectionManager.close(this.mediaProperties.peerConnection);
    }
  
    /**
     * Unsets the peer connections on the class
     * warning DO NOT CALL WITHOUT CLOSING PEER CONNECTIONS FIRST
     * @param {PeerConnection} peerConnection
     * @returns {undefined}
     * @public
     * @memberof Meeting
     */
    unsetPeerConnections() {
      this.mediaProperties.unsetPeerConnection();
      if (this.config.reconnection.detection) {
        this.webex.internal.mercury.off(ONLINE);
      }
    }
  
    /**
     * Convenience method to set the correlation id for the Meeting
     * @param {String} id correlation id to set on the class
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    setCorrelationId(id) {
      this.correlationId = id;
    }
  
    /**
     * Mute the audio for a meeting
     * @returns {Promise} resolves the data from muting audio {mute, self} or rejects if there is no audio set
     * @public
     * @memberof Meeting
     */
    muteAudio() {
      if (!this.audio || (this.audio && !this.audio.toggle)) {
        return Promise.reject(new ParameterError('no audio control associated to the meeting'));
      }
  
      const LOG_HEADER = 'Meeting:index#muteAudio -->';
  
      // First, stop sending the local audio media
      return logRequest(this.audio.toggle({
        mute: true,
        self: true
      })
        .then(() => {
          Metrics.postEvent({
            event: eventType.MUTED,
            meeting: this,
            data: {trigger: trigger.USER_INTERACTION, mediaType: mediaType.AUDIO}
          });
        }).catch((error) => {
          Metrics.sendOperationalMetric(
            METRICS_OPERATIONAL_MEASURES.MUTE_AUDIO_FAILURE,
            {
              correlation_id: this.correlationId,
              locus_id: this.locusUrl.split('/').pop(),
              reason: error.message,
              stack: error.stack
            }
          );
  
          throw error;
        }),
      {
        header: `${LOG_HEADER} muting audio`,
        success: `${LOG_HEADER} muted audio successfully`,
        failure: `${LOG_HEADER} muting audio failed, `
      });
    }
  
    /**
     * Unmute meeting audio
     * @returns {Promise} resolves data from muting audio {mute, self} or rejects if there is no audio set
     * @public
     * @memberof Meeting
     */
    unmuteAudio() {
      if (!this.audio || (this.audio && !this.audio.toggle)) {
        return Promise.reject(new ParameterError('no audio control associated to the meeting'));
      }
  
      const LOG_HEADER = 'Meeting:index#unmuteAudio -->';
  
      // First, send the control to unmute the participant on the server
      return logRequest(this.mute(this.members.selfId, false)
        // Next, start sending the local audio media
        .then(() => this.audio.toggle({
          mute: false,
          self: true
        }))
        .then(() => {
          Metrics.postEvent({
            event: eventType.UNMUTED,
            meeting: this,
            data: {trigger: trigger.USER_INTERACTION, mediaType: mediaType.AUDIO}
          });
        }).catch((error) => {
          Metrics.sendOperationalMetric(
            METRICS_OPERATIONAL_MEASURES.UNMUTE_AUDIO_FAILURE,
            {
              correlation_id: this.correlationId,
              locus_id: this.locusUrl.split('/').pop(),
              reason: error.message,
              stack: error.stack
            }
          );
  
          throw error;
        }),
      {
        header: `${LOG_HEADER} unmuting audio`,
        success: `${LOG_HEADER} unmuted audio successfully`,
        failure: `${LOG_HEADER} unmuting audio failed, `
      });
    }
  
    /**
     * Mute the video for a meeting
     * @returns {Promise} resolves data from muting video {mute, self} or rejects if there is no video set
     * @public
     * @memberof Meeting
     */
    muteVideo() {
      if (!this.video || (this.video && !this.video.toggle)) {
        return Promise.reject(new ParameterError('no video control associated to the meeting'));
      }
  
      const LOG_HEADER = 'Meeting:index#muteVideo -->';
  
      return logRequest(this.video.toggle({
        mute: true,
        self: true
      })
        .then(() => {
          Metrics.postEvent({
            event: eventType.MUTED,
            meeting: this,
            data: {trigger: trigger.USER_INTERACTION, mediaType: mediaType.VIDEO}
          });
        }).catch((error) => {
          Metrics.sendOperationalMetric(
            METRICS_OPERATIONAL_MEASURES.MUTE_VIDEO_FAILURE,
            {
              correlation_id: this.correlationId,
              locus_id: this.locusUrl.split('/').pop(),
              reason: error.message,
              stack: error.stack
            }
          );
  
          throw error;
        }),
      {
        header: `${LOG_HEADER} muting video`,
        success: `${LOG_HEADER} muted video successfully`,
        failure: `${LOG_HEADER} muting video failed, `
      });
    }
  
    /**
     * Unmute meeting video
     * @returns {Promise} resolves data from muting video {mute, self} or rejects if there is no video set
     * @public
     * @memberof Meeting
     */
    unmuteVideo() {
      if (!this.video || (this.video && !this.video.toggle)) {
        return Promise.reject(new ParameterError('no audio control associated to the meeting'));
      }
  
      const LOG_HEADER = 'Meeting:index#unmuteVideo -->';
  
      return logRequest(this.video.toggle({
        mute: false,
        self: true
      })
        .then(() => {
          Metrics.postEvent({
            event: eventType.UNMUTED,
            meeting: this,
            data: {trigger: trigger.USER_INTERACTION, mediaType: mediaType.VIDEO}
          });
        }).catch((error) => {
          Metrics.sendOperationalMetric(
            METRICS_OPERATIONAL_MEASURES.UNMUTE_VIDEO_FAILURE,
            {
              correlation_id: this.correlationId,
              locus_id: this.locusUrl.split('/').pop(),
              reason: error.message,
              stack: error.stack
            }
          );
  
          throw error;
        }),
      {
        header: `${LOG_HEADER} unmuting video`,
        success: `${LOG_HEADER} unmuted video successfully`,
        failure: `${LOG_HEADER} unmuting video failed, `
      });
    }
  
    /**
     * Shorthand function to join AND set up media
     * @param {Object} options - options to join with media
     * @param {JoinOptions} [options.joinOptions] - see #join()
     * @param {MediaDirection} options.mediaSettings - see #addMedia()
     * @param {AudioVideo} [options.audioVideoOptions] - see #getMediaStreams()
     * @returns {Promise} -- {join: see join(), media: see addMedia(), local: see getMediaStreams()}
     * @public
     * @memberof Meeting
     * @example
     * joinWithMedia({
     *  joinOptions: {resourceId: 'resourceId' },
     *  mediaSettings: {
     *   sendAudio: true,
     *   sendVideo: true,
     *   sendShare: false,
     *   receiveVideo:true,
     *   receiveAudio: true,
     *   receiveShare: true
     * }
     * audioVideoOptions: {
     *   audio: 'audioDeviceId',
     *   video: 'videoDeviceId'
     * }})
     */
    joinWithMedia(options = {}) {
      // TODO: add validations for parameters
      const {mediaSettings, joinOptions, audioVideoOptions} = options;
  
      return this.join(joinOptions)
        .then((joinResponse) =>
          this.getMediaStreams(mediaSettings, audioVideoOptions).then(([localStream, localShare]) =>
            this.addMedia({
              mediaSettings,
              localShare,
              localStream
            }).then((mediaResponse) => ({
              join: joinResponse,
              media: mediaResponse,
              local: [localStream, localShare]
            }))))
        .catch((error) => {
          LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
  
          Metrics.sendOperationalMetric(
            METRICS_OPERATIONAL_MEASURES.JOIN_WITH_MEDIA_FAILURE,
            {
              correlation_id: this.correlationId,
              locus_id: this.locusUrl.split('/').pop(),
              reason: error.message,
              stack: error.stack
            },
            {
              type: error.name
            }
          );
  
          return Promise.reject(error);
        });
    }
  
    /**
     * Initiates the reconnection of the media in the meeting
     *
     * @param {object} options
     * @returns {Promise} resolves with {reconnect} or errors with {error}
     * @public
     * @memberof Meeting
     */
    reconnect(options) {
      LoggerProxy.logger.log(`Meeting:index#reconnect --> attempting to reconnect meeting ${this.id}`);
  
      if (!this.reconnectionManager || !this.reconnectionManager.reconnect) {
        throw new ParameterError('Cannot reconnect, ReconnectionManager must first be defined.');
      }
  
      try {
        LoggerProxy.logger.info('Meeting:index#reconnect --> Validating reconnect ability.');
        this.reconnectionManager.validate();
      }
      catch (error) {
        // Unable to reconnect this call
        if (error instanceof ReconnectInProgress) {
          LoggerProxy.logger.info('Meeting:index#reconnect --> Unable to reconnect, reconnection in progress.');
        }
        else {
          LoggerProxy.logger.log('Meeting:index#reconnect --> Unable to reconnect.', error);
        }
  
        return Promise.resolve();
      }
  
      Trigger.trigger(
        this,
        {
          file: 'meeting/index',
          function: 'reconnect'
        },
        EVENT_TRIGGERS.MEETING_RECONNECTION_STARTING
      );
  
  
      return this.reconnectionManager
        .reconnect(options)
        .then(() => {
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'reconnect'
            },
            EVENT_TRIGGERS.MEETING_RECONNECTION_SUCCESS
          );
          LoggerProxy.logger.log('Meeting:index#reconnect --> Meeting reconnect success');
        })
        .catch((error) => {
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'reconnect'
            },
            EVENT_TRIGGERS.MEETING_RECONNECTION_FAILURE,
            {
              error: new ReconnectionError('Reconnection failure event', error)
            }
          );
  
          LoggerProxy.logger.error('Meeting:index#reconnect --> Meeting reconnect failed', error);
  
          Metrics.sendOperationalMetric(
            METRICS_OPERATIONAL_MEASURES.MEETING_RECONNECT_FAILURE,
            {
              correlation_id: this.correlationId,
              locus_id: this.locusUrl.split('/').pop(),
              reason: error.message,
              stack: error.stack
            }
          );
  
          return Promise.reject(new ReconnectionError('Reconnection failure event', error));
        })
        .finally(() => {
          this.reconnectionManager.reset();
        });
    }
  
    /**
     * Specify joining via audio (option: pstn), video, screenshare
     * @param {JoinOptions} options A configurable options object for joining a meeting
     * @returns {Promise} the join response
     * @public
     * @memberof Meeting
     * Scenario A: Joining own claimed personal meeting room
     * Scenario B: Joining other's claimed personal meeting room, do pass pin (if desired to join as host, or nullify), do pass moderator
     * Scenario C: Joining an unclaimed personal meeting room, -do not- pass pin or moderator on first try, -do- pass pin and moderator
     *             if joining as host on second loop, pass pin and pass moderator if joining as guest on second loop
     * Scenario D: Joining any other way (sip, pstn, conversationUrl, link just need to specify resourceId)
     */
    join(options = {}) {
      // If a join request is being processed, refer to the deferred promise.
      if (this.deferJoin) {
        return this.deferJoin;
      }
  
      // Scope-up the resolve/reject methods for handling within join().
      let joinFailed;
      let joinSuccess;
  
      // Create a deferred promise for a consistent resolve value from utils.
      // This also prevents redundant API calls.
      this.deferJoin = new Promise((resolve, reject) => {
        joinFailed = reject;
        joinSuccess = resolve;
      });
  
      if (options.rejoin) {
        LoggerProxy.logger.log(`Meeting:index#join --> Generating a new correlation id for meeting ${this.id}`);
        LoggerProxy.logger.log(`Meeting:index#join --> Previous correlation id ${this.correlationId}`);
        this.setCorrelationId(uuid.v4());
        LoggerProxy.logger.log(`Meeting:index#join --> New correlation id ${this.correlationId}`);
        this.meetingFiniteStateMachine.reset();
      }
  
      Metrics.postEvent({
        event: eventType.CALL_INITIATED,
        meeting: this,
        data: {trigger: trigger.USER_INTERACTION}
      });
  
      Metrics.sendOperationalMetric(
        METRICS_OPERATIONAL_MEASURES.JOIN_ATTEMPT,
        {
          correlation_id: this.correlationId
        }
      );
  
      LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
  
      if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
        this.meetingFiniteStateMachine.reset();
      }
      if (this.meetingFiniteStateMachine.state !== MEETING_STATE_MACHINE.STATES.RINGING) {
        this.meetingFiniteStateMachine.ring(_JOIN_);
      }
  
      // TODO: does this really need to be here?
      if (options.resourceId && this.destination && options.resourceId === this.destination) {
        this.wirelessShare = true;
      }
  
      if (options.meetingQuality) {
        if (typeof options.meetingQuality === 'string') {
          if (!QUALITY_LEVELS[options.meetingQuality]) {
            const errorMessage = `Meeting:index#join --> ${options.meetingQuality} not defined`;
            const error = new Error(errorMessage);
  
            LoggerProxy.logger.error(errorMessage);
  
            joinFailed(error);
            this.deferJoin = undefined;
  
            return Promise.reject(error);
          }
  
          this.mediaProperties.setLocalQualityLevel(options.meetingQuality);
          this.mediaProperties.setRemoteQualityLevel(options.meetingQuality);
        }
  
        if (typeof options.meetingQuality === 'object') {
          if (
            !QUALITY_LEVELS[options.meetingQuality.local] &&
            !QUALITY_LEVELS[options.meetingQuality.remote]
          ) {
            const errorMessage = `Meeting:index#join --> ${
              options.meetingQuality.local || options.meetingQuality.remote
            } not defined`;
  
            LoggerProxy.logger.error(errorMessage);
  
            const error = new Error(errorMessage);
  
            joinFailed(error);
            this.deferJoin = undefined;
  
            return Promise.reject(new Error(errorMessage));
          }
  
          if (options.meetingQuality.local) {
            this.mediaProperties.setLocalQualityLevel(options.meetingQuality.local);
          }
          if (options.meetingQuality.remote) {
            this.mediaProperties.setRemoteQualityLevel(options.meetingQuality.remote);
          }
        }
      }
  
      return MeetingUtil.joinMeetingOptions(this, options).then((join) => {
        this.meetingFiniteStateMachine.join();
        LoggerProxy.logger.log('Meeting:index#join --> Success');
  
        return join;
      })
        .then((join) => {
          joinSuccess(join);
          this.deferJoin = undefined;
  
          return join;
        })
        .catch((error) => {
          this.meetingFiniteStateMachine.fail(error);
          LoggerProxy.logger.error('Meeting:index#join --> Failed', error);
  
          // TODO:  change this to error codes and pre defined dictionary
          Metrics.sendOperationalMetric(
            METRICS_OPERATIONAL_MEASURES.JOIN_FAILURE,
            {
              correlation_id: this.correlationId,
              reason: error.error?.message,
              stack: error.stack
            }
          );
  
          // Upload logs on join Failure
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'join'
            },
            EVENTS.REQUEST_UPLOAD_LOGS,
            this
          );
  
          joinFailed(error);
          this.deferJoin = undefined;
  
          return Promise.reject(error);
        });
    }
  
    /**
     * Moves the call to the specified resourceId
     * @param {String} resourceId
     * @returns {Promise} once the move has been completed
     * @public
     * @memberof Meeting
     */
    moveTo(resourceId) {
      if (!resourceId) {
        throw new ParameterError('Cannot move call without a resourceId.');
      }
  
      Metrics.mediaCapabilities({
        meeting: this,
        data: {
          mediaCapabilities: {
            rx: {
              audio: false,
              share: false,
              share_audio: false,
              video: false,
              whiteboard: false
            },
            tx: {
              audio: false,
              share: false,
              share_audio: false,
              video: false,
              whiteboard: false
            }
          }
        }
      });
      Metrics.moveMedia({meeting: this});
  
      return MeetingUtil.joinMeetingOptions(this, {resourceId, moveToResource: true}).then(() => {
        this.meetingFiniteStateMachine.join();
  
        return this.updateMedia({
          mediaSettings: {
            sendVideo: false, receiveVideo: false, sendAudio: false, receiveAudio: false, sendShare: false, receiveShare: false
          }
        }).then(() => Promise.resolve(this));
      }).catch((error) => {
        this.meetingFiniteStateMachine.fail(error);
  
        return Promise.reject(error);
      });
    }
  
    /**
     * Moves the call from the specified resourceId, back to computer
     * @param {String} resourceId
     * @returns {Promise} once the move has been completed
     * @public
     * @memberof Meeting
     */
    moveFrom(resourceId) {
      if (!resourceId) {
        throw new ParameterError('Cannot move call without a resourceId.');
      }
      const oldCorrelationId = this.correlationId;
  
      this.id = uuid.v4();
  
      this.webex.meetings.meetingCollection.set(this);
  
      Metrics.mediaCapabilities({
        meeting: this,
        data: {
          mediaCapabilities: {
            rx: {
              audio: true,
              share: true,
              share_audio: true,
              video: true,
              whiteboard: true
            },
            tx: {
              audio: true,
              share: true,
              share_audio: true,
              video: true,
              whiteboard: true
            }
          }
        }
      });
      Metrics.moveMedia({meeting: this});
  
      return MeetingUtil.joinMeetingOptions(this).then((join) => this.getMediaStreams({sendAudio: true, sendVideo: true, sendShare: false})
        .then(([localStream, localShare]) =>
          this.updateMedia({
            mediaSettings: {
              sendAudio: true, receiveAudio: true, sendVideo: true, receiveVideo: true, sendShare: false, receiveShare: false
            },
            localStream,
            localShare
          })
            .then(() => Promise.resolve(join)))
        .then(() => MeetingUtil.leaveMeeting(this, {resourceId, correlationId: oldCorrelationId, moveMeeting: true})))
        .then(() => {
          this.webex.meetings.meetingCollection.delete(this.id);
  
          return Promise.resolve();
        });
    }
  
    /**
     * Get local media streams based on options passed
     * @param {MediaDirection} mediaDirection A configurable options object for joining a meeting
     * @param {AudioVideo} [audioVideo] audio/video object to set audioinput and videoinput devices, see #Media.getUserMedia
     * @param {SharePreferences} [sharePreferences] audio/video object to set audioinput and videoinput devices, see #Media.getUserMedia
     * @returns {Promise} see #Media.getUserMedia
     * @public
     * @todo should be static, or moved so can be called outside of a meeting
     * @memberof Meeting
     */
    getMediaStreams = (
      mediaDirection,
      // This return an OBJECT {video: {height, widght}}
      audioVideo = VIDEO_RESOLUTIONS[this.mediaProperties.localQualityLevel],
      sharePreferences
    ) => {
      if (
        mediaDirection &&
        (
          mediaDirection.sendAudio ||
          mediaDirection.sendVideo ||
          mediaDirection.sendShare
        )
      ) {
        if (mediaDirection &&
          (
            mediaDirection.sendAudio &&
            mediaDirection.sendVideo &&
            mediaDirection.sendShare
          ) &&
          bowser.safari
        ) {
          LoggerProxy.logger.warn('Meeting:index#getMediaStreams --> Setting `sendShare` to FALSE, due to complications with Safari');
  
          mediaDirection.sendShare = false;
  
          LoggerProxy.logger.warn('Meeting:index#getMediaStreams --> Enabling `sendShare` along with `sendAudio` & `sendVideo`, on Safari, causes a failure while setting up a screen share at the same time as the camera+mic stream');
          LoggerProxy.logger.warn('Meeting:index#getMediaStreams --> Please use `meeting.shareScreen()` to manually start the screen share after successfully joining the meeting');
        }
  
        // extract deviceId if exists otherwise default to null.
        const {deviceId: preferredVideoDevice} = (audioVideo && audioVideo.video || {deviceId: null});
        const lastVideoDeviceId = this.mediaProperties.getVideoDeviceId();
  
        if (preferredVideoDevice) {
          // Store new preferred video input device
          this.mediaProperties.setVideoDeviceId(preferredVideoDevice);
        }
        else if (lastVideoDeviceId) {
          // no new video preference specified so use last stored value,
          // works with empty object {} or media constraint.
          // eslint-disable-next-line no-param-reassign
          audioVideo = {
            ...audioVideo,
            video: {
              ...audioVideo.video,
              deviceId: lastVideoDeviceId
            }
          };
        }
  
        return Media.getSupportedDevice({
          sendAudio: mediaDirection.sendAudio,
          sendVideo: mediaDirection.sendVideo
        })
          .catch((error) => Promise.reject(
            new MediaError('Given constraints do not match permission set for either camera or microphone', error)
          ))
          .then((devicePermissions) =>
            Media.getUserMedia(
              {
                ...mediaDirection,
                sendAudio: (mediaDirection.sendAudio === devicePermissions.sendAudio),
                sendVideo: (mediaDirection.sendVideo === devicePermissions.sendVideo),
                isSharing: this.isSharing
              },
              audioVideo,
              sharePreferences,
              this.config
            )
              .catch((error) => {
                // Whenever there is a failure when trying to access a user's device
                // report it as an operational metric
                // This gives visibility into common errors and can help
                // with further troubleshooting
                const metricName = METRICS_OPERATIONAL_MEASURES.GET_USER_MEDIA_FAILURE;
                const data = {
                  correlation_id: this.correlationId,
                  locus_id: this.locusUrl.split('/').pop(),
                  reason: error.message,
                  stack: error.stack
                };
                const metadata = {
                  type: error.name
                };
  
                Metrics.sendOperationalMetric(metricName, data, metadata);
                throw new MediaError('Unable to retrieve media streams', error);
              }));
      }
  
      return Promise.reject(
        new MediaError('At least one of the mediaDirection value should be true')
      );
    };
  
  
    /**
     * Checks if the machine has at least one audio or video device
     * @param {Object} options
     * @param {Boolean} options.sendAudio
     * @param {Boolean} options.sendVideo
     * @returns {Object}
     * @memberof Meetings
     */
    getSupportedDevices = ({sendAudio = true, sendVideo = true}) => Media.getSupportedDevice({sendAudio, sendVideo});
  
    /**
     * Get the devices from the Media module
     * @returns {Promise} resolves to an array of DeviceInfo
     * @memberof Meetings
     */
    getDevices = () => Media.getDevices();
  
    /**
     * Specify joining via audio (option: pstn), video, screenshare
     * @param {Object} options A configurable options object for joining a meeting
     * @param {Object} options.resourceId pass the deviceId
     * @param {MediaDirection} options.mediaSettings pass media options
     * @param {MediaStream} options.localStream
     * @param {MediaStream} options.localShare
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    addMedia(options = {}) {
      const LOG_HEADER = 'Meeting:index#addMedia -->';
  
      if (MeetingUtil.isGuestUnjoined(this.locusInfo) && !this.wirelessShare) {
        return Promise.reject(new MediaError(`To add media, the ${this.guest ? 'guest' : ''} user must be ${this.guest ? 'admitted. Wait to be admitted' : 'joined to the meeting'} to call addMedia`));
      }
      const {localStream, localShare, mediaSettings} = options;
  
      LoggerProxy.logger.info(`${LOG_HEADER} Adding Media.`);
  
      Metrics.postEvent({
        event: eventType.MEDIA_CAPABILITIES,
        meeting: this,
        data: {
          mediaCapabilities: {
            rx: {
              audio: false,
              share: false,
              share_audio: false,
              video: false,
              whiteboard: false
            },
            tx: {
              audio: false,
              share: false,
              share_audio: false,
              video: false,
              whiteboard: false
            }
          }
        }
      });
  
      return MeetingUtil.validateOptions(options)
        .then(() => {
          if (!this.mediaProperties.peerConnection) {
            this.mediaProperties.setMediaPeerConnection(MediaUtil.createPeerConnection());
          }
          this.setReconnectListener();
          PeerConnectionManager.setPeerConnectionEvents(this);
  
          return this.preMedia(localStream, localShare, mediaSettings);
        })
        .then(() => Media.attachMedia(this.mediaProperties, {
          meetingId: this.id,
          remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
          enableRtx: this.config.enableRtx
        })
          .then((peerConnection) => {
            LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection Received from attachMedia `);
  
            this.setRemoteStream(peerConnection);
            MeetingUtil.startInternalStats(this);
  
            if (this.config.metrics.autoSendMQA) {
              this.startMediaQualityMetrics();
            }
  
            if (this.config.stats.enableStatsAnalyzer) {
              this.statsAnalyzer = new StatsAnalyzer(this.config.stats);
              this.statsAnalyzer.on(EVENT_TRIGGERS.MEDIA_QUALITY, (options) => {
                // TODO:  might have to send the same event to the developer
                // Add ip address info if geo hint is present
                options.data.intervalMetadata.peerReflexiveIP = this.webex.meetings.geoHintInfo?.clientAddress || options.data.intervalMetadata.peerReflexiveIP || MQA_STATS.DEFAULT_IP;
                Metrics.postEvent({event: eventType.MEDIA_QUALITY, meeting: this, data: {intervalData: options.data, networkType: options.networkType}});
              });
              this.statsAnalyzer.on(EVENT_TRIGGERS.MEETING_HIGH_PACKETLOSS, (res) => {
                if (res.mediaType === VIDEO) {
                  if (this.config.stats.autoDowngradeEnabled && this.canUpdateMedia()) {
                    LoggerProxy.logger.log(`${LOG_HEADER} High Packet Loss Detected, Adjusting Local Video Quality`);
                    if (this.mediaProperties.localQualityLevel === QUALITY_LEVELS.HIGH) {
                      this.setLocalVideoQuality(QUALITY_LEVELS.MEDIUM);
                    }
                    else if (this.mediaProperties.localQualityLevel === QUALITY_LEVELS.MEDIUM) {
                      this.setLocalVideoQuality(QUALITY_LEVELS.LOW);
                    }
                  }
                  Trigger.trigger(
                    this,
                    {
                      file: 'meeting/index',
                      function: 'addMedia'
                    },
                    EVENT_TRIGGERS.MEETING_HIGH_PACKETLOSS,
                    {
                      mediaType: res.mediaType,
                      packetLossRatio: res.packetLossRatio
                    }
                  );
                }
              });
            }
          })
          .catch((error) => {
            LoggerProxy.logger.error(`${LOG_HEADER} Error adding media , setting up peerconnection, `, error);
  
            Metrics.sendOperationalMetric(
              METRICS_OPERATIONAL_MEASURES.ADD_MEDIA_FAILURE,
              {
                correlation_id: this.correlationId,
                locus_id: this.locusUrl.split('/').pop(),
                reason: error.message,
                stack: error.stack
              }
            );
  
            throw error;
          })
          .then(() => new Promise((resolve, reject) => {
            let timerCount = 0;
  
            // eslint-disable-next-line func-names
            // eslint-disable-next-line prefer-arrow-callback
            if (this.type === _CALL_) {
              resolve();
            }
            const joiningTimer = setInterval(() => {
              timerCount += 1;
              if (this.meetingState === FULL_STATE.ACTIVE) {
                clearInterval(joiningTimer);
                resolve();
              }
  
              if (timerCount === 4) {
                clearInterval(joiningTimer);
                reject(new Error('Meeting is still not active '));
              }
            }, 1000);
          }))
          .then(() => logRequest(this.roap
            .sendRoapMediaRequest({
              sdp: this.mediaProperties.peerConnection.sdp,
              roapSeq: this.roapSeq,
              meeting: this // or can pass meeting ID
            }), {
            header: `${LOG_HEADER} Send Roap Media Request.`,
            success: `${LOG_HEADER} Successfully send roap media request`,
            failure: `${LOG_HEADER} Error joining the call on send roap media request, `
          }))
          .then(() => {
            const {peerConnection} = this.mediaProperties;
  
            return new Promise((resolve, reject) => {
              if (peerConnection.connectionState === CONNECTION_STATE.CONNECTED) {
                LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED`);
  
                resolve(peerConnection);
  
                return;
              }
              // Check if Peer Connection is STABLE (connected)
              const stabilityTimeout = setTimeout(() => {
                if (peerConnection.connectionState !== CONNECTION_STATE.CONNECTED) {
                  reject(new Error(`Unable to add media. PeerConnection NOT CONNECTED after ${PC_BAIL_TIMEOUT / 1000} seconds.`));
                }
                else {
                  LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED`);
                  resolve(peerConnection);
                }
              }, PC_BAIL_TIMEOUT);
  
              this.once(EVENT_TRIGGERS.MEDIA_READY, () => {
                LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED, clearing stability timer.`);
                clearTimeout(stabilityTimeout);
                resolve(peerConnection);
              });
            });
          })
          .then(() => {
            if (mediaSettings && mediaSettings.sendShare && localShare) {
              if (this.state === MEETING_STATE.STATES.JOINED) {
                return this.share();
              }
  
              // When the self state changes to JOINED then request the floor
              this.floorGrantPending = true;
            }
  
            return Promise.resolve();
          }))
        .catch((error) => {
          LoggerProxy.logger.error(`${LOG_HEADER} Error adding media failed to initiate PC and send request, `, error);
  
          Metrics.sendOperationalMetric(
            METRICS_OPERATIONAL_MEASURES.ADD_MEDIA_FAILURE,
            {
              correlation_id: this.correlationId,
              locus_id: this.locusUrl.split('/').pop(),
              reason: error.message,
              stack: error.stack
            }
          );
  
          // Upload logs on error while adding media
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'addMedia'
            },
            EVENTS.REQUEST_UPLOAD_LOGS,
            this
          );
  
          throw error;
        });
    }
  
    /**
     * Informs if the peer connection is in a state that can be updated with updateMedia (audio/video/share)
     * @returns {Boolean}
     */
    canUpdateMedia() {
      return this.mediaProperties.peerConnection.signalingState === SDP.STABLE;
    }
  
    /**
     * A confluence of updateAudio, updateVideo, and updateShare
     * this function re-establishes all of the media streams with new options
     * @param {Object} options
     * @param {MediaStream} options.localStream
     * @param {MediaStream} options.localShare
     * @param {MediaDirection} options.mediaSettings
     * @returns {Promise}
     * @todo fix setRemoteStream for updateMedia
     * @public
     * @memberof Meeting
     */
    updateMedia(options = {}) {
      const LOG_HEADER = 'Meeting:index#updateMedia -->';
  
      if (!this.canUpdateMedia()) {
        return Promise.reject(new MediaError('The peer connection is currently negotiation an offer. Please wait for a few seconds and try again.'));
      }
      const {localStream, localShare, mediaSettings} = options;
  
      const previousSendShareStatus = this.mediaProperties.mediaDirection.sendShare;
  
      return MeetingUtil.validateOptions(options)
        .then(() => this.preMedia(localStream, localShare, mediaSettings))
        .then(() => Media.updateMedia(this.mediaProperties, {
          meetingId: this.id,
          remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
          enableRtx: this.config.enableRtx
        })
          .then((peerConnection) => {
            LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection received from updateMedia, ${peerConnection}`);
            this.setRemoteStream(peerConnection);
            if (mediaSettings.receiveShare || localShare) {
              PeerConnectionManager.setContentSlides(peerConnection);
            }
          })
          .catch((error) => {
            LoggerProxy.logger.error(`${LOG_HEADER} Error updatedMedia, `, error);
  
            Metrics.sendOperationalMetric(
              METRICS_OPERATIONAL_MEASURES.UPDATE_MEDIA_FAILURE,
              {
                correlation_id: this.correlationId,
                locus_id: this.locusUrl.split('/').pop(),
                reason: error.message,
                stack: error.stack
              }
            );
  
            throw error;
          })
          .then(() =>
            logRequest(this.roap
              .sendRoapMediaRequest({
                sdp: this.mediaProperties.peerConnection.sdp,
                roapSeq: this.roapSeq,
                meeting: this // or can pass meeting ID
              }),
            {
              header: `${LOG_HEADER} sendRoapMediaRequest being sent`,
              success: `${LOG_HEADER} sendRoadMediaRequest successful`,
              failure: `${LOG_HEADER} Error updateMedia on send roap media request, `
            }))
          .then(() => this.checkForStopShare(mediaSettings.sendShare, previousSendShareStatus))
          .then((startShare) => {
            // This is a special case if we do an /floor grant followed by /media
            // we actually get a OFFER from the server and a GLAR condition happens
            if (startShare) {
              // We are assuming that the clients are connected when doing an update
              return this.share();
            }
  
            return Promise.resolve();
          }));
    }
  
    /**
     * Update the main audio streams with new parameters
     * @param {Object} options
     * @param {boolean} options.sendAudio
     * @param {boolean} options.receiveAudio
     * @param {MediaStream} options.stream
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    updateAudio(options) {
      if (!this.canUpdateMedia()) {
        return Promise.reject(new MediaError('The peer connection is currently negotiation an offer. Please wait for a few seconds and try again.'));
      }
      const {sendAudio, receiveAudio, stream} = options;
      const {audioTransceiver} = this.mediaProperties.peerConnection;
      const track = MeetingUtil.getTrack(stream).audioTrack;
  
      if (typeof sendAudio !== 'boolean' || typeof receiveAudio !== 'boolean') {
        return Promise.reject(new ParameterError('Pass sendVideo and receiveVideo parameter'));
      }
  
      return MeetingUtil.validateOptions({sendAudio, localStream: stream})
        .then(() => {
          let previousMediaDirection = {};
  
          if (this.mediaProperties.mediaDirection) {
            previousMediaDirection = {
              sendTrack: this.mediaProperties.mediaDirection.sendAudio,
              receiveTrack: this.mediaProperties.mediaDirection.receiveAudio
            };
          }
          else {
            this.mediaProperties.mediaDirection = {};
          }
  
          return MeetingUtil.updateTransceiver(
            {
              type: 'audio',
              sendTrack: options.sendAudio,
              receiveTrack: options.receiveAudio,
              track,
              transceiver: audioTransceiver,
              peerConnection: this.mediaProperties.peerConnection,
              previousMediaDirection
            },
            {
              mediaProperties: this.mediaProperties,
              roapSeq: this.roapSeq,
              meeting: this,
              id: this.id
            }
          );
        })
        .then(() => {
          this.setLocalTracks(stream);
          this.mediaProperties.mediaDirection.sendAudio = sendAudio;
          this.mediaProperties.mediaDirection.receiveAudio = receiveAudio;
  
          // audio state could be undefined if you have not sent audio before
          this.audio = this.audio || AudioStateMachine.create(this.mediaProperties.mediaDirection, this);
        });
    }
  
    /**
     * Update the main video streams with new parameters
     * @param {Object} options
     * @param {boolean} options.sendVideo
     * @param {boolean} options.receiveVideo
     * @param {MediaStream} options.stream
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    updateVideo(options) {
      if (!this.canUpdateMedia()) {
        return Promise.reject(new MediaError('The peer connection is currently negotiation an offer. Please wait for a few seconds and try again.'));
      }
      const {sendVideo, receiveVideo, stream} = options;
      const {videoTransceiver} = this.mediaProperties.peerConnection;
      const track = MeetingUtil.getTrack(stream).videoTrack;
  
      if (typeof sendVideo !== 'boolean' || typeof receiveVideo !== 'boolean') {
        return Promise.reject(new ParameterError('Pass sendVideo and receiveVideo parameter'));
      }
  
      return MeetingUtil.validateOptions({sendVideo, localStream: stream})
        .then(() => MeetingUtil.updateTransceiver({
          type: 'video',
          sendTrack: options.sendVideo,
          receiveTrack: options.receiveVideo,
          track,
          transceiver: videoTransceiver,
          peerConnection: this.mediaProperties.peerConnection,
          previousMediaDirection: {
            sendTrack: this.mediaProperties.mediaDirection.sendVideo,
            receiveTrack: this.mediaProperties.mediaDirection.receiveVideo
          }
        },
        {
          mediaProperties: this.mediaProperties,
          roapSeq: this.roapSeq,
          meeting: this,
          id: this.id
        }))
        .then(() => {
          this.setLocalTracks(stream);
          this.mediaProperties.mediaDirection.sendVideo = sendVideo;
          this.mediaProperties.mediaDirection.receiveVideo = receiveVideo;
  
          // video state could be undefined if you have not sent video before
          this.video = this.video || VideoStateMachine.create(this.mediaProperties.mediaDirection, this);
        });
    }
  
    /**
     * Internal function when stopping a share stream, cleanup
     * @param {boolean} sendShare
     * @param {boolean} previousShareStatus
     * @returns {Promise}
     * @private
     * @memberof Meeting
     */
    checkForStopShare(sendShare, previousShareStatus) {
      if (!this.isSharing && sendShare &&
        !previousShareStatus) {
        // When user starts sharing
        // return this.share();
        return Promise.resolve(true);
      }
      if (this.isSharing && !sendShare &&
        previousShareStatus) {
        // When user stops sharing
        return this.stopFloorRequest()
          .then(() => Promise.resolve(false));
      }
  
      return Promise.resolve();
    }
  
    /**
     * Update the share streams, can be used to start sharing
     * @param {Object} options
     * @param {boolean} options.sendShare
     * @param {boolean} options.receiveShare
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    updateShare(options) {
      if (!this.canUpdateMedia()) {
        return Promise.reject(new MediaError('The peer connection is currently negotiation an offer. Please wait for a few seconds and try again.'));
      }
      const {sendShare, receiveShare, stream} = options;
      const {shareTransceiver} = this.mediaProperties.peerConnection;
      const track = MeetingUtil.getTrack(stream).videoTrack;
  
      if (typeof sendShare !== 'boolean' || typeof receiveShare !== 'boolean') {
        return Promise.reject(new ParameterError('Pass sendShare and receiveShare parameter'));
      }
      const previousSendShareStatus = this.mediaProperties.mediaDirection.sendShare;
  
      return MeetingUtil.validateOptions({sendShare, localShare: stream})
        .then(() => this.checkForStopShare(sendShare, previousSendShareStatus))
        .then((startShare) => MeetingUtil.updateTransceiver({
          type: 'video',
          sendTrack: sendShare,
          receiveTrack: receiveShare,
          track,
          transceiver: shareTransceiver,
          peerConnection: this.mediaProperties.peerConnection,
          previousMediaDirection: {
            sendTrack: this.mediaProperties.mediaDirection.sendShare,
            receiveTrack: this.mediaProperties.mediaDirection.receiveShare
          }
        },
        {
          mediaProperties: this.mediaProperties,
          roapSeq: this.roapSeq,
          meeting: this,
          id: this.id
        })
          .then(() => {
            if (startShare) {
              return this.share();
            }
  
            return Promise.resolve();
          }))
        .then(() => {
          this.setLocalShareTrack(stream);
          this.mediaProperties.mediaDirection.sendShare = sendShare;
          this.mediaProperties.mediaDirection.receiveShare = receiveShare;
        });
    }
  
    /**
     * Do all the attach media pre set up before executing the actual attach
     * @param {MediaStream} localStream
     * @param {MediaStream} localShare
     * @param {MediaDirection} mediaSettings
     * @returns {undefined}
     * @private
     * @memberof Meeting
     */
    preMedia(localStream, localShare, mediaSettings) {
      // eslint-disable-next-line no-warning-comments
      // TODO wire into default config. There's currently an issue with the stateless plugin or how we register
      this.mediaProperties.setMediaDirection(Object.assign(this.config.mediaSettings, mediaSettings));
      // add a setup a function move the create and setup media in future
      // TODO: delete old audio and video if stale
      this.audio = this.audio || AudioStateMachine.create(this.mediaProperties.mediaDirection, this);
      this.video = this.video || VideoStateMachine.create(this.mediaProperties.mediaDirection, this);
      // Validation is already done in addMedia so no need to check if the lenght is greater then 0
      this.setLocalTracks(localStream);
      this.setLocalShareTrack(localShare);
    }
  
    /**
     * Acknowledge the meeting, outgoing or incoming
     * @param {String} type
     * @returns {Promise} resolve {message, ringing, response}
     * @public
     * @memberof Meeting
     */
    acknowledge(type) {
      if (!type) {
        return Promise.reject(new ParameterError('Type must be set to acknowledge the meeting.'));
      }
      if (type === _INCOMING_) {
        return this.meetingRequest
          .acknowledgeMeeting({
            locusUrl: this.locusUrl,
            deviceUrl: this.deviceUrl,
            correlationId: this.correlationId
          })
          .then((response) => Promise.resolve(response))
          .then((response) => {
            this.meetingFiniteStateMachine.ring(type);
            Metrics.postEvent({event: eventType.ALERT_DISPLAYED, meeting: this});
  
            return Promise.resolve({
              response
            });
          });
      }
  
      // TODO: outside of 1:1 incoming, and all outgoing calls
      return Promise.resolve({
        message: 'noop'
      });
    }
  
    /**
     * Decline this meeting
     * @param {String} reason
     * @returns {undefined}
     * @public
     * @memberof Meeting
     */
    decline(reason = _BUSY_) {
      return MeetingUtil.declineMeeting(this, reason).then((decline) => {
        this.meetingFiniteStateMachine.decline();
  
        return Promise.resolve(decline);
      }).catch((error) => {
        this.meetingFiniteStateMachine.fail(error);
  
        return Promise.reject(error);
      });
    }
  
    /**
     * Leave the current meeting
     * @param {Object} options leave options
     * @param {String} options.resourceId the device with which to leave from, empty if just the computer
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    leave(options = {}) {
      Metrics.postEvent({event: eventType.LEAVE, meeting: this, data: {trigger: trigger.USER_INTERACTION, canProceed: false}});
      const leaveReason = options.reason || MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST;
  
      LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
  
      return MeetingUtil.leaveMeeting(this, options)
        .then((leave) => {
          this.meetingFiniteStateMachine.leave();
  
          // upload logs on leave irrespective of meeting delete
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'leave'
            },
            EVENTS.REQUEST_UPLOAD_LOGS,
            this
          );
  
          // TODO: more testing before we remove this code, we are not sure the scenarios for destroy here
          if (this.wirelessShare || this.guest) {
            // If screen sharing clean the meeting object
            Trigger.trigger(
              this,
              {
                file: 'meeting/index',
                function: 'leave'
              },
              EVENTS.DESTROY_MEETING,
              {
                reason: options.reason,
                meetingId: this.id
              }
            );
          }
          LoggerProxy.logger.log('Meeting:index#leave --> LEAVE REASON ', leaveReason);
  
          return leave;
        }).catch((error) => {
          this.meetingFiniteStateMachine.fail(error);
          LoggerProxy.logger.error('Meeting:index#leave --> Failed to leave ', error);
          // upload logs on leave irrespective of meeting delete
          Trigger.trigger(
            this,
            {
              file: 'meeting/index',
              function: 'leave'
            },
            EVENTS.REQUEST_UPLOAD_LOGS,
            this
          );
          Metrics.sendOperationalMetric(
            METRICS_OPERATIONAL_MEASURES.MEETING_LEAVE_FAILURE,
            {
              correlation_id: this.correlationId,
              locus_id: this.locusUrl.split('/').pop(),
              reason: error.message,
              stack: error.stack
            }
          );
  
          return Promise.reject(error);
        });
    }
  
    /**
     * Start sharing content with server
     * @returns {Promise} see #meetingRequest.changeMeetingFloor
     * @private
     * @memberof Meeting
     */
    share() {
      const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
  
      if (content && !this.isSharing) {
        Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
  
        return this.meetingRequest.changeMeetingFloor({
          disposition: FLOOR_ACTION.GRANTED,
          personUrl: this.locusInfo.self.url,
          deviceUrl: this.deviceUrl,
          uri: content.url,
          resourceUrl: this.resourceUrl
        })
          .then(() => {
            this.isSharing = true;
            Trigger.trigger(
              this,
              {
                file: 'meeting/index',
                function: 'share'
              },
              EVENT_TRIGGERS.MEETING_STARTED_SHARING_LOCAL,
            );
            Metrics.postEvent({event: eventType.LOCAL_SHARE_FLOOR_GRANTED, meeting: this});
  
            return Promise.resolve();
          })
          .catch((error) => {
            LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
  
            Metrics.sendOperationalMetric(
              METRICS_OPERATIONAL_MEASURES.MEETING_SHARE_FAILURE,
              {
                correlation_id: this.correlationId,
                locus_id: this.locusUrl.split('/').pop(),
                reason: error.message,
                stack: error.stack
              }
            );
  
            return Promise.reject(error);
          });
      }
  
      return Promise.reject(new ParameterError('Cannot share without content.'));
    }
  
    /**
     * Stops the screen share
     * @returns {Promise} see #updateShare
     * @public
     * @memberof Meeting
     */
    stopShare() {
      return this.updateShare({
        sendShare: false,
        receiveShare: this.mediaProperties.mediaDirection.receiveShare
      });
    }
  
    /**
     * sends stops floor request
     * @returns {Promise} see #meetingRequest.changeMeetingFloor
     * @private
     * @memberof Meeting
     */
    stopFloorRequest() {
      const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
  
      if (content && this.isSharing) {
        Metrics.postEvent({event: eventType.SHARE_STOPPED, meeting: this});
        Media.stopTracks(this.mediaProperties.shareTrack);
  
        return this.meetingRequest.changeMeetingFloor({
          disposition: FLOOR_ACTION.RELEASED,
          personUrl: this.locusInfo.self.url,
          deviceUrl: this.deviceUrl,
          uri: content.url,
          resourceUrl: this.resourceUrl
        })
          .catch((error) => {
            LoggerProxy.logger.error('Meeting:index#stopFloorRequest --> Error ', error);
  
            Metrics.sendOperationalMetric(
              METRICS_OPERATIONAL_MEASURES.STOP_FLOOR_REQUEST_FAILURE,
              {
                correlation_id: this.correlationId,
                locus_id: this.locusUrl.split('/').pop(),
                reason: error.message,
                stack: error.stack
              }
            );
  
            return Promise.reject(error);
          })
          .finally(() => {
            this.isSharing = false;
            Trigger.trigger(
              this,
              {
                file: 'meeting/index',
                function: 'stopFloorRequest'
              },
              EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
              {
                reason: SHARE_STOPPED_REASON.SELF_STOPPED
              }
            );
          });
      }
  
      return Promise.reject(new ParameterError('Cannot stop share without content'));
    }
  
    /**
     * Intiate a recording of this meeting
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    startRecording() {
      return MeetingUtil.startRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
    }
  
    /**
     * End the recording of this meeting
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    stopRecording() {
      return MeetingUtil.stopRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
    }
  
    /**
     * Pauses the recording of this meeting
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    pauseRecording() {
      return MeetingUtil.pauseRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
    }
  
    /**
     * Resumes the recording of this meeting
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    resumeRecording() {
      return MeetingUtil.resumeRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
    }
  
    /**
     * Locks the current meeting if possible
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    lockMeeting() {
      return MeetingUtil.lockMeeting(this.inMeetingActions, this.meetingRequest, this.locusUrl);
    }
  
    /**
     * Unlocks the current meeting if possible
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    unlockMeeting() {
      return MeetingUtil.unlockMeeting(this.inMeetingActions, this.meetingRequest, this.locusUrl);
    }
  
    /**
     * Sends DTMF tones to the current meeting
     * @param {String} tones a string of one or more DTMF tones to send
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    sendDTMF(tones) {
      const reject = (message) => {
        LoggerProxy.logger.error(message);
  
        return Promise.reject(new Error(message));
      };
  
      if (this.locusInfo && this.locusInfo.self) {
        if (this.locusInfo.self.enableDTMF) {
          return this.meetingRequest
            .sendDTMF({
              locusUrl: this.locusInfo.self.url,
              deviceUrl: this.deviceUrl,
              tones
            });
        }
  
        return reject('Meeting:index#sendDTMF --> cannot send DTMF, meeting does not have DTMF enabled');
      }
  
      return reject('Meeting:index#sendDTMF --> cannot send DTMF, meeting does not have a connection to the "locus" call control service. Have you joined?');
    }
  
    /**
     * Sends request to change layout type for the current meeting for the specific participant/device only
     * @param {String} layoutType a layout type that should be available in meeting constants LAYOUT_TYPES
     * @returns {Promise}
     * @public
     * @memberof Meeting
     */
    changeVideoLayout(layoutType) {
      const reject = (message) => {
        LoggerProxy.logger.error(message);
  
        return Promise.reject(new Error(message));
      };
  
      const {mediaDirection, remoteVideoTrack} = this.mediaProperties;
  
      // TODO: We need a real time value for Audio, Video and Share send indicator
      if (mediaDirection.receiveVideo === true && remoteVideoTrack) {
        if (LAYOUT_TYPES.includes(layoutType)) {
          return this.meetingRequest
            .changeVideoLayout({
              locusUrl: this.locusInfo.self.url,
              deviceUrl: this.deviceUrl,
              layoutType
            })
            .then((response) => {
              if (response && response.body && response.body.locus) {
                this.locusInfo.onFullLocus(response.body.locus);
              }
            });
        }
  
        return reject('Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType recieved.');
      }
  
      return reject('Meeting:index#changeVideoLayout --> cannot change video layout, you are not recieving any video/share stream');
    }
  
    /**
     * Sets the quality of the local video stream
     * @param {String} level {LOW|MEDIUM|HIGH}
     * @returns {Promise}
     */
    setLocalVideoQuality(level) {
      LoggerProxy.logger.log(`Meeting:index#setLocalVideoQuality --> Setting quality to ${level}`);
  
      if (!VIDEO_RESOLUTIONS[level]) {
        const errorMessage = `Meeting:index#setLocalVideoQuality --> ${level} not defined`;
  
        LoggerProxy.logger.error(errorMessage);
  
        return Promise.reject(new Error(errorMessage));
      }
  
      if (!this.mediaProperties.mediaDirection.sendVideo) {
        const errorMessage = 'Meeting:index#setLocalVideoQuality --> unable to change video quality, sendVideo is disabled';
  
        LoggerProxy.logger.error(errorMessage);
  
        return Promise.reject(new Error(errorMessage));
      }
  
      // If level is already the same, don't do anything
      if (level === this.mediaProperties.localQualityLevel) {
        LoggerProxy.logger.warn(`Meeting:index#setLocalQualityLevel --> Quality already set to ${level}`);
  
        return Promise.resolve();
      }
  
      // Set the quality level in properties
      this.mediaProperties.setLocalQualityLevel(level);
  
      const mediaDirection = {
        sendAudio: this.mediaProperties.mediaDirection.sendAudio,
        sendVideo: this.mediaProperties.mediaDirection.sendVideo,
        sendShare: this.mediaProperties.mediaDirection.sendShare
      };
  
      return this.getMediaStreams(mediaDirection, VIDEO_RESOLUTIONS[level])
        .then(([localStream]) =>
          this.updateVideo({
            sendVideo: true,
            receiveVideo: true,
            stream: localStream
          }));
    }
  
    /**
     * Sets the quality level of the remote incoming media
     * @param {String} level {LOW|MEDIUM|HIGH}
     * @returns {Promise}
     */
    setRemoteQualityLevel(level) {
      LoggerProxy.logger.log(`Meeting:index#setRemoteQualityLevel --> Setting quality to ${level}`);
  
      if (!QUALITY_LEVELS[level]) {
        const errorMessage = `Meeting:index#setRemoteQualityLevel --> ${level} not defined`;
  
        LoggerProxy.logger.error(errorMessage);
  
        return Promise.reject(new Error(errorMessage));
      }
  
      if (!this.mediaProperties.mediaDirection.receiveAudio && !this.mediaProperties.mediaDirection.receiveVideo) {
        const errorMessage = 'Meeting:index#setRemoteQualityLevel --> unable to change remote quality, receiveVideo and receiveAudio is disabled';
  
        LoggerProxy.logger.error(errorMessage);
  
        return Promise.reject(new Error(errorMessage));
      }
  
      // If level is already the same, don't do anything
      if (level === this.mediaProperties.remoteQualityLevel) {
        LoggerProxy.logger.warn(`Meeting:index#setRemoteQualityLevel --> Quality already set to ${level}`);
  
        return Promise.resolve();
      }
  
      // Set the quality level in properties
      this.mediaProperties.setRemoteQualityLevel(level);
  
      return this.updateMedia({mediaSettings: this.mediaProperties.mediaDirection});
    }
  
    /**
     * Sets the quality level of all meeting media (incoming/outgoing)
     * @param {String} level {LOW|MEDIUM|HIGH}
     * @returns {Promise}
     */
    setMeetingQuality(level) {
      LoggerProxy.logger.log(`Meeting:index#setMeetingQuality --> Setting quality to ${level}`);
  
      if (!QUALITY_LEVELS[level]) {
        const errorMessage = `Meeting:index#setMeetingQuality --> ${level} not defined`;
  
        LoggerProxy.logger.error(errorMessage);
  
        return Promise.reject(new Error(errorMessage));
      }
  
      const previousLevel = {
        local: this.mediaProperties.localQualityLevel,
        remote: this.mediaProperties.remoteQualityLevel
      };
  
      // If level is already the same, don't do anything
      if (
        level === this.mediaProperties.localQualityLevel &&
        level === this.mediaProperties.remoteQualityLevel
      ) {
        LoggerProxy.logger.warn(`Meeting:index#setMeetingQuality --> Quality already set to ${level}`);
  
        return Promise.resolve();
      }
  
      // Determine the direction of our current media
      const {receiveAudio, receiveVideo, sendVideo} = this.mediaProperties.mediaDirection;
  
      return (sendVideo ? this.setLocalVideoQuality(level) : Promise.resolve())
        .then(() =>
          ((receiveAudio || receiveVideo) ?
            this.setRemoteQualityLevel(level) :
            Promise.resolve()))
        .catch((error) => {
          // From troubleshooting it seems that the stream itself doesn't change the max-fs if the peer connection isn't stable
          this.mediaProperties.setLocalQualityLevel(previousLevel.local);
          this.mediaProperties.setRemoteQualityLevel(previousLevel.remote);
  
          LoggerProxy.logger.error(`Meeting:index#setMeetingQuality --> ${error.message}`);
  
          Metrics.sendOperationalMetric(
            METRICS_OPERATIONAL_MEASURES.SET_MEETING_QUALITY_FAILURE,
            {
              correlation_id: this.correlationId,
              locus_id: this.locusUrl.split('/').pop(),
              reason: error.message,
              stack: error.stack
            },
            {
              type: error.name
            }
          );
  
          return Promise.reject(error);
        });
    }
  
    /**
    * @param {Object} options parameter
    * @param {Boolean} options.sendAudio send audio from the display share
    * @param {Boolean} options.sendShare send video from the display share
    * @param {Object} options.sharePreferences
    * @param {MediaTrackConstraints} options.sharePreferences.shareConstraints constraints to apply to video
    *   @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints}
    * @param {Boolean} options.sharePreferences.highFrameRate if shareConstraints isn't provided, set default values based off of this boolean
    * @returns {Promise.<MediaStream>}
    */
    shareScreen(options = {}) {
      LoggerProxy.logger.log('Meeting:index#shareScreen --> Getting local share');
  
      const shareConstraints = {
        sendShare: true,
        sendAudio: false,
        ...options
      };
  
      return Media.getDisplayMedia(shareConstraints, this.config)
        .then((shareStream) => this.updateShare({
          sendShare: true,
          receiveShare: this.mediaProperties.mediaDirection.receiveShare,
          stream: shareStream
        }))
        .catch((error) => {
          // Whenever there is a failure when trying to access a user's display
          // report it as an operational metric
          // This gives visibility into common errors and can help
          // with further troubleshooting
  
          // This metrics will get erros for getDisplayMedia and share errors for now
          // TODO: The getDisplayMedia errors need to be moved inside `media.getDisplayMedia`
          const metricName = METRICS_OPERATIONAL_MEASURES.GET_DISPLAY_MEDIA_FAILURE;
          const data = {
            correlation_id: this.correlationId,
            locus_id: this.locusUrl.split('/').pop(),
            reason: error.message,
            stack: error.stack
          };
          const metadata = {
            type: error.name
          };
  
          Metrics.sendOperationalMetric(metricName, data, metadata);
          throw new MediaError('Unable to retrieve display media stream', error);
        });
    }
  }
  