/** * 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, key: 'peerjs', path: '/', token: util.randomToken(), config: util.defaultConfig }, options); 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(); } // 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.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; 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; var connection = this.getConnection(peer, connectionId); if (connection) { util.warn('Offer received for existing Connection ID:', connectionId); //connection.handleMessage(message); } else { // Create a new connection. if (payload.type === 'media') { var 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; var 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.'; } throw new Error('It doesn\'t look like you have permission to list peers IDs. ' + helpfulError); cb([]); } else if (http.status !== 200) { cb([]); } else { cb(JSON.parse(http.responseText)); } }; http.send(null); } exports.Peer = Peer;