peer.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /**
  2. * A peer who can initiate connections with other peers.
  3. */
  4. function Peer(id, options) {
  5. if (id && id.constructor == Object) {
  6. options = id;
  7. id = undefined;
  8. }
  9. if (!(this instanceof Peer)) return new Peer(id, options);
  10. EventEmitter.call(this);
  11. options = util.extend({
  12. debug: false,
  13. host: '0.peerjs.com',
  14. port: 9000,
  15. key: 'peerjs',
  16. config: { 'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }] }
  17. }, options);
  18. this._options = options;
  19. util.debug = options.debug;
  20. // First check if browser can use PeerConnection/DataChannels.
  21. // TODO: when media is supported, lower browser version limit and move DC
  22. // check to where`connect` is called.
  23. var self = this;
  24. if (!util.isBrowserCompatible()) {
  25. util.setZeroTimeout(function() {
  26. self._abort('browser-incompatible', 'The current browser does not support WebRTC DataChannels');
  27. });
  28. return;
  29. }
  30. // Detect relative URL host.
  31. if (options.host === '/') {
  32. options.host = window.location.hostname;
  33. }
  34. // Ensure alphanumeric_-
  35. if (id && !/^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(id)) {
  36. util.setZeroTimeout(function() {
  37. self._abort('invalid-id', 'ID "' + id + '" is invalid');
  38. });
  39. return;
  40. }
  41. if (options.key && !/^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(options.key)) {
  42. util.setZeroTimeout(function() {
  43. self._abort('invalid-key', 'API KEY "' + options.key + '" is invalid');
  44. });
  45. return;
  46. }
  47. this._secure = util.isSecure();
  48. // Errors for now because no support for SSL on cloud server.
  49. if (this._secure && options.host === '0.peerjs.com') {
  50. util.setZeroTimeout(function() {
  51. self._abort('ssl-unavailable',
  52. 'The cloud server currently does not support HTTPS. Please run your own PeerServer to use HTTPS.');
  53. });
  54. return;
  55. }
  56. // States.
  57. this.destroyed = false;
  58. this.disconnected = false;
  59. // DataConnections for this peer.
  60. this.connections = {};
  61. // MediaConnections for this peer
  62. this.calls = {}
  63. // Connection managers.
  64. // peer => {
  65. // nextId: unique number to use for next manager created
  66. // dataManager: the last created data manager, for multiplexing data connections
  67. // managers: { id: manager} }
  68. // }
  69. this._managers = {};
  70. // Queued connections to make.
  71. this._queued = [];
  72. // Init immediately if ID is given, otherwise ask server for ID
  73. this.id = null;
  74. if (id) {
  75. this._initialize(id.toString());
  76. } else {
  77. this._retrieveId();
  78. }
  79. };
  80. util.inherits(Peer, EventEmitter);
  81. Peer.prototype._retrieveId = function(cb) {
  82. var self = this;
  83. try {
  84. var http = new XMLHttpRequest();
  85. var protocol = this._secure ? 'https://' : 'http://';
  86. var url = protocol + this._options.host + ':' + this._options.port + '/' + this._options.key + '/id';
  87. var queryString = '?ts=' + new Date().getTime() + '' + Math.random();
  88. url += queryString;
  89. // If there's no ID we need to wait for one before trying to init socket.
  90. http.open('get', url, true);
  91. http.onreadystatechange = function() {
  92. if (http.readyState === 4) {
  93. if (http.status !== 200) {
  94. throw 'Retrieve ID response not 200';
  95. return;
  96. }
  97. self.id = http.responseText;
  98. self._initialize(self.id);
  99. }
  100. };
  101. http.send(null);
  102. } catch(e) {
  103. this._abort('server-error', 'Could not get an ID from the server');
  104. }
  105. };
  106. Peer.prototype._initialize = function(id) {
  107. var self = this;
  108. this.id = id;
  109. this._socket = new Socket(this._options.host, this._options.port, this._options.key, this.id);
  110. this._socket.on('message', function(data) {
  111. self._handleServerJSONMessage(data);
  112. });
  113. this._socket.on('error', function(error) {
  114. util.log(error);
  115. self._abort('socket-error', error);
  116. });
  117. this._socket.on('close', function() {
  118. var msg = 'Underlying socket has closed';
  119. util.log('error', msg);
  120. self._abort('socket-closed', msg);
  121. });
  122. this._socket.start();
  123. }
  124. Peer.prototype._handleServerJSONMessage = function(message) {
  125. var peer = message.src;
  126. var managerId = message.manager;
  127. var manager = this._getManager(peer, managerId);
  128. var payload = message.payload;
  129. switch (message.type) {
  130. case 'OPEN':
  131. this._processQueue();
  132. this.emit('open', this.id);
  133. break;
  134. case 'ERROR':
  135. this._abort('server-error', payload.msg);
  136. break;
  137. case 'ID-TAKEN':
  138. this._abort('unavailable-id', 'ID `'+this.id+'` is taken');
  139. break;
  140. case 'OFFER':
  141. var options = {
  142. sdp: payload.sdp,
  143. labels: payload.labels,
  144. config: this._options.config
  145. };
  146. // Either forward to or create new manager
  147. if (!manager) {
  148. manager = this._createManager(managerId, peer, options);
  149. }
  150. manager.handleUpdate(options.labels);
  151. manager.handleSDP(payload.sdp, message.type, payload.call);
  152. break;
  153. case 'EXPIRE':
  154. peer.emit('error', new Error('Could not connect to peer ' + manager.peer));
  155. break;
  156. case 'ANSWER':
  157. // Forward to specific manager
  158. if (manager) {
  159. manager.handleSDP(payload.sdp, message.type);
  160. }
  161. break;
  162. case 'CANDIDATE':
  163. // Forward to specific manager
  164. if (manager) {
  165. manager.handleCandidate(payload);
  166. }
  167. break;
  168. case 'LEAVE':
  169. // Leave on all managers for a user
  170. if (this._managers[peer]) {
  171. var ids = Object.keys(this._managers[peer].managers);
  172. for (var i = 0; i < ids.length; i++) {
  173. this._managers[peer].managers[ids[i]].handleLeave();
  174. }
  175. }
  176. break;
  177. case 'INVALID-KEY':
  178. this._abort('invalid-key', 'API KEY "' + this._key + '" is invalid');
  179. break;
  180. default:
  181. util.log('Unrecognized message type:', message.type);
  182. break;
  183. }
  184. };
  185. /** Process queued calls to connect. */
  186. Peer.prototype._processQueue = function() {
  187. while (this._queued.length > 0) {
  188. var manager = this._queued.pop();
  189. manager.initialize(this.id, this._socket);
  190. }
  191. };
  192. /** Listeners for manager. */
  193. Peer.prototype._attachManagerListeners = function(manager) {
  194. var self = this;
  195. // Handle receiving a connection.
  196. manager.on('connection', function(connection) {
  197. self._managers[manager.peer].dataManager = manager;
  198. self.connections[connection.label] = connection;
  199. self.emit('connection', connection);
  200. });
  201. // Handle receiving a call
  202. manager.on('call', function(call) {
  203. self.calls[call.label] = call;
  204. self.emit('call', call);
  205. });
  206. // Handle a connection closing.
  207. manager.on('close', function() {
  208. if (!!self._managers[manager.peer]) {
  209. delete self._managers[manager.peer];
  210. // TODO: delete relevant calls and connections
  211. }
  212. });
  213. manager.on('error', function(err) {
  214. self.emit('error', err);
  215. });
  216. };
  217. Peer.prototype._getManager = function(peer, managerId) {
  218. if (this._managers[peer]) {
  219. return this._managers[peer].managers[managerId];
  220. }
  221. }
  222. Peer.prototype._getDataManager = function(peer) {
  223. if (this._managers[peer]) {
  224. return this._managers[peer].dataManager;
  225. }
  226. }
  227. /** Exposed connect function for users. Will try to connect later if user
  228. * is waiting for an ID. */
  229. Peer.prototype._createManager = function(managerId, peer, options) {
  230. if (this.disconnected) {
  231. var err = new Error('This Peer has been disconnected from the server and can no longer make connections.');
  232. err.type = 'server-disconnected';
  233. this.emit('error', err);
  234. return;
  235. }
  236. options = util.extend({
  237. config: this._options.config
  238. }, options);
  239. if (!this._managers[peer]) {
  240. this._managers[peer] = {nextId: 0, managers: {}};
  241. }
  242. managerId = managerId || peer + this._managers[peer].nextId++;
  243. var manager = new ConnectionManager(managerId, peer, options);
  244. if (!!this.id && !!this._socket) {
  245. manager.initialize(this.id, this._socket);
  246. } else {
  247. this._queued.push(manager);
  248. }
  249. this._attachManagerListeners(manager);
  250. this._managers[peer].managers[manager._managerId] = manager;
  251. return manager;
  252. };
  253. Peer.prototype.connect = function(peer, options) {
  254. var manager = this._getDataManager(peer);
  255. if (!manager) {
  256. manager = this._createManager(false, peer, options);
  257. }
  258. var connection = manager.connect(options);
  259. return connection;
  260. }
  261. Peer.prototype.call = function(peer, stream, options) {
  262. var manager = this._createManager(false, peer, options);
  263. var connection = manager.call(stream, options);
  264. return connection;
  265. }
  266. /** Destroys the Peer and emits an error message. */
  267. Peer.prototype._abort = function(type, message) {
  268. util.log('Aborting. Error:', message);
  269. var err = new Error(message);
  270. err.type = type;
  271. this.destroy();
  272. this.emit('error', err);
  273. };
  274. /**
  275. * Destroys the Peer: closes all active connections as well as the connection
  276. * to the server.
  277. * Warning: The peer can no longer create or accept connections after being
  278. * destroyed.
  279. */
  280. Peer.prototype.destroy = function() {
  281. if (!this.destroyed) {
  282. this._cleanup();
  283. this.destroyed = true;
  284. }
  285. };
  286. Peer.prototype._cleanup = function() {
  287. var self = this;
  288. if (!!this._managers) {
  289. var peers = Object.keys(this._managers);
  290. for (var i = 0, ii = peers.length; i < ii; i++) {
  291. var ids = Object.keys(this._managers[peers[i]]);
  292. for (var j = 0, jj = peers.length; j < jj; j++) {
  293. this._managers[peers[i]][ids[j]].close();
  294. }
  295. }
  296. }
  297. util.setZeroTimeout(function(){
  298. self.disconnect();
  299. });
  300. this.emit('close');
  301. };
  302. /**
  303. * Disconnects the Peer's connection to the PeerServer. Does not close any
  304. * active connections.
  305. * Warning: The peer can no longer create or accept connections after being
  306. * disconnected. It also cannot reconnect to the server.
  307. */
  308. Peer.prototype.disconnect = function() {
  309. if (!this.disconnected) {
  310. if (!!this._socket) {
  311. this._socket.close();
  312. }
  313. this.id = null;
  314. this.disconnected = true;
  315. }
  316. };
  317. /** The current browser. */
  318. Peer.browser = util.browserisms;
  319. exports.Peer = Peer;