Skip to content
Snippets Groups Projects
peer.js 14.1 KiB
Newer Older
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _rtcpeerconnection = require('rtcpeerconnection');

var _rtcpeerconnection2 = _interopRequireDefault(_rtcpeerconnection);

var _wildemitter = require('wildemitter');

var _wildemitter2 = _interopRequireDefault(_wildemitter);

var _filetransfer = require('filetransfer');

var _filetransfer2 = _interopRequireDefault(_filetransfer);

var _webrtcsupport = require('./webrtcsupport');

var _webrtcsupport2 = _interopRequireDefault(_webrtcsupport);

var _whatThePack = require('what-the-pack');

var _whatThePack2 = _interopRequireDefault(_whatThePack);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var _MessagePack$initiali = _whatThePack2.default.initialize(Math.pow(2, 22)),
    encode = _MessagePack$initiali.encode,
    decode = _MessagePack$initiali.decode;

function isAllTracksEnded(stream) {
  var isAllTracksEnded = true;
  stream.getTracks().forEach(function (t) {
    isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded;
  });
  return isAllTracksEnded;
}

var protoSend = RTCDataChannel.prototype.send;
RTCDataChannel.prototype.send = function (data) {
var Peer = function (_WildEmitter) {
  _inherits(Peer, _WildEmitter);

  function Peer(options) {
    _classCallCheck(this, Peer);

    var _this = _possibleConstructorReturn(this, (Peer.__proto__ || Object.getPrototypeOf(Peer)).call(this));

    var self = _this;
    _this.id = options.id;
    _this.parent = options.parent;
    _this.type = options.type || 'video';
    _this.oneway = options.oneway || false;
    _this.sharemyscreen = options.sharemyscreen || false;
    _this.browserPrefix = options.prefix;
    _this.stream = options.stream;
    _this.enableDataChannels = options.enableDataChannels === undefined ? _this.parent.config.enableDataChannels : options.enableDataChannels;
    _this.receiveMedia = options.receiveMedia || _this.parent.config.receiveMedia;
    _this.channels = {};
    _this.sid = options.sid || Date.now().toString();
    // Create an RTCPeerConnection via the polyfill
    _this.pc = new _rtcpeerconnection2.default(Object.assign({}, _this.parent.config.peerConnectionConfig, { iceServers: options.iceServers || _this.parent.config.peerConnectionConfig.iceServers }), _this.parent.config.peerConnectionConstraints);
    _this.pc.on('ice', _this.onIceCandidate.bind(_this));
    _this.pc.on('endOfCandidates', function (event) {
      self.send('endOfCandidates', event);
    });
    _this.pc.on('offer', function (offer) {
      if (self.parent.config.nick) offer.nick = self.parent.config.nick;
      self.send('offer', offer);
    });
    _this.pc.on('answer', function (answer) {
      if (self.parent.config.nick) answer.nick = self.parent.config.nick;
      self.send('answer', answer);
    });
    _this.pc.on('addStream', _this.handleRemoteStreamAdded.bind(_this));
    _this.pc.on('addChannel', _this.handleDataChannelAdded.bind(_this));
    _this.pc.on('removeStream', _this.handleStreamRemoved.bind(_this));
    // Just fire negotiation needed events for now
    // When browser re-negotiation handling seems to work
    // we can use this as the trigger for starting the offer/answer process
    // automatically. We'll just leave it be for now while this stabalizes.
    _this.pc.on('negotiationNeeded', _this.emit.bind(_this, 'negotiationNeeded'));
    _this.pc.on('iceConnectionStateChange', _this.emit.bind(_this, 'iceConnectionStateChange'));
    _this.pc.on('iceConnectionStateChange', function () {
      switch (self.pc.iceConnectionState) {
        case 'failed':
          // currently, in chrome only the initiator goes to failed
          // so we need to signal this to the peer
          if (self.pc.pc.localDescription.type === 'offer') {
            self.parent.emit('iceFailed', self);
            self.send('connectivityError');
          }
          break;
        case 'closed':
          _this.handleStreamRemoved(false);
          break;
        default:
          break;
      }
    });
    _this.pc.on('signalingStateChange', _this.emit.bind(_this, 'signalingStateChange'));
    _this.logger = _this.parent.logger;

    _this.parent.localStreams.forEach(function (stream) {
      self.pc.addStream(stream);
    });

    _this.on('channelOpen', function (channel) {});

    // proxy events to parent
    _this.on('*', function () {
      var _self$parent;

      (_self$parent = self.parent).emit.apply(_self$parent, arguments);
    });
    return _this;
  }

  _createClass(Peer, [{
    key: 'handleMessage',
    value: function handleMessage(message) {
      var self = this;
      this.logger.log('getting', message.type, message);
      if (message.prefix) this.browserPrefix = message.prefix;

      if (message.type === 'offer') {
        if (!this.nick) {
          var n = message.payload.nick;
          this.nick = n;
        }
        // delete message.payload.nick;
        this.pc.handleOffer(message.payload, function (err) {
          if (err) {
            return;
          }
          // auto-accept
          self.pc.answer(function (err, sessionDescription) {
            // self.send('answer', sessionDescription);
            // console.log('answering', sessionDescription);
          });
        });
      } else if (message.type === 'answer') {
        if (!this.nick) this.nick = message.payload.nick;
        delete message.payload.nick;
        this.pc.handleAnswer(message.payload);
      } else if (message.type === 'candidate') {
        this.pc.processIce(message.payload);
      } else if (message.type === 'connectivityError') {
        this.parent.emit('connectivityError', self);
      } else if (message.type === 'mute') {
        this.parent.emit('mute', { id: message.from, name: message.payload.name });
      } else if (message.type === 'unmute') {
        this.parent.emit('unmute', { id: message.from, name: message.payload.name });
      } else if (message.type === 'endOfCandidates') {
        // Edge requires an end-of-candidates. Since only Edge will have mLines or tracks on the
        // shim this will only be called in Edge.
        var mLines = this.pc.pc.transceivers || [];
        mLines.forEach(function (mLine) {
          if (mLine.iceTransport) {
            mLine.iceTransport.addRemoteCandidate({});
          }
        });
      } else if (message.type === 'signalData') {
        this.parent.emit('receivedSignalData', message.payload.type, message.payload.payload, self);
      }
    }

    // send via signaling channel

  }, {
    key: 'send',
    value: function send(messageType, payload) {
      var message = {
        to: this.id,
        sid: this.sid,
        broadcaster: this.broadcaster,
        roomType: this.type,
        type: messageType,
        payload: payload,
        prefix: _webrtcsupport2.default.prefix,
        timestamp: Date.now()
      };
      this.logger.log('sending', messageType, message);
      this.parent.emit('message', message);
    }

    // send via data channel
    // returns true when message was sent and false if channel is not open

  }, {
    key: 'sendDirectly',
    value: function sendDirectly(messageType, payload) {
      var channel = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'liowebrtc';
      var shout = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
      var messageId = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : Date.now() + '_' + Math.random() * 1000000;

      var message = {
        type: messageType,
        payload: payload,
        _id: messageId,
        shout: shout
      };
      this.logger.log('sending via datachannel', channel, messageType, message);
      var dc = this.getDataChannel(channel);
      if (dc.readyState !== 'open') return false;
      return true;
    }

    // Internal method registering handlers for a data channel and emitting events on the peer

  }, {
    key: '_observeDataChannel',
    value: function _observeDataChannel(channel, peer) {
      var self = this;
      channel.binaryType = 'arraybuffer';
      channel.onclose = this.emit.bind(this, 'channelClose', channel, peer);
      channel.onerror = this.emit.bind(this, 'channelError', channel, peer);
      channel.onmessage = function (event) {
        self.emit('channelMessage', self, channel.label, decode(_whatThePack2.default.Buffer.from(event.data)), channel, event);
      };
      channel.onopen = this.emit.bind(this, 'channelOpen', channel, peer);
    }

    // Fetch or create a data channel by the given name

  }, {
    key: 'getDataChannel',
    value: function getDataChannel() {
      var name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'liowebrtc';
      var opts = arguments[1];

      var channel = this.channels[name];
      opts || (opts = {});
      if (channel) return channel;
      // if we don't have one by this label, create it
      channel = this.channels[name] = this.pc.createDataChannel(name, opts);
      this._observeDataChannel(channel, this);
      return channel;
    }
  }, {
    key: 'onIceCandidate',
    value: function onIceCandidate(candidate) {
      if (this.closed) return;
      if (candidate) {
        var pcConfig = this.parent.config.peerConnectionConfig;
        if (_webrtcsupport2.default.prefix === 'moz' && pcConfig && pcConfig.iceTransports && candidate.candidate && candidate.candidate.candidate && !candidate.candidate.candidate.includes(pcConfig.iceTransports)) {
          this.logger.log('Ignoring ice candidate not matching pcConfig iceTransports type: ', pcConfig.iceTransports);
        } else {
          this.send('candidate', candidate);
        }
      } else {
        this.logger.log('End of candidates.');
      }
    }
  }, {
    key: 'start',
    value: function start() {
      var self = this;

      // well, the webrtc api requires that we either
      // a) create a datachannel a priori
      // b) do a renegotiation later to add the SCTP m-line
      // Let's do (a) first...
      if (this.enableDataChannels) {
        this.getDataChannel('liowebrtc');
      }

      this.pc.offer(this.receiveMedia, function (err, sessionDescription) {
        // self.send('offer', sessionDescription);
      });
    }
  }, {
    key: 'icerestart',
    value: function icerestart() {
      var constraints = this.receiveMedia;
      constraints.mandatory.IceRestart = true;
      this.pc.offer(constraints, function (err, success) {});
    }
  }, {
    key: 'end',
    value: function end() {
      var emitRemoval = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;

      if (this.closed) return;
      if (emitRemoval) {
        this.parent.emit('removedPeer', this);
      }
      this.pc.close();
      this.handleStreamRemoved(emitRemoval);
    }
  }, {
    key: 'handleRemoteStreamAdded',
    value: function handleRemoteStreamAdded(event) {
      var self = this;
      if (this.stream) {
        this.logger.warn('Already have a remote stream');
      } else {
        this.stream = event.stream;

        this.stream.getTracks().forEach(function (track) {
          track.addEventListener('ended', function () {
            if (isAllTracksEnded(self.stream)) {
              self.end();
            }
          });
        });

        this.parent.emit('peerStreamAdded', this.stream, this);
      }
    }
  }, {
    key: 'handleStreamRemoved',
    value: function handleStreamRemoved() {
      var emitRemoval = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;

      var peerIndex = this.parent.peers.indexOf(this);
      if (peerIndex > -1) {
        this.parent.peers.splice(peerIndex, 1);
        this.closed = true;
        if (emitRemoval) this.parent.emit('peerStreamRemoved', this);
      }
    }
  }, {
    key: 'handleDataChannelAdded',
    value: function handleDataChannelAdded(channel) {
      this.channels[channel.label] = channel;
      //this._observeDataChannel(channel, this);
    }
  }, {
    key: 'sendFile',
    value: function sendFile(file) {
      var sender = new _filetransfer2.default.Sender();
      var dc = this.getDataChannel('filetransfer' + new Date().getTime(), {
        protocol: INBAND_FILETRANSFER_V1
      });
      // override onopen
      dc.onopen = function () {
          size: file.size,
          name: file.name
        sender.send(file, dc);
      };
      // override onclose
      dc.onclose = function () {
        // ('sender received transfer');
        sender.emit('complete');
      };
      return sender;
    }
  }, {
    key: 'getStats',
    value: function getStats(selector) {
      // TODO: Use adapter.js to patch this across browsers
      return this.pc.pc.getStats(selector);
    }
  }]);

  return Peer;
}(_wildemitter2.default);

exports.default = Peer;