@webex/plugin-meetings 3.9.0 → 3.10.0-multi-llms.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 (126) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/common/errors/webex-errors.js +21 -1
  4. package/dist/common/errors/webex-errors.js.map +1 -1
  5. package/dist/constants.js +9 -0
  6. package/dist/constants.js.map +1 -1
  7. package/dist/controls-options-manager/index.js +22 -5
  8. package/dist/controls-options-manager/index.js.map +1 -1
  9. package/dist/index.js +9 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/interceptors/index.js +7 -0
  12. package/dist/interceptors/index.js.map +1 -1
  13. package/dist/interceptors/locusRouteToken.js +116 -0
  14. package/dist/interceptors/locusRouteToken.js.map +1 -0
  15. package/dist/interpretation/index.js +1 -1
  16. package/dist/interpretation/siLanguage.js +1 -1
  17. package/dist/locus-info/controlsUtils.js +11 -2
  18. package/dist/locus-info/controlsUtils.js.map +1 -1
  19. package/dist/locus-info/index.js +56 -14
  20. package/dist/locus-info/index.js.map +1 -1
  21. package/dist/locus-info/parser.js +4 -1
  22. package/dist/locus-info/parser.js.map +1 -1
  23. package/dist/media/index.js +5 -0
  24. package/dist/media/index.js.map +1 -1
  25. package/dist/media/properties.js +53 -5
  26. package/dist/media/properties.js.map +1 -1
  27. package/dist/meeting/in-meeting-actions.js +8 -0
  28. package/dist/meeting/in-meeting-actions.js.map +1 -1
  29. package/dist/meeting/index.js +340 -186
  30. package/dist/meeting/index.js.map +1 -1
  31. package/dist/meeting/muteState.js +2 -5
  32. package/dist/meeting/muteState.js.map +1 -1
  33. package/dist/meeting/request.js +177 -14
  34. package/dist/meeting/request.js.map +1 -1
  35. package/dist/meeting/util.js +39 -11
  36. package/dist/meeting/util.js.map +1 -1
  37. package/dist/meeting-info/meeting-info-v2.js +29 -21
  38. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  39. package/dist/meetings/index.js +31 -25
  40. package/dist/meetings/index.js.map +1 -1
  41. package/dist/member/index.js +9 -0
  42. package/dist/member/index.js.map +1 -1
  43. package/dist/member/types.js.map +1 -1
  44. package/dist/member/util.js +10 -0
  45. package/dist/member/util.js.map +1 -1
  46. package/dist/members/collection.js +13 -0
  47. package/dist/members/collection.js.map +1 -1
  48. package/dist/members/index.js +42 -20
  49. package/dist/members/index.js.map +1 -1
  50. package/dist/members/util.js +7 -2
  51. package/dist/members/util.js.map +1 -1
  52. package/dist/metrics/constants.js +2 -1
  53. package/dist/metrics/constants.js.map +1 -1
  54. package/dist/reachability/index.js +3 -3
  55. package/dist/reachability/index.js.map +1 -1
  56. package/dist/types/common/errors/webex-errors.d.ts +12 -0
  57. package/dist/types/constants.d.ts +7 -0
  58. package/dist/types/controls-options-manager/index.d.ts +9 -1
  59. package/dist/types/index.d.ts +2 -1
  60. package/dist/types/interceptors/index.d.ts +2 -1
  61. package/dist/types/interceptors/locusRouteToken.d.ts +38 -0
  62. package/dist/types/locus-info/index.d.ts +56 -2
  63. package/dist/types/media/properties.d.ts +21 -0
  64. package/dist/types/meeting/in-meeting-actions.d.ts +8 -0
  65. package/dist/types/meeting/index.d.ts +41 -1
  66. package/dist/types/meeting/request.d.ts +42 -0
  67. package/dist/types/meeting/util.d.ts +13 -3
  68. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  69. package/dist/types/meetings/index.d.ts +3 -1
  70. package/dist/types/member/index.d.ts +1 -0
  71. package/dist/types/member/types.d.ts +1 -0
  72. package/dist/types/member/util.d.ts +5 -0
  73. package/dist/types/members/collection.d.ts +6 -0
  74. package/dist/types/members/index.d.ts +12 -2
  75. package/dist/types/members/util.d.ts +6 -3
  76. package/dist/types/metrics/constants.d.ts +1 -0
  77. package/dist/webinar/index.js +1 -1
  78. package/package.json +24 -24
  79. package/src/common/errors/webex-errors.ts +19 -0
  80. package/src/constants.ts +10 -0
  81. package/src/controls-options-manager/index.ts +26 -5
  82. package/src/index.ts +4 -1
  83. package/src/interceptors/index.ts +2 -1
  84. package/src/interceptors/locusRouteToken.ts +80 -0
  85. package/src/locus-info/controlsUtils.ts +18 -0
  86. package/src/locus-info/index.ts +99 -17
  87. package/src/locus-info/parser.ts +5 -1
  88. package/src/media/index.ts +6 -0
  89. package/src/media/properties.ts +43 -0
  90. package/src/meeting/in-meeting-actions.ts +16 -0
  91. package/src/meeting/index.ts +207 -25
  92. package/src/meeting/muteState.ts +2 -6
  93. package/src/meeting/request.ts +141 -0
  94. package/src/meeting/util.ts +50 -20
  95. package/src/meeting-info/meeting-info-v2.ts +24 -5
  96. package/src/meetings/index.ts +9 -3
  97. package/src/member/index.ts +10 -0
  98. package/src/member/types.ts +1 -0
  99. package/src/member/util.ts +14 -0
  100. package/src/members/collection.ts +11 -0
  101. package/src/members/index.ts +38 -5
  102. package/src/members/util.ts +18 -2
  103. package/src/metrics/constants.ts +1 -0
  104. package/src/reachability/index.ts +3 -3
  105. package/test/unit/spec/common/browser-detection.js +0 -24
  106. package/test/unit/spec/controls-options-manager/index.js +47 -0
  107. package/test/unit/spec/fixture/locus.js +1 -0
  108. package/test/unit/spec/interceptors/locusRouteToken.ts +87 -0
  109. package/test/unit/spec/locus-info/index.js +91 -15
  110. package/test/unit/spec/locus-info/parser.js +3 -2
  111. package/test/unit/spec/media/index.ts +140 -9
  112. package/test/unit/spec/media/properties.ts +137 -0
  113. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -0
  114. package/test/unit/spec/meeting/index.js +469 -88
  115. package/test/unit/spec/meeting/muteState.js +32 -6
  116. package/test/unit/spec/meeting/request.js +21 -0
  117. package/test/unit/spec/meeting/utils.js +48 -16
  118. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  119. package/test/unit/spec/meetings/index.js +10 -7
  120. package/test/unit/spec/member/util.js +24 -0
  121. package/test/unit/spec/members/collection.js +120 -0
  122. package/test/unit/spec/members/index.js +72 -3
  123. package/test/unit/spec/members/request.js +55 -0
  124. package/test/unit/spec/members/utils.js +116 -14
  125. package/test/unit/spec/reachability/index.ts +158 -3
  126. package/test/unit/spec/roap/turnDiscovery.ts +3 -3
@@ -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,
@@ -96,6 +97,7 @@ import PermissionError from '../../../../src/common/errors/permission';
96
97
  import JoinWebinarError from '../../../../src/common/errors/join-webinar-error';
97
98
  import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
98
99
  import MultistreamNotSupportedError from '../../../../src/common/errors/multistream-not-supported-error';
100
+ import {SdpResponseTimeoutError} from '@webex/plugin-meetings/src/common/errors/webex-errors';
99
101
  import testUtils from '../../../utils/testUtils';
100
102
  import {
101
103
  MeetingInfoV2CaptchaError,
@@ -487,7 +489,7 @@ describe('plugin-meetings', () => {
487
489
 
488
490
  it('pstnCorrelationId getter/setter should work correctly', () => {
489
491
  const testPstnCorrelationId = uuid.v4();
490
-
492
+
491
493
  meeting.pstnCorrelationId = testPstnCorrelationId;
492
494
  assert.equal(meeting.pstnCorrelationId, testPstnCorrelationId);
493
495
  assert.equal(meeting.callStateForMetrics.pstnCorrelationId, testPstnCorrelationId);
@@ -1992,24 +1994,21 @@ describe('plugin-meetings', () => {
1992
1994
  it('should handle join failure', async () => {
1993
1995
  MeetingUtil.isPinOrGuest = sinon.stub().returns(false);
1994
1996
  webex.internal.newMetrics.submitClientEvent = sinon.stub();
1995
-
1997
+
1996
1998
  await meeting.join().catch(() => {
1997
1999
  assert.calledOnce(MeetingUtil.joinMeeting);
1998
-
2000
+
1999
2001
  // Assert that client.locus.join.response error event is not sent from this function, it is now emitted from MeetingUtil.joinMeeting
2000
2002
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
2001
- assert.calledWithMatch(
2002
- webex.internal.newMetrics.submitClientEvent,
2003
- {
2004
- name: 'client.call.initiated',
2005
- payload: {
2006
- trigger: 'user-interaction',
2007
- isRoapCallEnabled: true,
2008
- pstnAudioType: undefined
2009
- },
2010
- options: {meetingId: meeting.id},
2011
- }
2012
- );
2003
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2004
+ name: 'client.call.initiated',
2005
+ payload: {
2006
+ trigger: 'user-interaction',
2007
+ isRoapCallEnabled: true,
2008
+ pstnAudioType: undefined,
2009
+ },
2010
+ options: {meetingId: meeting.id},
2011
+ });
2013
2012
  });
2014
2013
  });
2015
2014
  it('should fail if password is required', async () => {
@@ -2216,6 +2215,7 @@ describe('plugin-meetings', () => {
2216
2215
  });
2217
2216
  meeting.audio = muteStateStub;
2218
2217
  meeting.video = muteStateStub;
2218
+ sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.ipv4_and_ipv6);
2219
2219
  sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
2220
2220
  sinon.stub(meeting, 'setupMediaConnectionListeners');
2221
2221
  sinon.stub(meeting, 'setMercuryListener');
@@ -2287,13 +2287,24 @@ describe('plugin-meetings', () => {
2287
2287
  close: sinon.stub(),
2288
2288
  forceRtcMetricsSend,
2289
2289
  });
2290
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2290
+
2291
+ const mockStatsMonitor = {removeAllListeners: sinon.stub()};
2292
+ const mockNetworkQualityMonitor = {removeAllListeners: sinon.stub()};
2293
+
2294
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2291
2295
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2296
+ meeting.statsMonitor = mockStatsMonitor;
2297
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2292
2298
  const error = await assert.isRejected(meeting.addMedia());
2293
2299
 
2294
2300
  assert.calledOnce(forceRtcMetricsSend);
2301
+ assert.calledOnce(mockStatsMonitor.removeAllListeners);
2302
+ assert.calledOnce(mockNetworkQualityMonitor.removeAllListeners);
2295
2303
 
2296
2304
  assert.isNull(meeting.statsAnalyzer);
2305
+ assert.isNull(meeting.statsMonitor);
2306
+ assert.isNull(meeting.networkQualityMonitor);
2307
+
2297
2308
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
2298
2309
  assert.calledWith(webex.internal.newMetrics.submitInternalEvent.firstCall, {
2299
2310
  name: 'internal.client.add-media.turn-discovery.start',
@@ -2337,6 +2348,7 @@ describe('plugin-meetings', () => {
2337
2348
  selected_subnet: null,
2338
2349
  numTransports: 1,
2339
2350
  iceCandidatesCount: 0,
2351
+ ipver: 1,
2340
2352
  }
2341
2353
  );
2342
2354
  });
@@ -2384,6 +2396,7 @@ describe('plugin-meetings', () => {
2384
2396
  subnet_reachable: null,
2385
2397
  selected_cluster: null,
2386
2398
  selected_subnet: null,
2399
+ ipver: 1,
2387
2400
  })
2388
2401
  );
2389
2402
 
@@ -2403,12 +2416,23 @@ describe('plugin-meetings', () => {
2403
2416
 
2404
2417
  meeting.waitForRemoteSDPAnswer = sinon.stub().rejects();
2405
2418
 
2406
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2419
+ const mockStatsMonitor = {removeAllListeners: sinon.stub()};
2420
+ const mockNetworkQualityMonitor = {removeAllListeners: sinon.stub()};
2421
+
2422
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2407
2423
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2424
+ meeting.statsMonitor = mockStatsMonitor;
2425
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2408
2426
 
2409
2427
  const error = await assert.isRejected(meeting.addMedia());
2410
2428
 
2411
2429
  assert.isNull(meeting.statsAnalyzer);
2430
+ assert.isNull(meeting.statsMonitor);
2431
+ assert.isNull(meeting.networkQualityMonitor);
2432
+
2433
+ assert.calledOnce(mockStatsMonitor.removeAllListeners);
2434
+ assert.calledOnce(mockNetworkQualityMonitor.removeAllListeners);
2435
+
2412
2436
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
2413
2437
  assert.calledWith(webex.internal.newMetrics.submitInternalEvent.firstCall, {
2414
2438
  name: 'internal.client.add-media.turn-discovery.start',
@@ -2452,6 +2476,7 @@ describe('plugin-meetings', () => {
2452
2476
  subnet_reachable: null,
2453
2477
  selected_cluster: null,
2454
2478
  selected_subnet: null,
2479
+ ipver: 1,
2455
2480
  }
2456
2481
  );
2457
2482
  });
@@ -2472,8 +2497,9 @@ describe('plugin-meetings', () => {
2472
2497
  },
2473
2498
  },
2474
2499
  });
2475
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2500
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2476
2501
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2502
+ meeting.statsMonitor = {removeAllListeners: sinon.stub()};
2477
2503
  const error = await assert.isRejected(meeting.addMedia());
2478
2504
 
2479
2505
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
@@ -2512,10 +2538,12 @@ describe('plugin-meetings', () => {
2512
2538
  subnet_reachable: null,
2513
2539
  selected_cluster: null,
2514
2540
  selected_subnet: null,
2541
+ ipver: 1,
2515
2542
  })
2516
2543
  );
2517
2544
 
2518
2545
  assert.isNull(meeting.statsAnalyzer);
2546
+ assert.isNull(meeting.statsMonitor);
2519
2547
  });
2520
2548
 
2521
2549
  it('should include the peer connection properties correctly for transcoded', async () => {
@@ -2532,8 +2560,14 @@ describe('plugin-meetings', () => {
2532
2560
  },
2533
2561
  },
2534
2562
  });
2535
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2563
+
2564
+ const mockStatsMonitor = {removeAllListeners: sinon.stub()};
2565
+ const mockNetworkQualityMonitor = {removeAllListeners: sinon.stub()};
2566
+
2567
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2536
2568
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2569
+ meeting.statsMonitor = mockStatsMonitor;
2570
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2537
2571
  const error = await assert.isRejected(meeting.addMedia());
2538
2572
 
2539
2573
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
@@ -2572,10 +2606,15 @@ describe('plugin-meetings', () => {
2572
2606
  subnet_reachable: null,
2573
2607
  selected_cluster: null,
2574
2608
  selected_subnet: null,
2609
+ ipver: 1,
2575
2610
  })
2576
2611
  );
2577
2612
 
2578
2613
  assert.isNull(meeting.statsAnalyzer);
2614
+ assert.isNull(meeting.statsMonitor);
2615
+ assert.isNull(meeting.networkQualityMonitor);
2616
+ assert.calledOnce(mockStatsMonitor.removeAllListeners);
2617
+ assert.calledOnce(mockNetworkQualityMonitor.removeAllListeners);
2579
2618
  });
2580
2619
 
2581
2620
  it('should work the second time addMedia is called in case the first time fails', async () => {
@@ -2638,7 +2677,11 @@ describe('plugin-meetings', () => {
2638
2677
  // simulate timeout waiting for the SDP answer that never comes
2639
2678
  await clock.tickAsync(ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT);
2640
2679
 
2641
- await assert.isRejected(result);
2680
+ await assert.isRejected(
2681
+ result,
2682
+ SdpResponseTimeoutError,
2683
+ 'Timed out waiting for REMOTE SDP ANSWER'
2684
+ );
2642
2685
 
2643
2686
  assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
2644
2687
  clientErrorCode: 2007,
@@ -3096,6 +3139,7 @@ describe('plugin-meetings', () => {
3096
3139
  subnet_reachable: null,
3097
3140
  selected_cluster: null,
3098
3141
  selected_subnet: null,
3142
+ ipver: 1,
3099
3143
  },
3100
3144
  ]);
3101
3145
 
@@ -3297,6 +3341,7 @@ describe('plugin-meetings', () => {
3297
3341
  connectionType: 'udp',
3298
3342
  selectedCandidatePairChanges: 2,
3299
3343
  ipVersion: 'IPv6',
3344
+ ipver: 1,
3300
3345
  numTransports: 1,
3301
3346
  isMultistream: false,
3302
3347
  retriedWithTurnServer: true,
@@ -3443,6 +3488,7 @@ describe('plugin-meetings', () => {
3443
3488
  meeting.iceCandidatesCount = 3;
3444
3489
  meeting.iceCandidateErrors.set('701_error', 3);
3445
3490
  meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
3491
+ MeetingUtil.getIpVersion.returns(IP_VERSION.only_ipv6);
3446
3492
 
3447
3493
  await meeting.addMedia({
3448
3494
  mediaSettings: {},
@@ -3458,6 +3504,7 @@ describe('plugin-meetings', () => {
3458
3504
  connectionType: 'udp',
3459
3505
  selectedCandidatePairChanges: 2,
3460
3506
  ipVersion: 'IPv6',
3507
+ ipver: 6,
3461
3508
  numTransports: 1,
3462
3509
  isMultistream: false,
3463
3510
  retriedWithTurnServer: false,
@@ -3536,6 +3583,7 @@ describe('plugin-meetings', () => {
3536
3583
  selected_cluster: null,
3537
3584
  selected_subnet: null,
3538
3585
  iceCandidatesCount: 0,
3586
+ ipver: 1,
3539
3587
  }
3540
3588
  );
3541
3589
 
@@ -3600,6 +3648,7 @@ describe('plugin-meetings', () => {
3600
3648
  selected_cluster: null,
3601
3649
  selected_subnet: null,
3602
3650
  iceCandidatesCount: 0,
3651
+ ipver: 1,
3603
3652
  }
3604
3653
  );
3605
3654
 
@@ -3646,6 +3695,7 @@ describe('plugin-meetings', () => {
3646
3695
  locus_id: meeting.locusUrl.split('/').pop(),
3647
3696
  connectionType: 'udp',
3648
3697
  ipVersion: 'IPv6',
3698
+ ipver: 1,
3649
3699
  selectedCandidatePairChanges: 2,
3650
3700
  numTransports: 1,
3651
3701
  isMultistream: false,
@@ -3726,6 +3776,7 @@ describe('plugin-meetings', () => {
3726
3776
  selected_cluster: 'some.cluster',
3727
3777
  selected_subnet: '1.X.X.X',
3728
3778
  iceCandidatesCount: 0,
3779
+ ipver: 1,
3729
3780
  }
3730
3781
  );
3731
3782
 
@@ -4031,12 +4082,13 @@ describe('plugin-meetings', () => {
4031
4082
  });
4032
4083
  });
4033
4084
 
4034
- it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
4085
+ it('counts the number of members that are in the meeting or lobby for MEDIA_QUALITY event', async () => {
4035
4086
  let fakeMembersCollection = {
4036
4087
  members: {
4037
- member1: {isInMeeting: true},
4038
- member2: {isInMeeting: true},
4039
- member3: {isInMeeting: false},
4088
+ member1: {isInMeeting: true, isInLobby: false},
4089
+ member2: {isInMeeting: false, isInLobby: true},
4090
+ member3: {isInMeeting: false, isInLobby: false},
4091
+ member4: {isInMeeting: true, isInLobby: false},
4040
4092
  },
4041
4093
  };
4042
4094
  sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
@@ -4055,11 +4107,12 @@ describe('plugin-meetings', () => {
4055
4107
  },
4056
4108
  payload: {
4057
4109
  intervals: [
4058
- sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
4110
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 3)),
4059
4111
  ],
4060
4112
  },
4061
4113
  });
4062
- fakeMembersCollection.members.member2.isInMeeting = false;
4114
+ // Move member2 from lobby to neither in meeting nor lobby
4115
+ fakeMembersCollection.members.member2.isInLobby = false;
4063
4116
 
4064
4117
  statsAnalyzerStub.emit(
4065
4118
  {file: 'test', function: 'test'},
@@ -4074,7 +4127,7 @@ describe('plugin-meetings', () => {
4074
4127
  },
4075
4128
  payload: {
4076
4129
  intervals: [
4077
- sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1)),
4130
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
4078
4131
  ],
4079
4132
  },
4080
4133
  });
@@ -4101,6 +4154,132 @@ describe('plugin-meetings', () => {
4101
4154
  });
4102
4155
  });
4103
4156
 
4157
+ describe('handles StatsMonitor events', () => {
4158
+ let statsMonitorStub;
4159
+ let prevConfigValue;
4160
+ let listeners;
4161
+
4162
+ beforeEach(async () => {
4163
+ meeting.meetingState = 'ACTIVE';
4164
+ prevConfigValue = meeting.config.stats.enableStatsAnalyzer;
4165
+
4166
+ meeting.config.stats.enableStatsAnalyzer = true;
4167
+
4168
+ listeners = {};
4169
+
4170
+ statsMonitorStub = {
4171
+ on: sinon.stub().callsFake((event, callback) => {
4172
+ listeners[event] = callback;
4173
+ }),
4174
+ removeAllListeners: sinon.stub(),
4175
+ };
4176
+
4177
+ sinon.stub(meeting.mediaProperties, 'sendMediaIssueMetric');
4178
+
4179
+ // mock the StatsMonitor constructor
4180
+ sinon.stub(InternalMediaCoreModule, 'StatsMonitor').returns(statsMonitorStub);
4181
+
4182
+ await meeting.addMedia({
4183
+ mediaSettings: {},
4184
+ });
4185
+ });
4186
+
4187
+ afterEach(() => {
4188
+ meeting.config.stats.enableStatsAnalyzer = prevConfigValue;
4189
+ sinon.restore();
4190
+ });
4191
+
4192
+ describe('INBOUND_AUDIO_ISSUE event', () => {
4193
+ it('should not trigger event when no unmuted members exist', () => {
4194
+ const fakeEventData = {issueSubType: 'DECODE_RESULTS_IN_ZERO_AUDIO_LEVEL'};
4195
+
4196
+ // Setup members that are either self or muted
4197
+ const mutedMember = {
4198
+ isSelf: false,
4199
+ isPairedWithSelf: false,
4200
+ isAudioMuted: true,
4201
+ };
4202
+ const selfMember = {
4203
+ isSelf: true,
4204
+ isPairedWithSelf: false,
4205
+ isAudioMuted: false,
4206
+ };
4207
+ const pairedMember = {
4208
+ isSelf: false,
4209
+ isPairedWithSelf: true,
4210
+ isAudioMuted: false,
4211
+ };
4212
+ meeting.members.membersCollection.getAll = sinon.stub().returns({
4213
+ member1: mutedMember,
4214
+ member2: selfMember,
4215
+ member3: pairedMember,
4216
+ });
4217
+
4218
+ // Reset the stub to clear any previous calls
4219
+ TriggerProxy.trigger.resetHistory();
4220
+
4221
+ // Emit the event from statsMonitor
4222
+ listeners[StatsMonitorEventNames.INBOUND_AUDIO_ISSUE](fakeEventData);
4223
+
4224
+ assert.neverCalledWith(
4225
+ TriggerProxy.trigger,
4226
+ meeting,
4227
+ sinon.match.object,
4228
+ EVENT_TRIGGERS.MEDIA_INBOUND_AUDIO_ISSUE_DETECTED,
4229
+ fakeEventData
4230
+ );
4231
+ assert.notCalled(meeting.mediaProperties.sendMediaIssueMetric);
4232
+ });
4233
+
4234
+ it('should trigger event and metric when there are multiple members and at least one is unmuted', () => {
4235
+ const fakeEventData = {issueSubType: 'DECODE_RESULTS_IN_ZERO_AUDIO_LEVEL'};
4236
+
4237
+ // Setup mixed members - some muted, one unmuted
4238
+ const mutedMember = {
4239
+ isSelf: false,
4240
+ isPairedWithSelf: false,
4241
+ isAudioMuted: true,
4242
+ };
4243
+ const unmutedMember = {
4244
+ isSelf: false,
4245
+ isPairedWithSelf: false,
4246
+ isAudioMuted: false,
4247
+ };
4248
+ const selfMember = {
4249
+ isSelf: true,
4250
+ isPairedWithSelf: false,
4251
+ isAudioMuted: false,
4252
+ };
4253
+ meeting.members.membersCollection.getAll = sinon.stub().returns({
4254
+ member1: mutedMember,
4255
+ member2: unmutedMember,
4256
+ member3: selfMember,
4257
+ });
4258
+
4259
+ // Reset the stub to clear any previous calls
4260
+ TriggerProxy.trigger.resetHistory();
4261
+
4262
+ // Emit the event from statsMonitor
4263
+ listeners[StatsMonitorEventNames.INBOUND_AUDIO_ISSUE](fakeEventData);
4264
+
4265
+ assert.calledWith(
4266
+ TriggerProxy.trigger,
4267
+ meeting,
4268
+ sinon.match.object,
4269
+ EVENT_TRIGGERS.MEDIA_INBOUND_AUDIO_ISSUE_DETECTED,
4270
+ fakeEventData
4271
+ );
4272
+
4273
+ assert.calledOnceWithExactly(
4274
+ meeting.mediaProperties.sendMediaIssueMetric,
4275
+ 'inbound_audio',
4276
+ fakeEventData.issueSubType,
4277
+ meeting.correlationId
4278
+ );
4279
+ });
4280
+ });
4281
+ });
4282
+
4104
4283
  describe('bundlePolicy', () => {
4105
4284
  const FAKE_TURN_URL = 'turns:webex.com:3478';
4106
4285
  const FAKE_TURN_USER = 'some-turn-username';
@@ -5567,6 +5746,7 @@ describe('plugin-meetings', () => {
5567
5746
  let multistreamEventListeners;
5568
5747
  let transcodedEventListeners;
5569
5748
  let mockStatsAnalyzerCtor;
5749
+ let statsMonitorStub;
5570
5750
 
5571
5751
  const setupFakeRoapMediaConnection = (fakeRoapMediaConnection, eventListeners) => {
5572
5752
  fakeRoapMediaConnection.on.callsFake((eventName, cb) => {
@@ -5598,6 +5778,14 @@ describe('plugin-meetings', () => {
5598
5778
  return {on: sinon.stub(), stopAnalyzer: sinon.stub()};
5599
5779
  });
5600
5780
 
5781
+ statsMonitorStub = {
5782
+ on: sinon.stub(),
5783
+ removeAllListeners: sinon.stub(),
5784
+ };
5785
+
5786
+ // mock the StatsMonitor constructor
5787
+ sinon.stub(InternalMediaCoreModule, 'StatsMonitor').returns(statsMonitorStub);
5788
+
5601
5789
  webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
5602
5790
  sinon.stub();
5603
5791
 
@@ -5660,6 +5848,7 @@ describe('plugin-meetings', () => {
5660
5848
  mockStatsAnalyzerCtor,
5661
5849
  sinon.match({
5662
5850
  isMultistream: true,
5851
+ statsMonitor: statsMonitorStub,
5663
5852
  })
5664
5853
  );
5665
5854
  const initialStatsAnalyzer = mockStatsAnalyzerCtor.returnValues[0];
@@ -6548,11 +6737,11 @@ describe('plugin-meetings', () => {
6548
6737
  clientUrl: meeting.deviceUrl,
6549
6738
  });
6550
6739
  assert.notCalled(meeting.meetingRequest.dialOut);
6551
-
6740
+
6552
6741
  // Verify pstnCorrelationId was set
6553
6742
  assert.exists(meeting.pstnCorrelationId);
6554
6743
  assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6555
- const firstPstnCorrelationId = meeting.pstnCorrelationId
6744
+ const firstPstnCorrelationId = meeting.pstnCorrelationId;
6556
6745
 
6557
6746
  meeting.meetingRequest.dialIn.resetHistory();
6558
6747
 
@@ -6625,18 +6814,22 @@ describe('plugin-meetings', () => {
6625
6814
  throw new Error('Promise resolved when it should have rejected');
6626
6815
  } catch (e) {
6627
6816
  assert.equal(e, error);
6628
-
6817
+
6629
6818
  // Verify behavioral metric was sent with dial_in_correlation_id
6630
- assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_IN_FAILURE, {
6631
- correlation_id: meeting.correlationId,
6632
- dial_in_url: meeting.dialInUrl,
6633
- dial_in_correlation_id: sinon.match.string,
6634
- locus_id: meeting.locusUrl.split('/').pop(),
6635
- client_url: meeting.deviceUrl,
6636
- reason: error.error.message,
6637
- stack: error.stack,
6638
- });
6639
-
6819
+ assert.calledWith(
6820
+ Metrics.sendBehavioralMetric,
6821
+ BEHAVIORAL_METRICS.ADD_DIAL_IN_FAILURE,
6822
+ {
6823
+ correlation_id: meeting.correlationId,
6824
+ dial_in_url: meeting.dialInUrl,
6825
+ dial_in_correlation_id: sinon.match.string,
6826
+ locus_id: meeting.locusUrl.split('/').pop(),
6827
+ client_url: meeting.deviceUrl,
6828
+ reason: error.error.message,
6829
+ stack: error.stack,
6830
+ }
6831
+ );
6832
+
6640
6833
  // Verify pstnCorrelationId was cleared after error
6641
6834
  assert.equal(meeting.pstnCorrelationId, undefined);
6642
6835
  }
@@ -6652,18 +6845,22 @@ describe('plugin-meetings', () => {
6652
6845
  throw new Error('Promise resolved when it should have rejected');
6653
6846
  } catch (e) {
6654
6847
  assert.equal(e, error);
6655
-
6848
+
6656
6849
  // Verify behavioral metric was sent with dial_out_correlation_id
6657
- assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_OUT_FAILURE, {
6658
- correlation_id: meeting.correlationId,
6659
- dial_out_url: meeting.dialOutUrl,
6660
- dial_out_correlation_id: sinon.match.string,
6661
- locus_id: meeting.locusUrl.split('/').pop(),
6662
- client_url: meeting.deviceUrl,
6663
- reason: error.error.message,
6664
- stack: error.stack,
6665
- });
6666
-
6850
+ assert.calledWith(
6851
+ Metrics.sendBehavioralMetric,
6852
+ BEHAVIORAL_METRICS.ADD_DIAL_OUT_FAILURE,
6853
+ {
6854
+ correlation_id: meeting.correlationId,
6855
+ dial_out_url: meeting.dialOutUrl,
6856
+ dial_out_correlation_id: sinon.match.string,
6857
+ locus_id: meeting.locusUrl.split('/').pop(),
6858
+ client_url: meeting.deviceUrl,
6859
+ reason: error.error.message,
6860
+ stack: error.stack,
6861
+ }
6862
+ );
6863
+
6667
6864
  // Verify pstnCorrelationId was cleared after error
6668
6865
  assert.equal(meeting.pstnCorrelationId, undefined);
6669
6866
  }
@@ -6686,12 +6883,12 @@ describe('plugin-meetings', () => {
6686
6883
 
6687
6884
  it('should disconnect phone audio and clear pstnCorrelationId', async () => {
6688
6885
  meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6689
-
6886
+
6690
6887
  await meeting.disconnectPhoneAudio();
6691
-
6888
+
6692
6889
  // Verify that pstnCorrelationId is cleared
6693
6890
  assert.equal(meeting.pstnCorrelationId, undefined);
6694
-
6891
+
6695
6892
  // Verify that MeetingUtil.disconnectPhoneAudio was called for both dial-in and dial-out
6696
6893
  assert.calledTwice(MeetingUtil.disconnectPhoneAudio);
6697
6894
  assert.calledWith(MeetingUtil.disconnectPhoneAudio, meeting, meeting.dialInUrl);
@@ -6702,12 +6899,12 @@ describe('plugin-meetings', () => {
6702
6899
  meeting.dialInDeviceStatus = 'IDLE';
6703
6900
  meeting.dialOutDeviceStatus = 'IDLE';
6704
6901
  meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6705
-
6902
+
6706
6903
  await meeting.disconnectPhoneAudio();
6707
-
6904
+
6708
6905
  // Verify that pstnCorrelationId is still cleared even when no phone connection is active
6709
6906
  assert.equal(meeting.pstnCorrelationId, undefined);
6710
- // And verify no disconnect was attempted
6907
+ // And verify no disconnect was attempted
6711
6908
  assert.notCalled(MeetingUtil.disconnectPhoneAudio);
6712
6909
  });
6713
6910
  });
@@ -7462,6 +7659,8 @@ describe('plugin-meetings', () => {
7462
7659
  'locus-id',
7463
7660
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
7464
7661
  {meetingId: meeting.id, sendCAevents: true},
7662
+ null,
7663
+ null,
7465
7664
  null
7466
7665
  );
7467
7666
  assert.deepEqual(meeting.meetingInfo, {
@@ -7508,6 +7707,8 @@ describe('plugin-meetings', () => {
7508
7707
  'locus-id',
7509
7708
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
7510
7709
  {meetingId: meeting.id, sendCAevents: true},
7710
+ null,
7711
+ null,
7511
7712
  null
7512
7713
  );
7513
7714
  assert.deepEqual(meeting.meetingInfo, {
@@ -7563,6 +7764,8 @@ describe('plugin-meetings', () => {
7563
7764
  permissionToken: FAKE_PERMISSION_TOKEN,
7564
7765
  },
7565
7766
  {meetingId: meeting.id, sendCAevents: true},
7767
+ null,
7768
+ null,
7566
7769
  null
7567
7770
  );
7568
7771
  assert.deepEqual(meeting.meetingInfo, {
@@ -9004,11 +9207,16 @@ describe('plugin-meetings', () => {
9004
9207
  meeting.hasMediaConnectionConnectedAtLeastOnce = false;
9005
9208
  meeting.setupMediaConnectionListeners();
9006
9209
 
9210
+ sinon.stub(MeetingUtil, 'getCaEventLabelsForIpVersion').returns(['fake labels']);
9211
+
9007
9212
  simulateConnectionStateChange(ConnectionState.Connecting);
9008
9213
 
9009
9214
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
9010
9215
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
9011
9216
  name: 'client.ice.start',
9217
+ payload: {
9218
+ labels: ['fake labels'],
9219
+ },
9012
9220
  options: {
9013
9221
  meetingId: meeting.id,
9014
9222
  },
@@ -10273,6 +10481,24 @@ describe('plugin-meetings', () => {
10273
10481
  );
10274
10482
  });
10275
10483
 
10484
+ it('listens to CONTROLS_AUTO_END_MEETING_WARNING_CHANGED', async () => {
10485
+ const state = {example: 'value'};
10486
+
10487
+ await meeting.locusInfo.emitScoped(
10488
+ {function: 'test', file: 'test'},
10489
+ LOCUSINFO.EVENTS.CONTROLS_AUTO_END_MEETING_WARNING_CHANGED,
10490
+ {state}
10491
+ );
10492
+
10493
+ assert.calledWith(
10494
+ TriggerProxy.trigger,
10495
+ meeting,
10496
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
10497
+ EVENT_TRIGGERS.MEETING_CONTROLS_AUTO_END_MEETING_WARNING_UPDATED,
10498
+ {state}
10499
+ );
10500
+ });
10501
+
10276
10502
  it('listens to CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED', async () => {
10277
10503
  const state = {example: 'value'};
10278
10504
 
@@ -10352,6 +10578,7 @@ describe('plugin-meetings', () => {
10352
10578
  describe('#setUpLocusUrlListener', () => {
10353
10579
  it('listens to the locus url update event', (done) => {
10354
10580
  const newLocusUrl = 'newLocusUrl/12345';
10581
+ const payload = {url: newLocusUrl};
10355
10582
 
10356
10583
  meeting.members = {locusUrlUpdate: sinon.stub().returns(Promise.resolve(test1))};
10357
10584
  meeting.recordingController = {setLocusUrl: sinon.stub().returns(undefined)};
@@ -10365,14 +10592,14 @@ describe('plugin-meetings', () => {
10365
10592
  meeting.locusInfo.emit(
10366
10593
  {function: 'test', file: 'test'},
10367
10594
  'LOCUS_INFO_UPDATE_URL',
10368
- newLocusUrl
10595
+ payload
10369
10596
  );
10370
10597
  assert.calledWith(meeting.members.locusUrlUpdate, newLocusUrl);
10371
10598
  assert.calledOnceWithExactly(meeting.breakouts.locusUrlUpdate, newLocusUrl);
10372
10599
  assert.calledOnceWithExactly(meeting.annotation.locusUrlUpdate, newLocusUrl);
10373
10600
  assert.calledWith(meeting.members.locusUrlUpdate, newLocusUrl);
10374
10601
  assert.calledWith(meeting.recordingController.setLocusUrl, newLocusUrl);
10375
- assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl);
10602
+ assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl, false);
10376
10603
  assert.calledWith(meeting.simultaneousInterpretation.locusUrlUpdate, newLocusUrl);
10377
10604
  assert.calledWith(meeting.webinar.locusUrlUpdate, newLocusUrl);
10378
10605
  assert.equal(meeting.locusUrl, newLocusUrl);
@@ -10390,6 +10617,22 @@ describe('plugin-meetings', () => {
10390
10617
  {locusUrl: 'newLocusUrl/12345'}
10391
10618
  );
10392
10619
 
10620
+ done();
10621
+ });
10622
+ it('update mainLocusUrl for controlsOptionManager if payload.isMainLocus as true', (done) => {
10623
+ const newLocusUrl = 'newLocusUrl/12345';
10624
+ const payload = {url: newLocusUrl, isMainLocus: true};
10625
+
10626
+ meeting.controlsOptionsManager = {setLocusUrl: sinon.stub().returns(undefined)};
10627
+
10628
+ meeting.locusInfo.emit(
10629
+ {function: 'test', file: 'test'},
10630
+ 'LOCUS_INFO_UPDATE_URL',
10631
+ payload
10632
+ );
10633
+
10634
+ assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl, true);
10635
+
10393
10636
  done();
10394
10637
  });
10395
10638
  });
@@ -10610,7 +10853,9 @@ describe('plugin-meetings', () => {
10610
10853
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
10611
10854
  (meeting.deviceUrl = 'deviceUrl.com'), (meeting.localShareInstanceId = '1234-5678');
10612
10855
  webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
10613
- webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
10856
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon
10857
+ .stub()
10858
+ .returns(1000);
10614
10859
  });
10615
10860
  it('should call changeMeetingFloor()', async () => {
10616
10861
  meeting.screenShareFloorState = 'GRANTED';
@@ -11227,6 +11472,8 @@ describe('plugin-meetings', () => {
11227
11472
  let canUserRenameOthersSpy;
11228
11473
  let canShareWhiteBoardSpy;
11229
11474
  let canMoveToLobbySpy;
11475
+ let isSpokenLanguageAutoDetectionEnabledSpy;
11476
+ let showAutoEndMeetingWarningSpy;
11230
11477
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
11231
11478
 
11232
11479
  beforeEach(() => {
@@ -11258,11 +11505,17 @@ describe('plugin-meetings', () => {
11258
11505
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
11259
11506
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
11260
11507
  canMoveToLobbySpy = sinon.spy(MeetingUtil, 'canMoveToLobby');
11508
+ showAutoEndMeetingWarningSpy = sinon.spy(MeetingUtil, 'showAutoEndMeetingWarning');
11509
+ isSpokenLanguageAutoDetectionEnabledSpy = sinon.spy(
11510
+ MeetingUtil,
11511
+ 'isSpokenLanguageAutoDetectionEnabled'
11512
+ );
11261
11513
  });
11262
11514
 
11263
11515
  afterEach(() => {
11264
11516
  inMeetingActionsSetSpy.restore();
11265
11517
  waitingForOthersToJoinSpy.restore();
11518
+ showAutoEndMeetingWarningSpy.restore();
11266
11519
  });
11267
11520
 
11268
11521
  forEach(
@@ -11810,6 +12063,8 @@ describe('plugin-meetings', () => {
11810
12063
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11811
12064
  assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11812
12065
  assert.calledWith(canMoveToLobbySpy, userDisplayHints);
12066
+ assert.calledWith(showAutoEndMeetingWarningSpy, userDisplayHints);
12067
+ assert.calledWith(isSpokenLanguageAutoDetectionEnabledSpy, userDisplayHints);
11813
12068
 
11814
12069
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11815
12070
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -12261,7 +12516,10 @@ describe('plugin-meetings', () => {
12261
12516
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
12262
12517
  meeting.deviceUrl = 'deviceUrl.com';
12263
12518
  webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12264
- webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
12519
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon
12520
+ .stub()
12521
+ .returns(1000);
12522
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
12265
12523
  });
12266
12524
  it('should stop the whiteboard share', async () => {
12267
12525
  const whiteboardShare = meeting.stopWhiteboardShare();
@@ -12363,6 +12621,11 @@ describe('plugin-meetings', () => {
12363
12621
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
12364
12622
  meeting.deviceUrl = 'my-web-url';
12365
12623
  meeting.locusInfo.info = {isWebinar: false};
12624
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12625
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon
12626
+ .stub()
12627
+ .returns(1500);
12628
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
12366
12629
  });
12367
12630
 
12368
12631
  const USER_IDS = {
@@ -12594,7 +12857,7 @@ describe('plugin-meetings', () => {
12594
12857
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12595
12858
  functionName: 'remoteShare',
12596
12859
  eventPayload: {
12597
- memberId: null,
12860
+ memberId: meeting.webinar.selfIsAttendee ? beneficiaryId : null,
12598
12861
  url,
12599
12862
  shareInstanceId,
12600
12863
  annotationInfo: undefined,
@@ -12610,8 +12873,8 @@ describe('plugin-meetings', () => {
12610
12873
 
12611
12874
  shareStatus =
12612
12875
  meeting.webinar.selfIsAttendee || meeting.guest
12613
- ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
12614
- : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
12876
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
12877
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
12615
12878
  }
12616
12879
 
12617
12880
  if (eventTrigger.member) {
@@ -12649,7 +12912,7 @@ describe('plugin-meetings', () => {
12649
12912
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12650
12913
  functionName: 'remoteShare',
12651
12914
  eventPayload: {
12652
- memberId: null,
12915
+ memberId: beneficiaryId,
12653
12916
  url,
12654
12917
  shareInstanceId,
12655
12918
  annotationInfo: undefined,
@@ -13508,30 +13771,78 @@ describe('plugin-meetings', () => {
13508
13771
  payloadTestHelper([data1, data2, data3]);
13509
13772
  });
13510
13773
  });
13511
- });
13512
13774
 
13513
- describe('handleShareVideoStreamMuteStateChange', () => {
13514
- it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
13515
- meeting.isMultistream = true;
13516
- meeting.statsAnalyzer = {shareVideoEncoderImplementation: 'OpenH264'};
13517
- meeting.mediaProperties.shareVideoStream = {
13518
- getSettings: sinon.stub().returns({displaySurface: 'monitor', frameRate: 30}),
13519
- };
13775
+ it('should send share stopped metric when whiteboard sharing stops', () => {
13776
+ // Start whiteboard sharing (this won't trigger metrics)
13777
+ const data1 = generateData(
13778
+ blankPayload,
13779
+ true, // isGranting: true
13780
+ false, // isContent: false (whiteboard)
13781
+ USER_IDS.ME,
13782
+ RESOURCE_URLS.WHITEBOARD_A
13783
+ );
13784
+
13785
+ // Stop whiteboard sharing (this should trigger metrics)
13786
+ const data2 = generateData(
13787
+ data1.payload,
13788
+ false, // isGranting: false (stopping share)
13789
+ false, // isContent: false (whiteboard)
13790
+ USER_IDS.ME
13791
+ );
13520
13792
 
13521
- meeting.handleShareVideoStreamMuteStateChange(true);
13793
+ // Trigger the events
13794
+ meeting.locusInfo.emit(
13795
+ {function: 'test', file: 'test'},
13796
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
13797
+ data1.payload
13798
+ );
13522
13799
 
13523
- assert.calledOnceWithExactly(
13524
- Metrics.sendBehavioralMetric,
13525
- BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE,
13526
- {
13527
- correlationId: meeting.correlationId,
13528
- muted: true,
13529
- encoderImplementation: 'OpenH264',
13530
- displaySurface: 'monitor',
13531
- isMultistream: true,
13532
- frameRate: 30,
13533
- }
13800
+ meeting.locusInfo.emit(
13801
+ {function: 'test', file: 'test'},
13802
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
13803
+ data2.payload
13534
13804
  );
13805
+
13806
+ // Verify metrics were called when whiteboard sharing stopped
13807
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
13808
+ key: 'internal.client.share.stopped',
13809
+ });
13810
+
13811
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
13812
+ name: 'client.share.stopped',
13813
+ payload: {
13814
+ mediaType: 'whiteboard',
13815
+ shareDuration: 1500, // mocked return value
13816
+ },
13817
+ options: {
13818
+ meetingId: meeting.id,
13819
+ },
13820
+ });
13821
+ });
13822
+
13823
+ describe('handleShareVideoStreamMuteStateChange', () => {
13824
+ it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
13825
+ meeting.isMultistream = true;
13826
+ meeting.statsAnalyzer = {shareVideoEncoderImplementation: 'OpenH264'};
13827
+ meeting.mediaProperties.shareVideoStream = {
13828
+ getSettings: sinon.stub().returns({displaySurface: 'monitor', frameRate: 30}),
13829
+ };
13830
+
13831
+ meeting.handleShareVideoStreamMuteStateChange(true);
13832
+
13833
+ assert.calledOnceWithExactly(
13834
+ Metrics.sendBehavioralMetric,
13835
+ BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE,
13836
+ {
13837
+ correlationId: meeting.correlationId,
13838
+ muted: true,
13839
+ encoderImplementation: 'OpenH264',
13840
+ displaySurface: 'monitor',
13841
+ isMultistream: true,
13842
+ frameRate: 30,
13843
+ }
13844
+ );
13845
+ });
13535
13846
  });
13536
13847
  });
13537
13848
  });
@@ -14733,11 +15044,81 @@ describe('plugin-meetings', () => {
14733
15044
  assert.exists(unsetStagePromise.then);
14734
15045
  await unsetStagePromise;
14735
15046
 
15047
+ assert.calledOnceWithExactly(meeting.meetingRequest.synchronizeStage, locusUrl, {
15048
+ overrideDefault: false,
15049
+ });
15050
+ });
15051
+ });
15052
+
15053
+ describe('#notifyHost', () => {
15054
+ beforeEach(() => {
15055
+ meeting.meetingRequest.notifyHost = sinon.stub().returns(Promise.resolve());
15056
+ });
15057
+
15058
+ it('sends the expected request', async () => {
15059
+ meeting.meetingInfo.siteFullUrl = `convergedats.webex.com`;
15060
+ const meetingUuid = 'meeting-uuid';
15061
+ const displayName = ['Test', 'User'];
15062
+ meeting.locusId = 'locusId';
15063
+
15064
+ const notifyHostPromise = meeting.notifyHost(meetingUuid, displayName);
15065
+
15066
+ assert.exists(notifyHostPromise.then);
15067
+ await notifyHostPromise;
15068
+
14736
15069
  assert.calledOnceWithExactly(
14737
- meeting.meetingRequest.synchronizeStage,
14738
- locusUrl,
14739
- {overrideDefault: false}
15070
+ meeting.meetingRequest.notifyHost,
15071
+ meeting.meetingInfo.siteFullUrl,
15072
+ meeting.locusId,
15073
+ meetingUuid,
15074
+ displayName
15075
+ );
15076
+ });
15077
+ });
15078
+
15079
+ describe('#sipCallOut', () => {
15080
+ beforeEach(() => {
15081
+ meeting.meetingRequest.sipCallOut = sinon.stub().returns(Promise.resolve({body: {}}));
15082
+ });
15083
+
15084
+ it('sends the expected request', async () => {
15085
+ const address = 'sip:user@example.com';
15086
+ const displayName = 'John Doe';
15087
+ const meetingId = 'a643beaa47f04eedac08f1310ca12366';
15088
+
15089
+ meeting.meetingInfo = {
15090
+ meetingId,
15091
+ };
15092
+
15093
+ const sipCallOutPromise = meeting.sipCallOut(address, displayName);
15094
+
15095
+ assert.exists(sipCallOutPromise.then);
15096
+ await sipCallOutPromise;
15097
+
15098
+ assert.calledOnceWithExactly(
15099
+ meeting.meetingRequest.sipCallOut,
15100
+ meetingId,
15101
+ meetingId,
15102
+ address,
15103
+ displayName
14740
15104
  );
14741
15105
  });
14742
15106
  });
15107
+
15108
+ describe('#cancelSipCallOut', () => {
15109
+ beforeEach(() => {
15110
+ meeting.meetingRequest.cancelSipCallOut = sinon.stub().returns(Promise.resolve({body: {}}));
15111
+ });
15112
+
15113
+ it('sends the expected request', async () => {
15114
+ const participantId = '12345-abcde';
15115
+
15116
+ const cancelSipCallOutPromise = meeting.cancelSipCallOut(participantId);
15117
+
15118
+ assert.exists(cancelSipCallOutPromise.then);
15119
+ await cancelSipCallOutPromise;
15120
+
15121
+ assert.calledOnceWithExactly(meeting.meetingRequest.cancelSipCallOut, participantId);
15122
+ });
15123
+ });
14743
15124
  });