You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LocalSSRCReplacement.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /* global $ */
  2. /*
  3. Here we do modifications of local video SSRCs. There are 2 situations we have
  4. to handle:
  5. 1. We generate SSRC for local recvonly video stream. This is the case when we
  6. have no local camera and it is not generated automatically, but SSRC=1 is
  7. used implicitly. If that happens RTCP packets will be dropped by the JVB
  8. and we won't be able to request video key frames correctly.
  9. 2. A hack to re-use SSRC of the first video stream for any new stream created
  10. in future. It turned out that Chrome may keep on using the SSRC of removed
  11. video stream in RTCP even though a new one has been created. So we just
  12. want to avoid that by re-using it. Jingle 'source-remove'/'source-add'
  13. notifications are blocked once first video SSRC is advertised to the focus.
  14. What this hack does:
  15. 1. Stores the SSRC of the first video stream created by
  16. a) scanning Jingle session-accept/session-invite for existing video SSRC
  17. b) watching for 'source-add' for new video stream if it has not been
  18. created in step a)
  19. 2. Exposes method 'mungeLocalVideoSSRC' which replaces any new video SSRC with
  20. the stored one. It is called by 'TracablePeerConnection' before local SDP is
  21. returned to the other parts of the application.
  22. 3. Scans 'source-remove'/'source-add' notifications for stored video SSRC and
  23. blocks those notifications. This makes Jicofo and all participants think
  24. that it exists all the time even if the video stream has been removed or
  25. replaced locally. Thanks to that there is no additional signaling activity
  26. on video mute or when switching to the desktop stream.
  27. */
  28. var SDP = require('./SDP');
  29. var RandomUtil = require('../util/RandomUtil');
  30. var RTCBrowserType = require('../RTC/RTCBrowserType');
  31. /**
  32. * The hack is enabled on all browsers except FF by default
  33. * FIXME finish the hack once removeStream method is implemented in FF
  34. * @type {boolean}
  35. */
  36. var isEnabled = !RTCBrowserType.isFirefox();
  37. /**
  38. * Stored SSRC of local video stream.
  39. */
  40. var localVideoSSRC;
  41. /**
  42. * SSRC, msid, mslabel, label used for recvonly video stream when we have no local camera.
  43. * This is in order to tell Chrome what SSRC should be used in RTCP requests
  44. * instead of 1.
  45. */
  46. var localRecvOnlySSRC, localRecvOnlyMSID, localRecvOnlyMSLabel, localRecvOnlyLabel;
  47. /**
  48. * cname for <tt>localRecvOnlySSRC</tt>
  49. */
  50. var localRecvOnlyCName;
  51. /**
  52. * Method removes <source> element which describes <tt>localVideoSSRC</tt>
  53. * from given Jingle IQ.
  54. * @param modifyIq 'source-add' or 'source-remove' Jingle IQ.
  55. * @param actionName display name of the action which will be printed in log
  56. * messages.
  57. * @returns {*} modified Jingle IQ, so that it does not contain <source> element
  58. * corresponding to <tt>localVideoSSRC</tt> or <tt>null</tt> if no
  59. * other SSRCs left to be signaled after removing it.
  60. */
  61. var filterOutSource = function (modifyIq, actionName) {
  62. var modifyIqTree = $(modifyIq.tree());
  63. if (!localVideoSSRC)
  64. return modifyIqTree[0];
  65. var videoSSRC = modifyIqTree.find(
  66. '>jingle>content[name="video"]' +
  67. '>description>source[ssrc="' + localVideoSSRC + '"]');
  68. if (!videoSSRC.length) {
  69. return modifyIqTree[0];
  70. }
  71. console.info(
  72. 'Blocking ' + actionName + ' for local video SSRC: ' + localVideoSSRC);
  73. videoSSRC.remove();
  74. // Check if any sources still left to be added/removed
  75. if (modifyIqTree.find('>jingle>content>description>source').length) {
  76. return modifyIqTree[0];
  77. } else {
  78. return null;
  79. }
  80. };
  81. /**
  82. * Scans given Jingle IQ for video SSRC and stores it.
  83. * @param jingleIq the Jingle IQ to be scanned for video SSRC.
  84. */
  85. var storeLocalVideoSSRC = function (jingleIq) {
  86. var videoSSRCs =
  87. $(jingleIq.tree())
  88. .find('>jingle>content[name="video"]>description>source');
  89. videoSSRCs.each(function (idx, ssrcElem) {
  90. if (localVideoSSRC)
  91. return;
  92. // We consider SSRC real only if it has msid attribute
  93. // recvonly streams in FF do not have it as well as local SSRCs
  94. // we generate for recvonly streams in Chrome
  95. var ssrSel = $(ssrcElem);
  96. var msid = ssrSel.find('>parameter[name="msid"]');
  97. if (msid.length) {
  98. var ssrcVal = ssrSel.attr('ssrc');
  99. if (ssrcVal) {
  100. localVideoSSRC = ssrcVal;
  101. console.info('Stored local video SSRC' +
  102. ' for future re-use: ' + localVideoSSRC);
  103. }
  104. }
  105. });
  106. };
  107. /**
  108. * Generates new label/mslabel attribute
  109. * @returns {string} label/mslabel attribute
  110. */
  111. function generateLabel() {
  112. return RandomUtil.random8digitsHex() + "-" + RandomUtil.random4digitsHex() + "-" +
  113. RandomUtil.random4digitsHex() + "-" + RandomUtil.random4digitsHex() + "-" + RandomUtil.random12digitsHex();
  114. }
  115. /**
  116. * Generates new SSRC, CNAME, mslabel, label and msid for local video recvonly stream.
  117. * FIXME what about eventual SSRC collision ?
  118. */
  119. function generateRecvonlySSRC() {
  120. localRecvOnlySSRC =
  121. Math.random().toString(10).substring(2, 11);
  122. localRecvOnlyCName =
  123. Math.random().toString(36).substring(2);
  124. localRecvOnlyMSLabel = generateLabel();
  125. localRecvOnlyLabel = generateLabel();
  126. localRecvOnlyMSID = localRecvOnlyMSLabel + " " + localRecvOnlyLabel;
  127. console.info(
  128. "Generated local recvonly SSRC: " + localRecvOnlySSRC +
  129. ", cname: " + localRecvOnlyCName);
  130. }
  131. var LocalSSRCReplacement = {
  132. /**
  133. * Method must be called before 'session-initiate' or 'session-invite' is
  134. * sent. Scans the IQ for local video SSRC and stores it if detected.
  135. *
  136. * @param sessionInit our 'session-initiate' or 'session-accept' Jingle IQ
  137. * which will be scanned for local video SSRC.
  138. */
  139. processSessionInit: function (sessionInit) {
  140. if (!isEnabled)
  141. return;
  142. if (localVideoSSRC) {
  143. console.error("Local SSRC stored already: " + localVideoSSRC);
  144. return;
  145. }
  146. storeLocalVideoSSRC(sessionInit);
  147. },
  148. /**
  149. * If we have local video SSRC stored searched given
  150. * <tt>localDescription</tt> for video SSRC and makes sure it is replaced
  151. * with the stored one.
  152. * @param localDescription local description object that will have local
  153. * video SSRC replaced with the stored one
  154. * @returns modified <tt>localDescription</tt> object.
  155. */
  156. mungeLocalVideoSSRC: function (localDescription) {
  157. if (!isEnabled)
  158. return localDescription;
  159. if (!localDescription) {
  160. console.warn("localDescription is null or undefined");
  161. return localDescription;
  162. }
  163. // IF we have local video SSRC stored make sure it is replaced
  164. // with old SSRC
  165. if (localVideoSSRC) {
  166. var newSdp = new SDP(localDescription.sdp);
  167. if (newSdp.media[1].indexOf("a=ssrc:") !== -1 &&
  168. !newSdp.containsSSRC(localVideoSSRC)) {
  169. // Get new video SSRC
  170. var map = newSdp.getMediaSsrcMap();
  171. var videoPart = map[1];
  172. var videoSSRCs = videoPart.ssrcs;
  173. var newSSRC = Object.keys(videoSSRCs)[0];
  174. console.info(
  175. "Replacing new video SSRC: " + newSSRC +
  176. " with " + localVideoSSRC);
  177. localDescription.sdp =
  178. newSdp.raw.replace(
  179. new RegExp('a=ssrc:' + newSSRC, 'g'),
  180. 'a=ssrc:' + localVideoSSRC);
  181. }
  182. } else {
  183. // Make sure we have any SSRC for recvonly video stream
  184. var sdp = new SDP(localDescription.sdp);
  185. if (sdp.media[1] && sdp.media[1].indexOf('a=ssrc:') === -1 &&
  186. sdp.media[1].indexOf('a=recvonly') !== -1) {
  187. if (!localRecvOnlySSRC) {
  188. generateRecvonlySSRC();
  189. }
  190. localVideoSSRC = localRecvOnlySSRC;
  191. console.info('No SSRC in video recvonly stream' +
  192. ' - adding SSRC: ' + localRecvOnlySSRC);
  193. sdp.media[1] += 'a=ssrc:' + localRecvOnlySSRC +
  194. ' cname:' + localRecvOnlyCName + '\r\n' +
  195. 'a=ssrc:' + localRecvOnlySSRC +
  196. ' msid:' + localRecvOnlyMSID + '\r\n' +
  197. 'a=ssrc:' + localRecvOnlySSRC +
  198. ' mslabel:' + localRecvOnlyMSLabel + '\r\n' +
  199. 'a=ssrc:' + localRecvOnlySSRC +
  200. ' label:' + localRecvOnlyLabel + '\r\n';
  201. localDescription.sdp = sdp.session + sdp.media.join('');
  202. }
  203. }
  204. return localDescription;
  205. },
  206. /**
  207. * Method must be called before 'source-add' notification is sent. In case
  208. * we have local video SSRC advertised already it will be removed from the
  209. * notification. If no other SSRCs are described by given IQ null will be
  210. * returned which means that there is no point in sending the notification.
  211. * @param sourceAdd 'source-add' Jingle IQ to be processed
  212. * @returns modified 'source-add' IQ which can be sent to the focus or
  213. * <tt>null</tt> if no notification shall be sent. It is no longer
  214. * a Strophe IQ Builder instance, but DOM element tree.
  215. */
  216. processSourceAdd: function (sourceAdd) {
  217. if (!isEnabled)
  218. return sourceAdd;
  219. if (!localVideoSSRC) {
  220. // Store local SSRC if available
  221. storeLocalVideoSSRC(sourceAdd);
  222. return sourceAdd;
  223. } else {
  224. return filterOutSource(sourceAdd, 'source-add');
  225. }
  226. },
  227. /**
  228. * Method must be called before 'source-remove' notification is sent.
  229. * Removes local video SSRC from the notification. If there are no other
  230. * SSRCs described in the given IQ <tt>null</tt> will be returned which
  231. * means that there is no point in sending the notification.
  232. * @param sourceRemove 'source-remove' Jingle IQ to be processed
  233. * @returns modified 'source-remove' IQ which can be sent to the focus or
  234. * <tt>null</tt> if no notification shall be sent. It is no longer
  235. * a Strophe IQ Builder instance, but DOM element tree.
  236. */
  237. processSourceRemove: function (sourceRemove) {
  238. if (!isEnabled)
  239. return sourceRemove;
  240. return filterOutSource(sourceRemove, 'source-remove');
  241. },
  242. /**
  243. * Turns the hack on or off
  244. * @param enabled <tt>true</tt> to enable the hack or <tt>false</tt>
  245. * to disable it
  246. */
  247. setEnabled: function (enabled) {
  248. isEnabled = enabled;
  249. }
  250. };
  251. module.exports = LocalSSRCReplacement;