@webex/plugin-meetings 3.0.0-beta.154 → 3.0.0-beta.155
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/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/meeting/index.js +14 -6
- package/dist/meeting/index.js.map +1 -1
- package/dist/meetings/meetings.types.js.map +1 -1
- package/dist/multistream/mediaRequestManager.js +111 -18
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/types/meetings/meetings.types.d.ts +1 -1
- package/dist/types/multistream/mediaRequestManager.d.ts +14 -0
- package/package.json +20 -20
- package/src/meeting/index.ts +16 -4
- package/src/meetings/meetings.types.ts +4 -1
- package/src/multistream/mediaRequestManager.ts +149 -40
- package/test/unit/spec/meeting/index.js +85 -0
- package/test/unit/spec/meetings/index.js +1 -3
- package/test/unit/spec/multistream/mediaRequestManager.ts +341 -9
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
getRecommendedMaxBitrateForFrameSize,
|
|
10
10
|
RecommendedOpusBitrates,
|
|
11
11
|
} from '@webex/internal-media-core';
|
|
12
|
-
import {
|
|
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:
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
}
|
|
@@ -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);
|