@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.
- package/dist/common/errors/webex-errors.js +5 -29
- package/dist/common/errors/webex-errors.js.map +1 -1
- package/dist/constants.js +15 -74
- package/dist/constants.js.map +1 -1
- package/dist/media/index.js +68 -213
- package/dist/media/index.js.map +1 -1
- package/dist/media/internal-media-core-wrapper.js +22 -0
- package/dist/media/internal-media-core-wrapper.js.map +1 -0
- package/dist/media/properties.js +20 -25
- package/dist/media/properties.js.map +1 -1
- package/dist/media/util.js +0 -27
- package/dist/media/util.js.map +1 -1
- package/dist/meeting/index.js +694 -432
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.js +1 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/util.js +3 -44
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +64 -5
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/util.js +24 -1
- package/dist/meetings/util.js.map +1 -1
- package/dist/members/index.js +68 -0
- package/dist/members/index.js.map +1 -1
- package/dist/multistream/mediaRequestManager.js +133 -0
- package/dist/multistream/mediaRequestManager.js.map +1 -0
- package/dist/multistream/multistreamMedia.js +116 -0
- package/dist/multistream/multistreamMedia.js.map +1 -0
- package/dist/multistream/receiveSlot.js +209 -0
- package/dist/multistream/receiveSlot.js.map +1 -0
- package/dist/multistream/receiveSlotManager.js +195 -0
- package/dist/multistream/receiveSlotManager.js.map +1 -0
- package/dist/multistream/remoteMedia.js +284 -0
- package/dist/multistream/remoteMedia.js.map +1 -0
- package/dist/multistream/remoteMediaGroup.js +243 -0
- package/dist/multistream/remoteMediaGroup.js.map +1 -0
- package/dist/multistream/remoteMediaManager.js +1113 -0
- package/dist/multistream/remoteMediaManager.js.map +1 -0
- package/dist/reconnection-manager/index.js +109 -130
- package/dist/reconnection-manager/index.js.map +1 -1
- package/dist/roap/index.js +57 -240
- package/dist/roap/index.js.map +1 -1
- package/dist/roap/request.js +2 -114
- package/dist/roap/request.js.map +1 -1
- package/dist/roap/turnDiscovery.js +11 -5
- package/dist/roap/turnDiscovery.js.map +1 -1
- package/dist/statsAnalyzer/global.js +2 -0
- package/dist/statsAnalyzer/global.js.map +1 -1
- package/dist/statsAnalyzer/index.js +39 -36
- package/dist/statsAnalyzer/index.js.map +1 -1
- package/package.json +20 -19
- package/src/common/errors/webex-errors.js +0 -18
- package/src/constants.ts +139 -180
- package/src/media/index.js +60 -194
- package/src/media/internal-media-core-wrapper.ts +9 -0
- package/src/media/properties.js +19 -25
- package/src/media/util.js +0 -22
- package/src/meeting/index.js +565 -320
- package/src/meeting/request.js +1 -0
- package/src/meeting/util.js +3 -46
- package/src/meetings/index.js +30 -1
- package/src/meetings/util.js +23 -2
- package/src/members/index.js +48 -0
- package/src/multistream/mediaRequestManager.ts +166 -0
- package/src/multistream/multistreamMedia.ts +92 -0
- package/src/multistream/receiveSlot.ts +141 -0
- package/src/multistream/receiveSlotManager.ts +142 -0
- package/src/multistream/remoteMedia.ts +219 -0
- package/src/multistream/remoteMediaGroup.ts +224 -0
- package/src/multistream/remoteMediaManager.ts +911 -0
- package/src/reconnection-manager/index.js +40 -53
- package/src/roap/index.js +47 -207
- package/src/roap/request.js +1 -72
- package/src/roap/turnDiscovery.ts +12 -6
- package/src/statsAnalyzer/global.js +2 -0
- package/src/statsAnalyzer/index.js +32 -46
- package/test/integration/spec/journey.js +1 -1
- package/test/unit/spec/media/index.ts +223 -0
- package/test/unit/spec/media/properties.ts +73 -82
- package/test/unit/spec/meeting/effectsState.js +1 -3
- package/test/unit/spec/meeting/index.js +420 -228
- package/test/unit/spec/meeting/muteState.js +7 -0
- package/test/unit/spec/meeting/utils.js +61 -2
- package/test/unit/spec/meetings/index.js +0 -4
- package/test/unit/spec/members/index.js +164 -2
- package/test/unit/spec/multistream/mediaRequestManager.ts +515 -0
- package/test/unit/spec/multistream/receiveSlot.ts +104 -0
- package/test/unit/spec/multistream/receiveSlotManager.ts +173 -0
- package/test/unit/spec/multistream/remoteMedia.ts +217 -0
- package/test/unit/spec/multistream/remoteMediaGroup.ts +396 -0
- package/test/unit/spec/multistream/remoteMediaManager.ts +1251 -0
- package/test/unit/spec/roap/index.ts +63 -35
- package/test/unit/spec/stats-analyzer/index.js +19 -22
- package/dist/peer-connection-manager/index.js +0 -794
- package/dist/peer-connection-manager/index.js.map +0 -1
- package/dist/roap/collection.js +0 -73
- package/dist/roap/collection.js.map +0 -1
- package/dist/roap/handler.js +0 -337
- package/dist/roap/handler.js.map +0 -1
- package/dist/roap/state.js +0 -164
- package/dist/roap/state.js.map +0 -1
- package/dist/roap/util.js +0 -102
- package/dist/roap/util.js.map +0 -1
- package/src/peer-connection-manager/index.js +0 -723
- package/src/roap/collection.js +0 -63
- package/src/roap/handler.js +0 -252
- package/src/roap/state.js +0 -149
- package/src/roap/util.js +0 -93
- package/test/unit/spec/peerconnection-manager/index.js +0 -188
- package/test/unit/spec/peerconnection-manager/utils.js +0 -48
- 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
|
+
});
|