/** * 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', config: util.defaultConfig }, options); this.options = options; // Detect relative URL host. if (options.host === '/') { options.host = window.location.hostname; } // 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 manually 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] // // Initialize the 'socket' (which is actually a mix of XHR streaming and // websockets.) var self = this; this.socket = new Socket(this.options.secure, this.options.host, this.options.port, 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('close', function() { if (!self.disconnected) { // If we haven't explicitly disconnected, emit error. self._abort('socket-closed', 'Underlying socket is already closed.'); } }); // // Start the connections if (id) { this._initialize(id); } else { this._retrieveId(); } // }; util.inherits(Peer, EventEmitter); /** 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.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); self._abort('server-error', 'Could not get an ID from the server'); } 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) { var self = this; this.id = id; this.socket.start(this.id); } /** 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._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.emit('error', new Error('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._lostMessages[connection.id]; if (messages) { for (var i = 0, ii = messages.length; i < ii; i += 1) { connection.handleMessage(messages[i]); } delete this._lostMessages[connection.id]; } } 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) { // 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 connection, to be claimed later. */ Peer.prototype._storeMessage = function(connectionId, message) { if (!this._lostMessages[connectionId]) { this._lostMessages[connectionId] = []; } this._lostMessages[connectionId].push(message); } /** * 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.'); this.emit('error', new Error('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.emit('error', new Error('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. */ Peer.prototype._abort = function(type, message) { util.error('Aborting. Error:', message); var err = new Error(message); err.type = type; this.destroy(); 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() { 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.id = null; } }); } exports.Peer = Peer;