File: source/stream-sdp.js

  1. /**
  2. * Function that modifies the session description to configure settings for OPUS audio codec.
  3. * @method _setSDPCodecParams
  4. * @private
  5. * @for Skylink
  6. * @since 0.6.16
  7. */
  8. Skylink.prototype._setSDPCodecParams = function(targetMid, sessionDescription) {
  9. var self = this;
  10.  
  11. var parseFn = function (type, codecName, samplingRate, settings) {
  12. var mLine = sessionDescription.sdp.match(new RegExp('m=' + type + '\ .*\r\n', 'gi'));
  13. // Find the m= line
  14. if (Array.isArray(mLine) && mLine.length > 0) {
  15. var codecsList = sessionDescription.sdp.match(new RegExp('a=rtpmap:.*\ ' + codecName + '\/' +
  16. (samplingRate ? samplingRate + (type === 'audio' ? '[\/]*.*' : '.*') : '.*') + '\r\n', 'gi'));
  17. // Get the list of codecs related to it
  18. if (Array.isArray(codecsList) && codecsList.length > 0) {
  19. for (var i = 0; i < codecsList.length; i++) {
  20. var payload = (codecsList[i].split('a=rtpmap:')[1] || '').split(' ')[0];
  21. if (!payload) {
  22. continue;
  23. }
  24. var fmtpLine = sessionDescription.sdp.match(new RegExp('a=fmtp:' + payload + '\ .*\r\n', 'gi'));
  25. var updatedFmtpLine = 'a=fmtp:' + payload + ' ';
  26. var addedKeys = [];
  27. // Check if a=fmtp: line exists
  28. if (Array.isArray(fmtpLine) && fmtpLine.length > 0) {
  29. var fmtpParts = (fmtpLine[0].split('a=fmtp:' + payload + ' ')[1] || '').replace(
  30. / /g, '').replace(/\r\n/g, '').split(';');
  31. for (var j = 0; j < fmtpParts.length; j++) {
  32. if (!fmtpParts[j]) {
  33. continue;
  34. }
  35. var keyAndValue = fmtpParts[j].split('=');
  36. if (settings.hasOwnProperty(keyAndValue[0])) {
  37. // Dont append parameter key+value if boolean and false
  38. updatedFmtpLine += typeof settings[keyAndValue[0]] === 'boolean' ? (settings[keyAndValue[0]] ?
  39. keyAndValue[0] + '=1;' : '') : keyAndValue[0] + '=' + settings[keyAndValue[0]] + ';';
  40. } else {
  41. updatedFmtpLine += fmtpParts[j] + ';';
  42. }
  43. addedKeys.push(keyAndValue[0]);
  44. }
  45. sessionDescription.sdp = sessionDescription.sdp.replace(fmtpLine[0], '');
  46. }
  47. for (var key in settings) {
  48. if (settings.hasOwnProperty(key) && addedKeys.indexOf(key) === -1) {
  49. // Dont append parameter key+value if boolean and false
  50. updatedFmtpLine += typeof settings[key] === 'boolean' ? (settings[key] ? key + '=1;' : '') :
  51. key + '=' + settings[key] + ';';
  52. addedKeys.push(key);
  53. }
  54. }
  55. if (updatedFmtpLine !== 'a=fmtp:' + payload + ' ') {
  56. sessionDescription.sdp = sessionDescription.sdp.replace(codecsList[i], codecsList[i] + updatedFmtpLine + '\r\n');
  57. }
  58. }
  59. }
  60. }
  61. };
  62.  
  63. // Set audio codecs -> OPUS
  64. // RFC: https://tools.ietf.org/html/draft-ietf-payload-rtp-opus-11
  65. parseFn('audio', self.AUDIO_CODEC.OPUS, 48000, (function () {
  66. var opusOptions = {};
  67. var audioSettings = self._streams.screenshare ? self._streams.screenshare.settings.audio :
  68. (self._streams.userMedia ? self._streams.userMedia.settings.audio : {});
  69. audioSettings = audioSettings && typeof audioSettings === 'object' ? audioSettings : {};
  70. if (typeof self._initOptions.codecParams.audio.opus.stereo === 'boolean') {
  71. opusOptions.stereo = self._initOptions.codecParams.audio.opus.stereo;
  72. } else if (typeof audioSettings.stereo === 'boolean') {
  73. opusOptions.stereo = audioSettings.stereo;
  74. }
  75. if (typeof self._initOptions.codecParams.audio.opus['sprop-stereo'] === 'boolean') {
  76. opusOptions['sprop-stereo'] = self._initOptions.codecParams.audio.opus['sprop-stereo'];
  77. } else if (typeof audioSettings.stereo === 'boolean') {
  78. opusOptions['sprop-stereo'] = audioSettings.stereo;
  79. }
  80. if (typeof self._initOptions.codecParams.audio.opus.usedtx === 'boolean') {
  81. opusOptions.usedtx = self._initOptions.codecParams.audio.opus.usedtx;
  82. } else if (typeof audioSettings.usedtx === 'boolean') {
  83. opusOptions.usedtx = audioSettings.usedtx;
  84. }
  85. if (typeof self._initOptions.codecParams.audio.opus.useinbandfec === 'boolean') {
  86. opusOptions.useinbandfec = self._initOptions.codecParams.audio.opus.useinbandfec;
  87. } else if (typeof audioSettings.useinbandfec === 'boolean') {
  88. opusOptions.useinbandfec = audioSettings.useinbandfec;
  89. }
  90. if (typeof self._initOptions.codecParams.audio.opus.maxplaybackrate === 'number') {
  91. opusOptions.maxplaybackrate = self._initOptions.codecParams.audio.opus.maxplaybackrate;
  92. } else if (typeof audioSettings.maxplaybackrate === 'number') {
  93. opusOptions.maxplaybackrate = audioSettings.maxplaybackrate;
  94. }
  95. if (typeof self._initOptions.codecParams.audio.opus.minptime === 'number') {
  96. opusOptions.minptime = self._initOptions.codecParams.audio.opus.minptime;
  97. } else if (typeof audioSettings.minptime === 'number') {
  98. opusOptions.minptime = audioSettings.minptime;
  99. }
  100. // Possible future params: sprop-maxcapturerate, maxaveragebitrate, sprop-stereo, cbr
  101. // NOT recommended: maxptime, ptime, rate, minptime
  102. return opusOptions;
  103. })());
  104.  
  105. // RFC: https://tools.ietf.org/html/rfc4733
  106. // Future: Set telephone-event: 100 0-15,66,70
  107.  
  108. // RFC: https://tools.ietf.org/html/draft-ietf-payload-vp8-17
  109. // Set video codecs -> VP8
  110. parseFn('video', self.VIDEO_CODEC.VP8, null, (function () {
  111. var vp8Options = {};
  112. // NOT recommended: max-fr, max-fs (all are codec decoder capabilities)
  113. if (typeof self._initOptions.codecParams.video.vp8.maxFr === 'number') {
  114. vp8Options['max-fr'] = self._initOptions.codecParams.video.vp8.maxFr;
  115. }
  116. if (typeof self._initOptions.codecParams.video.vp8.maxFs === 'number') {
  117. vp8Options['max-fs'] = self._initOptions.codecParams.video.vp8.maxFs;
  118. }
  119. return vp8Options;
  120. })());
  121.  
  122. // RFC: https://tools.ietf.org/html/draft-ietf-payload-vp9-02
  123. // Set video codecs -> VP9
  124. parseFn('video', self.VIDEO_CODEC.VP9, null, (function () {
  125. var vp9Options = {};
  126. // NOT recommended: max-fr, max-fs (all are codec decoder capabilities)
  127. if (typeof self._initOptions.codecParams.video.vp9.maxFr === 'number') {
  128. vp9Options['max-fr'] = self._initOptions.codecParams.video.vp9.maxFr;
  129. }
  130. if (typeof self._initOptions.codecParams.video.vp9.maxFs === 'number') {
  131. vp9Options['max-fs'] = self._initOptions.codecParams.video.vp9.maxFs;
  132. }
  133. return vp9Options;
  134. })());
  135.  
  136. // RFC: https://tools.ietf.org/html/rfc6184
  137. // Set the video codecs -> H264
  138. parseFn('video', self.VIDEO_CODEC.H264, null, (function () {
  139. var h264Options = {};
  140. if (typeof self._initOptions.codecParams.video.h264.levelAsymmetryAllowed === 'string') {
  141. h264Options['profile-level-id'] = self._initOptions.codecParams.video.h264.profileLevelId;
  142. }
  143. if (typeof self._initOptions.codecParams.video.h264.levelAsymmetryAllowed === 'boolean') {
  144. h264Options['level-asymmetry-allowed'] = self._initOptions.codecParams.video.h264.levelAsymmetryAllowed;
  145. }
  146. if (typeof self._initOptions.codecParams.video.h264.packetizationMode === 'boolean') {
  147. h264Options['packetization-mode'] = self._initOptions.codecParams.video.h264.packetizationMode;
  148. }
  149. // Possible future params (remove if they are decoder/encoder capabilities or info):
  150. // max-recv-level, max-mbps, max-smbps, max-fs, max-cpb, max-dpb, max-br,
  151. // max-mbps, max-smbps, max-fs, max-cpb, max-dpb, max-br, redundant-pic-cap, sprop-parameter-sets,
  152. // sprop-level-parameter-sets, use-level-src-parameter-sets, in-band-parameter-sets,
  153. // sprop-interleaving-depth, sprop-deint-buf-req, deint-buf-cap, sprop-init-buf-time,
  154. // sprop-max-don-diff, max-rcmd-nalu-size, sar-understood, sar-supported
  155. // NOT recommended: profile-level-id (WebRTC uses "42e00a" for the moment)
  156. // https://bugs.chromium.org/p/chromium/issues/detail?id=645599
  157. return h264Options;
  158. })());
  159.  
  160. return sessionDescription.sdp;
  161. };
  162.  
  163. /**
  164. * Function that modifies the session description to limit the maximum sending bandwidth.
  165. * Setting this may not necessarily work in Firefox.
  166. * @method _setSDPBitrate
  167. * @private
  168. * @for Skylink
  169. * @since 0.5.10
  170. */
  171. Skylink.prototype._setSDPBitrate = function(targetMid, sessionDescription) {
  172. var sdpLines = sessionDescription.sdp.split('\r\n');
  173. var parseFn = function (type, bw) {
  174. var mLineType = type;
  175. var mLineIndex = -1;
  176. var cLineIndex = -1;
  177.  
  178. if (type === 'data') {
  179. mLineType = 'application';
  180. }
  181.  
  182. for (var i = 0; i < sdpLines.length; i++) {
  183. if (sdpLines[i].indexOf('m=' + mLineType) === 0) {
  184. mLineIndex = i;
  185. } else if (mLineIndex > 0) {
  186. if (sdpLines[i].indexOf('m=') === 0) {
  187. break;
  188. }
  189.  
  190. if (sdpLines[i].indexOf('c=') === 0) {
  191. cLineIndex = i;
  192. // Remove previous b:AS settings
  193. } else if (sdpLines[i].indexOf('b=AS:') === 0 || sdpLines[i].indexOf('b:TIAS:') === 0) {
  194. sdpLines.splice(i, 1);
  195. i--;
  196. }
  197. }
  198. }
  199.  
  200. if (!(typeof bw === 'number' && bw > 0)) {
  201. log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Not limiting "' + type + '" bandwidth']);
  202. return;
  203. }
  204.  
  205. if (cLineIndex === -1) {
  206. log.error([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Failed setting "' +
  207. type + '" bandwidth as c-line is missing.']);
  208. return;
  209. }
  210.  
  211. // Follow RFC 4566, that the b-line should follow after c-line.
  212. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Limiting maximum sending "' + type + '" bandwidth ->'], bw);
  213. sdpLines.splice(cLineIndex + 1, 0, window.webrtcDetectedBrowser === 'firefox' ? 'b=TIAS:' + (bw * 1000 *
  214. (window.webrtcDetectedVersion > 52 && window.webrtcDetectedVersion < 55 ? 1000 : 1)).toFixed(0) : 'b=AS:' + bw);
  215. };
  216.  
  217. var bASAudioBw = this._streamsBandwidthSettings.bAS.audio;
  218. var bASVideoBw = this._streamsBandwidthSettings.bAS.video;
  219. var bASDataBw = this._streamsBandwidthSettings.bAS.data;
  220. var googleXMinBw = this._streamsBandwidthSettings.googleX.min;
  221. var googleXMaxBw = this._streamsBandwidthSettings.googleX.max;
  222.  
  223. if (this._peerCustomConfigs[targetMid]) {
  224. if (this._peerCustomConfigs[targetMid].bandwidth &&
  225. typeof this._peerCustomConfigs[targetMid].bandwidth === 'object') {
  226. if (typeof this._peerCustomConfigs[targetMid].bandwidth.audio === 'number') {
  227. bASAudioBw = this._peerCustomConfigs[targetMid].bandwidth.audio;
  228. }
  229. if (typeof this._peerCustomConfigs[targetMid].bandwidth.video === 'number') {
  230. bASVideoBw = this._peerCustomConfigs[targetMid].bandwidth.video;
  231. }
  232. if (typeof this._peerCustomConfigs[targetMid].bandwidth.data === 'number') {
  233. bASDataBw = this._peerCustomConfigs[targetMid].bandwidth.data;
  234. }
  235. }
  236. if (this._peerCustomConfigs[targetMid].googleXBandwidth &&
  237. typeof this._peerCustomConfigs[targetMid].googleXBandwidth === 'object') {
  238. if (typeof this._peerCustomConfigs[targetMid].googleXBandwidth.min === 'number') {
  239. googleXMinBw = this._peerCustomConfigs[targetMid].googleXBandwidth.min;
  240. }
  241. if (typeof this._peerCustomConfigs[targetMid].googleXBandwidth.max === 'number') {
  242. googleXMaxBw = this._peerCustomConfigs[targetMid].googleXBandwidth.max;
  243. }
  244. }
  245. }
  246.  
  247. parseFn('audio', bASAudioBw);
  248. parseFn('video', bASVideoBw);
  249. parseFn('data', bASDataBw);
  250.  
  251. // Sets the experimental google bandwidth
  252. if ((typeof googleXMinBw === 'number') || (typeof googleXMaxBw === 'number')) {
  253. var codec = null;
  254. var codecRtpMapLineIndex = -1;
  255. var codecFmtpLineIndex = -1;
  256.  
  257. for (var j = 0; j < sdpLines.length; j++) {
  258. if (sdpLines[j].indexOf('m=video') === 0) {
  259. codec = sdpLines[j].split(' ')[3];
  260. } else if (codec) {
  261. if (sdpLines[j].indexOf('m=') === 0) {
  262. break;
  263. }
  264.  
  265. if (sdpLines[j].indexOf('a=rtpmap:' + codec + ' ') === 0) {
  266. codecRtpMapLineIndex = j;
  267. } else if (sdpLines[j].indexOf('a=fmtp:' + codec + ' ') === 0) {
  268. sdpLines[j] = sdpLines[j].replace(/x-google-(min|max)-bitrate=[0-9]*[;]*/gi, '');
  269. codecFmtpLineIndex = j;
  270. break;
  271. }
  272. }
  273. }
  274.  
  275. if (codecRtpMapLineIndex > -1) {
  276. var xGoogleParams = '';
  277.  
  278. if (typeof googleXMinBw === 'number') {
  279. xGoogleParams += 'x-google-min-bitrate=' + googleXMinBw + ';';
  280. }
  281.  
  282. if (typeof googleXMaxBw === 'number') {
  283. xGoogleParams += 'x-google-max-bitrate=' + googleXMaxBw + ';';
  284. }
  285.  
  286. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Limiting x-google-bitrate ->'], xGoogleParams);
  287.  
  288. if (codecFmtpLineIndex > -1) {
  289. sdpLines[codecFmtpLineIndex] += (sdpLines[codecFmtpLineIndex].split(' ')[1] ? ';' : '') + xGoogleParams;
  290. } else {
  291. sdpLines.splice(codecRtpMapLineIndex + 1, 0, 'a=fmtp:' + codec + ' ' + xGoogleParams);
  292. }
  293. }
  294. }
  295.  
  296. return sdpLines.join('\r\n');
  297. };
  298.  
  299. /**
  300. * Function that modifies the session description to set the preferred audio/video codec.
  301. * @method _setSDPCodec
  302. * @private
  303. * @for Skylink
  304. * @since 0.6.16
  305. */
  306. Skylink.prototype._setSDPCodec = function(targetMid, sessionDescription, overrideSettings) {
  307. var self = this;
  308. var parseFn = function (type, codecSettings) {
  309. var codec = typeof codecSettings === 'object' ? codecSettings.codec : codecSettings;
  310. var samplingRate = typeof codecSettings === 'object' ? codecSettings.samplingRate : null;
  311. var channels = typeof codecSettings === 'object' ? codecSettings.channels : null;
  312.  
  313. if (codec === self[type === 'audio' ? 'AUDIO_CODEC' : 'VIDEO_CODEC'].AUTO) {
  314. log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type,
  315. 'Not preferring any codec for "' + type + '" streaming. Using browser selection.']);
  316. return;
  317. }
  318.  
  319. var mLine = sessionDescription.sdp.match(new RegExp('m=' + type + ' .*\r\n', 'gi'));
  320.  
  321. if (!(Array.isArray(mLine) && mLine.length > 0)) {
  322. log.error([targetMid, 'RTCSessionDesription', sessionDescription.type,
  323. 'Not preferring any codec for "' + type + '" streaming as m= line is not found.']);
  324. return;
  325. }
  326.  
  327. var setLineFn = function (codecsList, isSROk, isChnlsOk) {
  328. if (Array.isArray(codecsList) && codecsList.length > 0) {
  329. if (!isSROk) {
  330. samplingRate = null;
  331. }
  332. if (!isChnlsOk) {
  333. channels = null;
  334. }
  335. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Preferring "' +
  336. codec + '" (samplingRate: ' + (samplingRate || 'n/a') + ', channels: ' +
  337. (channels || 'n/a') + ') for "' + type + '" streaming.']);
  338.  
  339. var line = mLine[0];
  340. var lineParts = line.replace('\r\n', '').split(' ');
  341. // Set the m=x x UDP/xxx
  342. line = lineParts[0] + ' ' + lineParts[1] + ' ' + lineParts[2] + ' ';
  343. // Remove them to leave the codecs only
  344. lineParts.splice(0, 3);
  345. // Loop for the codecs list to append first
  346. for (var i = 0; i < codecsList.length; i++) {
  347. var parts = (codecsList[i].split('a=rtpmap:')[1] || '').split(' ');
  348. if (parts.length < 2) {
  349. continue;
  350. }
  351. line += parts[0] + ' ';
  352. }
  353. // Loop for later fallback codecs to append
  354. for (var j = 0; j < lineParts.length; j++) {
  355. if (line.indexOf(' ' + lineParts[j]) > 0) {
  356. lineParts.splice(j, 1);
  357. j--;
  358. } else if (sessionDescription.sdp.match(new RegExp('a=rtpmap:' + lineParts[j] +
  359. '\ ' + codec + '/.*\r\n', 'gi'))) {
  360. line += lineParts[j] + ' ';
  361. lineParts.splice(j, 1);
  362. j--;
  363. }
  364. }
  365. // Append the rest of the codecs
  366. line += lineParts.join(' ') + '\r\n';
  367. sessionDescription.sdp = sessionDescription.sdp.replace(mLine[0], line);
  368. return true;
  369. }
  370. };
  371.  
  372. // If samplingRate & channels
  373. if (samplingRate) {
  374. if (type === 'audio' && channels && setLineFn(sessionDescription.sdp.match(new RegExp('a=rtpmap:.*\ ' +
  375. codec + '\/' + samplingRate + (channels === 1 ? '[\/1]*' : '\/' + channels) + '\r\n', 'gi')), true, true)) {
  376. return;
  377. } else if (setLineFn(sessionDescription.sdp.match(new RegExp('a=rtpmap:.*\ ' + codec + '\/' +
  378. samplingRate + '[\/]*.*\r\n', 'gi')), true)) {
  379. return;
  380. }
  381. }
  382. if (type === 'audio' && channels && setLineFn(sessionDescription.sdp.match(new RegExp('a=rtpmap:.*\ ' +
  383. codec + '\/.*\/' + channels + '\r\n', 'gi')), false, true)) {
  384. return;
  385. }
  386.  
  387. setLineFn(sessionDescription.sdp.match(new RegExp('a=rtpmap:.*\ ' + codec + '\/.*\r\n', 'gi')));
  388. };
  389.  
  390. parseFn('audio', overrideSettings ? overrideSettings.audio : self._initOptions.audioCodec);
  391. parseFn('video', overrideSettings ? overrideSettings.video : self._initOptions.videoCodec);
  392.  
  393. return sessionDescription.sdp;
  394. };
  395.  
  396. /**
  397. * Function that modifies the session description to remove the previous experimental H264
  398. * codec that is apparently breaking connections.
  399. * NOTE: We should perhaps not remove it since H264 is supported?
  400. * @method _removeSDPFirefoxH264Pref
  401. * @private
  402. * @for Skylink
  403. * @since 0.5.2
  404. */
  405. Skylink.prototype._removeSDPFirefoxH264Pref = function(targetMid, sessionDescription) {
  406. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
  407. 'Removing Firefox experimental H264 flag to ensure interopability reliability']);
  408.  
  409. return sessionDescription.sdp.replace(/a=fmtp:0 profile-level-id=0x42e00c;packetization-mode=1\r\n/g, '');
  410. };
  411.  
  412. /**
  413. * Function that modifies the session description to remove apt/rtx lines that does exists.
  414. * @method _removeSDPUnknownAptRtx
  415. * @private
  416. * @for Skylink
  417. * @since 0.6.18
  418. */
  419. Skylink.prototype._removeSDPUnknownAptRtx = function (targetMid, sessionDescription) {
  420. var codecsPayload = []; // m=audio 9 UDP/TLS/RTP/SAVPF [Start from index 3] 102 9 0 8 97 13 118 101
  421. var sdpLines = sessionDescription.sdp.split('\r\n');
  422. var mediaLines = sessionDescription.sdp.split('m=');
  423.  
  424. // Remove unmapped rtx lines
  425. var formatRtx = function (str) {
  426. (str.match(/a=rtpmap:.*\ rtx\/.*\r\n/gi) || []).forEach(function (line) {
  427. var payload = (line.split('a=rtpmap:')[1] || '').split(' ')[0] || '';
  428. var fmtpLine = (str.match(new RegExp('a=fmtp:' + payload + '\ .*\r\n', 'gi')) || [])[0];
  429.  
  430. if (!fmtpLine) {
  431. str = str.replace(new RegExp(line, 'g'), '');
  432. return;
  433. }
  434.  
  435. var codecPayload = (fmtpLine.split(' apt=')[1] || '').replace(/\r\n/gi, '');
  436. var rtmpLine = str.match(new RegExp('a=rtpmap:' + codecPayload + '\ .*\r\n', 'gi'));
  437.  
  438. if (!rtmpLine) {
  439. str = str.replace(new RegExp(line, 'g'), '');
  440. str = str.replace(new RegExp(fmtpLine, 'g'), '');
  441. }
  442. });
  443.  
  444. return str;
  445. };
  446.  
  447. // Remove unmapped fmtp and rtcp-fb lines
  448. var formatFmtpRtcpFb = function (str) {
  449. (str.match(/a=(fmtp|rtcp-fb):.*\ rtx\/.*\r\n/gi) || []).forEach(function (line) {
  450. var payload = (line.split('a=' + (line.indexOf('rtcp') > 0 ? 'rtcp-fb' : 'fmtp'))[1] || '').split(' ')[0] || '';
  451. var rtmpLine = str.match(new RegExp('a=rtpmap:' + payload + '\ .*\r\n', 'gi'));
  452.  
  453. if (!rtmpLine) {
  454. str = str.replace(new RegExp(line, 'g'), '');
  455. }
  456. });
  457.  
  458. return str;
  459. };
  460.  
  461. // Remove rtx or apt= lines that prevent connections for browsers without VP8 or VP9 support
  462. // See: https://bugs.chromium.org/p/webrtc/issues/detail?id=3962
  463. for (var m = 0; m < mediaLines.length; m++) {
  464. mediaLines[m] = formatRtx(mediaLines[m]);
  465. mediaLines[m] = formatFmtpRtcpFb(mediaLines[m]);
  466. }
  467.  
  468. return mediaLines.join('m=');
  469. };
  470.  
  471. /**
  472. * Function that modifies the session description to remove codecs.
  473. * @method _removeSDPCodecs
  474. * @private
  475. * @for Skylink
  476. * @since 0.6.16
  477. */
  478. Skylink.prototype._removeSDPCodecs = function (targetMid, sessionDescription) {
  479. var audioSettings = this.getPeerInfo().settings.audio;
  480.  
  481. var parseFn = function (type, codec) {
  482. var payloadList = sessionDescription.sdp.match(new RegExp('a=rtpmap:(\\d*)\\ ' + codec + '.*', 'gi'));
  483.  
  484. if (!(Array.isArray(payloadList) && payloadList.length > 0)) {
  485. log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type,
  486. 'Not removing "' + codec + '" as it does not exists.']);
  487. return;
  488. }
  489.  
  490. for (var i = 0; i < payloadList.length; i++) {
  491. var payload = payloadList[i].split(' ')[0].split(':')[1];
  492.  
  493. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
  494. 'Removing "' + codec + '" payload ->'], payload);
  495.  
  496. sessionDescription.sdp = sessionDescription.sdp.replace(
  497. new RegExp('a=rtpmap:' + payload + '\\ .*\\r\\n', 'g'), '');
  498. sessionDescription.sdp = sessionDescription.sdp.replace(
  499. new RegExp('a=fmtp:' + payload + '\\ .*\\r\\n', 'g'), '');
  500. sessionDescription.sdp = sessionDescription.sdp.replace(
  501. new RegExp('a=rtpmap:\\d+ rtx\\/\\d+\\r\\na=fmtp:\\d+ apt=' + payload + '\\r\\n', 'g'), '');
  502.  
  503. // Remove the m-line codec
  504. var sdpLines = sessionDescription.sdp.split('\r\n');
  505.  
  506. for (var j = 0; j < sdpLines.length; j++) {
  507. if (sdpLines[j].indexOf('m=' + type) === 0) {
  508. var parts = sdpLines[j].split(' ');
  509.  
  510. if (parts.indexOf(payload) >= 3) {
  511. parts.splice(parts.indexOf(payload), 1);
  512. }
  513.  
  514. sdpLines[j] = parts.join(' ');
  515. break;
  516. }
  517. }
  518.  
  519. sessionDescription.sdp = sdpLines.join('\r\n');
  520. }
  521. };
  522.  
  523. if (this._initOptions.disableVideoFecCodecs) {
  524. if (this._hasMCU) {
  525. log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type,
  526. 'Not removing "ulpfec" or "red" codecs as connected to MCU to prevent connectivity issues.']);
  527. } else {
  528. parseFn('video', 'red');
  529. parseFn('video', 'ulpfec');
  530. }
  531. }
  532.  
  533. if (this._initOptions.disableComfortNoiseCodec && audioSettings && typeof audioSettings === 'object' && audioSettings.stereo) {
  534. parseFn('audio', 'CN');
  535. }
  536.  
  537. if (window.webrtcDetectedBrowser === 'edge' &&
  538. (((this._peerInformations[targetMid] || {}).agent || {}).name || 'unknown').name !== 'edge') {
  539. sessionDescription.sdp = sessionDescription.sdp.replace(/a=rtcp-fb:.*\ x-message\ .*\r\n/gi, '');
  540. }
  541.  
  542. return sessionDescription.sdp;
  543. };
  544.  
  545. /**
  546. * Function that modifies the session description to remove REMB packets fb.
  547. * @method _removeSDPREMBPackets
  548. * @private
  549. * @for Skylink
  550. * @since 0.6.16
  551. */
  552. Skylink.prototype._removeSDPREMBPackets = function (targetMid, sessionDescription) {
  553. if (!this._initOptions.disableREMB) {
  554. return sessionDescription.sdp;
  555. }
  556.  
  557. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Removing REMB packets.']);
  558. return sessionDescription.sdp.replace(/a=rtcp-fb:\d+ goog-remb\r\n/g, '');
  559. };
  560.  
  561. /**
  562. * Function that retrieves the session description selected codec.
  563. * @method _getSDPSelectedCodec
  564. * @private
  565. * @for Skylink
  566. * @since 0.6.16
  567. */
  568. Skylink.prototype._getSDPSelectedCodec = function (targetMid, sessionDescription, type, beSilentOnLogs) {
  569. var codecInfo = {
  570. name: null,
  571. implementation: null,
  572. clockRate: null,
  573. channels: null,
  574. payloadType: null,
  575. params: null
  576. };
  577.  
  578. if (!(sessionDescription && sessionDescription.sdp)) {
  579. return codecInfo;
  580. }
  581.  
  582. sessionDescription.sdp.split('m=').forEach(function (mediaItem, index) {
  583. if (index === 0 || mediaItem.indexOf(type + ' ') !== 0) {
  584. return;
  585. }
  586.  
  587. var codecs = (mediaItem.split('\r\n')[0] || '').split(' ');
  588. // Remove audio[0] 65266[1] UDP/TLS/RTP/SAVPF[2]
  589. codecs.splice(0, 3);
  590.  
  591. for (var i = 0; i < codecs.length; i++) {
  592. var match = mediaItem.match(new RegExp('a=rtpmap:' + codecs[i] + '.*\r\n', 'gi'));
  593.  
  594. if (!match) {
  595. continue;
  596. }
  597.  
  598. // Format: codec/clockRate/channels
  599. var parts = ((match[0] || '').replace(/\r\n/g, '').split(' ')[1] || '').split('/');
  600.  
  601. // Ignore rtcp codecs, dtmf or comfort noise
  602. if (['red', 'ulpfec', 'telephone-event', 'cn', 'rtx'].indexOf(parts[0].toLowerCase()) > -1) {
  603. continue;
  604. }
  605.  
  606. codecInfo.name = parts[0];
  607. codecInfo.clockRate = parseInt(parts[1], 10) || 0;
  608. codecInfo.channels = parseInt(parts[2] || '1', 10) || 1;
  609. codecInfo.payloadType = parseInt(codecs[i], 10);
  610. codecInfo.params = '';
  611.  
  612. // Get the list of codec parameters
  613. var params = mediaItem.match(new RegExp('a=fmtp:' + codecs[i] + '.*\r\n', 'gi')) || [];
  614. params.forEach(function (paramItem) {
  615. codecInfo.params += paramItem.replace(new RegExp('a=fmtp:' + codecs[i], 'gi'), '').replace(/\ /g, '').replace(/\r\n/g, '');
  616. });
  617. break;
  618. }
  619. });
  620.  
  621. if (!beSilentOnLogs) {
  622. log.debug([targetMid, 'RTCSessionDesription', sessionDescription.type,
  623. 'Parsing session description "' + type + '" codecs ->'], codecInfo);
  624. }
  625.  
  626. return codecInfo;
  627. };
  628.  
  629. /**
  630. * Function that modifies the session description to remove non-relay ICE candidates.
  631. * @method _removeSDPFilteredCandidates
  632. * @private
  633. * @for Skylink
  634. * @since 0.6.16
  635. */
  636. Skylink.prototype._removeSDPFilteredCandidates = function (targetMid, sessionDescription) {
  637. // Handle Firefox MCU Peer ICE candidates
  638. if (targetMid === 'MCU' && sessionDescription.type === this.HANDSHAKE_PROGRESS.ANSWER &&
  639. window.webrtcDetectedBrowser === 'firefox') {
  640. sessionDescription.sdp = sessionDescription.sdp.replace(/ generation 0/g, '');
  641. sessionDescription.sdp = sessionDescription.sdp.replace(/ udp /g, ' UDP ');
  642. }
  643.  
  644. if (this._initOptions.forceTURN && this._hasMCU) {
  645. log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Not filtering ICE candidates as ' +
  646. 'TURN connections are enforced as MCU is present (and act as a TURN itself) so filtering of ICE candidate ' +
  647. 'flags are not honoured']);
  648. return sessionDescription.sdp;
  649. }
  650.  
  651. if (this._initOptions.filterCandidatesType.host) {
  652. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Removing "host" ICE candidates.']);
  653. sessionDescription.sdp = sessionDescription.sdp.replace(/a=candidate:.*host.*\r\n/g, '');
  654. }
  655.  
  656. if (this._initOptions.filterCandidatesType.srflx) {
  657. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Removing "srflx" ICE candidates.']);
  658. sessionDescription.sdp = sessionDescription.sdp.replace(/a=candidate:.*srflx.*\r\n/g, '');
  659. }
  660.  
  661. if (this._initOptions.filterCandidatesType.relay) {
  662. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Removing "relay" ICE candidates.']);
  663. sessionDescription.sdp = sessionDescription.sdp.replace(/a=candidate:.*relay.*\r\n/g, '');
  664. }
  665.  
  666. // sessionDescription.sdp = sessionDescription.sdp.replace(/a=candidate:(?!.*relay.*).*\r\n/g, '');
  667.  
  668. return sessionDescription.sdp;
  669. };
  670.  
  671. /**
  672. * Function that retrieves the current list of support codecs.
  673. * @method _getCodecsSupport
  674. * @private
  675. * @for Skylink
  676. * @since 0.6.18
  677. */
  678. Skylink.prototype._getCodecsSupport = function (callback) {
  679. var self = this;
  680.  
  681. if (self._currentCodecSupport) {
  682. callback(null);
  683. return;
  684. }
  685.  
  686. self._currentCodecSupport = { audio: {}, video: {} };
  687.  
  688. // Safari 11 REQUIRES a stream first before connection works, hence let's spoof it for now
  689. if (AdapterJS.webrtcDetectedType === 'AppleWebKit') {
  690. self._currentCodecSupport.audio = {
  691. opus: ['48000/2']
  692. };
  693. self._currentCodecSupport.video = {
  694. h264: ['48000']
  695. };
  696. return callback(null);
  697. }
  698.  
  699. try {
  700. if (window.webrtcDetectedBrowser === 'edge') {
  701. var codecs = RTCRtpSender.getCapabilities().codecs;
  702.  
  703. for (var i = 0; i < codecs.length; i++) {
  704. if (['audio','video'].indexOf(codecs[i].kind) > -1 && codecs[i].name) {
  705. var codec = codecs[i].name.toLowerCase();
  706. self._currentCodecSupport[codecs[i].kind][codec] = codecs[i].clockRate +
  707. (codecs[i].numChannels > 1 ? '/' + codecs[i].numChannels : '');
  708. }
  709. }
  710. // Ignore .fecMechanisms for now
  711. callback(null);
  712.  
  713. } else {
  714. var pc = new RTCPeerConnection(null);
  715. var offerConstraints = AdapterJS.webrtcDetectedType !== 'plugin' ? {
  716. offerToReceiveAudio: true,
  717. offerToReceiveVideo: true
  718. } : {
  719. mandatory: {
  720. OfferToReceiveVideo: true,
  721. OfferToReceiveAudio: true
  722. }
  723. };
  724.  
  725. // Prevent errors and proceed with create offer still...
  726. try {
  727. var channel = pc.createDataChannel('test');
  728. self._binaryChunkType = channel.binaryType || self._binaryChunkType;
  729. self._binaryChunkType = self._binaryChunkType.toLowerCase().indexOf('array') > -1 ?
  730. self.DATA_TRANSFER_DATA_TYPE.ARRAY_BUFFER : self._binaryChunkType;
  731. // Set the value according to the property
  732. for (var prop in self.DATA_TRANSFER_DATA_TYPE) {
  733. if (self.DATA_TRANSFER_DATA_TYPE.hasOwnProperty(prop) &&
  734. self._binaryChunkType.toLowerCase() === self.DATA_TRANSFER_DATA_TYPE[prop].toLowerCase()) {
  735. self._binaryChunkType = self.DATA_TRANSFER_DATA_TYPE[prop];
  736. break;
  737. }
  738. }
  739. } catch (e) {}
  740.  
  741. pc.createOffer(function (offer) {
  742. self._currentCodecSupport = self._getSDPCodecsSupport(null, offer);
  743. callback(null);
  744.  
  745. }, function (error) {
  746. callback(error);
  747. }, offerConstraints);
  748. }
  749. } catch (error) {
  750. callback(error);
  751. }
  752. };
  753.  
  754. /**
  755. * Function that modifies the session description to handle the connection settings.
  756. * This is experimental and never recommended to end-users.
  757. * @method _handleSDPConnectionSettings
  758. * @private
  759. * @for Skylink
  760. * @since 0.6.16
  761. */
  762. Skylink.prototype._handleSDPConnectionSettings = function (targetMid, sessionDescription, direction) {
  763. var self = this;
  764.  
  765. if (!self._sdpSessions[targetMid]) {
  766. return sessionDescription.sdp;
  767. }
  768.  
  769. var sessionDescriptionStr = sessionDescription.sdp;
  770.  
  771. // Handle a=end-of-candidates signaling for non-trickle ICE before setting remote session description
  772. if (direction === 'remote' && !self.getPeerInfo(targetMid).config.enableIceTrickle) {
  773. sessionDescriptionStr = sessionDescriptionStr.replace(/a=end-of-candidates\r\n/g, '');
  774. }
  775.  
  776. var sdpLines = sessionDescriptionStr.split('\r\n');
  777. var peerAgent = ((self._peerInformations[targetMid] || {}).agent || {}).name || '';
  778. var peerVersion = ((self._peerInformations[targetMid] || {}).agent || {}).version || 0;
  779. var mediaType = '';
  780. var bundleLineIndex = -1;
  781. var bundleLineMids = [];
  782. var mLineIndex = -1;
  783. var settings = clone(self._sdpSettings);
  784.  
  785. if (targetMid === 'MCU') {
  786. settings.connection.audio = true;
  787. settings.connection.video = true;
  788. settings.connection.data = true;
  789. }
  790.  
  791. // Patches for MCU sending empty video stream despite audio+video is not sending at all
  792. // Apply as a=inactive when supported
  793. if (self._hasMCU) {
  794. var peerStreamSettings = clone(self.getPeerInfo(targetMid)).settings || {};
  795. settings.direction.audio.receive = targetMid === 'MCU' ? true : !!peerStreamSettings.audio;
  796. settings.direction.audio.send = targetMid === 'MCU' ? true : false;
  797. settings.direction.video.receive = targetMid === 'MCU' ? true : !!peerStreamSettings.video;
  798. settings.direction.video.send = targetMid === 'MCU' ? true : false;
  799. }
  800.  
  801. if (direction === 'remote') {
  802. var offerCodecs = self._getSDPCommonSupports(targetMid, sessionDescription);
  803.  
  804. if (!offerCodecs.audio) {
  805. settings.connection.audio = false;
  806. }
  807.  
  808. if (!offerCodecs.video) {
  809. settings.connection.video = false;
  810. }
  811. }
  812.  
  813. // ANSWERER: Reject only the m= lines. Returned rejected m= lines as well.
  814. // OFFERER: Remove m= lines
  815.  
  816. self._sdpSessions[targetMid][direction].mLines = [];
  817. self._sdpSessions[targetMid][direction].bundleLine = '';
  818. self._sdpSessions[targetMid][direction].connection = {
  819. audio: null,
  820. video: null,
  821. data: null
  822. };
  823.  
  824. for (var i = 0; i < sdpLines.length; i++) {
  825. // Cache the a=group:BUNDLE line used for remote answer from Edge later
  826. if (sdpLines[i].indexOf('a=group:BUNDLE') === 0) {
  827. self._sdpSessions[targetMid][direction].bundleLine = sdpLines[i];
  828. bundleLineIndex = i;
  829.  
  830. // Check if there's a need to reject m= line
  831. } else if (sdpLines[i].indexOf('m=') === 0) {
  832. mediaType = (sdpLines[i].split('m=')[1] || '').split(' ')[0] || '';
  833. mediaType = mediaType === 'application' ? 'data' : mediaType;
  834. mLineIndex++;
  835.  
  836. self._sdpSessions[targetMid][direction].mLines[mLineIndex] = sdpLines[i];
  837.  
  838. // Check if there is missing unsupported video codecs support and reject it regardles of MCU Peer or not
  839. if (!settings.connection[mediaType]) {
  840. log.log([targetMid, 'RTCSessionDesription', sessionDescription.type,
  841. 'Removing rejected m=' + mediaType + ' line ->'], sdpLines[i]);
  842.  
  843. // Check if answerer and we do not have the power to remove the m line if index is 0
  844. // Set as a=inactive because we do not have that power to reject it somehow..
  845. // first m= line cannot be rejected for BUNDLE
  846. if (self._peerConnectionConfig.bundlePolicy === self.BUNDLE_POLICY.MAX_BUNDLE &&
  847. bundleLineIndex > -1 && mLineIndex === 0 && (direction === 'remote' ?
  848. sessionDescription.type === this.HANDSHAKE_PROGRESS.OFFER :
  849. sessionDescription.type === this.HANDSHAKE_PROGRESS.ANSWER)) {
  850. log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type,
  851. 'Not removing rejected m=' + mediaType + ' line ->'], sdpLines[i]);
  852. settings.connection[mediaType] = true;
  853. if (['audio', 'video'].indexOf(mediaType) > -1) {
  854. settings.direction[mediaType].send = false;
  855. settings.direction[mediaType].receive = false;
  856. }
  857. continue;
  858. }
  859.  
  860. if (window.webrtcDetectedBrowser === 'edge') {
  861. sdpLines.splice(i, 1);
  862. i--;
  863. continue;
  864. } else if (direction === 'remote' || sessionDescription.type === this.HANDSHAKE_PROGRESS.ANSWER) {
  865. var parts = sdpLines[i].split(' ');
  866. parts[1] = 0;
  867. sdpLines[i] = parts.join(' ');
  868. continue;
  869. }
  870. }
  871. }
  872.  
  873. if (direction === 'remote' && sdpLines[i].indexOf('a=candidate:') === 0 &&
  874. !self.getPeerInfo(targetMid).config.enableIceTrickle) {
  875. if (sdpLines[i + 1] ? !(sdpLines[i + 1].indexOf('a=candidate:') === 0 ||
  876. sdpLines[i + 1].indexOf('a=end-of-candidates') === 0) : true) {
  877. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
  878. 'Appending end-of-candidates signal for non-trickle ICE connection.']);
  879. sdpLines.splice(i + 1, 0, 'a=end-of-candidates');
  880. i++;
  881. }
  882. }
  883.  
  884. if (mediaType) {
  885. // Remove lines if we are rejecting the media and ensure unless (rejectVideoMedia is true), MCU has to enable those m= lines
  886. if (!settings.connection[mediaType]) {
  887. sdpLines.splice(i, 1);
  888. i--;
  889.  
  890. // Store the mids session description
  891. } else if (sdpLines[i].indexOf('a=mid:') === 0) {
  892. bundleLineMids.push(sdpLines[i].split('a=mid:')[1] || '');
  893.  
  894. // Configure direction a=sendonly etc for local sessiondescription
  895. } else if (mediaType && ['a=sendrecv', 'a=sendonly', 'a=recvonly'].indexOf(sdpLines[i]) > -1) {
  896. if (['audio', 'video'].indexOf(mediaType) === -1) {
  897. self._sdpSessions[targetMid][direction].connection.data = sdpLines[i];
  898. continue;
  899. }
  900.  
  901. if (direction === 'local') {
  902. if (settings.direction[mediaType].send && !settings.direction[mediaType].receive) {
  903. sdpLines[i] = sdpLines[i].indexOf('send') > -1 ? 'a=sendonly' : 'a=inactive';
  904. } else if (!settings.direction[mediaType].send && settings.direction[mediaType].receive) {
  905. sdpLines[i] = sdpLines[i].indexOf('recv') > -1 ? 'a=recvonly' : 'a=inactive';
  906. } else if (!settings.direction[mediaType].send && !settings.direction[mediaType].receive) {
  907. // MCU currently does not support a=inactive flag.. what do we do here?
  908. sdpLines[i] = 'a=inactive';
  909. }
  910.  
  911. // Handle Chrome bundle bug. - See: https://bugs.chromium.org/p/webrtc/issues/detail?id=6280
  912. if (!self._hasMCU && window.webrtcDetectedBrowser !== 'firefox' && peerAgent === 'firefox' &&
  913. sessionDescription.type === self.HANDSHAKE_PROGRESS.OFFER && sdpLines[i] === 'a=recvonly') {
  914. log.warn([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Overriding any original settings ' +
  915. 'to receive only to send and receive to resolve chrome BUNDLE errors.']);
  916. sdpLines[i] = 'a=sendrecv';
  917. settings.direction[mediaType].send = true;
  918. settings.direction[mediaType].receive = true;
  919. }
  920. // Patch for incorrect responses
  921. } else if (sessionDescription.type === self.HANDSHAKE_PROGRESS.ANSWER) {
  922. var localOfferRes = self._sdpSessions[targetMid].local.connection[mediaType];
  923. // Parse a=sendonly response
  924. if (localOfferRes === 'a=sendonly') {
  925. sdpLines[i] = ['a=inactive', 'a=recvonly'].indexOf(sdpLines[i]) === -1 ?
  926. (sdpLines[i] === 'a=sendonly' ? 'a=inactive' : 'a=recvonly') : sdpLines[i];
  927. // Parse a=recvonly
  928. } else if (localOfferRes === 'a=recvonly') {
  929. sdpLines[i] = ['a=inactive', 'a=sendonly'].indexOf(sdpLines[i]) === -1 ?
  930. (sdpLines[i] === 'a=recvonly' ? 'a=inactive' : 'a=sendonly') : sdpLines[i];
  931. // Parse a=sendrecv
  932. } else if (localOfferRes === 'a=inactive') {
  933. sdpLines[i] = 'a=inactive';
  934. }
  935. }
  936. self._sdpSessions[targetMid][direction].connection[mediaType] = sdpLines[i];
  937. }
  938. }
  939.  
  940. // Remove weird empty characters for Edge case.. :(
  941. if (!(sdpLines[i] || '').replace(/\n|\r|\s|\ /gi, '')) {
  942. sdpLines.splice(i, 1);
  943. i--;
  944. }
  945. }
  946.  
  947. // Fix chrome "offerToReceiveAudio" local offer not removing audio BUNDLE
  948. if (bundleLineIndex > -1) {
  949. if (self._peerConnectionConfig.bundlePolicy === self.BUNDLE_POLICY.MAX_BUNDLE) {
  950. sdpLines[bundleLineIndex] = 'a=group:BUNDLE ' + bundleLineMids.join(' ');
  951. // Remove a=group:BUNDLE line
  952. } else if (self._peerConnectionConfig.bundlePolicy === self.BUNDLE_POLICY.NONE) {
  953. sdpLines.splice(bundleLineIndex, 1);
  954. }
  955. }
  956.  
  957. // Append empty space below
  958. if (window.webrtcDetectedBrowser !== 'edge') {
  959. if (!sdpLines[sdpLines.length - 1].replace(/\n|\r|\s/gi, '')) {
  960. sdpLines[sdpLines.length - 1] = '';
  961. } else {
  962. sdpLines.push('');
  963. }
  964. }
  965.  
  966. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Handling connection lines and direction ->'], settings);
  967.  
  968. return sdpLines.join('\r\n');
  969. };
  970.  
  971. /**
  972. * Function that parses and retrieves the session description fingerprint.
  973. * @method _getSDPFingerprint
  974. * @private
  975. * @for Skylink
  976. * @since 0.6.18
  977. */
  978. Skylink.prototype._getSDPFingerprint = function (targetMid, sessionDescription, beSilentOnLogs) {
  979. var fingerprint = {
  980. fingerprint: null,
  981. fingerprintAlgorithm: null,
  982. derBase64: null
  983. };
  984.  
  985. if (!(sessionDescription && sessionDescription.sdp)) {
  986. return fingerprint;
  987. }
  988.  
  989. var sdpLines = sessionDescription.sdp.split('\r\n');
  990.  
  991. for (var i = 0; i < sdpLines.length; i++) {
  992. if (sdpLines[i].indexOf('a=fingerprint') === 0) {
  993. var parts = sdpLines[i].replace('a=fingerprint:', '').split(' ');
  994. fingerprint.fingerprint = parts[1];
  995. fingerprint.fingerprintAlgorithm = parts[0];
  996. break;
  997. }
  998. }
  999.  
  1000. if (!beSilentOnLogs) {
  1001. log.debug([targetMid, 'RTCSessionDesription', sessionDescription.type,
  1002. 'Parsing session description fingerprint ->'], fingerprint);
  1003. }
  1004.  
  1005. return fingerprint;
  1006. };
  1007.  
  1008. /**
  1009. * Function that modifies the session description to append the MediaStream and MediaStreamTrack IDs that seems
  1010. * to be missing from Firefox answer session description to Chrome connection causing freezes in re-negotiation.
  1011. * @method _renderSDPOutput
  1012. * @private
  1013. * @for Skylink
  1014. * @since 0.6.25
  1015. */
  1016. Skylink.prototype._renderSDPOutput = function (targetMid, sessionDescription) {
  1017. var self = this;
  1018. var localStream = null;
  1019. var localStreamId = null;
  1020.  
  1021. if (!(sessionDescription && sessionDescription.sdp)) {
  1022. return;
  1023. }
  1024.  
  1025. if (!self._peerConnections[targetMid]) {
  1026. return sessionDescription.sdp;
  1027. }
  1028.  
  1029. if (self._peerConnections[targetMid].localStream) {
  1030. localStream = self._peerConnections[targetMid].localStream;
  1031. localStreamId = self._peerConnections[targetMid].localStreamId || self._peerConnections[targetMid].localStream.id;
  1032. }
  1033.  
  1034. // For non-trickle ICE, remove the a=end-of-candidates line first to append it properly later
  1035. var sdpLines = (!self._initOptions.enableIceTrickle ? sessionDescription.sdp.replace(/a=end-of-candidates\r\n/g, '') : sessionDescription.sdp).split('\r\n');
  1036. var agent = ((self._peerInformations[targetMid] || {}).agent || {}).name || '';
  1037.  
  1038. // Parse and replace with the correct msid to prevent unwanted streams.
  1039. // Making it simple without replacing with the track IDs or labels, neither setting prefixing "mslabel" and "label" as required labels.
  1040. if (localStream) {
  1041. var ssrcId = null;
  1042. var mediaType = '';
  1043.  
  1044. for (var i = 0; i < sdpLines.length; i++) {
  1045. if (sdpLines[i].indexOf('m=') === 0) {
  1046. mediaType = (sdpLines[i].split('m=')[1] || '').split(' ')[0] || '';
  1047. mediaType = ['audio', 'video'].indexOf(mediaType) === -1 ? '' : mediaType;
  1048.  
  1049. } else if (mediaType) {
  1050. if (sdpLines[i].indexOf('a=msid:') === 0) {
  1051. var msidParts = sdpLines[i].split(' ');
  1052. msidParts[0] = 'a=msid:' + localStreamId;
  1053. sdpLines[i] = msidParts.join(' ');
  1054.  
  1055. } else if (sdpLines[i].indexOf('a=ssrc:') === 0) {
  1056. var ssrcParts = null;
  1057.  
  1058. // Replace for "msid:" and "mslabel:"
  1059. if (sdpLines[i].indexOf(' msid:') > 0) {
  1060. ssrcParts = sdpLines[i].split(' msid:');
  1061. } else if (sdpLines[i].indexOf(' mslabel:') > 0) {
  1062. ssrcParts = sdpLines[i].split(' mslabel:');
  1063. }
  1064.  
  1065. if (ssrcParts) {
  1066. var ssrcMsidParts = (ssrcParts[1] || '').split(' ');
  1067. ssrcMsidParts[0] = localStreamId;
  1068. ssrcParts[1] = ssrcMsidParts.join(' ');
  1069.  
  1070. if (sdpLines[i].indexOf(' msid:') > 0) {
  1071. sdpLines[i] = ssrcParts.join(' msid:');
  1072. } else if (sdpLines[i].indexOf(' mslabel:') > 0) {
  1073. sdpLines[i] = ssrcParts.join(' mslabel:');
  1074. }
  1075. }
  1076. }
  1077. }
  1078. }
  1079. }
  1080.  
  1081. // For non-trickle ICE, append the signaling of end-of-candidates properly
  1082. if (!self._initOptions.enableIceTrickle){
  1083. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
  1084. 'Appending end-of-candidates signal for non-trickle ICE connection.']);
  1085.  
  1086. for (var e = 0; e < sdpLines.length; e++) {
  1087. if (sdpLines[e].indexOf('a=candidate:') === 0) {
  1088. if (sdpLines[e + 1] ? !(sdpLines[e + 1].indexOf('a=candidate:') === 0 ||
  1089. sdpLines[e + 1].indexOf('a=end-of-candidates') === 0) : true) {
  1090. sdpLines.splice(e + 1, 0, 'a=end-of-candidates');
  1091. e++;
  1092. }
  1093. }
  1094. }
  1095. }
  1096.  
  1097. // Replace the bundle policy to prevent complete removal of m= lines for some cases that do not accept missing m= lines except edge.
  1098. if (sessionDescription.type === this.HANDSHAKE_PROGRESS.ANSWER && this._sdpSessions[targetMid]) {
  1099. var bundleLineIndex = -1;
  1100. var mLineIndex = -1;
  1101.  
  1102. for (var j = 0; j < sdpLines.length; j++) {
  1103. if (sdpLines[j].indexOf('a=group:BUNDLE') === 0 && this._sdpSessions[targetMid].remote.bundleLine &&
  1104. this._peerConnectionConfig.bundlePolicy === this.BUNDLE_POLICY.MAX_BUNDLE) {
  1105. sdpLines[j] = this._sdpSessions[targetMid].remote.bundleLine;
  1106. } else if (sdpLines[j].indexOf('m=') === 0) {
  1107. mLineIndex++;
  1108. var compareA = sdpLines[j].split(' ');
  1109. var compareB = (this._sdpSessions[targetMid].remote.mLines[mLineIndex] || '').split(' ');
  1110.  
  1111. if (compareA[0] && compareB[0] && compareA[0] !== compareB[0]) {
  1112. compareB[1] = 0;
  1113. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
  1114. 'Appending middle rejected m= line ->'], compareB.join(' '));
  1115. sdpLines.splice(j, 0, compareB.join(' '));
  1116. j++;
  1117. mLineIndex++;
  1118. }
  1119. }
  1120. }
  1121.  
  1122. while (this._sdpSessions[targetMid].remote.mLines[mLineIndex + 1]) {
  1123. mLineIndex++;
  1124. var appendIndex = sdpLines.length;
  1125. if (!sdpLines[appendIndex - 1].replace(/\s/gi, '')) {
  1126. appendIndex -= 1;
  1127. }
  1128. var parts = (this._sdpSessions[targetMid].remote.mLines[mLineIndex] || '').split(' ');
  1129. parts[1] = 0;
  1130. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type,
  1131. 'Appending later rejected m= line ->'], parts.join(' '));
  1132. sdpLines.splice(appendIndex, 0, parts.join(' '));
  1133. }
  1134. }
  1135.  
  1136. // Ensure for chrome case to have empty "" at last line or it will return invalid SDP errors
  1137. if (window.webrtcDetectedBrowser === 'edge' && sessionDescription.type === this.HANDSHAKE_PROGRESS.OFFER &&
  1138. !sdpLines[sdpLines.length - 1].replace(/\s/gi, '')) {
  1139. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Removing last empty space for Edge browsers']);
  1140. sdpLines.splice(sdpLines.length - 1, 1);
  1141. }
  1142.  
  1143. /*
  1144. var outputStr = sdpLines.join('\r\n');
  1145. if (window.webrtcDetectedBrowser === 'edge' && this._streams.userMedia && this._streams.userMedia.stream) {
  1146. var correctStreamId = this._streams.userMedia.stream.id || this._streams.userMedia.stream.label;
  1147. outputStr = outputStr.replace(new RegExp('a=msid:.*\ ', 'gi'), 'a=msid:' + correctStreamId + ' ');
  1148. outputStr = outputStr.replace(new RegExp('\ msid:.*\ ', 'gi'), ' msid:' + correctStreamId + ' ');
  1149. }*/
  1150.  
  1151. log.info([targetMid, 'RTCSessionDescription', sessionDescription.type, 'Formatted output ->'], sdpLines.join('\r\n'));
  1152.  
  1153. return sdpLines.join('\r\n');
  1154. };
  1155.  
  1156. /**
  1157. * Function that parses the session description to get the MediaStream IDs.
  1158. * NOTE: It might not completely accurate if the setRemoteDescription() fails..
  1159. * @method _parseSDPMediaStreamIDs
  1160. * @private
  1161. * @for Skylink
  1162. * @since 0.6.25
  1163. */
  1164. Skylink.prototype._parseSDPMediaStreamIDs = function (targetMid, sessionDescription) {
  1165. if (!this._peerConnections[targetMid]) {
  1166. return;
  1167. }
  1168.  
  1169. if (!(sessionDescription && sessionDescription.sdp)) {
  1170. this._peerConnections[targetMid].remoteStreamId = null;
  1171. return;
  1172. }
  1173.  
  1174. var sdpLines = sessionDescription.sdp.split('\r\n');
  1175. var currentStreamId = null;
  1176.  
  1177. for (var i = 0; i < sdpLines.length; i++) {
  1178. // a=msid:{31145dc5-b3e2-da4c-a341-315ef3ebac6b} {e0cac7dd-64a0-7447-b719-7d5bf042ca05}
  1179. if (sdpLines[i].indexOf('a=msid:') === 0) {
  1180. currentStreamId = (sdpLines[i].split('a=msid:')[1] || '').split(' ')[0];
  1181. break;
  1182. // a=ssrc:691169016 msid:c58721ed-b7db-4e7c-ac37-47432a7a2d6f 2e27a4b8-bc74-4118-b3d4-0f1c4ed4869b
  1183. } else if (sdpLines[i].indexOf('a=ssrc:') === 0 && sdpLines[i].indexOf(' msid:') > 0) {
  1184. currentStreamId = (sdpLines[i].split(' msid:')[1] || '').split(' ')[0];
  1185. break;
  1186. }
  1187. }
  1188.  
  1189. // No stream set
  1190. if (!currentStreamId) {
  1191. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'No remote stream is sent.']);
  1192. this._peerConnections[targetMid].remoteStreamId = null;
  1193. // New stream set
  1194. } else if (currentStreamId !== this._peerConnections[targetMid].remoteStreamId) {
  1195. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'New remote stream is sent ->'], currentStreamId);
  1196. this._peerConnections[targetMid].remoteStreamId = currentStreamId;
  1197. // Same stream set
  1198. } else {
  1199. log.info([targetMid, 'RTCSessionDesription', sessionDescription.type, 'Same remote stream is sent ->'], currentStreamId);
  1200. }
  1201. };
  1202.  
  1203. /**
  1204. * Function that parses and retrieves the session description ICE candidates.
  1205. * @method _getSDPICECandidates
  1206. * @private
  1207. * @for Skylink
  1208. * @since 0.6.18
  1209. */
  1210. Skylink.prototype._getSDPICECandidates = function (targetMid, sessionDescription, beSilentOnLogs) {
  1211. var candidates = {
  1212. host: [],
  1213. srflx: [],
  1214. relay: []
  1215. };
  1216.  
  1217. if (!(sessionDescription && sessionDescription.sdp)) {
  1218. return candidates;
  1219. }
  1220.  
  1221. sessionDescription.sdp.split('m=').forEach(function (mediaItem, index) {
  1222. // Ignore the v=0 lines etc..
  1223. if (index === 0) {
  1224. return;
  1225. }
  1226.  
  1227. // Remove a=mid: and \r\n
  1228. var sdpMid = ((mediaItem.match(/a=mid:.*\r\n/gi) || [])[0] || '').replace(/a=mid:/gi, '').replace(/\r\n/, '');
  1229. var sdpMLineIndex = index - 1;
  1230.  
  1231. (mediaItem.match(/a=candidate:.*\r\n/gi) || []).forEach(function (item) {
  1232. // Remove \r\n for candidate type being set at the end of candidate DOM string.
  1233. var canType = (item.split(' ')[7] || 'host').replace(/\r\n/g, '');
  1234. candidates[canType] = candidates[canType] || [];
  1235. candidates[canType].push(new RTCIceCandidate({
  1236. sdpMid: sdpMid,
  1237. sdpMLineIndex: sdpMLineIndex,
  1238. // Remove initial "a=" in a=candidate
  1239. candidate: (item.split('a=')[1] || '').replace(/\r\n/g, '')
  1240. }));
  1241. });
  1242. });
  1243.  
  1244. if (!beSilentOnLogs) {
  1245. log.debug([targetMid, 'RTCSessionDesription', sessionDescription.type,
  1246. 'Parsing session description ICE candidates ->'], candidates);
  1247. }
  1248.  
  1249. return candidates;
  1250. };
  1251.  
  1252. /**
  1253. * Function that gets each media line SSRCs.
  1254. * @method _getSDPMediaSSRC
  1255. * @private
  1256. * @for Skylink
  1257. * @since 0.6.18
  1258. */
  1259. Skylink.prototype._getSDPMediaSSRC = function (targetMid, sessionDescription, beSilentOnLogs) {
  1260. var ssrcs = {
  1261. audio: 0,
  1262. video: 0
  1263. };
  1264.  
  1265. if (!(sessionDescription && sessionDescription.sdp)) {
  1266. return ssrcs;
  1267. }
  1268.  
  1269. sessionDescription.sdp.split('m=').forEach(function (mediaItem, index) {
  1270. // Ignore the v=0 lines etc..
  1271. if (index === 0) {
  1272. return;
  1273. }
  1274.  
  1275. var mediaType = (mediaItem.split(' ')[0] || '');
  1276. var ssrcLine = (mediaItem.match(/a=ssrc:.*\r\n/) || [])[0];
  1277.  
  1278. if (typeof ssrcs[mediaType] !== 'number') {
  1279. return;
  1280. }
  1281.  
  1282. if (ssrcLine) {
  1283. ssrcs[mediaType] = parseInt((ssrcLine.split('a=ssrc:')[1] || '').split(' ')[0], 10) || 0;
  1284. }
  1285. });
  1286.  
  1287. if (!beSilentOnLogs) {
  1288. log.debug([targetMid, 'RTCSessionDesription', sessionDescription.type,
  1289. 'Parsing session description media SSRCs ->'], ssrcs);
  1290. }
  1291.  
  1292. return ssrcs;
  1293. };
  1294.  
  1295. /**
  1296. * Function that parses the current list of supported codecs from session description.
  1297. * @method _getSDPCodecsSupport
  1298. * @private
  1299. * @for Skylink
  1300. * @since 0.6.18
  1301. */
  1302. Skylink.prototype._getSDPCodecsSupport = function (targetMid, sessionDescription) {
  1303. var self = this;
  1304. var codecs = {
  1305. audio: {},
  1306. video: {}
  1307. };
  1308.  
  1309. if (!(sessionDescription && sessionDescription.sdp)) {
  1310. return codecs;
  1311. }
  1312.  
  1313. var sdpLines = sessionDescription.sdp.split('\r\n');
  1314. var mediaType = '';
  1315.  
  1316. for (var i = 0; i < sdpLines.length; i++) {
  1317. if (sdpLines[i].indexOf('m=') === 0) {
  1318. mediaType = (sdpLines[i].split('m=')[1] || '').split(' ')[0];
  1319. continue;
  1320. }
  1321.  
  1322. if (sdpLines[i].indexOf('a=rtpmap:') === 0) {
  1323. var parts = (sdpLines[i].split(' ')[1] || '').split('/');
  1324. var codec = (parts[0] || '').toLowerCase();
  1325. var info = parts[1] + (parts[2] ? '/' + parts[2] : '');
  1326.  
  1327. if (['ulpfec', 'red', 'telephone-event', 'cn', 'rtx'].indexOf(codec) > -1) {
  1328. continue;
  1329. }
  1330.  
  1331. codecs[mediaType][codec] = codecs[mediaType][codec] || [];
  1332.  
  1333. if (codecs[mediaType][codec].indexOf(info) === -1) {
  1334. codecs[mediaType][codec].push(info);
  1335. }
  1336. }
  1337. }
  1338.  
  1339. log.info([targetMid || null, 'RTCSessionDescription', sessionDescription.type, 'Parsed codecs support ->'], codecs);
  1340. return codecs;
  1341. };
  1342.  
  1343. /**
  1344. * Function that checks if there are any common codecs supported for remote end.
  1345. * @method _getSDPCommonSupports
  1346. * @private
  1347. * @for Skylink
  1348. * @since 0.6.25
  1349. */
  1350. Skylink.prototype._getSDPCommonSupports = function (targetMid, sessionDescription) {
  1351. var self = this;
  1352. var offer = {
  1353. audio: false,
  1354. video: false
  1355. };
  1356.  
  1357. if (!targetMid || !(sessionDescription && sessionDescription.sdp)) {
  1358. offer.video = !!(self._currentCodecSupport.video.h264 || self._currentCodecSupport.video.vp8);
  1359. offer.audio = !!self._currentCodecSupport.audio.opus;
  1360.  
  1361. if (targetMid) {
  1362. var peerAgent = ((self._peerInformations[targetMid] || {}).agent || {}).name || '';
  1363.  
  1364. if (AdapterJS.webrtcDetectedBrowser === peerAgent) {
  1365. offer.video = Object.keys(self._currentCodecSupport.video).length > 0;
  1366. offer.audio = Object.keys(self._currentCodecSupport.audio).length > 0;
  1367. }
  1368. }
  1369. return offer;
  1370. }
  1371.  
  1372. var remoteCodecs = self._getSDPCodecsSupport(targetMid, sessionDescription);
  1373. var localCodecs = self._currentCodecSupport;
  1374.  
  1375. for (var ac in localCodecs.audio) {
  1376. if (localCodecs.audio.hasOwnProperty(ac) && localCodecs.audio[ac] && remoteCodecs.audio[ac]) {
  1377. offer.audio = true;
  1378. break;
  1379. }
  1380. }
  1381.  
  1382. for (var vc in localCodecs.video) {
  1383. if (localCodecs.video.hasOwnProperty(vc) && localCodecs.video[vc] && remoteCodecs.video[vc]) {
  1384. offer.video = true;
  1385. break;
  1386. }
  1387. }
  1388.  
  1389. return offer;
  1390. };
  1391.  
  1392. /**
  1393. * Function adds SCTP port number for Firefox 63.0.3 and above if its missing in the answer from MCU
  1394. * @method _setSCTPport
  1395. * @private
  1396. * @for Skylink
  1397. * @since 0.6.35
  1398. */
  1399. Skylink.prototype._setSCTPport = function (targetMid, sessionDescription) {
  1400. var self = this;
  1401. if (AdapterJS.webrtcDetectedBrowser === 'firefox' && AdapterJS.webrtcDetectedVersion >= 63 && self._hasMCU === true) {
  1402. var sdpLines = sessionDescription.sdp.split('\r\n');
  1403. var mLineType = 'application';
  1404. var mLineIndex = -1;
  1405. var sdpType = sessionDescription.type;
  1406.  
  1407. for (var i = 0; i < sdpLines.length; i++) {
  1408. if (sdpLines[i].indexOf('m=' + mLineType) === 0) {
  1409. mLineIndex = i;
  1410. } else if (mLineIndex > 0) {
  1411. if (sdpLines[i].indexOf('m=') === 0) {
  1412. break;
  1413. }
  1414.  
  1415. // Saving m=application line when creating offer into instance variable
  1416. if (sdpType === 'offer') {
  1417. self._mline = sdpLines[mLineIndex];
  1418. break;
  1419. }
  1420.  
  1421. // Replacing m=application line from instance variable
  1422. if (sdpType === 'answer') {
  1423. sdpLines[mLineIndex] = self._mline;
  1424. sdpLines.splice(mLineIndex + 1, 0, 'a=sctp-port:5000');
  1425. break;
  1426. }
  1427. }
  1428. }
  1429.  
  1430. return sdpLines.join('\r\n');
  1431. }
  1432.  
  1433. return sessionDescription.sdp;
  1434. };
  1435.  
  1436. /**
  1437. * Function sets the original DTLS role which was negotiated on first offer/ansswer exchange
  1438. * This needs to be done until https://bugzilla.mozilla.org/show_bug.cgi?id=1240897 is released in Firefox 68
  1439. * Estimated release date for Firefox 68 : 2019-07-09 (https://wiki.mozilla.org/Release_Management/Calendar)
  1440. * @method _setOriginalDTLSRole
  1441. * @private
  1442. * @for Skylink
  1443. * @since 0.6.35
  1444. */
  1445. Skylink.prototype._setOriginalDTLSRole = function (sessionDescription, isRemote) {
  1446. var self = this;
  1447. var sdpType = sessionDescription.type;
  1448. var role = null;
  1449. var aSetupPattern = null;
  1450. var invertRoleMap = { active: 'passive', passive: 'active' };
  1451.  
  1452. if (self._originalDTLSRole !== null || sdpType === 'offer') {
  1453. return;
  1454. }
  1455.  
  1456. aSetupPattern = sessionDescription.sdp.match(/a=setup:([a-z]+)/);
  1457.  
  1458. if (!aSetupPattern) {
  1459. return;
  1460. }
  1461.  
  1462. role = aSetupPattern[1];
  1463. self._originalDTLSRole = isRemote ? invertRoleMap[role] : role;
  1464. };
  1465.  
  1466.  
  1467. /**
  1468. * Function that modifies the DTLS role in answer sdp
  1469. * This needs to be done until https://bugzilla.mozilla.org/show_bug.cgi?id=1240897 is released in Firefox 68
  1470. * Estimated release date for Firefox 68 : 2019-07-09 (https://wiki.mozilla.org/Release_Management/Calendar)
  1471. * @method _modifyDTLSRole
  1472. * @private
  1473. * @for Skylink
  1474. * @since 1.0.0
  1475. */
  1476. Skylink.prototype._modifyDTLSRole = function (sessionDescription) {
  1477. var self = this;
  1478. var sdpType = sessionDescription.type;
  1479.  
  1480. if (self._originalDTLSRole === null || sdpType === 'offer') {
  1481. return;
  1482. }
  1483.  
  1484. sessionDescription.sdp = sessionDescription.sdp.replace(/a=setup:[a-z]+/g, 'a=setup:' + self._originalDTLSRole);
  1485. return sessionDescription.sdp;
  1486. };