'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) { protoSend.apply(this, [encode(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; dc.send(message); 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 () { dc.send({ 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;