@webex/plugin-meetings 3.8.0-next.55 → 3.8.0-next.57

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.
@@ -1976,5 +1976,11 @@ export default class Meeting extends StatelessWebexPlugin {
1976
1976
  * @returns {Promise<void>}
1977
1977
  */
1978
1978
  checkAndRefreshPermissionToken(threshold: number, reason: string): Promise<void>;
1979
+ /**
1980
+ * Gets the media reachability metrics
1981
+ *
1982
+ * @returns {Promise<MediaReachabilityMetrics>}
1983
+ */
1984
+ private getMediaReachabilityMetricFields;
1979
1985
  }
1980
1986
  export {};
@@ -227,6 +227,15 @@ export default class Meetings extends WebexPlugin {
227
227
  * @returns {undefined}
228
228
  */
229
229
  private _toggleIpv6BackendNativeSupport;
230
+ /**
231
+ * API to toggle usage of audio main DTX, needs to be called before webex.meetings.register()
232
+ *
233
+ * @param {Boolean} newValue
234
+ * @private
235
+ * @memberof Meetings
236
+ * @returns {undefined}
237
+ */
238
+ private _toggleDisableAudioMainDtx;
230
239
  /**
231
240
  * Executes a registration step and updates the registration status.
232
241
  * @param {Function} step - The registration step to execute.
@@ -458,7 +458,7 @@ var Webinar = _webexCore.WebexPlugin.extend({
458
458
  }, _callee7);
459
459
  }))();
460
460
  },
461
- version: "3.8.0-next.55"
461
+ version: "3.8.0-next.57"
462
462
  });
463
463
  var _default = exports.default = Webinar;
464
464
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -43,7 +43,7 @@
43
43
  "@webex/eslint-config-legacy": "0.0.0",
44
44
  "@webex/jest-config-legacy": "0.0.0",
45
45
  "@webex/legacy-tools": "0.0.0",
46
- "@webex/plugin-meetings": "3.8.0-next.55",
46
+ "@webex/plugin-meetings": "3.8.0-next.57",
47
47
  "@webex/plugin-rooms": "3.8.0-next.21",
48
48
  "@webex/test-helper-chai": "3.8.0-next.17",
49
49
  "@webex/test-helper-mocha": "3.8.0-next.17",
@@ -63,7 +63,7 @@
63
63
  "dependencies": {
64
64
  "@webex/common": "3.8.0-next.17",
65
65
  "@webex/event-dictionary-ts": "^1.0.1753",
66
- "@webex/internal-media-core": "2.14.7",
66
+ "@webex/internal-media-core": "2.15.0",
67
67
  "@webex/internal-plugin-conversation": "3.8.0-next.21",
68
68
  "@webex/internal-plugin-device": "3.8.0-next.17",
69
69
  "@webex/internal-plugin-llm": "3.8.0-next.20",
@@ -71,8 +71,8 @@
71
71
  "@webex/internal-plugin-metrics": "3.8.0-next.17",
72
72
  "@webex/internal-plugin-support": "3.8.0-next.21",
73
73
  "@webex/internal-plugin-user": "3.8.0-next.17",
74
- "@webex/internal-plugin-voicea": "3.8.0-next.55",
75
- "@webex/media-helpers": "3.8.0-next.19",
74
+ "@webex/internal-plugin-voicea": "3.8.0-next.57",
75
+ "@webex/media-helpers": "3.8.0-next.20",
76
76
  "@webex/plugin-people": "3.8.0-next.19",
77
77
  "@webex/plugin-rooms": "3.8.0-next.21",
78
78
  "@webex/web-capabilities": "^1.4.0",
@@ -92,5 +92,5 @@
92
92
  "//": [
93
93
  "TODO: upgrade jwt-decode when moving to node 18"
94
94
  ],
95
- "version": "3.8.0-next.55"
95
+ "version": "3.8.0-next.57"
96
96
  }
@@ -142,6 +142,7 @@ Media.createMediaConnection = (
142
142
  turnServerInfo?: TurnServerInfo;
143
143
  bundlePolicy?: BundlePolicy;
144
144
  iceCandidatesTimeout?: number;
145
+ disableAudioMainDtx?: boolean;
145
146
  }
146
147
  ) => {
147
148
  const {
@@ -153,6 +154,7 @@ Media.createMediaConnection = (
153
154
  turnServerInfo,
154
155
  bundlePolicy,
155
156
  iceCandidatesTimeout,
157
+ disableAudioMainDtx,
156
158
  } = options;
157
159
 
158
160
  const iceServers = [];
@@ -176,6 +178,10 @@ Media.createMediaConnection = (
176
178
  config.bundlePolicy = bundlePolicy;
177
179
  }
178
180
 
181
+ if (disableAudioMainDtx !== undefined) {
182
+ config.disableAudioMainDtx = disableAudioMainDtx;
183
+ }
184
+
179
185
  return new MultistreamRoapMediaConnection(
180
186
  config,
181
187
  meetingId,
@@ -164,6 +164,7 @@ import Member from '../member';
164
164
  import {BrbState, createBrbState} from './brbState';
165
165
  import MultistreamNotSupportedError from '../common/errors/multistream-not-supported-error';
166
166
  import JoinForbiddenError from '../common/errors/join-forbidden-error';
167
+ import {ReachabilityMetrics} from '../reachability/reachability.types';
167
168
 
168
169
  // default callback so we don't call an undefined function, but in practice it should never be used
169
170
  const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
@@ -262,6 +263,8 @@ type FetchMeetingInfoParams = {
262
263
  sendCAevents?: boolean;
263
264
  };
264
265
 
266
+ type MediaReachabilityMetrics = ReachabilityMetrics & {isSubnetReachable: boolean};
267
+
265
268
  /**
266
269
  * MediaDirection
267
270
  * @typedef {Object} MediaDirection
@@ -7009,6 +7012,8 @@ export default class Meeting extends StatelessWebexPlugin {
7009
7012
  bundlePolicy,
7010
7013
  // @ts-ignore - config coming from registerPlugin
7011
7014
  iceCandidatesTimeout: this.config.iceCandidatesGatheringTimeout,
7015
+ // @ts-ignore - config coming from registerPlugin
7016
+ disableAudioMainDtx: this.config.experimental.disableAudioMainDtx,
7012
7017
  }
7013
7018
  );
7014
7019
 
@@ -7769,14 +7774,10 @@ export default class Meeting extends StatelessWebexPlugin {
7769
7774
 
7770
7775
  const {connectionType, selectedCandidatePairChanges, numTransports} =
7771
7776
  await this.mediaProperties.getCurrentConnectionInfo();
7772
- // @ts-ignore
7773
- const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
7777
+
7774
7778
  const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);
7775
7779
 
7776
- // @ts-ignore
7777
- const isSubnetReachable = this.webex.meetings.reachability.isSubnetReachable(
7778
- this.mediaServerIp
7779
- );
7780
+ const reachabilityMetrics = await this.getMediaReachabilityMetricFields();
7780
7781
 
7781
7782
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
7782
7783
  correlation_id: this.correlationId,
@@ -7787,8 +7788,7 @@ export default class Meeting extends StatelessWebexPlugin {
7787
7788
  isMultistream: this.isMultistream,
7788
7789
  retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
7789
7790
  isJoinWithMediaRetry: this.joinWithMediaRetryInfo.isRetry,
7790
- isSubnetReachable,
7791
- ...reachabilityStats,
7791
+ ...reachabilityMetrics,
7792
7792
  ...iceCandidateErrors,
7793
7793
  iceCandidatesCount: this.iceCandidatesCount,
7794
7794
  });
@@ -7810,18 +7810,13 @@ export default class Meeting extends StatelessWebexPlugin {
7810
7810
  LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
7811
7811
 
7812
7812
  // @ts-ignore
7813
- const reachabilityMetrics = await this.webex.meetings.reachability.getReachabilityMetrics();
7813
+ const reachabilityMetrics = await this.getMediaReachabilityMetricFields();
7814
7814
 
7815
7815
  const {selectedCandidatePairChanges, numTransports} =
7816
7816
  await this.mediaProperties.getCurrentConnectionInfo();
7817
7817
 
7818
7818
  const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);
7819
7819
 
7820
- // @ts-ignore
7821
- const isSubnetReachable = this.webex.meetings.reachability.isSubnetReachable(
7822
- this.mediaServerIp
7823
- );
7824
-
7825
7820
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
7826
7821
  correlation_id: this.correlationId,
7827
7822
  locus_id: this.locusUrl.split('/').pop(),
@@ -7851,7 +7846,6 @@ export default class Meeting extends StatelessWebexPlugin {
7851
7846
  this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
7852
7847
  'unknown',
7853
7848
  ...reachabilityMetrics,
7854
- isSubnetReachable,
7855
7849
  ...iceCandidateErrors,
7856
7850
  iceCandidatesCount: this.iceCandidatesCount,
7857
7851
  });
@@ -9622,4 +9616,44 @@ export default class Meeting extends StatelessWebexPlugin {
9622
9616
 
9623
9617
  return Promise.resolve();
9624
9618
  }
9619
+
9620
+ /**
9621
+ * Gets the media reachability metrics
9622
+ *
9623
+ * @returns {Promise<MediaReachabilityMetrics>}
9624
+ */
9625
+ private async getMediaReachabilityMetricFields(): Promise<MediaReachabilityMetrics> {
9626
+ const reachabilityMetrics: ReachabilityMetrics =
9627
+ // @ts-ignore
9628
+ await this.webex.meetings.reachability.getReachabilityMetrics();
9629
+
9630
+ const successKeys: Array<keyof ReachabilityMetrics> = [
9631
+ 'reachability_public_udp_success',
9632
+ 'reachability_public_tcp_success',
9633
+ 'reachability_public_xtls_success',
9634
+ 'reachability_vmn_udp_success',
9635
+ 'reachability_vmn_tcp_success',
9636
+ 'reachability_vmn_xtls_success',
9637
+ ];
9638
+
9639
+ const totalSuccessCases = successKeys.reduce((total, key) => {
9640
+ const value = reachabilityMetrics[key];
9641
+ if (typeof value === 'number') {
9642
+ return total + value;
9643
+ }
9644
+
9645
+ return total;
9646
+ }, 0);
9647
+
9648
+ let isSubnetReachable = null;
9649
+ if (totalSuccessCases > 0) {
9650
+ // @ts-ignore
9651
+ isSubnetReachable = this.webex.meetings.reachability.isSubnetReachable(this.mediaServerIp);
9652
+ }
9653
+
9654
+ return {
9655
+ ...reachabilityMetrics,
9656
+ isSubnetReachable,
9657
+ };
9658
+ }
9625
9659
  }
@@ -807,6 +807,26 @@ export default class Meetings extends WebexPlugin {
807
807
  }
808
808
  }
809
809
 
810
+ /**
811
+ * API to toggle usage of audio main DTX, needs to be called before webex.meetings.register()
812
+ *
813
+ * @param {Boolean} newValue
814
+ * @private
815
+ * @memberof Meetings
816
+ * @returns {undefined}
817
+ */
818
+ private _toggleDisableAudioMainDtx(newValue: boolean) {
819
+ if (typeof newValue !== 'boolean') {
820
+ return;
821
+ }
822
+
823
+ // @ts-ignore
824
+ if (this.config.experimental.disableAudioMainDtx !== newValue) {
825
+ // @ts-ignore
826
+ this.config.experimental.disableAudioMainDtx = newValue;
827
+ }
828
+ }
829
+
810
830
  /**
811
831
  * Executes a registration step and updates the registration status.
812
832
  * @param {Function} step - The registration step to execute.
@@ -159,6 +159,7 @@ describe('createMediaConnection', () => {
159
159
  password: 'turn password',
160
160
  },
161
161
  bundlePolicy: 'max-bundle',
162
+ disableAudioMainDtx: false,
162
163
  });
163
164
  assert.calledOnce(multistreamRoapMediaConnectionConstructorStub);
164
165
  assert.calledWith(
@@ -172,6 +173,7 @@ describe('createMediaConnection', () => {
172
173
  },
173
174
  ],
174
175
  bundlePolicy: 'max-bundle',
176
+ disableAudioMainDtx: false,
175
177
  },
176
178
  'meeting id'
177
179
  );
@@ -262,6 +264,34 @@ describe('createMediaConnection', () => {
262
264
  );
263
265
  });
264
266
 
267
+ it('does not pass disableAudioMainDtx to MultistreamRoapMediaConnection if disableAudioMainDtx is undefined', () => {
268
+ const multistreamRoapMediaConnectionConstructorStub = sinon
269
+ .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
270
+ .returns(fakeRoapMediaConnection);
271
+
272
+ Media.createMediaConnection(true, 'debug string', 'meeting id', {
273
+ mediaProperties: {
274
+ mediaDirection: {
275
+ sendAudio: true,
276
+ sendVideo: true,
277
+ sendShare: false,
278
+ receiveAudio: true,
279
+ receiveVideo: true,
280
+ receiveShare: true,
281
+ },
282
+ },
283
+ disableAudioMainDtx: undefined,
284
+ });
285
+ assert.calledOnce(multistreamRoapMediaConnectionConstructorStub);
286
+ assert.calledWith(
287
+ multistreamRoapMediaConnectionConstructorStub,
288
+ {
289
+ iceServers: [],
290
+ },
291
+ 'meeting id'
292
+ );
293
+ });
294
+
265
295
  [
266
296
  {testCase: 'turnServerInfo is undefined', turnServerInfo: undefined},
267
297
  {
@@ -2166,7 +2166,7 @@ describe('plugin-meetings', () => {
2166
2166
  someReachabilityMetric1: 'some value1',
2167
2167
  someReachabilityMetric2: 'some value2',
2168
2168
  selectedCandidatePairChanges: 2,
2169
- isSubnetReachable: false,
2169
+ isSubnetReachable: null,
2170
2170
  numTransports: 1,
2171
2171
  iceCandidatesCount: 0,
2172
2172
  }
@@ -2213,7 +2213,7 @@ describe('plugin-meetings', () => {
2213
2213
  signalingState: 'unknown',
2214
2214
  connectionState: 'unknown',
2215
2215
  iceConnectionState: 'unknown',
2216
- isSubnetReachable: true,
2216
+ isSubnetReachable: null,
2217
2217
  })
2218
2218
  );
2219
2219
 
@@ -2279,7 +2279,7 @@ describe('plugin-meetings', () => {
2279
2279
  selectedCandidatePairChanges: 2,
2280
2280
  numTransports: 1,
2281
2281
  iceCandidatesCount: 0,
2282
- isSubnetReachable: true,
2282
+ isSubnetReachable: null,
2283
2283
  }
2284
2284
  );
2285
2285
  });
@@ -2337,7 +2337,7 @@ describe('plugin-meetings', () => {
2337
2337
  signalingState: 'have-local-offer',
2338
2338
  connectionState: 'connecting',
2339
2339
  iceConnectionState: 'checking',
2340
- isSubnetReachable: true,
2340
+ isSubnetReachable: null,
2341
2341
  })
2342
2342
  );
2343
2343
 
@@ -2395,7 +2395,7 @@ describe('plugin-meetings', () => {
2395
2395
  signalingState: 'have-local-offer',
2396
2396
  connectionState: 'connecting',
2397
2397
  iceConnectionState: 'checking',
2398
- isSubnetReachable: true,
2398
+ isSubnetReachable: null,
2399
2399
  })
2400
2400
  );
2401
2401
 
@@ -2731,7 +2731,7 @@ describe('plugin-meetings', () => {
2731
2731
  sinon.stub().returns(FAKE_ERROR));
2732
2732
  webex.meetings.reachability = {
2733
2733
  isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2734
- getReachabilityMetrics: sinon.stub().resolves(),
2734
+ getReachabilityMetrics: sinon.stub().resolves({}),
2735
2735
  stopReachability: sinon.stub(),
2736
2736
  isSubnetReachable: sinon.stub().returns(true),
2737
2737
  };
@@ -2917,7 +2917,7 @@ describe('plugin-meetings', () => {
2917
2917
  selectedCandidatePairChanges: 2,
2918
2918
  numTransports: 1,
2919
2919
  iceCandidatesCount: 0,
2920
- isSubnetReachable: true,
2920
+ isSubnetReachable: null,
2921
2921
  },
2922
2922
  ]);
2923
2923
 
@@ -3120,7 +3120,7 @@ describe('plugin-meetings', () => {
3120
3120
  retriedWithTurnServer: true,
3121
3121
  isJoinWithMediaRetry: false,
3122
3122
  iceCandidatesCount: 0,
3123
- isSubnetReachable: true,
3123
+ isSubnetReachable: null,
3124
3124
  },
3125
3125
  ]);
3126
3126
  meeting.roap.doTurnDiscovery;
@@ -3277,7 +3277,7 @@ describe('plugin-meetings', () => {
3277
3277
  iceCandidatesCount: 3,
3278
3278
  '701_error': 3,
3279
3279
  '701_turn_host_lookup_received_error': 1,
3280
- isSubnetReachable: true,
3280
+ isSubnetReachable: null,
3281
3281
  }
3282
3282
  );
3283
3283
 
@@ -3340,7 +3340,7 @@ describe('plugin-meetings', () => {
3340
3340
  iceConnectionState: 'unknown',
3341
3341
  selectedCandidatePairChanges: 2,
3342
3342
  numTransports: 1,
3343
- isSubnetReachable: true,
3343
+ isSubnetReachable: null,
3344
3344
  iceCandidatesCount: 0,
3345
3345
  }
3346
3346
  );
@@ -3402,6 +3402,120 @@ describe('plugin-meetings', () => {
3402
3402
  numTransports: 1,
3403
3403
  '701_error': 2,
3404
3404
  '701_turn_host_lookup_received_error': 1,
3405
+ isSubnetReachable: null,
3406
+ iceCandidatesCount: 0,
3407
+ }
3408
+ );
3409
+
3410
+ assert.isOk(errorThrown);
3411
+ });
3412
+
3413
+ it('should send valid isSubnetReachability if media connection success', async () => {
3414
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3415
+ turnServerInfo: undefined,
3416
+ turnDiscoverySkippedReason: undefined,
3417
+ });
3418
+ meeting.meetingState = 'ACTIVE';
3419
+ meeting.mediaProperties.waitForMediaConnectionConnected.resolves();
3420
+ meeting.webex.meetings.reachability = {
3421
+ getReachabilityMetrics: sinon.stub().resolves({
3422
+ reachability_public_udp_success: 5,
3423
+ }),
3424
+ stopReachability: sinon.stub(),
3425
+ isSubnetReachable: sinon.stub().returns(false),
3426
+ };
3427
+
3428
+ const forceRtcMetricsSend = sinon.stub().resolves();
3429
+ const closeMediaConnectionStub = sinon.stub();
3430
+ Media.createMediaConnection = sinon.stub().returns({
3431
+ close: closeMediaConnectionStub,
3432
+ forceRtcMetricsSend,
3433
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3434
+ initiateOffer: sinon.stub().resolves({}),
3435
+ on: sinon.stub(),
3436
+ });
3437
+
3438
+ await meeting
3439
+ .addMedia({
3440
+ mediaSettings: {},
3441
+ });
3442
+
3443
+ assert.calledWith(
3444
+ Metrics.sendBehavioralMetric,
3445
+ BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS,
3446
+ {
3447
+ correlation_id: meeting.correlationId,
3448
+ locus_id: meeting.locusUrl.split('/').pop(),
3449
+ connectionType: 'udp',
3450
+ selectedCandidatePairChanges: 2,
3451
+ numTransports: 1,
3452
+ isMultistream: false,
3453
+ retriedWithTurnServer: false,
3454
+ isJoinWithMediaRetry: false,
3455
+ iceCandidatesCount: 0,
3456
+ reachability_public_udp_success: 5,
3457
+ isSubnetReachable: false,
3458
+ }
3459
+ );
3460
+ });
3461
+
3462
+ it('should send valid isSubnetReachability if media connection fails', async () => {
3463
+ let errorThrown = undefined;
3464
+
3465
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3466
+ turnServerInfo: undefined,
3467
+ turnDiscoverySkippedReason: undefined,
3468
+ });
3469
+ meeting.meetingState = 'ACTIVE';
3470
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
3471
+ meeting.webex.meetings.reachability = {
3472
+ getReachabilityMetrics: sinon.stub().resolves({
3473
+ reachability_public_udp_success: 5,
3474
+ }),
3475
+ stopReachability: sinon.stub(),
3476
+ isSubnetReachable: sinon.stub().returns(true),
3477
+ };
3478
+
3479
+ const forceRtcMetricsSend = sinon.stub().resolves();
3480
+ const closeMediaConnectionStub = sinon.stub();
3481
+ Media.createMediaConnection = sinon.stub().returns({
3482
+ close: closeMediaConnectionStub,
3483
+ forceRtcMetricsSend,
3484
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3485
+ initiateOffer: sinon.stub().resolves({}),
3486
+ on: sinon.stub(),
3487
+ });
3488
+
3489
+ await meeting
3490
+ .addMedia({
3491
+ mediaSettings: {},
3492
+ })
3493
+ .catch((err) => {
3494
+ errorThrown = err;
3495
+ assert.instanceOf(err, AddMediaFailed);
3496
+ });
3497
+
3498
+ // Check that the only metric sent is ADD_MEDIA_FAILURE
3499
+ assert.calledOnceWithExactly(
3500
+ Metrics.sendBehavioralMetric,
3501
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
3502
+ {
3503
+ correlation_id: meeting.correlationId,
3504
+ locus_id: meeting.locusUrl.split('/').pop(),
3505
+ reason: errorThrown.message,
3506
+ stack: errorThrown.stack,
3507
+ code: errorThrown.code,
3508
+ turnDiscoverySkippedReason: undefined,
3509
+ turnServerUsed: true,
3510
+ retriedWithTurnServer: false,
3511
+ isMultistream: false,
3512
+ isJoinWithMediaRetry: false,
3513
+ signalingState: 'unknown',
3514
+ connectionState: 'unknown',
3515
+ iceConnectionState: 'unknown',
3516
+ selectedCandidatePairChanges: 2,
3517
+ numTransports: 1,
3518
+ reachability_public_udp_success: 5,
3405
3519
  isSubnetReachable: true,
3406
3520
  iceCandidatesCount: 0,
3407
3521
  }
@@ -400,6 +400,19 @@ describe('plugin-meetings', () => {
400
400
  });
401
401
  });
402
402
 
403
+ describe('#_toggleDisableAudioMainDtx', () => {
404
+ it('should have _toggleDisableAudioMainDtx', () => {
405
+ assert.equal(typeof webex.meetings._toggleDisableAudioMainDtx, 'function');
406
+ });
407
+
408
+ describe('success', () => {
409
+ it('should update meetings to disable audio main dtx', () => {
410
+ webex.meetings._toggleDisableAudioMainDtx(true);
411
+ assert.equal(webex.meetings.config.experimental.disableAudioMainDtx, true);
412
+ });
413
+ });
414
+ });
415
+
403
416
  describe('Public API Contracts', () => {
404
417
  describe('#register', () => {
405
418
  it('emits an event and resolves when register succeeds', async () => {
@@ -658,7 +671,7 @@ describe('plugin-meetings', () => {
658
671
  quality: 'LOW',
659
672
  authToken: 'fake_token',
660
673
  mirror: false,
661
- canvasResolutionScaling: 1
674
+ canvasResolutionScaling: 1,
662
675
  });
663
676
  assert.exists(result.enable);
664
677
  assert.exists(result.disable);
@@ -674,7 +687,7 @@ describe('plugin-meetings', () => {
674
687
  quality: 'HIGH',
675
688
  blurStrength: 'STRONG',
676
689
  bgImageUrl: 'https://test.webex.com/landscape.5a535788.jpg',
677
- canvasResolutionScaling: 1
690
+ canvasResolutionScaling: 1,
678
691
  };
679
692
 
680
693
  const result = await webex.meetings.createVirtualBackgroundEffect(effectOptions);