@webex/plugin-meetings 3.12.0-next.58 → 3.12.0-next.59
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/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/media/index.js +3 -1
- package/dist/media/index.js.map +1 -1
- package/dist/meeting/index.js +37 -10
- package/dist/meeting/index.js.map +1 -1
- package/dist/meetings/index.js +23 -0
- package/dist/meetings/index.js.map +1 -1
- package/dist/multistream/codec/constants.js +63 -0
- package/dist/multistream/codec/constants.js.map +1 -0
- package/dist/multistream/mediaRequestManager.js +62 -15
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/meeting/index.d.ts +9 -0
- package/dist/types/meetings/index.d.ts +10 -0
- package/dist/types/multistream/codec/constants.d.ts +7 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
- package/dist/webinar/index.js +1 -1
- package/package.json +1 -1
- package/src/config.ts +1 -0
- package/src/media/index.ts +3 -0
- package/src/meeting/index.ts +41 -2
- package/src/meetings/index.ts +21 -0
- package/src/multistream/codec/constants.ts +58 -0
- package/src/multistream/mediaRequestManager.ts +119 -28
- package/test/unit/spec/media/index.ts +31 -0
- package/test/unit/spec/meeting/index.js +154 -0
- package/test/unit/spec/meetings/index.js +27 -0
- package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
package/src/meetings/index.ts
CHANGED
|
@@ -935,6 +935,27 @@ export default class Meetings extends WebexPlugin {
|
|
|
935
935
|
}
|
|
936
936
|
}
|
|
937
937
|
|
|
938
|
+
/**
|
|
939
|
+
* API to toggle AV1 codec support for video slides in multistream,
|
|
940
|
+
* needs to be called before webex.meetings.joinWithMedia()
|
|
941
|
+
*
|
|
942
|
+
* @param {Boolean} newValue
|
|
943
|
+
* @private
|
|
944
|
+
* @memberof Meetings
|
|
945
|
+
* @returns {undefined}
|
|
946
|
+
*/
|
|
947
|
+
private _toggleEnableAv1SlidesSupport(newValue: boolean) {
|
|
948
|
+
if (typeof newValue !== 'boolean') {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// @ts-ignore
|
|
953
|
+
if (this.config.enableAv1SlidesSupport !== newValue) {
|
|
954
|
+
// @ts-ignore
|
|
955
|
+
this.config.enableAv1SlidesSupport = newValue;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
938
959
|
/**
|
|
939
960
|
* API to toggle stopping ICE Candidates Gathering after first relay candidate,
|
|
940
961
|
* needs to be called before webex.meetings.joinWithMedia()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {AV1EncodingParams, SupportedResolution} from '@webex/internal-media-core';
|
|
2
|
+
|
|
3
|
+
export const AV1_CODEC_PARAMETERS: Record<SupportedResolution, AV1EncodingParams> = {
|
|
4
|
+
'90p': {
|
|
5
|
+
levelIdx: 0,
|
|
6
|
+
tier: 0,
|
|
7
|
+
maxWidth: 160,
|
|
8
|
+
maxHeight: 90,
|
|
9
|
+
maxPicSize: 160 * 90,
|
|
10
|
+
maxDecodeRate: 5_529_600,
|
|
11
|
+
},
|
|
12
|
+
'180p': {
|
|
13
|
+
levelIdx: 0,
|
|
14
|
+
tier: 0,
|
|
15
|
+
maxWidth: 320,
|
|
16
|
+
maxHeight: 180,
|
|
17
|
+
maxPicSize: 320 * 180,
|
|
18
|
+
maxDecodeRate: 5_529_600,
|
|
19
|
+
},
|
|
20
|
+
'360p': {
|
|
21
|
+
levelIdx: 1,
|
|
22
|
+
tier: 0,
|
|
23
|
+
maxWidth: 640,
|
|
24
|
+
maxHeight: 360,
|
|
25
|
+
maxPicSize: 640 * 360,
|
|
26
|
+
maxDecodeRate: 10_454_400,
|
|
27
|
+
},
|
|
28
|
+
'540p': {
|
|
29
|
+
levelIdx: 4,
|
|
30
|
+
tier: 0,
|
|
31
|
+
maxWidth: 960,
|
|
32
|
+
maxHeight: 540,
|
|
33
|
+
maxPicSize: 960 * 540,
|
|
34
|
+
maxDecodeRate: 24_969_600,
|
|
35
|
+
},
|
|
36
|
+
'720p': {
|
|
37
|
+
levelIdx: 5,
|
|
38
|
+
tier: 0,
|
|
39
|
+
maxWidth: 1280,
|
|
40
|
+
maxHeight: 720,
|
|
41
|
+
maxPicSize: 1280 * 720,
|
|
42
|
+
maxDecodeRate: 39_938_400,
|
|
43
|
+
},
|
|
44
|
+
'1080p': {
|
|
45
|
+
levelIdx: 8,
|
|
46
|
+
tier: 0,
|
|
47
|
+
maxWidth: 1920,
|
|
48
|
+
maxHeight: 1080,
|
|
49
|
+
maxPicSize: 1920 * 1080,
|
|
50
|
+
maxDecodeRate: 77_856_768,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const H264_CODEC_PARAMETERS = {
|
|
55
|
+
maxFs: 8192,
|
|
56
|
+
maxFps: 3000,
|
|
57
|
+
maxMbps: 245760,
|
|
58
|
+
};
|
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
getRecommendedMaxBitrateForFrameSize,
|
|
10
10
|
RecommendedOpusBitrates,
|
|
11
11
|
NamedMediaGroup,
|
|
12
|
+
AV1Codec,
|
|
13
|
+
SupportedResolution,
|
|
14
|
+
AV1EncodingParams,
|
|
15
|
+
MediaType,
|
|
16
|
+
MediaCodecMimeType,
|
|
12
17
|
} from '@webex/internal-media-core';
|
|
13
18
|
import {cloneDeepWith, debounce} from 'lodash';
|
|
14
19
|
|
|
@@ -16,6 +21,7 @@ import LoggerProxy from '../common/logs/logger-proxy';
|
|
|
16
21
|
|
|
17
22
|
import {ReceiveSlot, ReceiveSlotEvents} from './receiveSlot';
|
|
18
23
|
import {MAX_FS_VALUES} from './remoteMedia';
|
|
24
|
+
import {AV1_CODEC_PARAMETERS, H264_CODEC_PARAMETERS} from './codec/constants';
|
|
19
25
|
|
|
20
26
|
export interface ActiveSpeakerPolicyInfo {
|
|
21
27
|
policy: 'active-speaker';
|
|
@@ -54,34 +60,49 @@ export interface MediaRequest {
|
|
|
54
60
|
|
|
55
61
|
export type MediaRequestId = string;
|
|
56
62
|
|
|
57
|
-
const CODEC_DEFAULTS = {
|
|
58
|
-
h264: {
|
|
59
|
-
maxFs: 8192,
|
|
60
|
-
maxFps: 3000,
|
|
61
|
-
maxMbps: 245760,
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
63
|
const DEBOUNCED_SOURCE_UPDATE_TIME = 1000;
|
|
66
64
|
|
|
65
|
+
const RESOLUTION_BUCKETS: Array<[SupportedResolution, number]> = [
|
|
66
|
+
['90p', MAX_FS_VALUES['90p']],
|
|
67
|
+
['180p', MAX_FS_VALUES['180p']],
|
|
68
|
+
['360p', MAX_FS_VALUES['360p']],
|
|
69
|
+
['540p', MAX_FS_VALUES['540p']],
|
|
70
|
+
['720p', MAX_FS_VALUES['720p']],
|
|
71
|
+
];
|
|
72
|
+
|
|
67
73
|
type DegradationPreferences = {
|
|
68
74
|
maxMacroblocksLimit: number;
|
|
69
75
|
};
|
|
70
76
|
|
|
71
77
|
type SendMediaRequestsCallback = (streamRequests: StreamRequest[]) => void;
|
|
78
|
+
type GetIngressPayloadTypeCallback = (
|
|
79
|
+
mediaType: MediaType,
|
|
80
|
+
codecMimeType: MediaCodecMimeType
|
|
81
|
+
) => number | undefined;
|
|
72
82
|
type Kind = 'audio' | 'video';
|
|
73
83
|
|
|
74
|
-
type
|
|
84
|
+
type AudioMediaRequestManagerOptions = {
|
|
75
85
|
degradationPreferences: DegradationPreferences;
|
|
76
|
-
kind:
|
|
86
|
+
kind: 'audio';
|
|
77
87
|
trimRequestsToNumOfSources: boolean; // if enabled, AS speaker requests will be trimmed based on the calls to setNumCurrentSources()
|
|
78
88
|
};
|
|
79
89
|
|
|
90
|
+
type VideoMediaRequestManagerOptions = {
|
|
91
|
+
degradationPreferences: DegradationPreferences;
|
|
92
|
+
kind: 'video';
|
|
93
|
+
trimRequestsToNumOfSources: boolean;
|
|
94
|
+
enableAv1?: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type Options = AudioMediaRequestManagerOptions | VideoMediaRequestManagerOptions;
|
|
98
|
+
|
|
80
99
|
type ClientRequestsMap = {[key: MediaRequestId]: MediaRequest};
|
|
81
100
|
|
|
82
101
|
export class MediaRequestManager {
|
|
83
102
|
private sendMediaRequestsCallback: SendMediaRequestsCallback;
|
|
84
103
|
|
|
104
|
+
private getIngressPayloadTypeCallback: GetIngressPayloadTypeCallback;
|
|
105
|
+
|
|
85
106
|
private kind: Kind;
|
|
86
107
|
|
|
87
108
|
private counter: number;
|
|
@@ -95,11 +116,17 @@ export class MediaRequestManager {
|
|
|
95
116
|
private debouncedSourceUpdateListener: () => void;
|
|
96
117
|
|
|
97
118
|
private trimRequestsToNumOfSources: boolean;
|
|
119
|
+
private enableAv1: boolean;
|
|
98
120
|
private numTotalSources: number;
|
|
99
121
|
private numLiveSources: number;
|
|
100
122
|
|
|
101
|
-
constructor(
|
|
123
|
+
constructor(
|
|
124
|
+
sendMediaRequestsCallback: SendMediaRequestsCallback,
|
|
125
|
+
getIngressPayloadTypeCallback: GetIngressPayloadTypeCallback,
|
|
126
|
+
options: Options
|
|
127
|
+
) {
|
|
102
128
|
this.sendMediaRequestsCallback = sendMediaRequestsCallback;
|
|
129
|
+
this.getIngressPayloadTypeCallback = getIngressPayloadTypeCallback;
|
|
103
130
|
this.counter = 0;
|
|
104
131
|
this.numLiveSources = 0;
|
|
105
132
|
this.numTotalSources = 0;
|
|
@@ -107,6 +134,7 @@ export class MediaRequestManager {
|
|
|
107
134
|
this.degradationPreferences = options.degradationPreferences;
|
|
108
135
|
this.kind = options.kind;
|
|
109
136
|
this.trimRequestsToNumOfSources = options.trimRequestsToNumOfSources;
|
|
137
|
+
this.enableAv1 = options.kind === 'video' && !!options.enableAv1;
|
|
110
138
|
this.sourceUpdateListener = this.commit.bind(this);
|
|
111
139
|
this.debouncedSourceUpdateListener = debounce(
|
|
112
140
|
this.sourceUpdateListener,
|
|
@@ -135,8 +163,8 @@ export class MediaRequestManager {
|
|
|
135
163
|
Object.values(clientRequests).forEach((mr) => {
|
|
136
164
|
if (mr.codecInfo) {
|
|
137
165
|
mr.codecInfo.maxFs = Math.min(
|
|
138
|
-
mr.preferredMaxFs ||
|
|
139
|
-
mr.codecInfo.maxFs ||
|
|
166
|
+
mr.preferredMaxFs || H264_CODEC_PARAMETERS.maxFs,
|
|
167
|
+
mr.codecInfo.maxFs || H264_CODEC_PARAMETERS.maxFs,
|
|
140
168
|
maxFsLimits[i]
|
|
141
169
|
);
|
|
142
170
|
// we only consider sources with "live" state
|
|
@@ -176,7 +204,7 @@ export class MediaRequestManager {
|
|
|
176
204
|
}
|
|
177
205
|
|
|
178
206
|
return getRecommendedMaxBitrateForFrameSize(
|
|
179
|
-
mediaRequest.codecInfo.maxFs ||
|
|
207
|
+
mediaRequest.codecInfo.maxFs || H264_CODEC_PARAMETERS.maxFs
|
|
180
208
|
);
|
|
181
209
|
}
|
|
182
210
|
|
|
@@ -192,12 +220,80 @@ export class MediaRequestManager {
|
|
|
192
220
|
// eslint-disable-next-line class-methods-use-this
|
|
193
221
|
private getH264MaxMbps(mediaRequest: MediaRequest): number {
|
|
194
222
|
// fallback for maxFps (not needed for maxFs, since there is a fallback already in getDegradedClientRequests)
|
|
195
|
-
const maxFps = mediaRequest.codecInfo.maxFps ||
|
|
223
|
+
const maxFps = mediaRequest.codecInfo.maxFps || H264_CODEC_PARAMETERS.maxFps;
|
|
196
224
|
|
|
197
225
|
// divided by 100 since maxFps is 3000 (for 30 frames per seconds)
|
|
198
226
|
return (mediaRequest.codecInfo.maxFs * maxFps) / 100;
|
|
199
227
|
}
|
|
200
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Returns the AV1 encoding parameters for a media request
|
|
231
|
+
* @param mediaRequest - The media request to get the AV1 encoding parameters for
|
|
232
|
+
* @returns {AV1EncodingParams} The AV1 encoding parameters
|
|
233
|
+
*/
|
|
234
|
+
// eslint-disable-next-line class-methods-use-this
|
|
235
|
+
private getAv1EncodingParams(mediaRequest: MediaRequest): AV1EncodingParams {
|
|
236
|
+
const frameSize = mediaRequest.codecInfo.maxFs || H264_CODEC_PARAMETERS.maxFs;
|
|
237
|
+
const resolution = RESOLUTION_BUCKETS.find(([, maxFs]) => frameSize <= maxFs)?.[0] ?? '1080p';
|
|
238
|
+
|
|
239
|
+
return AV1_CODEC_PARAMETERS[resolution];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private buildH264CodecInfo(mr: MediaRequest): WcmeCodecInfo | undefined {
|
|
243
|
+
if (!mr.codecInfo) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const h264PayloadType = this.getIngressPayloadTypeCallback(
|
|
248
|
+
mr.receiveSlots[0].mediaType,
|
|
249
|
+
MediaCodecMimeType.H264
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (h264PayloadType === undefined) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return WcmeCodecInfo.fromH264(
|
|
257
|
+
h264PayloadType,
|
|
258
|
+
new H264Codec(
|
|
259
|
+
mr.codecInfo.maxFs,
|
|
260
|
+
mr.codecInfo.maxFps || H264_CODEC_PARAMETERS.maxFps,
|
|
261
|
+
this.getH264MaxMbps(mr),
|
|
262
|
+
mr.codecInfo.maxWidth,
|
|
263
|
+
mr.codecInfo.maxHeight
|
|
264
|
+
)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private buildAv1CodecInfo(mr: MediaRequest): WcmeCodecInfo | undefined {
|
|
269
|
+
if (!this.enableAv1 || !mr.codecInfo) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const av1PayloadType = this.getIngressPayloadTypeCallback(
|
|
274
|
+
mr.receiveSlots[0].mediaType,
|
|
275
|
+
MediaCodecMimeType.AV1
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (av1PayloadType === undefined) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const av1EncodingParams = this.getAv1EncodingParams(mr);
|
|
283
|
+
|
|
284
|
+
return WcmeCodecInfo.fromAv1(
|
|
285
|
+
av1PayloadType,
|
|
286
|
+
new AV1Codec(
|
|
287
|
+
av1EncodingParams.levelIdx,
|
|
288
|
+
av1EncodingParams.tier,
|
|
289
|
+
mr.codecInfo.maxWidth || av1EncodingParams.maxWidth,
|
|
290
|
+
mr.codecInfo.maxHeight || av1EncodingParams.maxHeight,
|
|
291
|
+
av1EncodingParams.maxPicSize,
|
|
292
|
+
av1EncodingParams.maxDecodeRate
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
201
297
|
/** Modifies the passed in clientRequests and makes sure that in total they don't ask
|
|
202
298
|
* for more streams than there are available.
|
|
203
299
|
*
|
|
@@ -298,6 +394,12 @@ export class MediaRequestManager {
|
|
|
298
394
|
// map all the client media requests to wcme stream requests
|
|
299
395
|
Object.values(clientRequests).forEach((mr) => {
|
|
300
396
|
if (mr.receiveSlots.length > 0) {
|
|
397
|
+
const codecInfos: WcmeCodecInfo[] = mr.codecInfo
|
|
398
|
+
? [this.buildH264CodecInfo(mr), this.buildAv1CodecInfo(mr)].filter(
|
|
399
|
+
(info): info is WcmeCodecInfo => info !== undefined
|
|
400
|
+
)
|
|
401
|
+
: [];
|
|
402
|
+
|
|
301
403
|
streamRequests.push(
|
|
302
404
|
new StreamRequest(
|
|
303
405
|
mr.policyInfo.policy === 'active-speaker'
|
|
@@ -314,25 +416,14 @@ export class MediaRequestManager {
|
|
|
314
416
|
: new ReceiverSelectedInfo(mr.policyInfo.csi),
|
|
315
417
|
mr.receiveSlots.map((receiveSlot) => receiveSlot.wcmeReceiveSlot),
|
|
316
418
|
this.getMaxPayloadBitsPerSecond(mr),
|
|
317
|
-
|
|
318
|
-
WcmeCodecInfo.fromH264(
|
|
319
|
-
0x80,
|
|
320
|
-
new H264Codec(
|
|
321
|
-
mr.codecInfo.maxFs,
|
|
322
|
-
mr.codecInfo.maxFps || CODEC_DEFAULTS.h264.maxFps,
|
|
323
|
-
this.getH264MaxMbps(mr),
|
|
324
|
-
mr.codecInfo.maxWidth,
|
|
325
|
-
mr.codecInfo.maxHeight
|
|
326
|
-
)
|
|
327
|
-
),
|
|
328
|
-
]
|
|
419
|
+
codecInfos
|
|
329
420
|
)
|
|
330
421
|
);
|
|
331
422
|
}
|
|
332
423
|
});
|
|
333
424
|
|
|
334
425
|
this.sendMediaRequestsCallback(streamRequests);
|
|
335
|
-
LoggerProxy.logger.info(`multistream:sendRequests --> media requests sent
|
|
426
|
+
LoggerProxy.logger.info(`multistream:sendRequests --> media requests sent.`);
|
|
336
427
|
}
|
|
337
428
|
|
|
338
429
|
public addRequest(mediaRequest: MediaRequest, commit = true): MediaRequestId {
|
|
@@ -295,6 +295,7 @@ describe('createMediaConnection', () => {
|
|
|
295
295
|
bundlePolicy: 'max-bundle',
|
|
296
296
|
disableAudioMainDtx: false,
|
|
297
297
|
disableAudioTwcc: false,
|
|
298
|
+
enableAV1SlidesSupport: false,
|
|
298
299
|
},
|
|
299
300
|
'meeting id'
|
|
300
301
|
);
|
|
@@ -322,6 +323,26 @@ describe('createMediaConnection', () => {
|
|
|
322
323
|
assert.calledOnce(rtcMetrics.sendMetricsInQueue);
|
|
323
324
|
});
|
|
324
325
|
|
|
326
|
+
it('passes enableAV1SlidesSupport: true to MultistreamRoapMediaConnection when enableAv1SlidesSupport is set', () => {
|
|
327
|
+
const multistreamRoapMediaConnectionConstructorStub = sinon
|
|
328
|
+
.stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
|
|
329
|
+
.returns(fakeRoapMediaConnection);
|
|
330
|
+
|
|
331
|
+
Media.createMediaConnection(true, 'some debug id', 'meeting id', {
|
|
332
|
+
enableAv1SlidesSupport: true,
|
|
333
|
+
});
|
|
334
|
+
assert.calledOnce(multistreamRoapMediaConnectionConstructorStub);
|
|
335
|
+
assert.calledWith(
|
|
336
|
+
multistreamRoapMediaConnectionConstructorStub,
|
|
337
|
+
sinon.match({
|
|
338
|
+
iceServers: [],
|
|
339
|
+
disableAudioTwcc: true,
|
|
340
|
+
enableAV1SlidesSupport: true,
|
|
341
|
+
}),
|
|
342
|
+
'meeting id'
|
|
343
|
+
);
|
|
344
|
+
});
|
|
345
|
+
|
|
325
346
|
it('multistream non-firefox does not care about stopIceGatheringAfterFirstRelayCandidate', () => {
|
|
326
347
|
const multistreamRoapMediaConnectionConstructorStub = sinon
|
|
327
348
|
.stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
|
|
@@ -336,6 +357,7 @@ describe('createMediaConnection', () => {
|
|
|
336
357
|
{
|
|
337
358
|
iceServers: [],
|
|
338
359
|
disableAudioTwcc: true,
|
|
360
|
+
enableAV1SlidesSupport: false,
|
|
339
361
|
},
|
|
340
362
|
'meeting id'
|
|
341
363
|
);
|
|
@@ -359,6 +381,7 @@ describe('createMediaConnection', () => {
|
|
|
359
381
|
doFullIce: true,
|
|
360
382
|
stopIceGatheringAfterFirstRelayCandidate: true,
|
|
361
383
|
disableAudioTwcc: true,
|
|
384
|
+
enableAV1SlidesSupport: false,
|
|
362
385
|
},
|
|
363
386
|
'meeting id'
|
|
364
387
|
);
|
|
@@ -382,6 +405,7 @@ describe('createMediaConnection', () => {
|
|
|
382
405
|
doFullIce: true,
|
|
383
406
|
stopIceGatheringAfterFirstRelayCandidate: false,
|
|
384
407
|
disableAudioTwcc: true,
|
|
408
|
+
enableAV1SlidesSupport: false,
|
|
385
409
|
},
|
|
386
410
|
'meeting id'
|
|
387
411
|
);
|
|
@@ -418,6 +442,7 @@ describe('createMediaConnection', () => {
|
|
|
418
442
|
{
|
|
419
443
|
iceServers: [],
|
|
420
444
|
disableAudioTwcc: true,
|
|
445
|
+
enableAV1SlidesSupport: false,
|
|
421
446
|
},
|
|
422
447
|
'meeting id'
|
|
423
448
|
);
|
|
@@ -448,6 +473,7 @@ describe('createMediaConnection', () => {
|
|
|
448
473
|
{
|
|
449
474
|
iceServers: [],
|
|
450
475
|
disableAudioTwcc: true,
|
|
476
|
+
enableAV1SlidesSupport: false,
|
|
451
477
|
},
|
|
452
478
|
'meeting id'
|
|
453
479
|
);
|
|
@@ -477,6 +503,7 @@ describe('createMediaConnection', () => {
|
|
|
477
503
|
{
|
|
478
504
|
iceServers: [],
|
|
479
505
|
disableAudioTwcc: true,
|
|
506
|
+
enableAV1SlidesSupport: false,
|
|
480
507
|
},
|
|
481
508
|
'meeting id'
|
|
482
509
|
);
|
|
@@ -505,6 +532,7 @@ describe('createMediaConnection', () => {
|
|
|
505
532
|
{
|
|
506
533
|
iceServers: [],
|
|
507
534
|
disableAudioTwcc: true,
|
|
535
|
+
enableAV1SlidesSupport: false,
|
|
508
536
|
},
|
|
509
537
|
'meeting id'
|
|
510
538
|
);
|
|
@@ -591,6 +619,7 @@ describe('createMediaConnection', () => {
|
|
|
591
619
|
{
|
|
592
620
|
iceServers: [],
|
|
593
621
|
disableAudioTwcc: true,
|
|
622
|
+
enableAV1SlidesSupport: false,
|
|
594
623
|
enableInboundAudioLevelMonitoring: true,
|
|
595
624
|
}
|
|
596
625
|
);
|
|
@@ -602,6 +631,7 @@ describe('createMediaConnection', () => {
|
|
|
602
631
|
{
|
|
603
632
|
iceServers: [],
|
|
604
633
|
disableAudioTwcc: true,
|
|
634
|
+
enableAV1SlidesSupport: false,
|
|
605
635
|
enableInboundAudioLevelMonitoring: true,
|
|
606
636
|
}
|
|
607
637
|
);
|
|
@@ -613,6 +643,7 @@ describe('createMediaConnection', () => {
|
|
|
613
643
|
{
|
|
614
644
|
iceServers: [],
|
|
615
645
|
disableAudioTwcc: true,
|
|
646
|
+
enableAV1SlidesSupport: false,
|
|
616
647
|
doFullIce: true,
|
|
617
648
|
stopIceGatheringAfterFirstRelayCandidate: undefined,
|
|
618
649
|
}
|
|
@@ -420,6 +420,160 @@ describe('plugin-meetings', () => {
|
|
|
420
420
|
assert.instanceOf(meeting.mediaRequestManagers.screenShareVideo, MediaRequestManager);
|
|
421
421
|
});
|
|
422
422
|
|
|
423
|
+
it('getIngressPayloadType on webrtcMediaConnection is invoked for H264 when sending multistream video requests', () => {
|
|
424
|
+
const getIngressPayloadType = sinon.stub().returns(97);
|
|
425
|
+
|
|
426
|
+
meeting.isMultistream = true;
|
|
427
|
+
meeting.mediaProperties.webrtcMediaConnection = {
|
|
428
|
+
getIngressPayloadType,
|
|
429
|
+
requestMedia: sinon.stub(),
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const fakeReceiveSlot = {
|
|
433
|
+
on: sinon.stub(),
|
|
434
|
+
off: sinon.stub(),
|
|
435
|
+
sourceState: 'live',
|
|
436
|
+
mediaType: MediaType.VideoMain,
|
|
437
|
+
wcmeReceiveSlot: {id: 'fake-wcme-slot'},
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
meeting.mediaRequestManagers.video.addRequest(
|
|
441
|
+
{
|
|
442
|
+
policyInfo: {
|
|
443
|
+
policy: 'receiver-selected',
|
|
444
|
+
csi: 42,
|
|
445
|
+
},
|
|
446
|
+
receiveSlots: [fakeReceiveSlot],
|
|
447
|
+
codecInfo: {
|
|
448
|
+
codec: 'h264',
|
|
449
|
+
maxFs: 3600,
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
true
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
assert.calledOnceWithExactly(
|
|
456
|
+
getIngressPayloadType,
|
|
457
|
+
MediaType.VideoMain,
|
|
458
|
+
MediaCodecMimeType.H264
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('getIngressPayloadType on webrtcMediaConnection is invoked for H264 and AV1 for slides video when AV1 slides support is enabled', () => {
|
|
463
|
+
const localWebex = new MockWebex({
|
|
464
|
+
children: {
|
|
465
|
+
meetings: Meetings,
|
|
466
|
+
credentials: Credentials,
|
|
467
|
+
support: Support,
|
|
468
|
+
llm: LLM,
|
|
469
|
+
mercury: Mercury,
|
|
470
|
+
},
|
|
471
|
+
config: {
|
|
472
|
+
credentials: {
|
|
473
|
+
client_id: 'mock-client-id',
|
|
474
|
+
},
|
|
475
|
+
meetings: {
|
|
476
|
+
reconnection: {
|
|
477
|
+
enabled: false,
|
|
478
|
+
},
|
|
479
|
+
mediaSettings: {},
|
|
480
|
+
metrics: {},
|
|
481
|
+
stats: {},
|
|
482
|
+
experimental: {enableUnifiedMeetings: true},
|
|
483
|
+
degradationPreferences: {maxMacroblocksLimit: 8192},
|
|
484
|
+
enableAv1SlidesSupport: true,
|
|
485
|
+
},
|
|
486
|
+
metrics: {
|
|
487
|
+
type: ['behavioral'],
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
localWebex.internal.newMetrics.callDiagnosticMetrics.clearErrorCache = sinon.stub();
|
|
493
|
+
localWebex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId =
|
|
494
|
+
sinon.stub();
|
|
495
|
+
localWebex.internal.support.submitLogs = sinon.stub().returns(Promise.resolve());
|
|
496
|
+
localWebex.internal.services = {get: sinon.stub().returns('locus-url')};
|
|
497
|
+
localWebex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
|
|
498
|
+
localWebex.internal.metrics.submitClientMetrics = sinon.stub().returns(Promise.resolve());
|
|
499
|
+
localWebex.meetings.uploadLogs = sinon.stub().returns(Promise.resolve());
|
|
500
|
+
localWebex.meetings.reachability = {
|
|
501
|
+
isAnyPublicClusterReachable: sinon.stub().resolves(true),
|
|
502
|
+
getReachabilityResults: sinon.stub().resolves(undefined),
|
|
503
|
+
getReachabilityMetrics: sinon.stub().resolves({}),
|
|
504
|
+
stopReachability: sinon.stub(),
|
|
505
|
+
isSubnetReachable: sinon.stub().returns(true),
|
|
506
|
+
};
|
|
507
|
+
localWebex.internal.llm.isDataChannelTokenEnabled = sinon.stub().resolves(false);
|
|
508
|
+
localWebex.internal.llm.on = sinon.stub();
|
|
509
|
+
localWebex.internal.voicea.announce = sinon.stub();
|
|
510
|
+
localWebex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
|
|
511
|
+
{},
|
|
512
|
+
{parent: localWebex}
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
Metrics.initialSetup(localWebex);
|
|
516
|
+
|
|
517
|
+
const localMeeting = new Meeting(
|
|
518
|
+
{
|
|
519
|
+
userId: uuid1,
|
|
520
|
+
resource: uuid2,
|
|
521
|
+
deviceUrl: uuid3,
|
|
522
|
+
locus: {url: url1},
|
|
523
|
+
destination: testDestination,
|
|
524
|
+
destinationType: DESTINATION_TYPE.MEETING_ID,
|
|
525
|
+
correlationId,
|
|
526
|
+
selfId: uuid1,
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
parent: localWebex,
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const getIngressPayloadType = sinon.stub().callsFake((_mediaType, codecMimeType) => {
|
|
534
|
+
if (codecMimeType === MediaCodecMimeType.H264) {
|
|
535
|
+
return 97;
|
|
536
|
+
}
|
|
537
|
+
if (codecMimeType === MediaCodecMimeType.AV1) {
|
|
538
|
+
return 98;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return undefined;
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
localMeeting.isMultistream = true;
|
|
545
|
+
localMeeting.mediaProperties.webrtcMediaConnection = {
|
|
546
|
+
getIngressPayloadType,
|
|
547
|
+
requestMedia: sinon.stub(),
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const fakeReceiveSlot = {
|
|
551
|
+
on: sinon.stub(),
|
|
552
|
+
off: sinon.stub(),
|
|
553
|
+
sourceState: 'live',
|
|
554
|
+
mediaType: MediaType.VideoSlides,
|
|
555
|
+
wcmeReceiveSlot: {id: 'fake-wcme-slides-slot'},
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
localMeeting.mediaRequestManagers.screenShareVideo.addRequest(
|
|
559
|
+
{
|
|
560
|
+
policyInfo: {
|
|
561
|
+
policy: 'receiver-selected',
|
|
562
|
+
csi: 42,
|
|
563
|
+
},
|
|
564
|
+
receiveSlots: [fakeReceiveSlot],
|
|
565
|
+
codecInfo: {
|
|
566
|
+
codec: 'h264',
|
|
567
|
+
maxFs: 3600,
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
true
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
assert.calledWith(getIngressPayloadType, MediaType.VideoSlides, MediaCodecMimeType.H264);
|
|
574
|
+
assert.calledWith(getIngressPayloadType, MediaType.VideoSlides, MediaCodecMimeType.AV1);
|
|
575
|
+
});
|
|
576
|
+
|
|
423
577
|
it('uses meeting id as correlation id if not provided in constructor', () => {
|
|
424
578
|
const newMeeting = new Meeting(
|
|
425
579
|
{
|
|
@@ -426,6 +426,33 @@ describe('plugin-meetings', () => {
|
|
|
426
426
|
});
|
|
427
427
|
});
|
|
428
428
|
|
|
429
|
+
describe('#_toggleEnableAv1SlidesSupport', () => {
|
|
430
|
+
it('should have _toggleEnableAv1SlidesSupport', () => {
|
|
431
|
+
assert.equal(typeof webex.meetings._toggleEnableAv1SlidesSupport, 'function');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('success', () => {
|
|
435
|
+
it('should update meetings config to enable AV1 slides support', () => {
|
|
436
|
+
webex.meetings._toggleEnableAv1SlidesSupport(true);
|
|
437
|
+
assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
|
|
438
|
+
|
|
439
|
+
webex.meetings._toggleEnableAv1SlidesSupport(false);
|
|
440
|
+
assert.equal(webex.meetings.config.enableAv1SlidesSupport, false);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should not update config when called with a non-boolean value', () => {
|
|
444
|
+
webex.meetings._toggleEnableAv1SlidesSupport(true);
|
|
445
|
+
assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
|
|
446
|
+
|
|
447
|
+
webex.meetings._toggleEnableAv1SlidesSupport('invalid');
|
|
448
|
+
assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
|
|
449
|
+
|
|
450
|
+
webex.meetings._toggleEnableAv1SlidesSupport(undefined);
|
|
451
|
+
assert.equal(webex.meetings.config.enableAv1SlidesSupport, true);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
429
456
|
describe('#_toggleStopIceGatheringAfterFirstRelayCandidate', () => {
|
|
430
457
|
it('should have _toggleStopIceGatheringAfterFirstRelayCandidate', () => {
|
|
431
458
|
assert.equal(
|