@webex/plugin-meetings 3.9.0-webinar5k.1 → 3.10.0

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 (138) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +24 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/controls-options-manager/index.js +22 -5
  6. package/dist/controls-options-manager/index.js.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/interceptors/index.js +7 -0
  10. package/dist/interceptors/index.js.map +1 -1
  11. package/dist/interceptors/locusRouteToken.js +116 -0
  12. package/dist/interceptors/locusRouteToken.js.map +1 -0
  13. package/dist/interpretation/index.js +1 -1
  14. package/dist/interpretation/siLanguage.js +1 -1
  15. package/dist/locus-info/controlsUtils.js +11 -2
  16. package/dist/locus-info/controlsUtils.js.map +1 -1
  17. package/dist/locus-info/index.js +76 -322
  18. package/dist/locus-info/index.js.map +1 -1
  19. package/dist/locus-info/parser.js +4 -1
  20. package/dist/locus-info/parser.js.map +1 -1
  21. package/dist/media/properties.js +53 -5
  22. package/dist/media/properties.js.map +1 -1
  23. package/dist/meeting/in-meeting-actions.js +14 -0
  24. package/dist/meeting/in-meeting-actions.js.map +1 -1
  25. package/dist/meeting/index.js +467 -277
  26. package/dist/meeting/index.js.map +1 -1
  27. package/dist/meeting/request.js +177 -14
  28. package/dist/meeting/request.js.map +1 -1
  29. package/dist/meeting/type.js +7 -0
  30. package/dist/meeting/type.js.map +1 -0
  31. package/dist/meeting/util.js +100 -3
  32. package/dist/meeting/util.js.map +1 -1
  33. package/dist/meeting-info/meeting-info-v2.js +29 -21
  34. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  35. package/dist/meetings/index.js +20 -16
  36. package/dist/meetings/index.js.map +1 -1
  37. package/dist/member/index.js +9 -0
  38. package/dist/member/index.js.map +1 -1
  39. package/dist/member/util.js +10 -0
  40. package/dist/member/util.js.map +1 -1
  41. package/dist/members/index.js +10 -7
  42. package/dist/members/index.js.map +1 -1
  43. package/dist/members/util.js +7 -2
  44. package/dist/members/util.js.map +1 -1
  45. package/dist/metrics/constants.js +2 -1
  46. package/dist/metrics/constants.js.map +1 -1
  47. package/dist/multistream/mediaRequestManager.js +1 -1
  48. package/dist/multistream/mediaRequestManager.js.map +1 -1
  49. package/dist/multistream/remoteMedia.js +34 -5
  50. package/dist/multistream/remoteMedia.js.map +1 -1
  51. package/dist/multistream/remoteMediaGroup.js +42 -2
  52. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  53. package/dist/reachability/index.js +3 -3
  54. package/dist/reachability/index.js.map +1 -1
  55. package/dist/types/constants.d.ts +23 -0
  56. package/dist/types/controls-options-manager/index.d.ts +9 -1
  57. package/dist/types/interceptors/index.d.ts +2 -1
  58. package/dist/types/interceptors/locusRouteToken.d.ts +38 -0
  59. package/dist/types/locus-info/index.d.ts +9 -54
  60. package/dist/types/media/properties.d.ts +21 -0
  61. package/dist/types/meeting/in-meeting-actions.d.ts +14 -0
  62. package/dist/types/meeting/index.d.ts +64 -29
  63. package/dist/types/meeting/request.d.ts +42 -0
  64. package/dist/types/meeting/type.d.ts +9 -0
  65. package/dist/types/meeting/util.d.ts +13 -0
  66. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  67. package/dist/types/meetings/index.d.ts +3 -1
  68. package/dist/types/member/index.d.ts +1 -0
  69. package/dist/types/member/util.d.ts +5 -0
  70. package/dist/types/members/index.d.ts +12 -11
  71. package/dist/types/members/util.d.ts +8 -4
  72. package/dist/types/metrics/constants.d.ts +1 -0
  73. package/dist/types/multistream/remoteMedia.d.ts +20 -1
  74. package/dist/types/multistream/remoteMediaGroup.d.ts +11 -0
  75. package/dist/webinar/index.js +1 -1
  76. package/package.json +25 -27
  77. package/src/constants.ts +26 -2
  78. package/src/controls-options-manager/index.ts +26 -5
  79. package/src/index.ts +2 -1
  80. package/src/interceptors/index.ts +2 -1
  81. package/src/interceptors/locusRouteToken.ts +80 -0
  82. package/src/locus-info/controlsUtils.ts +18 -0
  83. package/src/locus-info/index.ts +69 -357
  84. package/src/locus-info/parser.ts +5 -1
  85. package/src/media/properties.ts +43 -0
  86. package/src/meeting/in-meeting-actions.ts +29 -0
  87. package/src/meeting/index.ts +296 -87
  88. package/src/meeting/request.ts +141 -0
  89. package/src/meeting/type.ts +9 -0
  90. package/src/meeting/util.ts +107 -3
  91. package/src/meeting-info/meeting-info-v2.ts +24 -5
  92. package/src/meetings/index.ts +15 -22
  93. package/src/member/index.ts +10 -0
  94. package/src/member/util.ts +14 -0
  95. package/src/members/index.ts +20 -10
  96. package/src/members/util.ts +20 -3
  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/reachability/index.ts +3 -3
  102. package/test/unit/spec/common/browser-detection.js +0 -24
  103. package/test/unit/spec/controls-options-manager/index.js +47 -0
  104. package/test/unit/spec/fixture/locus.js +1 -0
  105. package/test/unit/spec/interceptors/locusRouteToken.ts +87 -0
  106. package/test/unit/spec/locus-info/index.js +80 -361
  107. package/test/unit/spec/locus-info/parser.js +3 -2
  108. package/test/unit/spec/media/properties.ts +137 -0
  109. package/test/unit/spec/meeting/in-meeting-actions.ts +14 -0
  110. package/test/unit/spec/meeting/index.js +637 -53
  111. package/test/unit/spec/meeting/muteState.js +32 -6
  112. package/test/unit/spec/meeting/request.js +21 -0
  113. package/test/unit/spec/meeting/utils.js +171 -18
  114. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  115. package/test/unit/spec/meetings/index.js +12 -5
  116. package/test/unit/spec/member/util.js +24 -0
  117. package/test/unit/spec/members/collection.js +120 -0
  118. package/test/unit/spec/members/index.js +107 -2
  119. package/test/unit/spec/members/request.js +55 -0
  120. package/test/unit/spec/members/utils.js +116 -14
  121. package/test/unit/spec/multistream/mediaRequestManager.ts +19 -6
  122. package/test/unit/spec/multistream/remoteMedia.ts +66 -2
  123. package/test/unit/spec/reachability/index.ts +158 -3
  124. package/test/unit/spec/roap/turnDiscovery.ts +3 -3
  125. package/dist/hashTree/constants.js +0 -23
  126. package/dist/hashTree/constants.js.map +0 -1
  127. package/dist/hashTree/hashTree.js +0 -516
  128. package/dist/hashTree/hashTree.js.map +0 -1
  129. package/dist/hashTree/hashTreeParser.js +0 -521
  130. package/dist/hashTree/hashTreeParser.js.map +0 -1
  131. package/dist/types/hashTree/constants.d.ts +0 -8
  132. package/dist/types/hashTree/hashTree.d.ts +0 -128
  133. package/dist/types/hashTree/hashTreeParser.d.ts +0 -152
  134. package/src/hashTree/constants.ts +0 -12
  135. package/src/hashTree/hashTree.ts +0 -460
  136. package/src/hashTree/hashTreeParser.ts +0 -556
  137. package/test/unit/spec/hashTree/hashTree.ts +0 -394
  138. package/test/unit/spec/hashTree/hashTreeParser.ts +0 -156
@@ -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
  });
@@ -1949,21 +1990,25 @@ describe('plugin-meetings', () => {
1949
1990
  });
1950
1991
  });
1951
1992
 
1952
- it('should post error event if failed', async () => {
1993
+ it('should handle join failure', async () => {
1953
1994
  MeetingUtil.isPinOrGuest = sinon.stub().returns(false);
1995
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
1996
+
1954
1997
  await meeting.join().catch(() => {
1955
- assert.deepEqual(
1956
- webex.internal.newMetrics.submitClientEvent.getCall(1).args[0].name,
1957
- 'client.locus.join.response'
1958
- );
1959
- assert.match(
1960
- 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,
1961
2004
  {
1962
- code: 2,
1963
- error: null,
1964
- joinOptions: {},
1965
- sdkMessage:
1966
- '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},
1967
2012
  }
1968
2013
  );
1969
2014
  });
@@ -2172,6 +2217,7 @@ describe('plugin-meetings', () => {
2172
2217
  });
2173
2218
  meeting.audio = muteStateStub;
2174
2219
  meeting.video = muteStateStub;
2220
+ sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.ipv4_and_ipv6);
2175
2221
  sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
2176
2222
  sinon.stub(meeting, 'setupMediaConnectionListeners');
2177
2223
  sinon.stub(meeting, 'setMercuryListener');
@@ -2243,13 +2289,24 @@ describe('plugin-meetings', () => {
2243
2289
  close: sinon.stub(),
2244
2290
  forceRtcMetricsSend,
2245
2291
  });
2246
- // 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
2247
2297
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2298
+ meeting.statsMonitor = mockStatsMonitor;
2299
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2248
2300
  const error = await assert.isRejected(meeting.addMedia());
2249
2301
 
2250
2302
  assert.calledOnce(forceRtcMetricsSend);
2303
+ assert.calledOnce(mockStatsMonitor.removeAllListeners);
2304
+ assert.calledOnce(mockNetworkQualityMonitor.removeAllListeners);
2251
2305
 
2252
2306
  assert.isNull(meeting.statsAnalyzer);
2307
+ assert.isNull(meeting.statsMonitor);
2308
+ assert.isNull(meeting.networkQualityMonitor);
2309
+
2253
2310
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
2254
2311
  assert.calledWith(webex.internal.newMetrics.submitInternalEvent.firstCall, {
2255
2312
  name: 'internal.client.add-media.turn-discovery.start',
@@ -2293,6 +2350,7 @@ describe('plugin-meetings', () => {
2293
2350
  selected_subnet: null,
2294
2351
  numTransports: 1,
2295
2352
  iceCandidatesCount: 0,
2353
+ ipver: 1,
2296
2354
  }
2297
2355
  );
2298
2356
  });
@@ -2340,6 +2398,7 @@ describe('plugin-meetings', () => {
2340
2398
  subnet_reachable: null,
2341
2399
  selected_cluster: null,
2342
2400
  selected_subnet: null,
2401
+ ipver: 1,
2343
2402
  })
2344
2403
  );
2345
2404
 
@@ -2359,12 +2418,23 @@ describe('plugin-meetings', () => {
2359
2418
 
2360
2419
  meeting.waitForRemoteSDPAnswer = sinon.stub().rejects();
2361
2420
 
2362
- // 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
2363
2425
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2426
+ meeting.statsMonitor = mockStatsMonitor;
2427
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2364
2428
 
2365
2429
  const error = await assert.isRejected(meeting.addMedia());
2366
2430
 
2367
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
+
2368
2438
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
2369
2439
  assert.calledWith(webex.internal.newMetrics.submitInternalEvent.firstCall, {
2370
2440
  name: 'internal.client.add-media.turn-discovery.start',
@@ -2408,6 +2478,7 @@ describe('plugin-meetings', () => {
2408
2478
  subnet_reachable: null,
2409
2479
  selected_cluster: null,
2410
2480
  selected_subnet: null,
2481
+ ipver: 1,
2411
2482
  }
2412
2483
  );
2413
2484
  });
@@ -2428,8 +2499,9 @@ describe('plugin-meetings', () => {
2428
2499
  },
2429
2500
  },
2430
2501
  });
2431
- // 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
2432
2503
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2504
+ meeting.statsMonitor = {removeAllListeners: sinon.stub()};
2433
2505
  const error = await assert.isRejected(meeting.addMedia());
2434
2506
 
2435
2507
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
@@ -2468,10 +2540,12 @@ describe('plugin-meetings', () => {
2468
2540
  subnet_reachable: null,
2469
2541
  selected_cluster: null,
2470
2542
  selected_subnet: null,
2543
+ ipver: 1,
2471
2544
  })
2472
2545
  );
2473
2546
 
2474
2547
  assert.isNull(meeting.statsAnalyzer);
2548
+ assert.isNull(meeting.statsMonitor);
2475
2549
  });
2476
2550
 
2477
2551
  it('should include the peer connection properties correctly for transcoded', async () => {
@@ -2488,8 +2562,14 @@ describe('plugin-meetings', () => {
2488
2562
  },
2489
2563
  },
2490
2564
  });
2491
- // 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
2492
2570
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2571
+ meeting.statsMonitor = mockStatsMonitor;
2572
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2493
2573
  const error = await assert.isRejected(meeting.addMedia());
2494
2574
 
2495
2575
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
@@ -2528,10 +2608,15 @@ describe('plugin-meetings', () => {
2528
2608
  subnet_reachable: null,
2529
2609
  selected_cluster: null,
2530
2610
  selected_subnet: null,
2611
+ ipver: 1,
2531
2612
  })
2532
2613
  );
2533
2614
 
2534
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);
2535
2620
  });
2536
2621
 
2537
2622
  it('should work the second time addMedia is called in case the first time fails', async () => {
@@ -3052,6 +3137,7 @@ describe('plugin-meetings', () => {
3052
3137
  subnet_reachable: null,
3053
3138
  selected_cluster: null,
3054
3139
  selected_subnet: null,
3140
+ ipver: 1,
3055
3141
  },
3056
3142
  ]);
3057
3143
 
@@ -3253,6 +3339,7 @@ describe('plugin-meetings', () => {
3253
3339
  connectionType: 'udp',
3254
3340
  selectedCandidatePairChanges: 2,
3255
3341
  ipVersion: 'IPv6',
3342
+ ipver: 1,
3256
3343
  numTransports: 1,
3257
3344
  isMultistream: false,
3258
3345
  retriedWithTurnServer: true,
@@ -3399,6 +3486,7 @@ describe('plugin-meetings', () => {
3399
3486
  meeting.iceCandidatesCount = 3;
3400
3487
  meeting.iceCandidateErrors.set('701_error', 3);
3401
3488
  meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
3489
+ MeetingUtil.getIpVersion.returns(IP_VERSION.only_ipv6);
3402
3490
 
3403
3491
  await meeting.addMedia({
3404
3492
  mediaSettings: {},
@@ -3414,6 +3502,7 @@ describe('plugin-meetings', () => {
3414
3502
  connectionType: 'udp',
3415
3503
  selectedCandidatePairChanges: 2,
3416
3504
  ipVersion: 'IPv6',
3505
+ ipver: 6,
3417
3506
  numTransports: 1,
3418
3507
  isMultistream: false,
3419
3508
  retriedWithTurnServer: false,
@@ -3492,6 +3581,7 @@ describe('plugin-meetings', () => {
3492
3581
  selected_cluster: null,
3493
3582
  selected_subnet: null,
3494
3583
  iceCandidatesCount: 0,
3584
+ ipver: 1,
3495
3585
  }
3496
3586
  );
3497
3587
 
@@ -3556,6 +3646,7 @@ describe('plugin-meetings', () => {
3556
3646
  selected_cluster: null,
3557
3647
  selected_subnet: null,
3558
3648
  iceCandidatesCount: 0,
3649
+ ipver: 1,
3559
3650
  }
3560
3651
  );
3561
3652
 
@@ -3602,6 +3693,7 @@ describe('plugin-meetings', () => {
3602
3693
  locus_id: meeting.locusUrl.split('/').pop(),
3603
3694
  connectionType: 'udp',
3604
3695
  ipVersion: 'IPv6',
3696
+ ipver: 1,
3605
3697
  selectedCandidatePairChanges: 2,
3606
3698
  numTransports: 1,
3607
3699
  isMultistream: false,
@@ -3682,6 +3774,7 @@ describe('plugin-meetings', () => {
3682
3774
  selected_cluster: 'some.cluster',
3683
3775
  selected_subnet: '1.X.X.X',
3684
3776
  iceCandidatesCount: 0,
3777
+ ipver: 1,
3685
3778
  }
3686
3779
  );
3687
3780
 
@@ -3987,13 +4080,14 @@ describe('plugin-meetings', () => {
3987
4080
  });
3988
4081
  });
3989
4082
 
3990
- 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 () => {
3991
4084
  let fakeMembersCollection = {
3992
4085
  members: {
3993
- member1: {isInMeeting: true},
3994
- member2: {isInMeeting: true},
3995
- member3: {isInMeeting: false},
3996
- },
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
+ }
3997
4091
  };
3998
4092
  sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
3999
4093
  const fakeData = {intervalMetadata: {}};
@@ -4011,11 +4105,12 @@ describe('plugin-meetings', () => {
4011
4105
  },
4012
4106
  payload: {
4013
4107
  intervals: [
4014
- sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
4108
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 3)),
4015
4109
  ],
4016
4110
  },
4017
4111
  });
4018
- fakeMembersCollection.members.member2.isInMeeting = false;
4112
+ // Move member2 from lobby to neither in meeting nor lobby
4113
+ fakeMembersCollection.members.member2.isInLobby = false;
4019
4114
 
4020
4115
  statsAnalyzerStub.emit(
4021
4116
  {file: 'test', function: 'test'},
@@ -4030,7 +4125,7 @@ describe('plugin-meetings', () => {
4030
4125
  },
4031
4126
  payload: {
4032
4127
  intervals: [
4033
- sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1)),
4128
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
4034
4129
  ],
4035
4130
  },
4036
4131
  });
@@ -4057,6 +4152,132 @@ describe('plugin-meetings', () => {
4057
4152
  });
4058
4153
  });
4059
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
+
4060
4281
  describe('bundlePolicy', () => {
4061
4282
  const FAKE_TURN_URL = 'turns:webex.com:3478';
4062
4283
  const FAKE_TURN_USER = 'some-turn-username';
@@ -5523,6 +5744,7 @@ describe('plugin-meetings', () => {
5523
5744
  let multistreamEventListeners;
5524
5745
  let transcodedEventListeners;
5525
5746
  let mockStatsAnalyzerCtor;
5747
+ let statsMonitorStub;
5526
5748
 
5527
5749
  const setupFakeRoapMediaConnection = (fakeRoapMediaConnection, eventListeners) => {
5528
5750
  fakeRoapMediaConnection.on.callsFake((eventName, cb) => {
@@ -5554,6 +5776,14 @@ describe('plugin-meetings', () => {
5554
5776
  return {on: sinon.stub(), stopAnalyzer: sinon.stub()};
5555
5777
  });
5556
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
+
5557
5787
  webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
5558
5788
  sinon.stub();
5559
5789
 
@@ -5616,6 +5846,7 @@ describe('plugin-meetings', () => {
5616
5846
  mockStatsAnalyzerCtor,
5617
5847
  sinon.match({
5618
5848
  isMultistream: true,
5849
+ statsMonitor: statsMonitorStub,
5619
5850
  })
5620
5851
  );
5621
5852
  const initialStatsAnalyzer = mockStatsAnalyzerCtor.returnValues[0];
@@ -6498,25 +6729,36 @@ describe('plugin-meetings', () => {
6498
6729
  const DIAL_IN_URL = meeting.dialInUrl;
6499
6730
 
6500
6731
  assert.calledWith(meeting.meetingRequest.dialIn, {
6501
- correlationId: meeting.correlationId,
6732
+ correlationId: meeting.pstnCorrelationId,
6502
6733
  dialInUrl: DIAL_IN_URL,
6503
6734
  locusUrl: meeting.locusUrl,
6504
6735
  clientUrl: meeting.deviceUrl,
6505
6736
  });
6506
6737
  assert.notCalled(meeting.meetingRequest.dialOut);
6507
6738
 
6739
+ // Verify pstnCorrelationId was set
6740
+ assert.exists(meeting.pstnCorrelationId);
6741
+ assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6742
+ const firstPstnCorrelationId = meeting.pstnCorrelationId
6743
+
6508
6744
  meeting.meetingRequest.dialIn.resetHistory();
6509
6745
 
6510
6746
  // try again. the dial in urls should match
6511
6747
  await meeting.usePhoneAudio();
6512
6748
 
6513
6749
  assert.calledWith(meeting.meetingRequest.dialIn, {
6514
- correlationId: meeting.correlationId,
6750
+ correlationId: meeting.pstnCorrelationId,
6515
6751
  dialInUrl: DIAL_IN_URL,
6516
6752
  locusUrl: meeting.locusUrl,
6517
6753
  clientUrl: meeting.deviceUrl,
6518
6754
  });
6519
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
+ );
6520
6762
  });
6521
6763
 
6522
6764
  it('given a phone number, triggers dial-out, delegating request to meetingRequest correctly', async () => {
@@ -6526,7 +6768,7 @@ describe('plugin-meetings', () => {
6526
6768
  const DIAL_OUT_URL = meeting.dialOutUrl;
6527
6769
 
6528
6770
  assert.calledWith(meeting.meetingRequest.dialOut, {
6529
- correlationId: meeting.correlationId,
6771
+ correlationId: meeting.pstnCorrelationId,
6530
6772
  dialOutUrl: DIAL_OUT_URL,
6531
6773
  locusUrl: meeting.locusUrl,
6532
6774
  clientUrl: meeting.deviceUrl,
@@ -6534,49 +6776,126 @@ describe('plugin-meetings', () => {
6534
6776
  });
6535
6777
  assert.notCalled(meeting.meetingRequest.dialIn);
6536
6778
 
6779
+ // Verify pstnCorrelationId was set
6780
+ assert.exists(meeting.pstnCorrelationId);
6781
+ assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6782
+ const firstPstnCorrelationId = meeting.pstnCorrelationId;
6783
+
6537
6784
  meeting.meetingRequest.dialOut.resetHistory();
6538
6785
 
6539
6786
  // try again. the dial out urls should match
6540
6787
  await meeting.usePhoneAudio(phoneNumber);
6541
6788
 
6542
6789
  assert.calledWith(meeting.meetingRequest.dialOut, {
6543
- correlationId: meeting.correlationId,
6790
+ correlationId: meeting.pstnCorrelationId,
6544
6791
  dialOutUrl: DIAL_OUT_URL,
6545
6792
  locusUrl: meeting.locusUrl,
6546
6793
  clientUrl: meeting.deviceUrl,
6547
6794
  phoneNumber,
6548
6795
  });
6549
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
+ );
6550
6803
  });
6551
6804
 
6552
- it('rejects if the request failed (dial in)', () => {
6553
- 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'};
6554
6807
 
6555
6808
  meeting.meetingRequest.dialIn = sinon.stub().returns(Promise.reject(error));
6556
6809
 
6557
- return meeting
6558
- .usePhoneAudio()
6559
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6560
- .catch((e) => {
6561
- 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);
6562
6815
 
6563
- 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,
6564
6825
  });
6826
+
6827
+ // Verify pstnCorrelationId was cleared after error
6828
+ assert.equal(meeting.pstnCorrelationId, undefined);
6829
+ }
6565
6830
  });
6566
6831
 
6567
6832
  it('rejects if the request failed (dial out)', async () => {
6568
- const error = 'something bad happened';
6833
+ const error = {error: {message: 'dial out failed'}, stack: 'error stack'};
6569
6834
 
6570
6835
  meeting.meetingRequest.dialOut = sinon.stub().returns(Promise.reject(error));
6571
6836
 
6572
- return meeting
6573
- .usePhoneAudio('+441234567890')
6574
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6575
- .catch((e) => {
6576
- 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);
6577
6842
 
6578
- 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,
6579
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);
6580
6899
  });
6581
6900
  });
6582
6901
 
@@ -7330,6 +7649,8 @@ describe('plugin-meetings', () => {
7330
7649
  'locus-id',
7331
7650
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
7332
7651
  {meetingId: meeting.id, sendCAevents: true},
7652
+ null,
7653
+ null,
7333
7654
  null
7334
7655
  );
7335
7656
  assert.deepEqual(meeting.meetingInfo, {
@@ -7376,6 +7697,8 @@ describe('plugin-meetings', () => {
7376
7697
  'locus-id',
7377
7698
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
7378
7699
  {meetingId: meeting.id, sendCAevents: true},
7700
+ null,
7701
+ null,
7379
7702
  null
7380
7703
  );
7381
7704
  assert.deepEqual(meeting.meetingInfo, {
@@ -7431,6 +7754,8 @@ describe('plugin-meetings', () => {
7431
7754
  permissionToken: FAKE_PERMISSION_TOKEN,
7432
7755
  },
7433
7756
  {meetingId: meeting.id, sendCAevents: true},
7757
+ null,
7758
+ null,
7434
7759
  null
7435
7760
  );
7436
7761
  assert.deepEqual(meeting.meetingInfo, {
@@ -8093,6 +8418,7 @@ describe('plugin-meetings', () => {
8093
8418
 
8094
8419
  meeting.requestScreenShareFloor = sinon.stub().resolves({});
8095
8420
  meeting.releaseScreenShareFloor = sinon.stub().resolves({});
8421
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
8096
8422
  meeting.mediaProperties.mediaDirection = {
8097
8423
  sendAudio: 'fake value', // using non-boolean here so that we can check that these values are untouched in tests
8098
8424
  sendVideo: 'fake value',
@@ -8174,6 +8500,12 @@ describe('plugin-meetings', () => {
8174
8500
  payload: {mediaType: 'share', shareInstanceId: meeting.localShareInstanceId},
8175
8501
  options: {meetingId: meeting.id},
8176
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
+
8177
8509
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
8178
8510
 
8179
8511
  assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
@@ -8192,6 +8524,11 @@ describe('plugin-meetings', () => {
8192
8524
  options: {meetingId: meeting.id},
8193
8525
  });
8194
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
+
8195
8532
  assert.calledWith(
8196
8533
  meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
8197
8534
  stream
@@ -8860,11 +9197,16 @@ describe('plugin-meetings', () => {
8860
9197
  meeting.hasMediaConnectionConnectedAtLeastOnce = false;
8861
9198
  meeting.setupMediaConnectionListeners();
8862
9199
 
9200
+ sinon.stub(MeetingUtil, 'getCaEventLabelsForIpVersion').returns(['fake labels']);
9201
+
8863
9202
  simulateConnectionStateChange(ConnectionState.Connecting);
8864
9203
 
8865
9204
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
8866
9205
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8867
9206
  name: 'client.ice.start',
9207
+ payload: {
9208
+ labels: ['fake labels'],
9209
+ },
8868
9210
  options: {
8869
9211
  meetingId: meeting.id,
8870
9212
  },
@@ -10129,6 +10471,24 @@ describe('plugin-meetings', () => {
10129
10471
  );
10130
10472
  });
10131
10473
 
10474
+ it('listens to CONTROLS_AUTO_END_MEETING_WARNING_CHANGED', async () => {
10475
+ const state = {example: 'value'};
10476
+
10477
+ await meeting.locusInfo.emitScoped(
10478
+ {function: 'test', file: 'test'},
10479
+ LOCUSINFO.EVENTS.CONTROLS_AUTO_END_MEETING_WARNING_CHANGED,
10480
+ {state}
10481
+ );
10482
+
10483
+ assert.calledWith(
10484
+ TriggerProxy.trigger,
10485
+ meeting,
10486
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
10487
+ EVENT_TRIGGERS.MEETING_CONTROLS_AUTO_END_MEETING_WARNING_UPDATED,
10488
+ {state}
10489
+ );
10490
+ });
10491
+
10132
10492
  it('listens to CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED', async () => {
10133
10493
  const state = {example: 'value'};
10134
10494
 
@@ -10208,6 +10568,7 @@ describe('plugin-meetings', () => {
10208
10568
  describe('#setUpLocusUrlListener', () => {
10209
10569
  it('listens to the locus url update event', (done) => {
10210
10570
  const newLocusUrl = 'newLocusUrl/12345';
10571
+ const payload = {url: newLocusUrl}
10211
10572
 
10212
10573
  meeting.members = {locusUrlUpdate: sinon.stub().returns(Promise.resolve(test1))};
10213
10574
  meeting.recordingController = {setLocusUrl: sinon.stub().returns(undefined)};
@@ -10221,14 +10582,14 @@ describe('plugin-meetings', () => {
10221
10582
  meeting.locusInfo.emit(
10222
10583
  {function: 'test', file: 'test'},
10223
10584
  'LOCUS_INFO_UPDATE_URL',
10224
- newLocusUrl
10585
+ payload
10225
10586
  );
10226
10587
  assert.calledWith(meeting.members.locusUrlUpdate, newLocusUrl);
10227
10588
  assert.calledOnceWithExactly(meeting.breakouts.locusUrlUpdate, newLocusUrl);
10228
10589
  assert.calledOnceWithExactly(meeting.annotation.locusUrlUpdate, newLocusUrl);
10229
10590
  assert.calledWith(meeting.members.locusUrlUpdate, newLocusUrl);
10230
10591
  assert.calledWith(meeting.recordingController.setLocusUrl, newLocusUrl);
10231
- assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl);
10592
+ assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl, false);
10232
10593
  assert.calledWith(meeting.simultaneousInterpretation.locusUrlUpdate, newLocusUrl);
10233
10594
  assert.calledWith(meeting.webinar.locusUrlUpdate, newLocusUrl);
10234
10595
  assert.equal(meeting.locusUrl, newLocusUrl);
@@ -10246,6 +10607,22 @@ describe('plugin-meetings', () => {
10246
10607
  {locusUrl: 'newLocusUrl/12345'}
10247
10608
  );
10248
10609
 
10610
+ done();
10611
+ });
10612
+ it('update mainLocusUrl for controlsOptionManager if payload.isMainLocus as true', (done) => {
10613
+ const newLocusUrl = 'newLocusUrl/12345';
10614
+ const payload = {url: newLocusUrl, isMainLocus: true}
10615
+
10616
+ meeting.controlsOptionsManager = {setLocusUrl: sinon.stub().returns(undefined)};
10617
+
10618
+ meeting.locusInfo.emit(
10619
+ {function: 'test', file: 'test'},
10620
+ 'LOCUS_INFO_UPDATE_URL',
10621
+ payload
10622
+ );
10623
+
10624
+ assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl, true);
10625
+
10249
10626
  done();
10250
10627
  });
10251
10628
  });
@@ -10465,6 +10842,8 @@ describe('plugin-meetings', () => {
10465
10842
  meeting.mediaProperties = {mediaDirection: {sendShare: true}};
10466
10843
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
10467
10844
  (meeting.deviceUrl = 'deviceUrl.com'), (meeting.localShareInstanceId = '1234-5678');
10845
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
10846
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
10468
10847
  });
10469
10848
  it('should call changeMeetingFloor()', async () => {
10470
10849
  meeting.screenShareFloorState = 'GRANTED';
@@ -10482,6 +10861,22 @@ describe('plugin-meetings', () => {
10482
10861
  assert.exists(share.then);
10483
10862
  await share;
10484
10863
  assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
10864
+
10865
+ // ensure the share stop timestamp is saved
10866
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
10867
+ key: 'internal.client.share.stopped',
10868
+ });
10869
+
10870
+ // ensure the CA share stopped metric is submitted with duration
10871
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
10872
+ name: 'client.share.stopped',
10873
+ payload: {
10874
+ mediaType: 'share',
10875
+ shareInstanceId: meeting.localShareInstanceId,
10876
+ shareDuration: 1000,
10877
+ },
10878
+ options: {meetingId: meeting.id},
10879
+ });
10485
10880
  });
10486
10881
  it('should not call changeMeetingFloor() if someone else already has the floor', async () => {
10487
10882
  // change selfId so that it doesn't match the beneficiary id from meeting.locusInfo.mediaShares
@@ -11065,6 +11460,8 @@ describe('plugin-meetings', () => {
11065
11460
  let canUserRenameOthersSpy;
11066
11461
  let canShareWhiteBoardSpy;
11067
11462
  let canMoveToLobbySpy;
11463
+ let isSpokenLanguageAutoDetectionEnabledSpy;
11464
+ let showAutoEndMeetingWarningSpy;
11068
11465
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
11069
11466
 
11070
11467
  beforeEach(() => {
@@ -11096,11 +11493,15 @@ describe('plugin-meetings', () => {
11096
11493
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
11097
11494
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
11098
11495
  canMoveToLobbySpy = sinon.spy(MeetingUtil, 'canMoveToLobby');
11496
+ showAutoEndMeetingWarningSpy = sinon.spy(MeetingUtil, 'showAutoEndMeetingWarning');
11497
+ isSpokenLanguageAutoDetectionEnabledSpy = sinon.spy(MeetingUtil, 'isSpokenLanguageAutoDetectionEnabled');
11498
+
11099
11499
  });
11100
11500
 
11101
11501
  afterEach(() => {
11102
11502
  inMeetingActionsSetSpy.restore();
11103
11503
  waitingForOthersToJoinSpy.restore();
11504
+ showAutoEndMeetingWarningSpy.restore();
11104
11505
  });
11105
11506
 
11106
11507
  forEach(
@@ -11648,6 +12049,8 @@ describe('plugin-meetings', () => {
11648
12049
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11649
12050
  assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11650
12051
  assert.calledWith(canMoveToLobbySpy, userDisplayHints);
12052
+ assert.calledWith(showAutoEndMeetingWarningSpy, userDisplayHints);
12053
+ assert.calledWith(isSpokenLanguageAutoDetectionEnabledSpy, userDisplayHints);
11651
12054
 
11652
12055
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11653
12056
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -12054,6 +12457,7 @@ describe('plugin-meetings', () => {
12054
12457
  meeting.locusInfo.self = {url: url1};
12055
12458
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
12056
12459
  meeting.deviceUrl = 'deviceUrl.com';
12460
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12057
12461
  });
12058
12462
  it('should have #startWhiteboardShare', () => {
12059
12463
  assert.exists(meeting.startWhiteboardShare);
@@ -12081,6 +12485,11 @@ describe('plugin-meetings', () => {
12081
12485
  payload: {mediaType: 'whiteboard'},
12082
12486
  options: {meetingId: meeting.id},
12083
12487
  });
12488
+
12489
+ // ensure the share start timestamp is saved
12490
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
12491
+ key: 'internal.client.share.initiated',
12492
+ });
12084
12493
  });
12085
12494
  });
12086
12495
  describe('#stopWhiteboardShare', () => {
@@ -12092,6 +12501,9 @@ describe('plugin-meetings', () => {
12092
12501
  meeting.locusInfo.self = {url: url1};
12093
12502
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
12094
12503
  meeting.deviceUrl = 'deviceUrl.com';
12504
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12505
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
12506
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
12095
12507
  });
12096
12508
  it('should stop the whiteboard share', async () => {
12097
12509
  const whiteboardShare = meeting.stopWhiteboardShare();
@@ -12106,6 +12518,21 @@ describe('plugin-meetings', () => {
12106
12518
  uri: url1,
12107
12519
  });
12108
12520
  assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
12521
+
12522
+ // ensure the share stop timestamp is saved
12523
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
12524
+ key: 'internal.client.share.stopped',
12525
+ });
12526
+
12527
+ // ensure the CA share stopped metric is submitted with duration
12528
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
12529
+ name: 'client.share.stopped',
12530
+ payload: {
12531
+ mediaType: 'whiteboard',
12532
+ shareDuration: 1000,
12533
+ },
12534
+ options: {meetingId: meeting.id},
12535
+ });
12109
12536
  });
12110
12537
  });
12111
12538
  });
@@ -12178,6 +12605,9 @@ describe('plugin-meetings', () => {
12178
12605
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
12179
12606
  meeting.deviceUrl = 'my-web-url';
12180
12607
  meeting.locusInfo.info = {isWebinar: false};
12608
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12609
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1500);
12610
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
12181
12611
  });
12182
12612
 
12183
12613
  const USER_IDS = {
@@ -12404,12 +12834,12 @@ describe('plugin-meetings', () => {
12404
12834
  activeSharingId.whiteboard = beneficiaryId;
12405
12835
 
12406
12836
  eventTrigger.share.push(
12407
- meeting.webinar.selfIsAttendee
12837
+ meeting.webinar.selfIsAttendee || meeting.guest
12408
12838
  ? {
12409
12839
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12410
12840
  functionName: 'remoteShare',
12411
12841
  eventPayload: {
12412
- memberId: null,
12842
+ memberId: meeting.webinar.selfIsAttendee ? beneficiaryId : null,
12413
12843
  url,
12414
12844
  shareInstanceId,
12415
12845
  annotationInfo: undefined,
@@ -12423,7 +12853,8 @@ describe('plugin-meetings', () => {
12423
12853
  }
12424
12854
  );
12425
12855
 
12426
- shareStatus = meeting.webinar.selfIsAttendee
12856
+ shareStatus =
12857
+ meeting.webinar.selfIsAttendee || meeting.guest
12427
12858
  ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
12428
12859
  : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
12429
12860
  }
@@ -12463,7 +12894,7 @@ describe('plugin-meetings', () => {
12463
12894
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12464
12895
  functionName: 'remoteShare',
12465
12896
  eventPayload: {
12466
- memberId: null,
12897
+ memberId: beneficiaryId,
12467
12898
  url,
12468
12899
  shareInstanceId,
12469
12900
  annotationInfo: undefined,
@@ -12641,6 +13072,36 @@ describe('plugin-meetings', () => {
12641
13072
  });
12642
13073
  });
12643
13074
 
13075
+ describe('Whiteboard Share - User is guest', () => {
13076
+ it('User receives a remote share instead of whiteboard share', () => {
13077
+ // Set the guest flag
13078
+ meeting.guest = true;
13079
+
13080
+ // Step 1: Start sharing whiteboard A
13081
+ const data1 = generateData(
13082
+ blankPayload, // Initial payload
13083
+ true, // isGranting: Granting share
13084
+ false, // isContent: Whiteboard (not content)
13085
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
13086
+ RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
13087
+ );
13088
+
13089
+ // Step 2: Stop sharing whiteboard A
13090
+ const data2 = generateData(
13091
+ data1.payload, // Updated payload from Step 1
13092
+ false, // isGranting: Stopping share
13093
+ false, // isContent: Whiteboard
13094
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
13095
+ );
13096
+
13097
+ // Validate the payload changes and status updates
13098
+ payloadTestHelper([data1]);
13099
+
13100
+ // Specific assertions for guest
13101
+ assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
13102
+ });
13103
+ });
13104
+
12644
13105
  describe('Whiteboard A --> Whiteboard B', () => {
12645
13106
  it('Scenario #1: you share both whiteboards', () => {
12646
13107
  const data1 = generateData(
@@ -13292,7 +13753,54 @@ describe('plugin-meetings', () => {
13292
13753
  payloadTestHelper([data1, data2, data3]);
13293
13754
  });
13294
13755
  });
13295
- });
13756
+
13757
+ it('should send share stopped metric when whiteboard sharing stops', () => {
13758
+ // Start whiteboard sharing (this won't trigger metrics)
13759
+ const data1 = generateData(
13760
+ blankPayload,
13761
+ true, // isGranting: true
13762
+ false, // isContent: false (whiteboard)
13763
+ USER_IDS.ME,
13764
+ RESOURCE_URLS.WHITEBOARD_A
13765
+ );
13766
+
13767
+ // Stop whiteboard sharing (this should trigger metrics)
13768
+ const data2 = generateData(
13769
+ data1.payload,
13770
+ false, // isGranting: false (stopping share)
13771
+ false, // isContent: false (whiteboard)
13772
+ USER_IDS.ME
13773
+ );
13774
+
13775
+ // Trigger the events
13776
+ meeting.locusInfo.emit(
13777
+ {function: 'test', file: 'test'},
13778
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
13779
+ data1.payload
13780
+ );
13781
+
13782
+ meeting.locusInfo.emit(
13783
+ {function: 'test', file: 'test'},
13784
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
13785
+ data2.payload
13786
+ );
13787
+
13788
+ // Verify metrics were called when whiteboard sharing stopped
13789
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
13790
+ key: 'internal.client.share.stopped',
13791
+ });
13792
+
13793
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
13794
+ name: 'client.share.stopped',
13795
+ payload: {
13796
+ mediaType: 'whiteboard',
13797
+ shareDuration: 1500, // mocked return value
13798
+ },
13799
+ options: {
13800
+ meetingId: meeting.id,
13801
+ },
13802
+ });
13803
+ });
13296
13804
 
13297
13805
  describe('handleShareVideoStreamMuteStateChange', () => {
13298
13806
  it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
@@ -13319,6 +13827,7 @@ describe('plugin-meetings', () => {
13319
13827
  });
13320
13828
  });
13321
13829
  });
13830
+ });
13322
13831
 
13323
13832
  describe('#startKeepAlive', () => {
13324
13833
  let clock;
@@ -14524,4 +15033,79 @@ describe('plugin-meetings', () => {
14524
15033
  );
14525
15034
  });
14526
15035
  });
15036
+
15037
+ describe('#notifyHost', () => {
15038
+ beforeEach(() => {
15039
+ meeting.meetingRequest.notifyHost = sinon.stub().returns(Promise.resolve());
15040
+ });
15041
+
15042
+ it('sends the expected request', async () => {
15043
+ meeting.meetingInfo.siteFullUrl = `convergedats.webex.com`;
15044
+ const meetingUuid = 'meeting-uuid';
15045
+ const displayName = ['Test', 'User'];
15046
+ meeting.locusId = 'locusId';
15047
+
15048
+ const notifyHostPromise = meeting.notifyHost(meetingUuid, displayName);
15049
+
15050
+ assert.exists(notifyHostPromise.then);
15051
+ await notifyHostPromise;
15052
+
15053
+ assert.calledOnceWithExactly(
15054
+ meeting.meetingRequest.notifyHost,
15055
+ meeting.meetingInfo.siteFullUrl,
15056
+ meeting.locusId,
15057
+ meetingUuid,
15058
+ displayName,
15059
+ );
15060
+ });
15061
+ });
15062
+
15063
+ describe('#sipCallOut', () => {
15064
+ beforeEach(() => {
15065
+ meeting.meetingRequest.sipCallOut = sinon.stub().returns(Promise.resolve({body: {}}));
15066
+ });
15067
+
15068
+ it('sends the expected request', async () => {
15069
+ const address = 'sip:user@example.com';
15070
+ const displayName = 'John Doe';
15071
+ const meetingId = 'a643beaa47f04eedac08f1310ca12366';
15072
+
15073
+ meeting.meetingInfo = {
15074
+ meetingId,
15075
+ };
15076
+
15077
+ const sipCallOutPromise = meeting.sipCallOut(address, displayName);
15078
+
15079
+ assert.exists(sipCallOutPromise.then);
15080
+ await sipCallOutPromise;
15081
+
15082
+ assert.calledOnceWithExactly(
15083
+ meeting.meetingRequest.sipCallOut,
15084
+ meetingId,
15085
+ meetingId,
15086
+ address,
15087
+ displayName
15088
+ );
15089
+ });
15090
+ });
15091
+
15092
+ describe('#cancelSipCallOut', () => {
15093
+ beforeEach(() => {
15094
+ meeting.meetingRequest.cancelSipCallOut = sinon.stub().returns(Promise.resolve({body: {}}));
15095
+ });
15096
+
15097
+ it('sends the expected request', async () => {
15098
+ const participantId = '12345-abcde';
15099
+
15100
+ const cancelSipCallOutPromise = meeting.cancelSipCallOut(participantId);
15101
+
15102
+ assert.exists(cancelSipCallOutPromise.then);
15103
+ await cancelSipCallOutPromise;
15104
+
15105
+ assert.calledOnceWithExactly(
15106
+ meeting.meetingRequest.cancelSipCallOut,
15107
+ participantId
15108
+ );
15109
+ });
15110
+ });
14527
15111
  });