@webex/plugin-meetings 3.0.0-beta.1 → 3.0.0-beta.3

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 (111) hide show
  1. package/dist/common/errors/webex-errors.js +5 -29
  2. package/dist/common/errors/webex-errors.js.map +1 -1
  3. package/dist/constants.js +15 -74
  4. package/dist/constants.js.map +1 -1
  5. package/dist/media/index.js +68 -213
  6. package/dist/media/index.js.map +1 -1
  7. package/dist/media/internal-media-core-wrapper.js +22 -0
  8. package/dist/media/internal-media-core-wrapper.js.map +1 -0
  9. package/dist/media/properties.js +20 -25
  10. package/dist/media/properties.js.map +1 -1
  11. package/dist/media/util.js +0 -27
  12. package/dist/media/util.js.map +1 -1
  13. package/dist/meeting/index.js +694 -432
  14. package/dist/meeting/index.js.map +1 -1
  15. package/dist/meeting/request.js +1 -0
  16. package/dist/meeting/request.js.map +1 -1
  17. package/dist/meeting/util.js +3 -44
  18. package/dist/meeting/util.js.map +1 -1
  19. package/dist/meetings/index.js +64 -5
  20. package/dist/meetings/index.js.map +1 -1
  21. package/dist/meetings/util.js +24 -1
  22. package/dist/meetings/util.js.map +1 -1
  23. package/dist/members/index.js +68 -0
  24. package/dist/members/index.js.map +1 -1
  25. package/dist/multistream/mediaRequestManager.js +133 -0
  26. package/dist/multistream/mediaRequestManager.js.map +1 -0
  27. package/dist/multistream/multistreamMedia.js +116 -0
  28. package/dist/multistream/multistreamMedia.js.map +1 -0
  29. package/dist/multistream/receiveSlot.js +209 -0
  30. package/dist/multistream/receiveSlot.js.map +1 -0
  31. package/dist/multistream/receiveSlotManager.js +195 -0
  32. package/dist/multistream/receiveSlotManager.js.map +1 -0
  33. package/dist/multistream/remoteMedia.js +284 -0
  34. package/dist/multistream/remoteMedia.js.map +1 -0
  35. package/dist/multistream/remoteMediaGroup.js +243 -0
  36. package/dist/multistream/remoteMediaGroup.js.map +1 -0
  37. package/dist/multistream/remoteMediaManager.js +1113 -0
  38. package/dist/multistream/remoteMediaManager.js.map +1 -0
  39. package/dist/reconnection-manager/index.js +109 -130
  40. package/dist/reconnection-manager/index.js.map +1 -1
  41. package/dist/roap/index.js +57 -240
  42. package/dist/roap/index.js.map +1 -1
  43. package/dist/roap/request.js +2 -114
  44. package/dist/roap/request.js.map +1 -1
  45. package/dist/roap/turnDiscovery.js +11 -5
  46. package/dist/roap/turnDiscovery.js.map +1 -1
  47. package/dist/statsAnalyzer/global.js +2 -0
  48. package/dist/statsAnalyzer/global.js.map +1 -1
  49. package/dist/statsAnalyzer/index.js +39 -36
  50. package/dist/statsAnalyzer/index.js.map +1 -1
  51. package/package.json +20 -19
  52. package/src/common/errors/webex-errors.js +0 -18
  53. package/src/constants.ts +139 -180
  54. package/src/media/index.js +60 -194
  55. package/src/media/internal-media-core-wrapper.ts +9 -0
  56. package/src/media/properties.js +19 -25
  57. package/src/media/util.js +0 -22
  58. package/src/meeting/index.js +565 -320
  59. package/src/meeting/request.js +1 -0
  60. package/src/meeting/util.js +3 -46
  61. package/src/meetings/index.js +30 -1
  62. package/src/meetings/util.js +23 -2
  63. package/src/members/index.js +48 -0
  64. package/src/multistream/mediaRequestManager.ts +166 -0
  65. package/src/multistream/multistreamMedia.ts +92 -0
  66. package/src/multistream/receiveSlot.ts +141 -0
  67. package/src/multistream/receiveSlotManager.ts +142 -0
  68. package/src/multistream/remoteMedia.ts +219 -0
  69. package/src/multistream/remoteMediaGroup.ts +224 -0
  70. package/src/multistream/remoteMediaManager.ts +911 -0
  71. package/src/reconnection-manager/index.js +40 -53
  72. package/src/roap/index.js +47 -207
  73. package/src/roap/request.js +1 -72
  74. package/src/roap/turnDiscovery.ts +12 -6
  75. package/src/statsAnalyzer/global.js +2 -0
  76. package/src/statsAnalyzer/index.js +32 -46
  77. package/test/integration/spec/journey.js +1 -1
  78. package/test/unit/spec/media/index.ts +223 -0
  79. package/test/unit/spec/media/properties.ts +73 -82
  80. package/test/unit/spec/meeting/effectsState.js +1 -3
  81. package/test/unit/spec/meeting/index.js +420 -228
  82. package/test/unit/spec/meeting/muteState.js +7 -0
  83. package/test/unit/spec/meeting/utils.js +61 -2
  84. package/test/unit/spec/meetings/index.js +0 -4
  85. package/test/unit/spec/members/index.js +164 -2
  86. package/test/unit/spec/multistream/mediaRequestManager.ts +515 -0
  87. package/test/unit/spec/multistream/receiveSlot.ts +104 -0
  88. package/test/unit/spec/multistream/receiveSlotManager.ts +173 -0
  89. package/test/unit/spec/multistream/remoteMedia.ts +217 -0
  90. package/test/unit/spec/multistream/remoteMediaGroup.ts +396 -0
  91. package/test/unit/spec/multistream/remoteMediaManager.ts +1251 -0
  92. package/test/unit/spec/roap/index.ts +63 -35
  93. package/test/unit/spec/stats-analyzer/index.js +19 -22
  94. package/dist/peer-connection-manager/index.js +0 -794
  95. package/dist/peer-connection-manager/index.js.map +0 -1
  96. package/dist/roap/collection.js +0 -73
  97. package/dist/roap/collection.js.map +0 -1
  98. package/dist/roap/handler.js +0 -337
  99. package/dist/roap/handler.js.map +0 -1
  100. package/dist/roap/state.js +0 -164
  101. package/dist/roap/state.js.map +0 -1
  102. package/dist/roap/util.js +0 -102
  103. package/dist/roap/util.js.map +0 -1
  104. package/src/peer-connection-manager/index.js +0 -723
  105. package/src/roap/collection.js +0 -63
  106. package/src/roap/handler.js +0 -252
  107. package/src/roap/state.js +0 -149
  108. package/src/roap/util.js +0 -93
  109. package/test/unit/spec/peerconnection-manager/index.js +0 -188
  110. package/test/unit/spec/peerconnection-manager/utils.js +0 -48
  111. package/test/unit/spec/roap/util.js +0 -30
@@ -0,0 +1,1251 @@
1
+ /* eslint-disable require-jsdoc */
2
+ import EventEmitter from 'events';
3
+
4
+ import {MediaConnection as MC} from '@webex/internal-media-core';
5
+ import {
6
+ Configuration,
7
+ Event,
8
+ RemoteMediaManager,
9
+ VideoLayoutChangedEventData,
10
+ } from '@webex/plugin-meetings/src/multistream/remoteMediaManager';
11
+ import {RemoteMediaGroup} from '@webex/plugin-meetings/src/multistream/remoteMediaGroup';
12
+ import sinon from 'sinon';
13
+ import {assert} from '@webex/test-helper-chai';
14
+ import {cloneDeep} from 'lodash';
15
+ import {MediaRequest} from '@webex/plugin-meetings/src/multistream/mediaRequestManager';
16
+ import {CSI, ReceiveSlotId} from '@webex/plugin-meetings/src/multistream/receiveSlot';
17
+ import testUtils from '../../../utils/testUtils';
18
+
19
+ class FakeSlot extends EventEmitter {
20
+ public mediaType: MC.MediaType;
21
+
22
+ public id: string;
23
+
24
+ public csi?: number;
25
+
26
+ constructor(mediaType: MC.MediaType, id: string) {
27
+ super();
28
+ this.mediaType = mediaType;
29
+ this.id = id;
30
+ // Many of the tests use the same FakeSlot instance for all remote media, so it gets
31
+ // a lot of listeners registered causing a warning about a potential listener leak.
32
+ // Calling setMaxListeners() fixes the warning.
33
+ this.setMaxListeners(50);
34
+ }
35
+ }
36
+
37
+ const DefaultTestConfiguration: Configuration = {
38
+ audio: {
39
+ numOfActiveSpeakerStreams: 3,
40
+ },
41
+ video: {
42
+ preferLiveVideo: true,
43
+ initialLayoutId: 'AllEqual',
44
+
45
+ layouts: {
46
+ AllEqual: {
47
+ screenShareVideo: {size: null},
48
+ activeSpeakerVideoPaneGroups: [
49
+ {
50
+ id: 'main',
51
+ numPanes: 9,
52
+ size: 'best',
53
+ priority: 255,
54
+ },
55
+ ],
56
+ },
57
+ OnePlusFive: {
58
+ screenShareVideo: {size: null},
59
+ activeSpeakerVideoPaneGroups: [
60
+ {
61
+ id: 'mainBigOne',
62
+ numPanes: 1,
63
+ size: 'large',
64
+ priority: 255,
65
+ },
66
+ {
67
+ id: 'secondarySetOfSmallPanes',
68
+ numPanes: 5,
69
+ size: 'very small',
70
+ priority: 254,
71
+ },
72
+ ],
73
+ },
74
+ Single: {
75
+ screenShareVideo: {size: null},
76
+ activeSpeakerVideoPaneGroups: [
77
+ {
78
+ id: 'main',
79
+ numPanes: 1,
80
+ size: 'best',
81
+ priority: 255,
82
+ },
83
+ ],
84
+ },
85
+ Stage: {
86
+ screenShareVideo: {size: null},
87
+ activeSpeakerVideoPaneGroups: [
88
+ {
89
+ id: 'thumbnails',
90
+ numPanes: 6,
91
+ size: 'thumbnail',
92
+ priority: 255,
93
+ },
94
+ ],
95
+ memberVideoPanes: [
96
+ {id: 'stage-1', size: 'medium', csi: undefined},
97
+ {id: 'stage-2', size: 'medium', csi: undefined},
98
+ {id: 'stage-3', size: 'medium', csi: undefined},
99
+ {id: 'stage-4', size: 'medium', csi: undefined},
100
+ ],
101
+ },
102
+ },
103
+ },
104
+ screenShare: {
105
+ audio: true,
106
+ video: true,
107
+ },
108
+ };
109
+
110
+ describe('RemoteMediaManager', () => {
111
+ let remoteMediaManager;
112
+ let fakeReceiveSlotManager;
113
+ let fakeMediaRequestManagers;
114
+ let fakeAudioSlot;
115
+ let fakeVideoSlot;
116
+
117
+ beforeEach(() => {
118
+ fakeAudioSlot = new FakeSlot(MC.MediaType.AudioMain, 'fake audio slot');
119
+ fakeVideoSlot = new FakeSlot(MC.MediaType.VideoMain, 'fake video slot');
120
+
121
+ fakeReceiveSlotManager = {
122
+ allocateSlot: sinon.stub().callsFake((mediaType) => {
123
+ if (mediaType === MC.MediaType.AudioMain) {
124
+ return Promise.resolve(fakeAudioSlot);
125
+ }
126
+
127
+ return Promise.resolve(fakeVideoSlot);
128
+ }),
129
+ releaseSlot: sinon.stub(),
130
+ };
131
+
132
+ fakeMediaRequestManagers = {
133
+ audio: {
134
+ addRequest: sinon.stub(),
135
+ cancelRequest: sinon.stub(),
136
+ commit: sinon.stub(),
137
+ },
138
+ video: {
139
+ addRequest: sinon.stub(),
140
+ cancelRequest: sinon.stub(),
141
+ commit: sinon.stub(),
142
+ },
143
+ };
144
+
145
+ // create remote media manager with default configuration
146
+ remoteMediaManager = new RemoteMediaManager(
147
+ fakeReceiveSlotManager,
148
+ fakeMediaRequestManagers,
149
+ DefaultTestConfiguration
150
+ );
151
+ });
152
+
153
+ const resetHistory = () => {
154
+ fakeReceiveSlotManager.allocateSlot.resetHistory();
155
+ fakeReceiveSlotManager.releaseSlot.resetHistory();
156
+ fakeMediaRequestManagers.audio.addRequest.resetHistory();
157
+ fakeMediaRequestManagers.audio.cancelRequest.resetHistory();
158
+ fakeMediaRequestManagers.audio.commit.resetHistory();
159
+ fakeMediaRequestManagers.video.addRequest.resetHistory();
160
+ fakeMediaRequestManagers.video.cancelRequest.resetHistory();
161
+ fakeMediaRequestManagers.video.commit.resetHistory();
162
+ };
163
+
164
+ describe('start', () => {
165
+ it('rejects if called twice', async () => {
166
+ await remoteMediaManager.start();
167
+ await assert.isRejected(remoteMediaManager.start());
168
+ });
169
+
170
+ it('can be called again after stop()', async () => {
171
+ await remoteMediaManager.start();
172
+ remoteMediaManager.stop();
173
+
174
+ fakeReceiveSlotManager.allocateSlot.resetHistory();
175
+
176
+ await remoteMediaManager.start();
177
+
178
+ // check that the 2nd start() creates slots and media requests and is not a no-op
179
+ assert.calledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.AudioMain);
180
+ assert.calledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.VideoMain);
181
+
182
+ assert.called(fakeMediaRequestManagers.audio.addRequest);
183
+ assert.called(fakeMediaRequestManagers.video.addRequest);
184
+ });
185
+
186
+ it('creates a RemoteMediaGroup for audio correctly', async () => {
187
+ let createdAudioGroup: RemoteMediaGroup | null = null;
188
+
189
+ // create a config with just audio, no video at all and no screen share
190
+ const config = {
191
+ audio: {
192
+ numOfActiveSpeakerStreams: 5,
193
+ },
194
+ video: {
195
+ preferLiveVideo: false,
196
+ initialLayoutId: 'empty',
197
+ layouts: {
198
+ empty: {
199
+ screenShareVideo: {
200
+ size: null,
201
+ },
202
+ },
203
+ },
204
+ },
205
+ screenShare: {
206
+ audio: false,
207
+ video: false,
208
+ },
209
+ };
210
+
211
+ remoteMediaManager = new RemoteMediaManager(
212
+ fakeReceiveSlotManager,
213
+ fakeMediaRequestManagers,
214
+ config
215
+ );
216
+
217
+ remoteMediaManager.on(Event.AudioCreated, (audio: RemoteMediaGroup) => {
218
+ createdAudioGroup = audio;
219
+ });
220
+
221
+ remoteMediaManager.start();
222
+
223
+ await testUtils.flushPromises();
224
+
225
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 5);
226
+ assert.alwaysCalledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.AudioMain);
227
+
228
+ assert.isNotNull(createdAudioGroup);
229
+ if (createdAudioGroup) {
230
+ assert.strictEqual(createdAudioGroup.getRemoteMedia().length, 5);
231
+ assert.isTrue(
232
+ createdAudioGroup
233
+ .getRemoteMedia()
234
+ .every((remoteMedia) => remoteMedia.mediaType === MC.MediaType.AudioMain)
235
+ );
236
+ assert.strictEqual(createdAudioGroup.getRemoteMedia('pinned').length, 0);
237
+ }
238
+
239
+ assert.calledOnce(fakeMediaRequestManagers.audio.addRequest);
240
+ assert.calledWith(
241
+ fakeMediaRequestManagers.audio.addRequest,
242
+ sinon.match({
243
+ policyInfo: sinon.match({
244
+ policy: 'active-speaker',
245
+ priority: 255,
246
+ }),
247
+ receiveSlots: Array(5).fill(fakeAudioSlot),
248
+ codecInfo: undefined,
249
+ })
250
+ );
251
+ });
252
+
253
+ it('pre-allocates receive slots based on the biggest layout', async () => {
254
+ const config = cloneDeep(DefaultTestConfiguration);
255
+
256
+ config.audio.numOfActiveSpeakerStreams = 0;
257
+ config.video.layouts.huge = {
258
+ screenShareVideo: {
259
+ size: null,
260
+ },
261
+ activeSpeakerVideoPaneGroups: [
262
+ {
263
+ id: 'big one',
264
+ numPanes: 99,
265
+ size: 'small',
266
+ priority: 255,
267
+ },
268
+ ],
269
+ };
270
+
271
+ remoteMediaManager = new RemoteMediaManager(
272
+ fakeReceiveSlotManager,
273
+ fakeMediaRequestManagers,
274
+ config
275
+ );
276
+
277
+ await remoteMediaManager.start();
278
+
279
+ // even though our "big one" layout is not the default one, the remote media manager should still
280
+ // preallocate enough video receive slots for it up front
281
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 99);
282
+ assert.alwaysCalledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.VideoMain);
283
+ });
284
+
285
+ it('starts with the initial layout', async () => {
286
+ let receivedLayoutInfo: VideoLayoutChangedEventData | null = null;
287
+
288
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
289
+ receivedLayoutInfo = layoutInfo;
290
+ });
291
+
292
+ // the initial layout is "AllEqual", so we check that it gets selected by default
293
+ await remoteMediaManager.start();
294
+
295
+ assert.strictEqual(remoteMediaManager.getLayoutId(), 'AllEqual');
296
+ assert.isNotNull(receivedLayoutInfo);
297
+ if (receivedLayoutInfo) {
298
+ assert.strictEqual(receivedLayoutInfo.layoutId, 'AllEqual');
299
+ assert.strictEqual(Object.keys(receivedLayoutInfo.memberVideoPanes).length, 0);
300
+ assert.strictEqual(Object.keys(receivedLayoutInfo.activeSpeakerVideoPanes).length, 1); // this layout has only 1 active speaker group
301
+ assert.strictEqual(
302
+ receivedLayoutInfo.activeSpeakerVideoPanes.main.getRemoteMedia().length,
303
+ 9
304
+ );
305
+ }
306
+ });
307
+ });
308
+
309
+ describe('constructor', () => {
310
+ it('throws if the initial layout in the config is invalid', () => {
311
+ const config = cloneDeep(DefaultTestConfiguration);
312
+
313
+ config.video.initialLayoutId = 'invalid';
314
+
315
+ assert.throws(() => {
316
+ remoteMediaManager = new RemoteMediaManager(
317
+ fakeReceiveSlotManager,
318
+ fakeMediaRequestManagers,
319
+ config
320
+ );
321
+ }, 'invalid config: initialLayoutId "invalid" doesn\'t match any of the layouts');
322
+ });
323
+
324
+ it('throws if there are duplicate active speaker video pane groups', () => {
325
+ const config = cloneDeep(DefaultTestConfiguration);
326
+
327
+ config.video.layouts.test = {
328
+ screenShareVideo: {size: null},
329
+ activeSpeakerVideoPaneGroups: [
330
+ {
331
+ id: 'someDuplicate',
332
+ numPanes: 10,
333
+ priority: 255,
334
+ size: 'best',
335
+ },
336
+ {
337
+ id: 'other',
338
+ numPanes: 10,
339
+ priority: 254,
340
+ size: 'best',
341
+ },
342
+ {
343
+ id: 'someDuplicate',
344
+ numPanes: 10,
345
+ priority: 255,
346
+ size: 'best',
347
+ },
348
+ ],
349
+ };
350
+
351
+ assert.throws(() => {
352
+ remoteMediaManager = new RemoteMediaManager(
353
+ fakeReceiveSlotManager,
354
+ fakeMediaRequestManagers,
355
+ config
356
+ );
357
+ }, 'invalid config: duplicate active speaker video pane group id: someDuplicate');
358
+ });
359
+
360
+ it('throws if there are active speaker video pane groups with duplicate priority', () => {
361
+ const config = cloneDeep(DefaultTestConfiguration);
362
+
363
+ config.video.layouts.test = {
364
+ screenShareVideo: {size: null},
365
+ activeSpeakerVideoPaneGroups: [
366
+ {
367
+ id: 'group1',
368
+ numPanes: 10,
369
+ priority: 200,
370
+ size: 'best',
371
+ },
372
+ {
373
+ id: 'group2',
374
+ numPanes: 2,
375
+ priority: 200,
376
+ size: 'medium',
377
+ },
378
+ {
379
+ id: 'group3',
380
+ numPanes: 5,
381
+ priority: 100,
382
+ size: 'large',
383
+ },
384
+ ],
385
+ };
386
+
387
+ assert.throws(() => {
388
+ remoteMediaManager = new RemoteMediaManager(
389
+ fakeReceiveSlotManager,
390
+ fakeMediaRequestManagers,
391
+ config
392
+ );
393
+ }, 'invalid config: multiple active speaker video pane groups have same priority: 200');
394
+ });
395
+
396
+ it('throws if there are duplicate member video panes', () => {
397
+ const config = cloneDeep(DefaultTestConfiguration);
398
+
399
+ config.video.layouts.test = {
400
+ screenShareVideo: {size: null},
401
+ memberVideoPanes: [
402
+ {id: 'paneA', size: 'best', csi: 123},
403
+ {id: 'paneB', size: 'large', csi: 222},
404
+ {id: 'paneC', size: 'medium', csi: 333},
405
+ {id: 'paneB', size: 'small', csi: 444},
406
+ ],
407
+ };
408
+
409
+ assert.throws(() => {
410
+ remoteMediaManager = new RemoteMediaManager(
411
+ fakeReceiveSlotManager,
412
+ fakeMediaRequestManagers,
413
+ config
414
+ );
415
+ }, 'invalid config: duplicate member video pane id: paneB');
416
+ });
417
+ });
418
+
419
+ describe('stop', () => {
420
+ it('releases all the slots and invalidates all remote media', async () => {
421
+ let audioStopStub;
422
+ let videoActiveSpeakerGroupStopStub;
423
+ const memberVideoPaneStopStubs: any[] = [];
424
+
425
+ // change the initial layout to one that has both active speakers and receveiver selected videos
426
+ const config = cloneDeep(DefaultTestConfiguration);
427
+
428
+ config.video.initialLayoutId = 'Stage';
429
+
430
+ remoteMediaManager = new RemoteMediaManager(
431
+ fakeReceiveSlotManager,
432
+ fakeMediaRequestManagers,
433
+ config
434
+ );
435
+
436
+ remoteMediaManager.on(Event.AudioCreated, (audio: RemoteMediaGroup) => {
437
+ audioStopStub = sinon.stub(audio, 'stop');
438
+ });
439
+
440
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
441
+ // The "Stage" layout that we're using has only 1 active speaker group called "thumbnails"
442
+ videoActiveSpeakerGroupStopStub = sinon.stub(
443
+ layoutInfo.activeSpeakerVideoPanes.thumbnails,
444
+ 'stop'
445
+ );
446
+
447
+ Object.values(layoutInfo.memberVideoPanes).forEach((pane) => {
448
+ memberVideoPaneStopStubs.push(sinon.stub(pane, 'stop'));
449
+ });
450
+ });
451
+
452
+ await remoteMediaManager.start();
453
+
454
+ // we're using the default config that requires 3 main audio slots and 10 video slots (for Stage2x2With6ThumbnailsLayout)
455
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 13);
456
+
457
+ // our layout has 4 member video panes, we should have a stub for each of these panes' stop methods
458
+ assert.strictEqual(memberVideoPaneStopStubs.length, 4);
459
+
460
+ resetHistory();
461
+
462
+ remoteMediaManager.stop();
463
+
464
+ // check that all slots have been released
465
+ assert.callCount(fakeReceiveSlotManager.releaseSlot, 13);
466
+
467
+ // and that all RemoteMedia and RemoteMediaGroups have been stopped
468
+ assert.calledOnce(audioStopStub);
469
+ assert.calledWith(audioStopStub, true);
470
+ assert.calledOnce(videoActiveSpeakerGroupStopStub);
471
+ memberVideoPaneStopStubs.forEach((stub) => {
472
+ assert.calledOnce(stub);
473
+ });
474
+ assert.calledOnce(fakeMediaRequestManagers.video.commit);
475
+ });
476
+
477
+ it('can be called multiple times', async () => {
478
+ await remoteMediaManager.start();
479
+
480
+ // just checking that nothing crashes etc.
481
+ remoteMediaManager.stop();
482
+ remoteMediaManager.stop();
483
+ });
484
+ });
485
+ describe('setLayout', () => {
486
+ it('rejects if called with invalid layoutId', async () => {
487
+ await assert.isRejected(remoteMediaManager.setLayout('invalid value'));
488
+ });
489
+
490
+ it('rejects if called before calling start()', async () => {
491
+ await assert.isRejected(remoteMediaManager.setLayout('Stage'));
492
+ });
493
+
494
+ it('allocates more slots when switching to a layout that requires more slots', async () => {
495
+ // start with "Single" layout that needs just 1 video slot
496
+ const config = cloneDeep(DefaultTestConfiguration);
497
+
498
+ config.video.initialLayoutId = 'Single';
499
+
500
+ remoteMediaManager = new RemoteMediaManager(
501
+ fakeReceiveSlotManager,
502
+ fakeMediaRequestManagers,
503
+ config
504
+ );
505
+
506
+ await remoteMediaManager.start();
507
+
508
+ resetHistory();
509
+
510
+ // switch to "Stage" layout that requires 9 more video slots (10)
511
+ await remoteMediaManager.setLayout('Stage');
512
+
513
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 9);
514
+ assert.alwaysCalledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.VideoMain);
515
+ });
516
+
517
+ it('releases slots when switching to layout that requires less active speaker slots', async () => {
518
+ // start with "AllEqual" layout that needs just 9 video slots
519
+ const config = cloneDeep(DefaultTestConfiguration);
520
+
521
+ config.video.initialLayoutId = 'AllEqual';
522
+
523
+ remoteMediaManager = new RemoteMediaManager(
524
+ fakeReceiveSlotManager,
525
+ fakeMediaRequestManagers,
526
+ config
527
+ );
528
+
529
+ await remoteMediaManager.start();
530
+
531
+ resetHistory();
532
+
533
+ // switch to "OnePlusFive" layout that requires 3 less video slots (6)
534
+ await remoteMediaManager.setLayout('OnePlusFive');
535
+
536
+ // verify that 3 main video slots were released
537
+ assert.callCount(fakeReceiveSlotManager.releaseSlot, 3);
538
+ fakeReceiveSlotManager.releaseSlot.getCalls().forEach((call) => {
539
+ const slot = call.args[0];
540
+
541
+ assert.strictEqual(slot.mediaType, MC.MediaType.VideoMain);
542
+ });
543
+ });
544
+
545
+ describe('switching between different receiver selected layouts', () => {
546
+ let fakeSlots: {[key: ReceiveSlotId]: FakeSlot};
547
+ let slotCounter: number;
548
+
549
+ type Csi2SlotsMapping = {[key: CSI]: Array<ReceiveSlotId>};
550
+ // in these mappings: key is the CSI and value is an array of slot ids
551
+ // of slots that were used in media requests for that CSI
552
+ let csi2slotMappingBeforeLayoutChange: Csi2SlotsMapping;
553
+ let csi2slotMappingAfterLayoutChange: Csi2SlotsMapping;
554
+ let csi2slotMapping: Csi2SlotsMapping;
555
+
556
+ beforeEach(() => {
557
+ // setup the mocks so that we can keep track of all the slots and their CSIs
558
+ fakeSlots = {};
559
+ slotCounter = 0;
560
+
561
+ fakeReceiveSlotManager.allocateSlot.callsFake(() => {
562
+ slotCounter += 1;
563
+ const newSlotId = `fake video slot ${slotCounter}`;
564
+
565
+ fakeSlots[newSlotId] = new FakeSlot(MC.MediaType.VideoMain, newSlotId);
566
+ return fakeSlots[newSlotId];
567
+ });
568
+
569
+ csi2slotMappingBeforeLayoutChange = {};
570
+ csi2slotMappingAfterLayoutChange = {};
571
+
572
+ csi2slotMapping = csi2slotMappingBeforeLayoutChange;
573
+
574
+ fakeMediaRequestManagers.video.addRequest.callsFake((mediaRequest: MediaRequest) => {
575
+ if (mediaRequest.policyInfo.policy === 'receiver-selected') {
576
+ const slot = mediaRequest.receiveSlots[0] as unknown as FakeSlot;
577
+ const csi = mediaRequest.policyInfo.csi;
578
+
579
+ slot.csi = csi;
580
+ if (csi2slotMapping[csi]) {
581
+ csi2slotMapping[csi].push(slot.id);
582
+ } else {
583
+ csi2slotMapping[csi] = [slot.id];
584
+ }
585
+
586
+ return slot.id;
587
+ }
588
+ });
589
+ });
590
+
591
+ it('releases slots when switching to layout that requires less receiver selected slots', async () => {
592
+ const config = cloneDeep(DefaultTestConfiguration);
593
+
594
+ // This test starts with a layout that has 5 receiver selected video slots
595
+ // and switches to a different layout that has fewer slots, but 2 of them match CSIs
596
+ // from the initial layout. We want to verify that these 2 slots get re-used correctly.
597
+ config.audio.numOfActiveSpeakerStreams = 0;
598
+ config.screenShare.audio = false;
599
+ config.screenShare.video = false;
600
+ config.video.initialLayoutId = 'biggerLayout';
601
+ config.video.layouts['biggerLayout'] = {
602
+ screenShareVideo: {size: null},
603
+ memberVideoPanes: [
604
+ {id: '1', size: 'best', csi: 100},
605
+ {id: '2', size: 'best', csi: 200},
606
+ {id: '3', size: 'best', csi: 300},
607
+ {id: '4', size: 'best', csi: 400},
608
+ {id: '5', size: 'best', csi: 500},
609
+ ],
610
+ };
611
+ config.video.layouts['smallerLayout'] = {
612
+ screenShareVideo: {size: null},
613
+ memberVideoPanes: [
614
+ {id: '1', size: 'medium', csi: 200}, // this csi matches pane '2' from biggerLayout
615
+ {id: '2', size: 'medium', csi: 123},
616
+ {id: '3', size: 'medium', csi: 400}, // this csi matches pane '4' from biggerLayout
617
+ ],
618
+ };
619
+
620
+ remoteMediaManager = new RemoteMediaManager(
621
+ fakeReceiveSlotManager,
622
+ fakeMediaRequestManagers,
623
+ config
624
+ );
625
+
626
+ await remoteMediaManager.start();
627
+
628
+ resetHistory();
629
+
630
+ // switch the mock to now use csi2slotMappingAfterLayoutChange as we're about to change the layout
631
+ csi2slotMapping = csi2slotMappingAfterLayoutChange;
632
+
633
+ // switch to "smallerLayout" layout that requires 2 less video slots and has 2 receive selected slots with same CSIs
634
+ await remoteMediaManager.setLayout('smallerLayout');
635
+
636
+ // verify that 2 main video slots were released
637
+ assert.callCount(fakeReceiveSlotManager.releaseSlot, 2);
638
+
639
+ // verify that each CSI has 1 slot assigned
640
+ assert.equal(Object.keys(csi2slotMappingAfterLayoutChange).length, 3);
641
+ assert.equal(csi2slotMappingAfterLayoutChange[200].length, 1);
642
+ assert.equal(csi2slotMappingAfterLayoutChange[123].length, 1);
643
+ assert.equal(csi2slotMappingAfterLayoutChange[400].length, 1);
644
+
645
+ // verify that the slots have been re-used for csi 200 and 400
646
+ assert.equal(
647
+ csi2slotMappingBeforeLayoutChange[200][0],
648
+ csi2slotMappingAfterLayoutChange[200][0]
649
+ );
650
+ assert.equal(
651
+ csi2slotMappingBeforeLayoutChange[400][0],
652
+ csi2slotMappingAfterLayoutChange[400][0]
653
+ );
654
+ });
655
+
656
+ it('correctly handles a change to a layout that has member video panes with duplicate CSIs', async () => {
657
+ const config = cloneDeep(DefaultTestConfiguration);
658
+
659
+ // This test starts with a layout that has video slot with a specific CSI
660
+ // and switches to a different layout that 2 panes with that same CSI.
661
+ // We want to verify that the slot gets reused, but also that a 2nd slot is allocated.
662
+ config.audio.numOfActiveSpeakerStreams = 0;
663
+ config.screenShare.audio = false;
664
+ config.screenShare.video = false;
665
+ config.video.initialLayoutId = 'initialEmptyLayout';
666
+ config.video.layouts['initialEmptyLayout'] = {
667
+ screenShareVideo: {size: null},
668
+ memberVideoPanes: [{id: '2', size: 'medium', csi: 456}],
669
+ };
670
+ config.video.layouts['layoutWithDuplicateCSIs'] = {
671
+ screenShareVideo: {size: null},
672
+ memberVideoPanes: [
673
+ {id: '1', size: 'medium', csi: 123},
674
+ {id: '2', size: 'medium', csi: 456},
675
+ {id: '3', size: 'medium', csi: 456}, // duplicate CSI and also matching one of CSIs from previous layout
676
+ {id: '4', size: 'medium', csi: 789},
677
+ ],
678
+ };
679
+
680
+ remoteMediaManager = new RemoteMediaManager(
681
+ fakeReceiveSlotManager,
682
+ fakeMediaRequestManagers,
683
+ config
684
+ );
685
+
686
+ await remoteMediaManager.start();
687
+
688
+ resetHistory();
689
+
690
+ // switch the mock to now use csi2slotMappingAfterLayoutChange as we're about to change the layout
691
+ csi2slotMapping = csi2slotMappingAfterLayoutChange;
692
+
693
+ // switch to "smallerLayout" layout that requires 2 less video slots and has 2 receive selected slots with same CSIs
694
+ await remoteMediaManager.setLayout('layoutWithDuplicateCSIs');
695
+
696
+ // verify that the 2 member panes with duplicate CSI value of 456 have 2 separate receive slots allocated
697
+ assert.equal(csi2slotMappingAfterLayoutChange[456].length, 2);
698
+ assert.notEqual(
699
+ csi2slotMappingAfterLayoutChange[456][0],
700
+ csi2slotMappingAfterLayoutChange[456][1]
701
+ );
702
+
703
+ // and that one of them is the same re-used slot from previous layout
704
+ assert.isTrue(
705
+ csi2slotMappingBeforeLayoutChange[456][0] === csi2slotMappingAfterLayoutChange[456][0] ||
706
+ csi2slotMappingBeforeLayoutChange[456][0] === csi2slotMappingAfterLayoutChange[456][1]
707
+ );
708
+
709
+ // and the other panes have 1 slot each
710
+ assert.equal(csi2slotMappingAfterLayoutChange[123].length, 1);
711
+ assert.equal(csi2slotMappingAfterLayoutChange[789].length, 1);
712
+ });
713
+ });
714
+
715
+ describe('media requests', () => {
716
+ it('sends correct media requests when switching to a layout with receiver selected slots', async () => {
717
+ const config = cloneDeep(DefaultTestConfiguration);
718
+
719
+ config.video.layouts.Stage.memberVideoPanes = [
720
+ {id: 'stage-1', size: 'medium', csi: 11111},
721
+ {id: 'stage-2', size: 'medium', csi: 22222},
722
+ {id: 'stage-3', size: 'medium', csi: undefined},
723
+ {id: 'stage-4', size: 'medium', csi: undefined},
724
+ ];
725
+ remoteMediaManager = new RemoteMediaManager(
726
+ fakeReceiveSlotManager,
727
+ fakeMediaRequestManagers,
728
+ config
729
+ );
730
+
731
+ await remoteMediaManager.start();
732
+
733
+ resetHistory();
734
+
735
+ // switch to "Stage" layout that has an active speaker group and 4 receiver selected slots
736
+ // and a CSI set on 2 of them
737
+ await remoteMediaManager.setLayout('Stage');
738
+
739
+ assert.callCount(fakeMediaRequestManagers.video.addRequest, 3);
740
+ assert.calledWith(
741
+ fakeMediaRequestManagers.video.addRequest,
742
+ sinon.match({
743
+ policyInfo: sinon.match({
744
+ policy: 'active-speaker',
745
+ priority: 255,
746
+ }),
747
+ receiveSlots: Array(6).fill(fakeVideoSlot),
748
+ codecInfo: sinon.match({
749
+ codec: 'h264',
750
+ maxFs: 60,
751
+ }),
752
+ })
753
+ );
754
+ assert.calledWith(
755
+ fakeMediaRequestManagers.video.addRequest,
756
+ sinon.match({
757
+ policyInfo: sinon.match({
758
+ policy: 'receiver-selected',
759
+ csi: 11111,
760
+ }),
761
+ receiveSlots: Array(1).fill(fakeVideoSlot),
762
+ codecInfo: sinon.match({
763
+ codec: 'h264',
764
+ maxFs: 3600,
765
+ }),
766
+ })
767
+ );
768
+ assert.calledWith(
769
+ fakeMediaRequestManagers.video.addRequest,
770
+ sinon.match({
771
+ policyInfo: sinon.match({
772
+ policy: 'receiver-selected',
773
+ csi: 22222,
774
+ }),
775
+ receiveSlots: Array(1).fill(fakeVideoSlot),
776
+ codecInfo: sinon.match({
777
+ codec: 'h264',
778
+ maxFs: 3600,
779
+ }),
780
+ })
781
+ );
782
+ });
783
+
784
+ it('sends correct media requests when switching to a layout with multiple active-speaker groups', async () => {
785
+ // start with "AllEqual" layout that needs just 9 video slots
786
+ const config = cloneDeep(DefaultTestConfiguration);
787
+
788
+ config.video.initialLayoutId = 'AllEqual';
789
+
790
+ remoteMediaManager = new RemoteMediaManager(
791
+ fakeReceiveSlotManager,
792
+ fakeMediaRequestManagers,
793
+ config
794
+ );
795
+
796
+ const allEqualMediaRequestId = 'fake request id';
797
+
798
+ fakeMediaRequestManagers.video.addRequest.returns(allEqualMediaRequestId);
799
+
800
+ await remoteMediaManager.start();
801
+
802
+ resetHistory();
803
+
804
+ // switch to "OnePlusFive" layout that has 2 active speaker groups
805
+ await remoteMediaManager.setLayout('OnePlusFive');
806
+
807
+ // check that the previous active speaker request for "AllEqual" group was cancelled
808
+ assert.calledOnce(fakeMediaRequestManagers.video.cancelRequest);
809
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, allEqualMediaRequestId);
810
+
811
+ // check that 2 correct active speaker media requests were sent out
812
+ assert.callCount(fakeMediaRequestManagers.video.addRequest, 2);
813
+ assert.calledWith(
814
+ fakeMediaRequestManagers.video.addRequest,
815
+ sinon.match({
816
+ policyInfo: sinon.match({
817
+ policy: 'active-speaker',
818
+ priority: 255,
819
+ }),
820
+ receiveSlots: Array(1).fill(fakeVideoSlot),
821
+ codecInfo: sinon.match({
822
+ codec: 'h264',
823
+ maxFs: 8192,
824
+ }),
825
+ })
826
+ );
827
+ assert.calledWith(
828
+ fakeMediaRequestManagers.video.addRequest,
829
+ sinon.match({
830
+ policyInfo: sinon.match({
831
+ policy: 'active-speaker',
832
+ priority: 254,
833
+ }),
834
+ receiveSlots: Array(5).fill(fakeVideoSlot),
835
+ codecInfo: sinon.match({
836
+ codec: 'h264',
837
+ maxFs: 240,
838
+ }),
839
+ })
840
+ );
841
+ });
842
+
843
+ it('cancels all media requests for the previous layout when switching to a new one', async () => {
844
+ const config: Configuration = {
845
+ audio: {
846
+ numOfActiveSpeakerStreams: 0,
847
+ },
848
+ video: {
849
+ preferLiveVideo: true,
850
+ initialLayoutId: 'initial',
851
+ layouts: {
852
+ initial: {
853
+ screenShareVideo: {size: null},
854
+ activeSpeakerVideoPaneGroups: [
855
+ {
856
+ id: 'big',
857
+ numPanes: 10,
858
+ priority: 255,
859
+ size: 'large',
860
+ },
861
+ {
862
+ id: 'small',
863
+ numPanes: 3,
864
+ priority: 254,
865
+ size: 'medium',
866
+ },
867
+ ],
868
+ memberVideoPanes: [
869
+ {id: 'pane 1', size: 'best', csi: 123},
870
+ {id: 'pane 2', size: 'best', csi: 234},
871
+ ],
872
+ },
873
+ other: {
874
+ screenShareVideo: {size: null},
875
+ },
876
+ },
877
+ },
878
+ screenShare: {
879
+ audio: false,
880
+ video: false,
881
+ },
882
+ };
883
+
884
+ remoteMediaManager = new RemoteMediaManager(
885
+ fakeReceiveSlotManager,
886
+ fakeMediaRequestManagers,
887
+ config
888
+ );
889
+
890
+ let activeSpeakerRequestCounter = 0;
891
+ let receiverSelectedRequestCounter = 0;
892
+
893
+ // setup the mock for addRequest to return request ids that we want
894
+ fakeMediaRequestManagers.video.addRequest.callsFake((mediaRequest) => {
895
+ if (mediaRequest.policyInfo.policy === 'active-speaker') {
896
+ activeSpeakerRequestCounter += 1;
897
+
898
+ return `active speaker request ${activeSpeakerRequestCounter}`;
899
+ }
900
+ receiverSelectedRequestCounter += 1;
901
+
902
+ return `receiver selected request ${receiverSelectedRequestCounter}`;
903
+ });
904
+
905
+ await remoteMediaManager.start();
906
+
907
+ resetHistory();
908
+
909
+ // switch to "other" layout
910
+ await remoteMediaManager.setLayout('other');
911
+
912
+ // check that all the previous media requests for "initial" layout have been cancelled
913
+ assert.callCount(fakeMediaRequestManagers.video.cancelRequest, 4);
914
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, 'active speaker request 1');
915
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, 'active speaker request 2');
916
+ assert.calledWith(
917
+ fakeMediaRequestManagers.video.cancelRequest,
918
+ 'receiver selected request 1'
919
+ );
920
+ assert.calledWith(
921
+ fakeMediaRequestManagers.video.cancelRequest,
922
+ 'receiver selected request 2'
923
+ );
924
+
925
+ // new layout has no videos, so no new requests should be sent out
926
+ // check that 2 correct active speaker media requests were sent out
927
+ assert.callCount(fakeMediaRequestManagers.video.addRequest, 0);
928
+ });
929
+ });
930
+ });
931
+
932
+ describe('setRemoteVideoCsi', () => {
933
+ it('sends correct media requests', async () => {
934
+ let currentLayoutInfo: VideoLayoutChangedEventData | null = null;
935
+
936
+ await remoteMediaManager.start();
937
+
938
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
939
+ currentLayoutInfo = layoutInfo;
940
+ });
941
+ // switch to "Stage" layout which has some receiver selected slots
942
+ await remoteMediaManager.setLayout('Stage');
943
+ resetHistory();
944
+
945
+ assert.isNotNull(currentLayoutInfo);
946
+
947
+ if (currentLayoutInfo) {
948
+ const fakeRequestId1 = 'fake request id 1';
949
+ const fakeRequestId2 = 'fake request id 2';
950
+
951
+ fakeMediaRequestManagers.video.addRequest.returns(fakeRequestId1);
952
+
953
+ remoteMediaManager.setRemoteVideoCsi(currentLayoutInfo.memberVideoPanes['stage-1'], 1001);
954
+
955
+ // a new media request should have been sent out
956
+ assert.calledOnce(fakeMediaRequestManagers.video.addRequest);
957
+ assert.calledWith(
958
+ fakeMediaRequestManagers.video.addRequest,
959
+ sinon.match({
960
+ policyInfo: sinon.match({
961
+ policy: 'receiver-selected',
962
+ csi: 1001,
963
+ }),
964
+ receiveSlots: Array(1).fill(fakeVideoSlot),
965
+ codecInfo: sinon.match({
966
+ codec: 'h264',
967
+ maxFs: 3600,
968
+ }),
969
+ })
970
+ );
971
+ assert.notCalled(fakeMediaRequestManagers.video.cancelRequest);
972
+
973
+ resetHistory();
974
+
975
+ // change the same video pane again
976
+ remoteMediaManager.setRemoteVideoCsi(currentLayoutInfo.memberVideoPanes['stage-1'], 1002);
977
+
978
+ // a new media request should have been sent out
979
+ assert.calledOnce(fakeMediaRequestManagers.video.addRequest);
980
+ assert.calledWith(
981
+ fakeMediaRequestManagers.video.addRequest,
982
+ sinon.match({
983
+ policyInfo: sinon.match({
984
+ policy: 'receiver-selected',
985
+ csi: 1002,
986
+ }),
987
+ receiveSlots: Array(1).fill(fakeVideoSlot),
988
+ codecInfo: sinon.match({
989
+ codec: 'h264',
990
+ maxFs: 3600,
991
+ }),
992
+ })
993
+ );
994
+ // and previous one should have been cancelled
995
+ assert.calledOnce(fakeMediaRequestManagers.video.cancelRequest);
996
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, fakeRequestId1);
997
+
998
+ resetHistory();
999
+
1000
+ fakeMediaRequestManagers.video.addRequest.returns(fakeRequestId2);
1001
+
1002
+ // now change some other video pane
1003
+ remoteMediaManager.setRemoteVideoCsi(currentLayoutInfo.memberVideoPanes['stage-3'], 2001);
1004
+
1005
+ // a new media request should have been sent out
1006
+ assert.calledOnce(fakeMediaRequestManagers.video.addRequest);
1007
+ assert.calledWith(
1008
+ fakeMediaRequestManagers.video.addRequest,
1009
+ sinon.match({
1010
+ policyInfo: sinon.match({
1011
+ policy: 'receiver-selected',
1012
+ csi: 2001,
1013
+ }),
1014
+ receiveSlots: Array(1).fill(fakeVideoSlot),
1015
+ codecInfo: sinon.match({
1016
+ codec: 'h264',
1017
+ maxFs: 3600,
1018
+ }),
1019
+ })
1020
+ );
1021
+ // nothing should have been cancelled
1022
+ assert.notCalled(fakeMediaRequestManagers.video.cancelRequest);
1023
+
1024
+ resetHistory();
1025
+
1026
+ // now set CSI back to undefined
1027
+ remoteMediaManager.setRemoteVideoCsi(
1028
+ currentLayoutInfo.memberVideoPanes['stage-3'],
1029
+ undefined
1030
+ );
1031
+
1032
+ // no new media request should have been sent out
1033
+ assert.notCalled(fakeMediaRequestManagers.video.addRequest);
1034
+ // and previous one should have been cancelled
1035
+ assert.calledOnce(fakeMediaRequestManagers.video.cancelRequest);
1036
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, fakeRequestId2);
1037
+ }
1038
+ });
1039
+ });
1040
+
1041
+ describe('addMemberVideoPane()', () => {
1042
+ it('fails if there is no current layout', () => {
1043
+ // we haven't called start() so there is no layout set, yet
1044
+ assert.isRejected(
1045
+ remoteMediaManager.addMemberVideoPane({id: 'newPane', size: 'best', csi: 54321})
1046
+ );
1047
+ });
1048
+
1049
+ it('fails if called with a duplicate paneId', async () => {
1050
+ await remoteMediaManager.start();
1051
+ await remoteMediaManager.setLayout('Stage');
1052
+
1053
+ assert.isRejected(
1054
+ remoteMediaManager.addMemberVideoPane({id: 'stage-3', size: 'best', csi: 54321})
1055
+ );
1056
+ });
1057
+
1058
+ it('works as expected when called with a CSI value', async () => {
1059
+ await remoteMediaManager.start();
1060
+ await remoteMediaManager.setLayout('Stage');
1061
+
1062
+ resetHistory();
1063
+
1064
+ await remoteMediaManager.addMemberVideoPane({id: 'newPane', size: 'best', csi: 54321});
1065
+
1066
+ // new slot should be allocated
1067
+ assert.calledOnce(fakeReceiveSlotManager.allocateSlot);
1068
+ assert.calledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.VideoMain);
1069
+
1070
+ // and a media request sent out
1071
+ assert.calledOnce(fakeMediaRequestManagers.video.addRequest);
1072
+ assert.calledWith(
1073
+ fakeMediaRequestManagers.video.addRequest,
1074
+ sinon.match({
1075
+ policyInfo: sinon.match({
1076
+ policy: 'receiver-selected',
1077
+ csi: 54321,
1078
+ }),
1079
+ receiveSlots: Array(1).fill(fakeVideoSlot),
1080
+ codecInfo: sinon.match({
1081
+ codec: 'h264',
1082
+ maxFs: 8192,
1083
+ }),
1084
+ })
1085
+ );
1086
+ });
1087
+ it('works as expected when called without a CSI value', async () => {
1088
+ await remoteMediaManager.start();
1089
+ await remoteMediaManager.setLayout('Stage');
1090
+
1091
+ resetHistory();
1092
+
1093
+ await remoteMediaManager.addMemberVideoPane({id: 'newPane', size: 'best'});
1094
+
1095
+ // new slot should be allocated
1096
+ assert.calledOnce(fakeReceiveSlotManager.allocateSlot);
1097
+ assert.calledWith(fakeReceiveSlotManager.allocateSlot, MC.MediaType.VideoMain);
1098
+
1099
+ // but no media requests sent out
1100
+ assert.notCalled(fakeMediaRequestManagers.video.addRequest);
1101
+ });
1102
+ });
1103
+
1104
+ describe('removeMemberVideoPane()', () => {
1105
+ it('fails if there is no current layout', () => {
1106
+ // we haven't called start() so there is no layout set, yet
1107
+ assert.isRejected(remoteMediaManager.removeMemberVideoPane('newPane'));
1108
+ });
1109
+
1110
+ it('does nothing when called for a pane not in the current layout', async () => {
1111
+ await remoteMediaManager.start();
1112
+ await remoteMediaManager.setLayout('Stage');
1113
+
1114
+ resetHistory();
1115
+
1116
+ await remoteMediaManager.removeMemberVideoPane('some pane');
1117
+
1118
+ assert.notCalled(fakeReceiveSlotManager.releaseSlot);
1119
+ assert.notCalled(fakeMediaRequestManagers.video.cancelRequest);
1120
+ });
1121
+
1122
+ it('works as expected', async () => {
1123
+ await remoteMediaManager.start();
1124
+ await remoteMediaManager.setLayout('Stage');
1125
+
1126
+ const fakeNewSlot = new FakeSlot(MC.MediaType.VideoMain, 'fake video slot');
1127
+ const fakeRequestId = 'fake request id';
1128
+
1129
+ fakeReceiveSlotManager.allocateSlot.resolves(fakeNewSlot);
1130
+ fakeMediaRequestManagers.video.addRequest.returns(fakeRequestId);
1131
+
1132
+ // first, add some pane
1133
+ await remoteMediaManager.addMemberVideoPane({id: 'newPane', size: 'best', csi: 54321});
1134
+
1135
+ resetHistory();
1136
+
1137
+ // now remove it
1138
+ await remoteMediaManager.removeMemberVideoPane('newPane');
1139
+
1140
+ // slot should be released
1141
+ assert.calledOnce(fakeReceiveSlotManager.releaseSlot);
1142
+ assert.calledWith(fakeReceiveSlotManager.releaseSlot, fakeNewSlot);
1143
+
1144
+ // and a media request cancelled
1145
+ assert.calledOnce(fakeMediaRequestManagers.video.cancelRequest);
1146
+ assert.calledWith(fakeMediaRequestManagers.video.cancelRequest, fakeRequestId);
1147
+ });
1148
+ });
1149
+
1150
+ describe('pinActiveSpeakerVideoPane() and isPinned()', () => {
1151
+ it('throws if called on a pane not belonging to an active speaker group', async () => {
1152
+ let currentLayoutInfo: VideoLayoutChangedEventData | null = null;
1153
+
1154
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
1155
+ currentLayoutInfo = layoutInfo;
1156
+ });
1157
+
1158
+ await remoteMediaManager.start();
1159
+ await remoteMediaManager.setLayout('Stage');
1160
+
1161
+ assert.isNotNull(currentLayoutInfo);
1162
+
1163
+ if (currentLayoutInfo) {
1164
+ const remoteVideo = currentLayoutInfo.memberVideoPanes['stage-1'];
1165
+
1166
+ assert.throws(() => remoteMediaManager.pinActiveSpeakerVideoPane(remoteVideo));
1167
+ assert.throws(() => remoteMediaManager.isPinned(remoteVideo));
1168
+ }
1169
+ });
1170
+
1171
+ it('calls pin()/isPinned() on the correct remote media group', async () => {
1172
+ let currentLayoutInfo: VideoLayoutChangedEventData | null = null;
1173
+ let pinStub;
1174
+ let isPinnedStub;
1175
+
1176
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
1177
+ currentLayoutInfo = layoutInfo;
1178
+ pinStub = sinon.stub(layoutInfo.activeSpeakerVideoPanes.main, 'pin');
1179
+ isPinnedStub = sinon.stub(layoutInfo.activeSpeakerVideoPanes.main, 'isPinned');
1180
+ });
1181
+
1182
+ await remoteMediaManager.start();
1183
+
1184
+ assert.isNotNull(currentLayoutInfo);
1185
+
1186
+ if (currentLayoutInfo) {
1187
+ const remoteVideo = currentLayoutInfo.activeSpeakerVideoPanes.main.getRemoteMedia()[0];
1188
+
1189
+ // first test pinActiveSpeakerVideoPane()
1190
+ remoteMediaManager.pinActiveSpeakerVideoPane(remoteVideo);
1191
+
1192
+ assert.calledOnce(pinStub);
1193
+ assert.calledWith(pinStub, remoteVideo, undefined);
1194
+
1195
+ // now test isPinned()
1196
+ remoteMediaManager.isPinned(remoteVideo);
1197
+
1198
+ assert.calledOnce(isPinnedStub);
1199
+ assert.calledWith(isPinnedStub, remoteVideo);
1200
+ }
1201
+ });
1202
+ });
1203
+
1204
+ describe('unpinActiveSpeakerVideoPane', () => {
1205
+ it('throws if called on a remote media instance that was not pinned', async () => {
1206
+ let currentLayoutInfo: VideoLayoutChangedEventData | null = null;
1207
+
1208
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
1209
+ currentLayoutInfo = layoutInfo;
1210
+ });
1211
+
1212
+ await remoteMediaManager.start();
1213
+
1214
+ assert.isNotNull(currentLayoutInfo);
1215
+
1216
+ if (currentLayoutInfo) {
1217
+ const remoteVideoToUnPin =
1218
+ currentLayoutInfo.activeSpeakerVideoPanes.main.getRemoteMedia('unpinned')[0];
1219
+
1220
+ assert.throws(() => remoteMediaManager.unpinActiveSpeakerVideoPane(remoteVideoToUnPin));
1221
+ }
1222
+ });
1223
+
1224
+ it('calls unpin() on the correct remote media group', async () => {
1225
+ let currentLayoutInfo: VideoLayoutChangedEventData | null = null;
1226
+ let unpinStub;
1227
+
1228
+ remoteMediaManager.on(Event.VideoLayoutChanged, (layoutInfo: VideoLayoutChangedEventData) => {
1229
+ currentLayoutInfo = layoutInfo;
1230
+ unpinStub = sinon.stub(layoutInfo.activeSpeakerVideoPanes.main, 'unpin');
1231
+ });
1232
+
1233
+ await remoteMediaManager.start();
1234
+
1235
+ assert.isNotNull(currentLayoutInfo);
1236
+
1237
+ if (currentLayoutInfo) {
1238
+ const remoteVideo = currentLayoutInfo.activeSpeakerVideoPanes.main.getRemoteMedia()[0];
1239
+
1240
+ // first we need to pin it
1241
+ remoteMediaManager.pinActiveSpeakerVideoPane(remoteVideo, 99999);
1242
+
1243
+ // now we can unpin it
1244
+ remoteMediaManager.unpinActiveSpeakerVideoPane(remoteVideo);
1245
+
1246
+ assert.calledOnce(unpinStub);
1247
+ assert.calledWith(unpinStub, remoteVideo);
1248
+ }
1249
+ });
1250
+ });
1251
+ });