text: 'use strict';
  
  // Last time updated: 2021-07-05 5:40:48 PM UTC
  
  // _________________________
  // RTCMultiConnection v3.7.0
  
  // Open-Sourced: https://github.com/muaz-khan/RTCMultiConnection
  
  // --------------------------------------------------
  // Muaz Khan     - www.MuazKhan.com
  // MIT License   - www.WebRTC-Experiment.com/licence
  // --------------------------------------------------
  
  var RTCMultiConnection = function(roomid, forceOptions) {
  
      var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45';
  
      (function(that) {
          if (!that) {
              return;
          }
  
          if (typeof window !== 'undefined') {
              return;
          }
  
          if (typeof global === 'undefined') {
              return;
          }
  
          global.navigator = {
              userAgent: browserFakeUserAgent,
              getUserMedia: function() {}
          };
  
          if (!global.console) {
              global.console = {};
          }
  
          if (typeof global.console.debug === 'undefined') {
              global.console.debug = global.console.info = global.console.error = global.console.log = global.console.log || function() {
                  console.log(arguments);
              };
          }
  
          if (typeof document === 'undefined') {
              /*global document:true */
              that.document = {};
  
              document.createElement = document.captureStream = document.mozCaptureStream = function() {
                  var obj = {
                      getContext: function() {
                          return obj;
                      },
                      play: function() {},
                      pause: function() {},
                      drawImage: function() {},
                      toDataURL: function() {
                          return '';
                      }
                  };
                  return obj;
              };
  
              document.addEventListener = document.removeEventListener = that.addEventListener = that.removeEventListener = function() {};
  
              that.HTMLVideoElement = that.HTMLMediaElement = function() {};
          }
  
          if (typeof io === 'undefined') {
              that.io = function() {
                  return {
                      on: function(eventName, callback) {
                          callback = callback || function() {};
  
                          if (eventName === 'connect') {
                              callback();
                          }
                      },
                      emit: function(eventName, data, callback) {
                          callback = callback || function() {};
                          if (eventName === 'open-room' || eventName === 'join-room') {
                              callback(true, data.sessionid, null);
                          }
                      }
                  };
              };
          }
  
          if (typeof location === 'undefined') {
              /*global location:true */
              that.location = {
                  protocol: 'file:',
                  href: '',
                  hash: '',
                  origin: 'self'
              };
          }
  
          if (typeof screen === 'undefined') {
              /*global screen:true */
              that.screen = {
                  width: 0,
                  height: 0
              };
          }
  
          if (typeof URL === 'undefined') {
              /*global screen:true */
              that.URL = {
                  createObjectURL: function() {
                      return '';
                  },
                  revokeObjectURL: function() {
                      return '';
                  }
              };
          }
  
          /*global window:true */
          that.window = global;
      })(typeof global !== 'undefined' ? global : null);
  
      function SocketConnection(connection, connectCallback) {
          function isData(session) {
              return !session.audio && !session.video && !session.screen && session.data;
          }
  
          var parameters = '';
  
          parameters += '?userid=' + connection.userid;
          parameters += '&sessionid=' + connection.sessionid;
          parameters += '&msgEvent=' + connection.socketMessageEvent;
          parameters += '&socketCustomEvent=' + connection.socketCustomEvent;
          parameters += '&autoCloseEntireSession=' + !!connection.autoCloseEntireSession;
  
          if (connection.session.broadcast === true) {
              parameters += '&oneToMany=true';
          }
  
          parameters += '&maxParticipantsAllowed=' + connection.maxParticipantsAllowed;
  
          if (connection.enableScalableBroadcast) {
              parameters += '&enableScalableBroadcast=true';
              parameters += '&maxRelayLimitPerUser=' + (connection.maxRelayLimitPerUser || 2);
          }
  
          parameters += '&extra=' + JSON.stringify(connection.extra || {});
  
          if (connection.socketCustomParameters) {
              parameters += connection.socketCustomParameters;
          }
  
          try {
              io.sockets = {};
          } catch (e) {};
  
          if (!connection.socketURL) {
              connection.socketURL = '/';
          }
  
          if (connection.socketURL.substr(connection.socketURL.length - 1, 1) != '/') {
              // connection.socketURL = 'https://domain.com:9001/';
              throw '"socketURL" MUST end with a slash.';
          }
  
          if (connection.enableLogs) {
              if (connection.socketURL == '/') {
                  console.info('socket.io url is: ', location.origin + '/');
              } else {
                  console.info('socket.io url is: ', connection.socketURL);
              }
          }
  
          try {
              connection.socket = io.connect(connection.socketURL + parameters, connection.socketOptions);
          } catch (e) {
              console.error(e);
          }
  
          var mPeer = connection.multiPeersHandler;
  
          connection.socket.on('extra-data-updated', function(remoteUserId, extra) {
              if (!connection.peers[remoteUserId]) return;
              connection.peers[remoteUserId].extra = extra;
  
              connection.onExtraDataUpdated({
                  userid: remoteUserId,
                  extra: extra
              });
  
              updateExtraBackup(remoteUserId, extra);
          });
  
          function updateExtraBackup(remoteUserId, extra) {
              if (!connection.peersBackup[remoteUserId]) {
                  connection.peersBackup[remoteUserId] = {
                      userid: remoteUserId,
                      extra: {}
                  };
              }
  
              connection.peersBackup[remoteUserId].extra = extra;
          }
  
          function onMessageEvent(message) {
              if (message.remoteUserId != connection.userid) return;
  
              if (connection.peers[message.sender] && connection.peers[message.sender].extra != message.message.extra) {
                  connection.peers[message.sender].extra = message.extra;
                  connection.onExtraDataUpdated({
                      userid: message.sender,
                      extra: message.extra
                  });
  
                  updateExtraBackup(message.sender, message.extra);
              }
  
              if (message.message.streamSyncNeeded && connection.peers[message.sender]) {
                  var stream = connection.streamEvents[message.message.streamid];
                  if (!stream || !stream.stream) {
                      return;
                  }
  
                  var action = message.message.action;
  
                  if (action === 'ended' || action === 'inactive' || action === 'stream-removed') {
                      if (connection.peersBackup[stream.userid]) {
                          stream.extra = connection.peersBackup[stream.userid].extra;
                      }
                      connection.onstreamended(stream);
                      return;
                  }
  
                  var type = message.message.type != 'both' ? message.message.type : null;
  
                  if (typeof stream.stream[action] == 'function') {
                      stream.stream[action](type);
                  }
                  return;
              }
  
              if (message.message === 'dropPeerConnection') {
                  connection.deletePeer(message.sender);
                  return;
              }
  
              if (message.message.allParticipants) {
                  if (message.message.allParticipants.indexOf(message.sender) === -1) {
                      message.message.allParticipants.push(message.sender);
                  }
  
                  message.message.allParticipants.forEach(function(participant) {
                      mPeer[!connection.peers[participant] ? 'createNewPeer' : 'renegotiatePeer'](participant, {
                          localPeerSdpConstraints: {
                              OfferToReceiveAudio: connection.sdpConstraints.mandatory.OfferToReceiveAudio,
                              OfferToReceiveVideo: connection.sdpConstraints.mandatory.OfferToReceiveVideo
                          },
                          remotePeerSdpConstraints: {
                              OfferToReceiveAudio: connection.session.oneway ? !!connection.session.audio : connection.sdpConstraints.mandatory.OfferToReceiveAudio,
                              OfferToReceiveVideo: connection.session.oneway ? !!connection.session.video || !!connection.session.screen : connection.sdpConstraints.mandatory.OfferToReceiveVideo
                          },
                          isOneWay: !!connection.session.oneway || connection.direction === 'one-way',
                          isDataOnly: isData(connection.session)
                      });
                  });
                  return;
              }
  
              if (message.message.newParticipant) {
                  if (message.message.newParticipant == connection.userid) return;
                  if (!!connection.peers[message.message.newParticipant]) return;
  
                  mPeer.createNewPeer(message.message.newParticipant, message.message.userPreferences || {
                      localPeerSdpConstraints: {
                          OfferToReceiveAudio: connection.sdpConstraints.mandatory.OfferToReceiveAudio,
                          OfferToReceiveVideo: connection.sdpConstraints.mandatory.OfferToReceiveVideo
                      },
                      remotePeerSdpConstraints: {
                          OfferToReceiveAudio: connection.session.oneway ? !!connection.session.audio : connection.sdpConstraints.mandatory.OfferToReceiveAudio,
                          OfferToReceiveVideo: connection.session.oneway ? !!connection.session.video || !!connection.session.screen : connection.sdpConstraints.mandatory.OfferToReceiveVideo
                      },
                      isOneWay: !!connection.session.oneway || connection.direction === 'one-way',
                      isDataOnly: isData(connection.session)
                  });
                  return;
              }
  
              if (message.message.readyForOffer) {
                  if (connection.attachStreams.length) {
                      connection.waitingForLocalMedia = false;
                  }
  
                  if (connection.waitingForLocalMedia) {
                      // if someone is waiting to join you
                      // make sure that we've local media before making a handshake
                      setTimeout(function() {
                          onMessageEvent(message);
                      }, 1);
                      return;
                  }
              }
  
              if (message.message.newParticipationRequest && message.sender !== connection.userid) {
                  if (connection.peers[message.sender]) {
                      connection.deletePeer(message.sender);
                  }
  
                  var userPreferences = {
                      extra: message.extra || {},
                      localPeerSdpConstraints: message.message.remotePeerSdpConstraints || {
                          OfferToReceiveAudio: connection.sdpConstraints.mandatory.OfferToReceiveAudio,
                          OfferToReceiveVideo: connection.sdpConstraints.mandatory.OfferToReceiveVideo
                      },
                      remotePeerSdpConstraints: message.message.localPeerSdpConstraints || {
                          OfferToReceiveAudio: connection.session.oneway ? !!connection.session.audio : connection.sdpConstraints.mandatory.OfferToReceiveAudio,
                          OfferToReceiveVideo: connection.session.oneway ? !!connection.session.video || !!connection.session.screen : connection.sdpConstraints.mandatory.OfferToReceiveVideo
                      },
                      isOneWay: typeof message.message.isOneWay !== 'undefined' ? message.message.isOneWay : !!connection.session.oneway || connection.direction === 'one-way',
                      isDataOnly: typeof message.message.isDataOnly !== 'undefined' ? message.message.isDataOnly : isData(connection.session),
                      dontGetRemoteStream: typeof message.message.isOneWay !== 'undefined' ? message.message.isOneWay : !!connection.session.oneway || connection.direction === 'one-way',
                      dontAttachLocalStream: !!message.message.dontGetRemoteStream,
                      connectionDescription: message,
                      successCallback: function() {}
                  };
  
                  connection.onNewParticipant(message.sender, userPreferences);
                  return;
              }
  
              if (message.message.changedUUID) {
                  if (connection.peers[message.message.oldUUID]) {
                      connection.peers[message.message.newUUID] = connection.peers[message.message.oldUUID];
                      delete connection.peers[message.message.oldUUID];
                  }
              }
  
              if (message.message.userLeft) {
                  mPeer.onUserLeft(message.sender);
  
                  if (!!message.message.autoCloseEntireSession) {
                      connection.leave();
                  }
  
                  return;
              }
  
              mPeer.addNegotiatedMessage(message.message, message.sender);
          }
  
          connection.socket.on(connection.socketMessageEvent, onMessageEvent);
  
          var alreadyConnected = false;
  
          connection.socket.resetProps = function() {
              alreadyConnected = false;
          };
  
          connection.socket.on('connect', function() {
              if (alreadyConnected) {
                  return;
              }
              alreadyConnected = true;
  
              if (connection.enableLogs) {
                  console.info('socket.io connection is opened.');
              }
  
              setTimeout(function() {
                  connection.socket.emit('extra-data-updated', connection.extra);
              }, 1000);
  
              if (connectCallback) {
                  connectCallback(connection.socket);
              }
          });
  
          connection.socket.on('disconnect', function(event) {
              connection.onSocketDisconnect(event);
          });
  
          connection.socket.on('error', function(event) {
              connection.onSocketError(event);
          });
  
          connection.socket.on('user-disconnected', function(remoteUserId) {
              if (remoteUserId === connection.userid) {
                  return;
              }
  
              connection.onUserStatusChanged({
                  userid: remoteUserId,
                  status: 'offline',
                  extra: connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra || {} : {}
              });
  
              connection.deletePeer(remoteUserId);
          });
  
          connection.socket.on('user-connected', function(userid) {
              if (userid === connection.userid) {
                  return;
              }
  
              connection.onUserStatusChanged({
                  userid: userid,
                  status: 'online',
                  extra: connection.peers[userid] ? connection.peers[userid].extra || {} : {}
              });
          });
  
          connection.socket.on('closed-entire-session', function(sessionid, extra) {
              connection.leave();
              connection.onEntireSessionClosed({
                  sessionid: sessionid,
                  userid: sessionid,
                  extra: extra
              });
          });
  
          connection.socket.on('userid-already-taken', function(useridAlreadyTaken, yourNewUserId) {
              connection.onUserIdAlreadyTaken(useridAlreadyTaken, yourNewUserId);
          });
  
          connection.socket.on('logs', function(log) {
              if (!connection.enableLogs) return;
              console.debug('server-logs', log);
          });
  
          connection.socket.on('number-of-broadcast-viewers-updated', function(data) {
              connection.onNumberOfBroadcastViewersUpdated(data);
          });
  
          connection.socket.on('set-isInitiator-true', function(sessionid) {
              if (sessionid != connection.sessionid) return;
              connection.isInitiator = true;
          });
      }
  
      function MultiPeers(connection) {
          var self = this;
  
          var skipPeers = ['getAllParticipants', 'getLength', 'selectFirst', 'streams', 'send', 'forEach'];
          connection.peers = {
              getLength: function() {
                  var numberOfPeers = 0;
                  for (var peer in this) {
                      if (skipPeers.indexOf(peer) == -1) {
                          numberOfPeers++;
                      }
                  }
                  return numberOfPeers;
              },
              selectFirst: function() {
                  var firstPeer;
                  for (var peer in this) {
                      if (skipPeers.indexOf(peer) == -1) {
                          firstPeer = this[peer];
                      }
                  }
                  return firstPeer;
              },
              getAllParticipants: function(sender) {
                  var allPeers = [];
                  for (var peer in this) {
                      if (skipPeers.indexOf(peer) == -1 && peer != sender) {
                          allPeers.push(peer);
                      }
                  }
                  return allPeers;
              },
              forEach: function(callback) {
                  this.getAllParticipants().forEach(function(participant) {
                      callback(connection.peers[participant]);
                  });
              },
              send: function(data, remoteUserId) {
                  var that = this;
  
                  if (!isNull(data.size) && !isNull(data.type)) {
                      if (connection.enableFileSharing) {
                          self.shareFile(data, remoteUserId);
                          return;
                      }
  
                      if (typeof data !== 'string') {
                          data = JSON.stringify(data);
                      }
                  }
  
                  if (data.type !== 'text' && !(data instanceof ArrayBuffer) && !(data instanceof DataView)) {
                      TextSender.send({
                          text: data,
                          channel: this,
                          connection: connection,
                          remoteUserId: remoteUserId
                      });
                      return;
                  }
  
                  if (data.type === 'text') {
                      data = JSON.stringify(data);
                  }
  
                  if (remoteUserId) {
                      var remoteUser = connection.peers[remoteUserId];
                      if (remoteUser) {
                          if (!remoteUser.channels.length) {
                              connection.peers[remoteUserId].createDataChannel();
                              connection.renegotiate(remoteUserId);
                              setTimeout(function() {
                                  that.send(data, remoteUserId);
                              }, 3000);
                              return;
                          }
  
                          remoteUser.channels.forEach(function(channel) {
                              channel.send(data);
                          });
                          return;
                      }
                  }
  
                  this.getAllParticipants().forEach(function(participant) {
                      if (!that[participant].channels.length) {
                          connection.peers[participant].createDataChannel();
                          connection.renegotiate(participant);
                          setTimeout(function() {
                              that[participant].channels.forEach(function(channel) {
                                  channel.send(data);
                              });
                          }, 3000);
                          return;
                      }
  
                      that[participant].channels.forEach(function(channel) {
                          channel.send(data);
                      });
                  });
              }
          };
  
          this.uuid = connection.userid;
  
          this.getLocalConfig = function(remoteSdp, remoteUserId, userPreferences) {
              if (!userPreferences) {
                  userPreferences = {};
              }
  
              return {
                  streamsToShare: userPreferences.streamsToShare || {},
                  rtcMultiConnection: connection,
                  connectionDescription: userPreferences.connectionDescription,
                  userid: remoteUserId,
                  localPeerSdpConstraints: userPreferences.localPeerSdpConstraints,
                  remotePeerSdpConstraints: userPreferences.remotePeerSdpConstraints,
                  dontGetRemoteStream: !!userPreferences.dontGetRemoteStream,
                  dontAttachLocalStream: !!userPreferences.dontAttachLocalStream,
                  renegotiatingPeer: !!userPreferences.renegotiatingPeer,
                  peerRef: userPreferences.peerRef,
                  channels: userPreferences.channels || [],
                  onLocalSdp: function(localSdp) {
                      self.onNegotiationNeeded(localSdp, remoteUserId);
                  },
                  onLocalCandidate: function(localCandidate) {
                      localCandidate = OnIceCandidateHandler.processCandidates(connection, localCandidate)
                      if (localCandidate) {
                          self.onNegotiationNeeded(localCandidate, remoteUserId);
                      }
                  },
                  remoteSdp: remoteSdp,
                  onDataChannelMessage: function(message) {
                      if (!connection.fbr && connection.enableFileSharing) initFileBufferReader();
  
                      if (typeof message == 'string' || !connection.enableFileSharing) {
                          self.onDataChannelMessage(message, remoteUserId);
                          return;
                      }
  
                      var that = this;
  
                      if (message instanceof ArrayBuffer || message instanceof DataView) {
                          connection.fbr.convertToObject(message, function(object) {
                              that.onDataChannelMessage(object);
                          });
                          return;
                      }
  
                      if (message.readyForNextChunk) {
                          connection.fbr.getNextChunk(message, function(nextChunk, isLastChunk) {
                              connection.peers[remoteUserId].channels.forEach(function(channel) {
                                  channel.send(nextChunk);
                              });
                          }, remoteUserId);
                          return;
                      }
  
                      if (message.chunkMissing) {
                          connection.fbr.chunkMissing(message);
                          return;
                      }
  
                      connection.fbr.addChunk(message, function(promptNextChunk) {
                          connection.peers[remoteUserId].peer.channel.send(promptNextChunk);
                      });
                  },
                  onDataChannelError: function(error) {
                      self.onDataChannelError(error, remoteUserId);
                  },
                  onDataChannelOpened: function(channel) {
                      self.onDataChannelOpened(channel, remoteUserId);
                  },
                  onDataChannelClosed: function(event) {
                      self.onDataChannelClosed(event, remoteUserId);
                  },
                  onRemoteStream: function(stream) {
                      if (connection.peers[remoteUserId]) {
                          connection.peers[remoteUserId].streams.push(stream);
                      }
  
                      self.onGettingRemoteMedia(stream, remoteUserId);
                  },
                  onRemoteStreamRemoved: function(stream) {
                      self.onRemovingRemoteMedia(stream, remoteUserId);
                  },
                  onPeerStateChanged: function(states) {
                      self.onPeerStateChanged(states);
  
                      if (states.iceConnectionState === 'new') {
                          self.onNegotiationStarted(remoteUserId, states);
                      }
  
                      if (states.iceConnectionState === 'connected') {
                          self.onNegotiationCompleted(remoteUserId, states);
                      }
  
                      if (states.iceConnectionState.search(/closed|failed/gi) !== -1) {
                          self.onUserLeft(remoteUserId);
                          self.disconnectWith(remoteUserId);
                      }
                  }
              };
          };
  
          this.createNewPeer = function(remoteUserId, userPreferences) {
              if (connection.maxParticipantsAllowed <= connection.getAllParticipants().length) {
                  return;
              }
  
              userPreferences = userPreferences || {};
  
              if (connection.isInitiator && !!connection.session.audio && connection.session.audio === 'two-way' && !userPreferences.streamsToShare) {
                  userPreferences.isOneWay = false;
                  userPreferences.isDataOnly = false;
                  userPreferences.session = connection.session;
              }
  
              if (!userPreferences.isOneWay && !userPreferences.isDataOnly) {
                  userPreferences.isOneWay = true;
                  this.onNegotiationNeeded({
                      enableMedia: true,
                      userPreferences: userPreferences
                  }, remoteUserId);
                  return;
              }
  
              userPreferences = connection.setUserPreferences(userPreferences, remoteUserId);
              var localConfig = this.getLocalConfig(null, remoteUserId, userPreferences);
              connection.peers[remoteUserId] = new PeerInitiator(localConfig);
          };
  
          this.createAnsweringPeer = function(remoteSdp, remoteUserId, userPreferences) {
              userPreferences = connection.setUserPreferences(userPreferences || {}, remoteUserId);
  
              var localConfig = this.getLocalConfig(remoteSdp, remoteUserId, userPreferences);
              connection.peers[remoteUserId] = new PeerInitiator(localConfig);
          };
  
          this.renegotiatePeer = function(remoteUserId, userPreferences, remoteSdp) {
              if (!connection.peers[remoteUserId]) {
                  if (connection.enableLogs) {
                      console.error('Peer (' + remoteUserId + ') does not exist. Renegotiation skipped.');
                  }
                  return;
              }
  
              if (!userPreferences) {
                  userPreferences = {};
              }
  
              userPreferences.renegotiatingPeer = true;
              userPreferences.peerRef = connection.peers[remoteUserId].peer;
              userPreferences.channels = connection.peers[remoteUserId].channels;
  
              var localConfig = this.getLocalConfig(remoteSdp, remoteUserId, userPreferences);
  
              connection.peers[remoteUserId] = new PeerInitiator(localConfig);
          };
  
          this.replaceTrack = function(track, remoteUserId, isVideoTrack) {
              if (!connection.peers[remoteUserId]) {
                  throw 'This peer (' + remoteUserId + ') does not exist.';
              }
  
              var peer = connection.peers[remoteUserId].peer;
  
              if (!!peer.getSenders && typeof peer.getSenders === 'function' && peer.getSenders().length) {
                  peer.getSenders().forEach(function(rtpSender) {
                      if (isVideoTrack && rtpSender.track.kind === 'video') {
                          connection.peers[remoteUserId].peer.lastVideoTrack = rtpSender.track;
                          rtpSender.replaceTrack(track);
                      }
  
                      if (!isVideoTrack && rtpSender.track.kind === 'audio') {
                          connection.peers[remoteUserId].peer.lastAudioTrack = rtpSender.track;
                          rtpSender.replaceTrack(track);
                      }
                  });
                  return;
              }
  
              console.warn('RTPSender.replaceTrack is NOT supported.');
              this.renegotiatePeer(remoteUserId);
          };
  
          this.onNegotiationNeeded = function(message, remoteUserId) {};
          this.addNegotiatedMessage = function(message, remoteUserId) {
              if (message.type && message.sdp) {
                  if (message.type == 'answer') {
                      if (connection.peers[remoteUserId]) {
                          connection.peers[remoteUserId].addRemoteSdp(message);
                      }
                  }
  
                  if (message.type == 'offer') {
                      if (message.renegotiatingPeer) {
                          this.renegotiatePeer(remoteUserId, null, message);
                      } else {
                          this.createAnsweringPeer(message, remoteUserId);
                      }
                  }
  
                  if (connection.enableLogs) {
                      console.log('Remote peer\'s sdp:', message.sdp);
                  }
                  return;
              }
  
              if (message.candidate) {
                  if (connection.peers[remoteUserId]) {
                      connection.peers[remoteUserId].addRemoteCandidate(message);
                  }
  
                  if (connection.enableLogs) {
                      console.log('Remote peer\'s candidate pairs:', message.candidate);
                  }
                  return;
              }
  
              if (message.enableMedia) {
                  connection.session = message.userPreferences.session || connection.session;
  
                  if (connection.session.oneway && connection.attachStreams.length) {
                      connection.attachStreams = [];
                  }
  
                  if (message.userPreferences.isDataOnly && connection.attachStreams.length) {
                      connection.attachStreams.length = [];
                  }
  
                  var streamsToShare = {};
                  connection.attachStreams.forEach(function(stream) {
                      streamsToShare[stream.streamid] = {
                          isAudio: !!stream.isAudio,
                          isVideo: !!stream.isVideo,
                          isScreen: !!stream.isScreen
                      };
                  });
                  message.userPreferences.streamsToShare = streamsToShare;
  
                  self.onNegotiationNeeded({
                      readyForOffer: true,
                      userPreferences: message.userPreferences
                  }, remoteUserId);
              }
  
              if (message.readyForOffer) {
                  connection.onReadyForOffer(remoteUserId, message.userPreferences);
              }
  
              function cb(stream) {
                  gumCallback(stream, message, remoteUserId);
              }
          };
  
          function gumCallback(stream, message, remoteUserId) {
              var streamsToShare = {};
              connection.attachStreams.forEach(function(stream) {
                  streamsToShare[stream.streamid] = {
                      isAudio: !!stream.isAudio,
                      isVideo: !!stream.isVideo,
                      isScreen: !!stream.isScreen
                  };
              });
              message.userPreferences.streamsToShare = streamsToShare;
  
              self.onNegotiationNeeded({
                  readyForOffer: true,
                  userPreferences: message.userPreferences
              }, remoteUserId);
          }
  
          this.onGettingRemoteMedia = function(stream, remoteUserId) {};
          this.onRemovingRemoteMedia = function(stream, remoteUserId) {};
          this.onGettingLocalMedia = function(localStream) {};
          this.onLocalMediaError = function(error, constraints) {
              connection.onMediaError(error, constraints);
          };
  
          function initFileBufferReader() {
              connection.fbr = new FileBufferReader();
              connection.fbr.onProgress = function(chunk) {
                  connection.onFileProgress(chunk);
              };
              connection.fbr.onBegin = function(file) {
                  connection.onFileStart(file);
              };
              connection.fbr.onEnd = function(file) {
                  connection.onFileEnd(file);
              };
          }
  
          this.shareFile = function(file, remoteUserId) {
              initFileBufferReader();
  
              connection.fbr.readAsArrayBuffer(file, function(uuid) {
                  var arrayOfUsers = connection.getAllParticipants();
  
                  if (remoteUserId) {
                      arrayOfUsers = [remoteUserId];
                  }
  
                  arrayOfUsers.forEach(function(participant) {
                      connection.fbr.getNextChunk(uuid, function(nextChunk) {
                          connection.peers[participant].channels.forEach(function(channel) {
                              channel.send(nextChunk);
                          });
                      }, participant);
                  });
              }, {
                  userid: connection.userid,
                  // extra: connection.extra,
                  chunkSize: DetectRTC.browser.name === 'Firefox' ? 15 * 1000 : connection.chunkSize || 0
              });
          };
  
          if (typeof 'TextReceiver' !== 'undefined') {
              var textReceiver = new TextReceiver(connection);
          }
  
          this.onDataChannelMessage = function(message, remoteUserId) {
              textReceiver.receive(JSON.parse(message), remoteUserId, connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra : {});
          };
  
          this.onDataChannelClosed = function(event, remoteUserId) {
              event.userid = remoteUserId;
              event.extra = connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra : {};
              connection.onclose(event);
          };
  
          this.onDataChannelError = function(error, remoteUserId) {
              error.userid = remoteUserId;
              event.extra = connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra : {};
              connection.onerror(error);
          };
  
          this.onDataChannelOpened = function(channel, remoteUserId) {
              // keep last channel only; we are not expecting parallel/channels channels
              if (connection.peers[remoteUserId].channels.length) {
                  connection.peers[remoteUserId].channels = [channel];
                  return;
              }
  
              connection.peers[remoteUserId].channels.push(channel);
              connection.onopen({
                  userid: remoteUserId,
                  extra: connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra : {},
                  channel: channel
              });
          };
  
          this.onPeerStateChanged = function(state) {
              connection.onPeerStateChanged(state);
          };
  
          this.onNegotiationStarted = function(remoteUserId, states) {};
          this.onNegotiationCompleted = function(remoteUserId, states) {};
  
          this.getRemoteStreams = function(remoteUserId) {
              remoteUserId = remoteUserId || connection.peers.getAllParticipants()[0];
              return connection.peers[remoteUserId] ? connection.peers[remoteUserId].streams : [];
          };
      }
  
      'use strict';
  
      // Last Updated On: 2020-08-12 11:18:41 AM UTC
  
      // ________________
      // DetectRTC v1.4.1
  
      // Open-Sourced: https://github.com/muaz-khan/DetectRTC
  
      // --------------------------------------------------
      // Muaz Khan     - www.MuazKhan.com
      // MIT License   - www.WebRTC-Experiment.com/licence
      // --------------------------------------------------
  
      (function() {
  
          var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45';
  
          var isNodejs = typeof process === 'object' && typeof process.versions === 'object' && process.versions.node && /*node-process*/ !process.browser;
          if (isNodejs) {
              var version = process.versions.node.toString().replace('v', '');
              browserFakeUserAgent = 'Nodejs/' + version + ' (NodeOS) AppleWebKit/' + version + ' (KHTML, like Gecko) Nodejs/' + version + ' Nodejs/' + version
          }
  
          (function(that) {
              if (typeof window !== 'undefined') {
                  return;
              }
  
              if (typeof window === 'undefined' && typeof global !== 'undefined') {
                  global.navigator = {
                      userAgent: browserFakeUserAgent,
                      getUserMedia: function() {}
                  };
  
                  /*global window:true */
                  that.window = global;
              } else if (typeof window === 'undefined') {
                  // window = this;
              }
  
              if (typeof location === 'undefined') {
                  /*global location:true */
                  that.location = {
                      protocol: 'file:',
                      href: '',
                      hash: ''
                  };
              }
  
              if (typeof screen === 'undefined') {
                  /*global screen:true */
                  that.screen = {
                      width: 0,
                      height: 0
                  };
              }
          })(typeof global !== 'undefined' ? global : window);
  
          /*global navigator:true */
          var navigator = window.navigator;
  
          if (typeof navigator !== 'undefined') {
              if (typeof navigator.webkitGetUserMedia !== 'undefined') {
                  navigator.getUserMedia = navigator.webkitGetUserMedia;
              }
  
              if (typeof navigator.mozGetUserMedia !== 'undefined') {
                  navigator.getUserMedia = navigator.mozGetUserMedia;
              }
          } else {
              navigator = {
                  getUserMedia: function() {},
                  userAgent: browserFakeUserAgent
              };
          }
  
          var isMobileDevice = !!(/Android|webOS|iPhone|iPad|iPod|BB10|BlackBerry|IEMobile|Opera Mini|Mobile|mobile/i.test(navigator.userAgent || ''));
  
          var isEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveOrOpenBlob || !!navigator.msSaveBlob);
  
          var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
          var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 && ('netscape' in window) && / rv:/.test(navigator.userAgent);
          var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
          var isChrome = !!window.chrome && !isOpera;
          var isIE = typeof document !== 'undefined' && !!document.documentMode && !isEdge;
  
          // this one can also be used:
          // https://www.websocket.org/js/stuff.js (DetectBrowser.js)
  
          function getBrowserInfo() {
              var nVer = navigator.appVersion;
              var nAgt = navigator.userAgent;
              var browserName = navigator.appName;
              var fullVersion = '' + parseFloat(navigator.appVersion);
              var majorVersion = parseInt(navigator.appVersion, 10);
              var nameOffset, verOffset, ix;
  
              // In Opera, the true version is after 'Opera' or after 'Version'
              if (isOpera) {
                  browserName = 'Opera';
                  try {
                      fullVersion = navigator.userAgent.split('OPR/')[1].split(' ')[0];
                      majorVersion = fullVersion.split('.')[0];
                  } catch (e) {
                      fullVersion = '0.0.0.0';
                      majorVersion = 0;
                  }
              }
              // In MSIE version <=10, the true version is after 'MSIE' in userAgent
              // In IE 11, look for the string after 'rv:'
              else if (isIE) {
                  verOffset = nAgt.indexOf('rv:');
                  if (verOffset > 0) { //IE 11
                      fullVersion = nAgt.substring(verOffset + 3);
                  } else { //IE 10 or earlier
                      verOffset = nAgt.indexOf('MSIE');
                      fullVersion = nAgt.substring(verOffset + 5);
                  }
                  browserName = 'IE';
              }
              // In Chrome, the true version is after 'Chrome' 
              else if (isChrome) {
                  verOffset = nAgt.indexOf('Chrome');
                  browserName = 'Chrome';
                  fullVersion = nAgt.substring(verOffset + 7);
              }
              // In Safari, the true version is after 'Safari' or after 'Version' 
              else if (isSafari) {
                  // both and safri and chrome has same userAgent
                  if (nAgt.indexOf('CriOS') !== -1) {
                      verOffset = nAgt.indexOf('CriOS');
                      browserName = 'Chrome';
                      fullVersion = nAgt.substring(verOffset + 6);
                  } else if (nAgt.indexOf('FxiOS') !== -1) {
                      verOffset = nAgt.indexOf('FxiOS');
                      browserName = 'Firefox';
                      fullVersion = nAgt.substring(verOffset + 6);
                  } else {
                      verOffset = nAgt.indexOf('Safari');
  
                      browserName = 'Safari';
                      fullVersion = nAgt.substring(verOffset + 7);
  
                      if ((verOffset = nAgt.indexOf('Version')) !== -1) {
                          fullVersion = nAgt.substring(verOffset + 8);
                      }
  
                      if (navigator.userAgent.indexOf('Version/') !== -1) {
                          fullVersion = navigator.userAgent.split('Version/')[1].split(' ')[0];
                      }
                  }
              }
              // In Firefox, the true version is after 'Firefox' 
              else if (isFirefox) {
                  verOffset = nAgt.indexOf('Firefox');
                  browserName = 'Firefox';
                  fullVersion = nAgt.substring(verOffset + 8);
              }
  
              // In most other browsers, 'name/version' is at the end of userAgent 
              else if ((nameOffset = nAgt.lastIndexOf(' ') + 1) < (verOffset = nAgt.lastIndexOf('/'))) {
                  browserName = nAgt.substring(nameOffset, verOffset);
                  fullVersion = nAgt.substring(verOffset + 1);
  
                  if (browserName.toLowerCase() === browserName.toUpperCase()) {
                      browserName = navigator.appName;
                  }
              }
  
              if (isEdge) {
                  browserName = 'Edge';
                  fullVersion = navigator.userAgent.split('Edge/')[1];
                  // fullVersion = parseInt(navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)[2], 10).toString();
              }
  
              // trim the fullVersion string at semicolon/space/bracket if present
              if ((ix = fullVersion.search(/[; \)]/)) !== -1) {
                  fullVersion = fullVersion.substring(0, ix);
              }
  
              majorVersion = parseInt('' + fullVersion, 10);
  
              if (isNaN(majorVersion)) {
                  fullVersion = '' + parseFloat(navigator.appVersion);
                  majorVersion = parseInt(navigator.appVersion, 10);
              }
  
              return {
                  fullVersion: fullVersion,
                  version: majorVersion,
                  name: browserName,
                  isPrivateBrowsing: false
              };
          }
  
          // via: https://gist.github.com/cou929/7973956
  
          function retry(isDone, next) {
              var currentTrial = 0,
                  maxRetry = 50,
                  interval = 10,
                  isTimeout = false;
              var id = window.setInterval(
                  function() {
                      if (isDone()) {
                          window.clearInterval(id);
                          next(isTimeout);
                      }
                      if (currentTrial++ > maxRetry) {
                          window.clearInterval(id);
                          isTimeout = true;
                          next(isTimeout);
                      }
                  },
                  10
              );
          }
  
          function isIE10OrLater(userAgent) {
              var ua = userAgent.toLowerCase();
              if (ua.indexOf('msie') === 0 && ua.indexOf('trident') === 0) {
                  return false;
              }
              var match = /(?:msie|rv:)\s?([\d\.]+)/.exec(ua);
              if (match && parseInt(match[1], 10) >= 10) {
                  return true;
              }
              return false;
          }
  
          function detectPrivateMode(callback) {
              var isPrivate;
  
              try {
  
                  if (window.webkitRequestFileSystem) {
                      window.webkitRequestFileSystem(
                          window.TEMPORARY, 1,
                          function() {
                              isPrivate = false;
                          },
                          function(e) {
                              isPrivate = true;
                          }
                      );
                  } else if (window.indexedDB && /Firefox/.test(window.navigator.userAgent)) {
                      var db;
                      try {
                          db = window.indexedDB.open('test');
                          db.onerror = function() {
                              return true;
                          };
                      } catch (e) {
                          isPrivate = true;
                      }
  
                      if (typeof isPrivate === 'undefined') {
                          retry(
                              function isDone() {
                                  return db.readyState === 'done' ? true : false;
                              },
                              function next(isTimeout) {
                                  if (!isTimeout) {
                                      isPrivate = db.result ? false : true;
                                  }
                              }
                          );
                      }
                  } else if (isIE10OrLater(window.navigator.userAgent)) {
                      isPrivate = false;
                      try {
                          if (!window.indexedDB) {
                              isPrivate = true;
                          }
                      } catch (e) {
                          isPrivate = true;
                      }
                  } else if (window.localStorage && /Safari/.test(window.navigator.userAgent)) {
                      try {
                          window.localStorage.setItem('test', 1);
                      } catch (e) {
                          isPrivate = true;
                      }
  
                      if (typeof isPrivate === 'undefined') {
                          isPrivate = false;
                          window.localStorage.removeItem('test');
                      }
                  }
  
              } catch (e) {
                  isPrivate = false;
              }
  
              retry(
                  function isDone() {
                      return typeof isPrivate !== 'undefined' ? true : false;
                  },
                  function next(isTimeout) {
                      callback(isPrivate);
                  }
              );
          }
  
          var isMobile = {
              Android: function() {
                  return navigator.userAgent.match(/Android/i);
              },
              BlackBerry: function() {
                  return navigator.userAgent.match(/BlackBerry|BB10/i);
              },
              iOS: function() {
                  return navigator.userAgent.match(/iPhone|iPad|iPod/i);
              },
              Opera: function() {
                  return navigator.userAgent.match(/Opera Mini/i);
              },
              Windows: function() {
                  return navigator.userAgent.match(/IEMobile/i);
              },
              any: function() {
                  return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows());
              },
              getOsName: function() {
                  var osName = 'Unknown OS';
                  if (isMobile.Android()) {
                      osName = 'Android';
                  }
  
                  if (isMobile.BlackBerry()) {
                      osName = 'BlackBerry';
                  }
  
                  if (isMobile.iOS()) {
                      osName = 'iOS';
                  }
  
                  if (isMobile.Opera()) {
                      osName = 'Opera Mini';
                  }
  
                  if (isMobile.Windows()) {
                      osName = 'Windows';
                  }
  
                  return osName;
              }
          };
  
          // via: http://jsfiddle.net/ChristianL/AVyND/
          function detectDesktopOS() {
              var unknown = '-';
  
              var nVer = navigator.appVersion;
              var nAgt = navigator.userAgent;
  
              var os = unknown;
              var clientStrings = [{
                  s: 'Chrome OS',
                  r: /CrOS/
              }, {
                  s: 'Windows 10',
                  r: /(Windows 10.0|Windows NT 10.0)/
              }, {
                  s: 'Windows 8.1',
                  r: /(Windows 8.1|Windows NT 6.3)/
              }, {
                  s: 'Windows 8',
                  r: /(Windows 8|Windows NT 6.2)/
              }, {
                  s: 'Windows 7',
                  r: /(Windows 7|Windows NT 6.1)/
              }, {
                  s: 'Windows Vista',
                  r: /Windows NT 6.0/
              }, {
                  s: 'Windows Server 2003',
                  r: /Windows NT 5.2/
              }, {
                  s: 'Windows XP',
                  r: /(Windows NT 5.1|Windows XP)/
              }, {
                  s: 'Windows 2000',
                  r: /(Windows NT 5.0|Windows 2000)/
              }, {
                  s: 'Windows ME',
                  r: /(Win 9x 4.90|Windows ME)/
              }, {
                  s: 'Windows 98',
                  r: /(Windows 98|Win98)/
              }, {
                  s: 'Windows 95',
                  r: /(Windows 95|Win95|Windows_95)/
              }, {
                  s: 'Windows NT 4.0',
                  r: /(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/
              }, {
                  s: 'Windows CE',
                  r: /Windows CE/
              }, {
                  s: 'Windows 3.11',
                  r: /Win16/
              }, {
                  s: 'Android',
                  r: /Android/
              }, {
                  s: 'Open BSD',
                  r: /OpenBSD/
              }, {
                  s: 'Sun OS',
                  r: /SunOS/
              }, {
                  s: 'Linux',
                  r: /(Linux|X11)/
              }, {
                  s: 'iOS',
                  r: /(iPhone|iPad|iPod)/
              }, {
                  s: 'Mac OS X',
                  r: /Mac OS X/
              }, {
                  s: 'Mac OS',
                  r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/
              }, {
                  s: 'QNX',
                  r: /QNX/
              }, {
                  s: 'UNIX',
                  r: /UNIX/
              }, {
                  s: 'BeOS',
                  r: /BeOS/
              }, {
                  s: 'OS/2',
                  r: /OS\/2/
              }, {
                  s: 'Search Bot',
                  r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/
              }];
              for (var i = 0, cs; cs = clientStrings[i]; i++) {
                  if (cs.r.test(nAgt)) {
                      os = cs.s;
                      break;
                  }
              }
  
              var osVersion = unknown;
  
              if (/Windows/.test(os)) {
                  if (/Windows (.*)/.test(os)) {
                      osVersion = /Windows (.*)/.exec(os)[1];
                  }
                  os = 'Windows';
              }
  
              switch (os) {
                  case 'Mac OS X':
                      if (/Mac OS X (10[\.\_\d]+)/.test(nAgt)) {
                          osVersion = /Mac OS X (10[\.\_\d]+)/.exec(nAgt)[1];
                      }
                      break;
                  case 'Android':
                      if (/Android ([\.\_\d]+)/.test(nAgt)) {
                          osVersion = /Android ([\.\_\d]+)/.exec(nAgt)[1];
                      }
                      break;
                  case 'iOS':
                      if (/OS (\d+)_(\d+)_?(\d+)?/.test(nAgt)) {
                          osVersion = /OS (\d+)_(\d+)_?(\d+)?/.exec(nVer);
                          if (osVersion && osVersion.length > 3) {
                              osVersion = osVersion[1] + '.' + osVersion[2] + '.' + (osVersion[3] | 0);
                          }
                      }
                      break;
              }
  
              return {
                  osName: os,
                  osVersion: osVersion
              };
          }
  
          var osName = 'Unknown OS';
          var osVersion = 'Unknown OS Version';
  
          function getAndroidVersion(ua) {
              ua = (ua || navigator.userAgent).toLowerCase();
              var match = ua.match(/android\s([0-9\.]*)/);
              return match ? match[1] : false;
          }
  
          var osInfo = detectDesktopOS();
  
          if (osInfo && osInfo.osName && osInfo.osName != '-') {
              osName = osInfo.osName;
              osVersion = osInfo.osVersion;
          } else if (isMobile.any()) {
              osName = isMobile.getOsName();
  
              if (osName == 'Android') {
                  osVersion = getAndroidVersion();
              }
          }
  
          var isNodejs = typeof process === 'object' && typeof process.versions === 'object' && process.versions.node;
  
          if (osName === 'Unknown OS' && isNodejs) {
              osName = 'Nodejs';
              osVersion = process.versions.node.toString().replace('v', '');
          }
  
          var isCanvasSupportsStreamCapturing = false;
          var isVideoSupportsStreamCapturing = false;
          ['captureStream', 'mozCaptureStream', 'webkitCaptureStream'].forEach(function(item) {
              if (typeof document === 'undefined' || typeof document.createElement !== 'function') {
                  return;
              }
  
              if (!isCanvasSupportsStreamCapturing && item in document.createElement('canvas')) {
                  isCanvasSupportsStreamCapturing = true;
              }
  
              if (!isVideoSupportsStreamCapturing && item in document.createElement('video')) {
                  isVideoSupportsStreamCapturing = true;
              }
          });
  
          var regexIpv4Local = /^(192\.168\.|169\.254\.|10\.|172\.(1[6-9]|2\d|3[01]))/,
              regexIpv4 = /([0-9]{1,3}(\.[0-9]{1,3}){3})/,
              regexIpv6 = /[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7}/;
  
          // via: https://github.com/diafygi/webrtc-ips
          function DetectLocalIPAddress(callback, stream) {
              if (!DetectRTC.isWebRTCSupported) {
                  return;
              }
  
              var isPublic = true,
                  isIpv4 = true;
              getIPs(function(ip) {
                  if (!ip) {
                      callback(); // Pass nothing to tell that ICE-gathering-ended
                  } else if (ip.match(regexIpv4Local)) {
                      isPublic = false;
                      callback('Local: ' + ip, isPublic, isIpv4);
                  } else if (ip.match(regexIpv6)) { //via https://ourcodeworld.com/articles/read/257/how-to-get-the-client-ip-address-with-javascript-only
                      isIpv4 = false;
                      callback('Public: ' + ip, isPublic, isIpv4);
                  } else {
                      callback('Public: ' + ip, isPublic, isIpv4);
                  }
              }, stream);
          }
  
          function getIPs(callback, stream) {
              if (typeof document === 'undefined' || typeof document.getElementById !== 'function') {
                  return;
              }
  
              var ipDuplicates = {};
  
              var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
  
              if (!RTCPeerConnection) {
                  var iframe = document.getElementById('iframe');
                  if (!iframe) {
                      return;
                  }
                  var win = iframe.contentWindow;
                  RTCPeerConnection = win.RTCPeerConnection || win.mozRTCPeerConnection || win.webkitRTCPeerConnection;
              }
  
              if (!RTCPeerConnection) {
                  return;
              }
  
              var peerConfig = null;
  
              if (DetectRTC.browser === 'Chrome' && DetectRTC.browser.version < 58) {
                  // todo: add support for older Opera
                  peerConfig = {
                      optional: [{
                          RtpDataChannels: true
                      }]
                  };
              }
  
              var servers = {
                  iceServers: [{
                      urls: 'stun:stun.l.google.com:19302'
                  }]
              };
  
              var pc = new RTCPeerConnection(servers, peerConfig);
  
              if (stream) {
                  if (pc.addStream) {
                      pc.addStream(stream);
                  } else if (pc.addTrack && stream.getTracks()[0]) {
                      pc.addTrack(stream.getTracks()[0], stream);
                  }
              }
  
              function handleCandidate(candidate) {
                  if (!candidate) {
                      callback(); // Pass nothing to tell that ICE-gathering-ended
                      return;
                  }
  
                  var match = regexIpv4.exec(candidate);
                  if (!match) {
                      return;
                  }
                  var ipAddress = match[1];
                  var isPublic = (candidate.match(regexIpv4Local)),
                      isIpv4 = true;
  
                  if (ipDuplicates[ipAddress] === undefined) {
                      callback(ipAddress, isPublic, isIpv4);
                  }
  
                  ipDuplicates[ipAddress] = true;
              }
  
              // listen for candidate events
              pc.onicecandidate = function(event) {
                  if (event.candidate && event.candidate.candidate) {
                      handleCandidate(event.candidate.candidate);
                  } else {
                      handleCandidate(); // Pass nothing to tell that ICE-gathering-ended
                  }
              };
  
              // create data channel
              if (!stream) {
                  try {
                      pc.createDataChannel('sctp', {});
                  } catch (e) {}
              }
  
              // create an offer sdp
              if (DetectRTC.isPromisesSupported) {
                  pc.createOffer().then(function(result) {
                      pc.setLocalDescription(result).then(afterCreateOffer);
                  });
              } else {
                  pc.createOffer(function(result) {
                      pc.setLocalDescription(result, afterCreateOffer, function() {});
                  }, function() {});
              }
  
              function afterCreateOffer() {
                  var lines = pc.localDescription.sdp.split('\n');
  
                  lines.forEach(function(line) {
                      if (line && line.indexOf('a=candidate:') === 0) {
                          handleCandidate(line);
                      }
                  });
              }
          }
  
          var MediaDevices = [];
  
          var audioInputDevices = [];
          var audioOutputDevices = [];
          var videoInputDevices = [];
  
          if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
              // Firefox 38+ seems having support of enumerateDevices
              // Thanks @xdumaine/enumerateDevices
              navigator.enumerateDevices = function(callback) {
                  var enumerateDevices = navigator.mediaDevices.enumerateDevices();
                  if (enumerateDevices && enumerateDevices.then) {
                      navigator.mediaDevices.enumerateDevices().then(callback).catch(function() {
                          callback([]);
                      });
                  } else {
                      callback([]);
                  }
              };
          }
  
          // Media Devices detection
          var canEnumerate = false;
  
          /*global MediaStreamTrack:true */
          if (typeof MediaStreamTrack !== 'undefined' && 'getSources' in MediaStreamTrack) {
              canEnumerate = true;
          } else if (navigator.mediaDevices && !!navigator.mediaDevices.enumerateDevices) {
              canEnumerate = true;
          }
  
          var hasMicrophone = false;
          var hasSpeakers = false;
          var hasWebcam = false;
  
          var isWebsiteHasMicrophonePermissions = false;
          var isWebsiteHasWebcamPermissions = false;
  
          // http://dev.w3.org/2011/webrtc/editor/getusermedia.html#mediadevices
          function checkDeviceSupport(callback) {
              if (!canEnumerate) {
                  if (callback) {
                      callback();
                  }
                  return;
              }
  
              if (!navigator.enumerateDevices && window.MediaStreamTrack && window.MediaStreamTrack.getSources) {
                  navigator.enumerateDevices = window.MediaStreamTrack.getSources.bind(window.MediaStreamTrack);
              }
  
              if (!navigator.enumerateDevices && navigator.enumerateDevices) {
                  navigator.enumerateDevices = navigator.enumerateDevices.bind(navigator);
              }
  
              if (!navigator.enumerateDevices) {
                  if (callback) {
                      callback();
                  }
                  return;
              }
  
              MediaDevices = [];
  
              audioInputDevices = [];
              audioOutputDevices = [];
              videoInputDevices = [];
  
              hasMicrophone = false;
              hasSpeakers = false;
              hasWebcam = false;
  
              isWebsiteHasMicrophonePermissions = false;
              isWebsiteHasWebcamPermissions = false;
  
              // to prevent duplication
              var alreadyUsedDevices = {};
  
              navigator.enumerateDevices(function(devices) {
                  MediaDevices = [];
  
                  audioInputDevices = [];
                  audioOutputDevices = [];
                  videoInputDevices = [];
  
                  devices.forEach(function(_device) {
                      var device = {};
                      for (var d in _device) {
                          try {
                              if (typeof _device[d] !== 'function') {
                                  device[d] = _device[d];
                              }
                          } catch (e) {}
                      }
  
                      if (alreadyUsedDevices[device.deviceId + device.label + device.kind]) {
                          return;
                      }
  
                      // if it is MediaStreamTrack.getSources
                      if (device.kind === 'audio') {
                          device.kind = 'audioinput';
                      }
  
                      if (device.kind === 'video') {
                          device.kind = 'videoinput';
                      }
  
                      if (!device.deviceId) {
                          device.deviceId = device.id;
                      }
  
                      if (!device.id) {
                          device.id = device.deviceId;
                      }
  
                      if (!device.label) {
                          device.isCustomLabel = true;
  
                          if (device.kind === 'videoinput') {
                              device.label = 'Camera ' + (videoInputDevices.length + 1);
                          } else if (device.kind === 'audioinput') {
                              device.label = 'Microphone ' + (audioInputDevices.length + 1);
                          } else if (device.kind === 'audiooutput') {
                              device.label = 'Speaker ' + (audioOutputDevices.length + 1);
                          } else {
                              device.label = 'Please invoke getUserMedia once.';
                          }
  
                          if (typeof DetectRTC !== 'undefined' && DetectRTC.browser.isChrome && DetectRTC.browser.version >= 46 && !/^(https:|chrome-extension:)$/g.test(location.protocol || '')) {
                              if (typeof document !== 'undefined' && typeof document.domain === 'string' && document.domain.search && document.domain.search(/localhost|127.0./g) === -1) {
                                  device.label = 'HTTPs is required to get label of this ' + device.kind + ' device.';
                              }
                          }
                      } else {
                          // Firefox on Android still returns empty label
                          if (device.kind === 'videoinput' && !isWebsiteHasWebcamPermissions) {
                              isWebsiteHasWebcamPermissions = true;
                          }
  
                          if (device.kind === 'audioinput' && !isWebsiteHasMicrophonePermissions) {
                              isWebsiteHasMicrophonePermissions = true;
                          }
                      }
  
                      if (device.kind === 'audioinput') {
                          hasMicrophone = true;
  
                          if (audioInputDevices.indexOf(device) === -1) {
                              audioInputDevices.push(device);
                          }
                      }
  
                      if (device.kind === 'audiooutput') {
                          hasSpeakers = true;
  
                          if (audioOutputDevices.indexOf(device) === -1) {
                              audioOutputDevices.push(device);
                          }
                      }
  
                      if (device.kind === 'videoinput') {
                          hasWebcam = true;
  
                          if (videoInputDevices.indexOf(device) === -1) {
                              videoInputDevices.push(device);
                          }
                      }
  
                      // there is no 'videoouput' in the spec.
                      MediaDevices.push(device);
  
                      alreadyUsedDevices[device.deviceId + device.label + device.kind] = device;
                  });
  
                  if (typeof DetectRTC !== 'undefined') {
                      // to sync latest outputs
                      DetectRTC.MediaDevices = MediaDevices;
                      DetectRTC.hasMicrophone = hasMicrophone;
                      DetectRTC.hasSpeakers = hasSpeakers;
                      DetectRTC.hasWebcam = hasWebcam;
  
                      DetectRTC.isWebsiteHasWebcamPermissions = isWebsiteHasWebcamPermissions;
                      DetectRTC.isWebsiteHasMicrophonePermissions = isWebsiteHasMicrophonePermissions;
  
                      DetectRTC.audioInputDevices = audioInputDevices;
                      DetectRTC.audioOutputDevices = audioOutputDevices;
                      DetectRTC.videoInputDevices = videoInputDevices;
                  }
  
                  if (callback) {
                      callback();
                  }
              });
          }
  
          var DetectRTC = window.DetectRTC || {};
  
          // ----------
          // DetectRTC.browser.name || DetectRTC.browser.version || DetectRTC.browser.fullVersion
          DetectRTC.browser = getBrowserInfo();
  
          detectPrivateMode(function(isPrivateBrowsing) {
              DetectRTC.browser.isPrivateBrowsing = !!isPrivateBrowsing;
          });
  
          // DetectRTC.isChrome || DetectRTC.isFirefox || DetectRTC.isEdge
          DetectRTC.browser['is' + DetectRTC.browser.name] = true;
  
          // -----------
          DetectRTC.osName = osName;
          DetectRTC.osVersion = osVersion;
  
          var isNodeWebkit = typeof process === 'object' && typeof process.versions === 'object' && process.versions['node-webkit'];
  
          // --------- Detect if system supports WebRTC 1.0 or WebRTC 1.1.
          var isWebRTCSupported = false;
          ['RTCPeerConnection', 'webkitRTCPeerConnection', 'mozRTCPeerConnection', 'RTCIceGatherer'].forEach(function(item) {
              if (isWebRTCSupported) {
                  return;
              }
  
              if (item in window) {
                  isWebRTCSupported = true;
              }
          });
          DetectRTC.isWebRTCSupported = isWebRTCSupported;
  
          //-------
          DetectRTC.isORTCSupported = typeof RTCIceGatherer !== 'undefined';
  
          // --------- Detect if system supports screen capturing API
          var isScreenCapturingSupported = false;
          if (DetectRTC.browser.isChrome && DetectRTC.browser.version >= 35) {
              isScreenCapturingSupported = true;
          } else if (DetectRTC.browser.isFirefox && DetectRTC.browser.version >= 34) {
              isScreenCapturingSupported = true;
          } else if (DetectRTC.browser.isEdge && DetectRTC.browser.version >= 17) {
              isScreenCapturingSupported = true;
          } else if (DetectRTC.osName === 'Android' && DetectRTC.browser.isChrome) {
              isScreenCapturingSupported = true;
          }
  
          if (!!navigator.getDisplayMedia || (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia)) {
              isScreenCapturingSupported = true;
          }
  
          if (!/^(https:|chrome-extension:)$/g.test(location.protocol || '')) {
              var isNonLocalHost = typeof document !== 'undefined' && typeof document.domain === 'string' && document.domain.search && document.domain.search(/localhost|127.0./g) === -1;
              if (isNonLocalHost && (DetectRTC.browser.isChrome || DetectRTC.browser.isEdge || DetectRTC.browser.isOpera)) {
                  isScreenCapturingSupported = false;
              } else if (DetectRTC.browser.isFirefox) {
                  isScreenCapturingSupported = false;
              }
          }
          DetectRTC.isScreenCapturingSupported = isScreenCapturingSupported;
  
          // --------- Detect if WebAudio API are supported
          var webAudio = {
              isSupported: false,
              isCreateMediaStreamSourceSupported: false
          };
  
          ['AudioContext', 'webkitAudioContext', 'mozAudioContext', 'msAudioContext'].forEach(function(item) {
              if (webAudio.isSupported) {
                  return;
              }
  
              if (item in window) {
                  webAudio.isSupported = true;
  
                  if (window[item] && 'createMediaStreamSource' in window[item].prototype) {
                      webAudio.isCreateMediaStreamSourceSupported = true;
                  }
              }
          });
          DetectRTC.isAudioContextSupported = webAudio.isSupported;
          DetectRTC.isCreateMediaStreamSourceSupported = webAudio.isCreateMediaStreamSourceSupported;
  
          // ---------- Detect if SCTP/RTP channels are supported.
  
          var isRtpDataChannelsSupported = false;
          if (DetectRTC.browser.isChrome && DetectRTC.browser.version > 31) {
              isRtpDataChannelsSupported = true;
          }
          DetectRTC.isRtpDataChannelsSupported = isRtpDataChannelsSupported;
  
          var isSCTPSupportd = false;
          if (DetectRTC.browser.isFirefox && DetectRTC.browser.version > 28) {
              isSCTPSupportd = true;
          } else if (DetectRTC.browser.isChrome && DetectRTC.browser.version > 25) {
              isSCTPSupportd = true;
          } else if (DetectRTC.browser.isOpera && DetectRTC.browser.version >= 11) {
              isSCTPSupportd = true;
          }
          DetectRTC.isSctpDataChannelsSupported = isSCTPSupportd;
  
          // ---------
  
          DetectRTC.isMobileDevice = isMobileDevice; // "isMobileDevice" boolean is defined in "getBrowserInfo.js"
  
          // ------
          var isGetUserMediaSupported = false;
          if (navigator.getUserMedia) {
              isGetUserMediaSupported = true;
          } else if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
              isGetUserMediaSupported = true;
          }
  
          if (DetectRTC.browser.isChrome && DetectRTC.browser.version >= 46 && !/^(https:|chrome-extension:)$/g.test(location.protocol || '')) {
              if (typeof document !== 'undefined' && typeof document.domain === 'string' && document.domain.search && document.domain.search(/localhost|127.0./g) === -1) {
                  isGetUserMediaSupported = 'Requires HTTPs';
              }
          }
  
          if (DetectRTC.osName === 'Nodejs') {
              isGetUserMediaSupported = false;
          }
          DetectRTC.isGetUserMediaSupported = isGetUserMediaSupported;
  
          var displayResolution = '';
          if (screen.width) {
              var width = (screen.width) ? screen.width : '';
              var height = (screen.height) ? screen.height : '';
              displayResolution += '' + width + ' x ' + height;
          }
          DetectRTC.displayResolution = displayResolution;
  
          function getAspectRatio(w, h) {
              function gcd(a, b) {
                  return (b == 0) ? a : gcd(b, a % b);
              }
              var r = gcd(w, h);
              return (w / r) / (h / r);
          }
  
          DetectRTC.displayAspectRatio = getAspectRatio(screen.width, screen.height).toFixed(2);
  
          // ----------
          DetectRTC.isCanvasSupportsStreamCapturing = isCanvasSupportsStreamCapturing;
          DetectRTC.isVideoSupportsStreamCapturing = isVideoSupportsStreamCapturing;
  
          if (DetectRTC.browser.name == 'Chrome' && DetectRTC.browser.version >= 53) {
              if (!DetectRTC.isCanvasSupportsStreamCapturing) {
                  DetectRTC.isCanvasSupportsStreamCapturing = 'Requires chrome flag: enable-experimental-web-platform-features';
              }
  
              if (!DetectRTC.isVideoSupportsStreamCapturing) {
                  DetectRTC.isVideoSupportsStreamCapturing = 'Requires chrome flag: enable-experimental-web-platform-features';
              }
          }
  
          // ------
          DetectRTC.DetectLocalIPAddress = DetectLocalIPAddress;
  
          DetectRTC.isWebSocketsSupported = 'WebSocket' in window && 2 === window.WebSocket.CLOSING;
          DetectRTC.isWebSocketsBlocked = !DetectRTC.isWebSocketsSupported;
  
          if (DetectRTC.osName === 'Nodejs') {
              DetectRTC.isWebSocketsSupported = true;
              DetectRTC.isWebSocketsBlocked = false;
          }
  
          DetectRTC.checkWebSocketsSupport = function(callback) {
              callback = callback || function() {};
              try {
                  var starttime;
                  var websocket = new WebSocket('wss://echo.websocket.org:443/');
                  websocket.onopen = function() {
                      DetectRTC.isWebSocketsBlocked = false;
                      starttime = (new Date).getTime();
                      websocket.send('ping');
                  };
                  websocket.onmessage = function() {
                      DetectRTC.WebsocketLatency = (new Date).getTime() - starttime + 'ms';
                      callback();
                      websocket.close();
                      websocket = null;
                  };
                  websocket.onerror = function() {
                      DetectRTC.isWebSocketsBlocked = true;
                      callback();
                  };
              } catch (e) {
                  DetectRTC.isWebSocketsBlocked = true;
                  callback();
              }
          };
  
          // -------
          DetectRTC.load = function(callback) {
              callback = callback || function() {};
              checkDeviceSupport(callback);
          };
  
          // check for microphone/camera support!
          if (typeof checkDeviceSupport === 'function') {
              // checkDeviceSupport();
          }
  
          if (typeof MediaDevices !== 'undefined') {
              DetectRTC.MediaDevices = MediaDevices;
          } else {
              DetectRTC.MediaDevices = [];
          }
  
          DetectRTC.hasMicrophone = hasMicrophone;
          DetectRTC.hasSpeakers = hasSpeakers;
          DetectRTC.hasWebcam = hasWebcam;
  
          DetectRTC.isWebsiteHasWebcamPermissions = isWebsiteHasWebcamPermissions;
          DetectRTC.isWebsiteHasMicrophonePermissions = isWebsiteHasMicrophonePermissions;
  
          DetectRTC.audioInputDevices = audioInputDevices;
          DetectRTC.audioOutputDevices = audioOutputDevices;
          DetectRTC.videoInputDevices = videoInputDevices;
  
          // ------
          var isSetSinkIdSupported = false;
          if (typeof document !== 'undefined' && typeof document.createElement === 'function' && 'setSinkId' in document.createElement('video')) {
              isSetSinkIdSupported = true;
          }
          DetectRTC.isSetSinkIdSupported = isSetSinkIdSupported;
  
          // -----
          var isRTPSenderReplaceTracksSupported = false;
          if (DetectRTC.browser.isFirefox && typeof mozRTCPeerConnection !== 'undefined' /*&& DetectRTC.browser.version > 39*/ ) {
              /*global mozRTCPeerConnection:true */
              if ('getSenders' in mozRTCPeerConnection.prototype) {
                  isRTPSenderReplaceTracksSupported = true;
              }
          } else if (DetectRTC.browser.isChrome && typeof webkitRTCPeerConnection !== 'undefined') {
              /*global webkitRTCPeerConnection:true */
              if ('getSenders' in webkitRTCPeerConnection.prototype) {
                  isRTPSenderReplaceTracksSupported = true;
              }
          }
          DetectRTC.isRTPSenderReplaceTracksSupported = isRTPSenderReplaceTracksSupported;
  
          //------
          var isRemoteStreamProcessingSupported = false;
          if (DetectRTC.browser.isFirefox && DetectRTC.browser.version > 38) {
              isRemoteStreamProcessingSupported = true;
          }
          DetectRTC.isRemoteStreamProcessingSupported = isRemoteStreamProcessingSupported;
  
          //-------
          var isApplyConstraintsSupported = false;
  
          /*global MediaStreamTrack:true */
          if (typeof MediaStreamTrack !== 'undefined' && 'applyConstraints' in MediaStreamTrack.prototype) {
              isApplyConstraintsSupported = true;
          }
          DetectRTC.isApplyConstraintsSupported = isApplyConstraintsSupported;
  
          //-------
          var isMultiMonitorScreenCapturingSupported = false;
          if (DetectRTC.browser.isFirefox && DetectRTC.browser.version >= 43) {
              // version 43 merely supports platforms for multi-monitors
              // version 44 will support exact multi-monitor selection i.e. you can select any monitor for screen capturing.
              isMultiMonitorScreenCapturingSupported = true;
          }
          DetectRTC.isMultiMonitorScreenCapturingSupported = isMultiMonitorScreenCapturingSupported;
  
          DetectRTC.isPromisesSupported = !!('Promise' in window);
  
          // version is generated by "grunt"
          DetectRTC.version = '1.4.1';
  
          if (typeof DetectRTC === 'undefined') {
              window.DetectRTC = {};
          }
  
          var MediaStream = window.MediaStream;
  
          if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
              MediaStream = webkitMediaStream;
          }
  
          if (typeof MediaStream !== 'undefined' && typeof MediaStream === 'function') {
              DetectRTC.MediaStream = Object.keys(MediaStream.prototype);
          } else DetectRTC.MediaStream = false;
  
          if (typeof MediaStreamTrack !== 'undefined') {
              DetectRTC.MediaStreamTrack = Object.keys(MediaStreamTrack.prototype);
          } else DetectRTC.MediaStreamTrack = false;
  
          var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
  
          if (typeof RTCPeerConnection !== 'undefined') {
              DetectRTC.RTCPeerConnection = Object.keys(RTCPeerConnection.prototype);
          } else DetectRTC.RTCPeerConnection = false;
  
          window.DetectRTC = DetectRTC;
  
          if (typeof module !== 'undefined' /* && !!module.exports*/ ) {
              module.exports = DetectRTC;
          }
  
          if (typeof define === 'function' && define.amd) {
              define('DetectRTC', [], function() {
                  return DetectRTC;
              });
          }
      })();
  
      // globals.js
  
      if (typeof cordova !== 'undefined') {
          DetectRTC.isMobileDevice = true;
          DetectRTC.browser.name = 'Chrome';
      }
  
      if (navigator && navigator.userAgent && navigator.userAgent.indexOf('Crosswalk') !== -1) {
          DetectRTC.isMobileDevice = true;
          DetectRTC.browser.name = 'Chrome';
      }
  
      function fireEvent(obj, eventName, args) {
          if (typeof CustomEvent === 'undefined') {
              return;
          }
  
          var eventDetail = {
              arguments: args,
              __exposedProps__: args
          };
  
          var event = new CustomEvent(eventName, eventDetail);
          obj.dispatchEvent(event);
      }
  
      function setHarkEvents(connection, streamEvent) {
          if (!streamEvent.stream || !getTracks(streamEvent.stream, 'audio').length) return;
  
          if (!connection || !streamEvent) {
              throw 'Both arguments are required.';
          }
  
          if (!connection.onspeaking || !connection.onsilence) {
              return;
          }
  
          if (typeof hark === 'undefined') {
              throw 'hark.js not found.';
          }
  
          hark(streamEvent.stream, {
              onspeaking: function() {
                  connection.onspeaking(streamEvent);
              },
              onsilence: function() {
                  connection.onsilence(streamEvent);
              },
              onvolumechange: function(volume, threshold) {
                  if (!connection.onvolumechange) {
                      return;
                  }
                  connection.onvolumechange(merge({
                      volume: volume,
                      threshold: threshold
                  }, streamEvent));
              }
          });
      }
  
      function setMuteHandlers(connection, streamEvent) {
          if (!streamEvent.stream || !streamEvent.stream || !streamEvent.stream.addEventListener) return;
  
          streamEvent.stream.addEventListener('mute', function(event) {
              event = connection.streamEvents[streamEvent.streamid];
  
              event.session = {
                  audio: event.muteType === 'audio',
                  video: event.muteType === 'video'
              };
  
              connection.onmute(event);
          }, false);
  
          streamEvent.stream.addEventListener('unmute', function(event) {
              event = connection.streamEvents[streamEvent.streamid];
  
              event.session = {
                  audio: event.unmuteType === 'audio',
                  video: event.unmuteType === 'video'
              };
  
              connection.onunmute(event);
          }, false);
      }
  
      function getRandomString() {
          if (window.crypto && window.crypto.getRandomValues && navigator.userAgent.indexOf('Safari') === -1) {
              var a = window.crypto.getRandomValues(new Uint32Array(3)),
                  token = '';
              for (var i = 0, l = a.length; i < l; i++) {
                  token += a[i].toString(36);
              }
              return token;
          } else {
              return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, '');
          }
      }
  
      // Get HTMLAudioElement/HTMLVideoElement accordingly
      // todo: add API documentation for connection.autoCreateMediaElement
  
      function getRMCMediaElement(stream, callback, connection) {
          if (!connection.autoCreateMediaElement) {
              callback({});
              return;
          }
  
          var isAudioOnly = false;
          if (!getTracks(stream, 'video').length && !stream.isVideo && !stream.isScreen) {
              isAudioOnly = true;
          }
  
          if (DetectRTC.browser.name === 'Firefox') {
              if (connection.session.video || connection.session.screen) {
                  isAudioOnly = false;
              }
          }
  
          var mediaElement = document.createElement(isAudioOnly ? 'audio' : 'video');
  
          mediaElement.srcObject = stream;
  
          mediaElement.setAttribute('autoplay', true);
          mediaElement.setAttribute('playsinline', true);
          mediaElement.setAttribute('controls', true);
          mediaElement.setAttribute('muted', false);
          mediaElement.setAttribute('volume', 1);
  
          // http://goo.gl/WZ5nFl
          // Firefox don't yet support onended for any stream (remote/local)
          if (DetectRTC.browser.name === 'Firefox') {
              var streamEndedEvent = 'ended';
  
              if ('oninactive' in mediaElement) {
                  streamEndedEvent = 'inactive';
              }
  
              mediaElement.addEventListener(streamEndedEvent, function() {
                  // fireEvent(stream, streamEndedEvent, stream);
                  currentUserMediaRequest.remove(stream.idInstance);
  
                  if (stream.type === 'local') {
                      streamEndedEvent = 'ended';
  
                      if ('oninactive' in stream) {
                          streamEndedEvent = 'inactive';
                      }
  
                      StreamsHandler.onSyncNeeded(stream.streamid, streamEndedEvent);
  
                      connection.attachStreams.forEach(function(aStream, idx) {
                          if (stream.streamid === aStream.streamid) {
                              delete connection.attachStreams[idx];
                          }
                      });
  
                      var newStreamsArray = [];
                      connection.attachStreams.forEach(function(aStream) {
                          if (aStream) {
                              newStreamsArray.push(aStream);
                          }
                      });
                      connection.attachStreams = newStreamsArray;
  
                      var streamEvent = connection.streamEvents[stream.streamid];
  
                      if (streamEvent) {
                          connection.onstreamended(streamEvent);
                          return;
                      }
                      if (this.parentNode) {
                          this.parentNode.removeChild(this);
                      }
                  }
              }, false);
          }
  
          var played = mediaElement.play();
          if (typeof played !== 'undefined') {
              var cbFired = false;
              setTimeout(function() {
                  if (!cbFired) {
                      cbFired = true;
                      callback(mediaElement);
                  }
              }, 1000);
              played.then(function() {
                  if (cbFired) return;
                  cbFired = true;
                  callback(mediaElement);
              }).catch(function(error) {
                  if (cbFired) return;
                  cbFired = true;
                  callback(mediaElement);
              });
          } else {
              callback(mediaElement);
          }
      }
  
      // if IE
      if (!window.addEventListener) {
          window.addEventListener = function(el, eventName, eventHandler) {
              if (!el.attachEvent) {
                  return;
              }
              el.attachEvent('on' + eventName, eventHandler);
          };
      }
  
      function listenEventHandler(eventName, eventHandler) {
          window.removeEventListener(eventName, eventHandler);
          window.addEventListener(eventName, eventHandler, false);
      }
  
      window.attachEventListener = function(video, type, listener, useCapture) {
          video.addEventListener(type, listener, useCapture);
      };
  
      function removeNullEntries(array) {
          var newArray = [];
          array.forEach(function(item) {
              if (item) {
                  newArray.push(item);
              }
          });
          return newArray;
      }
  
  
      function isData(session) {
          return !session.audio && !session.video && !session.screen && session.data;
      }
  
      function isNull(obj) {
          return typeof obj === 'undefined';
      }
  
      function isString(obj) {
          return typeof obj === 'string';
      }
  
      var MediaStream = window.MediaStream;
  
      if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
          MediaStream = webkitMediaStream;
      }
  
      /*global MediaStream:true */
      if (typeof MediaStream !== 'undefined') {
          if (!('stop' in MediaStream.prototype)) {
              MediaStream.prototype.stop = function() {
                  this.getTracks().forEach(function(track) {
                      track.stop();
                  });
              };
          }
      }
  
      function isAudioPlusTab(connection, audioPlusTab) {
          if (connection.session.audio && connection.session.audio === 'two-way') {
              return false;
          }
  
          if (DetectRTC.browser.name === 'Firefox' && audioPlusTab !== false) {
              return true;
          }
  
          if (DetectRTC.browser.name !== 'Chrome' || DetectRTC.browser.version < 50) return false;
  
          if (typeof audioPlusTab === true) {
              return true;
          }
  
          if (typeof audioPlusTab === 'undefined' && connection.session.audio && connection.session.screen && !connection.session.video) {
              audioPlusTab = true;
              return true;
          }
  
          return false;
      }
  
      function getAudioScreenConstraints(screen_constraints) {
          if (DetectRTC.browser.name === 'Firefox') {
              return true;
          }
  
          if (DetectRTC.browser.name !== 'Chrome') return false;
  
          return {
              mandatory: {
                  chromeMediaSource: screen_constraints.mandatory.chromeMediaSource,
                  chromeMediaSourceId: screen_constraints.mandatory.chromeMediaSourceId
              }
          };
      }
  
      window.iOSDefaultAudioOutputDevice = window.iOSDefaultAudioOutputDevice || 'speaker'; // earpiece or speaker
  
      function getTracks(stream, kind) {
          if (!stream || !stream.getTracks) {
              return [];
          }
  
          return stream.getTracks().filter(function(t) {
              return t.kind === (kind || 'audio');
          });
      }
  
      function isUnifiedPlanSupportedDefault() {
          var canAddTransceiver = false;
  
          try {
              if (typeof RTCRtpTransceiver === 'undefined') return false;
              if (!('currentDirection' in RTCRtpTransceiver.prototype)) return false;
  
              var tempPc = new RTCPeerConnection();
  
              try {
                  tempPc.addTransceiver('audio');
                  canAddTransceiver = true;
              } catch (e) {}
  
              tempPc.close();
          } catch (e) {
              canAddTransceiver = false;
          }
  
          return canAddTransceiver && isUnifiedPlanSuppored();
      }
  
      function isUnifiedPlanSuppored() {
          var isUnifiedPlanSupported = false;
  
          try {
              var pc = new RTCPeerConnection({
                  sdpSemantics: 'unified-plan'
              });
  
              try {
                  var config = pc.getConfiguration();
                  if (config.sdpSemantics == 'unified-plan')
                      isUnifiedPlanSupported = true;
                  else if (config.sdpSemantics == 'plan-b')
                      isUnifiedPlanSupported = false;
                  else
                      isUnifiedPlanSupported = false;
              } catch (e) {
                  isUnifiedPlanSupported = false;
              }
          } catch (e) {
              isUnifiedPlanSupported = false;
          }
  
          return isUnifiedPlanSupported;
      }
  
      // ios-hacks.js
  
      function setCordovaAPIs() {
          // if (DetectRTC.osName !== 'iOS') return;
          if (typeof cordova === 'undefined' || typeof cordova.plugins === 'undefined' || typeof cordova.plugins.iosrtc === 'undefined') return;
  
          var iosrtc = cordova.plugins.iosrtc;
          window.webkitRTCPeerConnection = iosrtc.RTCPeerConnection;
          window.RTCSessionDescription = iosrtc.RTCSessionDescription;
          window.RTCIceCandidate = iosrtc.RTCIceCandidate;
          window.MediaStream = iosrtc.MediaStream;
          window.MediaStreamTrack = iosrtc.MediaStreamTrack;
          navigator.getUserMedia = navigator.webkitGetUserMedia = iosrtc.getUserMedia;
  
          iosrtc.debug.enable('iosrtc*');
          if (typeof iosrtc.selectAudioOutput == 'function') {
              iosrtc.selectAudioOutput(window.iOSDefaultAudioOutputDevice || 'speaker'); // earpiece or speaker
          }
          iosrtc.registerGlobals();
      }
  
      document.addEventListener('deviceready', setCordovaAPIs, false);
      setCordovaAPIs();
  
      // RTCPeerConnection.js
  
      var defaults = {};
  
      function setSdpConstraints(config) {
          var sdpConstraints = {
              OfferToReceiveAudio: !!config.OfferToReceiveAudio,
              OfferToReceiveVideo: !!config.OfferToReceiveVideo
          };
  
          return sdpConstraints;
      }
  
      var RTCPeerConnection;
      if (typeof window.RTCPeerConnection !== 'undefined') {
          RTCPeerConnection = window.RTCPeerConnection;
      } else if (typeof mozRTCPeerConnection !== 'undefined') {
          RTCPeerConnection = mozRTCPeerConnection;
      } else if (typeof webkitRTCPeerConnection !== 'undefined') {
          RTCPeerConnection = webkitRTCPeerConnection;
      }
  
      var RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
      var RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
      var MediaStreamTrack = window.MediaStreamTrack;
  
      function PeerInitiator(config) {
          if (typeof window.RTCPeerConnection !== 'undefined') {
              RTCPeerConnection = window.RTCPeerConnection;
          } else if (typeof mozRTCPeerConnection !== 'undefined') {
              RTCPeerConnection = mozRTCPeerConnection;
          } else if (typeof webkitRTCPeerConnection !== 'undefined') {
              RTCPeerConnection = webkitRTCPeerConnection;
          }
  
          RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
          RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
          MediaStreamTrack = window.MediaStreamTrack;
  
          if (!RTCPeerConnection) {
              throw 'WebRTC 1.0 (RTCPeerConnection) API are NOT available in this browser.';
          }
  
          var connection = config.rtcMultiConnection;
  
          this.extra = config.remoteSdp ? config.remoteSdp.extra : connection.extra;
          this.userid = config.userid;
          this.streams = [];
          this.channels = config.channels || [];
          this.connectionDescription = config.connectionDescription;
  
          this.addStream = function(session) {
              connection.addStream(session, self.userid);
          };
  
          this.removeStream = function(streamid) {
              connection.removeStream(streamid, self.userid);
          };
  
          var self = this;
  
          if (config.remoteSdp) {
              this.connectionDescription = config.remoteSdp.connectionDescription;
          }
  
          var allRemoteStreams = {};
  
          defaults.sdpConstraints = setSdpConstraints({
              OfferToReceiveAudio: true,
              OfferToReceiveVideo: true
          });
  
          var peer;
  
          var renegotiatingPeer = !!config.renegotiatingPeer;
          if (config.remoteSdp) {
              renegotiatingPeer = !!config.remoteSdp.renegotiatingPeer;
          }
  
          var localStreams = [];
          connection.attachStreams.forEach(function(stream) {
              if (!!stream) {
                  localStreams.push(stream);
              }
          });
  
          if (!renegotiatingPeer) {
              var iceTransports = 'all';
              if (connection.candidates.turn || connection.candidates.relay) {
                  if (!connection.candidates.stun && !connection.candidates.reflexive && !connection.candidates.host) {
                      iceTransports = 'relay';
                  }
              }
  
              try {
                  // ref: developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration
                  var params = {
                      iceServers: connection.iceServers,
                      iceTransportPolicy: connection.iceTransportPolicy || iceTransports
                  };
  
                  if (typeof connection.iceCandidatePoolSize !== 'undefined') {
                      params.iceCandidatePoolSize = connection.iceCandidatePoolSize;
                  }
  
                  if (typeof connection.bundlePolicy !== 'undefined') {
                      params.bundlePolicy = connection.bundlePolicy;
                  }
  
                  if (typeof connection.rtcpMuxPolicy !== 'undefined') {
                      params.rtcpMuxPolicy = connection.rtcpMuxPolicy;
                  }
  
                  if (!!connection.sdpSemantics) {
                      params.sdpSemantics = connection.sdpSemantics || 'unified-plan';
                  }
  
                  if (!connection.iceServers || !connection.iceServers.length) {
                      params = null;
                      connection.optionalArgument = null;
                  }
  
                  peer = new RTCPeerConnection(params, connection.optionalArgument);
              } catch (e) {
                  try {
                      var params = {
                          iceServers: connection.iceServers
                      };
  
                      peer = new RTCPeerConnection(params);
                  } catch (e) {
                      peer = new RTCPeerConnection();
                  }
              }
          } else {
              peer = config.peerRef;
          }
  
          if (!peer.getRemoteStreams && peer.getReceivers) {
              peer.getRemoteStreams = function() {
                  var stream = new MediaStream();
                  peer.getReceivers().forEach(function(receiver) {
                      stream.addTrack(receiver.track);
                  });
                  return [stream];
              };
          }
  
          if (!peer.getLocalStreams && peer.getSenders) {
              peer.getLocalStreams = function() {
                  var stream = new MediaStream();
                  peer.getSenders().forEach(function(sender) {
                      stream.addTrack(sender.track);
                  });
                  return [stream];
              };
          }
  
          peer.onicecandidate = function(event) {
              if (!event.candidate) {
                  if (!connection.trickleIce) {
                      var localSdp = peer.localDescription;
                      config.onLocalSdp({
                          type: localSdp.type,
                          sdp: localSdp.sdp,
                          remotePeerSdpConstraints: config.remotePeerSdpConstraints || false,
                          renegotiatingPeer: !!config.renegotiatingPeer || false,
                          connectionDescription: self.connectionDescription,
                          dontGetRemoteStream: !!config.dontGetRemoteStream,
                          extra: connection ? connection.extra : {},
                          streamsToShare: streamsToShare
                      });
                  }
                  return;
              }
  
              if (!connection.trickleIce) return;
              config.onLocalCandidate({
                  candidate: event.candidate.candidate,
                  sdpMid: event.candidate.sdpMid,
                  sdpMLineIndex: event.candidate.sdpMLineIndex
              });
          };
  
          localStreams.forEach(function(localStream) {
              if (config.remoteSdp && config.remoteSdp.remotePeerSdpConstraints && config.remoteSdp.remotePeerSdpConstraints.dontGetRemoteStream) {
                  return;
              }
  
              if (config.dontAttachLocalStream) {
                  return;
              }
  
              localStream = connection.beforeAddingStream(localStream, self);
  
              if (!localStream) return;
  
              peer.getLocalStreams().forEach(function(stream) {
                  if (localStream && stream.id == localStream.id) {
                      localStream = null;
                  }
              });
  
              if (localStream && localStream.getTracks) {
                  localStream.getTracks().forEach(function(track) {
                      try {
                          // last parameter is redundant for unified-plan
                          // starting from chrome version 72
                          peer.addTrack(track, localStream);
                      } catch (e) {}
                  });
              }
          });
  
          peer.oniceconnectionstatechange = peer.onsignalingstatechange = function() {
              var extra = self.extra;
              if (connection.peers[self.userid]) {
                  extra = connection.peers[self.userid].extra || extra;
              }
  
              if (!peer) {
                  return;
              }
  
              config.onPeerStateChanged({
                  iceConnectionState: peer.iceConnectionState,
                  iceGatheringState: peer.iceGatheringState,
                  signalingState: peer.signalingState,
                  extra: extra,
                  userid: self.userid
              });
  
              if (peer && peer.iceConnectionState && peer.iceConnectionState.search(/closed|failed/gi) !== -1 && self.streams instanceof Array) {
                  self.streams.forEach(function(stream) {
                      var streamEvent = connection.streamEvents[stream.id] || {
                          streamid: stream.id,
                          stream: stream,
                          type: 'remote'
                      };
  
                      connection.onstreamended(streamEvent);
                  });
              }
          };
  
          var sdpConstraints = {
              OfferToReceiveAudio: !!localStreams.length,
              OfferToReceiveVideo: !!localStreams.length
          };
  
          if (config.localPeerSdpConstraints) sdpConstraints = config.localPeerSdpConstraints;
  
          defaults.sdpConstraints = setSdpConstraints(sdpConstraints);
  
          var streamObject;
          var dontDuplicate = {};
  
          peer.ontrack = function(event) {
              if (!event || event.type !== 'track') return;
  
              event.stream = event.streams[event.streams.length - 1];
  
              if (!event.stream.id) {
                  event.stream.id = event.track.id;
              }
  
              if (dontDuplicate[event.stream.id] && DetectRTC.browser.name !== 'Safari') {
                  if (event.track) {
                      event.track.onended = function() { // event.track.onmute = 
                          peer && peer.onremovestream(event);
                      };
                  }
                  return;
              }
  
              dontDuplicate[event.stream.id] = event.stream.id;
  
              var streamsToShare = {};
              if (config.remoteSdp && config.remoteSdp.streamsToShare) {
                  streamsToShare = config.remoteSdp.streamsToShare;
              } else if (config.streamsToShare) {
                  streamsToShare = config.streamsToShare;
              }
  
              var streamToShare = streamsToShare[event.stream.id];
              if (streamToShare) {
                  event.stream.isAudio = streamToShare.isAudio;
                  event.stream.isVideo = streamToShare.isVideo;
                  event.stream.isScreen = streamToShare.isScreen;
              } else {
                  event.stream.isVideo = !!getTracks(event.stream, 'video').length;
                  event.stream.isAudio = !event.stream.isVideo;
                  event.stream.isScreen = false;
              }
  
              event.stream.streamid = event.stream.id;
  
              allRemoteStreams[event.stream.id] = event.stream;
              config.onRemoteStream(event.stream);
  
              event.stream.getTracks().forEach(function(track) {
                  track.onended = function() { // track.onmute = 
                      peer && peer.onremovestream(event);
                  };
              });
  
              event.stream.onremovetrack = function() {
                  peer && peer.onremovestream(event);
              };
          };
  
          peer.onremovestream = function(event) {
              // this event doesn't works anymore
              event.stream.streamid = event.stream.id;
  
              if (allRemoteStreams[event.stream.id]) {
                  delete allRemoteStreams[event.stream.id];
              }
  
              config.onRemoteStreamRemoved(event.stream);
          };
  
          if (typeof peer.removeStream !== 'function') {
              // removeStream backward compatibility
              peer.removeStream = function(stream) {
                  stream.getTracks().forEach(function(track) {
                      peer.removeTrack(track, stream);
                  });
              };
          }
  
          this.addRemoteCandidate = function(remoteCandidate) {
              peer.addIceCandidate(new RTCIceCandidate(remoteCandidate));
          };
  
          function oldAddRemoteSdp(remoteSdp, cb) {
              cb = cb || function() {};
  
              if (DetectRTC.browser.name !== 'Safari') {
                  remoteSdp.sdp = connection.processSdp(remoteSdp.sdp);
              }
              peer.setRemoteDescription(new RTCSessionDescription(remoteSdp), cb, function(error) {
                  if (!!connection.enableLogs) {
                      console.error('setRemoteDescription failed', '\n', error, '\n', remoteSdp.sdp);
                  }
  
                  cb();
              });
          }
  
          this.addRemoteSdp = function(remoteSdp, cb) {
              cb = cb || function() {};
  
              if (DetectRTC.browser.name !== 'Safari') {
                  remoteSdp.sdp = connection.processSdp(remoteSdp.sdp);
              }
  
              peer.setRemoteDescription(new RTCSessionDescription(remoteSdp)).then(cb, function(error) {
                  if (!!connection.enableLogs) {
                      console.error('setRemoteDescription failed', '\n', error, '\n', remoteSdp.sdp);
                  }
  
                  cb();
              }).catch(function(error) {
                  if (!!connection.enableLogs) {
                      console.error('setRemoteDescription failed', '\n', error, '\n', remoteSdp.sdp);
                  }
  
                  cb();
              });
          };
  
          var isOfferer = true;
  
          if (config.remoteSdp) {
              isOfferer = false;
          }
  
          this.createDataChannel = function() {
              var channel = peer.createDataChannel('sctp', {});
              setChannelEvents(channel);
          };
  
          if (connection.session.data === true && !renegotiatingPeer) {
              if (!isOfferer) {
                  peer.ondatachannel = function(event) {
                      var channel = event.channel;
                      setChannelEvents(channel);
                  };
              } else {
                  this.createDataChannel();
              }
          }
  
          this.enableDisableVideoEncoding = function(enable) {
              var rtcp;
              peer.getSenders().forEach(function(sender) {
                  if (!rtcp && sender.track.kind === 'video') {
                      rtcp = sender;
                  }
              });
  
              if (!rtcp || !rtcp.getParameters) return;
  
              var parameters = rtcp.getParameters();
              parameters.encodings[1] && (parameters.encodings[1].active = !!enable);
              parameters.encodings[2] && (parameters.encodings[2].active = !!enable);
              rtcp.setParameters(parameters);
          };
  
          if (config.remoteSdp) {
              if (config.remoteSdp.remotePeerSdpConstraints) {
                  sdpConstraints = config.remoteSdp.remotePeerSdpConstraints;
              }
              defaults.sdpConstraints = setSdpConstraints(sdpConstraints);
              this.addRemoteSdp(config.remoteSdp, function() {
                  createOfferOrAnswer('createAnswer');
              });
          }
  
          function setChannelEvents(channel) {
              // force ArrayBuffer in Firefox; which uses "Blob" by default.
              channel.binaryType = 'arraybuffer';
  
              channel.onmessage = function(event) {
                  config.onDataChannelMessage(event.data);
              };
  
              channel.onopen = function() {
                  config.onDataChannelOpened(channel);
              };
  
              channel.onerror = function(error) {
                  config.onDataChannelError(error);
              };
  
              channel.onclose = function(event) {
                  config.onDataChannelClosed(event);
              };
  
              channel.internalSend = channel.send;
              channel.send = function(data) {
                  if (channel.readyState !== 'open') {
                      return;
                  }
  
                  channel.internalSend(data);
              };
  
              peer.channel = channel;
          }
  
          if (connection.session.audio == 'two-way' || connection.session.video == 'two-way' || connection.session.screen == 'two-way') {
              defaults.sdpConstraints = setSdpConstraints({
                  OfferToReceiveAudio: connection.session.audio == 'two-way' || (config.remoteSdp && config.remoteSdp.remotePeerSdpConstraints && config.remoteSdp.remotePeerSdpConstraints.OfferToReceiveAudio),
                  OfferToReceiveVideo: connection.session.video == 'two-way' || connection.session.screen == 'two-way' || (config.remoteSdp && config.remoteSdp.remotePeerSdpConstraints && config.remoteSdp.remotePeerSdpConstraints.OfferToReceiveAudio)
              });
          }
  
          var streamsToShare = {};
          peer.getLocalStreams().forEach(function(stream) {
              streamsToShare[stream.streamid] = {
                  isAudio: !!stream.isAudio,
                  isVideo: !!stream.isVideo,
                  isScreen: !!stream.isScreen
              };
          });
  
          function oldCreateOfferOrAnswer(_method) {
              peer[_method](function(localSdp) {
                  if (DetectRTC.browser.name !== 'Safari') {
                      localSdp.sdp = connection.processSdp(localSdp.sdp);
                  }
                  peer.setLocalDescription(localSdp, function() {
                      if (!connection.trickleIce) return;
  
                      config.onLocalSdp({
                          type: localSdp.type,
                          sdp: localSdp.sdp,
                          remotePeerSdpConstraints: config.remotePeerSdpConstraints || false,
                          renegotiatingPeer: !!config.renegotiatingPeer || false,
                          connectionDescription: self.connectionDescription,
                          dontGetRemoteStream: !!config.dontGetRemoteStream,
                          extra: connection ? connection.extra : {},
                          streamsToShare: streamsToShare
                      });
  
                      connection.onSettingLocalDescription(self);
                  }, function(error) {
                      if (!!connection.enableLogs) {
                          console.error('setLocalDescription-error', error);
                      }
                  });
              }, function(error) {
                  if (!!connection.enableLogs) {
                      console.error('sdp-' + _method + '-error', error);
                  }
              }, defaults.sdpConstraints);
          }
  
          function createOfferOrAnswer(_method) {
              peer[_method](defaults.sdpConstraints).then(function(localSdp) {
                  if (DetectRTC.browser.name !== 'Safari') {
                      localSdp.sdp = connection.processSdp(localSdp.sdp);
                  }
                  peer.setLocalDescription(localSdp).then(function() {
                      if (!connection.trickleIce) return;
  
                      config.onLocalSdp({
                          type: localSdp.type,
                          sdp: localSdp.sdp,
                          remotePeerSdpConstraints: config.remotePeerSdpConstraints || false,
                          renegotiatingPeer: !!config.renegotiatingPeer || false,
                          connectionDescription: self.connectionDescription,
                          dontGetRemoteStream: !!config.dontGetRemoteStream,
                          extra: connection ? connection.extra : {},
                          streamsToShare: streamsToShare
                      });
  
                      connection.onSettingLocalDescription(self);
                  }, function(error) {
                      if (!connection.enableLogs) return;
                      console.error('setLocalDescription error', error);
                  });
              }, function(error) {
                  if (!!connection.enableLogs) {
                      console.error('sdp-error', error);
                  }
              });
          }
  
          if (isOfferer) {
              createOfferOrAnswer('createOffer');
          }
  
          peer.nativeClose = peer.close;
          peer.close = function() {
              if (!peer) {
                  return;
              }
  
              try {
                  if (peer.nativeClose !== peer.close) {
                      peer.nativeClose();
                  }
              } catch (e) {}
  
              peer = null;
              self.peer = null;
          };
  
          this.peer = peer;
      }
  
      // CodecsHandler.js
  
      var CodecsHandler = (function() {
          // use "RTCRtpTransceiver.setCodecPreferences"
          function preferCodec(sdp, codecName) {
              var info = splitLines(sdp);
  
              if (!info.videoCodecNumbers) {
                  return sdp;
              }
  
              if (codecName === 'vp8' && info.vp8LineNumber === info.videoCodecNumbers[0]) {
                  return sdp;
              }
  
              if (codecName === 'vp9' && info.vp9LineNumber === info.videoCodecNumbers[0]) {
                  return sdp;
              }
  
              if (codecName === 'h264' && info.h264LineNumber === info.videoCodecNumbers[0]) {
                  return sdp;
              }
  
              sdp = preferCodecHelper(sdp, codecName, info);
  
              return sdp;
          }
  
          function preferCodecHelper(sdp, codec, info, ignore) {
              var preferCodecNumber = '';
  
              if (codec === 'vp8') {
                  if (!info.vp8LineNumber) {
                      return sdp;
                  }
                  preferCodecNumber = info.vp8LineNumber;
              }
  
              if (codec === 'vp9') {
                  if (!info.vp9LineNumber) {
                      return sdp;
                  }
                  preferCodecNumber = info.vp9LineNumber;
              }
  
              if (codec === 'h264') {
                  if (!info.h264LineNumber) {
                      return sdp;
                  }
  
                  preferCodecNumber = info.h264LineNumber;
              }
  
              var newLine = info.videoCodecNumbersOriginal.split('SAVPF')[0] + 'SAVPF ';
  
              var newOrder = [preferCodecNumber];
  
              if (ignore) {
                  newOrder = [];
              }
  
              info.videoCodecNumbers.forEach(function(codecNumber) {
                  if (codecNumber === preferCodecNumber) return;
                  newOrder.push(codecNumber);
              });
  
              newLine += newOrder.join(' ');
  
              sdp = sdp.replace(info.videoCodecNumbersOriginal, newLine);
              return sdp;
          }
  
          function splitLines(sdp) {
              var info = {};
              sdp.split('\n').forEach(function(line) {
                  if (line.indexOf('m=video') === 0) {
                      info.videoCodecNumbers = [];
                      line.split('SAVPF')[1].split(' ').forEach(function(codecNumber) {
                          codecNumber = codecNumber.trim();
                          if (!codecNumber || !codecNumber.length) return;
                          info.videoCodecNumbers.push(codecNumber);
                          info.videoCodecNumbersOriginal = line;
                      });
                  }
  
                  if (line.indexOf('VP8/90000') !== -1 && !info.vp8LineNumber) {
                      info.vp8LineNumber = line.replace('a=rtpmap:', '').split(' ')[0];
                  }
  
                  if (line.indexOf('VP9/90000') !== -1 && !info.vp9LineNumber) {
                      info.vp9LineNumber = line.replace('a=rtpmap:', '').split(' ')[0];
                  }
  
                  if (line.indexOf('H264/90000') !== -1 && !info.h264LineNumber) {
                      info.h264LineNumber = line.replace('a=rtpmap:', '').split(' ')[0];
                  }
              });
  
              return info;
          }
  
          function removeVPX(sdp) {
              var info = splitLines(sdp);
  
              // last parameter below means: ignore these codecs
              sdp = preferCodecHelper(sdp, 'vp9', info, true);
              sdp = preferCodecHelper(sdp, 'vp8', info, true);
  
              return sdp;
          }
  
          function disableNACK(sdp) {
              if (!sdp || typeof sdp !== 'string') {
                  throw 'Invalid arguments.';
              }
  
              sdp = sdp.replace('a=rtcp-fb:126 nack\r\n', '');
              sdp = sdp.replace('a=rtcp-fb:126 nack pli\r\n', 'a=rtcp-fb:126 pli\r\n');
              sdp = sdp.replace('a=rtcp-fb:97 nack\r\n', '');
              sdp = sdp.replace('a=rtcp-fb:97 nack pli\r\n', 'a=rtcp-fb:97 pli\r\n');
  
              return sdp;
          }
  
          function prioritize(codecMimeType, peer) {
              if (!peer || !peer.getSenders || !peer.getSenders().length) {
                  return;
              }
  
              if (!codecMimeType || typeof codecMimeType !== 'string') {
                  throw 'Invalid arguments.';
              }
  
              peer.getSenders().forEach(function(sender) {
                  var params = sender.getParameters();
                  for (var i = 0; i < params.codecs.length; i++) {
                      if (params.codecs[i].mimeType == codecMimeType) {
                          params.codecs.unshift(params.codecs.splice(i, 1));
                          break;
                      }
                  }
                  sender.setParameters(params);
              });
          }
  
          function removeNonG722(sdp) {
              return sdp.replace(/m=audio ([0-9]+) RTP\/SAVPF ([0-9 ]*)/g, 'm=audio $1 RTP\/SAVPF 9');
          }
  
          function setBAS(sdp, bandwidth, isScreen) {
              if (!bandwidth) {
                  return sdp;
              }
  
              if (typeof isFirefox !== 'undefined' && isFirefox) {
                  return sdp;
              }
  
              if (isScreen) {
                  if (!bandwidth.screen) {
                      console.warn('It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.');
                  } else if (bandwidth.screen < 300) {
                      console.warn('It seems that you are using wrong bandwidth value for screen. Screen sharing is expected to fail.');
                  }
              }
  
              // if screen; must use at least 300kbs
              if (bandwidth.screen && isScreen) {
                  sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, '');
                  sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + bandwidth.screen + '\r\n');
              }
  
              // remove existing bandwidth lines
              if (bandwidth.audio || bandwidth.video) {
                  sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, '');
              }
  
              if (bandwidth.audio) {
                  sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n');
              }
  
              if (bandwidth.screen) {
                  sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + bandwidth.screen + '\r\n');
              } else if (bandwidth.video) {
                  sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + bandwidth.video + '\r\n');
              }
  
              return sdp;
          }
  
          // Find the line in sdpLines that starts with |prefix|, and, if specified,
          // contains |substr| (case-insensitive search).
          function findLine(sdpLines, prefix, substr) {
              return findLineInRange(sdpLines, 0, -1, prefix, substr);
          }
  
          // Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix|
          // and, if specified, contains |substr| (case-insensitive search).
          function findLineInRange(sdpLines, startLine, endLine, prefix, substr) {
              var realEndLine = endLine !== -1 ? endLine : sdpLines.length;
              for (var i = startLine; i < realEndLine; ++i) {
                  if (sdpLines[i].indexOf(prefix) === 0) {
                      if (!substr ||
                          sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) {
                          return i;
                      }
                  }
              }
              return null;
          }
  
          // Gets the codec payload type from an a=rtpmap:X line.
          function getCodecPayloadType(sdpLine) {
              var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');
              var result = sdpLine.match(pattern);
              return (result && result.length === 2) ? result[1] : null;
          }
  
          function setVideoBitrates(sdp, params) {
              params = params || {};
              var xgoogle_min_bitrate = params.min;
              var xgoogle_max_bitrate = params.max;
  
              var sdpLines = sdp.split('\r\n');
  
              // VP8
              var vp8Index = findLine(sdpLines, 'a=rtpmap', 'VP8/90000');
              var vp8Payload;
              if (vp8Index) {
                  vp8Payload = getCodecPayloadType(sdpLines[vp8Index]);
              }
  
              if (!vp8Payload) {
                  return sdp;
              }
  
              var rtxIndex = findLine(sdpLines, 'a=rtpmap', 'rtx/90000');
              var rtxPayload;
              if (rtxIndex) {
                  rtxPayload = getCodecPayloadType(sdpLines[rtxIndex]);
              }
  
              if (!rtxIndex) {
                  return sdp;
              }
  
              var rtxFmtpLineIndex = findLine(sdpLines, 'a=fmtp:' + rtxPayload.toString());
              if (rtxFmtpLineIndex !== null) {
                  var appendrtxNext = '\r\n';
                  appendrtxNext += 'a=fmtp:' + vp8Payload + ' x-google-min-bitrate=' + (xgoogle_min_bitrate || '228') + '; x-google-max-bitrate=' + (xgoogle_max_bitrate || '228');
                  sdpLines[rtxFmtpLineIndex] = sdpLines[rtxFmtpLineIndex].concat(appendrtxNext);
                  sdp = sdpLines.join('\r\n');
              }
  
              return sdp;
          }
  
          function setOpusAttributes(sdp, params) {
              params = params || {};
  
              var sdpLines = sdp.split('\r\n');
  
              // Opus
              var opusIndex = findLine(sdpLines, 'a=rtpmap', 'opus/48000');
              var opusPayload;
              if (opusIndex) {
                  opusPayload = getCodecPayloadType(sdpLines[opusIndex]);
              }
  
              if (!opusPayload) {
                  return sdp;
              }
  
              var opusFmtpLineIndex = findLine(sdpLines, 'a=fmtp:' + opusPayload.toString());
              if (opusFmtpLineIndex === null) {
                  return sdp;
              }
  
              var appendOpusNext = '';
              appendOpusNext += '; stereo=' + (typeof params.stereo != 'undefined' ? params.stereo : '1');
              appendOpusNext += '; sprop-stereo=' + (typeof params['sprop-stereo'] != 'undefined' ? params['sprop-stereo'] : '1');
  
              if (typeof params.maxaveragebitrate != 'undefined') {
                  appendOpusNext += '; maxaveragebitrate=' + (params.maxaveragebitrate || 128 * 1024 * 8);
              }
  
              if (typeof params.maxplaybackrate != 'undefined') {
                  appendOpusNext += '; maxplaybackrate=' + (params.maxplaybackrate || 128 * 1024 * 8);
              }
  
              if (typeof params.cbr != 'undefined') {
                  appendOpusNext += '; cbr=' + (typeof params.cbr != 'undefined' ? params.cbr : '1');
              }
  
              if (typeof params.useinbandfec != 'undefined') {
                  appendOpusNext += '; useinbandfec=' + params.useinbandfec;
              }
  
              if (typeof params.usedtx != 'undefined') {
                  appendOpusNext += '; usedtx=' + params.usedtx;
              }
  
              if (typeof params.maxptime != 'undefined') {
                  appendOpusNext += '\r\na=maxptime:' + params.maxptime;
              }
  
              sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].concat(appendOpusNext);
  
              sdp = sdpLines.join('\r\n');
              return sdp;
          }
  
          // forceStereoAudio => via webrtcexample.com
          // requires getUserMedia => echoCancellation:false
          function forceStereoAudio(sdp) {
              var sdpLines = sdp.split('\r\n');
              var fmtpLineIndex = null;
              for (var i = 0; i < sdpLines.length; i++) {
                  if (sdpLines[i].search('opus/48000') !== -1) {
                      var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
                      break;
                  }
              }
              for (var i = 0; i < sdpLines.length; i++) {
                  if (sdpLines[i].search('a=fmtp') !== -1) {
                      var payload = extractSdp(sdpLines[i], /a=fmtp:(\d+)/);
                      if (payload === opusPayload) {
                          fmtpLineIndex = i;
                          break;
                      }
                  }
              }
              if (fmtpLineIndex === null) return sdp;
              sdpLines[fmtpLineIndex] = sdpLines[fmtpLineIndex].concat('; stereo=1; sprop-stereo=1');
              sdp = sdpLines.join('\r\n');
              return sdp;
          }
  
          return {
              removeVPX: removeVPX,
              disableNACK: disableNACK,
              prioritize: prioritize,
              removeNonG722: removeNonG722,
              setApplicationSpecificBandwidth: function(sdp, bandwidth, isScreen) {
                  return setBAS(sdp, bandwidth, isScreen);
              },
              setVideoBitrates: function(sdp, params) {
                  return setVideoBitrates(sdp, params);
              },
              setOpusAttributes: function(sdp, params) {
                  return setOpusAttributes(sdp, params);
              },
              preferVP9: function(sdp) {
                  return preferCodec(sdp, 'vp9');
              },
              preferCodec: preferCodec,
              forceStereoAudio: forceStereoAudio
          };
      })();
  
      // backward compatibility
      window.BandwidthHandler = CodecsHandler;
  
      // OnIceCandidateHandler.js
  
      var OnIceCandidateHandler = (function() {
          function processCandidates(connection, icePair) {
              var candidate = icePair.candidate;
  
              var iceRestrictions = connection.candidates;
              var stun = iceRestrictions.stun;
              var turn = iceRestrictions.turn;
  
              if (!isNull(iceRestrictions.reflexive)) {
                  stun = iceRestrictions.reflexive;
              }
  
              if (!isNull(iceRestrictions.relay)) {
                  turn = iceRestrictions.relay;
              }
  
              if (!iceRestrictions.host && !!candidate.match(/typ host/g)) {
                  return;
              }
  
              if (!turn && !!candidate.match(/typ relay/g)) {
                  return;
              }
  
              if (!stun && !!candidate.match(/typ srflx/g)) {
                  return;
              }
  
              var protocol = connection.iceProtocols;
  
              if (!protocol.udp && !!candidate.match(/ udp /g)) {
                  return;
              }
  
              if (!protocol.tcp && !!candidate.match(/ tcp /g)) {
                  return;
              }
  
              if (connection.enableLogs) {
                  console.debug('Your candidate pairs:', candidate);
              }
  
              return {
                  candidate: candidate,
                  sdpMid: icePair.sdpMid,
                  sdpMLineIndex: icePair.sdpMLineIndex
              };
          }
  
          return {
              processCandidates: processCandidates
          };
      })();
  
      // IceServersHandler.js
  
      var IceServersHandler = (function() {
          function getIceServers(connection) {
              // resiprocate: 3344+4433
              // pions: 7575
              var iceServers = [{
                  'urls': [
                      'stun:stun.l.google.com:19302',
                      'stun:stun1.l.google.com:19302',
                      'stun:stun2.l.google.com:19302',
                      'stun:stun.l.google.com:19302?transport=udp',
                  ]
              }];
  
              return iceServers;
          }
  
          return {
              getIceServers: getIceServers
          };
      })();
  
      // getUserMediaHandler.js
  
      function setStreamType(constraints, stream) {
          if (constraints.mandatory && constraints.mandatory.chromeMediaSource) {
              stream.isScreen = true;
          } else if (constraints.mozMediaSource || constraints.mediaSource) {
              stream.isScreen = true;
          } else if (constraints.video) {
              stream.isVideo = true;
          } else if (constraints.audio) {
              stream.isAudio = true;
          }
      }
  
      // allow users to manage this object (to support re-capturing of screen/etc.)
      window.currentUserMediaRequest = {
          streams: [],
          mutex: false,
          queueRequests: [],
          remove: function(idInstance) {
              this.mutex = false;
  
              var stream = this.streams[idInstance];
              if (!stream) {
                  return;
              }
  
              stream = stream.stream;
  
              var options = stream.currentUserMediaRequestOptions;
  
              if (this.queueRequests.indexOf(options)) {
                  delete this.queueRequests[this.queueRequests.indexOf(options)];
                  this.queueRequests = removeNullEntries(this.queueRequests);
              }
  
              this.streams[idInstance].stream = null;
              delete this.streams[idInstance];
          }
      };
  
      function getUserMediaHandler(options) {
          if (currentUserMediaRequest.mutex === true) {
              currentUserMediaRequest.queueRequests.push(options);
              return;
          }
          currentUserMediaRequest.mutex = true;
  
          // easy way to match
          var idInstance = JSON.stringify(options.localMediaConstraints);
  
          function streaming(stream, returnBack) {
              setStreamType(options.localMediaConstraints, stream);
  
              var streamEndedEvent = 'ended';
  
              if ('oninactive' in stream) {
                  streamEndedEvent = 'inactive';
              }
              stream.addEventListener(streamEndedEvent, function() {
                  delete currentUserMediaRequest.streams[idInstance];
  
                  currentUserMediaRequest.mutex = false;
                  if (currentUserMediaRequest.queueRequests.indexOf(options)) {
                      delete currentUserMediaRequest.queueRequests[currentUserMediaRequest.queueRequests.indexOf(options)];
                      currentUserMediaRequest.queueRequests = removeNullEntries(currentUserMediaRequest.queueRequests);
                  }
              }, false);
  
              currentUserMediaRequest.streams[idInstance] = {
                  stream: stream
              };
              currentUserMediaRequest.mutex = false;
  
              if (currentUserMediaRequest.queueRequests.length) {
                  getUserMediaHandler(currentUserMediaRequest.queueRequests.shift());
              }
  
              // callback
              options.onGettingLocalMedia(stream, returnBack);
          }
  
          if (currentUserMediaRequest.streams[idInstance]) {
              streaming(currentUserMediaRequest.streams[idInstance].stream, true);
          } else {
              var isBlackBerry = !!(/BB10|BlackBerry/i.test(navigator.userAgent || ''));
              if (isBlackBerry || typeof navigator.mediaDevices === 'undefined' || typeof navigator.mediaDevices.getUserMedia !== 'function') {
                  navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
                  navigator.getUserMedia(options.localMediaConstraints, function(stream) {
                      stream.streamid = stream.streamid || stream.id || getRandomString();
                      stream.idInstance = idInstance;
                      streaming(stream);
                  }, function(error) {
                      options.onLocalMediaError(error, options.localMediaConstraints);
                  });
                  return;
              }
  
              if (typeof navigator.mediaDevices === 'undefined') {
                  navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
                  var getUserMediaSuccess = function() {};
                  var getUserMediaFailure = function() {};
  
                  var getUserMediaStream, getUserMediaError;
                  navigator.mediaDevices = {
                      getUserMedia: function(hints) {
                          navigator.getUserMedia(hints, function(getUserMediaSuccess) {
                              getUserMediaSuccess(stream);
                              getUserMediaStream = stream;
                          }, function(error) {
                              getUserMediaFailure(error);
                              getUserMediaError = error;
                          });
  
                          return {
                              then: function(successCB) {
                                  if (getUserMediaStream) {
                                      successCB(getUserMediaStream);
                                      return;
                                  }
  
                                  getUserMediaSuccess = successCB;
  
                                  return {
                                      then: function(failureCB) {
                                          if (getUserMediaError) {
                                              failureCB(getUserMediaError);
                                              return;
                                          }
  
                                          getUserMediaFailure = failureCB;
                                      }
                                  }
                              }
                          }
                      }
                  };
              }
  
              if (options.localMediaConstraints.isScreen === true) {
                  if (navigator.mediaDevices.getDisplayMedia) {
                      navigator.mediaDevices.getDisplayMedia(options.localMediaConstraints).then(function(stream) {
                          stream.streamid = stream.streamid || stream.id || getRandomString();
                          stream.idInstance = idInstance;
  
                          streaming(stream);
                      }).catch(function(error) {
                          options.onLocalMediaError(error, options.localMediaConstraints);
                      });
                  } else if (navigator.getDisplayMedia) {
                      navigator.getDisplayMedia(options.localMediaConstraints).then(function(stream) {
                          stream.streamid = stream.streamid || stream.id || getRandomString();
                          stream.idInstance = idInstance;
  
                          streaming(stream);
                      }).catch(function(error) {
                          options.onLocalMediaError(error, options.localMediaConstraints);
                      });
                  } else {
                      throw new Error('getDisplayMedia API is not availabe in this browser.');
                  }
                  return;
              }
  
              navigator.mediaDevices.getUserMedia(options.localMediaConstraints).then(function(stream) {
                  stream.streamid = stream.streamid || stream.id || getRandomString();
                  stream.idInstance = idInstance;
  
                  streaming(stream);
              }).catch(function(error) {
                  options.onLocalMediaError(error, options.localMediaConstraints);
              });
          }
      }
  
      // StreamsHandler.js
  
      var StreamsHandler = (function() {
          function handleType(type) {
              if (!type) {
                  return;
              }
  
              if (typeof type === 'string' || typeof type === 'undefined') {
                  return type;
              }
  
              if (type.audio && type.video) {
                  return null;
              }
  
              if (type.audio) {
                  return 'audio';
              }
  
              if (type.video) {
                  return 'video';
              }
  
              return;
          }
  
          function setHandlers(stream, syncAction, connection) {
              if (!stream || !stream.addEventListener) return;
  
              if (typeof syncAction == 'undefined' || syncAction == true) {
                  var streamEndedEvent = 'ended';
  
                  if ('oninactive' in stream) {
                      streamEndedEvent = 'inactive';
                  }
  
                  stream.addEventListener(streamEndedEvent, function() {
                      StreamsHandler.onSyncNeeded(this.streamid, streamEndedEvent);
                  }, false);
              }
  
              stream.mute = function(type, isSyncAction) {
                  type = handleType(type);
  
                  if (typeof isSyncAction !== 'undefined') {
                      syncAction = isSyncAction;
                  }
  
                  if (typeof type == 'undefined' || type == 'audio') {
                      getTracks(stream, 'audio').forEach(function(track) {
                          track.enabled = false;
                          connection.streamEvents[stream.streamid].isAudioMuted = true;
                      });
                  }
  
                  if (typeof type == 'undefined' || type == 'video') {
                      getTracks(stream, 'video').forEach(function(track) {
                          track.enabled = false;
                      });
                  }
  
                  if (typeof syncAction == 'undefined' || syncAction == true) {
                      StreamsHandler.onSyncNeeded(stream.streamid, 'mute', type);
                  }
  
                  connection.streamEvents[stream.streamid].muteType = type || 'both';
  
                  fireEvent(stream, 'mute', type);
              };
  
              stream.unmute = function(type, isSyncAction) {
                  type = handleType(type);
  
                  if (typeof isSyncAction !== 'undefined') {
                      syncAction = isSyncAction;
                  }
  
                  graduallyIncreaseVolume();
  
                  if (typeof type == 'undefined' || type == 'audio') {
                      getTracks(stream, 'audio').forEach(function(track) {
                          track.enabled = true;
                          connection.streamEvents[stream.streamid].isAudioMuted = false;
                      });
                  }
  
                  if (typeof type == 'undefined' || type == 'video') {
                      getTracks(stream, 'video').forEach(function(track) {
                          track.enabled = true;
                      });
  
                      // make sure that video unmute doesn't affects audio
                      if (typeof type !== 'undefined' && type == 'video' && connection.streamEvents[stream.streamid].isAudioMuted) {
                          (function looper(times) {
                              if (!times) {
                                  times = 0;
                              }
  
                              times++;
  
                              // check until five-seconds
                              if (times < 100 && connection.streamEvents[stream.streamid].isAudioMuted) {
                                  stream.mute('audio');
  
                                  setTimeout(function() {
                                      looper(times);
                                  }, 50);
                              }
                          })();
                      }
                  }
  
                  if (typeof syncAction == 'undefined' || syncAction == true) {
                      StreamsHandler.onSyncNeeded(stream.streamid, 'unmute', type);
                  }
  
                  connection.streamEvents[stream.streamid].unmuteType = type || 'both';
  
                  fireEvent(stream, 'unmute', type);
              };
  
              function graduallyIncreaseVolume() {
                  if (!connection.streamEvents[stream.streamid].mediaElement) {
                      return;
                  }
  
                  var mediaElement = connection.streamEvents[stream.streamid].mediaElement;
                  mediaElement.volume = 0;
                  afterEach(200, 5, function() {
                      try {
                          mediaElement.volume += .20;
                      } catch (e) {
                          mediaElement.volume = 1;
                      }
                  });
              }
          }
  
          function afterEach(setTimeoutInteval, numberOfTimes, callback, startedTimes) {
              startedTimes = (startedTimes || 0) + 1;
              if (startedTimes >= numberOfTimes) return;
  
              setTimeout(function() {
                  callback();
                  afterEach(setTimeoutInteval, numberOfTimes, callback, startedTimes);
              }, setTimeoutInteval);
          }
  
          return {
              setHandlers: setHandlers,
              onSyncNeeded: function(streamid, action, type) {}
          };
      })();
  
      // TextReceiver.js & TextSender.js
  
      function TextReceiver(connection) {
          var content = {};
  
          function receive(data, userid, extra) {
              // uuid is used to uniquely identify sending instance
              var uuid = data.uuid;
              if (!content[uuid]) {
                  content[uuid] = [];
              }
  
              content[uuid].push(data.message);
  
              if (data.last) {
                  var message = content[uuid].join('');
                  if (data.isobject) {
                      message = JSON.parse(message);
                  }
  
                  // latency detection
                  var receivingTime = new Date().getTime();
                  var latency = receivingTime - data.sendingTime;
  
                  var e = {
                      data: message,
                      userid: userid,
                      extra: extra,
                      latency: latency
                  };
  
                  if (connection.autoTranslateText) {
                      e.original = e.data;
                      connection.Translator.TranslateText(e.data, function(translatedText) {
                          e.data = translatedText;
                          connection.onmessage(e);
                      });
                  } else {
                      connection.onmessage(e);
                  }
  
                  delete content[uuid];
              }
          }
  
          return {
              receive: receive
          };
      }
  
      // TextSender.js
      var TextSender = {
          send: function(config) {
              var connection = config.connection;
  
              var channel = config.channel,
                  remoteUserId = config.remoteUserId,
                  initialText = config.text,
                  packetSize = connection.chunkSize || 1000,
                  textToTransfer = '',
                  isobject = false;
  
              if (!isString(initialText)) {
                  isobject = true;
                  initialText = JSON.stringify(initialText);
              }
  
              // uuid is used to uniquely identify sending instance
              var uuid = getRandomString();
              var sendingTime = new Date().getTime();
  
              sendText(initialText);
  
              function sendText(textMessage, text) {
                  var data = {
                      type: 'text',
                      uuid: uuid,
                      sendingTime: sendingTime
                  };
  
                  if (textMessage) {
                      text = textMessage;
                      data.packets = parseInt(text.length / packetSize);
                  }
  
                  if (text.length > packetSize) {
                      data.message = text.slice(0, packetSize);
                  } else {
                      data.message = text;
                      data.last = true;
                      data.isobject = isobject;
                  }
  
                  channel.send(data, remoteUserId);
  
                  textToTransfer = text.slice(data.message.length);
  
                  if (textToTransfer.length) {
                      setTimeout(function() {
                          sendText(null, textToTransfer);
                      }, connection.chunkInterval || 100);
                  }
              }
          }
      };
  
      // FileProgressBarHandler.js
  
      var FileProgressBarHandler = (function() {
          function handle(connection) {
              var progressHelper = {};
  
              // www.RTCMultiConnection.org/docs/onFileStart/
              connection.onFileStart = function(file) {
                  var div = document.createElement('div');
                  div.title = file.name;
                  div.innerHTML = '<label>0%</label> <progress></progress>';
  
                  if (file.remoteUserId) {
                      div.innerHTML += ' (Sharing with:' + file.remoteUserId + ')';
                  }
  
                  if (!connection.filesContainer) {
                      connection.filesContainer = document.body || document.documentElement;
                  }
  
                  connection.filesContainer.insertBefore(div, connection.filesContainer.firstChild);
  
                  if (!file.remoteUserId) {
                      progressHelper[file.uuid] = {
                          div: div,
                          progress: div.querySelector('progress'),
                          label: div.querySelector('label')
                      };
                      progressHelper[file.uuid].progress.max = file.maxChunks;
                      return;
                  }
  
                  if (!progressHelper[file.uuid]) {
                      progressHelper[file.uuid] = {};
                  }
  
                  progressHelper[file.uuid][file.remoteUserId] = {
                      div: div,
                      progress: div.querySelector('progress'),
                      label: div.querySelector('label')
                  };
                  progressHelper[file.uuid][file.remoteUserId].progress.max = file.maxChunks;
              };
  
              // www.RTCMultiConnection.org/docs/onFileProgress/
              connection.onFileProgress = function(chunk) {
                  var helper = progressHelper[chunk.uuid];
                  if (!helper) {
                      return;
                  }
                  if (chunk.remoteUserId) {
                      helper = progressHelper[chunk.uuid][chunk.remoteUserId];
                      if (!helper) {
                          return;
                      }
                  }
  
                  helper.progress.value = chunk.currentPosition || chunk.maxChunks || helper.progress.max;
                  updateLabel(helper.progress, helper.label);
              };
  
              // www.RTCMultiConnection.org/docs/onFileEnd/
              connection.onFileEnd = function(file) {
                  var helper = progressHelper[file.uuid];
                  if (!helper) {
                      console.error('No such progress-helper element exist.', file);
                      return;
                  }
  
                  if (file.remoteUserId) {
                      helper = progressHelper[file.uuid][file.remoteUserId];
                      if (!helper) {
                          return;
                      }
                  }
  
                  var div = helper.div;
                  if (file.type.indexOf('image') != -1) {
                      div.innerHTML = '<a href="' + file.url + '" download="' + file.name + '">Download <strong style="color:red;">' + file.name + '</strong> </a><br /><img src="' + file.url + '" title="' + file.name + '" style="max-width: 80%;">';
                  } else {
                      div.innerHTML = '<a href="' + file.url + '" download="' + file.name + '">Download <strong style="color:red;">' + file.name + '</strong> </a><br /><iframe src="' + file.url + '" title="' + file.name + '" style="width: 80%;border: 0;height: inherit;margin-top:1em;"></iframe>';
                  }
              };
  
              function updateLabel(progress, label) {
                  if (progress.position === -1) {
                      return;
                  }
  
                  var position = +progress.position.toFixed(2).split('.')[1] || 100;
                  label.innerHTML = position + '%';
              }
          }
  
          return {
              handle: handle
          };
      })();
  
      // TranslationHandler.js
  
      var TranslationHandler = (function() {
          function handle(connection) {
              connection.autoTranslateText = false;
              connection.language = 'en';
              connection.googKey = 'AIzaSyCgB5hmFY74WYB-EoWkhr9cAGr6TiTHrEE';
  
              // www.RTCMultiConnection.org/docs/Translator/
              connection.Translator = {
                  TranslateText: function(text, callback) {
                      // if(location.protocol === 'https:') return callback(text);
  
                      var newScript = document.createElement('script');
                      newScript.type = 'text/javascript';
  
                      var sourceText = encodeURIComponent(text); // escape
  
                      var randomNumber = 'method' + connection.token();
                      window[randomNumber] = function(response) {
                          if (response.data && response.data.translations[0] && callback) {
                              callback(response.data.translations[0].translatedText);
                              return;
                          }
  
                          if (response.error && response.error.message === 'Daily Limit Exceeded') {
                              console.error('Text translation failed. Error message: "Daily Limit Exceeded."');
                              return;
                          }
  
                          if (response.error) {
                              console.error(response.error.message);
                              return;
                          }
  
                          console.error(response);
                      };
  
                      var source = 'https://www.googleapis.com/language/translate/v2?key=' + connection.googKey + '&target=' + (connection.language || 'en-US') + '&callback=window.' + randomNumber + '&q=' + sourceText;
                      newScript.src = source;
                      document.getElementsByTagName('head')[0].appendChild(newScript);
                  },
                  getListOfLanguages: function(callback) {
                      var xhr = new XMLHttpRequest();
                      xhr.onreadystatechange = function() {
                          if (xhr.readyState == XMLHttpRequest.DONE) {
                              var response = JSON.parse(xhr.responseText);
  
                              if (response && response.data && response.data.languages) {
                                  callback(response.data.languages);
                                  return;
                              }
  
                              if (response.error && response.error.message === 'Daily Limit Exceeded') {
                                  console.error('Text translation failed. Error message: "Daily Limit Exceeded."');
                                  return;
                              }
  
                              if (response.error) {
                                  console.error(response.error.message);
                                  return;
                              }
  
                              console.error(response);
                          }
                      }
                      var url = 'https://www.googleapis.com/language/translate/v2/languages?key=' + connection.googKey + '&target=en';
                      xhr.open('GET', url, true);
                      xhr.send(null);
                  }
              };
          }
  
          return {
              handle: handle
          };
      })();
  
      // _____________________
      // RTCMultiConnection.js
  
      (function(connection) {
          forceOptions = forceOptions || {
              useDefaultDevices: true
          };
  
          connection.channel = connection.sessionid = (roomid || location.href.replace(/\/|:|#|\?|\$|\^|%|\.|`|~|!|\+|@|\[|\||]|\|*. /g, '').split('\n').join('').split('\r').join('')) + '';
  
          var mPeer = new MultiPeers(connection);
  
          var preventDuplicateOnStreamEvents = {};
          mPeer.onGettingLocalMedia = function(stream, callback) {
              callback = callback || function() {};
  
              if (preventDuplicateOnStreamEvents[stream.streamid]) {
                  callback();
                  return;
              }
              preventDuplicateOnStreamEvents[stream.streamid] = true;
  
              try {
                  stream.type = 'local';
              } catch (e) {}
  
              connection.setStreamEndHandler(stream);
  
              getRMCMediaElement(stream, function(mediaElement) {
                  mediaElement.id = stream.streamid;
                  mediaElement.muted = true;
                  mediaElement.volume = 0;
  
                  if (connection.attachStreams.indexOf(stream) === -1) {
                      connection.attachStreams.push(stream);
                  }
  
                  if (typeof StreamsHandler !== 'undefined') {
                      StreamsHandler.setHandlers(stream, true, connection);
                  }
                  var isAudioMuted = stream.getAudioTracks().filter(function(track) {
                      return track.enabled;
                  }).length === 0;
  
                  connection.streamEvents[stream.streamid] = {
                      stream: stream,
                      type: 'local',
                      mediaElement: mediaElement,
                      userid: connection.userid,
                      extra: connection.extra,
                      streamid: stream.streamid,
                      isAudioMuted: isAudioMuted
                  };
  
                  try {
                      setHarkEvents(connection, connection.streamEvents[stream.streamid]);
                      setMuteHandlers(connection, connection.streamEvents[stream.streamid]);
  
                      connection.onstream(connection.streamEvents[stream.streamid]);
                  } catch (e) {
                      //
                  }
  
                  callback();
              }, connection);
          };
  
          mPeer.onGettingRemoteMedia = function(stream, remoteUserId) {
              try {
                  stream.type = 'remote';
              } catch (e) {}
  
              connection.setStreamEndHandler(stream, 'remote-stream');
  
              getRMCMediaElement(stream, function(mediaElement) {
                  mediaElement.id = stream.streamid;
  
                  if (typeof StreamsHandler !== 'undefined') {
                      StreamsHandler.setHandlers(stream, false, connection);
                  }
  
                  connection.streamEvents[stream.streamid] = {
                      stream: stream,
                      type: 'remote',
                      userid: remoteUserId,
                      extra: connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra : {},
                      mediaElement: mediaElement,
                      streamid: stream.streamid
                  };
  
                  setMuteHandlers(connection, connection.streamEvents[stream.streamid]);
  
                  connection.onstream(connection.streamEvents[stream.streamid]);
              }, connection);
          };
  
          mPeer.onRemovingRemoteMedia = function(stream, remoteUserId) {
              var streamEvent = connection.streamEvents[stream.streamid];
              if (!streamEvent) {
                  streamEvent = {
                      stream: stream,
                      type: 'remote',
                      userid: remoteUserId,
                      extra: connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra : {},
                      streamid: stream.streamid,
                      mediaElement: connection.streamEvents[stream.streamid] ? connection.streamEvents[stream.streamid].mediaElement : null
                  };
              }
  
              if (connection.peersBackup[streamEvent.userid]) {
                  streamEvent.extra = connection.peersBackup[streamEvent.userid].extra;
              }
  
              connection.onstreamended(streamEvent);
  
              delete connection.streamEvents[stream.streamid];
          };
  
          mPeer.onNegotiationNeeded = function(message, remoteUserId, callback) {
              callback = callback || function() {};
  
              remoteUserId = remoteUserId || message.remoteUserId;
              message = message || '';
  
              // usually a message looks like this
              var messageToDeliver = {
                  remoteUserId: remoteUserId,
                  message: message,
                  sender: connection.userid
              };
  
              if (message.remoteUserId && message.message && message.sender) {
                  // if a code is manually passing required data
                  messageToDeliver = message;
              }
  
              connectSocket(function() {
                  connection.socket.emit(connection.socketMessageEvent, messageToDeliver, callback);
              });
          };
  
          function onUserLeft(remoteUserId) {
              connection.deletePeer(remoteUserId);
          }
  
          mPeer.onUserLeft = onUserLeft;
          mPeer.disconnectWith = function(remoteUserId, callback) {
              if (connection.socket) {
                  connection.socket.emit('disconnect-with', remoteUserId, callback || function() {});
              }
  
              connection.deletePeer(remoteUserId);
          };
  
          connection.socketOptions = {
              // 'force new connection': true, // For SocketIO version < 1.0
              // 'forceNew': true, // For SocketIO version >= 1.0
              'transport': 'polling' // fixing transport:unknown issues
          };
  
          function connectSocket(connectCallback) {
              connection.socketAutoReConnect = true;
  
              if (connection.socket) { // todo: check here readySate/etc. to make sure socket is still opened
                  if (connectCallback) {
                      connectCallback(connection.socket);
                  }
                  return;
              }
  
              if (typeof SocketConnection === 'undefined') {
                  if (typeof FirebaseConnection !== 'undefined') {
                      window.SocketConnection = FirebaseConnection;
                  } else if (typeof PubNubConnection !== 'undefined') {
                      window.SocketConnection = PubNubConnection;
                  } else {
                      throw 'SocketConnection.js seems missed.';
                  }
              }
  
              new SocketConnection(connection, function(s) {
                  if (connectCallback) {
                      connectCallback(connection.socket);
                  }
              });
          }
  
          // 1st paramter is roomid
          // 2rd paramter is a callback function
          connection.openOrJoin = function(roomid, callback) {
              callback = callback || function() {};
  
              connection.checkPresence(roomid, function(isRoomExist, roomid) {
                  if (isRoomExist) {
                      connection.sessionid = roomid;
  
                      var localPeerSdpConstraints = false;
                      var remotePeerSdpConstraints = false;
                      var isOneWay = !!connection.session.oneway;
                      var isDataOnly = isData(connection.session);
  
                      remotePeerSdpConstraints = {
                          OfferToReceiveAudio: connection.sdpConstraints.mandatory.OfferToReceiveAudio,
                          OfferToReceiveVideo: connection.sdpConstraints.mandatory.OfferToReceiveVideo
                      }
  
                      localPeerSdpConstraints = {
                          OfferToReceiveAudio: isOneWay ? !!connection.session.audio : connection.sdpConstraints.mandatory.OfferToReceiveAudio,
                          OfferToReceiveVideo: isOneWay ? !!connection.session.video || !!connection.session.screen : connection.sdpConstraints.mandatory.OfferToReceiveVideo
                      }
  
                      var connectionDescription = {
                          remoteUserId: connection.sessionid,
                          message: {
                              newParticipationRequest: true,
                              isOneWay: isOneWay,
                              isDataOnly: isDataOnly,
                              localPeerSdpConstraints: localPeerSdpConstraints,
                              remotePeerSdpConstraints: remotePeerSdpConstraints
                          },
                          sender: connection.userid
                      };
  
                      beforeJoin(connectionDescription.message, function() {
                          joinRoom(connectionDescription, callback);
                      });
                      return;
                  }
  
                  connection.waitingForLocalMedia = true;
                  connection.isInitiator = true;
  
                  connection.sessionid = roomid || connection.sessionid;
  
                  if (isData(connection.session)) {
                      openRoom(callback);
                      return;
                  }
  
                  connection.captureUserMedia(function() {
                      openRoom(callback);
                  });
              });
          };
  
          // don't allow someone to join this person until he has the media
          connection.waitingForLocalMedia = false;
  
          connection.open = function(roomid, callback) {
              callback = callback || function() {};
  
              connection.waitingForLocalMedia = true;
              connection.isInitiator = true;
  
              connection.sessionid = roomid || connection.sessionid;
  
              connectSocket(function() {
                  if (isData(connection.session)) {
                      openRoom(callback);
                      return;
                  }
  
                  connection.captureUserMedia(function() {
                      openRoom(callback);
                  });
              });
          };
  
          // this object keeps extra-data records for all connected users
          // this object is never cleared so you can always access extra-data even if a user left
          connection.peersBackup = {};
  
          connection.deletePeer = function(remoteUserId) {
              if (!remoteUserId || !connection.peers[remoteUserId]) {
                  return;
              }
  
              var eventObject = {
                  userid: remoteUserId,
                  extra: connection.peers[remoteUserId] ? connection.peers[remoteUserId].extra : {}
              };
  
              if (connection.peersBackup[eventObject.userid]) {
                  eventObject.extra = connection.peersBackup[eventObject.userid].extra;
              }
  
              connection.onleave(eventObject);
  
              if (!!connection.peers[remoteUserId]) {
                  connection.peers[remoteUserId].streams.forEach(function(stream) {
                      stream.stop();
                  });
  
                  var peer = connection.peers[remoteUserId].peer;
                  if (peer && peer.iceConnectionState !== 'closed') {
                      try {
                          peer.close();
                      } catch (e) {}
                  }
  
                  if (connection.peers[remoteUserId]) {
                      connection.peers[remoteUserId].peer = null;
                      delete connection.peers[remoteUserId];
                  }
              }
          }
  
          connection.rejoin = function(connectionDescription) {
              if (connection.isInitiator || !connectionDescription || !Object.keys(connectionDescription).length) {
                  return;
              }
  
              var extra = {};
  
              if (connection.peers[connectionDescription.remoteUserId]) {
                  extra = connection.peers[connectionDescription.remoteUserId].extra;
                  connection.deletePeer(connectionDescription.remoteUserId);
              }
  
              if (connectionDescription && connectionDescription.remoteUserId) {
                  connection.join(connectionDescription.remoteUserId);
  
                  connection.onReConnecting({
                      userid: connectionDescription.remoteUserId,
                      extra: extra
                  });
              }
          };
  
          connection.join = function(remoteUserId, options) {
              connection.sessionid = (remoteUserId ? remoteUserId.sessionid || remoteUserId.remoteUserId || remoteUserId : false) || connection.sessionid;
              connection.sessionid += '';
  
              var localPeerSdpConstraints = false;
              var remotePeerSdpConstraints = false;
              var isOneWay = false;
              var isDataOnly = false;
  
              if ((remoteUserId && remoteUserId.session) || !remoteUserId || typeof remoteUserId === 'string') {
                  var session = remoteUserId ? remoteUserId.session || connection.session : connection.session;
  
                  isOneWay = !!session.oneway;
                  isDataOnly = isData(session);
  
                  remotePeerSdpConstraints = {
                      OfferToReceiveAudio: connection.sdpConstraints.mandatory.OfferToReceiveAudio,
                      OfferToReceiveVideo: connection.sdpConstraints.mandatory.OfferToReceiveVideo
                  };
  
                  localPeerSdpConstraints = {
                      OfferToReceiveAudio: isOneWay ? !!connection.session.audio : connection.sdpConstraints.mandatory.OfferToReceiveAudio,
                      OfferToReceiveVideo: isOneWay ? !!connection.session.video || !!connection.session.screen : connection.sdpConstraints.mandatory.OfferToReceiveVideo
                  };
              }
  
              options = options || {};
  
              var cb = function() {};
              if (typeof options === 'function') {
                  cb = options;
                  options = {};
              }
  
              if (typeof options.localPeerSdpConstraints !== 'undefined') {
                  localPeerSdpConstraints = options.localPeerSdpConstraints;
              }
  
              if (typeof options.remotePeerSdpConstraints !== 'undefined') {
                  remotePeerSdpConstraints = options.remotePeerSdpConstraints;
              }
  
              if (typeof options.isOneWay !== 'undefined') {
                  isOneWay = options.isOneWay;
              }
  
              if (typeof options.isDataOnly !== 'undefined') {
                  isDataOnly = options.isDataOnly;
              }
  
              var connectionDescription = {
                  remoteUserId: connection.sessionid,
                  message: {
                      newParticipationRequest: true,
                      isOneWay: isOneWay,
                      isDataOnly: isDataOnly,
                      localPeerSdpConstraints: localPeerSdpConstraints,
                      remotePeerSdpConstraints: remotePeerSdpConstraints
                  },
                  sender: connection.userid
              };
  
              beforeJoin(connectionDescription.message, function() {
                  connectSocket(function() {
                      joinRoom(connectionDescription, cb);
                  });
              });
              return connectionDescription;
          };
  
          function joinRoom(connectionDescription, cb) {
              connection.socket.emit('join-room', {
                  sessionid: connection.sessionid,
                  session: connection.session,
                  mediaConstraints: connection.mediaConstraints,
                  sdpConstraints: connection.sdpConstraints,
                  streams: getStreamInfoForAdmin(),
                  extra: connection.extra,
                  password: typeof connection.password !== 'undefined' && typeof connection.password !== 'object' ? connection.password : ''
              }, function(isRoomJoined, error) {
                  if (isRoomJoined === true) {
                      if (connection.enableLogs) {
                          console.log('isRoomJoined: ', isRoomJoined, ' roomid: ', connection.sessionid);
                      }
  
                      if (!!connection.peers[connection.sessionid]) {
                          // on socket disconnect & reconnect
                          return;
                      }
  
                      mPeer.onNegotiationNeeded(connectionDescription);
                  }
  
                  if (isRoomJoined === false) {
                      if (connection.enableLogs) {
                          console.warn('isRoomJoined: ', error, ' roomid: ', connection.sessionid);
                      }
  
                      // [disabled] retry after 3 seconds
                      false && setTimeout(function() {
                          joinRoom(connectionDescription, cb);
                      }, 3000);
                  }
  
                  cb(isRoomJoined, connection.sessionid, error);
              });
          }
  
          connection.publicRoomIdentifier = '';
  
          function openRoom(callback) {
              if (connection.enableLogs) {
                  console.log('Sending open-room signal to socket.io');
              }
  
              connection.waitingForLocalMedia = false;
              connection.socket.emit('open-room', {
                  sessionid: connection.sessionid,
                  session: connection.session,
                  mediaConstraints: connection.mediaConstraints,
                  sdpConstraints: connection.sdpConstraints,
                  streams: getStreamInfoForAdmin(),
                  extra: connection.extra,
                  identifier: connection.publicRoomIdentifier,
                  password: typeof connection.password !== 'undefined' && typeof connection.password !== 'object' ? connection.password : ''
              }, function(isRoomOpened, error) {
                  if (isRoomOpened === true) {
                      if (connection.enableLogs) {
                          console.log('isRoomOpened: ', isRoomOpened, ' roomid: ', connection.sessionid);
                      }
                      callback(isRoomOpened, connection.sessionid);
                  }
  
                  if (isRoomOpened === false) {
                      if (connection.enableLogs) {
                          console.warn('isRoomOpened: ', error, ' roomid: ', connection.sessionid);
                      }
  
                      callback(isRoomOpened, connection.sessionid, error);
                  }
              });
          }
  
          function getStreamInfoForAdmin() {
              try {
                  return connection.streamEvents.selectAll('local').map(function(event) {
                      return {
                          streamid: event.streamid,
                          tracks: event.stream.getTracks().length
                      };
                  });
              } catch (e) {
                  return [];
              }
          }
  
          function beforeJoin(userPreferences, callback) {
              if (connection.dontCaptureUserMedia || userPreferences.isDataOnly) {
                  callback();
                  return;
              }
  
              var localMediaConstraints = {};
  
              if (userPreferences.localPeerSdpConstraints.OfferToReceiveAudio) {
                  localMediaConstraints.audio = connection.mediaConstraints.audio;
              }
  
              if (userPreferences.localPeerSdpConstraints.OfferToReceiveVideo) {
                  localMediaConstraints.video = connection.mediaConstraints.video;
              }
  
              var session = userPreferences.session || connection.session;
  
              if (session.oneway && session.audio !== 'two-way' && session.video !== 'two-way' && session.screen !== 'two-way') {
                  callback();
                  return;
              }
  
              if (session.oneway && session.audio && session.audio === 'two-way') {
                  session = {
                      audio: true
                  };
              }
  
              if (session.audio || session.video || session.screen) {
                  if (session.screen) {
                      if (DetectRTC.browser.name === 'Edge') {
                          navigator.getDisplayMedia({
                              video: true,
                              audio: isAudioPlusTab(connection)
                          }).then(function(screen) {
                              screen.isScreen = true;
                              mPeer.onGettingLocalMedia(screen);
  
                              if ((session.audio || session.video) && !isAudioPlusTab(connection)) {
                                  connection.invokeGetUserMedia(null, callback);
                              } else {
                                  callback(screen);
                              }
                          }, function(error) {
                              console.error('Unable to capture screen on Edge. HTTPs and version 17+ is required.');
                          });
                      } else {
                          connection.invokeGetUserMedia({
                              audio: isAudioPlusTab(connection),
                              video: true,
                              isScreen: true
                          }, (session.audio || session.video) && !isAudioPlusTab(connection) ? connection.invokeGetUserMedia(null, callback) : callback);
                      }
                  } else if (session.audio || session.video) {
                      connection.invokeGetUserMedia(null, callback, session);
                  }
              }
          }
  
          connection.getUserMedia = connection.captureUserMedia = function(callback, sessionForced) {
              callback = callback || function() {};
              var session = sessionForced || connection.session;
  
              if (connection.dontCaptureUserMedia || isData(session)) {
                  callback();
                  return;
              }
  
              if (session.audio || session.video || session.screen) {
                  if (session.screen) {
                      if (DetectRTC.browser.name === 'Edge') {
                          navigator.getDisplayMedia({
                              video: true,
                              audio: isAudioPlusTab(connection)
                          }).then(function(screen) {
                              screen.isScreen = true;
                              mPeer.onGettingLocalMedia(screen);
  
                              if ((session.audio || session.video) && !isAudioPlusTab(connection)) {
                                  var nonScreenSession = {};
                                  for (var s in session) {
                                      if (s !== 'screen') {
                                          nonScreenSession[s] = session[s];
                                      }
                                  }
                                  connection.invokeGetUserMedia(sessionForced, callback, nonScreenSession);
                                  return;
                              }
                              callback(screen);
                          }, function(error) {
                              console.error('Unable to capture screen on Edge. HTTPs and version 17+ is required.');
                          });
                      } else {
                          connection.invokeGetUserMedia({
                              audio: isAudioPlusTab(connection),
                              video: true,
                              isScreen: true
                          }, function(stream) {
                              if ((session.audio || session.video) && !isAudioPlusTab(connection)) {
                                  var nonScreenSession = {};
                                  for (var s in session) {
                                      if (s !== 'screen') {
                                          nonScreenSession[s] = session[s];
                                      }
                                  }
                                  connection.invokeGetUserMedia(sessionForced, callback, nonScreenSession);
                                  return;
                              }
                              callback(stream);
                          });
                      }
                  } else if (session.audio || session.video) {
                      connection.invokeGetUserMedia(sessionForced, callback, session);
                  }
              }
          };
  
          connection.onbeforeunload = function(arg1, dontCloseSocket) {
              if (!connection.closeBeforeUnload) {
                  return;
              }
  
              connection.peers.getAllParticipants().forEach(function(participant) {
                  mPeer.onNegotiationNeeded({
                      userLeft: true
                  }, participant);
  
                  if (connection.peers[participant] && connection.peers[participant].peer) {
                      connection.peers[participant].peer.close();
                  }
  
                  delete connection.peers[participant];
              });
  
              if (!dontCloseSocket) {
                  connection.closeSocket();
              }
  
              connection.isInitiator = false;
          };
  
          if (!window.ignoreBeforeUnload) {
              // user can implement its own version of window.onbeforeunload
              connection.closeBeforeUnload = true;
              window.addEventListener('beforeunload', connection.onbeforeunload, false);
          } else {
              connection.closeBeforeUnload = false;
          }
  
          connection.userid = getRandomString();
          connection.changeUserId = function(newUserId, callback) {
              callback = callback || function() {};
              connection.userid = newUserId || getRandomString();
              connection.socket.emit('changed-uuid', connection.userid, callback);
          };
  
          connection.extra = {};
          connection.attachStreams = [];
  
          connection.session = {
              audio: true,
              video: true
          };
  
          connection.enableFileSharing = false;
  
          // all values in kbps
          connection.bandwidth = {
              screen: false,
              audio: false,
              video: false
          };
  
          connection.codecs = {
              audio: 'opus',
              video: 'VP9'
          };
  
          connection.processSdp = function(sdp) {
              // ignore SDP modification if unified-pan is supported
              if (isUnifiedPlanSupportedDefault()) {
                  return sdp;
              }
  
              if (DetectRTC.browser.name === 'Safari') {
                  return sdp;
              }
  
              if (connection.codecs.video.toUpperCase() === 'VP8') {
                  sdp = CodecsHandler.preferCodec(sdp, 'vp8');
              }
  
              if (connection.codecs.video.toUpperCase() === 'VP9') {
                  sdp = CodecsHandler.preferCodec(sdp, 'vp9');
              }
  
              if (connection.codecs.video.toUpperCase() === 'H264') {
                  sdp = CodecsHandler.preferCodec(sdp, 'h264');
              }
  
              if (connection.codecs.audio === 'G722') {
                  sdp = CodecsHandler.removeNonG722(sdp);
              }
  
              if (DetectRTC.browser.name === 'Firefox') {
                  return sdp;
              }
  
              if (connection.bandwidth.video || connection.bandwidth.screen) {
                  sdp = CodecsHandler.setApplicationSpecificBandwidth(sdp, connection.bandwidth, !!connection.session.screen);
              }
  
              if (connection.bandwidth.video) {
                  sdp = CodecsHandler.setVideoBitrates(sdp, {
                      min: connection.bandwidth.video * 8 * 1024,
                      max: connection.bandwidth.video * 8 * 1024
                  });
              }
  
              if (connection.bandwidth.audio) {
                  sdp = CodecsHandler.setOpusAttributes(sdp, {
                      maxaveragebitrate: connection.bandwidth.audio * 8 * 1024,
                      maxplaybackrate: connection.bandwidth.audio * 8 * 1024,
                      stereo: 1,
                      maxptime: 3
                  });
              }
  
              return sdp;
          };
  
          if (typeof CodecsHandler !== 'undefined') {
              connection.BandwidthHandler = connection.CodecsHandler = CodecsHandler;
          }
  
          connection.mediaConstraints = {
              audio: {
                  mandatory: {},
                  optional: connection.bandwidth.audio ? [{
                      bandwidth: connection.bandwidth.audio * 8 * 1024 || 128 * 8 * 1024
                  }] : []
              },
              video: {
                  mandatory: {},
                  optional: connection.bandwidth.video ? [{
                      bandwidth: connection.bandwidth.video * 8 * 1024 || 128 * 8 * 1024
                  }, {
                      facingMode: 'user'
                  }] : [{
                      facingMode: 'user'
                  }]
              }
          };
  
          if (DetectRTC.browser.name === 'Firefox') {
              connection.mediaConstraints = {
                  audio: true,
                  video: true
              };
          }
  
          if (!forceOptions.useDefaultDevices && !DetectRTC.isMobileDevice) {
              DetectRTC.load(function() {
                  var lastAudioDevice, lastVideoDevice;
                  // it will force RTCMultiConnection to capture last-devices
                  // i.e. if external microphone is attached to system, we should prefer it over built-in devices.
                  DetectRTC.MediaDevices.forEach(function(device) {
                      if (device.kind === 'audioinput' && connection.mediaConstraints.audio !== false) {
                          lastAudioDevice = device;
                      }
  
                      if (device.kind === 'videoinput' && connection.mediaConstraints.video !== false) {
                          lastVideoDevice = device;
                      }
                  });
  
                  if (lastAudioDevice) {
                      if (DetectRTC.browser.name === 'Firefox') {
                          if (connection.mediaConstraints.audio !== true) {
                              connection.mediaConstraints.audio.deviceId = lastAudioDevice.id;
                          } else {
                              connection.mediaConstraints.audio = {
                                  deviceId: lastAudioDevice.id
                              }
                          }
                          return;
                      }
  
                      if (connection.mediaConstraints.audio == true) {
                          connection.mediaConstraints.audio = {
                              mandatory: {},
                              optional: []
                          }
                      }
  
                      if (!connection.mediaConstraints.audio.optional) {
                          connection.mediaConstraints.audio.optional = [];
                      }
  
                      var optional = [{
                          sourceId: lastAudioDevice.id
                      }];
  
                      connection.mediaConstraints.audio.optional = optional.concat(connection.mediaConstraints.audio.optional);
                  }
  
                  if (lastVideoDevice) {
                      if (DetectRTC.browser.name === 'Firefox') {
                          if (connection.mediaConstraints.video !== true) {
                              connection.mediaConstraints.video.deviceId = lastVideoDevice.id;
                          } else {
                              connection.mediaConstraints.video = {
                                  deviceId: lastVideoDevice.id
                              }
                          }
                          return;
                      }
  
                      if (connection.mediaConstraints.video == true) {
                          connection.mediaConstraints.video = {
                              mandatory: {},
                              optional: []
                          }
                      }
  
                      if (!connection.mediaConstraints.video.optional) {
                          connection.mediaConstraints.video.optional = [];
                      }
  
                      var optional = [{
                          sourceId: lastVideoDevice.id
                      }];
  
                      connection.mediaConstraints.video.optional = optional.concat(connection.mediaConstraints.video.optional);
                  }
              });
          }
  
          connection.sdpConstraints = {
              mandatory: {
                  OfferToReceiveAudio: true,
                  OfferToReceiveVideo: true
              },
              optional: [{
                  VoiceActivityDetection: false
              }]
          };
  
          connection.sdpSemantics = null; // "unified-plan" or "plan-b", ref: webrtc.org/web-apis/chrome/unified-plan/
          connection.iceCandidatePoolSize = null; // 0
          connection.bundlePolicy = null; // max-bundle
          connection.rtcpMuxPolicy = null; // "require" or "negotiate"
          connection.iceTransportPolicy = null; // "relay" or "all"
          connection.optionalArgument = {
              optional: [{
                  DtlsSrtpKeyAgreement: true
              }, {
                  googImprovedWifiBwe: true
              }, {
                  googScreencastMinBitrate: 300
              }, {
                  googIPv6: true
              }, {
                  googDscp: true
              }, {
                  googCpuUnderuseThreshold: 55
              }, {
                  googCpuOveruseThreshold: 85
              }, {
                  googSuspendBelowMinBitrate: true
              }, {
                  googCpuOveruseDetection: true
              }],
              mandatory: {}
          };
  
          connection.iceServers = IceServersHandler.getIceServers(connection);
  
          connection.candidates = {
              host: true,
              stun: true,
              turn: true
          };
  
          connection.iceProtocols = {
              tcp: true,
              udp: true
          };
  
          // EVENTs
          connection.onopen = function(event) {
              if (!!connection.enableLogs) {
                  console.info('Data connection has been opened between you & ', event.userid);
              }
          };
  
          connection.onclose = function(event) {
              if (!!connection.enableLogs) {
                  console.warn('Data connection has been closed between you & ', event.userid);
              }
          };
  
          connection.onerror = function(error) {
              if (!!connection.enableLogs) {
                  console.error(error.userid, 'data-error', error);
              }
          };
  
          connection.onmessage = function(event) {
              if (!!connection.enableLogs) {
                  console.debug('data-message', event.userid, event.data);
              }
          };
  
          connection.send = function(data, remoteUserId) {
              connection.peers.send(data, remoteUserId);
          };
  
          connection.close = connection.disconnect = connection.leave = function() {
              connection.onbeforeunload(false, true);
          };
  
          connection.closeEntireSession = function(callback) {
              callback = callback || function() {};
              connection.socket.emit('close-entire-session', function looper() {
                  if (connection.getAllParticipants().length) {
                      setTimeout(looper, 100);
                      return;
                  }
  
                  connection.onEntireSessionClosed({
                      sessionid: connection.sessionid,
                      userid: connection.userid,
                      extra: connection.extra
                  });
  
                  connection.changeUserId(null, function() {
                      connection.close();
                      callback();
                  });
              });
          };
  
          connection.onEntireSessionClosed = function(event) {
              if (!connection.enableLogs) return;
              console.info('Entire session is closed: ', event.sessionid, event.extra);
          };
  
          connection.onstream = function(e) {
              var parentNode = connection.videosContainer;
              parentNode.insertBefore(e.mediaElement, parentNode.firstChild);
              var played = e.mediaElement.play();
  
              if (typeof played !== 'undefined') {
                  played.catch(function() {
                      /*** iOS 11 doesn't allow automatic play and rejects ***/
                  }).then(function() {
                      setTimeout(function() {
                          e.mediaElement.play();
                      }, 2000);
                  });
                  return;
              }
  
              setTimeout(function() {
                  e.mediaElement.play();
              }, 2000);
          };
  
          connection.onstreamended = function(e) {
              if (!e.mediaElement) {
                  e.mediaElement = document.getElementById(e.streamid);
              }
  
              if (!e.mediaElement || !e.mediaElement.parentNode) {
                  return;
              }
  
              e.mediaElement.parentNode.removeChild(e.mediaElement);
          };
  
          connection.direction = 'many-to-many';
  
          connection.removeStream = function(streamid, remoteUserId) {
              var stream;
              connection.attachStreams.forEach(function(localStream) {
                  if (localStream.id === streamid) {
                      stream = localStream;
                  }
              });
  
              if (!stream) {
                  console.warn('No such stream exist.', streamid);
                  return;
              }
  
              connection.peers.getAllParticipants().forEach(function(participant) {
                  if (remoteUserId && participant !== remoteUserId) {
                      return;
                  }
  
                  var user = connection.peers[participant];
                  try {
                      user.peer.removeStream(stream);
                  } catch (e) {}
              });
  
              connection.renegotiate();
          };
  
          connection.addStream = function(session, remoteUserId) {
              if (!!session.getTracks) {
                  if (connection.attachStreams.indexOf(session) === -1) {
                      if (!session.streamid) {
                          session.streamid = session.id;
                      }
  
                      connection.attachStreams.push(session);
                  }
                  connection.renegotiate(remoteUserId);
                  return;
              }
  
              if (isData(session)) {
                  connection.renegotiate(remoteUserId);
                  return;
              }
  
              if (session.audio || session.video || session.screen) {
                  if (session.screen) {
                      if (DetectRTC.browser.name === 'Edge') {
                          navigator.getDisplayMedia({
                              video: true,
                              audio: isAudioPlusTab(connection)
                          }).then(function(screen) {
                              screen.isScreen = true;
                              mPeer.onGettingLocalMedia(screen);
  
                              if ((session.audio || session.video) && !isAudioPlusTab(connection)) {
                                  connection.invokeGetUserMedia(null, function(stream) {
                                      gumCallback(stream);
                                  });
                              } else {
                                  gumCallback(screen);
                              }
                          }, function(error) {
                              console.error('Unable to capture screen on Edge. HTTPs and version 17+ is required.');
                          });
                      } else {
                          connection.invokeGetUserMedia({
                              audio: isAudioPlusTab(connection),
                              video: true,
                              isScreen: true
                          }, function(stream) {
                              if ((session.audio || session.video) && !isAudioPlusTab(connection)) {
                                  connection.invokeGetUserMedia(null, function(stream) {
                                      gumCallback(stream);
                                  });
                              } else {
                                  gumCallback(stream);
                              }
                          });
                      }
                  } else if (session.audio || session.video) {
                      connection.invokeGetUserMedia(null, gumCallback);
                  }
              }
  
              function gumCallback(stream) {
                  if (session.streamCallback) {
                      session.streamCallback(stream);
                  }
  
                  connection.renegotiate(remoteUserId);
              }
          };
  
          connection.invokeGetUserMedia = function(localMediaConstraints, callback, session) {
              if (!session) {
                  session = connection.session;
              }
  
              if (!localMediaConstraints) {
                  localMediaConstraints = connection.mediaConstraints;
              }
  
              getUserMediaHandler({
                  onGettingLocalMedia: function(stream) {
                      var videoConstraints = localMediaConstraints.video;
                      if (videoConstraints) {
                          if (videoConstraints.mediaSource || videoConstraints.mozMediaSource) {
                              stream.isScreen = true;
                          } else if (videoConstraints.mandatory && videoConstraints.mandatory.chromeMediaSource) {
                              stream.isScreen = true;
                          }
                      }
  
                      if (!stream.isScreen) {
                          stream.isVideo = !!getTracks(stream, 'video').length;
                          stream.isAudio = !stream.isVideo && getTracks(stream, 'audio').length;
                      }
  
                      mPeer.onGettingLocalMedia(stream, function() {
                          if (typeof callback === 'function') {
                              callback(stream);
                          }
                      });
                  },
                  onLocalMediaError: function(error, constraints) {
                      mPeer.onLocalMediaError(error, constraints);
                  },
                  localMediaConstraints: localMediaConstraints || {
                      audio: session.audio ? localMediaConstraints.audio : false,
                      video: session.video ? localMediaConstraints.video : false
                  }
              });
          };
  
          function applyConstraints(stream, mediaConstraints) {
              if (!stream) {
                  if (!!connection.enableLogs) {
                      console.error('No stream to applyConstraints.');
                  }
                  return;
              }
  
              if (mediaConstraints.audio) {
                  getTracks(stream, 'audio').forEach(function(track) {
                      track.applyConstraints(mediaConstraints.audio);
                  });
              }
  
              if (mediaConstraints.video) {
                  getTracks(stream, 'video').forEach(function(track) {
                      track.applyConstraints(mediaConstraints.video);
                  });
              }
          }
  
          connection.applyConstraints = function(mediaConstraints, streamid) {
              if (!MediaStreamTrack || !MediaStreamTrack.prototype.applyConstraints) {
                  alert('track.applyConstraints is NOT supported in your browser.');
                  return;
              }
  
              if (streamid) {
                  var stream;
                  if (connection.streamEvents[streamid]) {
                      stream = connection.streamEvents[streamid].stream;
                  }
                  applyConstraints(stream, mediaConstraints);
                  return;
              }
  
              connection.attachStreams.forEach(function(stream) {
                  applyConstraints(stream, mediaConstraints);
              });
          };
  
          function replaceTrack(track, remoteUserId, isVideoTrack) {
              if (remoteUserId) {
                  mPeer.replaceTrack(track, remoteUserId, isVideoTrack);
                  return;
              }
  
              connection.peers.getAllParticipants().forEach(function(participant) {
                  mPeer.replaceTrack(track, participant, isVideoTrack);
              });
          }
  
          connection.replaceTrack = function(session, remoteUserId, isVideoTrack) {
              session = session || {};
  
              if (!RTCPeerConnection.prototype.getSenders) {
                  connection.addStream(session);
                  return;
              }
  
              if (session instanceof MediaStreamTrack) {
                  replaceTrack(session, remoteUserId, isVideoTrack);
                  return;
              }
  
              if (session instanceof MediaStream) {
                  if (getTracks(session, 'video').length) {
                      replaceTrack(getTracks(session, 'video')[0], remoteUserId, true);
                  }
  
                  if (getTracks(session, 'audio').length) {
                      replaceTrack(getTracks(session, 'audio')[0], remoteUserId, false);
                  }
                  return;
              }
  
              if (isData(session)) {
                  throw 'connection.replaceTrack requires audio and/or video and/or screen.';
                  return;
              }
  
              if (session.audio || session.video || session.screen) {
                  if (session.screen) {
                      if (DetectRTC.browser.name === 'Edge') {
                          navigator.getDisplayMedia({
                              video: true,
                              audio: isAudioPlusTab(connection)
                          }).then(function(screen) {
                              screen.isScreen = true;
                              mPeer.onGettingLocalMedia(screen);
  
                              if ((session.audio || session.video) && !isAudioPlusTab(connection)) {
                                  connection.invokeGetUserMedia(null, gumCallback);
                              } else {
                                  gumCallback(screen);
                              }
                          }, function(error) {
                              console.error('Unable to capture screen on Edge. HTTPs and version 17+ is required.');
                          });
                      } else {
                          connection.invokeGetUserMedia({
                              audio: isAudioPlusTab(connection),
                              video: true,
                              isScreen: true
                          }, (session.audio || session.video) && !isAudioPlusTab(connection) ? connection.invokeGetUserMedia(null, gumCallback) : gumCallback);
                      }
                  } else if (session.audio || session.video) {
                      connection.invokeGetUserMedia(null, gumCallback);
                  }
              }
  
              function gumCallback(stream) {
                  connection.replaceTrack(stream, remoteUserId, isVideoTrack || session.video || session.screen);
              }
          };
  
          connection.resetTrack = function(remoteUsersIds, isVideoTrack) {
              if (!remoteUsersIds) {
                  remoteUsersIds = connection.getAllParticipants();
              }
  
              if (typeof remoteUsersIds == 'string') {
                  remoteUsersIds = [remoteUsersIds];
              }
  
              remoteUsersIds.forEach(function(participant) {
                  var peer = connection.peers[participant].peer;
  
                  if ((typeof isVideoTrack === 'undefined' || isVideoTrack === true) && peer.lastVideoTrack) {
                      connection.replaceTrack(peer.lastVideoTrack, participant, true);
                  }
  
                  if ((typeof isVideoTrack === 'undefined' || isVideoTrack === false) && peer.lastAudioTrack) {
                      connection.replaceTrack(peer.lastAudioTrack, participant, false);
                  }
              });
          };
  
          connection.renegotiate = function(remoteUserId) {
              if (remoteUserId) {
                  mPeer.renegotiatePeer(remoteUserId);
                  return;
              }
  
              connection.peers.getAllParticipants().forEach(function(participant) {
                  mPeer.renegotiatePeer(participant);
              });
          };
  
          connection.setStreamEndHandler = function(stream, isRemote) {
              if (!stream || !stream.addEventListener) return;
  
              isRemote = !!isRemote;
  
              if (stream.alreadySetEndHandler) {
                  return;
              }
              stream.alreadySetEndHandler = true;
  
              var streamEndedEvent = 'ended';
  
              if ('oninactive' in stream) {
                  streamEndedEvent = 'inactive';
              }
  
              stream.addEventListener(streamEndedEvent, function() {
                  if (stream.idInstance) {
                      currentUserMediaRequest.remove(stream.idInstance);
                  }
  
                  if (!isRemote) {
                      // reset attachStreams
                      var streams = [];
                      connection.attachStreams.forEach(function(s) {
                          if (s.id != stream.id) {
                              streams.push(s);
                          }
                      });
                      connection.attachStreams = streams;
                  }
  
                  // connection.renegotiate();
  
                  var streamEvent = connection.streamEvents[stream.streamid];
                  if (!streamEvent) {
                      streamEvent = {
                          stream: stream,
                          streamid: stream.streamid,
                          type: isRemote ? 'remote' : 'local',
                          userid: connection.userid,
                          extra: connection.extra,
                          mediaElement: connection.streamEvents[stream.streamid] ? connection.streamEvents[stream.streamid].mediaElement : null
                      };
                  }
  
                  if (isRemote && connection.peers[streamEvent.userid]) {
                      // reset remote "streams"
                      var peer = connection.peers[streamEvent.userid].peer;
                      var streams = [];
                      peer.getRemoteStreams().forEach(function(s) {
                          if (s.id != stream.id) {
                              streams.push(s);
                          }
                      });
                      connection.peers[streamEvent.userid].streams = streams;
                  }
  
                  if (streamEvent.userid === connection.userid && streamEvent.type === 'remote') {
                      return;
                  }
  
                  if (connection.peersBackup[streamEvent.userid]) {
                      streamEvent.extra = connection.peersBackup[streamEvent.userid].extra;
                  }
  
                  connection.onstreamended(streamEvent);
  
                  delete connection.streamEvents[stream.streamid];
              }, false);
          };
  
          connection.onMediaError = function(error, constraints) {
              if (!!connection.enableLogs) {
                  console.error(error, constraints);
              }
          };
  
          connection.autoCloseEntireSession = false;
  
          connection.filesContainer = connection.videosContainer = document.body || document.documentElement;
          connection.isInitiator = false;
  
          connection.shareFile = mPeer.shareFile;
          if (typeof FileProgressBarHandler !== 'undefined') {
              FileProgressBarHandler.handle(connection);
          }
  
          if (typeof TranslationHandler !== 'undefined') {
              TranslationHandler.handle(connection);
          }
  
          connection.token = getRandomString;
  
          connection.onNewParticipant = function(participantId, userPreferences) {
              connection.acceptParticipationRequest(participantId, userPreferences);
          };
  
          connection.acceptParticipationRequest = function(participantId, userPreferences) {
              if (userPreferences.successCallback) {
                  userPreferences.successCallback();
                  delete userPreferences.successCallback;
              }
  
              mPeer.createNewPeer(participantId, userPreferences);
          };
  
          if (typeof StreamsHandler !== 'undefined') {
              connection.StreamsHandler = StreamsHandler;
          }
  
          connection.onleave = function(userid) {};
  
          connection.invokeSelectFileDialog = function(callback) {
              var selector = new FileSelector();
              selector.accept = '*.*';
              selector.selectSingleFile(callback);
          };
  
          connection.onmute = function(e) {
              if (!e || !e.mediaElement) {
                  return;
              }
  
              if (e.muteType === 'both' || e.muteType === 'video') {
                  e.mediaElement.src = null;
                  var paused = e.mediaElement.pause();
                  if (typeof paused !== 'undefined') {
                      paused.then(function() {
                          e.mediaElement.poster = e.snapshot || 'https://cdn.webrtc-experiment.com/images/muted.png';
                      });
                  } else {
                      e.mediaElement.poster = e.snapshot || 'https://cdn.webrtc-experiment.com/images/muted.png';
                  }
              } else if (e.muteType === 'audio') {
                  e.mediaElement.muted = true;
              }
          };
  
          connection.onunmute = function(e) {
              if (!e || !e.mediaElement || !e.stream) {
                  return;
              }
  
              if (e.unmuteType === 'both' || e.unmuteType === 'video') {
                  e.mediaElement.poster = null;
                  e.mediaElement.srcObject = e.stream;
                  e.mediaElement.play();
              } else if (e.unmuteType === 'audio') {
                  e.mediaElement.muted = false;
              }
          };
  
          connection.onExtraDataUpdated = function(event) {
              event.status = 'online';
              connection.onUserStatusChanged(event, true);
          };
  
          connection.getAllParticipants = function(sender) {
              return connection.peers.getAllParticipants(sender);
          };
  
          if (typeof StreamsHandler !== 'undefined') {
              StreamsHandler.onSyncNeeded = function(streamid, action, type) {
                  connection.peers.getAllParticipants().forEach(function(participant) {
                      mPeer.onNegotiationNeeded({
                          streamid: streamid,
                          action: action,
                          streamSyncNeeded: true,
                          type: type || 'both'
                      }, participant);
                  });
              };
          }
  
          connection.connectSocket = function(callback) {
              connectSocket(callback);
          };
  
          connection.closeSocket = function() {
              try {
                  io.sockets = {};
              } catch (e) {};
  
              if (!connection.socket) return;
  
              if (typeof connection.socket.disconnect === 'function') {
                  connection.socket.disconnect();
              }
  
              if (typeof connection.socket.resetProps === 'function') {
                  connection.socket.resetProps();
              }
  
              connection.socket = null;
          };
  
          connection.getSocket = function(callback) {
              if (!callback && connection.enableLogs) {
                  console.warn('getSocket.callback paramter is required.');
              }
  
              callback = callback || function() {};
  
              if (!connection.socket) {
                  connectSocket(function() {
                      callback(connection.socket);
                  });
              } else {
                  callback(connection.socket);
              }
  
              return connection.socket; // callback is preferred over return-statement
          };
  
          connection.getRemoteStreams = mPeer.getRemoteStreams;
  
          var skipStreams = ['selectFirst', 'selectAll', 'forEach'];
  
          connection.streamEvents = {
              selectFirst: function(options) {
                  return connection.streamEvents.selectAll(options)[0];
              },
              selectAll: function(options) {
                  if (!options) {
                      // default will always be all streams
                      options = {
                          local: true,
                          remote: true,
                          isScreen: true,
                          isAudio: true,
                          isVideo: true
                      };
                  }
  
                  if (options == 'local') {
                      options = {
                          local: true
                      };
                  }
  
                  if (options == 'remote') {
                      options = {
                          remote: true
                      };
                  }
  
                  if (options == 'screen') {
                      options = {
                          isScreen: true
                      };
                  }
  
                  if (options == 'audio') {
                      options = {
                          isAudio: true
                      };
                  }
  
                  if (options == 'video') {
                      options = {
                          isVideo: true
                      };
                  }
  
                  var streams = [];
                  Object.keys(connection.streamEvents).forEach(function(key) {
                      var event = connection.streamEvents[key];
  
                      if (skipStreams.indexOf(key) !== -1) return;
                      var ignore = true;
  
                      if (options.local && event.type === 'local') {
                          ignore = false;
                      }
  
                      if (options.remote && event.type === 'remote') {
                          ignore = false;
                      }
  
                      if (options.isScreen && event.stream.isScreen) {
                          ignore = false;
                      }
  
                      if (options.isVideo && event.stream.isVideo) {
                          ignore = false;
                      }
  
                      if (options.isAudio && event.stream.isAudio) {
                          ignore = false;
                      }
  
                      if (options.userid && event.userid === options.userid) {
                          ignore = false;
                      }
  
                      if (ignore === false) {
                          streams.push(event);
                      }
                  });
  
                  return streams;
              }
          };
  
          connection.socketURL = '/'; // generated via config.json
          connection.socketMessageEvent = 'RTCMultiConnection-Message'; // generated via config.json
          connection.socketCustomEvent = 'RTCMultiConnection-Custom-Message'; // generated via config.json
          connection.DetectRTC = DetectRTC;
  
          connection.setCustomSocketEvent = function(customEvent) {
              if (customEvent) {
                  connection.socketCustomEvent = customEvent;
              }
  
              if (!connection.socket) {
                  return;
              }
  
              connection.socket.emit('set-custom-socket-event-listener', connection.socketCustomEvent);
          };
  
          connection.getNumberOfBroadcastViewers = function(broadcastId, callback) {
              if (!connection.socket || !broadcastId || !callback) return;
  
              connection.socket.emit('get-number-of-users-in-specific-broadcast', broadcastId, callback);
          };
  
          connection.onNumberOfBroadcastViewersUpdated = function(event) {
              if (!connection.enableLogs || !connection.isInitiator) return;
              console.info('Number of broadcast (', event.broadcastId, ') viewers', event.numberOfBroadcastViewers);
          };
  
          connection.onUserStatusChanged = function(event, dontWriteLogs) {
              if (!!connection.enableLogs && !dontWriteLogs) {
                  console.info(event.userid, event.status);
              }
          };
  
          connection.getUserMediaHandler = getUserMediaHandler;
          connection.multiPeersHandler = mPeer;
          connection.enableLogs = true;
          connection.setCustomSocketHandler = function(customSocketHandler) {
              if (typeof SocketConnection !== 'undefined') {
                  SocketConnection = customSocketHandler;
              }
          };
  
          // default value should be 15k because [old]Firefox's receiving limit is 16k!
          // however 64k works chrome-to-chrome
          connection.chunkSize = 40 * 1000;
  
          connection.maxParticipantsAllowed = 1000;
  
          // eject or leave single user
          connection.disconnectWith = mPeer.disconnectWith;
  
          // check if room exist on server
          // we will pass roomid to the server and wait for callback (i.e. server's response)
          connection.checkPresence = function(roomid, callback) {
              roomid = roomid || connection.sessionid;
  
              if (SocketConnection.name === 'SSEConnection') {
                  SSEConnection.checkPresence(roomid, function(isRoomExist, _roomid, extra) {
                      if (!connection.socket) {
                          if (!isRoomExist) {
                              connection.userid = _roomid;
                          }
  
                          connection.connectSocket(function() {
                              callback(isRoomExist, _roomid, extra);
                          });
                          return;
                      }
                      callback(isRoomExist, _roomid);
                  });
                  return;
              }
  
              if (!connection.socket) {
                  connection.connectSocket(function() {
                      connection.checkPresence(roomid, callback);
                  });
                  return;
              }
  
              connection.socket.emit('check-presence', roomid + '', function(isRoomExist, _roomid, extra) {
                  if (connection.enableLogs) {
                      console.log('checkPresence.isRoomExist: ', isRoomExist, ' roomid: ', _roomid);
                  }
                  callback(isRoomExist, _roomid, extra);
              });
          };
  
          connection.onReadyForOffer = function(remoteUserId, userPreferences) {
              connection.multiPeersHandler.createNewPeer(remoteUserId, userPreferences);
          };
  
          connection.setUserPreferences = function(userPreferences) {
              if (connection.dontAttachStream) {
                  userPreferences.dontAttachLocalStream = true;
              }
  
              if (connection.dontGetRemoteStream) {
                  userPreferences.dontGetRemoteStream = true;
              }
  
              return userPreferences;
          };
  
          connection.updateExtraData = function() {
              connection.socket.emit('extra-data-updated', connection.extra);
          };
  
          connection.enableScalableBroadcast = false;
          connection.maxRelayLimitPerUser = 3; // each broadcast should serve only 3 users
  
          connection.dontCaptureUserMedia = false;
          connection.dontAttachStream = false;
          connection.dontGetRemoteStream = false;
  
          connection.onReConnecting = function(event) {
              if (connection.enableLogs) {
                  console.info('ReConnecting with', event.userid, '...');
              }
          };
  
          connection.beforeAddingStream = function(stream) {
              return stream;
          };
  
          connection.beforeRemovingStream = function(stream) {
              return stream;
          };
  
          if (typeof isChromeExtensionAvailable !== 'undefined') {
              connection.checkIfChromeExtensionAvailable = isChromeExtensionAvailable;
          }
  
          if (typeof isFirefoxExtensionAvailable !== 'undefined') {
              connection.checkIfChromeExtensionAvailable = isFirefoxExtensionAvailable;
          }
  
          if (typeof getChromeExtensionStatus !== 'undefined') {
              connection.getChromeExtensionStatus = getChromeExtensionStatus;
          }
  
          connection.modifyScreenConstraints = function(screen_constraints) {
              return screen_constraints;
          };
  
          connection.onPeerStateChanged = function(state) {
              if (connection.enableLogs) {
                  if (state.iceConnectionState.search(/closed|failed/gi) !== -1) {
                      console.error('Peer connection is closed between you & ', state.userid, state.extra, 'state:', state.iceConnectionState);
                  }
              }
          };
  
          connection.isOnline = true;
  
          listenEventHandler('online', function() {
              connection.isOnline = true;
          });
  
          listenEventHandler('offline', function() {
              connection.isOnline = false;
          });
  
          connection.isLowBandwidth = false;
          if (navigator && navigator.connection && navigator.connection.type) {
              connection.isLowBandwidth = navigator.connection.type.toString().toLowerCase().search(/wifi|cell/g) !== -1;
              if (connection.isLowBandwidth) {
                  connection.bandwidth = {
                      audio: false,
                      video: false,
                      screen: false
                  };
  
                  if (connection.mediaConstraints.audio && connection.mediaConstraints.audio.optional && connection.mediaConstraints.audio.optional.length) {
                      var newArray = [];
                      connection.mediaConstraints.audio.optional.forEach(function(opt) {
                          if (typeof opt.bandwidth === 'undefined') {
                              newArray.push(opt);
                          }
                      });
                      connection.mediaConstraints.audio.optional = newArray;
                  }
  
                  if (connection.mediaConstraints.video && connection.mediaConstraints.video.optional && connection.mediaConstraints.video.optional.length) {
                      var newArray = [];
                      connection.mediaConstraints.video.optional.forEach(function(opt) {
                          if (typeof opt.bandwidth === 'undefined') {
                              newArray.push(opt);
                          }
                      });
                      connection.mediaConstraints.video.optional = newArray;
                  }
              }
          }
  
          connection.getExtraData = function(remoteUserId, callback) {
              if (!remoteUserId) throw 'remoteUserId is required.';
  
              if (typeof callback === 'function') {
                  connection.socket.emit('get-remote-user-extra-data', remoteUserId, function(extra, remoteUserId, error) {
                      callback(extra, remoteUserId, error);
                  });
                  return;
              }
  
              if (!connection.peers[remoteUserId]) {
                  if (connection.peersBackup[remoteUserId]) {
                      return connection.peersBackup[remoteUserId].extra;
                  }
                  return {};
              }
  
              return connection.peers[remoteUserId].extra;
          };
  
          if (!!forceOptions.autoOpenOrJoin) {
              connection.openOrJoin(connection.sessionid);
          }
  
          connection.onUserIdAlreadyTaken = function(useridAlreadyTaken, yourNewUserId) {
              // via #683
              connection.close();
              connection.closeSocket();
  
              connection.isInitiator = false;
              connection.userid = connection.token();
  
              connection.join(connection.sessionid);
  
              if (connection.enableLogs) {
                  console.warn('Userid already taken.', useridAlreadyTaken, 'Your new userid:', connection.userid);
              }
          };
  
          connection.trickleIce = true;
          connection.version = '3.7.0';
  
          connection.onSettingLocalDescription = function(event) {
              if (connection.enableLogs) {
                  console.info('Set local description for remote user', event.userid);
              }
          };
  
          connection.resetScreen = function() {
              sourceId = null;
              if (DetectRTC && DetectRTC.screen) {
                  delete DetectRTC.screen.sourceId;
              }
  
              currentUserMediaRequest = {
                  streams: [],
                  mutex: false,
                  queueRequests: []
              };
          };
  
          // if disabled, "event.mediaElement" for "onstream" will be NULL
          connection.autoCreateMediaElement = true;
  
          // set password
          connection.password = null;
  
          // set password
          connection.setPassword = function(password, callback) {
              callback = callback || function() {};
              if (connection.socket) {
                  connection.socket.emit('set-password', password, callback);
              } else {
                  connection.password = password;
                  callback(true, connection.sessionid, null);
              }
          };
  
          connection.onSocketDisconnect = function(event) {
              if (connection.enableLogs) {
                  console.warn('socket.io connection is closed');
              }
          };
  
          connection.onSocketError = function(event) {
              if (connection.enableLogs) {
                  console.warn('socket.io connection is failed');
              }
          };
  
          // error messages
          connection.errors = {
              ROOM_NOT_AVAILABLE: 'Room not available',
              INVALID_PASSWORD: 'Invalid password',
              USERID_NOT_AVAILABLE: 'User ID does not exist',
              ROOM_PERMISSION_DENIED: 'Room permission denied',
              ROOM_FULL: 'Room full',
              DID_NOT_JOIN_ANY_ROOM: 'Did not join any room yet',
              INVALID_SOCKET: 'Invalid socket',
              PUBLIC_IDENTIFIER_MISSING: 'publicRoomIdentifier is required',
              INVALID_ADMIN_CREDENTIAL: 'Invalid username or password attempted'
          };
      })(this);
  
  };
  
  if (typeof module !== 'undefined' /* && !!module.exports*/ ) {
      module.exports = exports = RTCMultiConnection;
  }
  
  if (typeof define === 'function' && define.amd) {
      define('RTCMultiConnection', [], function() {
          return RTCMultiConnection;
      });
  }
  