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.

RtxModifier.spec.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. /* eslint-disable max-len*/
  2. import * as transform from 'sdp-transform';
  3. import RtxModifier from './RtxModifier.js';
  4. import SDPUtil from './SDPUtil';
  5. import { default as SampleSdpStrings } from './SampleSdpStrings.js';
  6. /**
  7. * Returns the number of video ssrcs in the given sdp
  8. * @param {object} parsedSdp the sdp as parsed by transform.parse
  9. * @returns {number} the number of video ssrcs in the given sdp
  10. */
  11. function numVideoSsrcs(parsedSdp) {
  12. const videoMLine = parsedSdp.media.find(m => m.type === 'video');
  13. return videoMLine.ssrcs
  14. .map(ssrcInfo => ssrcInfo.id)
  15. .filter((ssrc, index, array) => array.indexOf(ssrc) === index)
  16. .length;
  17. }
  18. /**
  19. * Return the (single) primary video ssrc in the given sdp
  20. * @param {object} parsedSdp the sdp as parsed by transform.parse
  21. * @returns {number} the primary video ssrc in the given sdp
  22. */
  23. function getPrimaryVideoSsrc(parsedSdp) {
  24. const videoMLine = parsedSdp.media.find(m => m.type === 'video');
  25. return parseInt(SDPUtil.parsePrimaryVideoSsrc(videoMLine), 10);
  26. }
  27. /**
  28. * Get the primary video ssrc(s) in the given sdp.
  29. * Only handles parsing 2 scenarios right now:
  30. * 1) Single video ssrc
  31. * 2) Multiple video ssrcs in a single simulcast group
  32. * @param {object} parsedSdp the sdp as parsed by transform.parse
  33. * @returns {list<number>} the primary video ssrcs in the given sdp
  34. */
  35. function getPrimaryVideoSsrcs(parsedSdp) {
  36. const videoMLine = parsedSdp.media.find(m => m.type === 'video');
  37. if (numVideoSsrcs(parsedSdp) === 1) {
  38. return [ videoMLine.ssrcs[0].id ];
  39. }
  40. const simGroups = getVideoGroups(parsedSdp, 'SIM');
  41. if (simGroups.length > 1) {
  42. return;
  43. }
  44. const simGroup = simGroups[0];
  45. return SDPUtil.parseGroupSsrcs(simGroup);
  46. }
  47. /**
  48. * Get the video groups that match the passed semantics from the
  49. * given sdp
  50. * @param {object} parsedSDp the sdp as parsed by transform.parse
  51. * @param {string} groupSemantics the semantics string of the groups
  52. * the caller is interested in
  53. * @returns {list<object>} a list of the groups from the given sdp
  54. * that matched the passed semantics
  55. */
  56. function getVideoGroups(parsedSdp, groupSemantics) {
  57. const videoMLine = parsedSdp.media.find(m => m.type === 'video');
  58. videoMLine.ssrcGroups = videoMLine.ssrcGroups || [];
  59. return videoMLine.ssrcGroups
  60. .filter(g => g.semantics === groupSemantics);
  61. }
  62. describe('RtxModifier', () => {
  63. let rtxModifier;
  64. beforeEach(() => {
  65. rtxModifier = new RtxModifier();
  66. });
  67. describe('modifyRtxSsrcs', () => {
  68. describe('when given an sdp with a single video ssrc', () => {
  69. let primaryVideoSsrc, singleVideoSdp;
  70. beforeEach(() => {
  71. singleVideoSdp = SampleSdpStrings.plainVideoSdp;
  72. primaryVideoSsrc = getPrimaryVideoSsrc(singleVideoSdp);
  73. });
  74. it('should add a single rtx ssrc', () => {
  75. // Call rtxModifier.modifyRtxSsrcs with an sdp that contains a single video
  76. // ssrc. The returned sdp should have an rtx ssrc and an fid group.
  77. const newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(singleVideoSdp));
  78. const newSdp = transform.parse(newSdpStr);
  79. const newPrimaryVideoSsrc = getPrimaryVideoSsrc(newSdp);
  80. expect(newPrimaryVideoSsrc).toEqual(primaryVideoSsrc);
  81. // Should now have an rtx ssrc as well
  82. expect(numVideoSsrcs(newSdp)).toEqual(2);
  83. // Should now have an FID group
  84. const fidGroups = getVideoGroups(newSdp, 'FID');
  85. expect(fidGroups.length).toEqual(1);
  86. const fidGroup = fidGroups[0];
  87. const fidGroupPrimarySsrc = SDPUtil.parseGroupSsrcs(fidGroup)[0];
  88. expect(fidGroupPrimarySsrc).toEqual(primaryVideoSsrc);
  89. });
  90. it('should re-use the same rtx ssrc for a primary ssrc it\'s seen before', () => {
  91. // Have rtxModifier generate an rtx ssrc via modifyRtxSsrcs. Then call it again
  92. // with the same primary ssrc in the sdp (but no rtx ssrc). It should use
  93. // the same rtx ssrc as before.
  94. let newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(singleVideoSdp));
  95. let newSdp = transform.parse(newSdpStr);
  96. let fidGroup = getVideoGroups(newSdp, 'FID')[0];
  97. const fidGroupRtxSsrc = SDPUtil.parseGroupSsrcs(fidGroup)[1];
  98. // Now pass the original sdp through again
  99. newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(singleVideoSdp));
  100. newSdp = transform.parse(newSdpStr);
  101. fidGroup = getVideoGroups(newSdp, 'FID')[0];
  102. const newFidGroupRtxSsrc = SDPUtil.parseGroupSsrcs(fidGroup)[1];
  103. expect(newFidGroupRtxSsrc).toEqual(fidGroupRtxSsrc);
  104. });
  105. it('should NOT re-use the same rtx ssrc for a primary ssrc it\'s seen before if the cache has been cleared', () => {
  106. // Call modifyRtxSsrcs to generate an rtx ssrc
  107. // Clear the rtxModifier cache
  108. // Call modifyRtxSsrcs to generate an rtx ssrc again with the same primary ssrc
  109. // --> We should get a different rtx ssrc
  110. let newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(singleVideoSdp));
  111. let newSdp = transform.parse(newSdpStr);
  112. let fidGroup = getVideoGroups(newSdp, 'FID')[0];
  113. const fidGroupRtxSsrc = SDPUtil.parseGroupSsrcs(fidGroup)[1];
  114. rtxModifier.clearSsrcCache();
  115. // Now pass the original sdp through again
  116. newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(singleVideoSdp));
  117. newSdp = transform.parse(newSdpStr);
  118. fidGroup = getVideoGroups(newSdp, 'FID')[0];
  119. const newFidGroupRtxSsrc = SDPUtil.parseGroupSsrcs(fidGroup)[1];
  120. expect(newFidGroupRtxSsrc).not.toEqual(fidGroupRtxSsrc);
  121. });
  122. it('should use the rtx ssrc from the cache when the cache has been manually set', () => {
  123. // Manually set an rtx ssrc mapping in the cache
  124. // Call modifyRtxSsrcs
  125. // -->The rtx ssrc used should be the one we set
  126. const forcedRtxSsrc = 123456;
  127. const ssrcCache = new Map();
  128. ssrcCache.set(primaryVideoSsrc, forcedRtxSsrc);
  129. rtxModifier.setSsrcCache(ssrcCache);
  130. const newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(singleVideoSdp));
  131. const newSdp = transform.parse(newSdpStr);
  132. const fidGroup = getVideoGroups(newSdp, 'FID')[0];
  133. const fidGroupRtxSsrc = SDPUtil.parseGroupSsrcs(fidGroup)[1];
  134. expect(fidGroupRtxSsrc).toEqual(forcedRtxSsrc);
  135. });
  136. });
  137. describe('when given an sdp with multiple video ssrcs', () => {
  138. let multipleVideoSdp, primaryVideoSsrcs;
  139. beforeEach(() => {
  140. multipleVideoSdp = SampleSdpStrings.simulcastSdp;
  141. primaryVideoSsrcs = getPrimaryVideoSsrcs(multipleVideoSdp);
  142. });
  143. it('should add rtx ssrcs for all of them', () => {
  144. // Call rtxModifier.modifyRtxSsrcs with an sdp that contains multiple video
  145. // ssrcs. The returned sdp should have an rtx ssrc and an fid group for all of them.
  146. const newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(multipleVideoSdp));
  147. const newSdp = transform.parse(newSdpStr);
  148. const newPrimaryVideoSsrcs = getPrimaryVideoSsrcs(newSdp);
  149. expect(newPrimaryVideoSsrcs).toEqual(primaryVideoSsrcs);
  150. // Should now have rtx ssrcs as well
  151. expect(numVideoSsrcs(newSdp)).toEqual(primaryVideoSsrcs.length * 2);
  152. // Should now have FID groups
  153. const fidGroups = getVideoGroups(newSdp, 'FID');
  154. expect(fidGroups.length).toEqual(primaryVideoSsrcs.length);
  155. fidGroups.forEach(fidGroup => {
  156. const fidGroupPrimarySsrc = SDPUtil.parseGroupSsrcs(fidGroup)[0];
  157. expect(primaryVideoSsrcs.indexOf(fidGroupPrimarySsrc)).not.toEqual(-1);
  158. });
  159. });
  160. it('should re-use the same rtx ssrcs for any primary ssrc it\'s seen before', () => {
  161. // Have rtxModifier generate an rtx ssrc via modifyRtxSsrcs. Then call it again
  162. // with the same primary ssrc in the sdp (but no rtx ssrc). It should use
  163. // the same rtx ssrc as before.
  164. let newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(multipleVideoSdp));
  165. let newSdp = transform.parse(newSdpStr);
  166. const rtxMapping = new Map();
  167. let fidGroups = getVideoGroups(newSdp, 'FID');
  168. // Save the first mapping that is made
  169. fidGroups.forEach(fidGroup => {
  170. const fidSsrcs = SDPUtil.parseGroupSsrcs(fidGroup);
  171. const fidGroupPrimarySsrc = fidSsrcs[0];
  172. const fidGroupRtxSsrc = fidSsrcs[1];
  173. rtxMapping.set(fidGroupPrimarySsrc, fidGroupRtxSsrc);
  174. });
  175. // Now pass the original sdp through again and make sure we get the same mapping
  176. newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(multipleVideoSdp));
  177. newSdp = transform.parse(newSdpStr);
  178. fidGroups = getVideoGroups(newSdp, 'FID');
  179. fidGroups.forEach(fidGroup => {
  180. const fidSsrcs = SDPUtil.parseGroupSsrcs(fidGroup);
  181. const fidGroupPrimarySsrc = fidSsrcs[0];
  182. const fidGroupRtxSsrc = fidSsrcs[1];
  183. expect(rtxMapping.has(fidGroupPrimarySsrc)).toBe(true);
  184. expect(rtxMapping.get(fidGroupPrimarySsrc)).toEqual(fidGroupRtxSsrc);
  185. });
  186. });
  187. it('should NOT re-use the same rtx ssrcs for any primary ssrc it\'s seen before if the cache has been cleared', () => {
  188. // Call modifyRtxSsrcs to generate an rtx ssrc
  189. // Clear the rtxModifier cache
  190. // Call modifyRtxSsrcs to generate rtx ssrcs again with the same primary ssrcs
  191. // --> We should get different rtx ssrcs
  192. let newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(multipleVideoSdp));
  193. let newSdp = transform.parse(newSdpStr);
  194. const rtxMapping = new Map();
  195. let fidGroups = getVideoGroups(newSdp, 'FID');
  196. // Save the first mapping that is made
  197. fidGroups.forEach(fidGroup => {
  198. const fidSsrcs = SDPUtil.parseGroupSsrcs(fidGroup);
  199. const fidGroupPrimarySsrc = fidSsrcs[0];
  200. const fidGroupRtxSsrc = fidSsrcs[1];
  201. rtxMapping.set(fidGroupPrimarySsrc, fidGroupRtxSsrc);
  202. });
  203. rtxModifier.clearSsrcCache();
  204. // Now pass the original sdp through again and make sure we get the same mapping
  205. newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(multipleVideoSdp));
  206. newSdp = transform.parse(newSdpStr);
  207. fidGroups = getVideoGroups(newSdp, 'FID');
  208. fidGroups.forEach(fidGroup => {
  209. const fidSsrcs = SDPUtil.parseGroupSsrcs(fidGroup);
  210. const fidGroupPrimarySsrc = fidSsrcs[0];
  211. const fidGroupRtxSsrc = fidSsrcs[1];
  212. expect(rtxMapping.has(fidGroupPrimarySsrc)).toBe(true);
  213. expect(rtxMapping.get(fidGroupPrimarySsrc)).not.toEqual(fidGroupRtxSsrc);
  214. });
  215. });
  216. it('should use the rtx ssrcs from the cache when the cache has been manually set', () => {
  217. // Manually set an rtx ssrc mapping in the cache
  218. // Call modifyRtxSsrcs
  219. // -->The rtx ssrc used should be the one we set
  220. const rtxMapping = new Map();
  221. primaryVideoSsrcs.forEach(ssrc => {
  222. rtxMapping.set(ssrc, SDPUtil.generateSsrc());
  223. });
  224. rtxModifier.setSsrcCache(rtxMapping);
  225. const newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(multipleVideoSdp));
  226. const newSdp = transform.parse(newSdpStr);
  227. const fidGroups = getVideoGroups(newSdp, 'FID');
  228. fidGroups.forEach(fidGroup => {
  229. const fidSsrcs = SDPUtil.parseGroupSsrcs(fidGroup);
  230. const fidGroupPrimarySsrc = fidSsrcs[0];
  231. const fidGroupRtxSsrc = fidSsrcs[1];
  232. expect(rtxMapping.has(fidGroupPrimarySsrc)).toBe(true);
  233. expect(rtxMapping.get(fidGroupPrimarySsrc)).toEqual(fidGroupRtxSsrc);
  234. });
  235. });
  236. });
  237. describe('when given an sdp with a flexfec stream', () => {
  238. it('should not add rtx for the flexfec ssrc', () => {
  239. const flexFecSdp = SampleSdpStrings.flexFecSdp;
  240. const newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(flexFecSdp));
  241. const newSdp = transform.parse(newSdpStr);
  242. const fidGroups = getVideoGroups(newSdp, 'FID');
  243. expect(fidGroups.length).toEqual(1);
  244. });
  245. });
  246. describe('(corner cases)', () => {
  247. it('should handle a recvonly video mline', () => {
  248. const sdp = SampleSdpStrings.plainVideoSdp;
  249. const videoMLine = sdp.media.find(m => m.type === 'video');
  250. videoMLine.direction = 'recvonly';
  251. const newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(sdp));
  252. expect(newSdpStr).toEqual(transform.write(sdp));
  253. });
  254. it('should handle a video mline with no video ssrcs', () => {
  255. const sdp = SampleSdpStrings.plainVideoSdp;
  256. const videoMLine = sdp.media.find(m => m.type === 'video');
  257. videoMLine.ssrcs = [];
  258. const newSdpStr = rtxModifier.modifyRtxSsrcs(transform.write(sdp));
  259. expect(newSdpStr).toEqual(transform.write(sdp));
  260. });
  261. });
  262. });
  263. describe('stripRtx', () => {
  264. beforeEach(() => { }); // eslint-disable-line no-empty-function
  265. it('should strip all rtx streams from an sdp with rtx', () => {
  266. const sdpStr = transform.write(SampleSdpStrings.rtxVideoSdp);
  267. const newSdpStr = rtxModifier.stripRtx(sdpStr);
  268. const newSdp = transform.parse(newSdpStr);
  269. const fidGroups = getVideoGroups(newSdp, 'FID');
  270. expect(fidGroups.length).toEqual(0);
  271. expect(numVideoSsrcs(newSdp)).toEqual(1);
  272. });
  273. it('should do nothing to an sdp with no rtx', () => {
  274. const sdpStr = transform.write(SampleSdpStrings.plainVideoSdp);
  275. const newSdpStr = rtxModifier.stripRtx(sdpStr);
  276. expect(newSdpStr).toEqual(sdpStr);
  277. });
  278. });
  279. });
  280. /* eslint-enable max-len*/