File: source/peer-connection.js

                      /**
                       * <blockquote class="info">
                       *   Learn more about how ICE works in this
                       *   <a href="https://temasys.com.sg/ice-what-is-this-sorcery/">article here</a>.
                       * </blockquote>
                       * The list of Peer connection session description exchanging states.
                       * @attribute PEER_CONNECTION_STATE
                       * @param {String} STABLE            <small>Value <code>"stable"</code></small>
                       *   The value of the state when there is no session description being exchanged between Peer connection.
                       * @param {String} HAVE_LOCAL_OFFER  <small>Value <code>"have-local-offer"</code></small>
                       *   The value of the state when local <code>"offer"</code> session description is set.
                       *   <small>This should transition to <code>STABLE</code> state after remote <code>"answer"</code>
                       *   session description is set.</small>
                       *   <small>See <a href="#event_handshakeProgress"><code>handshakeProgress</code> event</a> for a more
                       *   detailed exchanging of session description states.</small>
                       * @param {String} HAVE_REMOTE_OFFER <small>Value <code>"have-remote-offer"</code></small>
                       *   The value of the state when remote <code>"offer"</code> session description is set.
                       *   <small>This should transition to <code>STABLE</code> state after local <code>"answer"</code>
                       *   session description is set.</small>
                       *   <small>See <a href="#event_handshakeProgress"><code>handshakeProgress</code> event</a> for a more
                       *   detailed exchanging of session description states.</small>
                       * @param {String} CLOSED            <small>Value <code>"closed"</code></small>
                       *   The value of the state when Peer connection is closed and no session description can be exchanged and set.
                       * @type JSON
                       * @readOnly
                       * @for Skylink
                       * @since 0.5.0
                       */
                      Skylink.prototype.PEER_CONNECTION_STATE = {
                        STABLE: 'stable',
                        HAVE_LOCAL_OFFER: 'have-local-offer',
                        HAVE_REMOTE_OFFER: 'have-remote-offer',
                        CLOSED: 'closed'
                      };
                      
                      /**
                       * The list of <a href="#method_getConnectionStatus"><code>getConnectionStatus()</code>
                       * method</a> retrieval states.
                       * @attribute GET_CONNECTION_STATUS_STATE
                       * @param {Number} RETRIEVING <small>Value <code>0</code></small>
                       *   The value of the state when <code>getConnectionStatus()</code> is retrieving the Peer connection stats.
                       * @param {Number} RETRIEVE_SUCCESS <small>Value <code>1</code></small>
                       *   The value of the state when <code>getConnectionStatus()</code> has retrieved the Peer connection stats successfully.
                       * @param {Number} RETRIEVE_ERROR <small>Value <code>-1</code></small>
                       *   The value of the state when <code>getConnectionStatus()</code> has failed retrieving the Peer connection stats.
                       * @type JSON
                       * @readOnly
                       * @for Skylink
                       * @since 0.1.0
                       */
                      Skylink.prototype.GET_CONNECTION_STATUS_STATE = {
                        RETRIEVING: 0,
                        RETRIEVE_SUCCESS: 1,
                        RETRIEVE_ERROR: -1
                      };
                      
                      /**
                       * <blockquote class="info">
                       *  As there are more features getting implemented, there will be eventually more different types of
                       *  server Peers.
                       * </blockquote>
                       * The list of available types of server Peer connections.
                       * @attribute SERVER_PEER_TYPE
                       * @param {String} MCU <small>Value <code>"mcu"</code></small>
                       *   The value of the server Peer type that is used for MCU connection.
                       * @type JSON
                       * @readOnly
                       * @for Skylink
                       * @since 0.6.1
                       */
                      Skylink.prototype.SERVER_PEER_TYPE = {
                        MCU: 'mcu'
                        //SIP: 'sip'
                      };
                      
                      /**
                       * <blockquote class="info">
                       *  Learn more about how ICE works in this
                       *  <a href="https://temasys.com.sg/ice-what-is-this-sorcery/">article here</a>.
                       * </blockquote>
                       * The list of available Peer connection bundle policies.
                       * @attribute BUNDLE_POLICY
                       * @param {String} MAX_COMPAT <small>Value <code>"max-compat"</code></small>
                       *   The value of the bundle policy to generate ICE candidates for each media type
                       *   so each media type flows through different transports.
                       * @param {String} MAX_BUNDLE <small>Value <code>"max-bundle"</code></small>
                       *   The value of the bundle policy to generate ICE candidates for one media type
                       *   so all media type flows through a single transport.
                       * @param {String} BALANCED   <small>Value <code>"balanced"</code></small>
                       *   The value of the bundle policy to use <code>MAX_BUNDLE</code> if Peer supports it,
                       *   else fallback to <code>MAX_COMPAT</code>.
                       * @param {String} NONE       <small>Value <code>"none"</code></small>
                       *   The value of the bundle policy to not use any media bundle.
                       *   <small>This removes the <code>a=group:BUNDLE</code> line from session descriptions.</small>
                       * @type JSON
                       * @readOnly
                       * @for Skylink
                       * @since 0.6.18
                       */
                      Skylink.prototype.BUNDLE_POLICY = {
                        MAX_COMPAT: 'max-compat',
                        BALANCED: 'balanced',
                        MAX_BUNDLE: 'max-bundle',
                        NONE: 'none'
                      };
                      
                      /**
                       * <blockquote class="info">
                       *  Learn more about how ICE works in this
                       *  <a href="https://temasys.com.sg/ice-what-is-this-sorcery/">article here</a>.
                       * </blockquote>
                       * The list of available Peer connection RTCP mux policies.
                       * @attribute RTCP_MUX_POLICY
                       * @param {String} REQUIRE   <small>Value <code>"require"</code></small>
                       *   The value of the RTCP mux policy to generate ICE candidates for RTP only and RTCP shares the same ICE candidates.
                       * @param {String} NEGOTIATE <small>Value <code>"negotiate"</code></small>
                       *   The value of the RTCP mux policy to generate ICE candidates for both RTP and RTCP each.
                       * @type JSON
                       * @readOnly
                       * @for Skylink
                       * @since 0.6.18
                       */
                      Skylink.prototype.RTCP_MUX_POLICY = {
                        REQUIRE: 'require',
                        NEGOTIATE: 'negotiate'
                      };
                      
                      /**
                       * <blockquote class="info">
                       *  Learn more about how ICE works in this
                       *  <a href="https://temasys.com.sg/ice-what-is-this-sorcery/">article here</a>.
                       * </blockquote>
                       * The list of available Peer connection certificates cryptographic algorithm to use.
                       * @attribute PEER_CERTIFICATE
                       * @param {String} RSA   <small>Value <code>"RSA"</code></small>
                       *   The value of the Peer connection certificate algorithm to use RSA-1024.
                       * @param {String} ECDSA <small>Value <code>"ECDSA"</code></small>
                       *   The value of the Peer connection certificate algorithm to use ECDSA.
                       * @param {String} AUTO  <small>Value <code>"AUTO"</code></small>
                       *   The value of the Peer connection to use the default certificate generated.
                       * @type JSON
                       * @readOnly
                       * @for Skylink
                       * @since 0.6.18
                       */
                      Skylink.prototype.PEER_CERTIFICATE = {
                        RSA: 'RSA',
                        ECDSA: 'ECDSA',
                        AUTO: 'AUTO'
                      };
                      
                      /**
                       * <blockquote class="info">
                       *   Note that Edge browser does not support renegotiation.
                       *   For MCU enabled Peer connections with <code>options.mcuUseRenegoRestart</code> set to <code>false</code>
                       *   in the <a href="#method_init"><code>init()</code> method</a>, the restart functionality may differ, you
                       *   may learn more about how to workaround it
                       *   <a href="http://support.temasys.io/support/discussions/topics/12000002853">in this article here</a>.
                       *   For restarts with Peers connecting from Android, iOS or C++ SDKs, restarts might not work as written in
                       *   <a href="http://support.temasys.io/support/discussions/topics/12000005188">in this article here</a>.
                       *   Note that this functionality should be used when Peer connection stream freezes during a connection.
                       *   For a better user experience for only MCU enabled Peer connections, the functionality is throttled when invoked many
                       *   times in less than the milliseconds interval configured in the <a href="#method_init"><code>init()</code> method</a>.
                       * </blockquote>
                       * Function that refreshes Peer connections to update with the current streaming.
                       * @method refreshConnection
                       * @param {String|Array} [targetPeerId] <blockquote class="info">
                       *   Note that this is ignored if MCU is enabled for the App Key provided in
                       *   <a href="#method_init"><code>init()</code> method</a>. <code>refreshConnection()</code> will "refresh"
                       *   all Peer connections. See the <u>Event Sequence</u> for more information.</blockquote>
                       *   The target Peer ID to refresh connection with.
                       * - When provided as an Array, it will refresh all connections with all the Peer IDs provided.
                       * - When not provided, it will refresh all the currently connected Peers in the Room.
                       * @param {Boolean} [iceRestart=false] <blockquote class="info">
                       *   Note that this flag will not be honoured for MCU enabled Peer connections where
                       *   <code>options.mcuUseRenegoRestart</code> flag is set to <code>false</code> as it is not necessary since for MCU
                       *   "restart" case is to invoke <a href="#method_joinRoom"><code>joinRoom()</code> method</a> again, or that it is
                       *   not supported by the MCU.</blockquote>
                       *   The flag if ICE connections should restart when refreshing Peer connections.
                       *   <small>This is used when ICE connection state is <code>FAILED</code> or <code>DISCONNECTED</code>, which state
                       *   can be retrieved with the <a href="#event_iceConnectionState"><code>iceConnectionState</code> event</a>.</small>
                       * @param {JSON} [options] <blockquote class="info">
                       *   Note that for MCU connections, the <code>bandwidth</code> or <code>googleXBandwidth</code>
                       *   settings will override for all Peers or the current Room connection session settings.</blockquote>
                       *   The custom Peer configuration settings.
                       * @param {JSON} [options.bandwidth] The configuration to set the maximum streaming bandwidth to send to Peers.
                       *   <small>Object signature follows <a href="#method_joinRoom"><code>joinRoom()</code> method</a>
                       *   <code>options.bandwidth</code> settings.</small>
                       * @param {JSON} [options.googleXBandwidth] The configuration to set the experimental google
                       *   video streaming bandwidth sent to Peers.
                       *   <small>Object signature follows <a href="#method_joinRoom"><code>joinRoom()</code> method</a>
                       *   <code>options.googleXBandwidth</code> settings.</small>
                       * @param {Function} [callback] The callback function fired when request has completed.
                       *   <small>Function parameters signature is <code>function (error, success)</code></small>
                       *   <small>Function request completion is determined by the <a href="#event_peerRestart">
                       *   <code>peerRestart</code> event</a> triggering <code>isSelfInitiateRestart</code> parameter payload
                       *   value as <code>true</code> for all Peers targeted for request success.</small>
                       * @param {JSON} callback.error The error result in request.
                       *   <small>Defined as <code>null</code> when there are no errors in request</small>
                       * @param {Array} callback.error.listOfPeers The list of Peer IDs targeted.
                       * @param {JSON} callback.error.refreshErrors The list of Peer connection refresh errors.
                       * @param {Error|String} callback.error.refreshErrors.#peerId The Peer connection refresh error associated
                       *   with the Peer ID defined in <code>#peerId</code> property.
                       *   <small>If <code>#peerId</code> value is <code>"self"</code>, it means that it is the error when there
                       *   is no Peer connections to refresh with.</small>
                       * @param {JSON} callback.error.refreshSettings The list of Peer connection refresh settings.
                       * @param {JSON} callback.error.refreshSettings.#peerId The Peer connection refresh settings associated
                       *   with the Peer ID defined in <code>#peerId</code> property.
                       * @param {Boolean} callback.error.refreshSettings.#peerId.iceRestart The flag if ICE restart is enabled for
                       *   this Peer connection refresh session.
                       * @param {JSON} callback.error.refreshSettings.#peerId.customSettings The Peer connection custom settings.
                       *   <small>Object signature follows <a href="#method_getPeersCustomSettings"><code>getPeersCustomSettings</code>
                       *   method</a> returned per <code>#peerId</code> object.</small>
                       * @param {JSON} callback.success The success result in request.
                       *   <small>Defined as <code>null</code> when there are errors in request</small>
                       * @param {Array} callback.success.listOfPeers The list of Peer IDs targeted.
                       * @param {JSON} callback.success.refreshSettings The list of Peer connection refresh settings.
                       * @param {JSON} callback.success.refreshSettings.#peerId The Peer connection refresh settings associated
                       *   with the Peer ID defined in <code>#peerId</code> property.
                       * @param {Boolean} callback.success.refreshSettings.#peerId.iceRestart The flag if ICE restart is enabled for
                       *   this Peer connection refresh session.
                       * @param {JSON} callback.success.refreshSettings.#peerId.customSettings The Peer connection custom settings.
                       *   <small>Object signature follows <a href="#method_getPeersCustomSettings"><code>getPeersCustomSettings</code>
                       *   method</a> returned per <code>#peerId</code> object.</small>
                       * @trigger <ol class="desc-seq">
                       *   <li>Checks if MCU is enabled for App Key provided in <a href="#method_init"><code>init()</code> method</a><ol>
                       *   <li>If MCU is enabled: <ol><li>If there are connected Peers in the Room: <ol>
                       *   <li><a href="#event_peerRestart"><code>peerRestart</code> event</a> triggers parameter payload
                       *   <code>isSelfInitiateRestart</code> value as <code>true</code> for all connected Peer connections.</li>
                       *   <li><a href="#event_serverPeerRestart"><code>serverPeerRestart</code> event</a> triggers for
                       *   connected MCU server Peer connection.</li></ol></li>
                       *   <li>If <code>options.mcuUseRenegoRestart</code> value is <code>false</code> set in the
                       *   <a href="#method_init"><code>init()</code> method</a>: <ol><li>
                       *   Invokes <a href="#method_joinRoom"><code>joinRoom()</code> method</a> <small><code>refreshConnection()</code>
                       *   will retain the User session information except the Peer ID will be a different assigned ID due to restarting the
                       *   Room session.</small> <ol><li>If request has errors <ol><li><b>ABORT</b> and return error.
                       *   </li></ol></li></ol></li></ol></li></ol></li>
                       *   <li>Else: <ol><li>If there are connected Peers in the Room: <ol>
                       *   <li>Refresh connections for all targeted Peers. <ol>
                       *   <li>If Peer connection exists: <ol>
                       *   <li><a href="#event_peerRestart"><code>peerRestart</code> event</a> triggers parameter payload
                       *   <code>isSelfInitiateRestart</code> value as <code>true</code> for all targeted Peer connections.</li></ol></li>
                       *   <li>Else: <ol><li><b>ABORT</b> and return error.</li></ol></li>
                       *   </ol></li></ol></li></ol></ol></li></ol></li></ol>
                       * @example
                       *   // Example 1: Refreshing a Peer connection
                       *   function refreshFrozenVideoStream (peerId) {
                       *     skylinkDemo.refreshConnection(peerId, function (error, success) {
                       *       if (error) return;
                       *       console.log("Refreshing connection for '" + peerId + "'");
                       *     });
                       *   }
                       *
                       *   // Example 2: Refreshing a list of Peer connections
                       *   function refreshFrozenVideoStreamGroup (peerIdA, peerIdB) {
                       *     skylinkDemo.refreshConnection([peerIdA, peerIdB], function (error, success) {
                       *       if (error) {
                       *         if (error.transferErrors[peerIdA]) {
                       *           console.error("Failed refreshing connection for '" + peerIdA + "'");
                       *         } else {
                       *           console.log("Refreshing connection for '" + peerIdA + "'");
                       *         }
                       *         if (error.transferErrors[peerIdB]) {
                       *           console.error("Failed refreshing connection for '" + peerIdB + "'");
                       *         } else {
                       *           console.log("Refreshing connection for '" + peerIdB + "'");
                       *         }
                       *       } else {
                       *         console.log("Refreshing connection for '" + peerIdA + "' and '" + peerIdB + "'");
                       *       }
                       *     });
                       *   }
                       *
                       *   // Example 3: Refreshing all Peer connections
                       *   function refreshFrozenVideoStreamAll () {
                       *     skylinkDemo.refreshConnection(function (error, success) {
                       *       if (error) {
                       *         for (var i = 0; i < error.listOfPeers.length; i++) {
                       *           if (error.refreshErrors[error.listOfPeers[i]]) {
                       *             console.error("Failed refreshing connection for '" + error.listOfPeers[i] + "'");
                       *           } else {
                       *             console.info("Refreshing connection for '" + error.listOfPeers[i] + "'");
                       *           }
                       *         }
                       *       } else {
                       *         console.log("Refreshing connection for all Peers", success.listOfPeers);
                       *       }
                       *     });
                       *   }
                       *
                       *   // Example 4: Refresh Peer connection when ICE connection has failed or disconnected
                       *   //            and do a ICE connection refresh (only for non-MCU case)
                       *   skylinkDemo.on("iceConnectionState", function (state, peerId) {
                       *      if (!usesMCUKey && [skylinkDemo.ICE_CONNECTION_STATE.FAILED,
                       *        skylinkDemo.ICE_CONNECTION_STATE.DISCONNECTED].indexOf(state) > -1) {
                       *        skylinkDemo.refreshConnection(peerId, true);
                       *      }
                       *   });
                       * @for Skylink
                       * @since 0.5.5
                       */
                      Skylink.prototype.refreshConnection = function(targetPeerId, iceRestart, options, callback) {
                        var self = this;
                      
                        var listOfPeers = Object.keys(self._peerConnections);
                        var doIceRestart = false;
                        var bwOptions = {};
                      
                        if(Array.isArray(targetPeerId)) {
                          listOfPeers = targetPeerId;
                        } else if (typeof targetPeerId === 'string') {
                          listOfPeers = [targetPeerId];
                        } else if (typeof targetPeerId === 'boolean') {
                          doIceRestart = targetPeerId;
                        } else if (targetPeerId && typeof targetPeerId === 'object') {
                          bwOptions = targetPeerId;
                        } else if (typeof targetPeerId === 'function') {
                          callback = targetPeerId;
                        }
                      
                        if (typeof iceRestart === 'boolean') {
                          doIceRestart = iceRestart;
                        } else if (iceRestart && typeof iceRestart === 'object') {
                          bwOptions = iceRestart;
                        } else if (typeof iceRestart === 'function') {
                          callback = iceRestart;
                        }
                      
                        if (options && typeof options === 'object') {
                          bwOptions = options;
                        } else if (typeof options === 'function') {
                          callback = options;
                        }
                      
                        var emitErrorForPeersFn = function (error) {
                          log.error(error);
                      
                          if (typeof callback === 'function') {
                            var listOfPeerErrors = {};
                      
                            if (listOfPeers.length === 0) {
                              listOfPeerErrors.self = new Error(error);
                            } else {
                              for (var i = 0; i < listOfPeers.length; i++) {
                                listOfPeerErrors[listOfPeers[i]] = new Error(error);
                              }
                            }
                      
                            callback({
                              refreshErrors: listOfPeerRestartErrors,
                              listOfPeers: listOfPeers
                            }, null);
                          }
                        };
                      
                        if (listOfPeers.length === 0 && !(self._hasMCU && !self._mcuUseRenegoRestart)) {
                          emitErrorForPeersFn('There is currently no peer connections to restart');
                          return;
                        }
                      
                        if (window.webrtcDetectedBrowser === 'edge') {
                          emitErrorForPeersFn('Edge browser currently does not support renegotiation.');
                          return;
                        }
                      
                        self._throttle(function (runFn) {
                          if (!runFn && self._hasMCU && !self._mcuUseRenegoRestart) {
                            if (self._throttlingShouldThrowError) {
                              emitErrorForPeersFn('Unable to run as throttle interval has not reached (' + self._throttlingTimeouts.refreshConnection + 'ms).');
                            }
                            return;
                          }
                          self._refreshPeerConnection(listOfPeers, doIceRestart, bwOptions, callback);
                        }, 'refreshConnection', self._throttlingTimeouts.refreshConnection);
                      
                      };
                      
                      /**
                       * Function that refresh connections.
                       * @method _refreshPeerConnection
                       * @private
                       * @for Skylink
                       * @since 0.6.15
                       */
                      Skylink.prototype._refreshPeerConnection = function(listOfPeers, doIceRestart, bwOptions, callback) {
                        var self = this;
                        var listOfPeerRestarts = [];
                        var error = '';
                        var listOfPeerRestartErrors = {};
                        var listOfPeersSettings = {};
                      
                        // To fix jshint dont put functions within a loop
                        var refreshSinglePeerCallback = function (peerId) {
                          return function (error) {
                            if (listOfPeerRestarts.indexOf(peerId) === -1) {
                              if (error) {
                                log.error([peerId, 'RTCPeerConnection', null, 'Failed restarting for peer'], error);
                                listOfPeerRestartErrors[peerId] = error;
                              } else {
                                listOfPeersSettings[peerId] = {
                                  iceRestart: !self._hasMCU && self._peerInformations[peerId] && self._peerInformations[peerId].config &&
                                    self._peerInformations[peerId].config.enableIceRestart && self._enableIceRestart && doIceRestart,
                                  customSettings: self.getPeersCustomSettings()[peerId] || {}
                                };
                              }
                              listOfPeerRestarts.push(peerId);
                            }
                      
                            if (listOfPeerRestarts.length === listOfPeers.length) {
                              if (typeof callback === 'function') {
                                log.log([null, 'PeerConnection', null, 'Invoked all peers to restart. Firing callback']);
                      
                                if (Object.keys(listOfPeerRestartErrors).length > 0) {
                                  callback({
                                    refreshErrors: listOfPeerRestartErrors,
                                    listOfPeers: listOfPeers,
                                    refreshSettings: listOfPeersSettings
                                  }, null);
                                } else {
                                  callback(null, {
                                    listOfPeers: listOfPeers,
                                    refreshSettings: listOfPeersSettings
                                  });
                                }
                              }
                            }
                          };
                        };
                      
                        var refreshSinglePeer = function(peerId, peerCallback){
                          if (!self._peerConnections[peerId]) {
                            error = 'There is currently no existing peer connection made ' +
                              'with the peer. Unable to restart connection';
                            log.error([peerId, null, null, error]);
                            peerCallback(error);
                            return;
                          }
                      
                          log.log([peerId, 'PeerConnection', null, 'Restarting peer connection']);
                      
                          // do a hard reset on variable object
                          self._restartPeerConnection(peerId, doIceRestart, bwOptions, peerCallback);
                        };
                      
                        if(!self._hasMCU) {
                          var i;
                      
                          for (i = 0; i < listOfPeers.length; i++) {
                            var peerId = listOfPeers[i];
                      
                            if (Object.keys(self._peerConnections).indexOf(peerId) > -1) {
                              refreshSinglePeer(peerId, refreshSinglePeerCallback(peerId));
                            } else {
                              error = 'Peer connection with peer does not exists. Unable to restart';
                              log.error([peerId, 'PeerConnection', null, error]);
                              refreshSinglePeerCallback(peerId)(error);
                            }
                          }
                        } else {
                          self._restartMCUConnection(callback, doIceRestart, bwOptions);
                        }
                      };
                      
                      /**
                       * <blockquote class="info">
                       * Note that this is not well supported in the Edge browser.
                       * </blockquote>
                       * Function that retrieves Peer connection bandwidth and ICE connection stats.
                       * @method getConnectionStatus
                       * @param {String|Array} [targetPeerId] The target Peer ID to retrieve connection stats from.
                       * - When provided as an Array, it will retrieve all connection stats from all the Peer IDs provided.
                       * - When not provided, it will retrieve all connection stats from the currently connected Peers in the Room.
                       * @param {Function} [callback] The callback function fired when request has completed.
                       *   <small>Function parameters signature is <code>function (error, success)</code></small>
                       *   <small>Function request completion is determined by the <a href="#event_getConnectionStatusStateChange">
                       *   <code>getConnectionStatusStateChange</code> event</a> triggering <code>state</code> parameter payload
                       *   value as <code>RETRIEVE_SUCCESS</code> for all Peers targeted for request success.</small>
                       *   [Rel: Skylink.GET_CONNECTION_STATUS_STATE]
                       * @param {JSON} callback.error The error result in request.
                       *   <small>Defined as <code>null</code> when there are no errors in request</small>
                       * @param {Array} callback.error.listOfPeers The list of Peer IDs targeted.
                       * @param {JSON} callback.error.retrievalErrors The list of Peer connection stats retrieval errors.
                       * @param {Error|String} callback.error.retrievalErrors.#peerId The Peer connection stats retrieval error associated
                       *   with the Peer ID defined in <code>#peerId</code> property.
                       *   <small>If <code>#peerId</code> value is <code>"self"</code>, it means that it is the error when there
                       *   are no Peer connections to refresh with.</small>
                       * @param {JSON} callback.error.connectionStats The list of Peer connection stats.
                       *   <small>These are the Peer connection stats that has been managed to be successfully retrieved.</small>
                       * @param {JSON} callback.error.connectionStats.#peerId The Peer connection stats associated with
                       *   the Peer ID defined in <code>#peerId</code> property.
                       *   <small>Object signature matches the <code>stats</code> parameter payload received in the
                       *   <a href="#event_getConnectionStatusStateChange"><code>getConnectionStatusStateChange</code> event</a>.</small>
                       * @param {JSON} callback.success The success result in request.
                       *   <small>Defined as <code>null</code> when there are errors in request</small>
                       * @param {Array} callback.success.listOfPeers The list of Peer IDs targeted.
                       * @param {JSON} callback.success.connectionStats The list of Peer connection stats.
                       * @param {JSON} callback.success.connectionStats.#peerId The Peer connection stats associated with
                       *   the Peer ID defined in <code>#peerId</code> property.
                       *   <small>Object signature matches the <code>stats</code> parameter payload received in the
                       *   <a href="#event_getConnectionStatusStateChange"><code>getConnectionStatusStateChange</code> event</a>.</small>
                       * @trigger <ol class="desc-seq">
                       *   <li>Retrieves Peer connection stats for all targeted Peers. <ol>
                       *   <li>If Peer connection has closed or does not exists: <small>This can be checked with
                       *   <a href="#event_peerConnectionState"><code>peerConnectionState</code> event</a>
                       *   triggering parameter payload <code>state</code> as <code>CLOSED</code> for Peer.</small> <ol>
                       *   <li><a href="#event_getConnectionStatusStateChange"> <code>getConnectionStatusStateChange</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>RETRIEVE_ERROR</code>.</li>
                       *   <li><b>ABORT</b> and return error.</li></ol></li>
                       *   <li><a href="#event_getConnectionStatusStateChange"><code>getConnectionStatusStateChange</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>RETRIEVING</code>.</li>
                       *   <li>Received response from retrieval. <ol>
                       *   <li>If retrieval was successful: <ol>
                       *   <li><a href="#event_getConnectionStatusStateChange"><code>getConnectionStatusStateChange</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>RETRIEVE_SUCCESS</code>.</li></ol></li>
                       *   <li>Else: <ol>
                       *   <li><a href="#event_getConnectionStatusStateChange"> <code>getConnectionStatusStateChange</code> event</a>
                       *   triggers parameter payload <code>state</code> as <code>RETRIEVE_ERROR</code>.</li>
                       *   </ol></li></ol></li></ol></li></ol>
                       * @example
                       *   // Example 1: Retrieve a Peer connection stats
                       *   function startBWStatsInterval (peerId) {
                       *     setInterval(function () {
                       *       skylinkDemo.getConnectionStatus(peerId, function (error, success) {
                       *         if (error) return;
                       *         var sendVideoBytes  = success.connectionStats[peerId].video.sending.bytes;
                       *         var sendAudioBytes  = success.connectionStats[peerId].audio.sending.bytes;
                       *         var recvVideoBytes  = success.connectionStats[peerId].video.receiving.bytes;
                       *         var recvAudioBytes  = success.connectionStats[peerId].audio.receiving.bytes;
                       *         var localCandidate  = success.connectionStats[peerId].selectedCandidate.local;
                       *         var remoteCandidate = success.connectionStats[peerId].selectedCandidate.remote;
                       *         console.log("Sending audio (" + sendAudioBytes + "bps) video (" + sendVideoBytes + ")");
                       *         console.log("Receiving audio (" + recvAudioBytes + "bps) video (" + recvVideoBytes + ")");
                       *         console.log("Local candidate: " + localCandidate.ipAddress + ":" + localCandidate.portNumber +
                       *           "?transport=" + localCandidate.transport + " (type: " + localCandidate.candidateType + ")");
                       *         console.log("Remote candidate: " + remoteCandidate.ipAddress + ":" + remoteCandidate.portNumber +
                       *           "?transport=" + remoteCandidate.transport + " (type: " + remoteCandidate.candidateType + ")");
                       *       });
                       *     }, 1000);
                       *   }
                       *
                       *   // Example 2: Retrieve a list of Peer connection stats
                       *   function printConnStats (peerId, data) {
                       *     if (!data.connectionStats[peerId]) return;
                       *     var sendVideoBytes  = data.connectionStats[peerId].video.sending.bytes;
                       *     var sendAudioBytes  = data.connectionStats[peerId].audio.sending.bytes;
                       *     var recvVideoBytes  = data.connectionStats[peerId].video.receiving.bytes;
                       *     var recvAudioBytes  = data.connectionStats[peerId].audio.receiving.bytes;
                       *     var localCandidate  = data.connectionStats[peerId].selectedCandidate.local;
                       *     var remoteCandidate = data.connectionStats[peerId].selectedCandidate.remote;
                       *     console.log(peerId + " - Sending audio (" + sendAudioBytes + "bps) video (" + sendVideoBytes + ")");
                       *     console.log(peerId + " - Receiving audio (" + recvAudioBytes + "bps) video (" + recvVideoBytes + ")");
                       *     console.log(peerId + " - Local candidate: " + localCandidate.ipAddress + ":" + localCandidate.portNumber +
                       *       "?transport=" + localCandidate.transport + " (type: " + localCandidate.candidateType + ")");
                       *     console.log(peerId + " - Remote candidate: " + remoteCandidate.ipAddress + ":" + remoteCandidate.portNumber +
                       *       "?transport=" + remoteCandidate.transport + " (type: " + remoteCandidate.candidateType + ")");
                       *   }
                       *
                       *   function startBWStatsInterval (peerIdA, peerIdB) {
                       *     setInterval(function () {
                       *       skylinkDemo.getConnectionStatus([peerIdA, peerIdB], function (error, success) {
                       *         if (error) {
                       *           printConnStats(peerIdA, error.connectionStats);
                       *           printConnStats(peerIdB, error.connectionStats);
                       *         } else {
                       *           printConnStats(peerIdA, success.connectionStats);
                       *           printConnStats(peerIdB, success.connectionStats);
                       *         }
                       *       });
                       *     }, 1000);
                       *   }
                       *
                       *   // Example 3: Retrieve all Peer connection stats
                       *   function printConnStats (listOfPeers, data) {
                       *     listOfPeers.forEach(function (peerId) {
                       *       if (!data.connectionStats[peerId]) return;
                       *       var sendVideoBytes  = data.connectionStats[peerId].video.sending.bytes;
                       *       var sendAudioBytes  = data.connectionStats[peerId].audio.sending.bytes;
                       *       var recvVideoBytes  = data.connectionStats[peerId].video.receiving.bytes;
                       *       var recvAudioBytes  = data.connectionStats[peerId].audio.receiving.bytes;
                       *       var localCandidate  = data.connectionStats[peerId].selectedCandidate.local;
                       *       var remoteCandidate = data.connectionStats[peerId].selectedCandidate.remote;
                       *       console.log(peerId + " - Sending audio (" + sendAudioBytes + "bps) video (" + sendVideoBytes + ")");
                       *       console.log(peerId + " - Receiving audio (" + recvAudioBytes + "bps) video (" + recvVideoBytes + ")");
                       *       console.log(peerId + " - Local candidate: " + localCandidate.ipAddress + ":" + localCandidate.portNumber +
                       *         "?transport=" + localCandidate.transport + " (type: " + localCandidate.candidateType + ")");
                       *       console.log(peerId + " - Remote candidate: " + remoteCandidate.ipAddress + ":" + remoteCandidate.portNumber +
                       *         "?transport=" + remoteCandidate.transport + " (type: " + remoteCandidate.candidateType + ")");
                       *     });
                       *   }
                       *
                       *   function startBWStatsInterval (peerIdA, peerIdB) {
                       *     setInterval(function () {
                       *       skylinkDemo.getConnectionStatus(function (error, success) {
                       *         if (error) {
                       *           printConnStats(error.listOfPeers, error.connectionStats);
                       *         } else {
                       *           printConnStats(success.listOfPeers, success.connectionStats);
                       *         }
                       *       });
                       *     }, 1000);
                       *   }
                       * @for Skylink
                       * @since 0.6.14
                       */
                      Skylink.prototype.getConnectionStatus = function (targetPeerId, callback) {
                        var self = this;
                        var listOfPeers = Object.keys(self._peerConnections);
                        var listOfPeerStats = {};
                        var listOfPeerErrors = {};
                      
                        // getConnectionStatus([])
                        if (Array.isArray(targetPeerId)) {
                          listOfPeers = targetPeerId;
                      
                        // getConnectionStatus('...')
                        } else if (typeof targetPeerId === 'string' && !!targetPeerId) {
                          listOfPeers = [targetPeerId];
                      
                        // getConnectionStatus(function () {})
                        } else if (typeof targetPeerId === 'function') {
                          callback = targetPeerId;
                          targetPeerId = undefined;
                        }
                      
                        // Check if Peers list is empty, in which we throw an Error if there isn't any
                        if (listOfPeers.length === 0) {
                          listOfPeerErrors.self = new Error('There is currently no peer connections to retrieve connection status');
                      
                          log.error([null, 'RTCStatsReport', null, 'Retrieving request failure ->'], listOfPeerErrors.self);
                      
                          if (typeof callback === 'function') {
                            callback({
                              listOfPeers: listOfPeers,
                              retrievalErrors: listOfPeerErrors,
                              connectionStats: listOfPeerStats
                            }, null);
                          }
                          return;
                        }
                      
                        if (window.webrtcDetectedBrowser === 'edge') {
                          log.warn('Edge browser does not have well support for stats.');
                        }
                      
                        var completedTaskCounter = [];
                      
                        var checkCompletedFn = function (peerId) {
                          if (completedTaskCounter.indexOf(peerId) === -1) {
                            completedTaskCounter.push(peerId);
                          }
                      
                          if (completedTaskCounter.length === listOfPeers.length) {
                            if (typeof callback === 'function') {
                              if (Object.keys(listOfPeerErrors).length > 0) {
                                callback({
                                  listOfPeers: listOfPeers,
                                  retrievalErrors: listOfPeerErrors,
                                  connectionStats: listOfPeerStats
                                }, null);
                      
                              } else {
                                callback(null, {
                                  listOfPeers: listOfPeers,
                                  connectionStats: listOfPeerStats
                                });
                              }
                            }
                          }
                        };
                      
                        var statsFn = function (peerId) {
                          var retrieveFn = function (firstRetrieval, nextCb) {
                            return function (err, result) {
                              if (err) {
                                log.error([peerId, 'RTCStatsReport', null, 'Retrieval failure ->'], error);
                                listOfPeerErrors[peerId] = error;
                                self._trigger('getConnectionStatusStateChange', self.GET_CONNECTION_STATUS_STATE.RETRIEVE_ERROR,
                                  peerId, null, error);
                                checkCompletedFn(peerId);
                                if (firstRetrieval) {
                                  delete self._peerStats[peerId];
                                }
                                return;
                              }
                      
                              if (firstRetrieval) {
                                nextCb();
                              } else {
                                listOfPeerStats[peerId] = result;
                                self._trigger('getConnectionStatusStateChange', self.GET_CONNECTION_STATUS_STATE.RETRIEVE_SUCCESS,
                                  peerId, listOfPeerStats[peerId], null);
                                checkCompletedFn(peerId);
                              }
                            };
                          };
                      
                          if (!self._peerStats[peerId]) {
                            self._peerStats[peerId] = {};
                      
                            log.debug([peerId, 'RTCStatsReport', null, 'Retrieving first report to tabulate results']);
                      
                            self._retrieveStats(peerId, retrieveFn(true, function () {
                              self._retrieveStats(peerId, retrieveFn());
                            }), true);
                            return;
                          }
                      
                          self._retrieveStats(peerId, retrieveFn());
                        };
                      
                        // Loop through all the list of Peers selected to retrieve connection status
                        for (var i = 0; i < listOfPeers.length; i++) {
                          var peerId = listOfPeers[i];
                      
                          self._trigger('getConnectionStatusStateChange', self.GET_CONNECTION_STATUS_STATE.RETRIEVING,
                            peerId, null, null);
                      
                          // Check if the Peer connection exists first
                          if (self._peerConnections.hasOwnProperty(peerId) && self._peerConnections[peerId]) {
                            statsFn(peerId);
                      
                          } else {
                            listOfPeerErrors[peerId] = new Error('The peer connection object does not exists');
                      
                            log.error([peerId, 'RTCStatsReport', null, 'Retrieval failure ->'], listOfPeerErrors[peerId]);
                      
                            self._trigger('getConnectionStatusStateChange', self.GET_CONNECTION_STATUS_STATE.RETRIEVE_ERROR,
                              peerId, null, listOfPeerErrors[peerId]);
                      
                            checkCompletedFn(peerId);
                          }
                        }
                      };
                      
                      /**
                       * Function that retrieves Peer connection stats.
                       * @method _retrieveStats
                       * @private
                       * @for Skylink
                       * @since 0.6.18
                       */
                      Skylink.prototype._retrieveStats = function (peerId, callback, beSilentOnLogs, isAutoBwStats) {
                        var self = this;
                      
                        if (!beSilentOnLogs) {
                          log.debug([peerId, 'RTCStatsReport', null, 'Retrieivng connection status']);
                        }
                      
                        if (!self._peerStats[peerId] && !isAutoBwStats) {
                          return callback(new Error('No stats initiated yet.'));
                        }
                      
                        var pc = self._peerConnections[peerId];
                        var result = {
                          raw: null,
                          connection: {
                            iceConnectionState: pc.iceConnectionState,
                            iceGatheringState: pc.iceGatheringState,
                            signalingState: pc.signalingState,
                            remoteDescription: {
                              type: pc.remoteDescription ? pc.remoteDescription.type || null : null,
                              sdp : pc.remoteDescription ? pc.remoteDescription.sdp || null : null
                            },
                            localDescription: {
                              type: pc.localDescription ? pc.localDescription.type || null : null,
                              sdp : pc.localDescription ? pc.localDescription.sdp || null : null
                            },
                            candidates: clone(self._gatheredCandidates[peerId] || {
                              sending: { host: [], srflx: [], relay: [] },
                              receiving: { host: [], srflx: [], relay: [] }
                            }),
                            dataChannels: {},
                            constraints: self._peerConnStatus[peerId] ? self._peerConnStatus[peerId].constraints : null,
                            optional: self._peerConnStatus[peerId] ? self._peerConnStatus[peerId].optional : null,
                            sdpConstraints: self._peerConnStatus[peerId] ? self._peerConnStatus[peerId].sdpConstraints : null
                          },
                          audio: {
                            sending: {
                              ssrc: null,
                              bytes: 0,
                              packets: 0,
                              // Should not be for sending?
                              packetsLost: 0,
                              rtt: 0,
                              // Should not be for sending?
                              jitter: 0,
                              // Should not be for sending?
                              jitterBufferMs: null,
                              codec: self._getSDPSelectedCodec(peerId, pc.remoteDescription, 'audio', beSilentOnLogs),
                              nacks: null,
                              inputLevel: null,
                              echoReturnLoss: null,
                              echoReturnLossEnhancement: null,
                              totalBytes: 0,
                              totalPackets: 0,
                              totalPacketsLost: 0,
                              totalNacks: null
                            },
                            receiving: {
                              ssrc: null,
                              bytes: 0,
                              packets: 0,
                              packetsLost: 0,
                              packetsDiscarded: 0,
                              fractionLost: 0,
                              nacks: null,
                              jitter: 0,
                              jitterBufferMs: null,
                              codec: self._getSDPSelectedCodec(peerId, pc.remoteDescription, 'audio', beSilentOnLogs),
                              outputLevel: null,
                              totalBytes: 0,
                              totalPackets: 0,
                              totalPacketsLost: 0,
                              totalNacks: null
                            }
                          },
                          video: {
                            sending: {
                              ssrc: null,
                              bytes: 0,
                              packets: 0,
                              // Should not be for sending?
                              packetsLost: 0,
                              rtt: 0,
                              // Should not be for sending?
                              jitter: 0,
                              // Should not be for sending?
                              jitterBufferMs: null,
                              codec: self._getSDPSelectedCodec(peerId, pc.remoteDescription, 'video', beSilentOnLogs),
                              frameWidth: null,
                              frameHeight: null,
                              framesDecoded: null,
                              framesCorrupted: null,
                              framesDropped: null,
                              framesPerSecond: null,
                              framesInput: null,
                              frames: null,
                              frameRateEncoded: null,
                              frameRate: null,
                              frameRateInput: null,
                              frameRateMean: null,
                              frameRateStdDev: null,
                              nacks: null,
                              plis: null,
                              firs: null,
                              slis: null,
                              qpSum: null,
                              totalBytes: 0,
                              totalPackets: 0,
                              totalPacketsLost: 0,
                              totalNacks: null,
                              totalPlis: null,
                              totalFirs: null,
                              totalSlis: null,
                              totalFrames: null
                            },
                            receiving: {
                              ssrc: null,
                              bytes: 0,
                              packets: 0,
                              packetsDiscarded: 0,
                              packetsLost: 0,
                              fractionLost: 0,
                              jitter: 0,
                              jitterBufferMs: null,
                              codec: self._getSDPSelectedCodec(peerId, pc.remoteDescription, 'video', beSilentOnLogs),
                              frameWidth: null,
                              frameHeight: null,
                              framesDecoded: null,
                              framesCorrupted: null,
                              framesPerSecond: null,
                              framesDropped: null,
                              framesOutput: null,
                              frames: null,
                              frameRateMean: null,
                              frameRateStdDev: null,
                              nacks: null,
                              plis: null,
                              firs: null,
                              slis: null,
                              e2eDelay: null,
                              totalBytes: 0,
                              totalPackets: 0,
                              totalPacketsLost: 0,
                              totalNacks: null,
                              totalPlis: null,
                              totalFirs: null,
                              totalSlis: null,
                              totalFrames: null
                            }
                          },
                          selectedCandidate: {
                            local: {
                              ipAddress: null,
                              candidateType: null,
                              portNumber: null,
                              transport: null,
                              turnMediaTransport: null
                            },
                            remote: {
                              ipAddress: null,
                              candidateType: null,
                              portNumber: null,
                              transport: null
                            },
                            consentResponses: {
                              received: null,
                              sent: null,
                              totalReceived: null,
                              totalSent: null
                            },
                            consentRequests: {
                              received: null,
                              sent: null,
                              totalReceived: null,
                              totalSent: null
                            },
                            responses: {
                              received: null,
                              sent: null,
                              totalReceived: null,
                              totalSent: null
                            },
                            requests: {
                              received: null,
                              sent: null,
                              totalReceived: null,
                              totalSent: null
                            }
                          },
                          certificate: {
                            local: self._getSDPFingerprint(peerId, pc.localDescription, beSilentOnLogs),
                            remote: self._getSDPFingerprint(peerId, pc.remoteDescription, beSilentOnLogs),
                            dtlsCipher: null,
                            srtpCipher: null
                          }
                        };
                      
                        if (self._dataChannels[peerId]) {
                          for (var channelProp in self._dataChannels[peerId]) {
                            if (self._dataChannels[peerId].hasOwnProperty(channelProp) && self._dataChannels[peerId][channelProp]) {
                              result.connection.dataChannels[self._dataChannels[peerId][channelProp].channel.label] = {
                                label: self._dataChannels[peerId][channelProp].channel.label,
                                readyState: self._dataChannels[peerId][channelProp].channel.readyState,
                                channelType: channelProp === 'main' ? self.DATA_CHANNEL_TYPE.MESSAGING : self.DATA_CHANNEL_TYPE.DATA,
                                currentTransferId: self._dataChannels[peerId][channelProp].transferId || null,
                                currentStreamId: self._dataChannels[peerId][channelProp].streamId || null
                              };
                            }
                          }
                        }
                      
                        var loopFn = function (obj, fn) {
                          for (var prop in obj) {
                            if (obj.hasOwnProperty(prop) && obj[prop]) {
                              fn(obj[prop], prop);
                            }
                          }
                        };
                      
                        var formatCandidateFn = function (candidateDirType, candidate) {
                          result.selectedCandidate[candidateDirType].ipAddress = candidate.ipAddress;
                          result.selectedCandidate[candidateDirType].candidateType = candidate.candidateType;
                          result.selectedCandidate[candidateDirType].portNumber = typeof candidate.portNumber !== 'number' ?
                            parseInt(candidate.portNumber, 10) || null : candidate.portNumber;
                          result.selectedCandidate[candidateDirType].transport = candidate.transport;
                        };
                      
                        pc.getStats(null, function (stats) {
                          if (!beSilentOnLogs) {
                            log.debug([peerId, 'RTCStatsReport', null, 'Retrieval success ->'], stats);
                          }
                      
                          if (isAutoBwStats ? !self._peerStats[peerId] : !self._peerBandwidth[peerId]) {
                            callback(new Error('Peer connection stats object is not defined.', null));
                            return;
                          }
                      
                          result.raw = stats;
                      
                          if (window.webrtcDetectedBrowser === 'firefox') {
                            loopFn(stats, function (obj, prop) {
                              var dirType = '';
                      
                              // Receiving/Sending RTP packets
                              if (prop.indexOf('inbound_rtp') === 0 || prop.indexOf('outbound_rtp') === 0) {
                                dirType = prop.indexOf('inbound_rtp') === 0 ? 'receiving' : 'sending';
                      
                                if (isAutoBwStats) {
                                  if (!self._peerBandwidth[peerId][prop]) {
                                    self._peerBandwidth[peerId][prop] = obj;
                                  }
                                } else if (!self._peerStats[peerId][prop]) {
                                  self._peerStats[peerId][prop] = obj;
                                }
                      
                                result[obj.mediaType][dirType].bytes = self._parseConnectionStats(
                                  isAutoBwStats ? self._peerBandwidth[peerId][prop] : self._peerStats[peerId][prop],
                                  obj, dirType === 'receiving' ? 'bytesReceived' : 'bytesSent');
                                result[obj.mediaType][dirType].totalBytes = parseInt(
                                  (dirType === 'receiving' ? obj.bytesReceived : obj.bytesSent) || '0', 10);
                                result[obj.mediaType][dirType].packets = self._parseConnectionStats(
                                  isAutoBwStats ? self._peerBandwidth[peerId][prop] : self._peerStats[peerId][prop],
                                  obj, dirType === 'receiving' ? 'packetsReceived' : 'packetsSent');
                                result[obj.mediaType][dirType].totalPackets = parseInt(
                                  (dirType === 'receiving' ? obj.packetsReceived : obj.packetsSent) || '0', 10);
                                result[obj.mediaType][dirType].ssrc = obj.ssrc;
                      
                                if (obj.mediaType === 'video') {
                                  result.video[dirType].frameRateMean = obj.framerateMean || 0;
                                  result.video[dirType].frameRateStdDev = obj.framerateStdDev || 0;
                                  result.video[dirType].framesDropped = typeof obj.framesDropped === 'number' ? obj.framesDropped :
                                    (typeof obj.droppedFrames === 'number' ? obj.droppedFrames : null);
                                  result.video[dirType].framesCorrupted = typeof obj.framesCorrupted === 'number' ? obj.framesCorrupted : null;
                                  result.video[dirType].framesPerSecond = typeof obj.framesPerSecond === 'number' ? obj.framesPerSecond : null;
                      
                                  if (dirType === 'sending') {
                                    result.video[dirType].framesEncoded = typeof obj.framesEncoded === 'number' ? obj.framesEncoded : null;
                                    result.video[dirType].frames = typeof obj.framesSent === 'number' ? obj.framesSent : null;
                                  } else {
                                    result.video[dirType].framesDecoded = typeof obj.framesDecoded === 'number' ? obj.framesDecoded : null;
                                    result.video[dirType].frames = typeof obj.framesReceived === 'number' ? obj.framesReceived : null;
                                  }
                                }
                      
                                if (dirType === 'receiving') {
                                  obj.packetsDiscarded = (typeof obj.packetsDiscarded === 'number' ? obj.packetsDiscarded :
                                    obj.discardedPackets) || 0;
                                  obj.packetsLost = typeof obj.packetsLost === 'number' ? obj.packetsLost : 0;
                      
                                  result[obj.mediaType].receiving.packetsLost = self._parseConnectionStats(
                                    isAutoBwStats ? self._peerBandwidth[peerId][prop] : self._peerStats[peerId][prop],
                                    obj, 'packetsLost');
                                  result[obj.mediaType].receiving.packetsDiscarded = self._parseConnectionStats(
                                    isAutoBwStats ? self._peerBandwidth[peerId][prop] : self._peerStats[peerId][prop],
                                    obj, 'packetsDiscarded');
                                  result[obj.mediaType].receiving.totalPacketsDiscarded = obj.packetsDiscarded;
                                  result[obj.mediaType].receiving.totalPacketsLost = obj.packetsLost;
                                }
                      
                                if (isAutoBwStats) {
                                  self._peerBandwidth[peerId][prop] = obj;
                                } else if (!self._peerStats[peerId][prop]) {
                                  self._peerStats[peerId][prop] = obj;
                                }
                      
                              // Sending RTP packets lost
                              } else if (prop.indexOf('inbound_rtcp') === 0 || prop.indexOf('outbound_rtcp') === 0) {
                                dirType = prop.indexOf('inbound_rtp') === 0 ? 'receiving' : 'sending';
                      
                                if (isAutoBwStats) {
                                  if (!self._peerBandwidth[peerId][prop]) {
                                    self._peerBandwidth[peerId][prop] = obj;
                                  }
                                } else if (!self._peerStats[peerId][prop]) {
                                  self._peerStats[peerId][prop] = obj;
                                }
                      
                                if (dirType === 'sending') {
                                  result[obj.mediaType].sending.rtt = obj.mozRtt || 0;
                                  result[obj.mediaType].sending.targetBitrate = typeof obj.targetBitrate === 'number' ? obj.targetBitrate : 0;
                                } else {
                                  result[obj.mediaType].receiving.jitter = obj.jitter || 0;
                                }
                      
                                if (isAutoBwStats) {
                                  self._peerBandwidth[peerId][prop] = obj;
                                } else if (!self._peerStats[peerId][prop]) {
                                  self._peerStats[peerId][prop] = obj;
                                }
                      
                              // Candidates
                              } else if (obj.nominated && obj.selected) {
                                formatCandidateFn('remote', stats[obj.remoteCandidateId]);
                                formatCandidateFn('local', stats[obj.localCandidateId]);
                              }
                            });
                      
                          } else if (window.webrtcDetectedBrowser === 'edge') {
                            var tracks = [];
                      
                            if (pc.getRemoteStreams().length > 0) {
                              tracks = tracks.concat(pc.getRemoteStreams()[0].getTracks());
                            }
                      
                            if (pc.getLocalStreams().length > 0) {
                              tracks = tracks.concat(pc.getLocalStreams()[0].getTracks());
                            }
                      
                            loopFn(tracks, function (track) {
                              loopFn(stats, function (obj, prop) {
                                if (obj.type === 'track' && obj.trackIdentifier === track.id) {
                                  var dirType = obj.remoteSource ? 'receiving' : 'sending';
                                  var mediaType = track.kind;
                      
                                  if (mediaType === 'audio') {
                                    result[mediaType][dirType][dirType === 'sending' ? 'inputLevel' : 'outputLevel'] = obj.audioLevel;
                                    if (dirType === 'sending') {
                                      result[mediaType][dirType].echoReturnLoss = obj.echoReturnLoss;
                                      result[mediaType][dirType].echoReturnLossEnhancement = obj.echoReturnLossEnhancement;
                                    }
                                  } else {
                                    result[mediaType][dirType].frames = self._parseConnectionStats(
                                      isAutoBwStats ? self._peerBandwidth[peerId][prop] : self._peerStats[peerId][prop],
                                      obj,dirType === 'sending' ? obj.framesSent : obj.framesReceived);
                                    result[mediaType][dirType].framesDropped = obj.framesDropped;
                                    result[mediaType][dirType].framesDecoded = obj.framesDecoded;
                                    result[mediaType][dirType].framesCorrupted = obj.framesCorrupted;
                                    result[mediaType][dirType].framesPerSecond = obj.framesPerSecond;
                                    result[mediaType][dirType].frameHeight = obj.frameHeight || null;
                                    result[mediaType][dirType].frameWidth = obj.frameWidth || null;
                                    result[mediaType][dirType].totalFrames = dirType === 'sending' ? obj.framesSent : obj.framesReceived;
                                  }
                      
                                  loopFn(stats, function (streamObj, subprop) {
                                    if (streamObj.mediaTrackId === obj.id && ['outboundrtp', 'inboundrtp'].indexOf(streamObj.type) > -1) {
                                      if (isAutoBwStats) {
                                        if (!self._peerBandwidth[peerId][subprop]) {
                                          self._peerBandwidth[peerId][subprop] = streamObj;
                                        }
                                      } else if (!self._peerStats[peerId][subprop]) {
                                        self._peerStats[peerId][subprop] = streamObj;
                                      }
                      
                                      result[mediaType][dirType].ssrc = parseInt(streamObj.ssrc || '0', 10);
                                      result[mediaType][dirType].nacks = self._parseConnectionStats(
                                        isAutoBwStats ? self._peerBandwidth[peerId][subprop] : self._peerStats[peerId][subprop],
                                        streamObj, 'nackCount');
                                      result[mediaType][dirType].totalNacks = streamObj.nackCount;
                      
                                      if (mediaType === 'video') {
                                        result[mediaType][dirType].firs = self._parseConnectionStats(
                                          isAutoBwStats ? self._peerBandwidth[peerId][subprop] : self._peerStats[peerId][subprop],
                                          streamObj, 'firCount');
                                        result[mediaType][dirType].plis = self._parseConnectionStats(
                                          isAutoBwStats ? self._peerBandwidth[peerId][subprop] : self._peerStats[peerId][subprop],
                                          streamObj, 'pliCount');
                                        result[mediaType][dirType].slis = self._parseConnectionStats(
                                          isAutoBwStats ? self._peerBandwidth[peerId][subprop] : self._peerStats[peerId][subprop],
                                          streamObj, 'sliCount');
                                        result[mediaType][dirType].totalFirs = streamObj.firCount;
                                        result[mediaType][dirType].totalPlis = streamObj.plisCount;
                                        result[mediaType][dirType].totalSlis = streamObj.sliCount;
                                      }
                      
                                      result[mediaType][dirType].bytes = self._parseConnectionStats(
                                        isAutoBwStats ? self._peerBandwidth[peerId][subprop] : self._peerStats[peerId][subprop],
                                        streamObj, dirType === 'receiving' ? 'bytesReceived' : 'bytesSent');
                                      result[mediaType][dirType].packets = self._parseConnectionStats(
                                        isAutoBwStats ? self._peerBandwidth[peerId][subprop] : self._peerStats[peerId][subprop],
                                        streamObj, dirType === 'receiving' ? 'packetsReceived' : 'packetsSent');
                      
                                      result[mediaType][dirType].totalBytes = dirType === 'receiving' ? streamObj.bytesReceived : streamObj.bytesSent;
                                      result[mediaType][dirType].totalPackets = dirType === 'receiving' ? streamObj.packetsReceived : streamObj.packetsSent;
                      
                                      if (dirType === 'receiving') {
                                        result[mediaType][dirType].jitter = streamObj.jitter || 0;
                                        result[mediaType].receiving.fractionLost = streamObj.fractionLost;
                                        result[mediaType][dirType].packetsLost = self._parseConnectionStats(
                                          isAutoBwStats ? self._peerBandwidth[peerId][subprop] : self._peerStats[peerId][subprop],
                                          streamObj, 'packetsLost');
                                        result[mediaType][dirType].packetsDiscarded = self._parseConnectionStats(
                                          isAutoBwStats ? self._peerBandwidth[peerId][subprop] : self._peerStats[peerId][subprop],
                                          streamObj, 'packetsDiscarded');
                                        result[mediaType][dirType].totalPacketsLost = streamObj.packetsLost;
                                        result[mediaType][dirType].totalPacketsDiscarded = streamObj.packetsDiscarded || 0;
                                      } else {
                                        result[mediaType].sending.rtt = streamObj.roundTripTime || 0;
                                        result[mediaType].sending.targetBitrate = streamObj.targetBitrate || 0;
                                      }
                      
                                      if (result[mediaType][dirType].codec && streamObj.codecId) {
                                        result[mediaType][dirType].codec.name = streamObj.codecId;
                                      }
                                    }
                                  });
                                }
                              });
                            });
                      
                          } else {
                            var reportedCandidate = false;
                            var reportedCertificate = false;
                      
                            loopFn(stats, function (obj, prop) {
                              if (prop.indexOf('ssrc_') === 0) {
                                var dirType = prop.indexOf('_recv') > 0 ? 'receiving' : 'sending';
                      
                                // Polyfill fix for plugin. Plugin should fix this though
                                if (!obj.mediaType) {
                                  obj.mediaType = obj.hasOwnProperty('audioOutputLevel') || obj.hasOwnProperty('audioInputLevel') ||
                                    obj.hasOwnProperty('googEchoCancellationReturnLoss') || obj.hasOwnProperty('googEchoCancellation') ?
                                    'audio' : 'video';
                                }
                      
                                if (isAutoBwStats) {
                                  if (!self._peerBandwidth[peerId][prop]) {
                                    self._peerBandwidth[peerId][prop] = obj;
                                  }
                                } else if (!self._peerStats[peerId][prop]) {
                                  self._peerStats[peerId][prop] = obj;
                                }
                      
                                // Capture e2e delay
                                try {
                                  if (obj.mediaType === 'video' && dirType === 'receiving') {
                                    var captureStartNtpTimeMs = parseInt(obj.googCaptureStartNtpTimeMs || '0', 10);
                      
                                    if (captureStartNtpTimeMs > 0 && pc.getRemoteStreams().length > 0 && document &&
                                      typeof document.getElementsByTagName === 'function') {
                                      var streamId = pc.getRemoteStreams()[0].id || pc.getRemoteStreams()[0].label;
                                      var elements = [];
                      
                                      if (self._isUsingPlugin) {
                                        elements = document.getElementsByTagName('object');
                                      } else {
                                        elements = document.getElementsByTagName('video');
                      
                                        if (elements.length === 0) {
                                          elements = document.getElementsByTagName('audio');
                                        }
                                      }
                      
                                      for (var e = 0; e < elements.length; e++) {
                                        var videoElmStreamId = null;
                      
                                        if (self._isUsingPlugin) {
                                          if (!(elements[e].children && typeof elements[e].children === 'object' &&
                                            typeof elements[e].children.length === 'number' && elements[e].children.length > 0)) {
                                            break;
                                          }
                      
                                          for (var ec = 0; ec < elements[e].children.length; ec++) {
                                            if (elements[e].children[ec].name === 'streamId') {
                                              videoElmStreamId = elements[e].children[ec].value || null;
                                              break;
                                            }
                                          }
                      
                                        } else {
                                          videoElmStreamId = elements[e].srcObject ? elements[e].srcObject.id ||
                                            elements[e].srcObject.label : null;
                                        }
                      
                                        if (videoElmStreamId && videoElmStreamId === streamId) {
                                          result[obj.mediaType][dirType].e2eDelay = ((new Date()).getTime() + 2208988800000) -
                                            captureStartNtpTimeMs - elements[e].currentTime * 1000;
                                          break;
                                        }
                                      }
                                    }
                                  }
                                } catch (error) {
                                  if (!beSilentOnLogs) {
                                    log.warn([peerId, 'RTCStatsReport', null, 'Failed retrieving e2e delay ->'], error);
                                  }
                                }
                      
                                // Receiving/Sending RTP packets
                                result[obj.mediaType][dirType].ssrc = parseInt(obj.ssrc || '0', 10);
                                result[obj.mediaType][dirType].bytes = self._parseConnectionStats(
                                  isAutoBwStats ? self._peerBandwidth[peerId][prop] : self._peerStats[peerId][prop],
                                  obj, dirType === 'receiving' ? 'bytesReceived' : 'bytesSent');
                                result[obj.mediaType][dirType].packets = self._parseConnectionStats(
                                  isAutoBwStats ? self._peerBandwidth[peerId][prop] : self._peerStats[peerId][prop],
                                  obj, dirType === 'receiving' ? 'packetsReceived' : 'packetsSent');
                                result[obj.mediaType][dirType].nacks = self._parseConnectionStats(
                                  isAutoBwStats ? self._peerBandwidth[peerId][prop] : self._peerStats[peerId][prop],
                                  obj, dirType === 'receiving' ? 'googNacksReceived' : 'googNacksSent');
                                result[obj.mediaType][dirType].totalPackets = parseInt((dirType === 'receiving' ? obj.packetsReceived :
                                  obj.packetsSent) || '0', 10);
                                result[obj.mediaType][dirType].totalBytes = parseInt((dirType === 'receiving' ? obj.bytesReceived :
                                  obj.bytesSent) || '0', 10);
                                result[obj.mediaType][dirType].totalNacks = parseInt((dirType === 'receiving' ? obj.googNacksReceived :
                                  obj.googNacksSent) || '0', 10);
                      
                                if (result[obj.mediaType][dirType].codec) {
                                  if (obj.googCodecName && obj.googCodecName !== 'unknown') {
                                    result[obj.mediaType][dirType].codec.name = obj.googCodecName;
                                  }
                                  if (obj.codecImplementationName && obj.codecImplementationName !== 'unknown') {
                                    result[obj.mediaType][dirType].codec.implementation = obj.codecImplementationName;
                                  }
                                }
                      
                                if (dirType === 'sending') {
                                  // NOTE: Chrome sending audio does have it but plugin has..
                                  result[obj.mediaType].sending.rtt = parseFloat(obj.googRtt || '0', 10);
                                  result[obj.mediaType].sending.targetBitrate = obj.targetBitrate ? parseInt(obj.targetBitrate, 10) : null;
                                } else {
                                  result[obj.mediaType].receiving.packetsLost = self._parseConnectionStats(
                                    isAutoBwStats ? self._peerBandwidth[peerId][prop] : self._peerStats[peerId][prop],
                                    obj, 'packetsLost');
                                  result[obj.mediaType].receiving.packetsDiscarded = self._parseConnectionStats(
                                    isAutoBwStats ? self._peerBandwidth[peerId][prop] : self._peerStats[peerId][prop],
                                    obj, 'packetsDiscarded');
                                  result[obj.mediaType].receiving.jitter = parseFloat(obj.googJitterReceived || '0', 10);
                                  result[obj.mediaType].receiving.jitterBufferMs = obj.googJitterBufferMs ? parseFloat(obj.googJitterBufferMs || '0', 10) : null;
                                  result[obj.mediaType].receiving.totalPacketsLost = parseInt(obj.packetsLost || '0', 10);
                                  result[obj.mediaType].receiving.totalPacketsDiscarded = parseInt(obj.packetsDiscarded || '0', 10);
                                }
                      
                                if (obj.mediaType === 'video') {
                                  result.video[dirType].framesCorrupted = obj.framesCorrupted ? parseInt(obj.framesCorrupted, 10) : null;
                                  result.video[dirType].framesPerSecond = obj.framesPerSecond ? parseFloat(obj.framesPerSecond, 10) : null;
                                  result.video[dirType].framesDropped = obj.framesDropped ? parseInt(obj.framesDropped, 10) : null;
                      
                                  if (dirType === 'sending') {
                                    result.video[dirType].frameWidth = obj.googFrameWidthSent ?
                                      parseInt(obj.googFrameWidthSent, 10) : null;
                                    result.video[dirType].frameHeight = obj.googFrameHeightSent ?
                                      parseInt(obj.googFrameHeightSent, 10) : null;
                                    result.video[dirType].plis = obj.googPlisSent ?
                                      self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][prop] :
                                      self._peerStats[peerId][prop], obj, 'googPlisSent') : null;
                                    result.video[dirType].firs = obj.googFirsSent ?
                                      self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][prop] :
                                      self._peerStats[peerId][prop], obj, 'googFirsSent') : null;
                                    result[obj.mediaType][dirType].totalPlis = obj.googPlisSent ? parseInt(obj.googPlisSent, 10) : null;
                                    result[obj.mediaType][dirType].totalFirs = obj.googFirsSent ? parseInt(obj.googFirsSent, 10) : null;
                                    result.video[dirType].framesEncoded = obj.framesEncoded ? parseInt(obj.framesEncoded, 10) : null;
                                    result.video[dirType].frameRateEncoded = obj.googFrameRateEncoded ?
                                      parseInt(obj.googFrameRateEncoded, 10) : null;
                                    result.video[dirType].frameRateInput = obj.googFrameRateInput ?
                                      parseInt(obj.googFrameRateInput, 10) : null;
                                    result.video[dirType].frameRate = obj.googFrameRateSent ?
                                      parseInt(obj.googFrameRateSent, 10) : null;
                                    result.video[dirType].qpSum = obj.qpSum ? parseInt(obj.qpSum, 10) : null;
                                    result.video[dirType].frames = obj.framesSent ?
                                      self._parseConnectionStats(isAutoBwStats ? self._peerStats[peerId][prop] :
                                      self._peerStats[peerId][prop], obj, 'framesSent') : null;
                                    result.video[dirType].totalFrames = obj.framesSent ? parseInt(obj.framesSent, 10) : null;
                                  } else {
                                    result.video[dirType].frameWidth = obj.googFrameWidthReceived ?
                                      parseInt(obj.googFrameWidthReceived, 10) : null;
                                    result.video[dirType].frameHeight = obj.googFrameHeightReceived ?
                                      parseInt(obj.googFrameHeightReceived, 10) : null;
                                    result.video[dirType].plis = obj.googPlisReceived ?
                                      self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][prop] :
                                      self._peerStats[peerId][prop], obj, 'googPlisReceived') : null;
                                    result.video[dirType].firs = obj.googFirsReceived ?
                                      self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][prop] :
                                      self._peerStats[peerId][prop], obj, 'googFirsReceived') : null;
                                    result[obj.mediaType][dirType].totalPlis = obj.googPlisReceived ? parseInt(obj.googPlisReceived, 10) : null;
                                    result[obj.mediaType][dirType].totalFirs = obj.googFirsReceived ? parseInt(obj.googFirsReceived, 10) : null;
                                    result.video[dirType].framesDecoded = obj.framesDecoded ? parseInt(obj.framesDecoded, 10) : null;
                                    result.video[dirType].frameRateDecoded = obj.googFrameRateDecoded ?
                                      parseInt(obj.googFrameRateDecoded, 10) : null;
                                    result.video[dirType].frameRateOutput = obj.googFrameRateOutput ?
                                      parseInt(obj.googFrameRateOutput, 10) : null;
                                    result.video[dirType].frameRate = obj.googFrameRateReceived ?
                                      parseInt(obj.googFrameRateReceived, 10) : null;
                                    result.video[dirType].frames = obj.framesReceived ?
                                      self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][prop] :
                                      self._peerStats[peerId][prop], obj, 'framesReceived') : null;
                                    result.video[dirType].totalFrames = obj.framesReceived ? parseInt(obj.framesReceived, 10) : null;
                                  }
                                } else {
                                  if (dirType === 'receiving') {
                                    result.audio[dirType].outputLevel = parseFloat(obj.audioOutputLevel || '0', 10);
                                  } else {
                                    result.audio[dirType].inputLevel = parseFloat(obj.audioInputLevel || '0', 10);
                                    result.audio[dirType].echoReturnLoss = parseFloat(obj.googEchoCancellationReturnLoss || '0', 10);
                                    result.audio[dirType].echoReturnLossEnhancement = parseFloat(obj.googEchoCancellationReturnLossEnhancement || '0', 10);
                                  }
                                }
                      
                                if (isAutoBwStats) {
                                  self._peerBandwidth[peerId][prop] = obj;
                                } else if (!self._peerStats[peerId][prop]) {
                                  self._peerStats[peerId][prop] = obj;
                                }
                      
                                if (!reportedCandidate) {
                                  loopFn(stats, function (canObj, canProp) {
                                    if (!reportedCandidate && canProp.indexOf('Conn-') === 0) {
                                      if (obj.transportId === canObj.googChannelId) {
                                        if (isAutoBwStats) {
                                          if (!self._peerBandwidth[peerId][canProp]) {
                                            self._peerBandwidth[peerId][canProp] = canObj;
                                          }
                                        } else if (!self._peerStats[peerId][canProp]) {
                                          self._peerStats[peerId][canProp] = canObj;
                                        }
                      
                                        formatCandidateFn('local', stats[canObj.localCandidateId]);
                                        formatCandidateFn('remote', stats[canObj.remoteCandidateId]);
                                        result.selectedCandidate.writable = canObj.googWritable ? canObj.googWritable === 'true' : null;
                                        result.selectedCandidate.readable = canObj.googReadable ? canObj.googReadable === 'true' : null;
                                        result.selectedCandidate.rtt = canObj.googRtt ?
                                          self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][canProp] :
                                          self._peerStats[peerId][canProp], canObj, 'googRtt') : null;
                                        result.selectedCandidate.totalRtt = canObj.googRtt ? parseInt(canObj.googRtt, 10) : null;
                                        result.selectedCandidate.requests = {
                                          received: canObj.requestsReceived ?
                                            self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][canProp] :
                                            self._peerStats[peerId][canProp], canObj, 'requestsReceived') : null,
                                          sent: canObj.requestsSent ?
                                            self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][canProp] :
                                            self._peerStats[peerId][canProp], canObj, 'requestsSent') : null,
                                          totalReceived: canObj.requestsReceived ? parseInt(canObj.requestsReceived, 10) : null,
                                          totalSent: canObj.requestsSent ? parseInt(canObj.requestsSent, 10) : null
                                        };
                                        result.selectedCandidate.responses = {
                                          received: canObj.responsesReceived ?
                                            self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][canProp] :
                                            self._peerStats[peerId][canProp], canObj, 'responsesReceived') : null,
                                          sent: canObj.responsesSent ?
                                            self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][canProp] :
                                            self._peerStats[peerId][canProp], canObj, 'responsesSent') : null,
                                          totalReceived: canObj.responsesReceived ? parseInt(canObj.responsesReceived, 10) : null,
                                          totalSent: canObj.responsesSent ? parseInt(canObj.responsesSent, 10) : null
                                        };
                                        result.selectedCandidate.consentRequests = {
                                          received: canObj.consentRequestsReceived ?
                                            self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][canProp] :
                                            self._peerStats[peerId][canProp], canObj, 'consentRequestsReceived') : null,
                                          sent: canObj.consentRequestsSent ?
                                            self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][canProp] :
                                            self._peerStats[peerId][canProp], canObj, 'consentRequestsSent') : null,
                                          totalReceived: canObj.consentRequestsReceived ? parseInt(canObj.consentRequestsReceived, 10) : null,
                                          totalSent: canObj.consentRequestsSent ? parseInt(canObj.consentRequestsSent, 10) : null
                                        };
                                        result.selectedCandidate.consentResponses = {
                                          received: canObj.consentResponsesReceived ?
                                            self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][canProp] :
                                            self._peerStats[peerId][canProp], canObj, 'consentResponsesReceived') : null,
                                          sent: canObj.consentResponsesSent ?
                                            self._parseConnectionStats(isAutoBwStats ? self._peerBandwidth[peerId][canProp] :
                                            self._peerStats[peerId][canProp], canObj, 'consentResponsesSent') : null,
                                          totalReceived: canObj.consentResponsesReceived ? parseInt(canObj.consentResponsesReceived, 10) : null,
                                          totalSent: canObj.consentResponsesSent ? parseInt(canObj.consentResponsesSent, 10) : null
                                        };
                      
                                        if (isAutoBwStats) {
                                          if (!self._peerBandwidth[peerId][canProp]) {
                                            self._peerBandwidth[peerId][canProp] = canObj;
                                          }
                                        } else if (!self._peerStats[peerId][canProp]) {
                                          self._peerStats[peerId][canProp] = canObj;
                                        }
                                        reportedCandidate = true;
                                      }
                                    }
                                  });
                                }
                      
                                if (!reportedCertificate && stats[obj.transportId]) {
                                  result.certificate.srtpCipher = stats[obj.transportId].srtpCipher || null;
                                  result.certificate.dtlsCipher = stats[obj.transportId].dtlsCipher || null;
                      
                                  var localCertId = stats[obj.transportId].localCertificateId;
                                  var remoteCertId = stats[obj.transportId].remoteCertificateId;
                      
                                  if (localCertId && stats[localCertId]) {
                                    result.certificate.local.derBase64 = stats[localCertId].googDerBase64 || null;
                                    if (stats[localCertId].googFingerprint) {
                                      result.certificate.local.fingerprint = stats[localCertId].googFingerprint;
                                    }
                                    if (stats[localCertId].googFingerprintAlgorithm) {
                                      result.certificate.local.fingerprintAlgorithm = stats[localCertId].googFingerprintAlgorithm;
                                    }
                                  }
                      
                                  if (remoteCertId && stats[remoteCertId]) {
                                    result.certificate.remote.derBase64 = stats[remoteCertId].googDerBase64 || null;
                                    if (stats[remoteCertId].googFingerprint) {
                                      result.certificate.remote.fingerprint = stats[remoteCertId].googFingerprint;
                                    }
                                    if (stats[remoteCertId].googFingerprintAlgorithm) {
                                      result.certificate.remote.fingerprintAlgorithm = stats[remoteCertId].googFingerprintAlgorithm;
                                    }
                                  }
                                  reportedCertificate = true;
                                }
                              }
                            });
                          }
                      
                          if ((result.selectedCandidate.local.candidateType || '').indexOf('relay') === 0) {
                            result.selectedCandidate.local.turnMediaTransport = 'UDP';
                            if (self._forceTURNSSL && window.webrtcDetectedBrowser !== 'firefox') {
                              result.selectedCandidate.local.turnMediaTransport = 'TCP/TLS';
                            } else if ((self._TURNTransport === self.TURN_TRANSPORT.TCP || self._forceTURNSSL) &&
                              self._room && self._room.connection && self._room.connection.peerConfig &&
                              Array.isArray(self._room.connection.peerConfig.iceServers) &&
                              self._room.connection.peerConfig.iceServers[0] &&
                              self._room.connection.peerConfig.iceServers[0].urls[0] &&
                              self._room.connection.peerConfig.iceServers[0].urls[0].indexOf('?transport=tcp') > 0) {
                              result.selectedCandidate.local.turnMediaTransport = 'TCP';
                            }
                          } else {
                            result.selectedCandidate.local.turnMediaTransport = null;
                          }
                      
                          callback(null, result);
                      
                        }, function (error) {
                          if (!beSilentOnLogs) {
                            log.error([peerId, 'RTCStatsReport', null, 'Failed retrieving stats ->'], error);
                          }
                          callback(error, null);
                        });
                      };
                      
                      /**
                       * Function that starts the Peer connection session.
                       * Remember to remove previous method of reconnection (re-creating the Peer connection - destroy and create connection).
                       * @method _addPeer
                       * @private
                       * @for Skylink
                       * @since 0.5.4
                       */
                      Skylink.prototype._addPeer = function(targetMid, cert, peerBrowser, receiveOnly, isSS) {
                        var self = this;
                        if (self._peerConnections[targetMid]) {
                          log.error([targetMid, null, null, 'Connection to peer has already been made']);
                          return;
                        }
                      
                        self._peerConnStatus[targetMid] = {
                          connected: false,
                          init: false
                        };
                      
                        log.log([targetMid, null, null, 'Starting the connection to peer. Options provided:'], {
                          peerBrowser: peerBrowser,
                          receiveOnly: receiveOnly,
                          enableDataChannel: self._enableDataChannel
                        });
                      
                        log.info('Adding peer', isSS);
                      
                        self._peerConnections[targetMid] = self._createPeerConnection(targetMid, !!isSS, cert);
                      
                        if (!self._peerConnections[targetMid]) {
                          log.error([targetMid, null, null, 'Failed creating the connection to peer.']);
                          return;
                        }
                      
                        self._peerConnStatus[targetMid].init = true;
                        self._peerConnections[targetMid].hasScreen = !!isSS;
                      };
                      
                      /**
                       * Function that re-negotiates a Peer connection.
                       * Remember to remove previous method of reconnection (re-creating the Peer connection - destroy and create connection).
                       * @method _restartPeerConnection
                       * @private
                       * @for Skylink
                       * @since 0.5.8
                       */
                      Skylink.prototype._restartPeerConnection = function (peerId, doIceRestart, bwOptions, callback) {
                        var self = this;
                      
                        if (!self._peerConnections[peerId]) {
                          log.error([peerId, null, null, 'Peer does not have an existing ' +
                            'connection. Unable to restart']);
                          return;
                        }
                      
                        var pc = self._peerConnections[peerId];
                        var agent = (self.getPeerInfo(peerId) || {}).agent || {};
                      
                        // prevent restarts for other SDK clients
                        if (self._isLowerThanVersion(agent.SMProtocolVersion || '', '0.1.2')) {
                          var notSupportedError = new Error('Failed restarting with other agents connecting from other SDKs as ' +
                            're-negotiation is not supported by other SDKs');
                      
                          log.warn([peerId, 'RTCPeerConnection', null, 'Ignoring restart request as agent\'s SDK does not support it'],
                              notSupportedError);
                      
                          if (typeof callback === 'function') {
                            log.debug([peerId, 'RTCPeerConnection', null, 'Firing restart failure callback']);
                            callback(notSupportedError);
                          }
                          return;
                        }
                      
                        // This is when the state is stable and re-handshaking is possible
                        // This could be due to previous connection handshaking that is already done
                        if (pc.signalingState === self.PEER_CONNECTION_STATE.STABLE && self._peerConnections[peerId]) {
                          log.log([peerId, null, null, 'Sending restart message to signaling server ->'], {
                            iceRestart: doIceRestart,
                            options: bwOptions
                          });
                      
                          self._peerCustomConfigs[peerId] = self._peerCustomConfigs[peerId] || {};
                          self._peerCustomConfigs[peerId].bandwidth = self._peerCustomConfigs[peerId].bandwidth || {};
                          self._peerCustomConfigs[peerId].googleXBandwidth = self._peerCustomConfigs[peerId].googleXBandwidth || {};
                      
                          if (bwOptions.bandwidth && typeof bwOptions.bandwidth === 'object') {
                            if (typeof bwOptions.bandwidth.audio === 'number') {
                              self._peerCustomConfigs[peerId].bandwidth.audio = bwOptions.bandwidth.audio;
                            }
                            if (typeof bwOptions.bandwidth.video === 'number') {
                              self._peerCustomConfigs[peerId].bandwidth.video = bwOptions.bandwidth.video;
                            }
                            if (typeof bwOptions.bandwidth.data === 'number') {
                              self._peerCustomConfigs[peerId].bandwidth.data = bwOptions.bandwidth.data;
                            }
                          }
                      
                          if (bwOptions.googleXBandwidth && typeof bwOptions.googleXBandwidth === 'object') {
                            if (typeof bwOptions.googleXBandwidth.min === 'number') {
                              self._peerCustomConfigs[peerId].googleXBandwidth.min = bwOptions.googleXBandwidth.min;
                            }
                            if (typeof bwOptions.googleXBandwidth.max === 'number') {
                              self._peerCustomConfigs[peerId].googleXBandwidth.max = bwOptions.googleXBandwidth.max;
                            }
                          }
                      
                          var restartMsg = {
                            type: self._SIG_MESSAGE_TYPE.RESTART,
                            mid: self._user.sid,
                            rid: self._room.id,
                            agent: window.webrtcDetectedBrowser,
                            version: (window.webrtcDetectedVersion || 0).toString(),
                            os: window.navigator.platform,
                            userInfo: self._getUserInfo(peerId),
                            target: peerId,
                            weight: self._peerPriorityWeight,
                            receiveOnly: self.getPeerInfo().config.receiveOnly,
                            enableIceTrickle: self._enableIceTrickle,
                            enableDataChannel: self._enableDataChannel,
                            enableIceRestart: self._enableIceRestart,
                            doIceRestart: doIceRestart === true && self._enableIceRestart && self._peerInformations[peerId] &&
                              self._peerInformations[peerId].config.enableIceRestart,
                            isRestartResend: false,
                            temasysPluginVersion: AdapterJS.WebRTCPlugin.plugin ? AdapterJS.WebRTCPlugin.plugin.VERSION : null,
                            SMProtocolVersion: self.SM_PROTOCOL_VERSION,
                            DTProtocolVersion: self.DT_PROTOCOL_VERSION
                          };
                      
                          if (self._publishOnly) {
                            restartMsg.publishOnly = {
                              type: self._streams.screenshare && self._streams.screenshare.stream ? 'screenshare' : 'video'
                            };
                          }
                      
                          if (self._parentId) {
                            restartMsg.parentId = self._parentId;
                          }
                      
                          self._peerEndOfCandidatesCounter[peerId] = self._peerEndOfCandidatesCounter[peerId] || {};
                          self._peerEndOfCandidatesCounter[peerId].len = 0;
                          self._sendChannelMessage(restartMsg);
                          self._trigger('peerRestart', peerId, self.getPeerInfo(peerId), true, doIceRestart === true);
                      
                          if (typeof callback === 'function') {
                            log.debug([peerId, 'RTCPeerConnection', null, 'Firing restart callback']);
                            callback(null);
                          }
                      
                        } else {
                          // Let's check if the signalingState is stable first.
                          // In another galaxy or universe, where the local description gets dropped..
                          // In the offerHandler or answerHandler, do the appropriate flags to ignore or drop "extra" descriptions
                          if (pc.signalingState === self.PEER_CONNECTION_STATE.HAVE_LOCAL_OFFER) {
                            // Checks if the local description is defined first
                            var hasLocalDescription = pc.localDescription && pc.localDescription.sdp;
                            // By then it should have at least the local description..
                            if (hasLocalDescription) {
                              self._sendChannelMessage({
                                type: pc.localDescription.type,
                                sdp: pc.localDescription.sdp,
                                mid: self._user.sid,
                                target: peerId,
                                rid: self._room.id,
                                restart: true
                              });
                            } else {
                              var noLocalDescriptionError = 'Failed re-sending localDescription as there is ' +
                                'no localDescription set to connection. There could be a handshaking step error';
                              log.error([peerId, 'RTCPeerConnection', null, noLocalDescriptionError], {
                                  localDescription: pc.localDescription,
                                  remoteDescription: pc.remoteDescription
                              });
                              if (typeof callback === 'function') {
                                log.debug([peerId, 'RTCPeerConnection', null, 'Firing restart failure callback']);
                                callback(new Error(noLocalDescriptionError));
                              }
                            }
                          // It could have connection state closed
                          } else {
                            var unableToRestartError = 'Failed restarting as peer connection state is ' + pc.signalingState;
                            log.warn([peerId, 'RTCPeerConnection', null, unableToRestartError]);
                            if (typeof callback === 'function') {
                              log.debug([peerId, 'RTCPeerConnection', null, 'Firing restart failure callback']);
                              callback(new Error(unableToRestartError));
                            }
                          }
                        }
                      };
                      
                      /**
                       * Function that ends the Peer connection session.
                       * @method _removePeer
                       * @private
                       * @for Skylink
                       * @since 0.5.5
                       */
                      Skylink.prototype._removePeer = function(peerId) {
                        if (!this._peerConnections[peerId] && !this._peerInformations[peerId]) {
                          log.debug([peerId, 'RTCPeerConnection', null, 'Dropping the hangup from Peer as not connected to Peer at all.']);
                          return;
                        }
                      
                        var peerInfo = clone(this.getPeerInfo(peerId)) || {
                          userData: '',
                          settings: { audio: false, video: false, data: false },
                          mediaStatus: { audioMuted: true, videoMuted: true },
                          agent: {
                            name: 'unknown',
                            version: 0,
                            os: '',
                            pluginVersion: null
                          },
                          config: {
                            enableDataChannel: true,
                            enableIceRestart: false,
                            enableIceTrickle: true,
                            priorityWeight: 0,
                            publishOnly: false,
                            receiveOnly: true
                          },
                          parentId: null,
                          room: clone(this._selectedRoom)
                        };
                      
                        if (peerId !== 'MCU') {
                          this._trigger('peerLeft', peerId, peerInfo, false);
                        } else {
                          this._hasMCU = false;
                          log.log([peerId, null, null, 'MCU has stopped listening and left']);
                          this._trigger('serverPeerLeft', peerId, this.SERVER_PEER_TYPE.MCU);
                        }
                      
                        // check if health timer exists
                        if (this._peerConnections[peerId]) {
                          if (this._peerConnections[peerId].signalingState !== this.PEER_CONNECTION_STATE.CLOSED) {
                            this._peerConnections[peerId].close();
                          }
                          if (peerId !== 'MCU') {
                            this._handleEndedStreams(peerId);
                          }
                          delete this._peerConnections[peerId];
                        }
                        // remove peer informations session
                        if (this._peerInformations[peerId]) {
                          delete this._peerInformations[peerId];
                        }
                        // remove peer messages stamps session
                        if (this._peerMessagesStamps[peerId]) {
                          delete this._peerMessagesStamps[peerId];
                        }
                        // remove peer streams session
                        if (this._streamsSession[peerId]) {
                          delete this._streamsSession[peerId];
                        }
                        // remove peer streams session
                        if (this._peerEndOfCandidatesCounter[peerId]) {
                          delete this._peerEndOfCandidatesCounter[peerId];
                        }
                        // remove peer queued ICE candidates
                        if (this._peerCandidatesQueue[peerId]) {
                          delete this._peerCandidatesQueue[peerId];
                        }
                        // remove peer sdp session
                        if (this._sdpSessions[peerId]) {
                          delete this._sdpSessions[peerId];
                        }
                        // remove peer stats session
                        if (this._peerStats[peerId]) {
                          delete this._peerStats[peerId];
                        }
                        // remove peer bandwidth stats
                        if (this._peerBandwidth[peerId]) {
                          delete this._peerBandwidth[peerId];
                        }
                        // remove peer ICE candidates
                        if (this._gatheredCandidates[peerId]) {
                          delete this._gatheredCandidates[peerId];
                        }
                        // remove peer ICE candidates
                        if (this._peerCustomConfigs[peerId]) {
                          delete this._peerCustomConfigs[peerId];
                        }
                        // remove peer connection config
                        if (this._peerConnStatus[peerId]) {
                          delete this._peerConnStatus[peerId];
                        }
                        // close datachannel connection
                        if (this._dataChannels[peerId]) {
                          this._closeDataChannel(peerId);
                        }
                        log.log([peerId, 'RTCPeerConnection', null, 'Successfully removed peer']);
                      };
                      
                      /**
                       * Function that creates the Peer connection.
                       * @method _createPeerConnection
                       * @private
                       * @for Skylink
                       * @since 0.5.1
                       */
                      Skylink.prototype._createPeerConnection = function(targetMid, isScreenSharing, cert) {
                        var pc, self = this;
                        if (!self._inRoom || !(self._room && self._room.connection &&
                          self._room.connection.peerConfig && Array.isArray(self._room.connection.peerConfig.iceServers))) {
                          return;
                        }
                      
                        var constraints = {
                          iceServers: self._room.connection.peerConfig.iceServers,
                          iceTransportPolicy: self._filterCandidatesType.host && self._filterCandidatesType.srflx &&
                            !self._filterCandidatesType.relay ? 'relay' : 'all',
                          bundlePolicy: self._peerConnectionConfig.bundlePolicy === self.BUNDLE_POLICY.NONE ?
                            self.BUNDLE_POLICY.BALANCED : self._peerConnectionConfig.bundlePolicy,
                          rtcpMuxPolicy: self._peerConnectionConfig.rtcpMuxPolicy,
                          iceCandidatePoolSize: self._peerConnectionConfig.iceCandidatePoolSize
                        };
                        var optional = {
                          optional: [
                            { DtlsSrtpKeyAgreement: true },
                            { googIPv6: true }
                          ]
                        };
                      
                        if (cert) {
                          constraints.certificates = [cert];
                        }
                      
                        if (self._peerConnStatus[targetMid]) {
                          self._peerConnStatus[targetMid].constraints = constraints;
                          self._peerConnStatus[targetMid].optional = optional;
                        }
                      
                        // currently the AdapterJS 0.12.1-2 causes an issue to prevent firefox from
                        // using .urls feature
                        try {
                          log.debug([targetMid, 'RTCPeerConnection', null, 'Creating peer connection ->'], {
                            constraints: constraints,
                            optional: optional
                          });
                          pc = new RTCPeerConnection(constraints, optional);
                        } catch (error) {
                          log.error([targetMid, null, null, 'Failed creating peer connection:'], error);
                          self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error);
                          return null;
                        }
                        // attributes (added on by Temasys)
                        pc.setOffer = '';
                        pc.setAnswer = '';
                        pc.hasStream = false;
                        pc.hasScreen = !!isScreenSharing;
                        pc.hasMainChannel = false;
                        pc.firefoxStreamId = '';
                        pc.processingLocalSDP = false;
                        pc.processingRemoteSDP = false;
                        pc.gathered = false;
                        pc.gathering = false;
                      
                        // candidates
                        self._gatheredCandidates[targetMid] = {
                          sending: { host: [], srflx: [], relay: [] },
                          receiving: { host: [], srflx: [], relay: [] }
                        };
                      
                        self._streamsSession[targetMid] = self._streamsSession[targetMid] || {};
                        self._peerEndOfCandidatesCounter[targetMid] = self._peerEndOfCandidatesCounter[targetMid] || {};
                        self._sdpSessions[targetMid] = { local: {}, remote: {} };
                        self._peerBandwidth[targetMid] = {};
                        var bandwidth = null;
                      
                        // callbacks
                        // standard not implemented: onnegotiationneeded,
                        pc.ondatachannel = function(event) {
                          var dc = event.channel || event;
                          log.debug([targetMid, 'RTCDataChannel', dc.label, 'Received datachannel ->'], dc);
                          if (self._enableDataChannel && self._peerInformations[targetMid] &&
                            self._peerInformations[targetMid].config.enableDataChannel) {
                            var channelType = self.DATA_CHANNEL_TYPE.DATA;
                            var channelKey = dc.label;
                      
                            // if peer does not have main channel, the first item is main
                            if (!pc.hasMainChannel) {
                              channelType = self.DATA_CHANNEL_TYPE.MESSAGING;
                              channelKey = 'main';
                              pc.hasMainChannel = true;
                            }
                      
                            self._createDataChannel(targetMid, dc);
                      
                          } else {
                            log.warn([targetMid, 'RTCDataChannel', dc.label, 'Not adding datachannel as enable datachannel ' +
                              'is set to false']);
                          }
                        };
                      
                        pc.onaddstream = function(event) {
                          if (!self._peerConnections[targetMid]) {
                            return;
                          }
                      
                          var stream = event.stream || event;
                          var streamId = stream.id || stream.label;
                      
                          if (targetMid === 'MCU') {
                            log.warn([targetMid, 'MediaStream', streamId, 'Ignoring received remote stream from MCU ->'], stream);
                            return;
                          } else if (!self._sdpSettings.direction.audio.receive && !self._sdpSettings.direction.video.receive) {
                            log.warn([targetMid, 'MediaStream', streamId, 'Ignoring received empty remote stream ->'], stream);
                            return;
                          }
                      
                          // Fixes for the dirty-hack for Chrome offer to Firefox (inactive)
                          // See: ESS-680
                          if (!self._hasMCU && window.webrtcDetectedBrowser === 'firefox' &&
                            pc.getRemoteStreams().length > 1 && pc.remoteDescription && pc.remoteDescription.sdp) {
                      
                            if (pc.remoteDescription.sdp.indexOf(' msid:' + streamId + ' ') === -1) {
                              log.warn([targetMid, 'MediaStream', streamId, 'Ignoring received empty remote stream ->'], stream);
                              return;
                            }
                          }
                      
                          var peerSettings = clone(self.getPeerInfo(targetMid).settings);
                          var hasScreenshare = peerSettings.video && typeof peerSettings.video === 'object' && !!peerSettings.video.screenshare;
                      
                          pc.hasStream = true;
                          pc.hasScreen = !!hasScreenshare;
                      
                          self._streamsSession[targetMid][streamId] = peerSettings;
                          self._onRemoteStreamAdded(targetMid, stream, !!hasScreenshare);
                        };
                      
                        pc.onicecandidate = function(event) {
                          self._onIceCandidate(targetMid, event.candidate || event);
                        };
                      
                        pc.oniceconnectionstatechange = function(evt) {
                          var iceConnectionState = pc.iceConnectionState;
                      
                          log.debug([targetMid, 'RTCIceConnectionState', null, 'Ice connection state changed ->'], iceConnectionState);
                      
                          if (window.webrtcDetectedBrowser === 'edge') {
                            if (iceConnectionState === 'connecting') {
                              iceConnectionState = self.ICE_CONNECTION_STATE.CHECKING;
                            } else if (iceConnectionState === 'new') {
                              iceConnectionState = self.ICE_CONNECTION_STATE.FAILED;
                            }
                          }
                      
                          self._trigger('iceConnectionState', iceConnectionState, targetMid);
                      
                          if (iceConnectionState === self.ICE_CONNECTION_STATE.FAILED && self._enableIceTrickle) {
                            self._trigger('iceConnectionState', self.ICE_CONNECTION_STATE.TRICKLE_FAILED, targetMid);
                          }
                      
                          if (self._peerConnStatus[targetMid]) {
                            self._peerConnStatus[targetMid].connected = [self.ICE_CONNECTION_STATE.COMPLETED,
                              self.ICE_CONNECTION_STATE.CONNECTED].indexOf(iceConnectionState) > -1;
                          }
                      
                          if (!self._hasMCU && [self.ICE_CONNECTION_STATE.CONNECTED, self.ICE_CONNECTION_STATE.COMPLETED].indexOf(
                            iceConnectionState) > -1 && !!self._bandwidthAdjuster && !bandwidth && window.webrtcDetectedBrowser !== 'edge' &&
                            (((self._peerInformations[targetMid] || {}).agent || {}).name || 'edge') !== 'edge') {
                            var currentBlock = 0;
                            var formatTotalFn = function (arr) {
                              var total = 0;
                              for (var i = 0; i < arr.length; i++) {
                                total += arr[i];
                              }
                              return total / arr.length;
                            };
                            bandwidth = {
                              audio: { send: [], recv: [] },
                              video: { send: [], recv: [] }
                            };
                            var pullInterval = setInterval(function () {
                              if (!(self._peerConnections[targetMid] && self._peerConnections[targetMid].signalingState !==
                                self.PEER_CONNECTION_STATE.CLOSED) || !self._bandwidthAdjuster || !self._peerBandwidth[targetMid]) {
                                clearInterval(pullInterval);
                                return;
                              }
                              self._retrieveStats(targetMid, function (err, stats) {
                                if (!(self._peerConnections[targetMid] && self._peerConnections[targetMid].signalingState !==
                                  self.PEER_CONNECTION_STATE.CLOSED) || !self._bandwidthAdjuster) {
                                  clearInterval(pullInterval);
                                  return;
                                }
                                if (err) {
                                  bandwidth.audio.send.push(0);
                                  bandwidth.audio.recv.push(0);
                                  bandwidth.video.send.push(0);
                                  bandwidth.video.recv.push(0);
                                } else {
                                  bandwidth.audio.send.push(stats.audio.sending.bytes * 8);
                                  bandwidth.audio.recv.push(stats.audio.receiving.bytes * 8);
                                  bandwidth.video.send.push(stats.video.sending.bytes * 8);
                                  bandwidth.video.recv.push(stats.video.receiving.bytes * 8);
                                }
                                currentBlock++;
                                if (currentBlock === self._bandwidthAdjuster.interval) {
                                  currentBlock = 0;
                                  var totalAudioBw = formatTotalFn(bandwidth.audio.send);
                                  var totalVideoBw = formatTotalFn(bandwidth.video.send);
                                  if (!self._bandwidthAdjuster.useUploadBwOnly) {
                                    totalAudioBw += formatTotalFn(bandwidth.audio.recv);
                                    totalVideoBw += formatTotalFn(bandwidth.video.recv);
                                    totalAudioBw = totalAudioBw / 2;
                                    totalVideoBw = totalVideoBw / 2;
                                  }
                                  totalAudioBw = parseInt((totalAudioBw * (self._bandwidthAdjuster.limitAtPercentage / 100)) / 1000, 10);
                                  totalVideoBw = parseInt((totalVideoBw * (self._bandwidthAdjuster.limitAtPercentage / 100)) / 1000, 10);
                                  bandwidth = {
                                    audio: { send: [], recv: [] },
                                    video: { send: [], recv: [] }
                                  };
                                  self.refreshConnection(targetMid, {
                                    bandwidth: { audio: totalAudioBw, video: totalVideoBw }
                                  });
                                }
                              }, true, true);
                            }, 1000);
                          }
                        };
                      
                        pc.onsignalingstatechange = function() {
                          log.debug([targetMid, 'RTCSignalingState', null, 'Peer connection state changed ->'], pc.signalingState);
                          self._trigger('peerConnectionState', pc.signalingState, targetMid);
                        };
                        pc.onicegatheringstatechange = function() {
                          log.log([targetMid, 'RTCIceGatheringState', null, 'Ice gathering state changed ->'], pc.iceGatheringState);
                          self._trigger('candidateGenerationState', pc.iceGatheringState, targetMid);
                        };
                      
                        if (window.webrtcDetectedBrowser === 'firefox') {
                          pc.removeStream = function (stream) {
                            var senders = pc.getSenders();
                            for (var s = 0; s < senders.length; s++) {
                              var tracks = stream.getTracks();
                              for (var t = 0; t < tracks.length; t++) {
                                if (tracks[t] === senders[s].track) {
                                  pc.removeTrack(senders[s]);
                                }
                              }
                            }
                          };
                        }
                      
                        return pc;
                      };
                      
                      /**
                       * Function that handles the <code>_restartPeerConnection</code> scenario
                       *   for MCU enabled Peer connections.
                       * This is implemented currently by making the user leave and join the Room again.
                       * The Peer ID will not stay the same though.
                       * @method _restartMCUConnection
                       * @private
                       * @for Skylink
                       * @since 0.6.1
                       */
                      Skylink.prototype._restartMCUConnection = function(callback, doIceRestart, bwOptions) {
                        var self = this;
                        var listOfPeers = Object.keys(self._peerConnections);
                        var listOfPeerRestartErrors = {};
                        var sendRestartMsgFn = function (peerId) {
                          var restartMsg = {
                            type: self._SIG_MESSAGE_TYPE.RESTART,
                            mid: self._user.sid,
                            rid: self._room.id,
                            agent: window.webrtcDetectedBrowser,
                            version: (window.webrtcDetectedVersion || 0).toString(),
                            os: window.navigator.platform,
                            userInfo: self._getUserInfo(peerId),
                            target: peerId,
                            weight: self._peerPriorityWeight,
                            receiveOnly: self.getPeerInfo().config.receiveOnly,
                            enableIceTrickle: self._enableIceTrickle,
                            enableDataChannel: self._enableDataChannel,
                            enableIceRestart: self._enableIceRestart,
                            doIceRestart: self._mcuUseRenegoRestart && doIceRestart === true &&
                              self._enableIceRestart && self._peerInformations[peerId] &&
                              self._peerInformations[peerId].config.enableIceRestart,
                            isRestartResend: false,
                            temasysPluginVersion: AdapterJS.WebRTCPlugin.plugin ? AdapterJS.WebRTCPlugin.plugin.VERSION : null,
                            SMProtocolVersion: self.SM_PROTOCOL_VERSION,
                            DTProtocolVersion: self.DT_PROTOCOL_VERSION
                          };
                      
                          if (self._publishOnly) {
                            restartMsg.publishOnly = {
                              type: self._streams.screenshare && self._streams.screenshare.stream ? 'screenshare' : 'video'
                            };
                          }
                      
                          if (self._parentId) {
                            restartMsg.parentId = self._parentId;
                          }
                      
                          log.log([peerId, 'RTCPeerConnection', null, 'Sending restart message to signaling server ->'], restartMsg);
                      
                          self._sendChannelMessage(restartMsg);
                        };
                      
                        // Toggle the main bandwidth options.
                        if (bwOptions.bandwidth && typeof bwOptions.bandwidth === 'object') {
                          if (typeof bwOptions.bandwidth.audio === 'number') {
                            self._streamsBandwidthSettings.bAS.audio = bwOptions.bandwidth.audio;
                          }
                          if (typeof bwOptions.bandwidth.video === 'number') {
                            self._streamsBandwidthSettings.bAS.video = bwOptions.bandwidth.video;
                          }
                          if (typeof bwOptions.bandwidth.data === 'number') {
                            self._streamsBandwidthSettings.bAS.data = bwOptions.bandwidth.data;
                          }
                        }
                      
                        if (bwOptions.googleXBandwidth && typeof bwOptions.googleXBandwidth === 'object') {
                          if (typeof bwOptions.googleXBandwidth.min === 'number') {
                            self._streamsBandwidthSettings.googleX.min = bwOptions.googleXBandwidth.min;
                          }
                          if (typeof bwOptions.googleXBandwidth.max === 'number') {
                            self._streamsBandwidthSettings.googleX.max = bwOptions.googleXBandwidth.max;
                          }
                        }
                      
                        for (var i = 0; i < listOfPeers.length; i++) {
                          if (!self._peerConnections[listOfPeers[i]]) {
                            var error = 'Peer connection with peer does not exists. Unable to restart';
                            log.error([listOfPeers[i], 'PeerConnection', null, error]);
                            listOfPeerRestartErrors[listOfPeers[i]] = new Error(error);
                            continue;
                          }
                      
                          if (listOfPeers[i] !== 'MCU') {
                            self._trigger('peerRestart', listOfPeers[i], self.getPeerInfo(listOfPeers[i]), true, false);
                      
                            if (!self._mcuUseRenegoRestart) {
                              sendRestartMsgFn(listOfPeers[i]);
                            }
                          }
                        }
                      
                        self._trigger('serverPeerRestart', 'MCU', self.SERVER_PEER_TYPE.MCU);
                      
                        if (self._mcuUseRenegoRestart) {
                          self._peerEndOfCandidatesCounter.MCU = self._peerEndOfCandidatesCounter.MCU || {};
                          self._peerEndOfCandidatesCounter.MCU.len = 0;
                          sendRestartMsgFn('MCU');
                        } else {
                          // Restart with MCU = peer leaves then rejoins room
                          var peerJoinedFn = function (peerId, peerInfo, isSelf) {
                            log.log([null, 'PeerConnection', null, 'Invoked all peers to restart with MCU. Firing callback']);
                      
                            if (typeof callback === 'function') {
                              if (Object.keys(listOfPeerRestartErrors).length > 0) {
                                callback({
                                  refreshErrors: listOfPeerRestartErrors,
                                  listOfPeers: listOfPeers
                                }, null);
                              } else {
                                callback(null, {
                                  listOfPeers: listOfPeers
                                });
                              }
                            }
                          };
                      
                          self.once('peerJoined', peerJoinedFn, function (peerId, peerInfo, isSelf) {
                            return isSelf;
                          });
                      
                          self.leaveRoom(false, function (error, success) {
                            if (error) {
                              if (typeof callback === 'function') {
                                for (var i = 0; i < listOfPeers.length; i++) {
                                  listOfPeerRestartErrors[listOfPeers[i]] = error;
                                }
                                callback({
                                  refreshErrors: listOfPeerRestartErrors,
                                  listOfPeers: listOfPeers
                                }, null);
                              }
                            } else {
                              //self._trigger('serverPeerLeft', 'MCU', self.SERVER_PEER_TYPE.MCU);
                              self.joinRoom(self._selectedRoom, {
                                bandwidth: bwOptions.bandwidth || {},
                                googleXBandwidth: bwOptions.googleXBandwidth || {},
                                sdpSettings: clone(self._sdpSettings),
                                voiceActivityDetection: self._voiceActivityDetection,
                                publishOnly: !!self._publishOnly,
                                parentId: self._parentId || null,
                                autoBandwidthAdjustment: self._bandwidthAdjuster
                              });
                            }
                          });
                        }
                      };
                      
                      /**
                       * Function that handles the stats tabulation.
                       * @method _parseConnectionStats
                       * @private
                       * @for Skylink
                       * @since 0.6.16
                       */
                      Skylink.prototype._parseConnectionStats = function(prevStats, stats, prop) {
                        var nTime = stats.timestamp;
                        var oTime = prevStats ? prevStats.timestamp || 0 : 0;
                        var nVal = parseFloat(stats[prop] || '0', 10);
                        var oVal = parseFloat(prevStats ? prevStats[prop] || '0' : '0', 10);
                      
                        if ((new Date(nTime).getTime()) === (new Date(oTime).getTime())) {
                          return nVal;
                        }
                      
                        return parseFloat(((nVal - oVal) / (nTime - oTime) * 1000).toFixed(3) || '0', 10);
                      };
                      
                      /**
                       * Function that signals the end-of-candidates flag.
                       * @method _signalingEndOfCandidates
                       * @private
                       * @for Skylink
                       * @since 0.6.16
                       */
                      Skylink.prototype._signalingEndOfCandidates = function(targetMid) {
                        var self = this;
                      
                        if (!self._peerEndOfCandidatesCounter[targetMid]) {
                          return;
                        }
                      
                        // If remote description is set
                        if (self._peerConnections[targetMid].remoteDescription && self._peerConnections[targetMid].remoteDescription.sdp &&
                        // If end-of-candidates signal is received
                          typeof self._peerEndOfCandidatesCounter[targetMid].expectedLen === 'number' &&
                        // If all ICE candidates are received
                          self._peerEndOfCandidatesCounter[targetMid].len >= self._peerEndOfCandidatesCounter[targetMid].expectedLen &&
                        // If there is no ICE candidates queue
                          (self._peerCandidatesQueue[targetMid] ? self._peerCandidatesQueue[targetMid].length === 0 : true) &&
                        // If it has not been set yet
                          !self._peerEndOfCandidatesCounter[targetMid].hasSet) {
                          log.debug([targetMid, 'RTCPeerConnection', null, 'Signaling of end-of-candidates remote ICE gathering.']);
                          self._peerEndOfCandidatesCounter[targetMid].hasSet = true;
                          try {
                            if (window.webrtcDetectedBrowser === 'edge') {
                              var mLineCounter = -1;
                              var addedMids = [];
                              var sdpLines = self._peerConnections[targetMid].remoteDescription.sdp.split('\r\n');
                              var rejected = false;
                      
                              for (var i = 0; i < sdpLines.length; i++) {
                                if (sdpLines[i].indexOf('m=') === 0) {
                                  rejected = sdpLines[i].split(' ')[1] === '0';
                                  mLineCounter++;
                                } else if (sdpLines[i].indexOf('a=mid:') === 0 && !rejected) {
                                  var mid = sdpLines[i].split('a=mid:')[1] || '';
                                  if (mid && addedMids.indexOf(mid) === -1) {
                                    addedMids.push(mid);
                                    self._addIceCandidate(targetMid, 'endofcan-' + (new Date()).getTime(), new RTCIceCandidate({
                                      sdpMid: mid,
                                      sdpMLineIndex: mLineCounter,
                                      candidate: 'candidate:1 1 udp 1 0.0.0.0 9 typ endOfCandidates'
                                    }));
                                    // Start breaking after the first add because of max-bundle option
                                    if (self._peerConnectionConfig.bundlePolicy === self.BUNDLE_POLICY.MAX_BUNDLE) {
                                      break;
                                    }
                                  }
                                }
                              }
                      
                            } else if (AdapterJS && !self._isLowerThanVersion(AdapterJS.VERSION, '0.14.0')) {
                              self._peerConnections[targetMid].addIceCandidate(null);
                            }
                      
                            if (self._gatheredCandidates[targetMid]) {
                              self._trigger('candidatesGathered', targetMid, {
                                expected: self._peerEndOfCandidatesCounter[targetMid].expectedLen || 0,
                                received: self._peerEndOfCandidatesCounter[targetMid].len || 0,
                                processed: self._gatheredCandidates[targetMid].receiving.srflx.length +
                                  self._gatheredCandidates[targetMid].receiving.relay.length +
                                  self._gatheredCandidates[targetMid].receiving.host.length
                              });
                            }
                      
                          } catch (error) {
                            log.error([targetMid, 'RTCPeerConnection', null, 'Failed signaling end-of-candidates ->'], error);
                          }
                        }
                      };