sink.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. function Peer(options) {
  2. if (!(this instanceof Peer)) return new Peer(options);
  3. EventEmitter.call(this);
  4. options = util.extend({
  5. debug: false
  6. }, options);
  7. util.debug = options.debug;
  8. this._config = options.config || { 'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }] };
  9. this._id = null;
  10. // User handlers.
  11. this._handlers = {};
  12. // Source to connect to; null if waiting for a connection.
  13. this._peer = options.source || null;
  14. // Booleans to determine what streams to allow.
  15. this._video = options.video;
  16. this._data = options.data != undefined ? options.data : true;
  17. this._audio = options.audio;
  18. // Connections
  19. this._pc = null;
  20. this._dc = null;
  21. this._socket = new WebSocket(options.ws || 'ws://localhost');
  22. // Local streams for multiple use.
  23. this._localVideo = options.localVideo || null;
  24. this._localAudio = options.localAudio || null;
  25. // Init socket msg handlers
  26. var self = this;
  27. this._socket.onopen = function() {
  28. self.socketInit();
  29. };
  30. // Firefoxism: connectDataConnection ports.
  31. if (browserisms == 'Firefox' && !options.source) {
  32. if (!Peer.usedPorts) {
  33. Peer.usedPorts = [];
  34. }
  35. this.localPort = util.randomPort();
  36. while (Peer.usedPorts.indexOf(this.localPort) != -1) {
  37. this.localPort = util.randomPort();
  38. }
  39. this.remotePort = util.randomPort();
  40. while (this.remotePort == this.localPort ||
  41. Peer.usedPorts.indexOf(this.localPort) != -1) {
  42. this.remotePort = util.randomPort();
  43. }
  44. Peer.usedPorts.push(this.remotePort);
  45. Peer.usedPorts.push(this.localPort);
  46. }
  47. };
  48. util.inherits(Peer, EventEmitter);
  49. /** Start up websocket communications. */
  50. Peer.prototype.socketInit = function() {
  51. var self = this;
  52. if (!!this._peer) {
  53. // Announce as a sink if initiated with a source.
  54. this._socket.send(JSON.stringify({
  55. type: 'SINK',
  56. source: this._peer,
  57. isms: browserisms
  58. }));
  59. this._socket.onmessage = function(event) {
  60. var message = JSON.parse(event.data);
  61. switch (message.type) {
  62. case 'SINK-ID':
  63. self._id = message.id;
  64. self.emit('ready', self._id);
  65. self.startPeerConnection();
  66. break;
  67. case 'OFFER':
  68. var sdp = message.sdp;
  69. try {
  70. sdp = new RTCSessionDescription(message.sdp);
  71. } catch(e) {
  72. util.log('Firefox');
  73. }
  74. self._pc.setRemoteDescription(sdp, function() {
  75. util.log('setRemoteDescription: offer');
  76. // If we also have to set up a stream on the sink end, do so.
  77. self.handleStream(false, function() {
  78. self.maybeBrowserisms(false);
  79. });
  80. }, function(err) {
  81. util.log('failed to setRemoteDescription with offer, ', err);
  82. });
  83. break;
  84. case 'CANDIDATE':
  85. util.log(message.candidate);
  86. var candidate = new RTCIceCandidate(message.candidate);
  87. self._pc.addIceCandidate(candidate);
  88. break;
  89. case 'LEAVE':
  90. util.log('counterpart disconnected');
  91. if (!!self._pc && self._pc.readyState != 'closed') {
  92. self._pc.close();
  93. self._pc = null;
  94. self._peer = null;
  95. }
  96. if (!!self._dc && self._dc.readyState != 'closed') {
  97. self._dc.close();
  98. self._dc = null;
  99. }
  100. break;
  101. case 'PORT':
  102. if (browserisms && browserisms == 'Firefox') {
  103. if (!Peer.usedPorts) {
  104. Peer.usedPorts = [];
  105. }
  106. Peer.usedPorts.push(message.local);
  107. Peer.usedPorts.push(message.remote);
  108. self._pc.connectDataConnection(message.local, message.remote);
  109. break;
  110. }
  111. case 'DEFAULT':
  112. util.log('SINK: unrecognized message ', message.type);
  113. break;
  114. }
  115. };
  116. } else {
  117. // Otherwise, this sink is the originator to another sink and should wait
  118. // for an alert to begin the PC process.
  119. this._socket.send(JSON.stringify({
  120. type: 'SOURCE',
  121. isms: browserisms
  122. }));
  123. this._socket.onmessage = function(event) {
  124. var message = JSON.parse(event.data);
  125. switch (message.type) {
  126. case 'SOURCE-ID':
  127. self._id = message.id;
  128. self.emit('ready', self._id);
  129. break;
  130. case 'SINK-CONNECTED':
  131. self._peer = message.sink;
  132. self.startPeerConnection();
  133. self.handleStream(true, function() {
  134. self.maybeBrowserisms(true);
  135. });
  136. break;
  137. case 'ANSWER':
  138. var sdp = message.sdp;
  139. try {
  140. sdp = new RTCSessionDescription(message.sdp);
  141. } catch(e) {
  142. util.log('Firefox');
  143. }
  144. self._pc.setRemoteDescription(sdp, function() {
  145. util.log('setRemoteDescription: answer');
  146. // Firefoxism
  147. if (browserisms == 'Firefox') {
  148. self._pc.connectDataConnection(self.localPort, self.remotePort);
  149. self._socket.send(JSON.stringify({
  150. type: 'PORT',
  151. dst: self._peer,
  152. remote: self.localPort,
  153. local: self.remotePort
  154. }));
  155. }
  156. util.log('ORIGINATOR: PeerConnection success');
  157. }, function(err) {
  158. util.log('failed to setRemoteDescription, ', err);
  159. });
  160. break;
  161. case 'CANDIDATE':
  162. util.log(message.candidate);
  163. var candidate = new RTCIceCandidate(message.candidate);
  164. self._pc.addIceCandidate(candidate);
  165. break;
  166. case 'LEAVE':
  167. util.log('counterpart disconnected');
  168. if (!!self._pc && self._pc.readyState != 'closed') {
  169. self._pc.close();
  170. self._pc = null;
  171. self._peer = null;
  172. }
  173. if (!!self._dc && self._dc.readyState != 'closed') {
  174. self._dc.close();
  175. self._dc = null;
  176. }
  177. break;
  178. case 'DEFAULT':
  179. util.log('ORIGINATOR: message not recognized ', message.type);
  180. }
  181. };
  182. }
  183. // Makes sure things clean up neatly when window is closed.
  184. window.onbeforeunload = function() {
  185. if (!!self._pc && self._pc.readyState != 'closed') {
  186. self._pc.close();
  187. }
  188. if (!!self._socket && !!self._peer) {
  189. self._socket.send(JSON.stringify({ type: 'LEAVE', dst: self._peer }));
  190. if (!!self._dc && self._dc.readyState != 'closed') {
  191. self._dc.close();
  192. }
  193. }
  194. }
  195. };
  196. /** Takes care of ice handlers. */
  197. Peer.prototype.setupIce = function() {
  198. var self = this;
  199. this._pc.onicecandidate = function(event) {
  200. util.log('candidates received');
  201. if (event.candidate) {
  202. self._socket.send(JSON.stringify({
  203. type: 'CANDIDATE',
  204. candidate: event.candidate,
  205. dst: self._peer
  206. }));
  207. } else {
  208. util.log("End of candidates.");
  209. }
  210. };
  211. };
  212. /** Starts a PeerConnection and sets up handlers. */
  213. Peer.prototype.startPeerConnection = function() {
  214. this._pc = new RTCPeerConnection(this._config, { optional:[ { RtpDataChannels: true } ]});
  215. this.setupIce();
  216. this.setupAudioVideo();
  217. };
  218. /** Decide whether to handle Firefoxisms. */
  219. Peer.prototype.maybeBrowserisms = function(originator) {
  220. var self = this;
  221. if (browserisms == 'Firefox' && !this._video && !this._audio/* && !this._stream*/) {
  222. getUserMedia({ audio: true, fake: true }, function(s) {
  223. self._pc.addStream(s);
  224. if (originator) {
  225. self.makeOffer();
  226. } else {
  227. self.makeAnswer();
  228. }
  229. }, function(err) { util.log('crap'); });
  230. } else {
  231. if (originator) {
  232. this.makeOffer();
  233. } else {
  234. this.makeAnswer();
  235. }
  236. }
  237. }
  238. /** Create an answer for PC. */
  239. Peer.prototype.makeAnswer = function() {
  240. var self = this;
  241. this._pc.createAnswer(function(answer) {
  242. util.log('createAnswer');
  243. self._pc.setLocalDescription(answer, function() {
  244. util.log('setLocalDescription: answer');
  245. self._socket.send(JSON.stringify({
  246. type: 'ANSWER',
  247. src: self._id,
  248. sdp: answer,
  249. dst: self._peer
  250. }));
  251. }, function(err) {
  252. util.log('failed to setLocalDescription, ', err)
  253. });
  254. }, function(err) {
  255. util.log('failed to create answer, ', err)
  256. });
  257. };
  258. /** Create an offer for PC. */
  259. Peer.prototype.makeOffer = function() {
  260. var self = this;
  261. this._pc.createOffer(function(offer) {
  262. util.log('createOffer')
  263. self._pc.setLocalDescription(offer, function() {
  264. util.log('setLocalDescription: offer');
  265. self._socket.send(JSON.stringify({
  266. type: 'OFFER',
  267. sdp: offer,
  268. dst: self._peer,
  269. src: self._id
  270. }));
  271. }, function(err) {
  272. util.log('failed to setLocalDescription, ', err);
  273. });
  274. });
  275. };
  276. /** Sets up A/V stream handler. */
  277. Peer.prototype.setupAudioVideo = function() {
  278. var self = this;
  279. util.log('onaddstream handler added');
  280. this._pc.onaddstream = function(obj) {
  281. util.log('Remote stream added');
  282. // this._stream = true;
  283. self.emit('remotestream', obj.type, obj.stream);
  284. };
  285. };
  286. /** Handle the different types of streams requested by user. */
  287. Peer.prototype.handleStream = function(originator, cb) {
  288. if (this._data) {
  289. this.setupDataChannel(originator);
  290. }
  291. this.getAudioVideo(originator, cb);
  292. };
  293. /** Get A/V streams. */
  294. Peer.prototype.getAudioVideo = function(originator, cb) {
  295. var self = this;
  296. if (this._video && !this._localVideo) {
  297. getUserMedia({ video: true }, function(vstream) {
  298. self._pc.addStream(vstream);
  299. self._localVideo = vstream;
  300. util.log('Local video stream added');
  301. self.emit('localstream', 'video', vstream);
  302. if (self._audio && !self._localAudio) {
  303. getUserMedia({ audio: true }, function(astream) {
  304. self._pc.addStream(astream);
  305. self._localAudio = astream;
  306. util.log('Local audio stream added');
  307. self.emit('localstream', 'audio', astream);
  308. cb();
  309. }, function(err) { util.log('Audio cannot start'); cb(); });
  310. } else {
  311. if (self._audio) {
  312. self._pc.addStream(self._localAudio);
  313. }
  314. cb();
  315. }
  316. }, function(err) { util.log('Video cannot start', err); cb(); });
  317. } else if (this._audio && !this._localAudio) {
  318. getUserMedia({ audio: true }, function(astream) {
  319. self._pc.addStream(astream);
  320. self._localAudio = astream;
  321. util.log('Local audio stream added');
  322. self.emit('localstream', 'audio', astream);
  323. cb();
  324. }, function(err) { util.log('Audio cannot start'); cb(); });
  325. } else {
  326. if (this._audio) {
  327. this._pc.addStream(this._localAudio);
  328. }
  329. if (this._video) {
  330. this._pc.addStream(this._localVideo);
  331. }
  332. util.log('no audio/video streams initiated');
  333. cb();
  334. }
  335. };
  336. /** Sets up DataChannel handlers. */
  337. Peer.prototype.setupDataChannel = function(originator, cb) {
  338. var self = this;
  339. if (originator) {
  340. /** ORIGINATOR SETUP */
  341. if (browserisms == 'Webkit') {
  342. this._pc.onstatechange = function() {
  343. util.log('State Change: ', self._pc.readyState);
  344. /*if (self._pc.readyState == 'active') {
  345. util.log('ORIGINATOR: active state detected');
  346. self._dc = self._pc.createDataChannel('StreamAPI', { reliable: false });
  347. self._dc.binaryType = 'blob';
  348. if (!!self._handlers['connection']) {
  349. self._handlers['connection'](self._peer);
  350. }
  351. self._dc.onmessage = function(e) {
  352. self.handleDataMessage(e);
  353. };
  354. }*/
  355. }
  356. } else {
  357. this._pc.onconnection = function() {
  358. util.log('ORIGINATOR: onconnection triggered');
  359. self.startDataChannel();
  360. };
  361. }
  362. } else {
  363. /** TARGET SETUP */
  364. this._pc.ondatachannel = function(dc) {
  365. util.log('SINK: ondatachannel triggered');
  366. self._dc = dc;
  367. self._dc.binaryType = 'blob';
  368. self.emit('connection', self._peer);
  369. self._dc.onmessage = function(e) {
  370. self.handleDataMessage(e);
  371. };
  372. };
  373. this._pc.onconnection = function() {
  374. util.log('SINK: onconnection triggered');
  375. };
  376. }
  377. this._pc.onclosedconnection = function() {
  378. // Remove socket handlers perhaps.
  379. };
  380. };
  381. Peer.prototype.startDataChannel = function() {
  382. var self = this;
  383. this._dc = this._pc.createDataChannel(this._peer, { reliable: false });
  384. this._dc.binaryType = 'blob';
  385. this.emit('connection', this._peer);
  386. this._dc.onmessage = function(e) {
  387. self.handleDataMessage(e);
  388. };
  389. };
  390. /** Allows user to send data. */
  391. Peer.prototype.send = function(data) {
  392. var ab = BinaryPack.pack(data);
  393. this._dc.send(ab);
  394. }
  395. // Handles a DataChannel message.
  396. // TODO: have these extend Peer, which will impl these generic handlers.
  397. Peer.prototype.handleDataMessage = function(e) {
  398. var self = this;
  399. var fr = new FileReader();
  400. fr.onload = function(evt) {
  401. var ab = evt.target.result;
  402. var data = BinaryPack.unpack(ab);
  403. self.emit('data', data);
  404. };
  405. fr.readAsArrayBuffer(e.data);
  406. }
  407. exports.Peer = Peer;