peer.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. /**
  2. * A peer who can initiate connections with other peers.
  3. */
  4. function Peer(id, options) {
  5. if (id.constructor == Object) {
  6. options = id;
  7. id = undefined;
  8. }
  9. if (!(this instanceof Peer)) return new Peer(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. // Ensure alphanumeric_-
  21. if (id && !/^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(id)) {
  22. this._abort('invalid-id', 'ID "' + id + '" is invalid');
  23. return
  24. }
  25. if (options.key && !/^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(options.key)) {
  26. this._abort('invalid-key', 'API KEY "' + options.key + '" is invalid');
  27. return
  28. }
  29. // Connections for this peer.
  30. this.connections = {};
  31. // Queued connections to make.
  32. this._queued = [];
  33. // Init immediately if ID is given, otherwise ask server for ID
  34. if (id) {
  35. this.id = id;
  36. this._init();
  37. } else {
  38. this._getId();
  39. }
  40. };
  41. util.inherits(Peer, EventEmitter);
  42. Peer.prototype._getId = function(cb) {
  43. var self = this;
  44. try {
  45. var http = new XMLHttpRequest();
  46. var url = 'http://' + this._options.host + ':' + this._options.port + '/' + this._options.key + '/id';
  47. // If there's no ID we need to wait for one before trying to init socket.
  48. http.open('get', url, true);
  49. http.onreadystatechange = function() {
  50. if (http.readyState === 4) {
  51. self.id = http.responseText;
  52. self._init();
  53. }
  54. };
  55. http.send(null);
  56. } catch(e) {
  57. this._abort('server-error', 'Could not get an ID from the server');
  58. }
  59. };
  60. Peer.prototype._init = function() {
  61. var self = this;
  62. this._socket = new Socket(this._options.host, this._options.port, this._options.key, this.id);
  63. this._socket.on('message', function(data) {
  64. self._handleServerJSONMessage(data);
  65. });
  66. this._socket.on('error', function(error) {
  67. util.log(error);
  68. self._abort('socket-error', error);
  69. });
  70. this._socket.on('close', function() {
  71. var msg = 'Underlying socket has closed';
  72. util.log('error', msg);
  73. self._abort('socket-closed', msg);
  74. });
  75. this._socket.start();
  76. }
  77. Peer.prototype._handleServerJSONMessage = function(message) {
  78. var peer = message.src;
  79. var connection = this.connections[peer];
  80. payload = message.payload;
  81. switch (message.type) {
  82. case 'OPEN':
  83. this._processQueue();
  84. this.emit('open', this.id);
  85. break;
  86. case 'ERROR':
  87. util.log(payload.msg);
  88. this._abort('server-error', payload.msg);
  89. break;
  90. case 'ID-TAKEN':
  91. this._abort('unavailable-id', 'ID `'+this.id+'` is taken');
  92. break;
  93. case 'OFFER':
  94. var options = {
  95. metadata: payload.metadata,
  96. serialization: payload.serialization,
  97. sdp: payload.sdp,
  98. config: this._options.config
  99. };
  100. var connection = new DataConnection(this.id, peer, this._socket, options);
  101. this._attachConnectionListeners(connection);
  102. this.connections[peer] = connection;
  103. this.emit('connection', connection, payload.metadata);
  104. break;
  105. case 'EXPIRE':
  106. connection = this.connections[peer];
  107. if (connection) {
  108. connection.close();
  109. connection.emit('error', new Error('Could not connect to peer ' + connection.peer));
  110. }
  111. break;
  112. case 'ANSWER':
  113. if (connection) {
  114. connection.handleSDP(payload.sdp, message.type);
  115. }
  116. break;
  117. case 'CANDIDATE':
  118. if (connection) {
  119. connection.handleCandidate(payload);
  120. }
  121. break;
  122. case 'LEAVE':
  123. if (connection) {
  124. connection.handleLeave();
  125. }
  126. break;
  127. case 'INVALID-KEY':
  128. this._abort('invalid-key', 'API KEY "' + this._key + '" is invalid');
  129. break;
  130. case 'PORT':
  131. //if (util.browserisms === 'Firefox') {
  132. // connection.handlePort(payload);
  133. // break;
  134. //}
  135. default:
  136. util.log('Unrecognized message type:', message.type);
  137. break;
  138. }
  139. };
  140. /** Process queued calls to connect. */
  141. Peer.prototype._processQueue = function() {
  142. while (this._queued.length > 0) {
  143. var conn = this._queued.pop();
  144. conn.initialize(this.id, this._socket);
  145. }
  146. };
  147. /** Destroys the Peer and emits an error message. */
  148. Peer.prototype._abort = function(type, message) {
  149. var err = new Error(message);
  150. err.type = type;
  151. this.emit('error', err);
  152. this.destroy();
  153. };
  154. Peer.prototype._cleanup = function() {
  155. var self = this;
  156. var peers = Object.keys(this.connections);
  157. for (var i = 0, ii = peers.length; i < ii; i++) {
  158. this.connections[peers[i]].close();
  159. }
  160. util.setZeroTimeout(function(){
  161. self._socket.close();
  162. });
  163. this.emit('close');
  164. };
  165. /** Listeners for DataConnection events. */
  166. Peer.prototype._attachConnectionListeners = function(connection) {
  167. var self = this;
  168. connection.on('close', function(peer) {
  169. if (self.connections[peer]) {
  170. delete self.connections[peer];
  171. }
  172. });
  173. };
  174. /** Exposed connect function for users. Will try to connect later if user
  175. * is waiting for an ID. */
  176. // TODO: pause XHR streaming when not in use and start again when this is
  177. // called.
  178. Peer.prototype.connect = function(peer, options) {
  179. if (this.destroyed) {
  180. this._abort('peer-destroyed', 'This Peer has been destroyed and is no longer able to make connections.')
  181. }
  182. options = util.extend({
  183. config: this._options.config
  184. }, options);
  185. var connection = new DataConnection(this.id, peer, this._socket, options);
  186. this._attachConnectionListeners(connection);
  187. this.connections[peer] = connection;
  188. if (!this.id) {
  189. this._queued.push(connection);
  190. }
  191. return connection;
  192. };
  193. Peer.prototype.destroy = function() {
  194. if (!this.destroyed) {
  195. this._cleanup();
  196. this.destroyed = true;
  197. }
  198. };
  199. exports.Peer = Peer;