var util = require("./util"); var EventEmitter = require("eventemitter3"); var Socket = require("./socket"); var MediaConnection = require("./mediaconnection"); var DataConnection = require("./dataconnection"); /** * A peer who can initiate connections with other peers. */ function Peer(id, options) { if (!(this instanceof Peer)) return new Peer(id, options); EventEmitter.call(this); // Deal with overloading if (id && id.constructor == Object) { options = id; id = undefined; } else if (id) { // Ensure id is a string id = id.toString(); } // // Configurize options options = util.extend( { debug: 0, // 1: Errors, 2: Warnings, 3: All logs host: util.CLOUD_HOST, port: util.CLOUD_PORT, path: "/", token: util.randomToken(), config: util.defaultConfig }, options ); options.key = "peerjs"; this.options = options; // Detect relative URL host. if (options.host === "/") { options.host = window.location.hostname; } // Set path correctly. if (options.path[0] !== "/") { options.path = "/" + options.path; } if (options.path[options.path.length - 1] !== "/") { options.path += "/"; } // Set whether we use SSL to same as current host if (options.secure === undefined && options.host !== util.CLOUD_HOST) { options.secure = util.isSecure(); } else if(options.host == util.CLOUD_HOST){ options.secure = true; } // Set a custom log function if present if (options.logFunction) { util.setLogFunction(options.logFunction); } util.setLogLevel(options.debug); // // Sanity checks // Ensure WebRTC supported if (!util.supports.audioVideo && !util.supports.data) { this._delayedAbort( "browser-incompatible", "The current browser does not support WebRTC" ); return; } // Ensure alphanumeric id if (!util.validateId(id)) { this._delayedAbort("invalid-id", 'ID "' + id + '" is invalid'); return; } // Ensure valid key // if (!util.validateKey(options.key)) { // this._delayedAbort( // "invalid-key", // 'API KEY "' + options.key + '" is invalid' // ); // return; // } // Ensure not using unsecure cloud server on SSL page // if (options.secure && options.host === "0.peerjs.com") { // this._delayedAbort( // "ssl-unavailable", // "The cloud server currently does not support HTTPS. Please run your own PeerServer to use HTTPS." // ); // return; // } // // States. this.destroyed = false; // Connections have been killed this.disconnected = false; // Connection to PeerServer killed but P2P connections still active this.open = false; // Sockets and such are not yet open. // // References this.connections = {}; // DataConnections for this peer. this._lostMessages = {}; // src => [list of messages] // // Start the server connection this._initializeServerConnection(); if (id) { this._initialize(id); } else { this._retrieveId(); } // } util.inherits(Peer, EventEmitter); // Initialize the 'socket' (which is actually a mix of XHR streaming and // websockets.) Peer.prototype._initializeServerConnection = function() { var self = this; this.socket = new Socket( this.options.secure, this.options.host, this.options.port, this.options.path, this.options.key, this.options.wsport ); this.socket.on("message", function(data) { self._handleMessage(data); }); this.socket.on("error", function(error) { self._abort("socket-error", error); }); this.socket.on("disconnected", function() { // If we haven't explicitly disconnected, emit error and disconnect. if (!self.disconnected) { self.emitError("network", "Lost connection to server."); self.disconnect(); } }); this.socket.on("close", function() { // If we haven't explicitly disconnected, emit error. if (!self.disconnected) { self._abort("socket-closed", "Underlying socket is already closed."); } }); }; /** Get a unique ID from the server via XHR. */ Peer.prototype._retrieveId = function(cb) { var self = this; var http = new XMLHttpRequest(); var protocol = this.options.secure ? "https://" : "http://"; var url = protocol + this.options.host + ":" + this.options.port + this.options.path + this.options.key + "/id"; var queryString = "?ts=" + new Date().getTime() + "" + Math.random(); url += queryString; // If there's no ID we need to wait for one before trying to init socket. http.open("get", url, true); http.onerror = function(e) { util.error("Error retrieving ID", e); var pathError = ""; if (self.options.path === "/" && self.options.host !== util.CLOUD_HOST) { pathError = " If you passed in a `path` to your self-hosted PeerServer, " + "you'll also need to pass in that same path when creating a new " + "Peer."; } self._abort( "server-error", "Could not get an ID from the server." + pathError ); }; http.onreadystatechange = function() { if (http.readyState !== 4) { return; } if (http.status !== 200) { http.onerror(); return; } self._initialize(http.responseText); }; http.send(null); }; /** Initialize a connection with the server. */ Peer.prototype._initialize = function(id) { this.id = id; this.socket.start(this.id, this.options.token); }; /** Handles messages from the server. */ Peer.prototype._handleMessage = function(message) { var type = message.type; var payload = message.payload; var peer = message.src; var connection; switch (type) { case "OPEN": // The connection to the server is open. this.emit("open", this.id); this.open = true; break; case "ERROR": // Server error. this._abort("server-error", payload.msg); break; case "ID-TAKEN": // The selected ID is taken. this._abort("unavailable-id", "ID `" + this.id + "` is taken"); break; case "INVALID-KEY": // The given API key cannot be found. this._abort( "invalid-key", 'API KEY "' + this.options.key + '" is invalid' ); break; // case "LEAVE": // Another peer has closed its connection to this peer. util.log("Received leave message from", peer); this._cleanupPeer(peer); break; case "EXPIRE": // The offer sent to a peer has expired without response. this.emitError("peer-unavailable", "Could not connect to peer " + peer); break; case "OFFER": // we should consider switching this to CALL/CONNECT, but this is the least breaking option. var connectionId = payload.connectionId; connection = this.getConnection(peer, connectionId); if (connection) { connection.close(); util.warn("Offer received for existing Connection ID:", connectionId); } // Create a new connection. if (payload.type === "media") { connection = new MediaConnection(peer, this, { connectionId: connectionId, _payload: payload, metadata: payload.metadata }); this._addConnection(peer, connection); this.emit("call", connection); } else if (payload.type === "data") { connection = new DataConnection(peer, this, { connectionId: connectionId, _payload: payload, metadata: payload.metadata, label: payload.label, serialization: payload.serialization, reliable: payload.reliable }); this._addConnection(peer, connection); this.emit("connection", connection); } else { util.warn("Received malformed connection type:", payload.type); return; } // Find messages. var messages = this._getMessages(connectionId); for (var i = 0, ii = messages.length; i < ii; i += 1) { connection.handleMessage(messages[i]); } break; default: if (!payload) { util.warn( "You received a malformed message from " + peer + " of type " + type ); return; } var id = payload.connectionId; connection = this.getConnection(peer, id); if (connection && connection.pc) { // Pass it on. connection.handleMessage(message); } else if (id) { // Store for possible later use this._storeMessage(id, message); } else { util.warn("You received an unrecognized message:", message); } break; } }; /** Stores messages without a set up connection, to be claimed later. */ Peer.prototype._storeMessage = function(connectionId, message) { if (!this._lostMessages[connectionId]) { this._lostMessages[connectionId] = []; } this._lostMessages[connectionId].push(message); }; /** Retrieve messages from lost message store */ Peer.prototype._getMessages = function(connectionId) { var messages = this._lostMessages[connectionId]; if (messages) { delete this._lostMessages[connectionId]; return messages; } else { return []; } }; /** * Returns a DataConnection to the specified peer. See documentation for a * complete list of options. */ Peer.prototype.connect = function(peer, options) { if (this.disconnected) { util.warn( "You cannot connect to a new Peer because you called " + ".disconnect() on this Peer and ended your connection with the " + "server. You can create a new Peer to reconnect, or call reconnect " + "on this peer if you believe its ID to still be available." ); this.emitError( "disconnected", "Cannot connect to new Peer after disconnecting from server." ); return; } var connection = new DataConnection(peer, this, options); this._addConnection(peer, connection); return connection; }; /** * Returns a MediaConnection to the specified peer. See documentation for a * complete list of options. */ Peer.prototype.call = function(peer, stream, options) { if (this.disconnected) { util.warn( "You cannot connect to a new Peer because you called " + ".disconnect() on this Peer and ended your connection with the " + "server. You can create a new Peer to reconnect." ); this.emitError( "disconnected", "Cannot connect to new Peer after disconnecting from server." ); return; } if (!stream) { util.error( "To call a peer, you must provide a stream from your browser's `getUserMedia`." ); return; } options = options || {}; options._stream = stream; var call = new MediaConnection(peer, this, options); this._addConnection(peer, call); return call; }; /** Add a data/media connection to this peer. */ Peer.prototype._addConnection = function(peer, connection) { if (!this.connections[peer]) { this.connections[peer] = []; } this.connections[peer].push(connection); }; /** Retrieve a data/media connection for this peer. */ Peer.prototype.getConnection = function(peer, id) { var connections = this.connections[peer]; if (!connections) { return null; } for (var i = 0, ii = connections.length; i < ii; i++) { if (connections[i].id === id) { return connections[i]; } } return null; }; Peer.prototype._delayedAbort = function(type, message) { var self = this; util.setZeroTimeout(function() { self._abort(type, message); }); }; /** * Destroys the Peer and emits an error message. * The Peer is not destroyed if it's in a disconnected state, in which case * it retains its disconnected state and its existing connections. */ Peer.prototype._abort = function(type, message) { util.error("Aborting!"); if (!this._lastServerId) { this.destroy(); } else { this.disconnect(); } this.emitError(type, message); }; /** Emits a typed error message. */ Peer.prototype.emitError = function(type, err) { util.error("Error:", err); if (typeof err === "string") { err = new Error(err); } err.type = type; this.emit("error", err); }; /** * Destroys the Peer: closes all active connections as well as the connection * to the server. * Warning: The peer can no longer create or accept connections after being * destroyed. */ Peer.prototype.destroy = function() { if (!this.destroyed) { this._cleanup(); this.disconnect(); this.destroyed = true; } }; /** Disconnects every connection on this peer. */ Peer.prototype._cleanup = function() { if (this.connections) { var peers = Object.keys(this.connections); for (var i = 0, ii = peers.length; i < ii; i++) { this._cleanupPeer(peers[i]); } } this.emit("close"); }; /** Closes all connections to this peer. */ Peer.prototype._cleanupPeer = function(peer) { var connections = this.connections[peer]; for (var j = 0, jj = connections.length; j < jj; j += 1) { connections[j].close(); } }; /** * Disconnects the Peer's connection to the PeerServer. Does not close any * active connections. * Warning: The peer can no longer create or accept connections after being * disconnected. It also cannot reconnect to the server. */ Peer.prototype.disconnect = function() { var self = this; util.setZeroTimeout(function() { if (!self.disconnected) { self.disconnected = true; self.open = false; if (self.socket) { self.socket.close(); } self.emit("disconnected", self.id); self._lastServerId = self.id; self.id = null; } }); }; /** Attempts to reconnect with the same ID. */ Peer.prototype.reconnect = function() { if (this.disconnected && !this.destroyed) { util.log("Attempting reconnection to server with ID " + this._lastServerId); this.disconnected = false; this._initializeServerConnection(); this._initialize(this._lastServerId); } else if (this.destroyed) { throw new Error( "This peer cannot reconnect to the server. It has already been destroyed." ); } else if (!this.disconnected && !this.open) { // Do nothing. We're still connecting the first time. util.error( "In a hurry? We're still trying to make the initial connection!" ); } else { throw new Error( "Peer " + this.id + " cannot reconnect because it is not disconnected from the server!" ); } }; /** * Get a list of available peer IDs. If you're running your own server, you'll * want to set allow_discovery: true in the PeerServer options. If you're using * the cloud server, email team@peerjs.com to get the functionality enabled for * your key. */ Peer.prototype.listAllPeers = function(cb) { cb = cb || function() {}; var self = this; var http = new XMLHttpRequest(); var protocol = this.options.secure ? "https://" : "http://"; var url = protocol + this.options.host + ":" + this.options.port + this.options.path + this.options.key + "/peers"; var queryString = "?ts=" + new Date().getTime() + "" + Math.random(); url += queryString; // If there's no ID we need to wait for one before trying to init socket. http.open("get", url, true); http.onerror = function(e) { self._abort("server-error", "Could not get peers from the server."); cb([]); }; http.onreadystatechange = function() { if (http.readyState !== 4) { return; } if (http.status === 401) { var helpfulError = ""; if (self.options.host !== util.CLOUD_HOST) { helpfulError = "It looks like you're using the cloud server. You can email " + "team@peerjs.com to enable peer listing for your API key."; } else { helpfulError = "You need to enable `allow_discovery` on your self-hosted " + "PeerServer to use this feature."; } cb([]); throw new Error( "It doesn't look like you have permission to list peers IDs. " + helpfulError ); } else if (http.status !== 200) { cb([]); } else { cb(JSON.parse(http.responseText)); } }; http.send(null); }; module.exports = Peer;