peer.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. var util = require('util');
  2. var webrtcSupport = require('webrtcsupport');
  3. var PeerConnection = require('rtcpeerconnection');
  4. var WildEmitter = require('wildemitter');
  5. var FileTransfer = require('filetransfer');
  6. // the inband-v1 protocol is sending metadata inband in a serialized JSON object
  7. // followed by the actual data. Receiver closes the datachannel upon completion
  8. var INBAND_FILETRANSFER_V1 = 'https://simplewebrtc.com/protocol/filetransfer#inband-v1';
  9. function isAllTracksEnded(stream) {
  10. var isAllTracksEnded = true;
  11. stream.getTracks().forEach(function (t) {
  12. isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded;
  13. });
  14. return isAllTracksEnded;
  15. }
  16. function Peer(options) {
  17. var self = this;
  18. // call emitter constructor
  19. WildEmitter.call(this);
  20. this.id = options.id;
  21. this.parent = options.parent;
  22. this.type = options.type || 'video';
  23. this.oneway = options.oneway || false;
  24. this.sharemyscreen = options.sharemyscreen || false;
  25. this.browserPrefix = options.prefix;
  26. this.stream = options.stream;
  27. this.enableDataChannels = options.enableDataChannels === undefined ? this.parent.config.enableDataChannels : options.enableDataChannels;
  28. this.receiveMedia = options.receiveMedia || this.parent.config.receiveMedia;
  29. this.channels = {};
  30. this.sid = options.sid || Date.now().toString();
  31. // Create an RTCPeerConnection via the polyfill
  32. this.pc = new PeerConnection(this.parent.config.peerConnectionConfig, this.parent.config.peerConnectionConstraints);
  33. this.pc.on('ice', this.onIceCandidate.bind(this));
  34. this.pc.on('endOfCandidates', function (event) {
  35. self.send('endOfCandidates', event);
  36. });
  37. this.pc.on('offer', function (offer) {
  38. if (self.parent.config.nick) offer.nick = self.parent.config.nick;
  39. self.send('offer', offer);
  40. });
  41. this.pc.on('answer', function (answer) {
  42. if (self.parent.config.nick) answer.nick = self.parent.config.nick;
  43. self.send('answer', answer);
  44. });
  45. this.pc.on('addStream', this.handleRemoteStreamAdded.bind(this));
  46. this.pc.on('addChannel', this.handleDataChannelAdded.bind(this));
  47. this.pc.on('removeStream', this.handleStreamRemoved.bind(this));
  48. // Just fire negotiation needed events for now
  49. // When browser re-negotiation handling seems to work
  50. // we can use this as the trigger for starting the offer/answer process
  51. // automatically. We'll just leave it be for now while this stabalizes.
  52. this.pc.on('negotiationNeeded', this.emit.bind(this, 'negotiationNeeded'));
  53. this.pc.on('iceConnectionStateChange', this.emit.bind(this, 'iceConnectionStateChange'));
  54. this.pc.on('iceConnectionStateChange', function () {
  55. switch (self.pc.iceConnectionState) {
  56. case 'failed':
  57. // currently, in chrome only the initiator goes to failed
  58. // so we need to signal this to the peer
  59. if (self.pc.pc.peerconnection.localDescription.type === 'offer') {
  60. self.parent.emit('iceFailed', self);
  61. self.send('connectivityError');
  62. }
  63. break;
  64. }
  65. });
  66. this.pc.on('signalingStateChange', this.emit.bind(this, 'signalingStateChange'));
  67. this.logger = this.parent.logger;
  68. // handle screensharing/broadcast mode
  69. if (options.type === 'screen') {
  70. if (this.parent.localScreen && this.sharemyscreen) {
  71. this.logger.log('adding local screen stream to peer connection');
  72. this.pc.addStream(this.parent.localScreen);
  73. this.broadcaster = options.broadcaster;
  74. }
  75. } else {
  76. this.parent.localStreams.forEach(function (stream) {
  77. self.pc.addStream(stream);
  78. });
  79. }
  80. this.on('channelOpen', function (channel) {
  81. if (channel.protocol === INBAND_FILETRANSFER_V1) {
  82. channel.onmessage = function (event) {
  83. var metadata = JSON.parse(event.data);
  84. var receiver = new FileTransfer.Receiver();
  85. receiver.receive(metadata, channel);
  86. self.emit('fileTransfer', metadata, receiver);
  87. receiver.on('receivedFile', function (file, metadata) {
  88. receiver.channel.close();
  89. });
  90. };
  91. }
  92. });
  93. // proxy events to parent
  94. this.on('*', function () {
  95. self.parent.emit.apply(self.parent, arguments);
  96. });
  97. }
  98. util.inherits(Peer, WildEmitter);
  99. Peer.prototype.handleMessage = function (message) {
  100. var self = this;
  101. this.logger.log('getting', message.type, message);
  102. if (message.prefix) this.browserPrefix = message.prefix;
  103. if (message.type === 'offer') {
  104. if (!this.nick) this.nick = message.payload.nick;
  105. delete message.payload.nick;
  106. this.pc.handleOffer(message.payload, function (err) {
  107. if (err) {
  108. return;
  109. }
  110. // auto-accept
  111. self.pc.answer(function (err, sessionDescription) {
  112. //self.send('answer', sessionDescription);
  113. });
  114. });
  115. } else if (message.type === 'answer') {
  116. if (!this.nick) this.nick = message.payload.nick;
  117. delete message.payload.nick;
  118. this.pc.handleAnswer(message.payload);
  119. } else if (message.type === 'candidate') {
  120. this.pc.processIce(message.payload);
  121. } else if (message.type === 'connectivityError') {
  122. this.parent.emit('connectivityError', self);
  123. } else if (message.type === 'mute') {
  124. this.parent.emit('mute', {id: message.from, name: message.payload.name});
  125. } else if (message.type === 'unmute') {
  126. this.parent.emit('unmute', {id: message.from, name: message.payload.name});
  127. } else if (message.type === 'endOfCandidates') {
  128. // Edge requires an end-of-candidates. Since only Edge will have mLines or tracks on the
  129. // shim this will only be called in Edge.
  130. var mLines = this.pc.pc.peerconnection.transceivers || [];
  131. mLines.forEach(function (mLine) {
  132. if (mLine.iceTransport) {
  133. mLine.iceTransport.addRemoteCandidate({});
  134. }
  135. });
  136. }
  137. };
  138. // send via signalling channel
  139. Peer.prototype.send = function (messageType, payload) {
  140. var message = {
  141. to: this.id,
  142. sid: this.sid,
  143. broadcaster: this.broadcaster,
  144. roomType: this.type,
  145. type: messageType,
  146. payload: payload,
  147. prefix: webrtcSupport.prefix
  148. };
  149. this.logger.log('sending', messageType, message);
  150. this.parent.emit('message', message);
  151. };
  152. // send via data channel
  153. // returns true when message was sent and false if channel is not open
  154. Peer.prototype.sendDirectly = function (channel, messageType, payload) {
  155. var message = {
  156. type: messageType,
  157. payload: payload
  158. };
  159. this.logger.log('sending via datachannel', channel, messageType, message);
  160. var dc = this.getDataChannel(channel);
  161. if (dc.readyState != 'open') return false;
  162. dc.send(JSON.stringify(message));
  163. return true;
  164. };
  165. // Internal method registering handlers for a data channel and emitting events on the peer
  166. Peer.prototype._observeDataChannel = function (channel) {
  167. var self = this;
  168. channel.onclose = this.emit.bind(this, 'channelClose', channel);
  169. channel.onerror = this.emit.bind(this, 'channelError', channel);
  170. channel.onmessage = function (event) {
  171. self.emit('channelMessage', self, channel.label, JSON.parse(event.data), channel, event);
  172. };
  173. channel.onopen = this.emit.bind(this, 'channelOpen', channel);
  174. };
  175. // Fetch or create a data channel by the given name
  176. Peer.prototype.getDataChannel = function (name, opts) {
  177. if (!webrtcSupport.supportDataChannel) return this.emit('error', new Error('createDataChannel not supported'));
  178. var channel = this.channels[name];
  179. opts || (opts = {});
  180. if (channel) return channel;
  181. // if we don't have one by this label, create it
  182. channel = this.channels[name] = this.pc.createDataChannel(name, opts);
  183. this._observeDataChannel(channel);
  184. return channel;
  185. };
  186. Peer.prototype.onIceCandidate = function (candidate) {
  187. if (this.closed) return;
  188. if (candidate) {
  189. var pcConfig = this.parent.config.peerConnectionConfig;
  190. if (webrtcSupport.prefix === 'moz' && pcConfig && pcConfig.iceTransports &&
  191. candidate.candidate && candidate.candidate.candidate &&
  192. candidate.candidate.candidate.indexOf(pcConfig.iceTransports) < 0) {
  193. this.logger.log('Ignoring ice candidate not matching pcConfig iceTransports type: ', pcConfig.iceTransports);
  194. } else {
  195. this.send('candidate', candidate);
  196. }
  197. } else {
  198. this.logger.log("End of candidates.");
  199. }
  200. };
  201. Peer.prototype.start = function () {
  202. var self = this;
  203. // well, the webrtc api requires that we either
  204. // a) create a datachannel a priori
  205. // b) do a renegotiation later to add the SCTP m-line
  206. // Let's do (a) first...
  207. if (this.enableDataChannels) {
  208. this.getDataChannel('simplewebrtc');
  209. }
  210. this.pc.offer(this.receiveMedia, function (err, sessionDescription) {
  211. //self.send('offer', sessionDescription);
  212. });
  213. };
  214. Peer.prototype.icerestart = function () {
  215. var constraints = this.receiveMedia;
  216. constraints.mandatory.IceRestart = true;
  217. this.pc.offer(constraints, function (err, success) { });
  218. };
  219. Peer.prototype.end = function () {
  220. if (this.closed) return;
  221. this.pc.close();
  222. this.handleStreamRemoved();
  223. };
  224. Peer.prototype.handleRemoteStreamAdded = function (event) {
  225. var self = this;
  226. if (this.stream) {
  227. this.logger.warn('Already have a remote stream');
  228. } else {
  229. this.stream = event.stream;
  230. this.stream.getTracks().forEach(function (track) {
  231. track.addEventListener('ended', function () {
  232. if (isAllTracksEnded(self.stream)) {
  233. self.end();
  234. }
  235. });
  236. });
  237. this.parent.emit('peerStreamAdded', this);
  238. }
  239. };
  240. Peer.prototype.handleStreamRemoved = function () {
  241. var peerIndex = this.parent.peers.indexOf(this);
  242. if (peerIndex > -1) {
  243. this.parent.peers.splice(peerIndex, 1);
  244. this.closed = true;
  245. this.parent.emit('peerStreamRemoved', this);
  246. }
  247. };
  248. Peer.prototype.handleDataChannelAdded = function (channel) {
  249. this.channels[channel.label] = channel;
  250. this._observeDataChannel(channel);
  251. };
  252. Peer.prototype.sendFile = function (file) {
  253. var sender = new FileTransfer.Sender();
  254. var dc = this.getDataChannel('filetransfer' + (new Date()).getTime(), {
  255. protocol: INBAND_FILETRANSFER_V1
  256. });
  257. // override onopen
  258. dc.onopen = function () {
  259. dc.send(JSON.stringify({
  260. size: file.size,
  261. name: file.name
  262. }));
  263. sender.send(file, dc);
  264. };
  265. // override onclose
  266. dc.onclose = function () {
  267. console.log('sender received transfer');
  268. sender.emit('complete');
  269. };
  270. return sender;
  271. };
  272. module.exports = Peer;