@webex/plugin-meetings 3.0.0-stream-classes.4 → 3.0.0-test.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +12 -0
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/common/errors/no-meeting-info.js +51 -0
  5. package/dist/common/errors/no-meeting-info.js.map +1 -0
  6. package/dist/common/errors/reclaim-host-role-errors.js +158 -0
  7. package/dist/common/errors/reclaim-host-role-errors.js.map +1 -0
  8. package/dist/common/errors/webex-errors.js +23 -3
  9. package/dist/common/errors/webex-errors.js.map +1 -1
  10. package/dist/common/logs/request.js +5 -1
  11. package/dist/common/logs/request.js.map +1 -1
  12. package/dist/config.js +1 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/constants.js +69 -9
  15. package/dist/constants.js.map +1 -1
  16. package/dist/index.js +11 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/interceptors/index.js +15 -0
  19. package/dist/interceptors/index.js.map +1 -0
  20. package/dist/interceptors/locusRetry.js +93 -0
  21. package/dist/interceptors/locusRetry.js.map +1 -0
  22. package/dist/interpretation/index.js +16 -2
  23. package/dist/interpretation/index.js.map +1 -1
  24. package/dist/interpretation/siLanguage.js +1 -1
  25. package/dist/locus-info/index.js +40 -11
  26. package/dist/locus-info/index.js.map +1 -1
  27. package/dist/locus-info/mediaSharesUtils.js +15 -1
  28. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  29. package/dist/locus-info/parser.js +42 -21
  30. package/dist/locus-info/parser.js.map +1 -1
  31. package/dist/media/index.js +10 -6
  32. package/dist/media/index.js.map +1 -1
  33. package/dist/media/properties.js +13 -3
  34. package/dist/media/properties.js.map +1 -1
  35. package/dist/mediaQualityMetrics/config.js +135 -330
  36. package/dist/mediaQualityMetrics/config.js.map +1 -1
  37. package/dist/meeting/in-meeting-actions.js +4 -0
  38. package/dist/meeting/in-meeting-actions.js.map +1 -1
  39. package/dist/meeting/index.js +2187 -1074
  40. package/dist/meeting/index.js.map +1 -1
  41. package/dist/meeting/muteState.js +37 -25
  42. package/dist/meeting/muteState.js.map +1 -1
  43. package/dist/meeting/request.js +34 -19
  44. package/dist/meeting/request.js.map +1 -1
  45. package/dist/meeting/util.js +71 -0
  46. package/dist/meeting/util.js.map +1 -1
  47. package/dist/meeting-info/index.js +48 -23
  48. package/dist/meeting-info/index.js.map +1 -1
  49. package/dist/meeting-info/meeting-info-v2.js +25 -4
  50. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  51. package/dist/meeting-info/utilv2.js +1 -1
  52. package/dist/meeting-info/utilv2.js.map +1 -1
  53. package/dist/meetings/collection.js +17 -0
  54. package/dist/meetings/collection.js.map +1 -1
  55. package/dist/meetings/index.js +142 -57
  56. package/dist/meetings/index.js.map +1 -1
  57. package/dist/meetings/util.js +2 -6
  58. package/dist/meetings/util.js.map +1 -1
  59. package/dist/member/index.js +9 -0
  60. package/dist/member/index.js.map +1 -1
  61. package/dist/member/util.js +11 -0
  62. package/dist/member/util.js.map +1 -1
  63. package/dist/members/index.js +17 -1
  64. package/dist/members/index.js.map +1 -1
  65. package/dist/members/types.js.map +1 -1
  66. package/dist/members/util.js +15 -4
  67. package/dist/members/util.js.map +1 -1
  68. package/dist/metrics/constants.js +15 -1
  69. package/dist/metrics/constants.js.map +1 -1
  70. package/dist/multistream/mediaRequestManager.js +1 -1
  71. package/dist/multistream/mediaRequestManager.js.map +1 -1
  72. package/dist/multistream/remoteMediaGroup.js +16 -2
  73. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  74. package/dist/multistream/remoteMediaManager.js +222 -73
  75. package/dist/multistream/remoteMediaManager.js.map +1 -1
  76. package/dist/multistream/sendSlotManager.js +22 -0
  77. package/dist/multistream/sendSlotManager.js.map +1 -1
  78. package/dist/reachability/clusterReachability.js +356 -0
  79. package/dist/reachability/clusterReachability.js.map +1 -0
  80. package/dist/reachability/index.js +262 -432
  81. package/dist/reachability/index.js.map +1 -1
  82. package/dist/reachability/request.js +1 -1
  83. package/dist/reachability/request.js.map +1 -1
  84. package/dist/reachability/util.js +29 -0
  85. package/dist/reachability/util.js.map +1 -0
  86. package/dist/reconnection-manager/index.js +113 -96
  87. package/dist/reconnection-manager/index.js.map +1 -1
  88. package/dist/roap/index.js +57 -25
  89. package/dist/roap/index.js.map +1 -1
  90. package/dist/roap/request.js +5 -13
  91. package/dist/roap/request.js.map +1 -1
  92. package/dist/roap/turnDiscovery.js +173 -81
  93. package/dist/roap/turnDiscovery.js.map +1 -1
  94. package/dist/rtcMetrics/index.js +68 -6
  95. package/dist/rtcMetrics/index.js.map +1 -1
  96. package/dist/statsAnalyzer/index.js +338 -289
  97. package/dist/statsAnalyzer/index.js.map +1 -1
  98. package/dist/statsAnalyzer/mqaUtil.js +296 -156
  99. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  100. package/dist/types/common/errors/no-meeting-info.d.ts +14 -0
  101. package/dist/types/common/errors/reclaim-host-role-errors.d.ts +60 -0
  102. package/dist/types/common/errors/webex-errors.d.ts +13 -1
  103. package/dist/types/common/logs/request.d.ts +2 -0
  104. package/dist/types/config.d.ts +1 -1
  105. package/dist/types/constants.d.ts +66 -13
  106. package/dist/types/index.d.ts +1 -1
  107. package/dist/types/interceptors/index.d.ts +2 -0
  108. package/dist/types/interceptors/locusRetry.d.ts +27 -0
  109. package/dist/types/locus-info/index.d.ts +1 -1
  110. package/dist/types/locus-info/parser.d.ts +3 -2
  111. package/dist/types/mediaQualityMetrics/config.d.ts +99 -223
  112. package/dist/types/meeting/in-meeting-actions.d.ts +4 -0
  113. package/dist/types/meeting/index.d.ts +285 -34
  114. package/dist/types/meeting/locusMediaRequest.d.ts +1 -2
  115. package/dist/types/meeting/muteState.d.ts +2 -8
  116. package/dist/types/meeting/request.d.ts +4 -1
  117. package/dist/types/meeting/util.d.ts +25 -1
  118. package/dist/types/meeting-info/index.d.ts +7 -0
  119. package/dist/types/meeting-info/meeting-info-v2.d.ts +1 -0
  120. package/dist/types/meetings/collection.d.ts +9 -0
  121. package/dist/types/meetings/index.d.ts +42 -14
  122. package/dist/types/member/index.d.ts +1 -0
  123. package/dist/types/members/types.d.ts +1 -0
  124. package/dist/types/members/util.d.ts +5 -0
  125. package/dist/types/metrics/constants.d.ts +15 -0
  126. package/dist/types/multistream/mediaRequestManager.d.ts +2 -0
  127. package/dist/types/multistream/remoteMediaGroup.d.ts +2 -0
  128. package/dist/types/multistream/remoteMediaManager.d.ts +25 -1
  129. package/dist/types/multistream/sendSlotManager.d.ts +9 -0
  130. package/dist/types/reachability/clusterReachability.d.ts +109 -0
  131. package/dist/types/reachability/index.d.ts +59 -112
  132. package/dist/types/reachability/request.d.ts +1 -1
  133. package/dist/types/reachability/util.d.ts +8 -0
  134. package/dist/types/reconnection-manager/index.d.ts +10 -0
  135. package/dist/types/roap/index.d.ts +2 -1
  136. package/dist/types/roap/request.d.ts +2 -1
  137. package/dist/types/roap/turnDiscovery.d.ts +21 -4
  138. package/dist/types/rtcMetrics/index.d.ts +15 -1
  139. package/dist/types/statsAnalyzer/index.d.ts +28 -11
  140. package/dist/types/statsAnalyzer/mqaUtil.d.ts +28 -4
  141. package/dist/types/webinar/collection.d.ts +16 -0
  142. package/dist/types/webinar/index.d.ts +5 -0
  143. package/dist/webinar/collection.js +44 -0
  144. package/dist/webinar/collection.js.map +1 -0
  145. package/dist/webinar/index.js +69 -0
  146. package/dist/webinar/index.js.map +1 -0
  147. package/package.json +3 -2
  148. package/src/common/errors/no-meeting-info.ts +24 -0
  149. package/src/common/errors/reclaim-host-role-errors.ts +134 -0
  150. package/src/common/errors/webex-errors.ts +19 -2
  151. package/src/common/logs/request.ts +5 -1
  152. package/src/config.ts +1 -1
  153. package/src/constants.ts +71 -6
  154. package/src/index.ts +5 -0
  155. package/src/interceptors/index.ts +3 -0
  156. package/src/interceptors/locusRetry.ts +67 -0
  157. package/src/interpretation/index.ts +18 -1
  158. package/src/locus-info/index.ts +52 -16
  159. package/src/locus-info/mediaSharesUtils.ts +16 -0
  160. package/src/locus-info/parser.ts +47 -21
  161. package/src/media/index.ts +8 -6
  162. package/src/media/properties.ts +17 -2
  163. package/src/mediaQualityMetrics/config.ts +103 -238
  164. package/src/meeting/in-meeting-actions.ts +8 -0
  165. package/src/meeting/index.ts +1510 -529
  166. package/src/meeting/muteState.ts +34 -20
  167. package/src/meeting/request.ts +19 -1
  168. package/src/meeting/util.ts +97 -0
  169. package/src/meeting-info/index.ts +47 -20
  170. package/src/meeting-info/meeting-info-v2.ts +27 -5
  171. package/src/meeting-info/utilv2.ts +1 -1
  172. package/src/meetings/collection.ts +13 -0
  173. package/src/meetings/index.ts +112 -31
  174. package/src/meetings/util.ts +2 -8
  175. package/src/member/index.ts +9 -0
  176. package/src/member/util.ts +14 -0
  177. package/src/members/index.ts +29 -2
  178. package/src/members/types.ts +1 -0
  179. package/src/members/util.ts +15 -1
  180. package/src/metrics/constants.ts +14 -0
  181. package/src/multistream/mediaRequestManager.ts +4 -1
  182. package/src/multistream/remoteMediaGroup.ts +19 -0
  183. package/src/multistream/remoteMediaManager.ts +141 -18
  184. package/src/multistream/sendSlotManager.ts +29 -0
  185. package/src/reachability/clusterReachability.ts +320 -0
  186. package/src/reachability/index.ts +221 -382
  187. package/src/reachability/request.ts +1 -1
  188. package/src/reachability/util.ts +24 -0
  189. package/src/reconnection-manager/index.ts +87 -83
  190. package/src/roap/index.ts +60 -24
  191. package/src/roap/request.ts +3 -16
  192. package/src/roap/turnDiscovery.ts +112 -39
  193. package/src/rtcMetrics/index.ts +71 -5
  194. package/src/statsAnalyzer/index.ts +430 -427
  195. package/src/statsAnalyzer/mqaUtil.ts +317 -168
  196. package/src/webinar/collection.ts +31 -0
  197. package/src/webinar/index.ts +62 -0
  198. package/test/integration/spec/converged-space-meetings.js +7 -7
  199. package/test/integration/spec/journey.js +86 -104
  200. package/test/integration/spec/space-meeting.js +9 -9
  201. package/test/unit/spec/interceptors/locusRetry.ts +131 -0
  202. package/test/unit/spec/interpretation/index.ts +36 -3
  203. package/test/unit/spec/locus-info/index.js +205 -12
  204. package/test/unit/spec/locus-info/lib/SeqCmp.json +16 -0
  205. package/test/unit/spec/locus-info/mediaSharesUtils.ts +10 -0
  206. package/test/unit/spec/locus-info/parser.js +54 -13
  207. package/test/unit/spec/media/index.ts +20 -4
  208. package/test/unit/spec/media/properties.ts +2 -2
  209. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  210. package/test/unit/spec/meeting/index.js +4027 -1075
  211. package/test/unit/spec/meeting/muteState.js +219 -67
  212. package/test/unit/spec/meeting/request.js +63 -12
  213. package/test/unit/spec/meeting/utils.js +93 -0
  214. package/test/unit/spec/meeting-info/index.js +180 -61
  215. package/test/unit/spec/meeting-info/meetinginfov2.js +196 -53
  216. package/test/unit/spec/meetings/collection.js +12 -0
  217. package/test/unit/spec/meetings/index.js +619 -206
  218. package/test/unit/spec/meetings/utils.js +35 -12
  219. package/test/unit/spec/member/index.js +8 -7
  220. package/test/unit/spec/member/util.js +32 -0
  221. package/test/unit/spec/members/index.js +130 -17
  222. package/test/unit/spec/members/utils.js +26 -0
  223. package/test/unit/spec/multistream/mediaRequestManager.ts +20 -2
  224. package/test/unit/spec/multistream/remoteMediaGroup.ts +80 -1
  225. package/test/unit/spec/multistream/remoteMediaManager.ts +210 -3
  226. package/test/unit/spec/multistream/sendSlotManager.ts +50 -18
  227. package/test/unit/spec/reachability/clusterReachability.ts +279 -0
  228. package/test/unit/spec/reachability/index.ts +505 -135
  229. package/test/unit/spec/reachability/util.ts +40 -0
  230. package/test/unit/spec/reconnection-manager/index.js +74 -17
  231. package/test/unit/spec/roap/index.ts +181 -61
  232. package/test/unit/spec/roap/request.ts +27 -3
  233. package/test/unit/spec/roap/turnDiscovery.ts +362 -101
  234. package/test/unit/spec/rtcMetrics/index.ts +57 -3
  235. package/test/unit/spec/stats-analyzer/index.js +1225 -12
  236. package/test/unit/spec/webinar/collection.ts +13 -0
  237. package/test/unit/spec/webinar/index.ts +60 -0
  238. package/test/utils/integrationTestUtils.js +4 -4
  239. package/test/utils/webex-test-users.js +12 -4
@@ -18,6 +18,7 @@ import testUtils from '../../../utils/testUtils';
18
18
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
19
19
  import LoggerConfig from '@webex/plugin-meetings/src/common/logs/logger-config';
20
20
  import { expect } from 'chai';
21
+ import { NamedMediaGroup } from "@webex/json-multistream";
21
22
 
22
23
  class FakeSlot extends EventEmitter {
23
24
  public mediaType: MediaType;
@@ -299,6 +300,204 @@ describe('RemoteMediaManager', () => {
299
300
  );
300
301
  });
301
302
 
303
+ it('creates a RemoteMediaGroup for named media group audio correctly', async () => {
304
+ let createdInterpretationAudioGroup: RemoteMediaGroup | null = null;
305
+ // create a config with just audio, no video at all and no screen share
306
+ const config: Configuration = {
307
+ audio: {
308
+ numOfActiveSpeakerStreams: 3,
309
+ numOfScreenShareStreams: 0,
310
+ },
311
+ video: {
312
+ preferLiveVideo: false,
313
+ initialLayoutId: 'empty',
314
+ layouts: {
315
+ empty: {},
316
+ },
317
+ },
318
+ namedMediaGroup: {type: 1, value: 20},
319
+ };
320
+
321
+ remoteMediaManager = new RemoteMediaManager(
322
+ fakeReceiveSlotManager,
323
+ fakeMediaRequestManagers,
324
+ config
325
+ );
326
+
327
+ remoteMediaManager.on(Event.InterpretationAudioCreated, (audio: RemoteMediaGroup) => {
328
+ createdInterpretationAudioGroup = audio;
329
+ });
330
+
331
+ remoteMediaManager.start();
332
+
333
+ await testUtils.flushPromises();
334
+
335
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 4);
336
+ assert.alwaysCalledWith(fakeReceiveSlotManager.allocateSlot, MediaType.AudioMain);
337
+
338
+ assert.isNotNull(createdInterpretationAudioGroup);
339
+ if (createdInterpretationAudioGroup) {
340
+ assert.strictEqual(createdInterpretationAudioGroup.getRemoteMedia().length, 1);
341
+ assert.isTrue(
342
+ createdInterpretationAudioGroup
343
+ .getRemoteMedia()
344
+ .every((remoteMedia) => remoteMedia.mediaType === MediaType.AudioMain)
345
+ );
346
+ assert.strictEqual(createdInterpretationAudioGroup.getRemoteMedia('pinned').length, 0);
347
+ assert.calledTwice(fakeMediaRequestManagers.audio.addRequest);
348
+ assert.calledWith(
349
+ fakeMediaRequestManagers.audio.addRequest,
350
+ sinon.match({
351
+ policyInfo: sinon.match({
352
+ policy: 'active-speaker',
353
+ priority: 255,
354
+ namedMediaGroups: sinon.match([{type: 1, value: 20}]),
355
+ }),
356
+ receiveSlots: Array(1).fill(fakeAudioSlot),
357
+ codecInfo: undefined,
358
+ }),
359
+ false,
360
+ );
361
+ }
362
+ });
363
+
364
+ it('creates new media request when call setReceiveNamedMediaGroup', async () => {
365
+ let createdInterpretationAudioGroup: RemoteMediaGroup | null = null;
366
+ // create a config with just audio, no video at all and no screen share
367
+ const config: Configuration = {
368
+ audio: {
369
+ numOfActiveSpeakerStreams: 3,
370
+ numOfScreenShareStreams: 0,
371
+ },
372
+ video: {
373
+ preferLiveVideo: false,
374
+ initialLayoutId: 'empty',
375
+ layouts: {
376
+ empty: {},
377
+ },
378
+ },
379
+ namedMediaGroup: {type: 1, value: 24},
380
+ };
381
+
382
+ remoteMediaManager = new RemoteMediaManager(
383
+ fakeReceiveSlotManager,
384
+ fakeMediaRequestManagers,
385
+ config
386
+ );
387
+
388
+ remoteMediaManager.on(Event.InterpretationAudioCreated, (audio: RemoteMediaGroup) => {
389
+ createdInterpretationAudioGroup = audio;
390
+ createdInterpretationAudioGroup.setNamedMediaGroup = sinon.stub();
391
+ });
392
+
393
+ await remoteMediaManager.start();
394
+
395
+ // requires 3 main audio slots and one interpretation audio slot
396
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 4);
397
+
398
+
399
+ resetHistory();
400
+
401
+
402
+ remoteMediaManager.setReceiveNamedMediaGroup(MediaType.AudioMain, 28);
403
+
404
+ // check that setNamedMediaGroup has been called
405
+ assert.calledOnce(createdInterpretationAudioGroup.setNamedMediaGroup);
406
+ assert.calledWith(
407
+ createdInterpretationAudioGroup.setNamedMediaGroup,
408
+ {type: 1, value: 28},
409
+ true,
410
+ );
411
+
412
+ });
413
+
414
+ it('ignore duplicated group when call setReceiveNamedMediaGroup', async () => {
415
+ let createdAudioGroup: RemoteMediaGroup | null = null;
416
+ let audioStopStub;
417
+ // create a config with just audio, no video at all and no screen share
418
+ const config: Configuration = {
419
+ audio: {
420
+ numOfActiveSpeakerStreams: 3,
421
+ numOfScreenShareStreams: 0,
422
+ },
423
+ video: {
424
+ preferLiveVideo: false,
425
+ initialLayoutId: 'empty',
426
+ layouts: {
427
+ empty: {},
428
+ },
429
+ },
430
+ namedMediaGroup: {type: 1, value: 24},
431
+ };
432
+
433
+ remoteMediaManager = new RemoteMediaManager(
434
+ fakeReceiveSlotManager,
435
+ fakeMediaRequestManagers,
436
+ config
437
+ );
438
+
439
+ remoteMediaManager.on(Event.AudioCreated, (audio: RemoteMediaGroup) => {
440
+ audioStopStub = sinon.stub(audio, 'stop');
441
+ });
442
+
443
+ await remoteMediaManager.start();
444
+
445
+ // we're using the default config that requires 3 main audio slots
446
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 4);
447
+
448
+
449
+ resetHistory();
450
+
451
+ remoteMediaManager.setReceiveNamedMediaGroup(MediaType.AudioMain, 24);
452
+
453
+ assert.notCalled(audioStopStub);
454
+ assert.callCount(fakeReceiveSlotManager.releaseSlot, 0);
455
+
456
+ await testUtils.flushPromises();
457
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 0);
458
+ assert.notCalled(fakeReceiveSlotManager.allocateSlot);
459
+
460
+ });
461
+
462
+
463
+ it('should throw error if set receive named media group which type is not audio', async () => {
464
+ let createdAudioGroup: RemoteMediaGroup | null = null;
465
+ let audioStopStub;
466
+ // create a config with just audio, no video at all and no screen share
467
+ const config: Configuration = {
468
+ audio: {
469
+ numOfActiveSpeakerStreams: 3,
470
+ numOfScreenShareStreams: 0,
471
+ },
472
+ video: {
473
+ preferLiveVideo: false,
474
+ initialLayoutId: 'empty',
475
+ layouts: {
476
+ empty: {},
477
+ },
478
+ },
479
+ namedMediaGroup: {type: 1, value: 24},
480
+ };
481
+
482
+ remoteMediaManager = new RemoteMediaManager(
483
+ fakeReceiveSlotManager,
484
+ fakeMediaRequestManagers,
485
+ config
486
+ );
487
+
488
+ // Assuming setReceiveNamedMediaGroup returns a promise
489
+ it('should throw error when media type is not audio-main', async () => {
490
+ try {
491
+ await remoteMediaManager.setReceiveNamedMediaGroup(MediaType.VideoMain, 0);
492
+ // If the promise resolves successfully, we should fail the test
493
+ throw new Error('Expected an error but none was thrown');
494
+ } catch (error) {
495
+ // Check if the error message matches the expected one
496
+ expect(error.message).to.equal('cannot set receive named media group which media type is not audio-main');
497
+ }
498
+ });
499
+ });
500
+
302
501
  it('pre-allocates receive slots based on the biggest layout', async () => {
303
502
  const config = cloneDeep(DefaultTestConfiguration);
304
503
 
@@ -693,7 +892,7 @@ describe('RemoteMediaManager', () => {
693
892
  });
694
893
 
695
894
  expect(config.video.preferLiveVideo).to.equal(true);
696
-
895
+
697
896
  assert.calledOnce(fakeMediaRequestManagers.video.commit);
698
897
  });
699
898
  });
@@ -747,7 +946,11 @@ describe('RemoteMediaManager', () => {
747
946
 
748
947
  assert.calledWith(
749
948
  logger.log,
750
- 'RemoteMediaManager#updateVideoReceiveSlots --> receive slots updated: unused=0, activeSpeaker=6, receiverSelected=4\ngroup: thumbnails\nfake video slot fake video slot fake video slot fake video slot fake video slot fake video slot\nreceiverSelected:\n stage-1: fake video slot\n stage-2: fake video slot\n stage-3: fake video slot\n stage-4: fake video slot\n'
949
+ 'RemoteMediaManager#setLayout --> new layout selected: Stage'
950
+ );
951
+ assert.calledWith(
952
+ logger.log,
953
+ 'RemoteMediaManager#logMainVideoReceiveSlots --> MAIN VIDEO receive slots: unused=0, activeSpeaker=6, receiverSelected=4\ngroup: thumbnails\nfake video slot, fake video slot, fake video slot, fake video slot, fake video slot, fake video slot\nreceiverSelected:\n stage-1: fake video slot\n stage-2: fake video slot\n stage-3: fake video slot\n stage-4: fake video slot\n'
751
954
  );
752
955
  });
753
956
 
@@ -769,7 +972,11 @@ describe('RemoteMediaManager', () => {
769
972
 
770
973
  assert.calledWith(
771
974
  logger.log,
772
- 'RemoteMediaManager#updateVideoReceiveSlots --> receive slots updated: unused=0, activeSpeaker=9, receiverSelected=0\ngroup: main\nfake video slot fake video slot fake video slot fake video slot fake video slot fake video slot fake video slot fake video slot fake video slot\nreceiverSelected:\n'
975
+ 'RemoteMediaManager#setLayout --> new layout selected: AllEqual'
976
+ );
977
+ assert.calledWith(
978
+ logger.log,
979
+ 'RemoteMediaManager#logMainVideoReceiveSlots --> MAIN VIDEO receive slots: unused=0, activeSpeaker=9, receiverSelected=0\ngroup: main\nfake video slot, fake video slot, fake video slot, fake video slot, fake video slot, fake video slot, fake video slot, fake video slot, fake video slot\nreceiverSelected:\n'
773
980
  );
774
981
  });
775
982
 
@@ -10,11 +10,11 @@ describe('SendSlotsManager', () => {
10
10
  info: sinon.stub(),
11
11
  },
12
12
  };
13
-
13
+
14
14
  beforeEach(() => {
15
15
  sendSlotsManager = new SendSlotManager(LoggerProxy);
16
16
  });
17
-
17
+
18
18
  describe('createSlot', () => {
19
19
  let mediaConnection;
20
20
  const mediaType = MediaType.AudioMain;
@@ -27,19 +27,19 @@ describe('SendSlotsManager', () => {
27
27
 
28
28
  it('should create a slot for the given mediaType', () => {
29
29
  sendSlotsManager.createSlot(mediaConnection, mediaType);
30
-
30
+
31
31
  expect(mediaConnection.createSendSlot.calledWith(mediaType, true));
32
32
  });
33
33
 
34
34
  it('should create a slot for the given mediaType & active state', () => {
35
35
  sendSlotsManager.createSlot(mediaConnection, mediaType, false);
36
-
36
+
37
37
  expect(mediaConnection.createSendSlot.calledWith(mediaType, false));
38
38
  });
39
-
39
+
40
40
  it('should throw an error if a slot for the given mediaType already exists', () => {
41
41
  sendSlotsManager.createSlot(mediaConnection, mediaType);
42
-
42
+
43
43
  expect(() => sendSlotsManager.createSlot(mediaConnection, mediaType)).to.throw(`Slot for ${mediaType} already exists`);
44
44
  });
45
45
  });
@@ -56,7 +56,7 @@ describe('SendSlotsManager', () => {
56
56
 
57
57
  it('should return the slot for the given mediaType', () => {
58
58
  const slot = sendSlotsManager.createSlot(mediaConnection,mediaType);
59
-
59
+
60
60
  expect(sendSlotsManager.getSlot(mediaType)).to.equal(slot);
61
61
  });
62
62
 
@@ -64,7 +64,7 @@ describe('SendSlotsManager', () => {
64
64
  expect(() => sendSlotsManager.getSlot(mediaType)).to.throw(`Slot for ${mediaType} does not exist`);
65
65
  });
66
66
  });
67
-
67
+
68
68
  describe('publishStream', () => {
69
69
  let mediaConnection;
70
70
  const mediaType = MediaType.AudioMain;
@@ -82,9 +82,9 @@ describe('SendSlotsManager', () => {
82
82
  };
83
83
  mediaConnection.createSendSlot.returns(slot);
84
84
  sendSlotsManager.createSlot(mediaConnection, mediaType);
85
-
85
+
86
86
  await sendSlotsManager.publishStream(mediaType, stream);
87
-
87
+
88
88
  expect(slot.publishStream.calledWith(stream));
89
89
  });
90
90
 
@@ -112,9 +112,9 @@ describe('SendSlotsManager', () => {
112
112
  };
113
113
  mediaConnection.createSendSlot.returns(slot);
114
114
  sendSlotsManager.createSlot(mediaConnection, mediaType);
115
-
115
+
116
116
  await sendSlotsManager.unpublishStream(mediaType);
117
-
117
+
118
118
  expect(slot.unpublishStream.called);
119
119
  });
120
120
 
@@ -126,6 +126,38 @@ describe('SendSlotsManager', () => {
126
126
  });
127
127
  });
128
128
 
129
+ describe('setNamedMediaGroups', () => {
130
+ let mediaConnection;
131
+ const mediaType = MediaType.AudioMain;
132
+ const groups = [{type: 1, value: 20}];
133
+
134
+ beforeEach(() => {
135
+ mediaConnection = {
136
+ createSendSlot: sinon.stub(),
137
+ } as MultistreamRoapMediaConnection;
138
+ });
139
+
140
+ it('should publish the given stream to the sendSlot for the given mediaType', async () => {
141
+ const slot = {
142
+ setNamedMediaGroups: sinon.stub().resolves(),
143
+ };
144
+ mediaConnection.createSendSlot.returns(slot);
145
+ sendSlotsManager.createSlot(mediaConnection, mediaType);
146
+
147
+ await sendSlotsManager.setNamedMediaGroups(mediaType, groups);
148
+
149
+ expect(slot.setNamedMediaGroups.calledWith(groups));
150
+ });
151
+
152
+ it('should throw an error if the given mediaType is not audio', () => {
153
+ expect(() => sendSlotsManager.setNamedMediaGroups(MediaType.VideoMain, groups)).to.throw(`sendSlotManager cannot set named media group which media type is ${MediaType.VideoMain}`)
154
+ });
155
+
156
+ it('should throw an error if a slot for the given mediaType does not exist', () => {
157
+ expect(() => sendSlotsManager.setNamedMediaGroups(mediaType, groups)).to.throw(`Slot for ${mediaType} does not exist`)
158
+ });
159
+ });
160
+
129
161
  describe('setActive', () => {
130
162
  let mediaConnection;
131
163
  const mediaType = MediaType.AudioMain;
@@ -142,9 +174,9 @@ describe('SendSlotsManager', () => {
142
174
  };
143
175
  mediaConnection.createSendSlot.returns(slot);
144
176
  sendSlotsManager.createSlot(mediaConnection, mediaType);
145
-
177
+
146
178
  await sendSlotsManager.setActive(mediaType,true);
147
-
179
+
148
180
  expect(slot.setActive.called);
149
181
  });
150
182
 
@@ -170,9 +202,9 @@ describe('SendSlotsManager', () => {
170
202
  };
171
203
  mediaConnection.createSendSlot.returns(slot);
172
204
  sendSlotsManager.createSlot(mediaConnection, mediaType);
173
-
205
+
174
206
  await sendSlotsManager.setCodecParameters(mediaType, codecParameters);
175
-
207
+
176
208
  expect(slot.setCodecParameters.calledWith(codecParameters));
177
209
  });
178
210
 
@@ -200,9 +232,9 @@ describe('SendSlotsManager', () => {
200
232
  };
201
233
  mediaConnection.createSendSlot.returns(slot);
202
234
  sendSlotsManager.createSlot(mediaConnection, mediaType);
203
-
235
+
204
236
  await sendSlotsManager.deleteCodecParameters(mediaType,[]);
205
-
237
+
206
238
  expect(slot.deleteCodecParameters.called);
207
239
  });
208
240
 
@@ -0,0 +1,279 @@
1
+ import {assert} from '@webex/test-helper-chai';
2
+ import MockWebex from '@webex/test-helper-mock-webex';
3
+ import sinon from 'sinon';
4
+ import testUtils from '../../../utils/testUtils';
5
+
6
+ // packages/@webex/plugin-meetings/test/unit/spec/reachability/clusterReachability.ts
7
+ import { ClusterReachability } from '@webex/plugin-meetings/src/reachability/clusterReachability'; // replace with actual path
8
+
9
+ describe('ClusterReachability', () => {
10
+ let previousRTCPeerConnection;
11
+ let clusterReachability;
12
+ let fakePeerConnection;
13
+
14
+ const FAKE_OFFER = {type: 'offer', sdp: 'fake sdp'};
15
+
16
+ beforeEach(() => {
17
+ fakePeerConnection = {
18
+ createOffer: sinon.stub().resolves(FAKE_OFFER),
19
+ setLocalDescription: sinon.stub().resolves(),
20
+ close: sinon.stub(),
21
+ iceGatheringState: 'new',
22
+ };
23
+
24
+ previousRTCPeerConnection = global.RTCPeerConnection;
25
+ global.RTCPeerConnection = sinon.stub().returns(fakePeerConnection);
26
+
27
+ clusterReachability = new ClusterReachability('testName', {
28
+ isVideoMesh: false,
29
+ udp: ['stun:udp1', 'stun:udp2'],
30
+ tcp: ['stun:tcp1.webex.com', 'stun:tcp2.webex.com:5004'],
31
+ xtls: ['xtls1', 'xtls2'],
32
+ });
33
+
34
+ });
35
+
36
+ afterEach(() => {
37
+ global.RTCPeerConnection = previousRTCPeerConnection;
38
+ });
39
+
40
+ it('should create an instance correctly', () => {
41
+ assert.instanceOf(clusterReachability, ClusterReachability);
42
+ assert.equal(clusterReachability.name, 'testName');
43
+ assert.equal(clusterReachability.isVideoMesh, false);
44
+ assert.equal(clusterReachability.numUdpUrls, 2);
45
+ assert.equal(clusterReachability.numTcpUrls, 2);
46
+ });
47
+
48
+ it('should create a peer connection with the right config', () => {
49
+ assert.calledOnceWithExactly(global.RTCPeerConnection, {
50
+ iceServers: [
51
+ {username: '', credential: '', urls: ['stun:udp1']},
52
+ {username: '', credential: '', urls: ['stun:udp2']},
53
+ {username: 'webexturnreachuser', credential: 'webexturnreachpwd', urls: ['turn:tcp1.webex.com?transport=tcp']},
54
+ {username: 'webexturnreachuser', credential: 'webexturnreachpwd', urls: ['turn:tcp2.webex.com:5004?transport=tcp']}
55
+ ],
56
+ iceCandidatePoolSize: 0,
57
+ iceTransportPolicy: 'all',
58
+ });
59
+ });
60
+
61
+ it('should create a peer connection with the right config even if lists of urls are empty', () => {
62
+ (global.RTCPeerConnection as any).resetHistory();
63
+
64
+ clusterReachability = new ClusterReachability('testName', {
65
+ isVideoMesh: false,
66
+ udp: [],
67
+ tcp: [],
68
+ xtls: [],
69
+ });
70
+
71
+ assert.calledOnceWithExactly(global.RTCPeerConnection, {
72
+ iceServers: [],
73
+ iceCandidatePoolSize: 0,
74
+ iceTransportPolicy: 'all',
75
+ });
76
+ });
77
+
78
+ it('returns correct results before start() is called', () => {
79
+ assert.deepEqual(clusterReachability.getResult(), {
80
+ udp: {result: 'untested'},
81
+ tcp: {result: 'untested'},
82
+ xtls: {result: 'untested'}
83
+ });
84
+ });
85
+
86
+ describe('#start', () => {
87
+ let clock;
88
+
89
+ beforeEach(() => {
90
+ clock = sinon.useFakeTimers();
91
+ });
92
+
93
+ afterEach(() => {
94
+ clock.restore();
95
+ })
96
+
97
+ it('should initiate the ICE gathering process', async () => {
98
+ const promise = clusterReachability.start();
99
+
100
+ await testUtils.flushPromises();
101
+
102
+ // check that the right listeners are setup
103
+ assert.isFunction(fakePeerConnection.onicecandidate);
104
+ assert.isFunction(fakePeerConnection.onicegatheringstatechange);
105
+
106
+ // check that the right webrtc APIs are called
107
+ assert.calledOnceWithExactly(fakePeerConnection.createOffer, {offerToReceiveAudio: true});
108
+ assert.calledOnce(fakePeerConnection.setLocalDescription);
109
+
110
+ await clock.tickAsync(3000);// move the clock so that reachability times out
111
+ await promise;
112
+ });
113
+
114
+ it('resolves and has correct result as soon as it finds that both udp and tcp is reachable', async () => {
115
+ const promise = clusterReachability.start();
116
+
117
+ await clock.tickAsync(100);
118
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp'}});
119
+
120
+ await clock.tickAsync(100);
121
+ fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
122
+
123
+ await promise;
124
+
125
+ assert.deepEqual(clusterReachability.getResult(), {
126
+ udp: {result: 'reachable', latencyInMilliseconds: 100, clientMediaIPs: ['somePublicIp']},
127
+ tcp: {result: 'reachable', latencyInMilliseconds: 200},
128
+ xtls: {result: 'untested'}
129
+ });
130
+ });
131
+
132
+ it('times out correctly', async () => {
133
+ const promise = clusterReachability.start();
134
+
135
+ // progress time without any candidates
136
+ await clock.tickAsync(3000);
137
+ await promise;
138
+
139
+ assert.deepEqual(clusterReachability.getResult(), {
140
+ udp: {result: 'unreachable'},
141
+ tcp: {result: 'unreachable'},
142
+ xtls: {result: 'untested'}
143
+ });
144
+ });
145
+
146
+ it('times out correctly for video mesh nodes', async () => {
147
+ clusterReachability = new ClusterReachability('testName', {
148
+ isVideoMesh: true,
149
+ udp: ['stun:udp1', 'stun:udp2'],
150
+ tcp: ['stun:tcp1.webex.com', 'stun:tcp2.webex.com:5004'],
151
+ xtls: ['xtls1', 'xtls2'],
152
+ });
153
+
154
+ const promise = clusterReachability.start();
155
+
156
+ // video mesh nodes have shorter timeout of just 1s
157
+ await clock.tickAsync(1000);
158
+ await promise;
159
+
160
+ assert.deepEqual(clusterReachability.getResult(), {
161
+ udp: {result: 'unreachable'},
162
+ tcp: {result: 'unreachable'},
163
+ xtls: {result: 'untested'}
164
+ });
165
+ });
166
+
167
+ it('resolves when ICE gathering is completed', async () => {
168
+ const promise = clusterReachability.start();
169
+
170
+ await testUtils.flushPromises();
171
+
172
+ fakePeerConnection.iceConnectionState = 'complete';
173
+ fakePeerConnection.onicegatheringstatechange();
174
+ await promise;
175
+
176
+ assert.deepEqual(clusterReachability.getResult(), {
177
+ udp: {result: 'unreachable'},
178
+ tcp: {result: 'unreachable'},
179
+ xtls: {result: 'untested'}
180
+ });
181
+ });
182
+
183
+ it('resolves with the right result when ICE gathering is completed', async () => {
184
+ const promise = clusterReachability.start();
185
+
186
+ // send 1 candidate
187
+ await clock.tickAsync(30);
188
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1'}});
189
+
190
+ fakePeerConnection.iceConnectionState = 'complete';
191
+ fakePeerConnection.onicegatheringstatechange();
192
+ await promise;
193
+
194
+ assert.deepEqual(clusterReachability.getResult(), {
195
+ udp: {result: 'reachable', latencyInMilliseconds: 30, clientMediaIPs: ['somePublicIp1']},
196
+ tcp: {result: 'unreachable'},
197
+ xtls: {result: 'untested'}
198
+ });
199
+ });
200
+
201
+ it('should store latency only for the first srflx candidate, but IPs from all of them', async () => {
202
+ const promise = clusterReachability.start();
203
+
204
+ await clock.tickAsync(10);
205
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1'}});
206
+
207
+ // generate more candidates
208
+ await clock.tickAsync(10);
209
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp2'}});
210
+
211
+ await clock.tickAsync(10);
212
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp3'}});
213
+
214
+ await clock.tickAsync(3000);// move the clock so that reachability times out
215
+
216
+ await promise;
217
+
218
+ // latency should be from only the first candidates, but the clientMediaIps should be from all UDP candidates (not TCP)
219
+ assert.deepEqual(clusterReachability.getResult(), {
220
+ udp: {result: 'reachable', latencyInMilliseconds: 10, clientMediaIPs: ['somePublicIp1', 'somePublicIp2', 'somePublicIp3']},
221
+ tcp: {result: 'unreachable'},
222
+ xtls: {result: 'untested'}
223
+ });
224
+ });
225
+
226
+ it('should store latency only for the first relay candidate', async () => {
227
+ const promise = clusterReachability.start();
228
+
229
+ await clock.tickAsync(10);
230
+ fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp1'}});
231
+
232
+ // generate more candidates
233
+ await clock.tickAsync(10);
234
+ fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp2'}});
235
+
236
+ await clock.tickAsync(10);
237
+ fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp3'}});
238
+
239
+ await clock.tickAsync(3000);// move the clock so that reachability times out
240
+
241
+ await promise;
242
+
243
+ // latency should be from only the first candidates, but the clientMediaIps should be from only from UDP candidates
244
+ assert.deepEqual(clusterReachability.getResult(), {
245
+ udp: {result: 'unreachable'},
246
+ tcp: {result: 'reachable', latencyInMilliseconds: 10},
247
+ xtls: {result: 'untested'}
248
+ });
249
+ });
250
+
251
+ it('ignores duplicate clientMediaIps', async () => {
252
+ const promise = clusterReachability.start();
253
+
254
+ // generate candidates with duplicate addresses
255
+ await clock.tickAsync(10);
256
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1'}});
257
+
258
+ await clock.tickAsync(10);
259
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1'}});
260
+
261
+ await clock.tickAsync(10);
262
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp2'}});
263
+
264
+ await clock.tickAsync(10);
265
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp2'}});
266
+
267
+ // send also a relay candidate so that the reachability check finishes
268
+ fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
269
+
270
+ await promise;
271
+
272
+ assert.deepEqual(clusterReachability.getResult(), {
273
+ udp: {result: 'reachable', latencyInMilliseconds: 10, clientMediaIPs: ['somePublicIp1', 'somePublicIp2']},
274
+ tcp: {result: 'reachable', latencyInMilliseconds: 40},
275
+ xtls: {result: 'untested'}
276
+ });
277
+ });
278
+ });
279
+ });