@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.
Files changed (34) hide show
  1. package/dist/aiEnableRequest/index.js +1 -1
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/config.js +1 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/interpretation/index.js +1 -1
  7. package/dist/interpretation/siLanguage.js +1 -1
  8. package/dist/media/index.js +3 -1
  9. package/dist/media/index.js.map +1 -1
  10. package/dist/meeting/index.js +37 -10
  11. package/dist/meeting/index.js.map +1 -1
  12. package/dist/meetings/index.js +23 -0
  13. package/dist/meetings/index.js.map +1 -1
  14. package/dist/multistream/codec/constants.js +63 -0
  15. package/dist/multistream/codec/constants.js.map +1 -0
  16. package/dist/multistream/mediaRequestManager.js +62 -15
  17. package/dist/multistream/mediaRequestManager.js.map +1 -1
  18. package/dist/types/config.d.ts +1 -0
  19. package/dist/types/meeting/index.d.ts +9 -0
  20. package/dist/types/meetings/index.d.ts +10 -0
  21. package/dist/types/multistream/codec/constants.d.ts +7 -0
  22. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  23. package/dist/webinar/index.js +1 -1
  24. package/package.json +1 -1
  25. package/src/config.ts +1 -0
  26. package/src/media/index.ts +3 -0
  27. package/src/meeting/index.ts +41 -2
  28. package/src/meetings/index.ts +21 -0
  29. package/src/multistream/codec/constants.ts +58 -0
  30. package/src/multistream/mediaRequestManager.ts +119 -28
  31. package/test/unit/spec/media/index.ts +31 -0
  32. package/test/unit/spec/meeting/index.js +154 -0
  33. package/test/unit/spec/meetings/index.js +27 -0
  34. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
@@ -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 Options = {
84
+ type AudioMediaRequestManagerOptions = {
75
85
  degradationPreferences: DegradationPreferences;
76
- kind: 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(sendMediaRequestsCallback: SendMediaRequestsCallback, options: Options) {
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 || CODEC_DEFAULTS.h264.maxFs,
139
- mr.codecInfo.maxFs || CODEC_DEFAULTS.h264.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 || CODEC_DEFAULTS.h264.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 || CODEC_DEFAULTS.h264.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
- mr.codecInfo && [
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(