File: source/peer-handshake.js

  1. /**
  2. * The list of Peer connection states.
  3. * @attribute HANDSHAKE_PROGRESS
  4. * @param {String} ENTER <small>Value <code>"enter"</code></small>
  5. * The value of the connection state when Peer has just entered the Room.
  6. * <small>At this stage, <a href="#event_peerJoined"><code>peerJoined</code> event</a>
  7. * is triggered.</small>
  8. * @param {String} WELCOME <small>Value <code>"welcome"</code></small>
  9. * The value of the connection state when Peer is aware that User has entered the Room.
  10. * <small>At this stage, <a href="#event_peerJoined"><code>peerJoined</code> event</a>
  11. * is triggered and Peer connection may commence.</small>
  12. * @param {String} OFFER <small>Value <code>"offer"</code></small>
  13. * The value of the connection state when Peer connection has set the local / remote <code>"offer"</code>
  14. * session description to start streaming connection.
  15. * @param {String} ANSWER <small>Value <code>"answer"</code></small>
  16. * The value of the connection state when Peer connection has set the local / remote <code>"answer"</code>
  17. * session description to establish streaming connection.
  18. * @param {String} ERROR <small>Value <code>"error"</code></small>
  19. * The value of the connection state when Peer connection has failed to establish streaming connection.
  20. * <small>This happens when there are errors that occurs in creating local <code>"offer"</code> /
  21. * <code>"answer"</code>, or when setting remote / local <code>"offer"</code> / <code>"answer"</code>.</small>
  22. * @type JSON
  23. * @readOnly
  24. * @for Skylink
  25. * @since 0.1.0
  26. */
  27. Skylink.prototype.HANDSHAKE_PROGRESS = {
  28. ENTER: 'enter',
  29. WELCOME: 'welcome',
  30. OFFER: 'offer',
  31. ANSWER: 'answer',
  32. ERROR: 'error'
  33. };
  34.  
  35. /**
  36. * Stores the list of Peer connection health timers.
  37. * This timers sets a timeout which checks and waits if Peer connection is successfully established,
  38. * or else it will attempt to re-negotiate with the Peer connection again.
  39. * @attribute _peerConnectionHealthTimers
  40. * @param {Object} <#peerId> The Peer connection health timer.
  41. * @type JSON
  42. * @private
  43. * @for Skylink
  44. * @since 0.5.5
  45. */
  46. Skylink.prototype._peerConnectionHealthTimers = {};
  47.  
  48. /**
  49. * Stores the list of Peer connection "healthy" flags, which indicates if Peer connection is
  50. * successfully established, and when the health timers expires, it will clear the timer
  51. * and not attempt to re-negotiate with the Peer connection again.
  52. * @attribute _peerConnectionHealth
  53. * @param {Boolean} <#peerId> The flag that indicates if Peer connection has been successfully established.
  54. * @type JSON
  55. * @private
  56. * @since 0.5.5
  57. */
  58. Skylink.prototype._peerConnectionHealth = {};
  59.  
  60. /**
  61. * Stores the User connection priority weight.
  62. * If Peer has a higher connection weight, it will do the offer from its Peer connection first.
  63. * @attribute _peerPriorityWeight
  64. * @type Number
  65. * @private
  66. * @for Skylink
  67. * @since 0.5.0
  68. */
  69. Skylink.prototype._peerPriorityWeight = 0;
  70.  
  71. /**
  72. * Function that creates the Peer connection offer session description.
  73. * @method _doOffer
  74. * @private
  75. * @for Skylink
  76. * @since 0.5.2
  77. */
  78. Skylink.prototype._doOffer = function(targetMid, peerBrowser) {
  79. var self = this;
  80. var pc = self._peerConnections[targetMid] || self._addPeer(targetMid, peerBrowser);
  81.  
  82. log.log([targetMid, null, null, 'Checking caller status'], peerBrowser);
  83.  
  84. // Added checks to ensure that connection object is defined first
  85. if (!pc) {
  86. log.warn([targetMid, 'RTCSessionDescription', 'offer', 'Dropping of creating of offer ' +
  87. 'as connection does not exists']);
  88. return;
  89. }
  90.  
  91. // Added checks to ensure that state is "stable" if setting local "offer"
  92. if (pc.signalingState !== self.PEER_CONNECTION_STATE.STABLE) {
  93. log.warn([targetMid, 'RTCSessionDescription', 'offer',
  94. 'Dropping of creating of offer as signalingState is not "' +
  95. self.PEER_CONNECTION_STATE.STABLE + '" ->'], pc.signalingState);
  96. return;
  97. }
  98.  
  99. var offerConstraints = {
  100. offerToReceiveAudio: true,
  101. offerToReceiveVideo: true
  102. };
  103.  
  104. // NOTE: Removing ICE restart functionality as of now since Firefox does not support it yet
  105. // Check if ICE connection failed or disconnected, and if so, do an ICE restart
  106. /*if ([self.ICE_CONNECTION_STATE.DISCONNECTED, self.ICE_CONNECTION_STATE.FAILED].indexOf(pc.iceConnectionState) > -1) {
  107. offerConstraints.iceRestart = true;
  108. }*/
  109.  
  110. // Prevent undefined OS errors
  111. peerBrowser.os = peerBrowser.os || '';
  112.  
  113. /*
  114. Ignoring these old codes as Firefox 39 and below is no longer supported
  115. if (window.webrtcDetectedType === 'moz' && peerBrowser.agent === 'MCU') {
  116. unifiedOfferConstraints.mandatory = unifiedOfferConstraints.mandatory || {};
  117. unifiedOfferConstraints.mandatory.MozDontOfferDataChannel = true;
  118. beOfferer = true;
  119. }
  120.  
  121. if (window.webrtcDetectedBrowser === 'firefox' && window.webrtcDetectedVersion >= 32) {
  122. unifiedOfferConstraints = {
  123. offerToReceiveAudio: true,
  124. offerToReceiveVideo: true
  125. };
  126. }
  127. */
  128.  
  129. // Fallback to use mandatory constraints for plugin based browsers
  130. if (['IE', 'safari'].indexOf(window.webrtcDetectedBrowser) > -1) {
  131. offerConstraints = {
  132. mandatory: {
  133. OfferToReceiveAudio: true,
  134. OfferToReceiveVideo: true
  135. }
  136. };
  137. }
  138.  
  139. if (self._enableDataChannel) {
  140. if (typeof self._dataChannels[targetMid] !== 'object') {
  141. log.error([targetMid, 'RTCDataChannel', null, 'Create offer error as unable to create datachannel ' +
  142. 'as datachannels array is undefined'], self._dataChannels[targetMid]);
  143. return;
  144. }
  145.  
  146. // Edge doesn't support datachannels yet
  147. if (!self._dataChannels[targetMid].main && window.webrtcDetectedBrowser !== 'edge') {
  148. self._dataChannels[targetMid].main =
  149. self._createDataChannel(targetMid, self.DATA_CHANNEL_TYPE.MESSAGING, null, targetMid);
  150. self._peerConnections[targetMid].hasMainChannel = true;
  151. }
  152. }
  153.  
  154. log.debug([targetMid, null, null, 'Creating offer with config:'], offerConstraints);
  155.  
  156. pc.createOffer(function(offer) {
  157. log.debug([targetMid, null, null, 'Created offer'], offer);
  158.  
  159. self._setLocalAndSendMessage(targetMid, offer);
  160.  
  161. }, function(error) {
  162. self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error);
  163.  
  164. log.error([targetMid, null, null, 'Failed creating an offer:'], error);
  165.  
  166. }, offerConstraints);
  167. };
  168.  
  169. /**
  170. * Function that creates the Peer connection answer session description.
  171. * This comes after receiving and setting the offer session description.
  172. * @method _doAnswer
  173. * @private
  174. * @for Skylink
  175. * @since 0.1.0
  176. */
  177. Skylink.prototype._doAnswer = function(targetMid) {
  178. var self = this;
  179. log.log([targetMid, null, null, 'Creating answer with config:'],
  180. self._room.connection.sdpConstraints);
  181. var pc = self._peerConnections[targetMid];
  182.  
  183. // Added checks to ensure that connection object is defined first
  184. if (!pc) {
  185. log.warn([targetMid, 'RTCSessionDescription', 'answer', 'Dropping of creating of answer ' +
  186. 'as connection does not exists']);
  187. return;
  188. }
  189.  
  190. // Added checks to ensure that state is "have-remote-offer" if setting local "answer"
  191. if (pc.signalingState !== self.PEER_CONNECTION_STATE.HAVE_REMOTE_OFFER) {
  192. log.warn([targetMid, 'RTCSessionDescription', 'answer',
  193. 'Dropping of creating of answer as signalingState is not "' +
  194. self.PEER_CONNECTION_STATE.HAVE_REMOTE_OFFER + '" ->'], pc.signalingState);
  195. return;
  196. }
  197.  
  198. // No ICE restart constraints for createAnswer as it fails in chrome 48
  199. // { iceRestart: true }
  200. pc.createAnswer(function(answer) {
  201. log.debug([targetMid, null, null, 'Created answer'], answer);
  202. self._setLocalAndSendMessage(targetMid, answer);
  203. }, function(error) {
  204. log.error([targetMid, null, null, 'Failed creating an answer:'], error);
  205. self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error);
  206. });//, self._room.connection.sdpConstraints);
  207. };
  208.  
  209. /**
  210. * Function that starts the Peer connection health timer.
  211. * To count as a "healthy" successful established Peer connection, the
  212. * ICE connection state has to be "connected" or "completed",
  213. * messaging Datachannel type state has to be "opened" (if Datachannel is enabled)
  214. * and Signaling state has to be "stable".
  215. * Should consider dropping of counting messaging Datachannel type being opened as
  216. * it should not involve the actual Peer connection for media (audio/video) streaming.
  217. * @method _startPeerConnectionHealthCheck
  218. * @private
  219. * @for Skylink
  220. * @since 0.5.5
  221. */
  222. Skylink.prototype._startPeerConnectionHealthCheck = function (peerId, toOffer) {
  223. var self = this;
  224. var timer = self._enableIceTrickle ? (toOffer ? 12500 : 10000) : 50000;
  225. timer = (self._hasMCU) ? 105000 : timer;
  226.  
  227. // increase timeout for android/ios
  228. /*var agent = self.getPeerInfo(peerId).agent;
  229. if (['Android', 'iOS'].indexOf(agent.name) > -1) {
  230. timer = 105000;
  231. }*/
  232.  
  233. timer += self._retryCount*10000;
  234.  
  235. log.log([peerId, 'PeerConnectionHealth', null,
  236. 'Initializing check for peer\'s connection health']);
  237.  
  238. if (self._peerConnectionHealthTimers[peerId]) {
  239. // might be a re-handshake again
  240. self._stopPeerConnectionHealthCheck(peerId);
  241. }
  242.  
  243. self._peerConnectionHealthTimers[peerId] = setTimeout(function () {
  244. // re-handshaking should start here.
  245. var connectionStable = false;
  246. var pc = self._peerConnections[peerId];
  247.  
  248. if (pc) {
  249. var dc = (self._dataChannels[peerId] || {}).main;
  250.  
  251. var dcConnected = pc.hasMainChannel ? dc && dc.readyState === self.DATA_CHANNEL_STATE.OPEN : true;
  252. var iceConnected = pc.iceConnectionState === self.ICE_CONNECTION_STATE.CONNECTED ||
  253. pc.iceConnectionState === self.ICE_CONNECTION_STATE.COMPLETED;
  254. var signalingConnected = pc.signalingState === self.PEER_CONNECTION_STATE.STABLE;
  255.  
  256. connectionStable = dcConnected && iceConnected && signalingConnected;
  257.  
  258. log.debug([peerId, 'PeerConnectionHealth', null, 'Connection status'], {
  259. dcConnected: dcConnected,
  260. iceConnected: iceConnected,
  261. signalingConnected: signalingConnected
  262. });
  263. }
  264.  
  265. log.debug([peerId, 'PeerConnectionHealth', null, 'Require reconnection?'], connectionStable);
  266.  
  267. if (!connectionStable) {
  268. log.warn([peerId, 'PeerConnectionHealth', null, 'Peer\'s health timer ' +
  269. 'has expired'], 10000);
  270.  
  271. // clear the loop first
  272. self._stopPeerConnectionHealthCheck(peerId);
  273.  
  274. log.debug([peerId, 'PeerConnectionHealth', null,
  275. 'Ice connection state time out. Re-negotiating connection']);
  276.  
  277. //Maximum increament is 5 minutes
  278. if (self._retryCount<30){
  279. //Increase after each consecutive connection failure
  280. self._retryCount++;
  281. }
  282.  
  283. // do a complete clean
  284. if (!self._hasMCU) {
  285. self._restartPeerConnection(peerId, true, true, null, false);
  286. } else {
  287. self._restartMCUConnection();
  288. }
  289. } else {
  290. self._peerConnectionHealth[peerId] = true;
  291. }
  292. }, timer);
  293. };
  294.  
  295. /**
  296. * Function that stops the Peer connection health timer.
  297. * This happens when Peer connection has been successfully established or when
  298. * Peer leaves the Room.
  299. * @method _stopPeerConnectionHealthCheck
  300. * @private
  301. * @for Skylink
  302. * @since 0.5.5
  303. */
  304. Skylink.prototype._stopPeerConnectionHealthCheck = function (peerId) {
  305. var self = this;
  306.  
  307. if (self._peerConnectionHealthTimers[peerId]) {
  308. log.debug([peerId, 'PeerConnectionHealth', null,
  309. 'Stopping peer connection health timer check']);
  310.  
  311. clearTimeout(self._peerConnectionHealthTimers[peerId]);
  312. delete self._peerConnectionHealthTimers[peerId];
  313.  
  314. } else {
  315. log.debug([peerId, 'PeerConnectionHealth', null,
  316. 'Peer connection health does not have a timer check']);
  317. }
  318. };
  319.  
  320. /**
  321. * Function that sets the local session description and sends to Peer.
  322. * If trickle ICE is disabled, the local session description will be sent after
  323. * ICE gathering has been completed.
  324. * @method _setLocalAndSendMessage
  325. * @private
  326. * @for Skylink
  327. * @since 0.5.2
  328. */
  329. Skylink.prototype._setLocalAndSendMessage = function(targetMid, sessionDescription) {
  330. var self = this;
  331. var pc = self._peerConnections[targetMid];
  332.  
  333. /*if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER && pc.setAnswer) {
  334. log.log([targetMid, 'RTCSessionDescription', sessionDescription.type,
  335. 'Ignoring session description. User has already set local answer'], sessionDescription);
  336. return;
  337. }
  338. if (sessionDescription.type === self.HANDSHAKE_PROGRESS.OFFER && pc.setOffer) {
  339. log.log([targetMid, 'RTCSessionDescription', sessionDescription.type,
  340. 'Ignoring session description. User has already set local offer'], sessionDescription);
  341. return;
  342. }*/
  343.  
  344. // Added checks to ensure that sessionDescription is defined first
  345. if (!(!!sessionDescription && !!sessionDescription.sdp)) {
  346. log.warn([targetMid, 'RTCSessionDescription', null, 'Dropping of setting local unknown sessionDescription ' +
  347. 'as received sessionDescription is empty ->'], sessionDescription);
  348. return;
  349. }
  350.  
  351. // Added checks to ensure that connection object is defined first
  352. if (!pc) {
  353. log.warn([targetMid, 'RTCSessionDescription', sessionDescription.type, 'Dropping of setting local "' +
  354. sessionDescription.type + '" as connection does not exists']);
  355. return;
  356. }
  357.  
  358. // Added checks to ensure that state is "stable" if setting local "offer"
  359. if (sessionDescription.type === self.HANDSHAKE_PROGRESS.OFFER &&
  360. pc.signalingState !== self.PEER_CONNECTION_STATE.STABLE) {
  361. log.warn([targetMid, 'RTCSessionDescription', sessionDescription.type,
  362. 'Dropping of setting local "offer" as signalingState is not "' +
  363. self.PEER_CONNECTION_STATE.STABLE + '" ->'], pc.signalingState);
  364. return;
  365.  
  366. // Added checks to ensure that state is "have-remote-offer" if setting local "answer"
  367. } else if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER &&
  368. pc.signalingState !== self.PEER_CONNECTION_STATE.HAVE_REMOTE_OFFER) {
  369. log.warn([targetMid, 'RTCSessionDescription', sessionDescription.type,
  370. 'Dropping of setting local "answer" as signalingState is not "' +
  371. self.PEER_CONNECTION_STATE.HAVE_REMOTE_OFFER + '" ->'], pc.signalingState);
  372. return;
  373. }
  374.  
  375.  
  376. // NOTE ALEX: handle the pc = 0 case, just to be sure
  377. var sdpLines = sessionDescription.sdp.split('\r\n');
  378.  
  379. // remove h264 invalid pref
  380. sdpLines = self._removeSDPFirefoxH264Pref(sdpLines);
  381.  
  382. // Check if stereo was enabled
  383. if (self._streams.userMedia && self._streams.userMedia.settings.audio) {
  384. if (self._streams.userMedia.settings.stereo) {
  385. log.info([targetMid, null, null, 'Enabling OPUS stereo flag']);
  386. self._addSDPStereo(sdpLines);
  387. }
  388. }
  389.  
  390. // Set SDP max bitrate
  391. if (self._streamsBandwidthSettings) {
  392. sdpLines = self._setSDPBitrate(sdpLines, self._streamsBandwidthSettings);
  393. }
  394.  
  395. // set sdp resolution
  396. /*if (self._streamSettings.hasOwnProperty('video')) {
  397. sdpLines = self._setSDPVideoResolution(sdpLines, self._streamSettings.video);
  398. }*/
  399.  
  400. /*log.info([targetMid, null, null, 'Custom bandwidth settings:'], {
  401. audio: (self._streamSettings.bandwidth.audio || 'Not set') + ' kB/s',
  402. video: (self._streamSettings.bandwidth.video || 'Not set') + ' kB/s',
  403. data: (self._streamSettings.bandwidth.data || 'Not set') + ' kB/s'
  404. });*/
  405.  
  406. /*if (self._streamSettings.video.hasOwnProperty('frameRate') &&
  407. self._streamSettings.video.hasOwnProperty('resolution')){
  408. log.info([targetMid, null, null, 'Custom resolution settings:'], {
  409. frameRate: (self._streamSettings.video.frameRate || 'Not set') + ' fps',
  410. width: (self._streamSettings.video.resolution.width || 'Not set') + ' px',
  411. height: (self._streamSettings.video.resolution.height || 'Not set') + ' px'
  412. });
  413. }*/
  414.  
  415. // set video codec
  416. if (self._selectedVideoCodec !== self.VIDEO_CODEC.AUTO) {
  417. sdpLines = self._setSDPVideoCodec(sdpLines);
  418. } else {
  419. log.log([targetMid, null, null, 'Not setting any video codec']);
  420. }
  421.  
  422. // set audio codec
  423. if (self._selectedAudioCodec !== self.AUDIO_CODEC.AUTO) {
  424. sdpLines = self._setSDPAudioCodec(sdpLines);
  425. } else {
  426. log.log([targetMid, null, null, 'Not setting any audio codec']);
  427. }
  428.  
  429. sessionDescription.sdp = sdpLines.join('\r\n');
  430.  
  431. var removeVP9AptRtxPayload = false;
  432. var agent = (self._peerInformations[targetMid] || {}).agent || {};
  433.  
  434. if (agent.pluginVersion) {
  435. // 0.8.870 supports
  436. var parts = agent.pluginVersion.split('.');
  437. removeVP9AptRtxPayload = parseInt(parts[0], 10) >= 0 && parseInt(parts[1], 10) >= 8 &&
  438. parseInt(parts[2], 10) >= 870;
  439. }
  440.  
  441. // Remove rtx or apt= lines that prevent connections for browsers without VP8 or VP9 support
  442. // See: https://bugs.chromium.org/p/webrtc/issues/detail?id=3962
  443. if (['chrome', 'opera'].indexOf(window.webrtcDetectedBrowser) > -1 && removeVP9AptRtxPayload) {
  444. log.warn([targetMid, null, null, 'Removing apt= and rtx payload lines causing connectivity issues']);
  445.  
  446. sessionDescription.sdp = sessionDescription.sdp.replace(/a=rtpmap:\d+ rtx\/\d+\r\na=fmtp:\d+ apt=101\r\n/g, '');
  447. sessionDescription.sdp = sessionDescription.sdp.replace(/a=rtpmap:\d+ rtx\/\d+\r\na=fmtp:\d+ apt=107\r\n/g, '');
  448. }
  449.  
  450. // NOTE ALEX: opus should not be used for mobile
  451. // Set Opus as the preferred codec in SDP if Opus is present.
  452. //sessionDescription.sdp = preferOpus(sessionDescription.sdp);
  453. // limit bandwidth
  454. //sessionDescription.sdp = this._limitBandwidth(sessionDescription.sdp);
  455. log.log([targetMid, 'RTCSessionDescription', sessionDescription.type,
  456. 'Updated session description:'], sessionDescription);
  457.  
  458. // Added checks if there is a current local sessionDescription being processing before processing this one
  459. if (pc.processingLocalSDP) {
  460. log.warn([targetMid, 'RTCSessionDescription', sessionDescription.type,
  461. 'Dropping of setting local ' + sessionDescription.type + ' as there is another ' +
  462. 'sessionDescription being processed ->'], sessionDescription);
  463. return;
  464. }
  465.  
  466. pc.processingLocalSDP = true;
  467.  
  468. pc.setLocalDescription(sessionDescription, function() {
  469. log.debug([targetMid, sessionDescription.type, 'Local description set']);
  470.  
  471. pc.processingLocalSDP = false;
  472.  
  473. self._trigger('handshakeProgress', sessionDescription.type, targetMid);
  474. if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER) {
  475. pc.setAnswer = 'local';
  476. } else {
  477. pc.setOffer = 'local';
  478. }
  479.  
  480. if (!self._enableIceTrickle && !pc.gathered) {
  481. log.log([targetMid, 'RTCSessionDescription', sessionDescription.type,
  482. 'Waiting for Ice gathering to complete to prevent Ice trickle']);
  483. return;
  484. }
  485.  
  486. // make checks for firefox session description
  487. if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER && window.webrtcDetectedBrowser === 'firefox') {
  488. sessionDescription.sdp = self._addSDPSsrcFirefoxAnswer(targetMid, sessionDescription.sdp);
  489. }
  490.  
  491. self._sendChannelMessage({
  492. type: sessionDescription.type,
  493. sdp: sessionDescription.sdp,
  494. mid: self._user.sid,
  495. target: targetMid,
  496. rid: self._room.id,
  497. userInfo: self._getUserInfo()
  498. });
  499.  
  500. }, function(error) {
  501. self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error);
  502.  
  503. pc.processingLocalSDP = false;
  504.  
  505. log.error([targetMid, 'RTCSessionDescription', sessionDescription.type,
  506. 'Failed setting local description: '], error);
  507. });
  508. };
  509.