123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 |
- 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;
|