@webex/plugin-meetings 3.0.0-beta.154 → 3.0.0-beta.156

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.
@@ -9,7 +9,7 @@ import {
9
9
  getRecommendedMaxBitrateForFrameSize,
10
10
  RecommendedOpusBitrates,
11
11
  } from '@webex/internal-media-core';
12
- import {cloneDeep, debounce, isEmpty} from 'lodash';
12
+ import {cloneDeepWith, debounce, isEmpty} from 'lodash';
13
13
 
14
14
  import LoggerProxy from '../common/logs/logger-proxy';
15
15
 
@@ -72,7 +72,11 @@ type Kind = 'audio' | 'video';
72
72
  type Options = {
73
73
  degradationPreferences: DegradationPreferences;
74
74
  kind: Kind;
75
+ trimRequestsToNumOfSources: boolean; // if enabled, AS speaker requests will be trimmed based on the calls to setNumCurrentSources()
75
76
  };
77
+
78
+ type ClientRequestsMap = {[key: MediaRequestId]: MediaRequest};
79
+
76
80
  export class MediaRequestManager {
77
81
  private sendMediaRequestsCallback: SendMediaRequestsCallback;
78
82
 
@@ -80,7 +84,7 @@ export class MediaRequestManager {
80
84
 
81
85
  private counter: number;
82
86
 
83
- private clientRequests: {[key: MediaRequestId]: MediaRequest};
87
+ private clientRequests: ClientRequestsMap;
84
88
 
85
89
  private degradationPreferences: DegradationPreferences;
86
90
 
@@ -90,12 +94,19 @@ export class MediaRequestManager {
90
94
 
91
95
  private previousStreamRequests: Array<StreamRequest> = [];
92
96
 
97
+ private trimRequestsToNumOfSources: boolean;
98
+ private numTotalSources: number;
99
+ private numLiveSources: number;
100
+
93
101
  constructor(sendMediaRequestsCallback: SendMediaRequestsCallback, options: Options) {
94
102
  this.sendMediaRequestsCallback = sendMediaRequestsCallback;
95
103
  this.counter = 0;
104
+ this.numLiveSources = 0;
105
+ this.numTotalSources = 0;
96
106
  this.clientRequests = {};
97
107
  this.degradationPreferences = options.degradationPreferences;
98
108
  this.kind = options.kind;
109
+ this.trimRequestsToNumOfSources = options.trimRequestsToNumOfSources;
99
110
  this.sourceUpdateListener = this.commit.bind(this);
100
111
  this.debouncedSourceUpdateListener = debounce(
101
112
  this.sourceUpdateListener,
@@ -108,8 +119,7 @@ export class MediaRequestManager {
108
119
  this.sendRequests(); // re-send requests after preferences are set
109
120
  }
110
121
 
111
- private getDegradedClientRequests() {
112
- const clientRequests = cloneDeep(this.clientRequests);
122
+ private getDegradedClientRequests(clientRequests: ClientRequestsMap) {
113
123
  const maxFsLimits = [
114
124
  getMaxFs('best'),
115
125
  getMaxFs('large'),
@@ -122,7 +132,7 @@ export class MediaRequestManager {
122
132
  // reduce max-fs until total macroblocks is below limit
123
133
  for (let i = 0; i < maxFsLimits.length; i += 1) {
124
134
  let totalMacroblocksRequested = 0;
125
- Object.entries(clientRequests).forEach(([id, mr]) => {
135
+ Object.values(clientRequests).forEach((mr) => {
126
136
  if (mr.codecInfo) {
127
137
  mr.codecInfo.maxFs = Math.min(
128
138
  mr.preferredMaxFs || CODEC_DEFAULTS.h264.maxFs,
@@ -130,9 +140,7 @@ export class MediaRequestManager {
130
140
  maxFsLimits[i]
131
141
  );
132
142
  // we only consider sources with "live" state
133
- const slotsWithLiveSource = this.clientRequests[id].receiveSlots.filter(
134
- (rs) => rs.sourceState === 'live'
135
- );
143
+ const slotsWithLiveSource = mr.receiveSlots.filter((rs) => rs.sourceState === 'live');
136
144
  totalMacroblocksRequested += mr.codecInfo.maxFs * slotsWithLiveSource.length;
137
145
  }
138
146
  });
@@ -149,8 +157,6 @@ export class MediaRequestManager {
149
157
  );
150
158
  }
151
159
  }
152
-
153
- return clientRequests;
154
160
  }
155
161
 
156
162
  /**
@@ -231,42 +237,136 @@ export class MediaRequestManager {
231
237
  this.previousStreamRequests = [];
232
238
  }
233
239
 
240
+ /** Modifies the passed in clientRequests and makes sure that in total they don't ask
241
+ * for more streams than there are available.
242
+ *
243
+ * @param {Object} clientRequests
244
+ * @returns {void}
245
+ */
246
+ private trimRequests(clientRequests: ClientRequestsMap) {
247
+ const preferLiveVideo = this.getPreferLiveVideo();
248
+
249
+ if (!this.trimRequestsToNumOfSources) {
250
+ return;
251
+ }
252
+
253
+ // preferLiveVideo being undefined means that there are no active-speaker requests so we don't need to do any trimming
254
+ if (preferLiveVideo === undefined) {
255
+ return;
256
+ }
257
+
258
+ let numStreamsAvailable = preferLiveVideo ? this.numLiveSources : this.numTotalSources;
259
+
260
+ Object.values(clientRequests)
261
+ .sort((a, b) => {
262
+ // we have to count how many streams we're asking for
263
+ // and should not ask for more than numStreamsAvailable in total,
264
+ // so we might need to trim active-speaker requests and first ones to trim should be
265
+ // the ones with lowest priority
266
+
267
+ // receiver-selected requests have priority over active-speakers
268
+ if (a.policyInfo.policy === 'receiver-selected') {
269
+ return -1;
270
+ }
271
+ if (b.policyInfo.policy === 'receiver-selected') {
272
+ return 1;
273
+ }
274
+
275
+ // and active-speakers are sorted by descending priority
276
+ return b.policyInfo.priority - a.policyInfo.priority;
277
+ })
278
+ .forEach((request) => {
279
+ // we only trim active-speaker requests
280
+ if (request.policyInfo.policy === 'active-speaker') {
281
+ const trimmedCount = Math.min(numStreamsAvailable, request.receiveSlots.length);
282
+
283
+ request.receiveSlots.length = trimmedCount;
284
+
285
+ numStreamsAvailable -= trimmedCount;
286
+ } else {
287
+ numStreamsAvailable -= request.receiveSlots.length;
288
+ }
289
+
290
+ if (numStreamsAvailable < 0) {
291
+ numStreamsAvailable = 0;
292
+ }
293
+ });
294
+ }
295
+
296
+ private getPreferLiveVideo(): boolean | undefined {
297
+ let preferLiveVideo;
298
+
299
+ Object.values(this.clientRequests).forEach((mr) => {
300
+ if (mr.policyInfo.policy === 'active-speaker') {
301
+ // take the value from first encountered active speaker request
302
+ if (preferLiveVideo === undefined) {
303
+ preferLiveVideo = mr.policyInfo.preferLiveVideo;
304
+ }
305
+
306
+ if (mr.policyInfo.preferLiveVideo !== preferLiveVideo) {
307
+ throw new Error(
308
+ 'a mix of active-speaker groups with different values for preferLiveVideo is not supported'
309
+ );
310
+ }
311
+ }
312
+ });
313
+
314
+ return preferLiveVideo;
315
+ }
316
+
317
+ private cloneClientRequests(): ClientRequestsMap {
318
+ // we clone the client requests but without cloning the ReceiveSlots that they reference
319
+ return cloneDeepWith(this.clientRequests, (value, key) => {
320
+ if (key === 'receiveSlots') {
321
+ return [...value];
322
+ }
323
+
324
+ return undefined;
325
+ });
326
+ }
327
+
234
328
  private sendRequests() {
235
329
  const streamRequests: StreamRequest[] = [];
236
330
 
237
- const clientRequests = this.getDegradedClientRequests();
331
+ // clone the requests so that any modifications we do to them don't affect the original ones
332
+ const clientRequests = this.cloneClientRequests();
333
+
334
+ this.trimRequests(clientRequests);
335
+ this.getDegradedClientRequests(clientRequests);
238
336
 
239
337
  // map all the client media requests to wcme stream requests
240
338
  Object.values(clientRequests).forEach((mr) => {
241
- streamRequests.push(
242
- new StreamRequest(
243
- mr.policyInfo.policy === 'active-speaker'
244
- ? Policy.ActiveSpeaker
245
- : Policy.ReceiverSelected,
246
- mr.policyInfo.policy === 'active-speaker'
247
- ? new ActiveSpeakerInfo(
248
- mr.policyInfo.priority,
249
- mr.policyInfo.crossPriorityDuplication,
250
- mr.policyInfo.crossPolicyDuplication,
251
- mr.policyInfo.preferLiveVideo
252
- )
253
- : new ReceiverSelectedInfo(mr.policyInfo.csi),
254
- mr.receiveSlots.map((receiveSlot) => receiveSlot.wcmeReceiveSlot),
255
- this.getMaxPayloadBitsPerSecond(mr),
256
- mr.codecInfo && [
257
- new WcmeCodecInfo(
258
- 0x80,
259
- new H264Codec(
260
- mr.codecInfo.maxFs,
261
- mr.codecInfo.maxFps || CODEC_DEFAULTS.h264.maxFps,
262
- this.getH264MaxMbps(mr),
263
- mr.codecInfo.maxWidth,
264
- mr.codecInfo.maxHeight
265
- )
266
- ),
267
- ]
268
- )
269
- );
339
+ if (mr.receiveSlots.length > 0) {
340
+ streamRequests.push(
341
+ new StreamRequest(
342
+ mr.policyInfo.policy === 'active-speaker'
343
+ ? Policy.ActiveSpeaker
344
+ : Policy.ReceiverSelected,
345
+ mr.policyInfo.policy === 'active-speaker'
346
+ ? new ActiveSpeakerInfo(
347
+ mr.policyInfo.priority,
348
+ mr.policyInfo.crossPriorityDuplication,
349
+ mr.policyInfo.crossPolicyDuplication,
350
+ mr.policyInfo.preferLiveVideo
351
+ )
352
+ : new ReceiverSelectedInfo(mr.policyInfo.csi),
353
+ mr.receiveSlots.map((receiveSlot) => receiveSlot.wcmeReceiveSlot),
354
+ this.getMaxPayloadBitsPerSecond(mr),
355
+ mr.codecInfo && [
356
+ new WcmeCodecInfo(
357
+ 0x80,
358
+ new H264Codec(
359
+ mr.codecInfo.maxFs,
360
+ mr.codecInfo.maxFps || CODEC_DEFAULTS.h264.maxFps,
361
+ this.getH264MaxMbps(mr),
362
+ mr.codecInfo.maxWidth,
363
+ mr.codecInfo.maxHeight
364
+ )
365
+ ),
366
+ ]
367
+ )
368
+ );
369
+ }
270
370
  });
271
371
 
272
372
  //! IMPORTANT: this is only a temporary fix. This will soon be done in the jmp layer (@webex/json-multistream)
@@ -327,5 +427,14 @@ export class MediaRequestManager {
327
427
 
328
428
  public reset() {
329
429
  this.clientRequests = {};
430
+ this.numTotalSources = 0;
431
+ this.numLiveSources = 0;
432
+ }
433
+
434
+ public setNumCurrentSources(numTotalSources: number, numLiveSources: number) {
435
+ this.numTotalSources = numTotalSources;
436
+ this.numLiveSources = numLiveSources;
437
+
438
+ this.sendRequests();
330
439
  }
331
440
  }
@@ -186,6 +186,26 @@ describe('plugin-meetings', () => {
186
186
  });
187
187
  });
188
188
 
189
+ describe('#isNeedHandleRoster', () => {
190
+ it('return true if no sequence in locus/breakoutRosterLocus', () => {
191
+ breakout.breakoutRosterLocus = null;
192
+ assert.equal(breakout.isNeedHandleRoster(), true);
193
+
194
+ breakout.breakoutRosterLocus = {sequence: {entries: [123]}};
195
+ assert.equal(breakout.isNeedHandleRoster(null), true);
196
+
197
+ assert.equal(breakout.isNeedHandleRoster({sequence: {entries: []}}), true);
198
+ });
199
+ it('return true if the locus sequence is bigger than last one', () => {
200
+ breakout.breakoutRosterLocus = {sequence: {entries: [123]}};
201
+ assert.equal(breakout.isNeedHandleRoster({sequence: {entries: [124]}}), true);
202
+ });
203
+ it('return false if the locus sequence is smaller than last one', () => {
204
+ breakout.breakoutRosterLocus = {sequence: {entries: [123]}};
205
+ assert.equal(breakout.isNeedHandleRoster({sequence: {entries: [122]}}), false);
206
+ });
207
+ });
208
+
189
209
  describe('#parseRoster', () => {
190
210
  it('calls locusParticipantsUpdate', () => {
191
211
  breakout.members = {
@@ -197,7 +217,17 @@ describe('plugin-meetings', () => {
197
217
 
198
218
  assert.calledOnceWithExactly(breakout.members.locusParticipantsUpdate, locusData);
199
219
  assert.equal(result, undefined);
200
- })
220
+ });
221
+ it('not call locusParticipantsUpdate if sequence is expired', () => {
222
+ breakout.members = {
223
+ locusParticipantsUpdate: sinon.stub(),
224
+ };
225
+ breakout.isNeedHandleRoster = sinon.stub().returns(false);
226
+ const locusData = {some: 'data'};
227
+ breakout.parseRoster(locusData);
228
+
229
+ assert.notCalled(breakout.members.locusParticipantsUpdate);
230
+ });
201
231
  })
202
232
  });
203
233
  });
@@ -1375,6 +1375,16 @@ describe('plugin-meetings', () => {
1375
1375
  );
1376
1376
  assert.calledOnce(fakeMediaConnection.initiateOffer);
1377
1377
  });
1378
+
1379
+ it('succeeds even if getDevices() throws', async () => {
1380
+ meeting.meetingState = 'ACTIVE';
1381
+
1382
+ sinon
1383
+ .stub(internalMediaModule, 'getDevices')
1384
+ .rejects(new Error('fake error'));
1385
+
1386
+ await meeting.addMedia();
1387
+ })
1378
1388
  });
1379
1389
 
1380
1390
  /* This set of tests are like semi-integration tests, they use real MuteState, Media, LocusMediaRequest and Roap classes.
@@ -4265,6 +4275,81 @@ describe('plugin-meetings', () => {
4265
4275
  });
4266
4276
  });
4267
4277
  });
4278
+
4279
+ describe('audio and video source count change events', () => {
4280
+ beforeEach(() => {
4281
+ TriggerProxy.trigger.resetHistory();
4282
+ meeting.setupMediaConnectionListeners();
4283
+ });
4284
+
4285
+ it('registers for audio and video source count changed', () => {
4286
+ assert.isFunction(eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED]);
4287
+ assert.isFunction(eventListeners[Event.AUDIO_SOURCES_COUNT_CHANGED]);
4288
+ })
4289
+
4290
+ it('forwards the VIDEO_SOURCES_COUNT_CHANGED event as "media:remoteVideoSourceCountChanged"', () => {
4291
+ const numTotalSources = 10;
4292
+ const numLiveSources = 6;
4293
+ const mediaContent = 'SLIDES';
4294
+
4295
+ sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources');
4296
+
4297
+ eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](numTotalSources, numLiveSources, mediaContent);
4298
+
4299
+ assert.calledOnceWithExactly(TriggerProxy.trigger,
4300
+ meeting,
4301
+ sinon.match.object,
4302
+ 'media:remoteVideoSourceCountChanged',
4303
+ {
4304
+ numTotalSources,
4305
+ numLiveSources,
4306
+ mediaContent,
4307
+ }
4308
+ );
4309
+ });
4310
+
4311
+ it('forwards the AUDIO_SOURCES_COUNT_CHANGED event as "media:remoteAudioSourceCountChanged"', () => {
4312
+ const numTotalSources = 5;
4313
+ const numLiveSources = 2;
4314
+ const mediaContent = 'MAIN';
4315
+
4316
+ eventListeners[Event.AUDIO_SOURCES_COUNT_CHANGED](numTotalSources, numLiveSources, mediaContent);
4317
+
4318
+ assert.calledOnceWithExactly(TriggerProxy.trigger,
4319
+ meeting,
4320
+ sinon.match.object,
4321
+ 'media:remoteAudioSourceCountChanged',
4322
+ {
4323
+ numTotalSources,
4324
+ numLiveSources,
4325
+ mediaContent,
4326
+ }
4327
+ );
4328
+ });
4329
+
4330
+ it('calls setNumCurrentSources() when receives VIDEO_SOURCES_COUNT_CHANGED event for MAIN', () => {
4331
+ const numTotalSources = 20;
4332
+ const numLiveSources = 10;
4333
+
4334
+ const setNumCurrentSourcesSpy = sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources');
4335
+
4336
+ eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](numTotalSources, numLiveSources, 'MAIN');
4337
+
4338
+ assert.calledOnceWithExactly(setNumCurrentSourcesSpy, numTotalSources, numLiveSources);
4339
+ });
4340
+
4341
+ it('does not call setNumCurrentSources() when receives VIDEO_SOURCES_COUNT_CHANGED event for SLIDES', () => {
4342
+ const numTotalSources = 20;
4343
+ const numLiveSources = 10;
4344
+
4345
+ const setNumCurrentSourcesSpy = sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources');
4346
+
4347
+ eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](numTotalSources, numLiveSources, 'SLIDES');
4348
+
4349
+ assert.notCalled(setNumCurrentSourcesSpy);
4350
+ });
4351
+
4352
+ })
4268
4353
  });
4269
4354
  describe('#setUpLocusInfoSelfListener', () => {
4270
4355
  it('listens to the self unadmitted guest event', (done) => {
@@ -431,10 +431,8 @@ describe('plugin-meetings', () => {
431
431
  it('creates noise reduction effect with custom options passed', async () => {
432
432
  const effectOptions = {
433
433
  audioContext: {},
434
- workletProcessorUrl: "test.url.com",
435
434
  mode: "WORKLET",
436
- env: "prod",
437
- avoidSimd: false
435
+ env: "prod"
438
436
  };
439
437
 
440
438
  const result = await webex.meetings.createNoiseReductionEffect(effectOptions);