@webex/plugin-meetings 3.8.1-web-workers-keepalive.1 → 3.9.0-multipleLLM.1

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 (121) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +26 -2
  4. package/dist/constants.js.map +1 -1
  5. package/dist/interpretation/index.js +1 -1
  6. package/dist/interpretation/siLanguage.js +1 -1
  7. package/dist/locus-info/index.js +77 -95
  8. package/dist/locus-info/index.js.map +1 -1
  9. package/dist/locus-info/parser.js +4 -1
  10. package/dist/locus-info/parser.js.map +1 -1
  11. package/dist/media/properties.js +53 -5
  12. package/dist/media/properties.js.map +1 -1
  13. package/dist/meeting/brbState.js +14 -12
  14. package/dist/meeting/brbState.js.map +1 -1
  15. package/dist/meeting/in-meeting-actions.js +8 -0
  16. package/dist/meeting/in-meeting-actions.js.map +1 -1
  17. package/dist/meeting/index.js +443 -225
  18. package/dist/meeting/index.js.map +1 -1
  19. package/dist/meeting/muteState.js +2 -5
  20. package/dist/meeting/muteState.js.map +1 -1
  21. package/dist/meeting/request.js +44 -0
  22. package/dist/meeting/request.js.map +1 -1
  23. package/dist/meeting/request.type.js.map +1 -1
  24. package/dist/meeting/type.js +7 -0
  25. package/dist/meeting/type.js.map +1 -0
  26. package/dist/meeting/util.js +98 -13
  27. package/dist/meeting/util.js.map +1 -1
  28. package/dist/meeting-info/meeting-info-v2.js +29 -21
  29. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  30. package/dist/meetings/index.js +18 -10
  31. package/dist/meetings/index.js.map +1 -1
  32. package/dist/member/index.js.map +1 -1
  33. package/dist/member/types.js.map +1 -1
  34. package/dist/members/collection.js +13 -0
  35. package/dist/members/collection.js.map +1 -1
  36. package/dist/members/index.js +53 -29
  37. package/dist/members/index.js.map +1 -1
  38. package/dist/members/request.js +3 -3
  39. package/dist/members/request.js.map +1 -1
  40. package/dist/members/util.js +25 -8
  41. package/dist/members/util.js.map +1 -1
  42. package/dist/metrics/constants.js +2 -1
  43. package/dist/metrics/constants.js.map +1 -1
  44. package/dist/multistream/mediaRequestManager.js +1 -1
  45. package/dist/multistream/mediaRequestManager.js.map +1 -1
  46. package/dist/multistream/remoteMedia.js +34 -5
  47. package/dist/multistream/remoteMedia.js.map +1 -1
  48. package/dist/multistream/remoteMediaGroup.js +42 -2
  49. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  50. package/dist/multistream/sendSlotManager.js +32 -2
  51. package/dist/multistream/sendSlotManager.js.map +1 -1
  52. package/dist/reachability/index.js +3 -3
  53. package/dist/reachability/index.js.map +1 -1
  54. package/dist/types/constants.d.ts +24 -0
  55. package/dist/types/locus-info/index.d.ts +54 -10
  56. package/dist/types/media/properties.d.ts +21 -0
  57. package/dist/types/meeting/brbState.d.ts +0 -1
  58. package/dist/types/meeting/in-meeting-actions.d.ts +8 -0
  59. package/dist/types/meeting/index.d.ts +51 -20
  60. package/dist/types/meeting/request.d.ts +18 -1
  61. package/dist/types/meeting/request.type.d.ts +74 -0
  62. package/dist/types/meeting/type.d.ts +9 -0
  63. package/dist/types/meeting/util.d.ts +13 -3
  64. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  65. package/dist/types/meetings/index.d.ts +3 -1
  66. package/dist/types/member/types.d.ts +1 -0
  67. package/dist/types/members/collection.d.ts +6 -0
  68. package/dist/types/members/index.d.ts +22 -9
  69. package/dist/types/members/request.d.ts +1 -1
  70. package/dist/types/members/util.d.ts +13 -6
  71. package/dist/types/metrics/constants.d.ts +1 -0
  72. package/dist/types/multistream/remoteMedia.d.ts +20 -1
  73. package/dist/types/multistream/remoteMediaGroup.d.ts +11 -0
  74. package/dist/types/multistream/sendSlotManager.d.ts +16 -0
  75. package/dist/webinar/index.js +1 -1
  76. package/package.json +23 -24
  77. package/src/constants.ts +25 -2
  78. package/src/locus-info/index.ts +133 -96
  79. package/src/locus-info/parser.ts +5 -1
  80. package/src/media/properties.ts +43 -0
  81. package/src/meeting/brbState.ts +9 -7
  82. package/src/meeting/in-meeting-actions.ts +17 -0
  83. package/src/meeting/index.ts +273 -42
  84. package/src/meeting/muteState.ts +2 -6
  85. package/src/meeting/request.ts +39 -0
  86. package/src/meeting/request.type.ts +64 -0
  87. package/src/meeting/type.ts +9 -0
  88. package/src/meeting/util.ts +114 -22
  89. package/src/meeting-info/meeting-info-v2.ts +24 -5
  90. package/src/meetings/index.ts +12 -5
  91. package/src/member/index.ts +1 -0
  92. package/src/member/types.ts +1 -0
  93. package/src/members/collection.ts +11 -0
  94. package/src/members/index.ts +51 -15
  95. package/src/members/request.ts +2 -2
  96. package/src/members/util.ts +34 -6
  97. package/src/metrics/constants.ts +1 -0
  98. package/src/multistream/mediaRequestManager.ts +7 -7
  99. package/src/multistream/remoteMedia.ts +34 -4
  100. package/src/multistream/remoteMediaGroup.ts +37 -2
  101. package/src/multistream/sendSlotManager.ts +34 -2
  102. package/src/reachability/index.ts +3 -3
  103. package/test/unit/spec/locus-info/index.js +229 -98
  104. package/test/unit/spec/locus-info/parser.js +3 -2
  105. package/test/unit/spec/media/properties.ts +137 -0
  106. package/test/unit/spec/meeting/brbState.ts +9 -9
  107. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -0
  108. package/test/unit/spec/meeting/index.js +1022 -93
  109. package/test/unit/spec/meeting/muteState.js +32 -6
  110. package/test/unit/spec/meeting/request.js +92 -0
  111. package/test/unit/spec/meeting/utils.js +167 -17
  112. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  113. package/test/unit/spec/meetings/index.js +12 -1
  114. package/test/unit/spec/members/collection.js +120 -0
  115. package/test/unit/spec/members/index.js +140 -12
  116. package/test/unit/spec/members/request.js +57 -2
  117. package/test/unit/spec/members/utils.js +139 -17
  118. package/test/unit/spec/multistream/mediaRequestManager.ts +19 -6
  119. package/test/unit/spec/multistream/remoteMedia.ts +66 -2
  120. package/test/unit/spec/multistream/sendSlotManager.ts +59 -0
  121. package/test/unit/spec/reachability/index.ts +158 -1
@@ -39,6 +39,7 @@ import {
39
39
  ConnectionState,
40
40
  MediaConnectionEventNames,
41
41
  StatsAnalyzerEventNames,
42
+ StatsMonitorEventNames,
42
43
  Errors,
43
44
  ErrorType,
44
45
  RemoteTrackType,
@@ -56,6 +57,7 @@ import * as MeetingRequestImport from '@webex/plugin-meetings/src/meeting/reques
56
57
  import LocusInfo from '@webex/plugin-meetings/src/locus-info';
57
58
  import MediaProperties from '@webex/plugin-meetings/src/media/properties';
58
59
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
60
+ import MembersUtil from '@webex/plugin-meetings/src/members/util';
59
61
  import MeetingsUtil from '@webex/plugin-meetings/src/meetings/util';
60
62
  import Media from '@webex/plugin-meetings/src/media/index';
61
63
  import ReconnectionManager from '@webex/plugin-meetings/src/reconnection-manager';
@@ -244,6 +246,7 @@ describe('plugin-meetings', () => {
244
246
  });
245
247
 
246
248
  webex.internal.newMetrics.callDiagnosticMetrics.clearErrorCache = sinon.stub();
249
+ webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId = sinon.stub();
247
250
  webex.internal.support.submitLogs = sinon.stub().returns(Promise.resolve());
248
251
  webex.internal.services = {get: sinon.stub().returns('locus-url')};
249
252
  webex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
@@ -368,6 +371,35 @@ describe('plugin-meetings', () => {
368
371
  assert.instanceOf(meeting.simultaneousInterpretation, SimultaneousInterpretation);
369
372
  assert.instanceOf(meeting.webinar, Webinar);
370
373
  });
374
+
375
+ it('should call the callback with the meeting that has id already set', () => {
376
+ let meetingIdFromCallback;
377
+ // check that the meeting id is already set correctly at the time when the callback is called
378
+ const meetingCreationCallback = sinon.stub().callsFake((meeting) => {
379
+ meetingIdFromCallback = meeting.id;
380
+ });
381
+
382
+ meeting = new Meeting(
383
+ {
384
+ userId: uuid1,
385
+ resource: uuid2,
386
+ deviceUrl: uuid3,
387
+ locus: {url: url1},
388
+ destination: testDestination,
389
+ destinationType: DESTINATION_TYPE.MEETING_ID,
390
+ correlationId,
391
+ selfId: uuid1,
392
+ },
393
+ {
394
+ parent: webex,
395
+ },
396
+ meetingCreationCallback
397
+ );
398
+ assert.exists(meeting.id);
399
+ assert.calledOnceWithExactly(meetingCreationCallback, meeting);
400
+ assert.equal(meeting.id, meetingIdFromCallback);
401
+ });
402
+
371
403
  it('creates MediaRequestManager instances', () => {
372
404
  assert.instanceOf(meeting.mediaRequestManagers.audio, MediaRequestManager);
373
405
  assert.instanceOf(meeting.mediaRequestManagers.video, MediaRequestManager);
@@ -454,6 +486,18 @@ describe('plugin-meetings', () => {
454
486
  });
455
487
  });
456
488
 
489
+ it('pstnCorrelationId getter/setter should work correctly', () => {
490
+ const testPstnCorrelationId = uuid.v4();
491
+
492
+ meeting.pstnCorrelationId = testPstnCorrelationId;
493
+ assert.equal(meeting.pstnCorrelationId, testPstnCorrelationId);
494
+ assert.equal(meeting.callStateForMetrics.pstnCorrelationId, testPstnCorrelationId);
495
+
496
+ meeting.pstnCorrelationId = undefined;
497
+ assert.equal(meeting.pstnCorrelationId, undefined);
498
+ assert.equal(meeting.callStateForMetrics.pstnCorrelationId, undefined);
499
+ });
500
+
457
501
  describe('creates ReceiveSlot manager instance', () => {
458
502
  let mockReceiveSlotManagerCtor;
459
503
  let providedCreateSlotCallback;
@@ -581,7 +625,6 @@ describe('plugin-meetings', () => {
581
625
  assert.isFalse(meeting.isLocusCall());
582
626
  });
583
627
  });
584
-
585
628
  describe('#invite', () => {
586
629
  it('should have #invite', () => {
587
630
  assert.exists(meeting.invite);
@@ -592,8 +635,6 @@ describe('plugin-meetings', () => {
592
635
  it('should proxy members #addMember and return a promise', async () => {
593
636
  const invite = meeting.invite(uuid1, false);
594
637
 
595
- assert.exists(invite.then);
596
- await invite;
597
638
  assert.calledOnce(meeting.members.addMember);
598
639
  assert.calledWith(meeting.members.addMember, uuid1, false);
599
640
  });
@@ -614,20 +655,20 @@ describe('plugin-meetings', () => {
614
655
  assert.calledWith(meeting.members.cancelPhoneInvite, uuid1);
615
656
  });
616
657
  });
617
- describe('#cancelSIPInvite', () => {
618
- it('should have #cancelSIPInvite', () => {
619
- assert.exists(meeting.cancelSIPInvite);
658
+ describe('#cancelInviteByMemberId', () => {
659
+ it('should have #cancelInviteByMemberId', () => {
660
+ assert.exists(meeting.cancelInviteByMemberId);
620
661
  });
621
662
  beforeEach(() => {
622
- meeting.members.cancelSIPInvite = sinon.stub().returns(Promise.resolve(test1));
663
+ meeting.members.cancelInviteByMemberId = sinon.stub().returns(Promise.resolve(test1));
623
664
  });
624
- it('should proxy members #cancelSIPInvite and return a promise', async () => {
625
- const cancel = meeting.cancelSIPInvite({memberId: uuid1});
665
+ it('should proxy members #cancelInviteByMemberId and return a promise', async () => {
666
+ const cancel = meeting.cancelInviteByMemberId({memberId: uuid1});
626
667
 
627
668
  assert.exists(cancel.then);
628
669
  await cancel;
629
- assert.calledOnce(meeting.members.cancelSIPInvite);
630
- assert.calledWith(meeting.members.cancelSIPInvite, {memberId: uuid1});
670
+ assert.calledOnce(meeting.members.cancelInviteByMemberId);
671
+ assert.calledWith(meeting.members.cancelInviteByMemberId, {memberId: uuid1});
631
672
  });
632
673
  });
633
674
  describe('#admit', () => {
@@ -1219,14 +1260,13 @@ describe('plugin-meetings', () => {
1219
1260
  allowMediaInLobby: true,
1220
1261
  },
1221
1262
  });
1222
-
1263
+
1223
1264
  assert.calledWithMatch(
1224
1265
  meeting.addMediaInternal,
1225
1266
  sinon.match.any,
1226
1267
  sinon.match.any,
1227
1268
  sinon.match.any,
1228
- sinon.match.has('videoEnabled', false)
1229
- .and(sinon.match.has('allowMediaInLobby', true))
1269
+ sinon.match.has('videoEnabled', false).and(sinon.match.has('allowMediaInLobby', true))
1230
1270
  );
1231
1271
  });
1232
1272
 
@@ -1235,23 +1275,21 @@ describe('plugin-meetings', () => {
1235
1275
  joinOptions,
1236
1276
  mediaOptions: {
1237
1277
  audioEnabled: false,
1238
- sendAudio: true,
1239
- receiveAudio: false,
1278
+ sendAudio: true,
1279
+ receiveAudio: false,
1240
1280
  allowMediaInLobby: true,
1241
1281
  },
1242
1282
  });
1243
-
1283
+
1244
1284
  assert.calledWithMatch(
1245
1285
  meeting.addMediaInternal,
1246
1286
  sinon.match.any,
1247
1287
  sinon.match.any,
1248
1288
  sinon.match.any,
1249
- sinon.match.has('audioEnabled', false)
1250
- .and(sinon.match.has('allowMediaInLobby', true))
1289
+ sinon.match.has('audioEnabled', false).and(sinon.match.has('allowMediaInLobby', true))
1251
1290
  );
1252
- });
1291
+ });
1253
1292
 
1254
-
1255
1293
  it('should use provided send/receive values when videoEnabled/audioEnabled are true or not set', async () => {
1256
1294
  await meeting.joinWithMedia({
1257
1295
  joinOptions,
@@ -1263,7 +1301,7 @@ describe('plugin-meetings', () => {
1263
1301
  allowMediaInLobby: true,
1264
1302
  },
1265
1303
  });
1266
-
1304
+
1267
1305
  assert.calledWith(
1268
1306
  meeting.addMediaInternal,
1269
1307
  sinon.match.any,
@@ -1301,12 +1339,11 @@ describe('plugin-meetings', () => {
1301
1339
  sinon.restore();
1302
1340
  });
1303
1341
  it('should call voicea.onSpokenLanguageUpdate when joined', async () => {
1304
-
1305
1342
  meeting.joinedWith = {state: 'JOINED'};
1306
1343
  await meeting.locusInfo.emitScoped(
1307
1344
  {function: 'test', file: 'test'},
1308
1345
  LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIPTION_SPOKEN_LANGUAGE_UPDATED,
1309
- {spokenLanguage: 'fr'},
1346
+ {spokenLanguage: 'fr'}
1310
1347
  );
1311
1348
  assert.calledWith(webex.internal.voicea.onSpokenLanguageUpdate, 'fr', meeting.id);
1312
1349
  assert.equal(meeting.transcription.languageOptions.currentSpokenLanguage, 'fr');
@@ -1319,12 +1356,11 @@ describe('plugin-meetings', () => {
1319
1356
  });
1320
1357
 
1321
1358
  it('should also call voicea.onSpokenLanguageUpdate when not joined', async () => {
1322
-
1323
1359
  meeting.joinedWith = {state: 'NOT_JOINED'};
1324
1360
  await meeting.locusInfo.emitScoped(
1325
1361
  {function: 'test', file: 'test'},
1326
1362
  LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIPTION_SPOKEN_LANGUAGE_UPDATED,
1327
- {spokenLanguage: 'de'},
1363
+ {spokenLanguage: 'de'}
1328
1364
  );
1329
1365
  assert.calledWith(webex.internal.voicea.onSpokenLanguageUpdate, 'de');
1330
1366
  assert.equal(meeting.transcription.languageOptions.currentSpokenLanguage, 'de');
@@ -1954,21 +1990,25 @@ describe('plugin-meetings', () => {
1954
1990
  });
1955
1991
  });
1956
1992
 
1957
- it('should post error event if failed', async () => {
1993
+ it('should handle join failure', async () => {
1958
1994
  MeetingUtil.isPinOrGuest = sinon.stub().returns(false);
1995
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
1996
+
1959
1997
  await meeting.join().catch(() => {
1960
- assert.deepEqual(
1961
- webex.internal.newMetrics.submitClientEvent.getCall(1).args[0].name,
1962
- 'client.locus.join.response'
1963
- );
1964
- assert.match(
1965
- webex.internal.newMetrics.submitClientEvent.getCall(1).args[0].options.rawError,
1998
+ assert.calledOnce(MeetingUtil.joinMeeting);
1999
+
2000
+ // Assert that client.locus.join.response error event is not sent from this function, it is now emitted from MeetingUtil.joinMeeting
2001
+ assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
2002
+ assert.calledWithMatch(
2003
+ webex.internal.newMetrics.submitClientEvent,
1966
2004
  {
1967
- code: 2,
1968
- error: null,
1969
- joinOptions: {},
1970
- sdkMessage:
1971
- 'There was an issue joining the meeting, meeting could be in a bad state.',
2005
+ name: 'client.call.initiated',
2006
+ payload: {
2007
+ trigger: 'user-interaction',
2008
+ isRoapCallEnabled: true,
2009
+ pstnAudioType: undefined
2010
+ },
2011
+ options: {meetingId: meeting.id},
1972
2012
  }
1973
2013
  );
1974
2014
  });
@@ -2169,16 +2209,15 @@ describe('plugin-meetings', () => {
2169
2209
  };
2170
2210
  meeting.mediaProperties.setMediaDirection = sinon.stub().returns(true);
2171
2211
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
2172
- meeting.mediaProperties.getCurrentConnectionInfo = sinon
2173
- .stub()
2174
- .resolves({
2175
- connectionType: 'udp',
2176
- selectedCandidatePairChanges: 2,
2177
- numTransports: 1,
2178
- ipVersion: 'IPv6',
2179
- });
2212
+ meeting.mediaProperties.getCurrentConnectionInfo = sinon.stub().resolves({
2213
+ connectionType: 'udp',
2214
+ selectedCandidatePairChanges: 2,
2215
+ numTransports: 1,
2216
+ ipVersion: 'IPv6',
2217
+ });
2180
2218
  meeting.audio = muteStateStub;
2181
2219
  meeting.video = muteStateStub;
2220
+ sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.ipv4_and_ipv6);
2182
2221
  sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
2183
2222
  sinon.stub(meeting, 'setupMediaConnectionListeners');
2184
2223
  sinon.stub(meeting, 'setMercuryListener');
@@ -2250,13 +2289,24 @@ describe('plugin-meetings', () => {
2250
2289
  close: sinon.stub(),
2251
2290
  forceRtcMetricsSend,
2252
2291
  });
2253
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2292
+
2293
+ const mockStatsMonitor = {removeAllListeners: sinon.stub()};
2294
+ const mockNetworkQualityMonitor = {removeAllListeners: sinon.stub()};
2295
+
2296
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2254
2297
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2298
+ meeting.statsMonitor = mockStatsMonitor;
2299
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2255
2300
  const error = await assert.isRejected(meeting.addMedia());
2256
2301
 
2257
2302
  assert.calledOnce(forceRtcMetricsSend);
2303
+ assert.calledOnce(mockStatsMonitor.removeAllListeners);
2304
+ assert.calledOnce(mockNetworkQualityMonitor.removeAllListeners);
2258
2305
 
2259
2306
  assert.isNull(meeting.statsAnalyzer);
2307
+ assert.isNull(meeting.statsMonitor);
2308
+ assert.isNull(meeting.networkQualityMonitor);
2309
+
2260
2310
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
2261
2311
  assert.calledWith(webex.internal.newMetrics.submitInternalEvent.firstCall, {
2262
2312
  name: 'internal.client.add-media.turn-discovery.start',
@@ -2300,6 +2350,7 @@ describe('plugin-meetings', () => {
2300
2350
  selected_subnet: null,
2301
2351
  numTransports: 1,
2302
2352
  iceCandidatesCount: 0,
2353
+ ipver: 1,
2303
2354
  }
2304
2355
  );
2305
2356
  });
@@ -2347,6 +2398,7 @@ describe('plugin-meetings', () => {
2347
2398
  subnet_reachable: null,
2348
2399
  selected_cluster: null,
2349
2400
  selected_subnet: null,
2401
+ ipver: 1,
2350
2402
  })
2351
2403
  );
2352
2404
 
@@ -2366,12 +2418,23 @@ describe('plugin-meetings', () => {
2366
2418
 
2367
2419
  meeting.waitForRemoteSDPAnswer = sinon.stub().rejects();
2368
2420
 
2369
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2421
+ const mockStatsMonitor = {removeAllListeners: sinon.stub()};
2422
+ const mockNetworkQualityMonitor = {removeAllListeners: sinon.stub()};
2423
+
2424
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2370
2425
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2426
+ meeting.statsMonitor = mockStatsMonitor;
2427
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2371
2428
 
2372
2429
  const error = await assert.isRejected(meeting.addMedia());
2373
2430
 
2374
2431
  assert.isNull(meeting.statsAnalyzer);
2432
+ assert.isNull(meeting.statsMonitor);
2433
+ assert.isNull(meeting.networkQualityMonitor);
2434
+
2435
+ assert.calledOnce(mockStatsMonitor.removeAllListeners);
2436
+ assert.calledOnce(mockNetworkQualityMonitor.removeAllListeners);
2437
+
2375
2438
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
2376
2439
  assert.calledWith(webex.internal.newMetrics.submitInternalEvent.firstCall, {
2377
2440
  name: 'internal.client.add-media.turn-discovery.start',
@@ -2415,6 +2478,7 @@ describe('plugin-meetings', () => {
2415
2478
  subnet_reachable: null,
2416
2479
  selected_cluster: null,
2417
2480
  selected_subnet: null,
2481
+ ipver: 1,
2418
2482
  }
2419
2483
  );
2420
2484
  });
@@ -2435,8 +2499,9 @@ describe('plugin-meetings', () => {
2435
2499
  },
2436
2500
  },
2437
2501
  });
2438
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2502
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2439
2503
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2504
+ meeting.statsMonitor = {removeAllListeners: sinon.stub()};
2440
2505
  const error = await assert.isRejected(meeting.addMedia());
2441
2506
 
2442
2507
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
@@ -2475,10 +2540,12 @@ describe('plugin-meetings', () => {
2475
2540
  subnet_reachable: null,
2476
2541
  selected_cluster: null,
2477
2542
  selected_subnet: null,
2543
+ ipver: 1,
2478
2544
  })
2479
2545
  );
2480
2546
 
2481
2547
  assert.isNull(meeting.statsAnalyzer);
2548
+ assert.isNull(meeting.statsMonitor);
2482
2549
  });
2483
2550
 
2484
2551
  it('should include the peer connection properties correctly for transcoded', async () => {
@@ -2495,8 +2562,14 @@ describe('plugin-meetings', () => {
2495
2562
  },
2496
2563
  },
2497
2564
  });
2498
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2565
+
2566
+ const mockStatsMonitor = {removeAllListeners: sinon.stub()};
2567
+ const mockNetworkQualityMonitor = {removeAllListeners: sinon.stub()};
2568
+
2569
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2499
2570
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2571
+ meeting.statsMonitor = mockStatsMonitor;
2572
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2500
2573
  const error = await assert.isRejected(meeting.addMedia());
2501
2574
 
2502
2575
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
@@ -2535,10 +2608,15 @@ describe('plugin-meetings', () => {
2535
2608
  subnet_reachable: null,
2536
2609
  selected_cluster: null,
2537
2610
  selected_subnet: null,
2611
+ ipver: 1,
2538
2612
  })
2539
2613
  );
2540
2614
 
2541
2615
  assert.isNull(meeting.statsAnalyzer);
2616
+ assert.isNull(meeting.statsMonitor);
2617
+ assert.isNull(meeting.networkQualityMonitor);
2618
+ assert.calledOnce(mockStatsMonitor.removeAllListeners);
2619
+ assert.calledOnce(mockNetworkQualityMonitor.removeAllListeners);
2542
2620
  });
2543
2621
 
2544
2622
  it('should work the second time addMedia is called in case the first time fails', async () => {
@@ -3059,6 +3137,7 @@ describe('plugin-meetings', () => {
3059
3137
  subnet_reachable: null,
3060
3138
  selected_cluster: null,
3061
3139
  selected_subnet: null,
3140
+ ipver: 1,
3062
3141
  },
3063
3142
  ]);
3064
3143
 
@@ -3260,6 +3339,7 @@ describe('plugin-meetings', () => {
3260
3339
  connectionType: 'udp',
3261
3340
  selectedCandidatePairChanges: 2,
3262
3341
  ipVersion: 'IPv6',
3342
+ ipver: 1,
3263
3343
  numTransports: 1,
3264
3344
  isMultistream: false,
3265
3345
  retriedWithTurnServer: true,
@@ -3401,11 +3481,12 @@ describe('plugin-meetings', () => {
3401
3481
  meeting.mediaConnections = [
3402
3482
  {
3403
3483
  mediaAgentCluster: 'some.cluster',
3404
- }
3405
- ]
3484
+ },
3485
+ ];
3406
3486
  meeting.iceCandidatesCount = 3;
3407
3487
  meeting.iceCandidateErrors.set('701_error', 3);
3408
3488
  meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
3489
+ MeetingUtil.getIpVersion.returns(IP_VERSION.only_ipv6);
3409
3490
 
3410
3491
  await meeting.addMedia({
3411
3492
  mediaSettings: {},
@@ -3421,6 +3502,7 @@ describe('plugin-meetings', () => {
3421
3502
  connectionType: 'udp',
3422
3503
  selectedCandidatePairChanges: 2,
3423
3504
  ipVersion: 'IPv6',
3505
+ ipver: 6,
3424
3506
  numTransports: 1,
3425
3507
  isMultistream: false,
3426
3508
  retriedWithTurnServer: false,
@@ -3499,6 +3581,7 @@ describe('plugin-meetings', () => {
3499
3581
  selected_cluster: null,
3500
3582
  selected_subnet: null,
3501
3583
  iceCandidatesCount: 0,
3584
+ ipver: 1,
3502
3585
  }
3503
3586
  );
3504
3587
 
@@ -3563,6 +3646,7 @@ describe('plugin-meetings', () => {
3563
3646
  selected_cluster: null,
3564
3647
  selected_subnet: null,
3565
3648
  iceCandidatesCount: 0,
3649
+ ipver: 1,
3566
3650
  }
3567
3651
  );
3568
3652
 
@@ -3583,12 +3667,12 @@ describe('plugin-meetings', () => {
3583
3667
  stopReachability: sinon.stub(),
3584
3668
  isSubnetReachable: sinon.stub().returns(false),
3585
3669
  };
3586
- meeting.mediaServerIp = '1.2.3.4';
3670
+ meeting.mediaServerIp = '1.2.3.4';
3587
3671
  meeting.mediaConnections = [
3588
3672
  {
3589
3673
  mediaAgentCluster: 'some.cluster',
3590
- }
3591
- ]
3674
+ },
3675
+ ];
3592
3676
 
3593
3677
  const forceRtcMetricsSend = sinon.stub().resolves();
3594
3678
  const closeMediaConnectionStub = sinon.stub();
@@ -3609,6 +3693,7 @@ describe('plugin-meetings', () => {
3609
3693
  locus_id: meeting.locusUrl.split('/').pop(),
3610
3694
  connectionType: 'udp',
3611
3695
  ipVersion: 'IPv6',
3696
+ ipver: 1,
3612
3697
  selectedCandidatePairChanges: 2,
3613
3698
  numTransports: 1,
3614
3699
  isMultistream: false,
@@ -3638,12 +3723,12 @@ describe('plugin-meetings', () => {
3638
3723
  stopReachability: sinon.stub(),
3639
3724
  isSubnetReachable: sinon.stub().returns(true),
3640
3725
  };
3641
- meeting.mediaServerIp = '1.2.3.4';
3726
+ meeting.mediaServerIp = '1.2.3.4';
3642
3727
  meeting.mediaConnections = [
3643
3728
  {
3644
3729
  mediaAgentCluster: 'some.cluster',
3645
- }
3646
- ]
3730
+ },
3731
+ ];
3647
3732
 
3648
3733
  const forceRtcMetricsSend = sinon.stub().resolves();
3649
3734
  const closeMediaConnectionStub = sinon.stub();
@@ -3689,6 +3774,7 @@ describe('plugin-meetings', () => {
3689
3774
  selected_cluster: 'some.cluster',
3690
3775
  selected_subnet: '1.X.X.X',
3691
3776
  iceCandidatesCount: 0,
3777
+ ipver: 1,
3692
3778
  }
3693
3779
  );
3694
3780
 
@@ -3994,13 +4080,14 @@ describe('plugin-meetings', () => {
3994
4080
  });
3995
4081
  });
3996
4082
 
3997
- it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
4083
+ it('counts the number of members that are in the meeting or lobby for MEDIA_QUALITY event', async () => {
3998
4084
  let fakeMembersCollection = {
3999
4085
  members: {
4000
- member1: {isInMeeting: true},
4001
- member2: {isInMeeting: true},
4002
- member3: {isInMeeting: false},
4003
- },
4086
+ member1: {isInMeeting: true, isInLobby: false},
4087
+ member2: {isInMeeting: false, isInLobby: true},
4088
+ member3: {isInMeeting: false, isInLobby: false},
4089
+ member4: {isInMeeting: true, isInLobby: false},
4090
+ }
4004
4091
  };
4005
4092
  sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
4006
4093
  const fakeData = {intervalMetadata: {}};
@@ -4018,11 +4105,12 @@ describe('plugin-meetings', () => {
4018
4105
  },
4019
4106
  payload: {
4020
4107
  intervals: [
4021
- sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
4108
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 3)),
4022
4109
  ],
4023
4110
  },
4024
4111
  });
4025
- fakeMembersCollection.members.member2.isInMeeting = false;
4112
+ // Move member2 from lobby to neither in meeting nor lobby
4113
+ fakeMembersCollection.members.member2.isInLobby = false;
4026
4114
 
4027
4115
  statsAnalyzerStub.emit(
4028
4116
  {file: 'test', function: 'test'},
@@ -4037,7 +4125,7 @@ describe('plugin-meetings', () => {
4037
4125
  },
4038
4126
  payload: {
4039
4127
  intervals: [
4040
- sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1)),
4128
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
4041
4129
  ],
4042
4130
  },
4043
4131
  });
@@ -4064,6 +4152,132 @@ describe('plugin-meetings', () => {
4064
4152
  });
4065
4153
  });
4066
4154
 
4155
+ describe('handles StatsMonitor events', () => {
4156
+ let statsMonitorStub;
4157
+ let prevConfigValue;
4158
+ let listeners;
4159
+
4160
+ beforeEach(async () => {
4161
+ meeting.meetingState = 'ACTIVE';
4162
+ prevConfigValue = meeting.config.stats.enableStatsAnalyzer;
4163
+
4164
+ meeting.config.stats.enableStatsAnalyzer = true;
4165
+
4166
+ listeners = {};
4167
+
4168
+ statsMonitorStub = {
4169
+ on: sinon.stub().callsFake((event, callback) => {
4170
+ listeners[event] = callback;
4171
+ }),
4172
+ removeAllListeners: sinon.stub(),
4173
+ };
4174
+
4175
+ sinon.stub(meeting.mediaProperties, 'sendMediaIssueMetric');
4176
+
4177
+ // mock the StatsMonitor constructor
4178
+ sinon.stub(InternalMediaCoreModule, 'StatsMonitor').returns(statsMonitorStub);
4179
+
4180
+ await meeting.addMedia({
4181
+ mediaSettings: {},
4182
+ });
4183
+ });
4184
+
4185
+ afterEach(() => {
4186
+ meeting.config.stats.enableStatsAnalyzer = prevConfigValue;
4187
+ sinon.restore();
4188
+ });
4189
+
4190
+ describe('INBOUND_AUDIO_ISSUE event', () => {
4191
+ it('should not trigger event when no unmuted members exist', () => {
4192
+ const fakeEventData = {issueSubType: 'DECODE_RESULTS_IN_ZERO_AUDIO_LEVEL'};
4193
+
4194
+ // Setup members that are either self or muted
4195
+ const mutedMember = {
4196
+ isSelf: false,
4197
+ isPairedWithSelf: false,
4198
+ isAudioMuted: true,
4199
+ };
4200
+ const selfMember = {
4201
+ isSelf: true,
4202
+ isPairedWithSelf: false,
4203
+ isAudioMuted: false,
4204
+ };
4205
+ const pairedMember = {
4206
+ isSelf: false,
4207
+ isPairedWithSelf: true,
4208
+ isAudioMuted: false,
4209
+ };
4210
+ meeting.members.membersCollection.getAll = sinon.stub().returns({
4211
+ member1: mutedMember,
4212
+ member2: selfMember,
4213
+ member3: pairedMember,
4214
+ });
4215
+
4216
+ // Reset the stub to clear any previous calls
4217
+ TriggerProxy.trigger.resetHistory();
4218
+
4219
+ // Emit the event from statsMonitor
4220
+ listeners[StatsMonitorEventNames.INBOUND_AUDIO_ISSUE](fakeEventData);
4221
+
4222
+ assert.neverCalledWith(
4223
+ TriggerProxy.trigger,
4224
+ meeting,
4225
+ sinon.match.object,
4226
+ EVENT_TRIGGERS.MEDIA_INBOUND_AUDIO_ISSUE_DETECTED,
4227
+ fakeEventData
4228
+ );
4229
+ assert.notCalled(meeting.mediaProperties.sendMediaIssueMetric);
4230
+ });
4231
+
4232
+ it('should trigger event and metric when there are multiple members and at least one is unmuted', () => {
4233
+ const fakeEventData = {issueSubType: 'DECODE_RESULTS_IN_ZERO_AUDIO_LEVEL'};
4234
+
4235
+ // Setup mixed members - some muted, one unmuted
4236
+ const mutedMember = {
4237
+ isSelf: false,
4238
+ isPairedWithSelf: false,
4239
+ isAudioMuted: true,
4240
+ };
4241
+ const unmutedMember = {
4242
+ isSelf: false,
4243
+ isPairedWithSelf: false,
4244
+ isAudioMuted: false,
4245
+ };
4246
+ const selfMember = {
4247
+ isSelf: true,
4248
+ isPairedWithSelf: false,
4249
+ isAudioMuted: false,
4250
+ };
4251
+ meeting.members.membersCollection.getAll = sinon.stub().returns({
4252
+ member1: mutedMember,
4253
+ member2: unmutedMember,
4254
+ member3: selfMember,
4255
+ });
4256
+
4257
+ // Reset the stub to clear any previous calls
4258
+ TriggerProxy.trigger.resetHistory();
4259
+
4260
+ // Emit the event from statsMonitor
4261
+ listeners[StatsMonitorEventNames.INBOUND_AUDIO_ISSUE](fakeEventData);
4262
+
4263
+ assert.calledWith(
4264
+ TriggerProxy.trigger,
4265
+ meeting,
4266
+ sinon.match.object,
4267
+ EVENT_TRIGGERS.MEDIA_INBOUND_AUDIO_ISSUE_DETECTED,
4268
+ fakeEventData
4269
+ );
4270
+
4271
+ assert.calledOnceWithExactly(
4272
+ meeting.mediaProperties.sendMediaIssueMetric,
4273
+ 'inbound_audio',
4274
+ fakeEventData.issueSubType,
4275
+ meeting.correlationId
4276
+ );
4277
+ });
4278
+ });
4279
+ });
4280
+
4067
4281
  describe('bundlePolicy', () => {
4068
4282
  const FAKE_TURN_URL = 'turns:webex.com:3478';
4069
4283
  const FAKE_TURN_USER = 'some-turn-username';
@@ -4355,9 +4569,7 @@ describe('plugin-meetings', () => {
4355
4569
  const error = new Error();
4356
4570
  meeting.meetingRequest.setBrb = sinon.stub().rejects(error);
4357
4571
 
4358
- await expect(
4359
- meeting.beRightBack(true)
4360
- ).to.be.rejectedWith(error);
4572
+ await expect(meeting.beRightBack(true)).to.be.rejectedWith(error);
4361
4573
 
4362
4574
  assert.isFalse(meeting.brbState.state.syncToServerInProgress);
4363
4575
  });
@@ -5532,6 +5744,7 @@ describe('plugin-meetings', () => {
5532
5744
  let multistreamEventListeners;
5533
5745
  let transcodedEventListeners;
5534
5746
  let mockStatsAnalyzerCtor;
5747
+ let statsMonitorStub;
5535
5748
 
5536
5749
  const setupFakeRoapMediaConnection = (fakeRoapMediaConnection, eventListeners) => {
5537
5750
  fakeRoapMediaConnection.on.callsFake((eventName, cb) => {
@@ -5563,6 +5776,14 @@ describe('plugin-meetings', () => {
5563
5776
  return {on: sinon.stub(), stopAnalyzer: sinon.stub()};
5564
5777
  });
5565
5778
 
5779
+ statsMonitorStub = {
5780
+ on: sinon.stub(),
5781
+ removeAllListeners: sinon.stub(),
5782
+ };
5783
+
5784
+ // mock the StatsMonitor constructor
5785
+ sinon.stub(InternalMediaCoreModule, 'StatsMonitor').returns(statsMonitorStub);
5786
+
5566
5787
  webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
5567
5788
  sinon.stub();
5568
5789
 
@@ -5625,6 +5846,7 @@ describe('plugin-meetings', () => {
5625
5846
  mockStatsAnalyzerCtor,
5626
5847
  sinon.match({
5627
5848
  isMultistream: true,
5849
+ statsMonitor: statsMonitorStub,
5628
5850
  })
5629
5851
  );
5630
5852
  const initialStatsAnalyzer = mockStatsAnalyzerCtor.returnValues[0];
@@ -6507,25 +6729,36 @@ describe('plugin-meetings', () => {
6507
6729
  const DIAL_IN_URL = meeting.dialInUrl;
6508
6730
 
6509
6731
  assert.calledWith(meeting.meetingRequest.dialIn, {
6510
- correlationId: meeting.correlationId,
6732
+ correlationId: meeting.pstnCorrelationId,
6511
6733
  dialInUrl: DIAL_IN_URL,
6512
6734
  locusUrl: meeting.locusUrl,
6513
6735
  clientUrl: meeting.deviceUrl,
6514
6736
  });
6515
6737
  assert.notCalled(meeting.meetingRequest.dialOut);
6516
6738
 
6739
+ // Verify pstnCorrelationId was set
6740
+ assert.exists(meeting.pstnCorrelationId);
6741
+ assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6742
+ const firstPstnCorrelationId = meeting.pstnCorrelationId
6743
+
6517
6744
  meeting.meetingRequest.dialIn.resetHistory();
6518
6745
 
6519
6746
  // try again. the dial in urls should match
6520
6747
  await meeting.usePhoneAudio();
6521
6748
 
6522
6749
  assert.calledWith(meeting.meetingRequest.dialIn, {
6523
- correlationId: meeting.correlationId,
6750
+ correlationId: meeting.pstnCorrelationId,
6524
6751
  dialInUrl: DIAL_IN_URL,
6525
6752
  locusUrl: meeting.locusUrl,
6526
6753
  clientUrl: meeting.deviceUrl,
6527
6754
  });
6528
6755
  assert.notCalled(meeting.meetingRequest.dialOut);
6756
+ // A new PSTN correlationId should be generated for the second attempt
6757
+ assert.notEqual(
6758
+ meeting.pstnCorrelationId,
6759
+ firstPstnCorrelationId,
6760
+ 'pstnCorrelationId should be regenerated on each dial-in attempt'
6761
+ );
6529
6762
  });
6530
6763
 
6531
6764
  it('given a phone number, triggers dial-out, delegating request to meetingRequest correctly', async () => {
@@ -6535,7 +6768,7 @@ describe('plugin-meetings', () => {
6535
6768
  const DIAL_OUT_URL = meeting.dialOutUrl;
6536
6769
 
6537
6770
  assert.calledWith(meeting.meetingRequest.dialOut, {
6538
- correlationId: meeting.correlationId,
6771
+ correlationId: meeting.pstnCorrelationId,
6539
6772
  dialOutUrl: DIAL_OUT_URL,
6540
6773
  locusUrl: meeting.locusUrl,
6541
6774
  clientUrl: meeting.deviceUrl,
@@ -6543,49 +6776,126 @@ describe('plugin-meetings', () => {
6543
6776
  });
6544
6777
  assert.notCalled(meeting.meetingRequest.dialIn);
6545
6778
 
6779
+ // Verify pstnCorrelationId was set
6780
+ assert.exists(meeting.pstnCorrelationId);
6781
+ assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6782
+ const firstPstnCorrelationId = meeting.pstnCorrelationId;
6783
+
6546
6784
  meeting.meetingRequest.dialOut.resetHistory();
6547
6785
 
6548
6786
  // try again. the dial out urls should match
6549
6787
  await meeting.usePhoneAudio(phoneNumber);
6550
6788
 
6551
6789
  assert.calledWith(meeting.meetingRequest.dialOut, {
6552
- correlationId: meeting.correlationId,
6790
+ correlationId: meeting.pstnCorrelationId,
6553
6791
  dialOutUrl: DIAL_OUT_URL,
6554
6792
  locusUrl: meeting.locusUrl,
6555
6793
  clientUrl: meeting.deviceUrl,
6556
6794
  phoneNumber,
6557
6795
  });
6558
6796
  assert.notCalled(meeting.meetingRequest.dialIn);
6797
+ // A new PSTN correlationId should be generated for the second attempt
6798
+ assert.notEqual(
6799
+ meeting.pstnCorrelationId,
6800
+ firstPstnCorrelationId,
6801
+ 'pstnCorrelationId should be regenerated on each dial-out attempt'
6802
+ );
6559
6803
  });
6560
6804
 
6561
- it('rejects if the request failed (dial in)', () => {
6562
- const error = 'something bad happened';
6805
+ it('rejects if the request failed (dial in)', async () => {
6806
+ const error = {error: {message: 'dial in failed'}, stack: 'error stack'};
6563
6807
 
6564
6808
  meeting.meetingRequest.dialIn = sinon.stub().returns(Promise.reject(error));
6565
6809
 
6566
- return meeting
6567
- .usePhoneAudio()
6568
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6569
- .catch((e) => {
6570
- assert.equal(e, error);
6810
+ try {
6811
+ await meeting.usePhoneAudio();
6812
+ throw new Error('Promise resolved when it should have rejected');
6813
+ } catch (e) {
6814
+ assert.equal(e, error);
6571
6815
 
6572
- return Promise.resolve();
6816
+ // Verify behavioral metric was sent with dial_in_correlation_id
6817
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_IN_FAILURE, {
6818
+ correlation_id: meeting.correlationId,
6819
+ dial_in_url: meeting.dialInUrl,
6820
+ dial_in_correlation_id: sinon.match.string,
6821
+ locus_id: meeting.locusUrl.split('/').pop(),
6822
+ client_url: meeting.deviceUrl,
6823
+ reason: error.error.message,
6824
+ stack: error.stack,
6573
6825
  });
6826
+
6827
+ // Verify pstnCorrelationId was cleared after error
6828
+ assert.equal(meeting.pstnCorrelationId, undefined);
6829
+ }
6574
6830
  });
6575
6831
 
6576
6832
  it('rejects if the request failed (dial out)', async () => {
6577
- const error = 'something bad happened';
6833
+ const error = {error: {message: 'dial out failed'}, stack: 'error stack'};
6578
6834
 
6579
6835
  meeting.meetingRequest.dialOut = sinon.stub().returns(Promise.reject(error));
6580
6836
 
6581
- return meeting
6582
- .usePhoneAudio('+441234567890')
6583
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6584
- .catch((e) => {
6585
- assert.equal(e, error);
6837
+ try {
6838
+ await meeting.usePhoneAudio('+441234567890');
6839
+ throw new Error('Promise resolved when it should have rejected');
6840
+ } catch (e) {
6841
+ assert.equal(e, error);
6586
6842
 
6587
- return Promise.resolve();
6843
+ // Verify behavioral metric was sent with dial_out_correlation_id
6844
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_OUT_FAILURE, {
6845
+ correlation_id: meeting.correlationId,
6846
+ dial_out_url: meeting.dialOutUrl,
6847
+ dial_out_correlation_id: sinon.match.string,
6848
+ locus_id: meeting.locusUrl.split('/').pop(),
6849
+ client_url: meeting.deviceUrl,
6850
+ reason: error.error.message,
6851
+ stack: error.stack,
6588
6852
  });
6853
+
6854
+ // Verify pstnCorrelationId was cleared after error
6855
+ assert.equal(meeting.pstnCorrelationId, undefined);
6856
+ }
6857
+ });
6858
+ });
6859
+
6860
+ describe('#disconnectPhoneAudio', () => {
6861
+ beforeEach(() => {
6862
+ // Mock the MeetingUtil.disconnectPhoneAudio method
6863
+ sinon.stub(MeetingUtil, 'disconnectPhoneAudio').resolves();
6864
+ meeting.dialInUrl = 'dialin:///test-dial-in-url';
6865
+ meeting.dialOutUrl = 'dialout:///test-dial-out-url';
6866
+ meeting.dialInDeviceStatus = 'JOINED';
6867
+ meeting.dialOutDeviceStatus = 'JOINED';
6868
+ });
6869
+
6870
+ afterEach(() => {
6871
+ MeetingUtil.disconnectPhoneAudio.restore();
6872
+ });
6873
+
6874
+ it('should disconnect phone audio and clear pstnCorrelationId', async () => {
6875
+ meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6876
+
6877
+ await meeting.disconnectPhoneAudio();
6878
+
6879
+ // Verify that pstnCorrelationId is cleared
6880
+ assert.equal(meeting.pstnCorrelationId, undefined);
6881
+
6882
+ // Verify that MeetingUtil.disconnectPhoneAudio was called for both dial-in and dial-out
6883
+ assert.calledTwice(MeetingUtil.disconnectPhoneAudio);
6884
+ assert.calledWith(MeetingUtil.disconnectPhoneAudio, meeting, meeting.dialInUrl);
6885
+ assert.calledWith(MeetingUtil.disconnectPhoneAudio, meeting, meeting.dialOutUrl);
6886
+ });
6887
+
6888
+ it('should handle case when no PSTN connection is active', async () => {
6889
+ meeting.dialInDeviceStatus = 'IDLE';
6890
+ meeting.dialOutDeviceStatus = 'IDLE';
6891
+ meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6892
+
6893
+ await meeting.disconnectPhoneAudio();
6894
+
6895
+ // Verify that pstnCorrelationId is still cleared even when no phone connection is active
6896
+ assert.equal(meeting.pstnCorrelationId, undefined);
6897
+ // And verify no disconnect was attempted
6898
+ assert.notCalled(MeetingUtil.disconnectPhoneAudio);
6589
6899
  });
6590
6900
  });
6591
6901
 
@@ -7339,6 +7649,8 @@ describe('plugin-meetings', () => {
7339
7649
  'locus-id',
7340
7650
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
7341
7651
  {meetingId: meeting.id, sendCAevents: true},
7652
+ null,
7653
+ null,
7342
7654
  null
7343
7655
  );
7344
7656
  assert.deepEqual(meeting.meetingInfo, {
@@ -7385,6 +7697,8 @@ describe('plugin-meetings', () => {
7385
7697
  'locus-id',
7386
7698
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
7387
7699
  {meetingId: meeting.id, sendCAevents: true},
7700
+ null,
7701
+ null,
7388
7702
  null
7389
7703
  );
7390
7704
  assert.deepEqual(meeting.meetingInfo, {
@@ -7440,6 +7754,8 @@ describe('plugin-meetings', () => {
7440
7754
  permissionToken: FAKE_PERMISSION_TOKEN,
7441
7755
  },
7442
7756
  {meetingId: meeting.id, sendCAevents: true},
7757
+ null,
7758
+ null,
7443
7759
  null
7444
7760
  );
7445
7761
  assert.deepEqual(meeting.meetingInfo, {
@@ -8016,11 +8332,13 @@ describe('plugin-meetings', () => {
8016
8332
  meeting.isoLocalClientMeetingJoinTime = undefined;
8017
8333
  assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
8018
8334
  });
8335
+
8019
8336
  it('should fallback to system clock ISO string when given an invalid value', () => {
8020
8337
  const currentSystemTime = new Date().toISOString();
8021
8338
  meeting.isoLocalClientMeetingJoinTime = 'invalid-date';
8022
8339
  assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
8023
8340
  });
8341
+
8024
8342
  it('should set the isoLocalClientMeetingJoinTime correctly for a valid date string', () => {
8025
8343
  const validDateString = 'Tue, 01 Apr 2025 13:00:36 GMT';
8026
8344
  const expectedISOString = new Date(validDateString).toISOString();
@@ -8100,6 +8418,7 @@ describe('plugin-meetings', () => {
8100
8418
 
8101
8419
  meeting.requestScreenShareFloor = sinon.stub().resolves({});
8102
8420
  meeting.releaseScreenShareFloor = sinon.stub().resolves({});
8421
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
8103
8422
  meeting.mediaProperties.mediaDirection = {
8104
8423
  sendAudio: 'fake value', // using non-boolean here so that we can check that these values are untouched in tests
8105
8424
  sendVideo: 'fake value',
@@ -8181,6 +8500,12 @@ describe('plugin-meetings', () => {
8181
8500
  payload: {mediaType: 'share', shareInstanceId: meeting.localShareInstanceId},
8182
8501
  options: {meetingId: meeting.id},
8183
8502
  });
8503
+
8504
+ // ensure the share start timestamp is saved
8505
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
8506
+ key: 'internal.client.share.initiated',
8507
+ });
8508
+
8184
8509
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
8185
8510
 
8186
8511
  assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
@@ -8199,6 +8524,11 @@ describe('plugin-meetings', () => {
8199
8524
  options: {meetingId: meeting.id},
8200
8525
  });
8201
8526
 
8527
+ // ensure the share start timestamp is saved
8528
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
8529
+ key: 'internal.client.share.initiated',
8530
+ });
8531
+
8202
8532
  assert.calledWith(
8203
8533
  meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
8204
8534
  stream
@@ -8867,11 +9197,16 @@ describe('plugin-meetings', () => {
8867
9197
  meeting.hasMediaConnectionConnectedAtLeastOnce = false;
8868
9198
  meeting.setupMediaConnectionListeners();
8869
9199
 
9200
+ sinon.stub(MeetingUtil, 'getCaEventLabelsForIpVersion').returns(['fake labels']);
9201
+
8870
9202
  simulateConnectionStateChange(ConnectionState.Connecting);
8871
9203
 
8872
9204
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
8873
9205
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8874
9206
  name: 'client.ice.start',
9207
+ payload: {
9208
+ labels: ['fake labels'],
9209
+ },
8875
9210
  options: {
8876
9211
  meetingId: meeting.id,
8877
9212
  },
@@ -9194,6 +9529,7 @@ describe('plugin-meetings', () => {
9194
9529
  meeting.deferSDPAnswer = {
9195
9530
  reject: sinon.stub(),
9196
9531
  };
9532
+
9197
9533
  const clearTimeoutSpy = sinon.spy(clock, 'clearTimeout');
9198
9534
 
9199
9535
  const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
@@ -10471,6 +10807,8 @@ describe('plugin-meetings', () => {
10471
10807
  meeting.mediaProperties = {mediaDirection: {sendShare: true}};
10472
10808
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
10473
10809
  (meeting.deviceUrl = 'deviceUrl.com'), (meeting.localShareInstanceId = '1234-5678');
10810
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
10811
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
10474
10812
  });
10475
10813
  it('should call changeMeetingFloor()', async () => {
10476
10814
  meeting.screenShareFloorState = 'GRANTED';
@@ -10488,6 +10826,22 @@ describe('plugin-meetings', () => {
10488
10826
  assert.exists(share.then);
10489
10827
  await share;
10490
10828
  assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
10829
+
10830
+ // ensure the share stop timestamp is saved
10831
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
10832
+ key: 'internal.client.share.stopped',
10833
+ });
10834
+
10835
+ // ensure the CA share stopped metric is submitted with duration
10836
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
10837
+ name: 'client.share.stopped',
10838
+ payload: {
10839
+ mediaType: 'share',
10840
+ shareInstanceId: meeting.localShareInstanceId,
10841
+ shareDuration: 1000,
10842
+ },
10843
+ options: {meetingId: meeting.id},
10844
+ });
10491
10845
  });
10492
10846
  it('should not call changeMeetingFloor() if someone else already has the floor', async () => {
10493
10847
  // change selfId so that it doesn't match the beneficiary id from meeting.locusInfo.mediaShares
@@ -11071,6 +11425,7 @@ describe('plugin-meetings', () => {
11071
11425
  let canUserRenameOthersSpy;
11072
11426
  let canShareWhiteBoardSpy;
11073
11427
  let canMoveToLobbySpy;
11428
+ let isSpokenLanguageAutoDetectionEnabledSpy;
11074
11429
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
11075
11430
 
11076
11431
  beforeEach(() => {
@@ -11102,6 +11457,8 @@ describe('plugin-meetings', () => {
11102
11457
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
11103
11458
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
11104
11459
  canMoveToLobbySpy = sinon.spy(MeetingUtil, 'canMoveToLobby');
11460
+ isSpokenLanguageAutoDetectionEnabledSpy = sinon.spy(MeetingUtil, 'isSpokenLanguageAutoDetectionEnabled');
11461
+
11105
11462
  });
11106
11463
 
11107
11464
  afterEach(() => {
@@ -11654,6 +12011,7 @@ describe('plugin-meetings', () => {
11654
12011
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11655
12012
  assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11656
12013
  assert.calledWith(canMoveToLobbySpy, userDisplayHints);
12014
+ assert.calledWith(isSpokenLanguageAutoDetectionEnabledSpy, userDisplayHints);
11657
12015
 
11658
12016
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11659
12017
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -12060,6 +12418,7 @@ describe('plugin-meetings', () => {
12060
12418
  meeting.locusInfo.self = {url: url1};
12061
12419
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
12062
12420
  meeting.deviceUrl = 'deviceUrl.com';
12421
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12063
12422
  });
12064
12423
  it('should have #startWhiteboardShare', () => {
12065
12424
  assert.exists(meeting.startWhiteboardShare);
@@ -12087,6 +12446,11 @@ describe('plugin-meetings', () => {
12087
12446
  payload: {mediaType: 'whiteboard'},
12088
12447
  options: {meetingId: meeting.id},
12089
12448
  });
12449
+
12450
+ // ensure the share start timestamp is saved
12451
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
12452
+ key: 'internal.client.share.initiated',
12453
+ });
12090
12454
  });
12091
12455
  });
12092
12456
  describe('#stopWhiteboardShare', () => {
@@ -12098,6 +12462,9 @@ describe('plugin-meetings', () => {
12098
12462
  meeting.locusInfo.self = {url: url1};
12099
12463
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
12100
12464
  meeting.deviceUrl = 'deviceUrl.com';
12465
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12466
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
12467
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
12101
12468
  });
12102
12469
  it('should stop the whiteboard share', async () => {
12103
12470
  const whiteboardShare = meeting.stopWhiteboardShare();
@@ -12112,6 +12479,21 @@ describe('plugin-meetings', () => {
12112
12479
  uri: url1,
12113
12480
  });
12114
12481
  assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
12482
+
12483
+ // ensure the share stop timestamp is saved
12484
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
12485
+ key: 'internal.client.share.stopped',
12486
+ });
12487
+
12488
+ // ensure the CA share stopped metric is submitted with duration
12489
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
12490
+ name: 'client.share.stopped',
12491
+ payload: {
12492
+ mediaType: 'whiteboard',
12493
+ shareDuration: 1000,
12494
+ },
12495
+ options: {meetingId: meeting.id},
12496
+ });
12115
12497
  });
12116
12498
  });
12117
12499
  });
@@ -12184,6 +12566,9 @@ describe('plugin-meetings', () => {
12184
12566
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
12185
12567
  meeting.deviceUrl = 'my-web-url';
12186
12568
  meeting.locusInfo.info = {isWebinar: false};
12569
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12570
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1500);
12571
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
12187
12572
  });
12188
12573
 
12189
12574
  const USER_IDS = {
@@ -12410,12 +12795,12 @@ describe('plugin-meetings', () => {
12410
12795
  activeSharingId.whiteboard = beneficiaryId;
12411
12796
 
12412
12797
  eventTrigger.share.push(
12413
- meeting.webinar.selfIsAttendee
12798
+ meeting.webinar.selfIsAttendee || meeting.guest
12414
12799
  ? {
12415
12800
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12416
12801
  functionName: 'remoteShare',
12417
12802
  eventPayload: {
12418
- memberId: null,
12803
+ memberId: meeting.webinar.selfIsAttendee ? beneficiaryId : null,
12419
12804
  url,
12420
12805
  shareInstanceId,
12421
12806
  annotationInfo: undefined,
@@ -12429,7 +12814,8 @@ describe('plugin-meetings', () => {
12429
12814
  }
12430
12815
  );
12431
12816
 
12432
- shareStatus = meeting.webinar.selfIsAttendee
12817
+ shareStatus =
12818
+ meeting.webinar.selfIsAttendee || meeting.guest
12433
12819
  ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
12434
12820
  : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
12435
12821
  }
@@ -12469,7 +12855,7 @@ describe('plugin-meetings', () => {
12469
12855
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12470
12856
  functionName: 'remoteShare',
12471
12857
  eventPayload: {
12472
- memberId: null,
12858
+ memberId: beneficiaryId,
12473
12859
  url,
12474
12860
  shareInstanceId,
12475
12861
  annotationInfo: undefined,
@@ -12647,6 +13033,36 @@ describe('plugin-meetings', () => {
12647
13033
  });
12648
13034
  });
12649
13035
 
13036
+ describe('Whiteboard Share - User is guest', () => {
13037
+ it('User receives a remote share instead of whiteboard share', () => {
13038
+ // Set the guest flag
13039
+ meeting.guest = true;
13040
+
13041
+ // Step 1: Start sharing whiteboard A
13042
+ const data1 = generateData(
13043
+ blankPayload, // Initial payload
13044
+ true, // isGranting: Granting share
13045
+ false, // isContent: Whiteboard (not content)
13046
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
13047
+ RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
13048
+ );
13049
+
13050
+ // Step 2: Stop sharing whiteboard A
13051
+ const data2 = generateData(
13052
+ data1.payload, // Updated payload from Step 1
13053
+ false, // isGranting: Stopping share
13054
+ false, // isContent: Whiteboard
13055
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
13056
+ );
13057
+
13058
+ // Validate the payload changes and status updates
13059
+ payloadTestHelper([data1]);
13060
+
13061
+ // Specific assertions for guest
13062
+ assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
13063
+ });
13064
+ });
13065
+
12650
13066
  describe('Whiteboard A --> Whiteboard B', () => {
12651
13067
  it('Scenario #1: you share both whiteboards', () => {
12652
13068
  const data1 = generateData(
@@ -13298,7 +13714,54 @@ describe('plugin-meetings', () => {
13298
13714
  payloadTestHelper([data1, data2, data3]);
13299
13715
  });
13300
13716
  });
13301
- });
13717
+
13718
+ it('should send share stopped metric when whiteboard sharing stops', () => {
13719
+ // Start whiteboard sharing (this won't trigger metrics)
13720
+ const data1 = generateData(
13721
+ blankPayload,
13722
+ true, // isGranting: true
13723
+ false, // isContent: false (whiteboard)
13724
+ USER_IDS.ME,
13725
+ RESOURCE_URLS.WHITEBOARD_A
13726
+ );
13727
+
13728
+ // Stop whiteboard sharing (this should trigger metrics)
13729
+ const data2 = generateData(
13730
+ data1.payload,
13731
+ false, // isGranting: false (stopping share)
13732
+ false, // isContent: false (whiteboard)
13733
+ USER_IDS.ME
13734
+ );
13735
+
13736
+ // Trigger the events
13737
+ meeting.locusInfo.emit(
13738
+ {function: 'test', file: 'test'},
13739
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
13740
+ data1.payload
13741
+ );
13742
+
13743
+ meeting.locusInfo.emit(
13744
+ {function: 'test', file: 'test'},
13745
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
13746
+ data2.payload
13747
+ );
13748
+
13749
+ // Verify metrics were called when whiteboard sharing stopped
13750
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
13751
+ key: 'internal.client.share.stopped',
13752
+ });
13753
+
13754
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
13755
+ name: 'client.share.stopped',
13756
+ payload: {
13757
+ mediaType: 'whiteboard',
13758
+ shareDuration: 1500, // mocked return value
13759
+ },
13760
+ options: {
13761
+ meetingId: meeting.id,
13762
+ },
13763
+ });
13764
+ });
13302
13765
 
13303
13766
  describe('handleShareVideoStreamMuteStateChange', () => {
13304
13767
  it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
@@ -13325,6 +13788,7 @@ describe('plugin-meetings', () => {
13325
13788
  });
13326
13789
  });
13327
13790
  });
13791
+ });
13328
13792
 
13329
13793
  describe('#startKeepAlive', () => {
13330
13794
  let clock;
@@ -14091,4 +14555,469 @@ describe('plugin-meetings', () => {
14091
14555
  assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA);
14092
14556
  });
14093
14557
  });
14558
+
14559
+ describe('#setStage', () => {
14560
+ const check = async (options, expectedVideoLayout) => {
14561
+ const locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${uuidv4()}`;
14562
+ meeting.locusUrl = locusUrl;
14563
+
14564
+ const setStagePromise = meeting.setStage(options);
14565
+
14566
+ assert.exists(setStagePromise.then);
14567
+ await setStagePromise;
14568
+
14569
+ assert.calledOnceWithExactly(
14570
+ meeting.meetingRequest.synchronizeStage,
14571
+ locusUrl,
14572
+ expectedVideoLayout
14573
+ );
14574
+ };
14575
+
14576
+ beforeEach(() => {
14577
+ meeting.meetingRequest.synchronizeStage = sinon.stub().returns(Promise.resolve());
14578
+ });
14579
+
14580
+ it('sends the expected request when no options are provided', async () => {
14581
+ await check(undefined, {
14582
+ overrideDefault: true,
14583
+ lockAttendeeViewOnStageOnly: false,
14584
+ stageParameters: {
14585
+ activeSpeakerProportion: 0.5,
14586
+ showActiveSpeaker: {show: false, order: 0},
14587
+ stageManagerType: 0,
14588
+ },
14589
+ });
14590
+ });
14591
+
14592
+ it('sends the expected request when empty options are provided', async () => {
14593
+ await check(
14594
+ {},
14595
+ {
14596
+ overrideDefault: true,
14597
+ lockAttendeeViewOnStageOnly: false,
14598
+ stageParameters: {
14599
+ activeSpeakerProportion: 0.5,
14600
+ showActiveSpeaker: {show: false, order: 0},
14601
+ stageManagerType: 0,
14602
+ },
14603
+ }
14604
+ );
14605
+ });
14606
+
14607
+ [0.25, 0.5, 0.75].forEach((activeSpeakerProportion) => {
14608
+ it(`sends the expected request when only the active speaker proportion option is provided as ${activeSpeakerProportion}`, async () => {
14609
+ await check(
14610
+ {activeSpeakerProportion},
14611
+ {
14612
+ overrideDefault: true,
14613
+ lockAttendeeViewOnStageOnly: false,
14614
+ stageParameters: {
14615
+ activeSpeakerProportion,
14616
+ showActiveSpeaker: {show: false, order: 0},
14617
+ stageManagerType: 0,
14618
+ },
14619
+ }
14620
+ );
14621
+ });
14622
+ });
14623
+
14624
+ it('sends the expected request when only the custom background option is provided', async () => {
14625
+ const customBackground = {
14626
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14627
+ };
14628
+
14629
+ await check(
14630
+ {customBackground},
14631
+ {
14632
+ overrideDefault: true,
14633
+ lockAttendeeViewOnStageOnly: false,
14634
+ stageParameters: {
14635
+ activeSpeakerProportion: 0.5,
14636
+ showActiveSpeaker: {show: false, order: 0},
14637
+ stageManagerType: 2,
14638
+ },
14639
+ customLayouts: {background: customBackground},
14640
+ }
14641
+ );
14642
+ });
14643
+
14644
+ it('sends the expected request when only the custom logo option is provided', async () => {
14645
+ const customLogo = {
14646
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14647
+ position: 'LowerRight',
14648
+ };
14649
+
14650
+ await check(
14651
+ {customLogo},
14652
+ {
14653
+ overrideDefault: true,
14654
+ lockAttendeeViewOnStageOnly: false,
14655
+ stageParameters: {
14656
+ activeSpeakerProportion: 0.5,
14657
+ showActiveSpeaker: {show: false, order: 0},
14658
+ stageManagerType: 1,
14659
+ },
14660
+ customLayouts: {logo: customLogo},
14661
+ }
14662
+ );
14663
+ });
14664
+
14665
+ it('sends the expected request when only the custom name label option is provided', async () => {
14666
+ const customNameLabel = {
14667
+ accentColor: '#0A7806',
14668
+ background: {color: 'rgba(255, 255, 255, 1)'},
14669
+ border: {color: 'rgba(255, 255, 255, 1)'},
14670
+ content: {
14671
+ displayName: {color: 'rgba(0, 0, 0, 0.95)'},
14672
+ subtitle: {color: 'rgba(0, 0, 0, 0.6)'},
14673
+ },
14674
+ decoration: {color: 'rgba(10, 120, 6, 1)'},
14675
+ fadeOut: {delay: 15},
14676
+ type: 'Primary',
14677
+ };
14678
+
14679
+ await check(
14680
+ {customNameLabel},
14681
+ {
14682
+ overrideDefault: true,
14683
+ lockAttendeeViewOnStageOnly: false,
14684
+ stageParameters: {
14685
+ activeSpeakerProportion: 0.5,
14686
+ showActiveSpeaker: {show: false, order: 0},
14687
+ stageManagerType: 4,
14688
+ },
14689
+ nameLabelStyle: customNameLabel,
14690
+ }
14691
+ );
14692
+ });
14693
+
14694
+ it('sends the expected request when only the custom background and logo options are provided', async () => {
14695
+ const customBackground = {
14696
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14697
+ };
14698
+ const customLogo = {
14699
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14700
+ position: 'UpperRight',
14701
+ };
14702
+
14703
+ await check(
14704
+ {customBackground, customLogo},
14705
+ {
14706
+ overrideDefault: true,
14707
+ lockAttendeeViewOnStageOnly: false,
14708
+ stageParameters: {
14709
+ activeSpeakerProportion: 0.5,
14710
+ showActiveSpeaker: {show: false, order: 0},
14711
+ stageManagerType: 3,
14712
+ },
14713
+ customLayouts: {background: customBackground, logo: customLogo},
14714
+ }
14715
+ );
14716
+ });
14717
+
14718
+ it('sends the expected request when only the custom background and name label options are provided', async () => {
14719
+ const customBackground = {
14720
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14721
+ };
14722
+ const customNameLabel = {
14723
+ accentColor: '#00A3FF',
14724
+ background: {color: 'rgba(0, 163, 255, 1)'},
14725
+ border: {color: 'rgba(0, 163, 255, 1)'},
14726
+ content: {
14727
+ displayName: {color: 'rgba(255, 255, 255, 0.95)'},
14728
+ subtitle: {color: 'rgba(255, 255, 255, 0.7)'},
14729
+ },
14730
+ decoration: {color: 'rgba(255, 255, 255, 0.95)'},
14731
+ fadeOut: {delay: 15},
14732
+ type: 'PrimaryInverted',
14733
+ };
14734
+
14735
+ await check(
14736
+ {customBackground, customNameLabel},
14737
+ {
14738
+ overrideDefault: true,
14739
+ lockAttendeeViewOnStageOnly: false,
14740
+ stageParameters: {
14741
+ activeSpeakerProportion: 0.5,
14742
+ showActiveSpeaker: {show: false, order: 0},
14743
+ stageManagerType: 6,
14744
+ },
14745
+ customLayouts: {background: customBackground},
14746
+ nameLabelStyle: customNameLabel,
14747
+ }
14748
+ );
14749
+ });
14750
+
14751
+ it('sends the expected request when only the custom logo and name label options are provided', async () => {
14752
+ const customLogo = {
14753
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14754
+ position: 'UpperLeft',
14755
+ };
14756
+ const customNameLabel = {
14757
+ accentColor: '#942B2B',
14758
+ background: {color: 'rgba(255, 255, 255, 1)'},
14759
+ border: {color: 'rgba(148, 43, 43, 1)'},
14760
+ content: {
14761
+ displayName: {color: 'rgba(0, 0, 0, 0.95)'},
14762
+ subtitle: {color: 'rgba(0, 0, 0, 0.6)'},
14763
+ },
14764
+ decoration: {color: 'rgba(0, 0, 0, 0)'},
14765
+ fadeOut: {delay: 15},
14766
+ type: 'Secondary',
14767
+ };
14768
+
14769
+ await check(
14770
+ {customLogo, customNameLabel},
14771
+ {
14772
+ overrideDefault: true,
14773
+ lockAttendeeViewOnStageOnly: false,
14774
+ stageParameters: {
14775
+ activeSpeakerProportion: 0.5,
14776
+ showActiveSpeaker: {show: false, order: 0},
14777
+ stageManagerType: 5,
14778
+ },
14779
+ customLayouts: {logo: customLogo},
14780
+ nameLabelStyle: customNameLabel,
14781
+ }
14782
+ );
14783
+ });
14784
+
14785
+ it('sends the expected request when only the custom background, logo, name label options are provided', async () => {
14786
+ const customBackground = {
14787
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14788
+ };
14789
+ const customLogo = {
14790
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14791
+ position: 'LowerLeft',
14792
+ };
14793
+ const customNameLabel = {
14794
+ accentColor: '#EBD960',
14795
+ background: {color: 'rgba(235, 217, 96, 0.55)'},
14796
+ border: {color: 'rgba(235, 217, 96, 0.55)'},
14797
+ content: {
14798
+ displayName: {color: 'rgba(255, 255, 255, 0.95)'},
14799
+ subtitle: {color: 'rgba(255, 255, 255, 0.7)'},
14800
+ },
14801
+ decoration: {color: 'rgba(0, 0, 0, 0)'},
14802
+ fadeOut: {delay: 15},
14803
+ type: 'SecondaryInverted',
14804
+ };
14805
+
14806
+ await check(
14807
+ {customBackground, customLogo, customNameLabel},
14808
+ {
14809
+ overrideDefault: true,
14810
+ lockAttendeeViewOnStageOnly: false,
14811
+ stageParameters: {
14812
+ activeSpeakerProportion: 0.5,
14813
+ showActiveSpeaker: {show: false, order: 0},
14814
+ stageManagerType: 7,
14815
+ },
14816
+ customLayouts: {background: customBackground, logo: customLogo},
14817
+ nameLabelStyle: customNameLabel,
14818
+ }
14819
+ );
14820
+ });
14821
+
14822
+ it('sends the expected request when only the important participants option is provided as empty', async () => {
14823
+ await check(
14824
+ {importantParticipants: []},
14825
+ {
14826
+ overrideDefault: true,
14827
+ lockAttendeeViewOnStageOnly: false,
14828
+ stageParameters: {
14829
+ activeSpeakerProportion: 0.5,
14830
+ showActiveSpeaker: {show: false, order: 0},
14831
+ stageManagerType: 0,
14832
+ },
14833
+ }
14834
+ );
14835
+ });
14836
+
14837
+ it('sends the expected request when only the important participants option is provided as populated', async () => {
14838
+ const importantParticipants = [
14839
+ {mainCsi: 11111111, participantId: uuidv4()},
14840
+ {mainCsi: 22222222, participantId: uuidv4()},
14841
+ {mainCsi: 33333333, participantId: uuidv4()},
14842
+ {mainCsi: 44444444, participantId: uuidv4()},
14843
+ {mainCsi: 55555555, participantId: uuidv4()},
14844
+ {mainCsi: 66666666, participantId: uuidv4()},
14845
+ {mainCsi: 77777777, participantId: uuidv4()},
14846
+ {mainCsi: 88888888, participantId: uuidv4()},
14847
+ ];
14848
+
14849
+ await check(
14850
+ {importantParticipants},
14851
+ {
14852
+ overrideDefault: true,
14853
+ lockAttendeeViewOnStageOnly: false,
14854
+ stageParameters: {
14855
+ activeSpeakerProportion: 0.5,
14856
+ importantParticipants: [
14857
+ {...importantParticipants[0], order: 1},
14858
+ {...importantParticipants[1], order: 2},
14859
+ {...importantParticipants[2], order: 3},
14860
+ {...importantParticipants[3], order: 4},
14861
+ {...importantParticipants[4], order: 5},
14862
+ {...importantParticipants[5], order: 6},
14863
+ {...importantParticipants[6], order: 7},
14864
+ {...importantParticipants[7], order: 8},
14865
+ ],
14866
+ showActiveSpeaker: {show: false, order: 0},
14867
+ stageManagerType: 0,
14868
+ },
14869
+ }
14870
+ );
14871
+ });
14872
+
14873
+ [false, true].forEach((lockAttendeeViewOnStage) => {
14874
+ it(`sends the expected request when only the lock attendee view on stage option is provided as ${lockAttendeeViewOnStage}`, async () => {
14875
+ await check(
14876
+ {lockAttendeeViewOnStage},
14877
+ {
14878
+ overrideDefault: true,
14879
+ lockAttendeeViewOnStageOnly: lockAttendeeViewOnStage,
14880
+ stageParameters: {
14881
+ activeSpeakerProportion: 0.5,
14882
+ showActiveSpeaker: {show: false, order: 0},
14883
+ stageManagerType: 0,
14884
+ },
14885
+ }
14886
+ );
14887
+ });
14888
+ });
14889
+
14890
+ [false, true].forEach((showActiveSpeaker) => {
14891
+ it(`sends the expected request when only the show active speaker option is provided as ${showActiveSpeaker}`, async () => {
14892
+ await check(
14893
+ {showActiveSpeaker},
14894
+ {
14895
+ overrideDefault: true,
14896
+ lockAttendeeViewOnStageOnly: false,
14897
+ stageParameters: {
14898
+ activeSpeakerProportion: 0.5,
14899
+ showActiveSpeaker: {show: showActiveSpeaker, order: 0},
14900
+ stageManagerType: 0,
14901
+ },
14902
+ }
14903
+ );
14904
+ });
14905
+ });
14906
+
14907
+ it('sends the expected request when all options are provided', async () => {
14908
+ const activeSpeakerProportion = 0.6;
14909
+ const customBackground = {
14910
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14911
+ };
14912
+ const customLogo = {
14913
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14914
+ position: 'UpperMiddle',
14915
+ };
14916
+ const customNameLabel = {
14917
+ accentColor: '#0A7806',
14918
+ background: {color: 'rgba(255, 255, 255, 1)'},
14919
+ border: {color: 'rgba(255, 255, 255, 1)'},
14920
+ content: {
14921
+ displayName: {color: 'rgba(0, 0, 0, 0.95)'},
14922
+ subtitle: {color: 'rgba(0, 0, 0, 0.6)'},
14923
+ },
14924
+ decoration: {color: 'rgba(10, 120, 6, 1)'},
14925
+ fadeOut: {delay: 15},
14926
+ type: 'Primary',
14927
+ };
14928
+ const importantParticipants = [
14929
+ {mainCsi: 11111111, participantId: uuidv4()},
14930
+ {mainCsi: 22222222, participantId: uuidv4()},
14931
+ {mainCsi: 33333333, participantId: uuidv4()},
14932
+ {mainCsi: 44444444, participantId: uuidv4()},
14933
+ {mainCsi: 55555555, participantId: uuidv4()},
14934
+ {mainCsi: 66666666, participantId: uuidv4()},
14935
+ {mainCsi: 77777777, participantId: uuidv4()},
14936
+ {mainCsi: 88888888, participantId: uuidv4()},
14937
+ ];
14938
+ const lockAttendeeViewOnStage = true;
14939
+ const showActiveSpeaker = true;
14940
+
14941
+ await check(
14942
+ {
14943
+ activeSpeakerProportion,
14944
+ customBackground,
14945
+ customLogo,
14946
+ customNameLabel,
14947
+ importantParticipants,
14948
+ lockAttendeeViewOnStage,
14949
+ showActiveSpeaker,
14950
+ },
14951
+ {
14952
+ overrideDefault: true,
14953
+ lockAttendeeViewOnStageOnly: lockAttendeeViewOnStage,
14954
+ stageParameters: {
14955
+ activeSpeakerProportion,
14956
+ importantParticipants: [
14957
+ {...importantParticipants[0], order: 1},
14958
+ {...importantParticipants[1], order: 2},
14959
+ {...importantParticipants[2], order: 3},
14960
+ {...importantParticipants[3], order: 4},
14961
+ {...importantParticipants[4], order: 5},
14962
+ {...importantParticipants[5], order: 6},
14963
+ {...importantParticipants[6], order: 7},
14964
+ {...importantParticipants[7], order: 8},
14965
+ ],
14966
+ showActiveSpeaker: {show: showActiveSpeaker, order: 0},
14967
+ stageManagerType: 7,
14968
+ },
14969
+ customLayouts: {background: customBackground, logo: customLogo},
14970
+ nameLabelStyle: customNameLabel,
14971
+ }
14972
+ );
14973
+ });
14974
+ });
14975
+
14976
+ describe('#unsetStage', () => {
14977
+ beforeEach(() => {
14978
+ meeting.meetingRequest.synchronizeStage = sinon.stub().returns(Promise.resolve());
14979
+ });
14980
+
14981
+ it('sends the expected request', async () => {
14982
+ const locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${uuidv4()}`;
14983
+ meeting.locusUrl = locusUrl;
14984
+
14985
+ const unsetStagePromise = meeting.unsetStage();
14986
+
14987
+ assert.exists(unsetStagePromise.then);
14988
+ await unsetStagePromise;
14989
+
14990
+ assert.calledOnceWithExactly(
14991
+ meeting.meetingRequest.synchronizeStage,
14992
+ locusUrl,
14993
+ {overrideDefault: false}
14994
+ );
14995
+ });
14996
+ });
14997
+
14998
+ describe('#notifyHost', () => {
14999
+ beforeEach(() => {
15000
+ meeting.meetingRequest.notifyHost = sinon.stub().returns(Promise.resolve());
15001
+ });
15002
+
15003
+ it('sends the expected request', async () => {
15004
+ meeting.meetingInfo.siteFullUrl = `convergedats.webex.com`;
15005
+ const meetingUuid = 'meeting-uuid';
15006
+ const displayName = ['Test', 'User'];
15007
+ meeting.locusId = 'locusId';
15008
+
15009
+ const notifyHostPromise = meeting.notifyHost(meetingUuid, displayName);
15010
+
15011
+ assert.exists(notifyHostPromise.then);
15012
+ await notifyHostPromise;
15013
+
15014
+ assert.calledOnceWithExactly(
15015
+ meeting.meetingRequest.notifyHost,
15016
+ meeting.meetingInfo.siteFullUrl,
15017
+ meeting.locusId,
15018
+ meetingUuid,
15019
+ displayName,
15020
+ );
15021
+ });
15022
+ });
14094
15023
  });