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.

CodecSelection.spec.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import { MockPeerConnection, MockRTC } from '../RTC/MockClasses';
  2. import { nextTick } from '../util/TestUtils';
  3. import JingleSessionPC from '../xmpp/JingleSessionPC';
  4. import { MockChatRoom, MockStropheConnection } from '../xmpp/MockClasses';
  5. import { MockConference, MockLocalTrack, MockParticipant } from './MockClasses';
  6. import { FixedSizeArray, QualityController } from './QualityController';
  7. describe('Codec Selection', () => {
  8. let qualityController;
  9. let conference;
  10. let connection;
  11. let jingleSession;
  12. let options;
  13. let participant1, participant2, participant3;
  14. let rtc;
  15. const SID = 'sid12345';
  16. let tpc;
  17. beforeEach(() => {
  18. rtc = new MockRTC();
  19. conference = new MockConference(rtc);
  20. connection = new MockStropheConnection();
  21. jingleSession = new JingleSessionPC(
  22. SID,
  23. 'peer1',
  24. 'peer2',
  25. connection,
  26. { },
  27. { },
  28. false,
  29. false);
  30. jingleSession.initialize(
  31. /* ChatRoom */ new MockChatRoom(),
  32. /* RTC */ rtc,
  33. /* Signaling layer */ conference._signalingLayer,
  34. /* options */ { });
  35. conference.jvbJingleSession = jingleSession;
  36. });
  37. describe('when codec preference list is used in config.js', () => {
  38. beforeEach(() => {
  39. options = {
  40. jvb: {
  41. preferenceOrder: [ 'VP9', 'VP8', 'H264' ],
  42. screenshareCodec: 'VP9'
  43. },
  44. p2p: {}
  45. };
  46. qualityController = new QualityController(conference, options);
  47. jasmine.clock().install();
  48. spyOn(jingleSession, 'setVideoCodecs');
  49. });
  50. afterEach(() => {
  51. jasmine.clock().uninstall();
  52. });
  53. it('and remote endpoints use the new codec selection logic', async () => {
  54. // Add a second user joining the call.
  55. participant1 = new MockParticipant('remote-1');
  56. conference.addParticipant(participant1, [ 'vp9', 'vp8' ]);
  57. await nextTick(1000);
  58. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
  59. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'vp8' ], 'vp9');
  60. // Add a third user joining the call with a subset of codecs.
  61. participant2 = new MockParticipant('remote-2');
  62. conference.addParticipant(participant2, [ 'vp8' ]);
  63. // Make p2 leave the call.
  64. conference.removeParticipant(participant2);
  65. await nextTick(1000);
  66. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(2);
  67. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'vp8' ], 'vp9');
  68. });
  69. it('and remote endpoints use the old codec selection logic (RN)', async () => {
  70. // Add a second user joining the call.
  71. participant1 = new MockParticipant('remote-1');
  72. conference.addParticipant(participant1, null, 'vp8');
  73. await nextTick(1000);
  74. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
  75. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8' ], 'vp9');
  76. // Add a third user (newer) to the call.
  77. participant2 = new MockParticipant('remote-2');
  78. conference.addParticipant(participant2, [ 'vp9', 'vp8' ]);
  79. // Make p1 leave the call
  80. conference.removeParticipant(participant1);
  81. await nextTick(1000);
  82. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(2);
  83. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'vp8' ], 'vp9');
  84. });
  85. });
  86. describe('when deprecated configs are used in config.js', () => {
  87. beforeEach(() => {
  88. options = {
  89. jvb: {
  90. preferredCodec: 'VP9',
  91. disabledCodec: 'H264'
  92. },
  93. p2p: {}
  94. };
  95. qualityController = new QualityController(conference, options);
  96. spyOn(jingleSession, 'setVideoCodecs');
  97. jasmine.clock().install();
  98. });
  99. afterEach(() => {
  100. jasmine.clock().uninstall();
  101. });
  102. it('and remote endpoints use the new codec selection logic', async () => {
  103. // Add a second user joining the call.
  104. participant1 = new MockParticipant('remote-1');
  105. conference.addParticipant(participant1, [ 'vp9', 'vp8', 'h264' ]);
  106. await nextTick(1000);
  107. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
  108. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'vp8' ], undefined);
  109. // Add a third user joining the call with a subset of codecs.
  110. participant2 = new MockParticipant('remote-2');
  111. conference.addParticipant(participant2, [ 'vp8' ]);
  112. // Make p2 leave the call
  113. conference.removeParticipant(participant2);
  114. await nextTick(1000);
  115. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(2);
  116. });
  117. it('and remote endpoint prefers a codec that is locally disabled', async () => {
  118. // Add a second user joining the call the prefers H.264 and VP8.
  119. participant1 = new MockParticipant('remote-1');
  120. conference.addParticipant(participant1, [ 'h264', 'vp8' ]);
  121. await nextTick(1200);
  122. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8' ], undefined);
  123. });
  124. it('and remote endpoints use the old codec selection logic (RN)', async () => {
  125. // Add a second user joining the call.
  126. participant1 = new MockParticipant('remote-1');
  127. conference.addParticipant(participant1, null, 'vp8');
  128. await nextTick(1000);
  129. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
  130. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8' ], undefined);
  131. // Add a third user (newer) to the call.
  132. participant2 = new MockParticipant('remote-2');
  133. conference.addParticipant(participant2, [ 'vp9', 'vp8', 'h264' ]);
  134. // Make p1 leave the call
  135. conference.removeParticipant(participant1);
  136. jasmine.clock().tick(1000);
  137. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(2);
  138. });
  139. });
  140. describe('when codec switching is triggered based on outbound-rtp stats', () => {
  141. beforeEach(() => {
  142. options = {
  143. jvb: {
  144. preferenceOrder: [ 'AV1', 'VP9', 'VP8' ]
  145. },
  146. p2p: {}
  147. };
  148. jasmine.clock().install();
  149. qualityController = new QualityController(conference, options);
  150. spyOn(jingleSession, 'setVideoCodecs');
  151. });
  152. afterEach(() => {
  153. jasmine.clock().uninstall();
  154. });
  155. it('and encode resolution is limited by cpu for camera tracks', async () => {
  156. const localTrack = new MockLocalTrack('1', 720, 'camera');
  157. participant1 = new MockParticipant('remote-1');
  158. conference.addParticipant(participant1, [ 'av1', 'vp9', 'vp8' ]);
  159. await nextTick(1000);
  160. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'av1', 'vp9', 'vp8' ], undefined);
  161. participant2 = new MockParticipant('remote-2');
  162. conference.addParticipant(participant2, [ 'av1', 'vp9', 'vp8' ]);
  163. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
  164. qualityController.codecController.changeCodecPreferenceOrder(localTrack, 'av1');
  165. await nextTick(121000);
  166. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'av1', 'vp8' ], undefined);
  167. participant3 = new MockParticipant('remote-3');
  168. conference.addParticipant(participant3, [ 'av1', 'vp9', 'vp8' ]);
  169. // Expect the local endpoint to continue sending VP9.
  170. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'av1', 'vp8' ], undefined);
  171. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(3);
  172. });
  173. it('and does not change codec if the current codec is already the lowest complexity codec', async () => {
  174. const localTrack = new MockLocalTrack('1', 720, 'camera');
  175. qualityController.codecController.codecPreferenceOrder.jvb = [ 'vp8', 'vp9', 'av1' ];
  176. participant1 = new MockParticipant('remote-1');
  177. conference.addParticipant(participant1, [ 'av1', 'vp9', 'vp8' ]);
  178. await nextTick(1000);
  179. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8', 'vp9', 'av1' ], undefined);
  180. participant2 = new MockParticipant('remote-2');
  181. conference.addParticipant(participant2, [ 'av1', 'vp9', 'vp8' ]);
  182. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8', 'vp9', 'av1' ], undefined);
  183. qualityController.codecController.changeCodecPreferenceOrder(localTrack, 'vp8');
  184. await nextTick(121000);
  185. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8', 'vp9', 'av1' ], undefined);
  186. participant3 = new MockParticipant('remote-3');
  187. conference.addParticipant(participant3, [ 'av1', 'vp9', 'vp8' ]);
  188. // Expect the local endpoint to continue sending VP9.
  189. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8', 'vp9', 'av1' ], undefined);
  190. });
  191. });
  192. describe('when codec switching should be triggered based on outbound-rtp stats', () => {
  193. beforeEach(() => {
  194. options = {
  195. enableAdaptiveMode: true,
  196. jvb: {
  197. preferenceOrder: [ 'AV1', 'VP9', 'VP8' ]
  198. },
  199. p2p: {}
  200. };
  201. jasmine.clock().install();
  202. tpc = new MockPeerConnection();
  203. qualityController = new QualityController(conference, options);
  204. spyOn(jingleSession, 'setVideoCodecs');
  205. });
  206. afterEach(() => {
  207. jasmine.clock().uninstall();
  208. });
  209. it('and encode resolution is limited by cpu for camera tracks', async () => {
  210. const localTrack = new MockLocalTrack('1', 720, 'camera');
  211. participant1 = new MockParticipant('remote-1');
  212. conference.addParticipant(participant1, [ 'av1', 'vp9', 'vp8' ]);
  213. await nextTick(1000);
  214. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
  215. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'av1', 'vp9', 'vp8' ], undefined);
  216. participant2 = new MockParticipant('remote-2');
  217. conference.addParticipant(participant2, [ 'av1', 'vp9', 'vp8' ]);
  218. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
  219. const sourceStats = {
  220. avgEncodeTime: 12,
  221. codec: 'AV1',
  222. encodeResolution: 360,
  223. qualityLimitationReason: 'cpu',
  224. localTrack,
  225. timestamp: 1,
  226. tpc
  227. };
  228. qualityController._encodeTimeStats = new Map();
  229. const data = new FixedSizeArray(10);
  230. data.add(sourceStats);
  231. qualityController._encodeTimeStats.set(localTrack.rtcId, data);
  232. qualityController._performQualityOptimizations(sourceStats);
  233. await nextTick(60000);
  234. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'av1', 'vp8' ], undefined);
  235. participant3 = new MockParticipant('remote-3');
  236. conference.addParticipant(participant3, [ 'av1', 'vp9', 'vp8' ]);
  237. await nextTick(1000);
  238. // Expect the local endpoint to continue sending VP9.
  239. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp9', 'av1', 'vp8' ], undefined);
  240. // If the cpu limitation continues to exist, client should switch to vp8.
  241. const updatedStats = {
  242. avgEncodeTime: 12,
  243. codec: 'VP9',
  244. encodeResolution: 360,
  245. qualityLimitationReason: 'cpu',
  246. localTrack,
  247. timestamp: 1,
  248. tpc
  249. };
  250. data.add(updatedStats);
  251. qualityController._performQualityOptimizations(updatedStats);
  252. await nextTick(60000);
  253. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'vp8', 'vp9', 'av1' ], undefined);
  254. });
  255. });
  256. describe('When codec switching should not be triggered based on outbound-rtp stats', () => {
  257. beforeEach(() => {
  258. options = {
  259. enableAdaptiveMode: false,
  260. jvb: {
  261. preferenceOrder: [ 'AV1', 'VP9', 'VP8' ]
  262. },
  263. p2p: {}
  264. };
  265. jasmine.clock().install();
  266. tpc = new MockPeerConnection();
  267. qualityController = new QualityController(conference, options);
  268. spyOn(jingleSession, 'setVideoCodecs');
  269. });
  270. afterEach(() => {
  271. jasmine.clock().uninstall();
  272. });
  273. it('and the client encounters cpu limitation with high complexity codec', async () => {
  274. const localTrack = new MockLocalTrack('1', 720, 'camera');
  275. const sourceStats = {
  276. avgEncodeTime: 12,
  277. codec: 'AV1',
  278. encodeResolution: 360,
  279. qualityLimitationReason: 'cpu',
  280. localTrack,
  281. timestamp: 1,
  282. tpc
  283. };
  284. qualityController._performQualityOptimizations(sourceStats);
  285. await nextTick(60000);
  286. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(0);
  287. });
  288. });
  289. describe('When multiple joins and leaves happen in a quick burst', () => {
  290. beforeEach(() => {
  291. options = {
  292. jvb: {
  293. preferenceOrder: [ 'AV1', 'VP9', 'VP8' ],
  294. screenshareCodec: 'VP9'
  295. },
  296. p2p: {}
  297. };
  298. jasmine.clock().install();
  299. qualityController = new QualityController(conference, options);
  300. spyOn(jingleSession, 'setVideoCodecs');
  301. });
  302. afterEach(() => {
  303. jasmine.clock().uninstall();
  304. });
  305. it('should call setVideoCodecs only once within the same tick', async () => {
  306. participant1 = new MockParticipant('remote-1');
  307. conference.addParticipant(participant1, [ 'vp9', 'vp8' ]);
  308. // Add a third user joining the call with a subset of codecs.
  309. participant2 = new MockParticipant('remote-2');
  310. conference.addParticipant(participant2, [ 'vp8' ]);
  311. // Make p1 and p2 leave the call.
  312. conference.removeParticipant(participant2);
  313. conference.removeParticipant(participant1);
  314. await nextTick(1000);
  315. expect(jingleSession.setVideoCodecs).toHaveBeenCalledTimes(1);
  316. expect(jingleSession.setVideoCodecs).toHaveBeenCalledWith([ 'av1', 'vp9', 'vp8' ], 'vp9');
  317. });
  318. });
  319. });