From 14adace866d4c71c5a9e5e39129f770670d78427 Mon Sep 17 00:00:00 2001 From: Moritz Langenstein <ml5717@ic.ac.uk> Date: Fri, 11 Oct 2019 18:03:50 +0100 Subject: [PATCH] (ml5717) Pre-built liowebrtc to dist/ --- .gitignore | 1 - dist/Edge.js | 56 ++++ dist/PeerGraph.js | 130 ++++++++ dist/PeerNode.js | 103 ++++++ dist/PeerOptimizer.js | 129 ++++++++ dist/constants.js | 42 +++ dist/liowebrtc.js | 656 +++++++++++++++++++++++++++++++++++++ dist/localmedia.js | 324 ++++++++++++++++++ dist/peer.js | 356 ++++++++++++++++++++ dist/socketioconnection.js | 51 +++ dist/webrtc.js | 236 +++++++++++++ dist/webrtcsupport.js | 45 +++ 12 files changed, 2128 insertions(+), 1 deletion(-) create mode 100644 dist/Edge.js create mode 100644 dist/PeerGraph.js create mode 100644 dist/PeerNode.js create mode 100644 dist/PeerOptimizer.js create mode 100644 dist/constants.js create mode 100644 dist/liowebrtc.js create mode 100644 dist/localmedia.js create mode 100644 dist/peer.js create mode 100644 dist/socketioconnection.js create mode 100644 dist/webrtc.js create mode 100644 dist/webrtcsupport.js diff --git a/.gitignore b/.gitignore index 74c2dde..d5ba7b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules .DS_Store -dist yarn-error.log .eslintcache diff --git a/dist/Edge.js b/dist/Edge.js new file mode 100644 index 0000000..4f0eec1 --- /dev/null +++ b/dist/Edge.js @@ -0,0 +1,56 @@ +"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; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Edge = function () { + function Edge(startNode, endNode) { + var weight = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + + _classCallCheck(this, Edge); + + this.node1 = startNode; + this.node2 = endNode; + this.weight = weight; + } + + _createClass(Edge, [{ + key: "getId", + value: function getId() { + var startNodeId = this.node1.getId(); + var endNodeId = this.node2.getId(); + return startNodeId + "_" + endNodeId; + } + }, { + key: "getWeight", + value: function getWeight() { + return this.weight; + } + }, { + key: "setWeight", + value: function setWeight(weight) { + this.weight = weight; + } + }, { + key: "reverse", + value: function reverse() { + var tmp = this.node1; + this.node1 = this.node2; + this.node2 = tmp; + } + }, { + key: "toString", + value: function toString() { + return this.getId(); + } + }]); + + return Edge; +}(); + +exports.default = Edge; \ No newline at end of file diff --git a/dist/PeerGraph.js b/dist/PeerGraph.js new file mode 100644 index 0000000..45a57e7 --- /dev/null +++ b/dist/PeerGraph.js @@ -0,0 +1,130 @@ +"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; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var PeerGraph = function () { + function PeerGraph() { + _classCallCheck(this, PeerGraph); + + this.nodes = {}; + this.edges = {}; + this.edgeCount = 0; + } + + _createClass(PeerGraph, [{ + key: "addEdge", + value: function addEdge(edge) { + var node1 = this.getNodeById(edge.node1.getId()); + var node2 = this.getNodeById(edge.node2.getId()); + if (!node1) { + this.addNode(edge.node1); + node1 = this.getNodeById(edge.node1.getId()); + } + if (!node2) { + this.addNode(edge.node2); + node2 = this.getNodeById(edge.node2.getId()); + } + + if (this.edges[edge.getId()]) { + // throw new Error('Edge already exists'); + } else { + this.edges[edge.getId()] = edge; + } + // Add edge to both node instances because it's an undirected graph + node1.addEdge(edge); + node2.addEdge(edge); + return this; + } + }, { + key: "addNode", + value: function addNode(newNode) { + this.nodes[newNode.getId()] = newNode; + return this; + } + }, { + key: "getNodeById", + value: function getNodeById(id) { + return this.nodes[id]; + } + }, { + key: "getNeighbors", + value: function getNeighbors(node) { + return node.getNeighbors(); + } + }, { + key: "getWeight", + value: function getWeight() { + return this.getAllEdges().reduce(function (weight, edge) { + return weight + edge.weight; + }, 0); + } + }, { + key: "getAllNodes", + value: function getAllNodes() { + return Object.values(this.nodes); + } + }, { + key: "getAllEdges", + value: function getAllEdges() { + return Object.values(this.edges); + } + }, { + key: "findNodeById", + value: function findNodeById(nodeId) { + if (this.nodes[nodeId]) { + return this.nodes[nodeId]; + } + return null; + } + }, { + key: "findEdge", + value: function findEdge(node1, node2) { + var node = this.getNodeById(node1.getId()); + if (!node) { + return null; + } + return node.findEdge(node2); + } + }, { + key: "deleteEdge", + value: function deleteEdge(edge) { + if (!edge) { + return; + } + if (this.edges[edge.getId()]) { + delete this.edges[edge.getId()]; + } + var node1 = this.getNodeById(edge.node1.getId()); + var node2 = this.getNodeById(edge.node2.getId()); + node1.deleteEdge(edge); + node2.deleteEdge(edge); + } + }, { + key: "getNodeIndices", + value: function getNodeIndices() { + var nodeIndices = {}; + this.getAllNodes().forEach(function (node, index) { + nodeIndices[node.getId()] = index; + }); + return nodeIndices; + } + }, { + key: "toString", + value: function toString() { + return Object.keys(this.nodes).toString(); + } + }, { + key: "toJSON", + value: function toJSON() {} + }]); + + return PeerGraph; +}(); + +exports.default = PeerGraph; \ No newline at end of file diff --git a/dist/PeerNode.js b/dist/PeerNode.js new file mode 100644 index 0000000..c689119 --- /dev/null +++ b/dist/PeerNode.js @@ -0,0 +1,103 @@ +'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; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Node = function () { + function Node(value) { + _classCallCheck(this, Node); + + if (value === undefined) { + throw new Error('Node must have an ID'); + } + this.value = value; + this.edges = {}; + } + + _createClass(Node, [{ + key: 'addEdge', + value: function addEdge(edge) { + this.edges[edge.getId()] = edge; + return this; + } + }, { + key: 'deleteEdge', + value: function deleteEdge(edge) { + delete this.edges[edge.getId()]; + } + }, { + key: 'getEdges', + value: function getEdges() { + return Object.values(this.edges); + } + }, { + key: 'getDegree', + value: function getDegree() { + return Object.keys(this.edges).length; + } + }, { + key: 'getNeighbors', + value: function getNeighbors() { + var _this = this; + + var edges = Object.values(this.edges); + var nodes = edges.map(function (e) { + return e.node1 === _this ? e.node2 : e.node1; + }); + return nodes; + } + }, { + key: 'hasEdge', + value: function hasEdge(requiredEdge) { + var edgeNode = this.edges.filter(function (edge) { + return edge.getId() === requiredEdge.getId(); + }); + return !!edgeNode.length; + } + }, { + key: 'hasNeighbor', + value: function hasNeighbor(node) { + var nodeWeWant = Object.values(this.edges).filter(function (e) { + return e.node1.getId() === node.getId() || e.node2.getId() === node.getId(); + }); + return !!nodeWeWant.length; + } + }, { + key: 'findEdge', + value: function findEdge(node) { + var result = Object.values(this.edges).filter(function (e) { + return e.node1.getId() === node.getId() || e.node2.getId() === node.getId(); + }); + return result.length ? result[0] : null; + } + }, { + key: 'getId', + value: function getId() { + return this.value; + } + }, { + key: 'deleteAllEdges', + value: function deleteAllEdges() { + var _this2 = this; + + this.getEdges().forEach(function (e) { + return _this2.deleteEdge(e); + }); + return this; + } + }, { + key: 'toString', + value: function toString() { + return '' + this.value; + } + }]); + + return Node; +}(); + +exports.default = Node; \ No newline at end of file diff --git a/dist/PeerOptimizer.js b/dist/PeerOptimizer.js new file mode 100644 index 0000000..2193282 --- /dev/null +++ b/dist/PeerOptimizer.js @@ -0,0 +1,129 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Graph = undefined; +exports.addNode = addNode; +exports.addConnection = addConnection; +exports.removeConnection = removeConnection; +exports.getNeighbors = getNeighbors; +exports.isNeighbor = isNeighbor; +exports.getPeerLatencies = getPeerLatencies; +exports.average = average; +exports.squaredDiffs = squaredDiffs; +exports.stdDeviation = stdDeviation; +exports.getLatencyZScores = getLatencyZScores; +exports.getDroppablePeer = getDroppablePeer; + +var _PeerGraph = require('./PeerGraph'); + +var _PeerGraph2 = _interopRequireDefault(_PeerGraph); + +var _Edge = require('./Edge'); + +var _Edge2 = _interopRequireDefault(_Edge); + +var _PeerNode = require('./PeerNode'); + +var _PeerNode2 = _interopRequireDefault(_PeerNode); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var Graph = exports.Graph = new _PeerGraph2.default(); + +function addNode(nodeId) { + var node = new _PeerNode2.default(nodeId); + Graph.addNode(node); +} + +function addConnection(node1Id, node2Id) { + var latency = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + + var nodeA = Graph.getNodeById(node1Id) || new _PeerNode2.default(node1Id); + var nodeB = Graph.getNodeById(node2Id) || new _PeerNode2.default(node2Id); + var edgeAB = new _Edge2.default(nodeA, nodeB, latency); + return Graph.addEdge(edgeAB); +} + +function removeConnection(node1Id, node2Id) { + var nodeA = Graph.getNodeById(node1Id); + var nodeB = Graph.getNodeById(node2Id); + if (nodeA && nodeB) Graph.deleteEdge(Graph.findEdge(nodeA, nodeB)); +} + +function getNeighbors(nodeId) { + var node = Graph.getNodeById(nodeId); + var neighbors = node.getNeighbors(); + return neighbors.map(function (n) { + return n.getId(); + }); +} + +function isNeighbor(node1Id, node2Id) { + var nodeA = Graph.getNodeById(node1Id) || new _PeerNode2.default(node1Id); + var nodeB = Graph.getNodeById(node2Id) || new _PeerNode2.default(node2Id); + if (nodeA.hasNeighbor(nodeB)) { + return true; + } + return false; +} + +function getPeerLatencies(nodeId) { + var node = Graph.findNodeById(nodeId); + if (node) { + var result = {}; + var edges = node.getEdges(); + edges.forEach(function (e) { + var id = e.node1.getId() === nodeId ? e.node2.getId() : e.node1.getId(); + var latency = e.getWeight(); + result[id] = latency; + }); + return result; + } +} + +function average(vals) { + var total = vals.reduce(function (sum, val) { + return val + sum; + }); + return total / vals.length; +} + +function squaredDiffs(vals, avg) { + var sqd = vals.map(function (val) { + return Math.pow(val - avg, 2); + }); + return sqd; +} + +function stdDeviation(sqDiffs) { + var sum = sqDiffs.reduce(function (total, x) { + return total + x; + }); + return Math.sqrt(sum / sqDiffs.length); +} + +function getLatencyZScores(nodeId) { + var peerLatencyCache = getPeerLatencies(nodeId); + var peerIds = Object.keys(peerLatencyCache); + var peerLatencies = Object.values(peerLatencyCache); + var avg = average(peerLatencies); + var standardDeviation = stdDeviation(squaredDiffs(peerLatencies, avg)); + var zScores = {}; + peerIds.forEach(function (val, i) { + zScores[val] = (peerLatencies[i] - avg) / standardDeviation; + }); + return zScores; +} + +function getDroppablePeer(nodeId) { + var zScores = getLatencyZScores(nodeId); + var droppable = zScores.filter(function (s) { + return s <= -1; + }); + var orderedDroppable = droppable.sort(function (a, b) { + return b - a; + }); + return orderedDroppable[0]; +} \ No newline at end of file diff --git a/dist/constants.js b/dist/constants.js new file mode 100644 index 0000000..ce701fe --- /dev/null +++ b/dist/constants.js @@ -0,0 +1,42 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var inheritedMethods = exports.inheritedMethods = ['mute', 'unmute', 'pauseVideo', 'resumeVideo', 'pause', 'resume', 'sendToAll', 'sendDirectlyToAll', 'getPeers', 'getPeerByNick', 'getPeerById', 'shout', 'whisper', 'broadcast', 'transmit']; + +var defaultConfig = exports.defaultConfig = { + url: 'https://sm1.lio.app:443/', + socketio: { forceNew: true }, + connection: null, + debug: false, + localVideoEl: '', + remoteVideosEl: '', + enableDataChannels: true, + autoRequestMedia: false, + dataOnly: false, + autoRemoveVideos: true, + adjustPeerVolume: true, + peerVolumeWhenSpeaking: 0.25, + media: { + video: true, + audio: true + }, + receiveMedia: { + offerToReceiveAudio: 1, + offerToReceiveVideo: 1 + }, + localVideo: { + autoplay: true, + mirror: true, + muted: true, + audio: false + }, + constraints: { + maxPeers: 0, + minPeers: 2 + }, + selfOptimize: true +}; + +var defaultChannel = exports.defaultChannel = 'liowebrtc'; \ No newline at end of file diff --git a/dist/liowebrtc.js b/dist/liowebrtc.js new file mode 100644 index 0000000..a07362a --- /dev/null +++ b/dist/liowebrtc.js @@ -0,0 +1,656 @@ +'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 _wildemitter = require('wildemitter'); + +var _wildemitter2 = _interopRequireDefault(_wildemitter); + +var _attachmediastream = require('attachmediastream'); + +var _attachmediastream2 = _interopRequireDefault(_attachmediastream); + +var _mockconsole = require('mockconsole'); + +var _mockconsole2 = _interopRequireDefault(_mockconsole); + +var _webrtc = require('./webrtc'); + +var _webrtc2 = _interopRequireDefault(_webrtc); + +var _webrtcsupport = require('./webrtcsupport'); + +var _webrtcsupport2 = _interopRequireDefault(_webrtcsupport); + +var _socketioconnection = require('./socketioconnection'); + +var _socketioconnection2 = _interopRequireDefault(_socketioconnection); + +var _PeerOptimizer = require('./PeerOptimizer'); + +var _constants = require('./constants'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return 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 LioWebRTC = function (_WildEmitter) { + _inherits(LioWebRTC, _WildEmitter); + + function LioWebRTC(opts) { + _classCallCheck(this, LioWebRTC); + + var _this = _possibleConstructorReturn(this, (LioWebRTC.__proto__ || Object.getPrototypeOf(LioWebRTC)).call(this)); + + var self = _this; + var options = opts || {}; + _this.config = _constants.defaultConfig; + var config = _this.config; + + _this.peerDataCache = {}; + _this.unconnectivePeers = {}; + _this.id = ''; + _this.roomCount = 0; + _this.roomName = ''; + + var connection = void 0; + // Set up logger + _this.logger = function () { + if (opts.debug) { + return opts.logger || console; + } + return opts.logger || _mockconsole2.default; + }(); + + // Set our config from options + Object.keys(options).forEach(function (o) { + _this.config[o] = options[o]; + }); + + if (options.dataOnly) { + _this.config.media.video = false; + _this.config.media.audio = false; + _this.config.receiveMedia.offerToReceiveAudio = false; + _this.config.receiveMedia.offerToReceiveVideo = false; + } + + if (!_this.config.media.video && _this.config.media.audio) { + _this.config.localVideo.audio = true; + } + _this.capabilities = _webrtcsupport2.default; + if (_this.config.connection === null) { + connection = _this.connection = new _socketioconnection2.default(_this.config); + } else { + connection = _this.connection = _this.config.connection; + } + + connection.on('connect', function () { + self.emit('connectionReady', connection.getSessionid()); + self.sessionReady = true; + self.testReadiness(); + }); + + connection.on('message', function (message) { + var peers = self.webrtc.getPeers(message.from, message.roomType); + var totalPeers = self.webrtc.getPeers().length; + var peer = void 0; + + if (message.type === 'offer') { + if (peers.length) { + peers.forEach(function (p) { + if (p.sid === message.sid) peer = p; + }); + // if (!peer) peer = peers[0]; // fallback for old protocol versions + } + if (_this.config.dataOnly && _this.config.constraints.maxPeers > 0 && totalPeers >= _this.config.constraints.maxPeers) { + return; + } + if (!peer) { + peer = self.webrtc.createPeer({ + id: message.from, + sid: message.sid, + type: message.roomType, + enableDataChannels: self.config.enableDataChannels, + sharemyscreen: message.roomType === 'screen' && !message.broadcaster, + broadcaster: message.roomType === 'screen' && !message.broadcaster ? self.connection.getSessionid() : null + }); + if (_this.config.dataOnly && _this.config.constraints.maxPeers > 0) { + _this.sendPing(peer, peer.id, true); + } else { + peer.start(); + _this.emit('createdPeer', peer); + } + } else { + return; + } + peer.handleMessage(message); + } else if (peers.length) { + peers.forEach(function (p) { + p.handleMessage(message); + }); + } + }); + + connection.on('remove', function (room) { + if (room.id !== self.connection.getSessionid()) { + self.webrtc.removePeers(room.id, room.type); + } + }); + + opts.logger = _this.logger; + opts.debug = false; + _this.webrtc = new _webrtc2.default(opts); + _constants.inheritedMethods.forEach(function (method) { + self[method] = self.webrtc[method].bind(self.webrtc); + }); + + // proxy events from WebRTC + _this.webrtc.on('*', function () { + // eslint-disable-line + self.emit.apply(self, arguments); // eslint-disable-line + }); + + // log all events in debug mode + if (config.debug) { + _this.on('*', _this.logger.log.bind(_this.logger, 'LioWebRTC event:')); + } + + // check for readiness + _this.webrtc.on('localStream', function () { + self.testReadiness(); + }); + + _this.webrtc.on('message', function (payload) { + self.connection.emit('message', payload); + }); + + _this.webrtc.on('peerStreamAdded', _this.handlePeerStreamAdded.bind(_this)); + _this.webrtc.on('peerStreamRemoved', _this.handlePeerStreamRemoved.bind(_this)); + + // echo cancellation attempts + if (_this.config.adjustPeerVolume) { + _this.webrtc.on('speaking', _this.setVolumeForAll.bind(_this, _this.config.peerVolumeWhenSpeaking)); + _this.webrtc.on('stoppedSpeaking', _this.setVolumeForAll.bind(_this, 1)); + } + + connection.on('stunservers', function (args) { + // resets/overrides the config + self.webrtc.config.peerConnectionConfig.iceServers = args; + self.emit('stunservers', args); + }); + connection.on('turnservers', function (args) { + // appends to the config + self.webrtc.config.peerConnectionConfig.iceServers = self.webrtc.config.peerConnectionConfig.iceServers.concat(args); + self.emit('turnservers', args); + }); + /* + this.webrtc.on('iceFailed', (peer) => { + // local ice failure + }); + this.webrtc.on('connectivityError', (peer) => { + // remote ice failure + }); + */ + + // sending mute/unmute to all peers + _this.webrtc.on('audioOn', function () { + self.webrtc.sendToAll('unmute', { name: 'audio' }); + }); + _this.webrtc.on('audioOff', function () { + self.webrtc.sendToAll('mute', { name: 'audio' }); + }); + _this.webrtc.on('videoOn', function () { + self.webrtc.sendToAll('unmute', { name: 'video' }); + }); + _this.webrtc.on('videoOff', function () { + self.webrtc.sendToAll('mute', { name: 'video' }); + }); + + self.on('removedPeer', function (peer) { + if (peer.id) { + (0, _PeerOptimizer.removeConnection)(_this.id, peer.id); + } + }); + + self.on('channelClose', function (channel) { + if (channel.label === 'liowebrtc' && _this.config.dataOnly && _this.config.constraints.maxPeers > 0 && (0, _PeerOptimizer.getNeighbors)(_this.id).length < _this.config.constraints.minPeers) { + _this.connectToRandomPeer(); + } + }); + + _this.webrtc.on('channelMessage', function (peer, label, data) { + if (data.payload._id && _this.peerDataCache[data.payload._id]) { + return; + } + switch (data.type) { + case '_volume': + self.emit('remoteVolumeChange', data.payload, peer); + break; + case '_propagate': + if (_this.seenPeerEvent(data.payload._id)) { + return; + } + // Re-propagate message + _this.propagateMessage(data.payload); + _this.cachePeerEvent(data.payload._id, data.payload.senderId); + // Emit the propagated data as if it were received directly + self.emit('receivedPeerData', data.payload.type, data.payload.payload, { + id: data.payload.senderId, + nick: data.payload.senderNick, + isForwarded: true + }); + break; + case '_ping': + _this.sendPong(peer, data.payload); + break; + case '_pong': + (0, _PeerOptimizer.addConnection)(_this.id, peer.id, Date.now() - data.payload[0] + data.payload[1]); + break; + case '_connections': + data.payload.forEach(function (connection) { + return (0, _PeerOptimizer.addConnection)(peer.id, connection.id, connection.weight); + }); + break; + default: + if (_this.seenPeerEvent(data._id)) { + return; + } + _this.cachePeerEvent(data._id, peer.id); + self.emit('receivedPeerData', data.type, data.payload, peer); + if (_this.config.constraints.maxPeers > 0 && data.shout) { + data.senderId = peer.id; + var fwdData = Object.assign({}, { senderId: peer.id, senderNick: peer.nick }, data); + _this.propagateMessage(fwdData); + } + break; + } + }); + + if (_this.config.autoRequestMedia) _this.startLocalVideo(); + return _this; + } + + _createClass(LioWebRTC, [{ + key: 'cachePeerEvent', + value: function cachePeerEvent(eventId, peerId) { + if (!this.peerDataCache[eventId]) { + this.peerDataCache[eventId] = { + recipients: _defineProperty({}, peerId, true), + timestamp: Date.now() + }; + return; + } + if (!this.peerDataCache[eventId].recipients[peerId]) { + this.peerDataCache[eventId].recipients[peerId] = true; + } + if (Object.keys(this.peerDataCache).length > 1024) { + // Sort by timestamp + var sortedCache = Object.entries(this.peerDataCache).sort(function (a, b) { + return a[1].timestamp - b[1].timestamp; + }); + // Delete oldest item + delete this.peerDataCache[sortedCache[0][0]]; + } + } + }, { + key: 'seenPeerEvent', + value: function seenPeerEvent(eventId) { + if (this.peerDataCache[eventId]) { + return true; + } + return false; + } + }, { + key: 'sendPong', + value: function sendPong(peer, start) { + var channel = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _constants.defaultChannel; + + var now = Date.now(); + peer.sendDirectly('_pong', [now, now - start], channel); + } + }, { + key: 'sendPing', + value: function sendPing(peer, peerId) { + var firstPing = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + var channel = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : _constants.defaultChannel; + + if (firstPing) peer.start(); + setTimeout(this.ping.bind(this, peer, peerId, firstPing, channel), 1000); + } + }, { + key: 'ping', + value: function ping(peer, peerId, firstPing, channel) { + var tries = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0; + + if (peer.sendDirectly('_ping', Date.now(), channel)) { + // this.logger.log('sent ping to', peer.id); + if (firstPing) this.emit('createdPeer', peer); + } else { + // The channel is closed + if (tries === 2) { + this.unconnectivePeers[peerId] = true; + peer.end(false); + return; + } + setTimeout(this.ping.bind(this, peer, peerId, firstPing, channel, tries + 1), 1000); + } + } + }, { + key: 'connectToRandomPeer', + value: function connectToRandomPeer() { + var _this2 = this; + + this.getClients(function (err, clients) { + var ids = Object.keys(clients).filter(function (c) { + return !(_this2.unconnectivePeers[c] === true || c === _this2.id || (0, _PeerOptimizer.isNeighbor)(_this2.id, c)); + }); + if (ids.length) { + var randId = ids[Math.floor(Math.random() * ids.length)]; + _this2.connectToPeer(randId, clients[randId]); + } + }); + } + }, { + key: 'sendConnections', + value: function sendConnections(peer) { + var _this3 = this; + + var channel = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _constants.defaultChannel; + + if (peer.sendDirectly('_connections', this.getPeers().map(function (p) { + var edge = _PeerOptimizer.Graph.findEdge(_this3.id, p.id); + return { id: p.id, weight: edge.getWeight() }; + }), channel)) { + // connections sent + } else { + peer.end(); + } + } + }, { + key: 'propagateMessage', + value: function propagateMessage(data) { + var _this4 = this; + + var channel = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _constants.defaultChannel; + + this.getPeers().forEach(function (peer) { + if (!_this4.peerDataCache[data._id]) { + _this4.cachePeerEvent(data._id, data.senderId); + } + if (!_this4.peerDataCache[data._id].recipients[peer.id]) { + peer.sendDirectly('_propagate', data, channel, true); + } + }); + } + }, { + key: 'trimPeers', + value: function trimPeers() { + var pid = (0, _PeerOptimizer.getDroppablePeer)(); + var peer = this.webrtc.getPeerById(pid); + } + }, { + key: 'leaveRoom', + value: function leaveRoom() { + if (this.roomName) { + this.connection.emit('leave'); + while (this.webrtc.peers.length) { + this.webrtc.peers[0].end(); + } + this.emit('leftRoom', this.roomName); + this.roomName = undefined; + } + } + }, { + key: 'disconnect', + value: function disconnect() { + this.connection.disconnect(); + delete this.connection; + } + }, { + key: 'handlePeerStreamAdded', + value: function handlePeerStreamAdded(stream, peer) { + var self = this; + //this.emit('peerStreamAdded', stream, peer); + + // send our mute status to new peer if we're muted + // currently called with a small delay because it arrives before + // the video element is created otherwise (which happens after + // the async setRemoteDescription-createAnswer) + setTimeout(function () { + if (!self.webrtc.isAudioEnabled()) { + peer.send('mute', { name: 'audio' }); + } + if (!self.webrtc.isVideoEnabled()) { + peer.send('mute', { name: 'video' }); + } + }, 250); + } + }, { + key: 'handlePeerStreamRemoved', + value: function handlePeerStreamRemoved(peer) { + // (this.config.media.video) this.emit('peerStreamRemoved', peer); + } + }, { + key: 'getDomId', + value: function getDomId(peer) { + // eslint-disable-line + return [peer.id, peer.type, peer.broadcaster ? 'broadcasting' : 'incoming'].join('_'); + } + }, { + key: 'getMyId', + value: function getMyId() { + return this.id; + } + }, { + key: 'getContainerId', + value: function getContainerId(peer) { + return 'container_' + this.getDomId(peer); + } + + // set volume on video tag for all peers takse a value between 0 and 1 + + }, { + key: 'setVolumeForAll', + value: function setVolumeForAll(volume) { + this.webrtc.peers.forEach(function (peer) { + if (peer.videoEl) peer.videoEl.volume = volume; + }); + } + }, { + key: 'getClients', + value: function getClients(callback) { + this.connection.emit('getClients', this.roomName, function (err, clients) { + if (callback) callback(err, clients.clients); + }); + } + }, { + key: 'joinRoom', + value: function joinRoom(name, cb) { + var _this5 = this; + + var self = this; + this.roomName = name; + this.connection.emit('join', name, function (err, roomDescription) { + if (err) { + self.emit('error', err); + } else { + var id = void 0; + var client = void 0; + var type = void 0; + var peer = void 0; + + _this5.roomCount = Object.keys(roomDescription.clients).length; + // console.log(roomDescription); + _this5.id = roomDescription.you; + (0, _PeerOptimizer.addNode)(_this5.id); + _this5.unconnectivePeers[_this5.id] = true; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = Object.keys(roomDescription.clients).reverse().filter(function (item) { + return item !== _this5.id; + })[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + id = _step.value; + + client = roomDescription.clients[id]; + for (type in client) { + if (client[type]) { + var peerCount = _this5.webrtc.getPeers().length; + if (_this5.config.dataOnly && _this5.config.constraints.maxPeers > 0 && (peerCount >= _this5.config.constraints.minPeers || peerCount >= _this5.config.constraints.maxPeers)) { + break; + } + peer = self.webrtc.createPeer({ + id: id, + type: type, + enableDataChannels: self.config.enableDataChannels && type !== 'screen', + receiveMedia: { + offerToReceiveAudio: type !== 'screen' && !_this5.config.dataOnly && _this5.config.receiveMedia.offerToReceiveAudio ? 1 : 0, + offerToReceiveVideo: !_this5.config.dataOnly && self.config.receiveMedia.offerToReceiveVideo ? 1 : 0 + } + }); + if (_this5.config.dataOnly && _this5.config.constraints.maxPeers > 0) { + _this5.sendPing(peer, peer.id, true); + } else { + peer.start(); + _this5.emit('createdPeer', peer); + } + } + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + } + + if (cb) cb(err, roomDescription); + self.emit('joinedRoom', name); + }); + } + }, { + key: 'startLocalVideo', + value: function startLocalVideo() { + var _this6 = this; + + var self = this; + this.webrtc.start(this.config.media, function (err, stream) { + if (err) { + self.emit('localMediaError', err); + } else { + (0, _attachmediastream2.default)(stream, _this6.config.localVideoEl, _this6.config.localVideo); + } + }); + } + }, { + key: 'attachStream', + value: function attachStream(stream, el, opts) { + // eslint-disable-line + var options = { + autoplay: true, + muted: false, + mirror: true, + audio: false + }; + (0, _attachmediastream2.default)(stream, el, opts || options); + } + }, { + key: 'setLocalVideo', + value: function setLocalVideo(element) { + this.config.localVideoEl = element; + } + }, { + key: 'stopLocalVideo', + value: function stopLocalVideo() { + this.webrtc.stop(); + } + }, { + key: 'quit', + value: function quit() { + this.stopLocalVideo(); + this.leaveRoom(); + this.disconnect(); + } + }, { + key: 'testReadiness', + value: function testReadiness() { + var self = this; + if (this.sessionReady) { + if (this.config.dataOnly || !this.config.media.video && !this.config.media.audio || this.webrtc.localStreams.length > 0) { + self.emit('ready', self.connection.getSessionid()); + } + } + } + }, { + key: 'connectToPeer', + value: function connectToPeer(peerId, client) { + var type = void 0; + var peer = void 0; + for (type in client) { + if (client[type]) { + var peerCount = this.webrtc.getPeers().length; + if (this.config.constraints.maxPeers > 0 && peerCount >= this.config.constraints.maxPeers) { + break; + } + peer = this.webrtc.createPeer({ + id: peerId, + type: type, + enableDataChannels: this.config.enableDataChannels && type !== 'screen', + receiveMedia: { + offerToReceiveAudio: type !== 'screen' && !this.config.dataOnly && this.config.receiveMedia.offerToReceiveAudio ? 1 : 0, + offerToReceiveVideo: !this.config.dataOnly && this.config.receiveMedia.offerToReceiveVideo ? 1 : 0 + } + }); + if (this.config.dataOnly && this.config.constraints.maxPeers > 0) { + this.sendPing(peer, peerId, true); + } else { + peer.start(); + this.emit('createdPeer', peer); + } + } + } + } + }, { + key: 'createRoom', + value: function createRoom(name, cb) { + this.roomName = name; + if (arguments.length === 2) { + this.connection.emit('create', name, cb); + } else { + this.connection.emit('create', name); + } + } + }, { + key: 'sendFile', + value: function sendFile() { + if (!_webrtcsupport2.default.dataChannel) { + return this.emit('error', new Error('DataChannelNotSupported')); + } + } + }]); + + return LioWebRTC; +}(_wildemitter2.default); + +exports.default = LioWebRTC; \ No newline at end of file diff --git a/dist/localmedia.js b/dist/localmedia.js new file mode 100644 index 0000000..140fc6a --- /dev/null +++ b/dist/localmedia.js @@ -0,0 +1,324 @@ +'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 _hark = require('hark'); + +var _hark2 = _interopRequireDefault(_hark); + +var _wildemitter = require('wildemitter'); + +var _wildemitter2 = _interopRequireDefault(_wildemitter); + +var _mockconsole = require('mockconsole'); + +var _mockconsole2 = _interopRequireDefault(_mockconsole); + +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; } + +function isAllTracksEnded(stream) { + var isAllTracksEnded = true; + stream.getTracks().forEach(function (t) { + isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded; + }); + return isAllTracksEnded; +} + +function shouldWorkAroundFirefoxStopStream() { + if (typeof window === 'undefined') { + return false; + } + if (!window.navigator.mozGetUserMedia) { + return false; + } + var match = window.navigator.userAgent.match(/Firefox\/(\d+)\./); + var version = match && match.length >= 1 && parseInt(match[1], 10); + return version < 50; +} + +var LocalMedia = function (_WildEmitter) { + _inherits(LocalMedia, _WildEmitter); + + function LocalMedia(opts) { + _classCallCheck(this, LocalMedia); + + var _this = _possibleConstructorReturn(this, (LocalMedia.__proto__ || Object.getPrototypeOf(LocalMedia)).call(this)); + + var config = _this.config = { + detectSpeakingEvents: false, + audioFallback: false, + media: { + audio: true, + video: true + }, + harkOptions: null, + logger: _mockconsole2.default + }; + + var item = void 0; + for (item in opts) { + if (opts.hasOwnProperty(item)) { + _this.config[item] = opts[item]; + } + } + + _this.logger = config.logger; + _this._log = _this.logger.log.bind(_this.logger, 'LocalMedia:'); + _this._logerror = _this.logger.error.bind(_this.logger, 'LocalMedia:'); + + _this.localStreams = []; + _this.localScreens = []; + + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + _this._logerror('Your browser does not support local media capture.'); + } + + _this._audioMonitors = []; + _this.on('localStreamStopped', _this._stopAudioMonitor.bind(_this)); + _this.on('localScreenStopped', _this._stopAudioMonitor.bind(_this)); + return _this; + } + + _createClass(LocalMedia, [{ + key: 'start', + value: function start(mediaConstraints, cb) { + var self = this; + var constraints = mediaConstraints || this.config.media; + + this.emit('localStreamRequested', constraints); + + navigator.mediaDevices.getUserMedia(constraints).then(function (stream) { + if (constraints.audio && self.config.detectSpeakingEvents) { + self._setupAudioMonitor(stream, self.config.harkOptions); + } + self.localStreams.push(stream); + + stream.getTracks().forEach(function (track) { + track.addEventListener('ended', function () { + if (isAllTracksEnded(stream)) { + self._removeStream(stream); + } + }); + }); + + self.emit('localStream', stream); + + if (cb) { + return cb(null, stream); + } + }).catch(function (err) { + // Fallback for users without a camera + if (self.config.audioFallback && err.name === 'NotFoundError' && constraints.video !== false) { + constraints.video = false; + self.start(constraints, cb); + return; + } + + self.emit('localStreamRequestFailed', constraints); + + if (cb) { + return cb(err, null); + } + }); + } + }, { + key: 'stop', + value: function stop(stream) { + this.stopStream(stream); + } + }, { + key: 'stopStream', + value: function stopStream(stream) { + var self = this; + + if (stream) { + var idx = this.localStreams.indexOf(stream); + if (idx > -1) { + stream.getTracks().forEach(function (track) { + track.stop(); + }); + + // Half-working fix for Firefox, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1208373 + if (shouldWorkAroundFirefoxStopStream()) { + this._removeStream(stream); + } + } + } else { + this.localStreams.forEach(function (stream) { + stream.getTracks().forEach(function (track) { + track.stop(); + }); + + // Half-working fix for Firefox, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1208373 + if (shouldWorkAroundFirefoxStopStream()) { + self._removeStream(stream); + } + }); + } + } + // Audio controls + + }, { + key: 'mute', + value: function mute() { + this._audioEnabled(false); + this.emit('audioOff'); + } + }, { + key: 'unmute', + value: function unmute() { + this._audioEnabled(true); + this.emit('audioOn'); + } + + // Video controls + + }, { + key: 'pauseVideo', + value: function pauseVideo() { + this._videoEnabled(false); + this.emit('videoOff'); + } + }, { + key: 'resumeVideo', + value: function resumeVideo() { + this._videoEnabled(true); + this.emit('videoOn'); + } + + // Combined controls + + }, { + key: 'pause', + value: function pause() { + this.mute(); + this.pauseVideo(); + } + }, { + key: 'resume', + value: function resume() { + this.unmute(); + this.resumeVideo(); + } + + // Internal methods for enabling/disabling audio/video + + }, { + key: '_audioEnabled', + value: function _audioEnabled(bool) { + this.localStreams.forEach(function (stream) { + stream.getAudioTracks().forEach(function (track) { + track.enabled = !!bool; + }); + }); + } + }, { + key: '_videoEnabled', + value: function _videoEnabled(bool) { + this.localStreams.forEach(function (stream) { + stream.getVideoTracks().forEach(function (track) { + track.enabled = !!bool; + }); + }); + } + + // check if all audio streams are enabled + + }, { + key: 'isAudioEnabled', + value: function isAudioEnabled() { + var enabled = true; + this.localStreams.forEach(function (stream) { + stream.getAudioTracks().forEach(function (track) { + enabled = enabled && track.enabled; + }); + }); + return enabled; + } + + // check if all video streams are enabled + + }, { + key: 'isVideoEnabled', + value: function isVideoEnabled() { + var enabled = true; + this.localStreams.forEach(function (stream) { + stream.getVideoTracks().forEach(function (track) { + enabled = enabled && track.enabled; + }); + }); + return enabled; + } + }, { + key: '_removeStream', + value: function _removeStream(stream) { + var idx = this.localStreams.indexOf(stream); + if (idx > -1) { + this.localStreams.splice(idx, 1); + this.emit('localStreamStopped', stream); + } else { + idx = this.localScreens.indexOf(stream); + if (idx > -1) { + this.localScreens.splice(idx, 1); + this.emit('localScreenStopped', stream); + } + } + } + }, { + key: '_setupAudioMonitor', + value: function _setupAudioMonitor(stream, harkOptions) { + this._log('Setup audio'); + var audio = (0, _hark2.default)(stream, harkOptions); + var self = this; + var timeout = void 0; + + audio.on('speaking', function () { + self.emit('speaking'); + }); + + audio.on('stopped_speaking', function () { + if (timeout) { + clearTimeout(timeout); + } + + timeout = setTimeout(function () { + self.emit('stoppedSpeaking'); + }, 1000); + }); + audio.on('volume_change', function (volume, threshold) { + self.emit('volumeChange', volume, threshold); + }); + + this._audioMonitors.push({ audio: audio, stream: stream }); + } + }, { + key: '_stopAudioMonitor', + value: function _stopAudioMonitor(stream) { + var idx = -1; + this._audioMonitors.forEach(function (monitors, i) { + if (monitors.stream === stream) { + idx = i; + } + }); + + if (idx > -1) { + this._audioMonitors[idx].audio.stop(); + this._audioMonitors.splice(idx, 1); + } + } + }]); + + return LocalMedia; +}(_wildemitter2.default); + +exports.default = LocalMedia; \ No newline at end of file diff --git a/dist/peer.js b/dist/peer.js new file mode 100644 index 0000000..e044006 --- /dev/null +++ b/dist/peer.js @@ -0,0 +1,356 @@ +'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); + +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; } + +function isAllTracksEnded(stream) { + var isAllTracksEnded = true; + stream.getTracks().forEach(function (t) { + isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded; + }); + return isAllTracksEnded; +} + +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(_this.parent.config.peerConnectionConfig, _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(JSON.stringify(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.onclose = this.emit.bind(this, 'channelClose', channel); + channel.onerror = this.emit.bind(this, 'channelError', channel); + channel.onmessage = function (event) { + self.emit('channelMessage', self, channel.label, JSON.parse(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; + this.pc.close(); + this.handleStreamRemoved(emitRemoval); + if (emitRemoval) { + this.parent.emit('removedPeer', this); + } + } + }, { + 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(JSON.stringify({ + size: file.size, + name: file.name + })); + sender.send(file, dc); + }; + // override onclose + dc.onclose = function () { + // ('sender received transfer'); + sender.emit('complete'); + }; + return sender; + } + }]); + + return Peer; +}(_wildemitter2.default); + +exports.default = Peer; \ No newline at end of file diff --git a/dist/socketioconnection.js b/dist/socketioconnection.js new file mode 100644 index 0000000..9bd8d89 --- /dev/null +++ b/dist/socketioconnection.js @@ -0,0 +1,51 @@ +'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 _socket = require('socket.io-client'); + +var _socket2 = _interopRequireDefault(_socket); + +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"); } } + +var SocketIoConnection = function () { + function SocketIoConnection(config) { + _classCallCheck(this, SocketIoConnection); + + this.connection = (0, _socket2.default)(config.url, config.socketio); + } + + _createClass(SocketIoConnection, [{ + key: 'on', + value: function on(ev, fn) { + this.connection.on(ev, fn); + } + }, { + key: 'emit', + value: function emit() { + var _connection; + + (_connection = this.connection).emit.apply(_connection, arguments); + } + }, { + key: 'getSessionid', + value: function getSessionid() { + return this.connection.id; + } + }, { + key: 'disconnect', + value: function disconnect() { + return this.connection.disconnect(); + } + }]); + + return SocketIoConnection; +}(); + +exports.default = SocketIoConnection; \ No newline at end of file diff --git a/dist/webrtc.js b/dist/webrtc.js new file mode 100644 index 0000000..dbe7b73 --- /dev/null +++ b/dist/webrtc.js @@ -0,0 +1,236 @@ +'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 _util = require('util'); + +var _util2 = _interopRequireDefault(_util); + +var _mockconsole = require('mockconsole'); + +var _mockconsole2 = _interopRequireDefault(_mockconsole); + +var _localmedia = require('./localmedia'); + +var _localmedia2 = _interopRequireDefault(_localmedia); + +var _peer = require('./peer'); + +var _peer2 = _interopRequireDefault(_peer); + +var _webrtcsupport = require('./webrtcsupport'); + +var _webrtcsupport2 = _interopRequireDefault(_webrtcsupport); + +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 WebRTC = function (_LocalMedia) { + _inherits(WebRTC, _LocalMedia); + + function WebRTC(opts) { + _classCallCheck(this, WebRTC); + + var _this = _possibleConstructorReturn(this, (WebRTC.__proto__ || Object.getPrototypeOf(WebRTC)).call(this, opts)); + + var self = _this; + var options = opts || {}; + var config = _this.config = { + debug: false, + peerConnectionConfig: { + iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] + }, + peerConnectionConstraints: { + optional: [] + }, + receiveMedia: { + offerToReceiveAudio: 1, + offerToReceiveVideo: 1 + }, + enableDataChannels: true + }; + var item = void 0; + + _this.logger = function () { + // we assume that if you're in debug mode and you didn't + // pass in a logger, you actually want to log as much as + // possible. + if (opts.debug) { + return opts.logger || console; + } + // or we'll use your logger which should have its own logic + // for output. Or we'll return the no-op. + return opts.logger || _mockconsole2.default; + }(); + + // set options + for (item in options) { + if (options.hasOwnProperty(item)) { + _this.config[item] = options[item]; + } + } + + // check for support + if (!_webrtcsupport2.default.support) { + _this.logger.error('Your browser doesn\'t seem to support WebRTC'); + } + + // where we'll store our peer connections + _this.peers = []; + + // call localMedia constructor + // localMedia.call(this, this.config); + + _this.on('speaking', function () { + if (!self.hardMuted) { + self.peers.forEach(function (peer) { + if (peer.enableDataChannels) { + var dc = peer.getDataChannel('liowebrtc'); + if (dc.readyState !== 'open') return; + dc.sendDirectlyToAll(JSON.stringify({ type: 'speaking' })); + } + }); + } + }); + _this.on('stoppedSpeaking', function () { + if (!self.hardMuted) { + self.peers.forEach(function (peer) { + if (peer.enableDataChannels) { + var dc = peer.getDataChannel('liowebrtc'); + if (dc.readyState !== 'open') return; + dc.sendDirectlyToAll(JSON.stringify({ type: 'stoppedSpeaking' })); + } + }); + } + }); + _this.on('volumeChange', function (volume, treshold) { + if (!self.hardMuted) { + self.peers.forEach(function (peer) { + if (peer.enableDataChannels) { + var dc = peer.getDataChannel('liowebrtc'); + if (dc.readyState !== 'open') return; + dc.sendDirectlyToAll(JSON.stringify({ type: 'payload', volume: volume })); + } + }); + } + }); + + // log events in debug mode + if (_this.config.debug) { + _this.on('*', function (event, val1, val2) { + var logger = void 0; + // if you didn't pass in a logger and you explicitly turning on debug + // we're just going to assume you're wanting log output with console + if (self.config.logger === _mockconsole2.default) { + logger = console; + } else { + logger = self.logger; + } + logger.log('event:', event, val1, val2); + }); + } + return _this; + } + + _createClass(WebRTC, [{ + key: 'createPeer', + value: function createPeer(opts) { + var peer = void 0; + opts.parent = this; + peer = new _peer2.default(opts); + this.peers.push(peer); + return peer; + } + + // removes peers + + }, { + key: 'removePeers', + value: function removePeers(id, type) { + this.getPeers(id, type).forEach(function (peer) { + peer.end(); + }); + } + + // fetches all Peer objects by session id and/or type + + }, { + key: 'getPeers', + value: function getPeers(sessionId, type) { + return this.peers.filter(function (peer) { + return (!sessionId || peer.id === sessionId) && (!type || peer.type === type); + }); + } + }, { + key: 'getPeerById', + value: function getPeerById(id) { + return this.peers.filter(function (p) { + return p.id === id; + })[0]; + } + }, { + key: 'getPeerByNick', + value: function getPeerByNick(nick) { + return this.peers.filter(function (p) { + return p.nick === nick; + })[0]; + } + + // sends message to all + + }, { + key: 'sendToAll', + value: function sendToAll(message, payload) { + this.peers.forEach(function (peer) { + peer.send(message, payload); + }); + } + + // sends message to all using a datachannel + // only sends to anyone who has an open datachannel + + }, { + key: 'sendDirectlyToAll', + value: function sendDirectlyToAll(message, payload, channel, shout) { + var msgId = Date.now() + '_' + Math.random() * 1000000; + this.peers.forEach(function (peer) { + if (peer.enableDataChannels) { + peer.sendDirectly(message, payload, channel, shout, msgId); + } + }); + } + }, { + key: 'shout', + value: function shout(messageType, payload) { + this.sendDirectlyToAll(messageType, payload, 'liowebrtc', true); + } + }, { + key: 'whisper', + value: function whisper(peer, messageType, payload) { + peer.sendDirectly(messageType, payload); + } + }, { + key: 'broadcast', + value: function broadcast(messageType, payload) { + this.sendToAll('signalData', { type: messageType, payload: payload }); + } + }, { + key: 'transmit', + value: function transmit(peer, messageType, payload) { + peer.send('signalData', { type: messageType, payload: payload }); + } + }]); + + return WebRTC; +}(_localmedia2.default); + +exports.default = WebRTC; \ No newline at end of file diff --git a/dist/webrtcsupport.js b/dist/webrtcsupport.js new file mode 100644 index 0000000..b624b30 --- /dev/null +++ b/dist/webrtcsupport.js @@ -0,0 +1,45 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var prefix = void 0; +var version = void 0; + +if (window.mozRTCPeerConnection || navigator.mozGetUserMedia) { + prefix = 'moz'; + version = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); +} else if (window.webkitRTCPeerConnection || navigator.webkitGetUserMedia) { + prefix = 'webkit'; + version = navigator.userAgent.match(/Chrom(e|ium)/) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10); +} + +var PC = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; +var IceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; +var SessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; +var MediaStream = window.webkitMediaStream || window.MediaStream; +var screenSharing = window.location.protocol === 'https:' && (prefix === 'webkit' && version >= 26 || prefix === 'moz' && version >= 33); +var AudioContext = window.AudioContext || window.webkitAudioContext; +var videoEl = document.createElement('video'); +var supportVp8 = videoEl && videoEl.canPlayType && videoEl.canPlayType('video/webm; codecs="vp8", vorbis') === 'probably'; +var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia || navigator.mozGetUserMedia; + +// export support flags and constructors.prototype && PC +exports.default = { + prefix: prefix, + browserVersion: version, + support: !!PC && !!getUserMedia, + supportRTCPeerConnection: !!PC, + supportVp8: supportVp8, + supportGetUserMedia: !!getUserMedia, + supportDataChannel: !!(PC && PC.prototype && PC.prototype.createDataChannel), + supportWebAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), + supportMediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), + supportScreenSharing: !!screenSharing, + AudioContext: AudioContext, + PeerConnection: PC, + SessionDescription: SessionDescription, + IceCandidate: IceCandidate, + MediaStream: MediaStream, + getUserMedia: getUserMedia +}; \ No newline at end of file -- GitLab