@webex/plugin-meetings 3.9.0 → 3.10.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +9 -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 +56 -14
  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/index.js +5 -0
  22. package/dist/media/index.js.map +1 -1
  23. package/dist/media/properties.js +53 -5
  24. package/dist/media/properties.js.map +1 -1
  25. package/dist/meeting/in-meeting-actions.js +8 -0
  26. package/dist/meeting/in-meeting-actions.js.map +1 -1
  27. package/dist/meeting/index.js +339 -185
  28. package/dist/meeting/index.js.map +1 -1
  29. package/dist/meeting/muteState.js +2 -5
  30. package/dist/meeting/muteState.js.map +1 -1
  31. package/dist/meeting/request.js +177 -14
  32. package/dist/meeting/request.js.map +1 -1
  33. package/dist/meeting/util.js +39 -11
  34. package/dist/meeting/util.js.map +1 -1
  35. package/dist/meeting-info/meeting-info-v2.js +29 -21
  36. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  37. package/dist/meetings/index.js +31 -25
  38. package/dist/meetings/index.js.map +1 -1
  39. package/dist/member/index.js +9 -0
  40. package/dist/member/index.js.map +1 -1
  41. package/dist/member/types.js.map +1 -1
  42. package/dist/member/util.js +10 -0
  43. package/dist/member/util.js.map +1 -1
  44. package/dist/members/collection.js +13 -0
  45. package/dist/members/collection.js.map +1 -1
  46. package/dist/members/index.js +42 -20
  47. package/dist/members/index.js.map +1 -1
  48. package/dist/members/util.js +7 -2
  49. package/dist/members/util.js.map +1 -1
  50. package/dist/metrics/constants.js +2 -1
  51. package/dist/metrics/constants.js.map +1 -1
  52. package/dist/reachability/index.js +3 -3
  53. package/dist/reachability/index.js.map +1 -1
  54. package/dist/types/constants.d.ts +7 -0
  55. package/dist/types/controls-options-manager/index.d.ts +9 -1
  56. package/dist/types/interceptors/index.d.ts +2 -1
  57. package/dist/types/interceptors/locusRouteToken.d.ts +38 -0
  58. package/dist/types/locus-info/index.d.ts +56 -2
  59. package/dist/types/media/properties.d.ts +21 -0
  60. package/dist/types/meeting/in-meeting-actions.d.ts +8 -0
  61. package/dist/types/meeting/index.d.ts +41 -1
  62. package/dist/types/meeting/request.d.ts +42 -0
  63. package/dist/types/meeting/util.d.ts +13 -3
  64. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  65. package/dist/types/meetings/index.d.ts +3 -1
  66. package/dist/types/member/index.d.ts +1 -0
  67. package/dist/types/member/types.d.ts +1 -0
  68. package/dist/types/member/util.d.ts +5 -0
  69. package/dist/types/members/collection.d.ts +6 -0
  70. package/dist/types/members/index.d.ts +12 -2
  71. package/dist/types/members/util.d.ts +6 -3
  72. package/dist/types/metrics/constants.d.ts +1 -0
  73. package/dist/webinar/index.js +1 -1
  74. package/package.json +24 -24
  75. package/src/constants.ts +10 -0
  76. package/src/controls-options-manager/index.ts +26 -5
  77. package/src/index.ts +2 -1
  78. package/src/interceptors/index.ts +2 -1
  79. package/src/interceptors/locusRouteToken.ts +80 -0
  80. package/src/locus-info/controlsUtils.ts +18 -0
  81. package/src/locus-info/index.ts +99 -17
  82. package/src/locus-info/parser.ts +5 -1
  83. package/src/media/index.ts +6 -0
  84. package/src/media/properties.ts +43 -0
  85. package/src/meeting/in-meeting-actions.ts +16 -0
  86. package/src/meeting/index.ts +205 -24
  87. package/src/meeting/muteState.ts +2 -6
  88. package/src/meeting/request.ts +141 -0
  89. package/src/meeting/util.ts +50 -20
  90. package/src/meeting-info/meeting-info-v2.ts +24 -5
  91. package/src/meetings/index.ts +9 -3
  92. package/src/member/index.ts +10 -0
  93. package/src/member/types.ts +1 -0
  94. package/src/member/util.ts +14 -0
  95. package/src/members/collection.ts +11 -0
  96. package/src/members/index.ts +38 -5
  97. package/src/members/util.ts +18 -2
  98. package/src/metrics/constants.ts +1 -0
  99. package/src/reachability/index.ts +3 -3
  100. package/test/unit/spec/common/browser-detection.js +0 -24
  101. package/test/unit/spec/controls-options-manager/index.js +47 -0
  102. package/test/unit/spec/fixture/locus.js +1 -0
  103. package/test/unit/spec/interceptors/locusRouteToken.ts +87 -0
  104. package/test/unit/spec/locus-info/index.js +91 -15
  105. package/test/unit/spec/locus-info/parser.js +3 -2
  106. package/test/unit/spec/media/index.ts +140 -9
  107. package/test/unit/spec/media/properties.ts +137 -0
  108. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -0
  109. package/test/unit/spec/meeting/index.js +398 -30
  110. package/test/unit/spec/meeting/muteState.js +32 -6
  111. package/test/unit/spec/meeting/request.js +21 -0
  112. package/test/unit/spec/meeting/utils.js +48 -16
  113. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  114. package/test/unit/spec/meetings/index.js +10 -7
  115. package/test/unit/spec/member/util.js +24 -0
  116. package/test/unit/spec/members/collection.js +120 -0
  117. package/test/unit/spec/members/index.js +72 -3
  118. package/test/unit/spec/members/request.js +55 -0
  119. package/test/unit/spec/members/utils.js +116 -14
  120. package/test/unit/spec/reachability/index.ts +158 -3
  121. 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,
@@ -487,7 +488,7 @@ describe('plugin-meetings', () => {
487
488
 
488
489
  it('pstnCorrelationId getter/setter should work correctly', () => {
489
490
  const testPstnCorrelationId = uuid.v4();
490
-
491
+
491
492
  meeting.pstnCorrelationId = testPstnCorrelationId;
492
493
  assert.equal(meeting.pstnCorrelationId, testPstnCorrelationId);
493
494
  assert.equal(meeting.callStateForMetrics.pstnCorrelationId, testPstnCorrelationId);
@@ -1992,10 +1993,10 @@ describe('plugin-meetings', () => {
1992
1993
  it('should handle join failure', async () => {
1993
1994
  MeetingUtil.isPinOrGuest = sinon.stub().returns(false);
1994
1995
  webex.internal.newMetrics.submitClientEvent = sinon.stub();
1995
-
1996
+
1996
1997
  await meeting.join().catch(() => {
1997
1998
  assert.calledOnce(MeetingUtil.joinMeeting);
1998
-
1999
+
1999
2000
  // Assert that client.locus.join.response error event is not sent from this function, it is now emitted from MeetingUtil.joinMeeting
2000
2001
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
2001
2002
  assert.calledWithMatch(
@@ -2216,6 +2217,7 @@ describe('plugin-meetings', () => {
2216
2217
  });
2217
2218
  meeting.audio = muteStateStub;
2218
2219
  meeting.video = muteStateStub;
2220
+ sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.ipv4_and_ipv6);
2219
2221
  sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
2220
2222
  sinon.stub(meeting, 'setupMediaConnectionListeners');
2221
2223
  sinon.stub(meeting, 'setMercuryListener');
@@ -2287,13 +2289,24 @@ describe('plugin-meetings', () => {
2287
2289
  close: sinon.stub(),
2288
2290
  forceRtcMetricsSend,
2289
2291
  });
2290
- // 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
2291
2297
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2298
+ meeting.statsMonitor = mockStatsMonitor;
2299
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2292
2300
  const error = await assert.isRejected(meeting.addMedia());
2293
2301
 
2294
2302
  assert.calledOnce(forceRtcMetricsSend);
2303
+ assert.calledOnce(mockStatsMonitor.removeAllListeners);
2304
+ assert.calledOnce(mockNetworkQualityMonitor.removeAllListeners);
2295
2305
 
2296
2306
  assert.isNull(meeting.statsAnalyzer);
2307
+ assert.isNull(meeting.statsMonitor);
2308
+ assert.isNull(meeting.networkQualityMonitor);
2309
+
2297
2310
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
2298
2311
  assert.calledWith(webex.internal.newMetrics.submitInternalEvent.firstCall, {
2299
2312
  name: 'internal.client.add-media.turn-discovery.start',
@@ -2337,6 +2350,7 @@ describe('plugin-meetings', () => {
2337
2350
  selected_subnet: null,
2338
2351
  numTransports: 1,
2339
2352
  iceCandidatesCount: 0,
2353
+ ipver: 1,
2340
2354
  }
2341
2355
  );
2342
2356
  });
@@ -2384,6 +2398,7 @@ describe('plugin-meetings', () => {
2384
2398
  subnet_reachable: null,
2385
2399
  selected_cluster: null,
2386
2400
  selected_subnet: null,
2401
+ ipver: 1,
2387
2402
  })
2388
2403
  );
2389
2404
 
@@ -2403,12 +2418,23 @@ describe('plugin-meetings', () => {
2403
2418
 
2404
2419
  meeting.waitForRemoteSDPAnswer = sinon.stub().rejects();
2405
2420
 
2406
- // 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
2407
2425
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2426
+ meeting.statsMonitor = mockStatsMonitor;
2427
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2408
2428
 
2409
2429
  const error = await assert.isRejected(meeting.addMedia());
2410
2430
 
2411
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
+
2412
2438
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
2413
2439
  assert.calledWith(webex.internal.newMetrics.submitInternalEvent.firstCall, {
2414
2440
  name: 'internal.client.add-media.turn-discovery.start',
@@ -2452,6 +2478,7 @@ describe('plugin-meetings', () => {
2452
2478
  subnet_reachable: null,
2453
2479
  selected_cluster: null,
2454
2480
  selected_subnet: null,
2481
+ ipver: 1,
2455
2482
  }
2456
2483
  );
2457
2484
  });
@@ -2472,8 +2499,9 @@ describe('plugin-meetings', () => {
2472
2499
  },
2473
2500
  },
2474
2501
  });
2475
- // 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
2476
2503
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2504
+ meeting.statsMonitor = {removeAllListeners: sinon.stub()};
2477
2505
  const error = await assert.isRejected(meeting.addMedia());
2478
2506
 
2479
2507
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
@@ -2512,10 +2540,12 @@ describe('plugin-meetings', () => {
2512
2540
  subnet_reachable: null,
2513
2541
  selected_cluster: null,
2514
2542
  selected_subnet: null,
2543
+ ipver: 1,
2515
2544
  })
2516
2545
  );
2517
2546
 
2518
2547
  assert.isNull(meeting.statsAnalyzer);
2548
+ assert.isNull(meeting.statsMonitor);
2519
2549
  });
2520
2550
 
2521
2551
  it('should include the peer connection properties correctly for transcoded', async () => {
@@ -2532,8 +2562,14 @@ describe('plugin-meetings', () => {
2532
2562
  },
2533
2563
  },
2534
2564
  });
2535
- // 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
2536
2570
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2571
+ meeting.statsMonitor = mockStatsMonitor;
2572
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2537
2573
  const error = await assert.isRejected(meeting.addMedia());
2538
2574
 
2539
2575
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
@@ -2572,10 +2608,15 @@ describe('plugin-meetings', () => {
2572
2608
  subnet_reachable: null,
2573
2609
  selected_cluster: null,
2574
2610
  selected_subnet: null,
2611
+ ipver: 1,
2575
2612
  })
2576
2613
  );
2577
2614
 
2578
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);
2579
2620
  });
2580
2621
 
2581
2622
  it('should work the second time addMedia is called in case the first time fails', async () => {
@@ -3096,6 +3137,7 @@ describe('plugin-meetings', () => {
3096
3137
  subnet_reachable: null,
3097
3138
  selected_cluster: null,
3098
3139
  selected_subnet: null,
3140
+ ipver: 1,
3099
3141
  },
3100
3142
  ]);
3101
3143
 
@@ -3297,6 +3339,7 @@ describe('plugin-meetings', () => {
3297
3339
  connectionType: 'udp',
3298
3340
  selectedCandidatePairChanges: 2,
3299
3341
  ipVersion: 'IPv6',
3342
+ ipver: 1,
3300
3343
  numTransports: 1,
3301
3344
  isMultistream: false,
3302
3345
  retriedWithTurnServer: true,
@@ -3443,6 +3486,7 @@ describe('plugin-meetings', () => {
3443
3486
  meeting.iceCandidatesCount = 3;
3444
3487
  meeting.iceCandidateErrors.set('701_error', 3);
3445
3488
  meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
3489
+ MeetingUtil.getIpVersion.returns(IP_VERSION.only_ipv6);
3446
3490
 
3447
3491
  await meeting.addMedia({
3448
3492
  mediaSettings: {},
@@ -3458,6 +3502,7 @@ describe('plugin-meetings', () => {
3458
3502
  connectionType: 'udp',
3459
3503
  selectedCandidatePairChanges: 2,
3460
3504
  ipVersion: 'IPv6',
3505
+ ipver: 6,
3461
3506
  numTransports: 1,
3462
3507
  isMultistream: false,
3463
3508
  retriedWithTurnServer: false,
@@ -3536,6 +3581,7 @@ describe('plugin-meetings', () => {
3536
3581
  selected_cluster: null,
3537
3582
  selected_subnet: null,
3538
3583
  iceCandidatesCount: 0,
3584
+ ipver: 1,
3539
3585
  }
3540
3586
  );
3541
3587
 
@@ -3600,6 +3646,7 @@ describe('plugin-meetings', () => {
3600
3646
  selected_cluster: null,
3601
3647
  selected_subnet: null,
3602
3648
  iceCandidatesCount: 0,
3649
+ ipver: 1,
3603
3650
  }
3604
3651
  );
3605
3652
 
@@ -3646,6 +3693,7 @@ describe('plugin-meetings', () => {
3646
3693
  locus_id: meeting.locusUrl.split('/').pop(),
3647
3694
  connectionType: 'udp',
3648
3695
  ipVersion: 'IPv6',
3696
+ ipver: 1,
3649
3697
  selectedCandidatePairChanges: 2,
3650
3698
  numTransports: 1,
3651
3699
  isMultistream: false,
@@ -3726,6 +3774,7 @@ describe('plugin-meetings', () => {
3726
3774
  selected_cluster: 'some.cluster',
3727
3775
  selected_subnet: '1.X.X.X',
3728
3776
  iceCandidatesCount: 0,
3777
+ ipver: 1,
3729
3778
  }
3730
3779
  );
3731
3780
 
@@ -4031,13 +4080,14 @@ describe('plugin-meetings', () => {
4031
4080
  });
4032
4081
  });
4033
4082
 
4034
- 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 () => {
4035
4084
  let fakeMembersCollection = {
4036
4085
  members: {
4037
- member1: {isInMeeting: true},
4038
- member2: {isInMeeting: true},
4039
- member3: {isInMeeting: false},
4040
- },
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
+ }
4041
4091
  };
4042
4092
  sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
4043
4093
  const fakeData = {intervalMetadata: {}};
@@ -4055,11 +4105,12 @@ describe('plugin-meetings', () => {
4055
4105
  },
4056
4106
  payload: {
4057
4107
  intervals: [
4058
- sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
4108
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 3)),
4059
4109
  ],
4060
4110
  },
4061
4111
  });
4062
- fakeMembersCollection.members.member2.isInMeeting = false;
4112
+ // Move member2 from lobby to neither in meeting nor lobby
4113
+ fakeMembersCollection.members.member2.isInLobby = false;
4063
4114
 
4064
4115
  statsAnalyzerStub.emit(
4065
4116
  {file: 'test', function: 'test'},
@@ -4074,7 +4125,7 @@ describe('plugin-meetings', () => {
4074
4125
  },
4075
4126
  payload: {
4076
4127
  intervals: [
4077
- sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1)),
4128
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
4078
4129
  ],
4079
4130
  },
4080
4131
  });
@@ -4101,6 +4152,132 @@ describe('plugin-meetings', () => {
4101
4152
  });
4102
4153
  });
4103
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
+
4104
4281
  describe('bundlePolicy', () => {
4105
4282
  const FAKE_TURN_URL = 'turns:webex.com:3478';
4106
4283
  const FAKE_TURN_USER = 'some-turn-username';
@@ -5567,6 +5744,7 @@ describe('plugin-meetings', () => {
5567
5744
  let multistreamEventListeners;
5568
5745
  let transcodedEventListeners;
5569
5746
  let mockStatsAnalyzerCtor;
5747
+ let statsMonitorStub;
5570
5748
 
5571
5749
  const setupFakeRoapMediaConnection = (fakeRoapMediaConnection, eventListeners) => {
5572
5750
  fakeRoapMediaConnection.on.callsFake((eventName, cb) => {
@@ -5598,6 +5776,14 @@ describe('plugin-meetings', () => {
5598
5776
  return {on: sinon.stub(), stopAnalyzer: sinon.stub()};
5599
5777
  });
5600
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
+
5601
5787
  webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
5602
5788
  sinon.stub();
5603
5789
 
@@ -5660,6 +5846,7 @@ describe('plugin-meetings', () => {
5660
5846
  mockStatsAnalyzerCtor,
5661
5847
  sinon.match({
5662
5848
  isMultistream: true,
5849
+ statsMonitor: statsMonitorStub,
5663
5850
  })
5664
5851
  );
5665
5852
  const initialStatsAnalyzer = mockStatsAnalyzerCtor.returnValues[0];
@@ -6548,7 +6735,7 @@ describe('plugin-meetings', () => {
6548
6735
  clientUrl: meeting.deviceUrl,
6549
6736
  });
6550
6737
  assert.notCalled(meeting.meetingRequest.dialOut);
6551
-
6738
+
6552
6739
  // Verify pstnCorrelationId was set
6553
6740
  assert.exists(meeting.pstnCorrelationId);
6554
6741
  assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
@@ -6625,7 +6812,7 @@ describe('plugin-meetings', () => {
6625
6812
  throw new Error('Promise resolved when it should have rejected');
6626
6813
  } catch (e) {
6627
6814
  assert.equal(e, error);
6628
-
6815
+
6629
6816
  // Verify behavioral metric was sent with dial_in_correlation_id
6630
6817
  assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_IN_FAILURE, {
6631
6818
  correlation_id: meeting.correlationId,
@@ -6636,7 +6823,7 @@ describe('plugin-meetings', () => {
6636
6823
  reason: error.error.message,
6637
6824
  stack: error.stack,
6638
6825
  });
6639
-
6826
+
6640
6827
  // Verify pstnCorrelationId was cleared after error
6641
6828
  assert.equal(meeting.pstnCorrelationId, undefined);
6642
6829
  }
@@ -6652,7 +6839,7 @@ describe('plugin-meetings', () => {
6652
6839
  throw new Error('Promise resolved when it should have rejected');
6653
6840
  } catch (e) {
6654
6841
  assert.equal(e, error);
6655
-
6842
+
6656
6843
  // Verify behavioral metric was sent with dial_out_correlation_id
6657
6844
  assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_OUT_FAILURE, {
6658
6845
  correlation_id: meeting.correlationId,
@@ -6663,7 +6850,7 @@ describe('plugin-meetings', () => {
6663
6850
  reason: error.error.message,
6664
6851
  stack: error.stack,
6665
6852
  });
6666
-
6853
+
6667
6854
  // Verify pstnCorrelationId was cleared after error
6668
6855
  assert.equal(meeting.pstnCorrelationId, undefined);
6669
6856
  }
@@ -6686,12 +6873,12 @@ describe('plugin-meetings', () => {
6686
6873
 
6687
6874
  it('should disconnect phone audio and clear pstnCorrelationId', async () => {
6688
6875
  meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6689
-
6876
+
6690
6877
  await meeting.disconnectPhoneAudio();
6691
-
6878
+
6692
6879
  // Verify that pstnCorrelationId is cleared
6693
6880
  assert.equal(meeting.pstnCorrelationId, undefined);
6694
-
6881
+
6695
6882
  // Verify that MeetingUtil.disconnectPhoneAudio was called for both dial-in and dial-out
6696
6883
  assert.calledTwice(MeetingUtil.disconnectPhoneAudio);
6697
6884
  assert.calledWith(MeetingUtil.disconnectPhoneAudio, meeting, meeting.dialInUrl);
@@ -6702,9 +6889,9 @@ describe('plugin-meetings', () => {
6702
6889
  meeting.dialInDeviceStatus = 'IDLE';
6703
6890
  meeting.dialOutDeviceStatus = 'IDLE';
6704
6891
  meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6705
-
6892
+
6706
6893
  await meeting.disconnectPhoneAudio();
6707
-
6894
+
6708
6895
  // Verify that pstnCorrelationId is still cleared even when no phone connection is active
6709
6896
  assert.equal(meeting.pstnCorrelationId, undefined);
6710
6897
  // And verify no disconnect was attempted
@@ -7462,6 +7649,8 @@ describe('plugin-meetings', () => {
7462
7649
  'locus-id',
7463
7650
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
7464
7651
  {meetingId: meeting.id, sendCAevents: true},
7652
+ null,
7653
+ null,
7465
7654
  null
7466
7655
  );
7467
7656
  assert.deepEqual(meeting.meetingInfo, {
@@ -7508,6 +7697,8 @@ describe('plugin-meetings', () => {
7508
7697
  'locus-id',
7509
7698
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
7510
7699
  {meetingId: meeting.id, sendCAevents: true},
7700
+ null,
7701
+ null,
7511
7702
  null
7512
7703
  );
7513
7704
  assert.deepEqual(meeting.meetingInfo, {
@@ -7563,6 +7754,8 @@ describe('plugin-meetings', () => {
7563
7754
  permissionToken: FAKE_PERMISSION_TOKEN,
7564
7755
  },
7565
7756
  {meetingId: meeting.id, sendCAevents: true},
7757
+ null,
7758
+ null,
7566
7759
  null
7567
7760
  );
7568
7761
  assert.deepEqual(meeting.meetingInfo, {
@@ -9004,11 +9197,16 @@ describe('plugin-meetings', () => {
9004
9197
  meeting.hasMediaConnectionConnectedAtLeastOnce = false;
9005
9198
  meeting.setupMediaConnectionListeners();
9006
9199
 
9200
+ sinon.stub(MeetingUtil, 'getCaEventLabelsForIpVersion').returns(['fake labels']);
9201
+
9007
9202
  simulateConnectionStateChange(ConnectionState.Connecting);
9008
9203
 
9009
9204
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
9010
9205
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
9011
9206
  name: 'client.ice.start',
9207
+ payload: {
9208
+ labels: ['fake labels'],
9209
+ },
9012
9210
  options: {
9013
9211
  meetingId: meeting.id,
9014
9212
  },
@@ -10273,6 +10471,24 @@ describe('plugin-meetings', () => {
10273
10471
  );
10274
10472
  });
10275
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
+
10276
10492
  it('listens to CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED', async () => {
10277
10493
  const state = {example: 'value'};
10278
10494
 
@@ -10352,6 +10568,7 @@ describe('plugin-meetings', () => {
10352
10568
  describe('#setUpLocusUrlListener', () => {
10353
10569
  it('listens to the locus url update event', (done) => {
10354
10570
  const newLocusUrl = 'newLocusUrl/12345';
10571
+ const payload = {url: newLocusUrl}
10355
10572
 
10356
10573
  meeting.members = {locusUrlUpdate: sinon.stub().returns(Promise.resolve(test1))};
10357
10574
  meeting.recordingController = {setLocusUrl: sinon.stub().returns(undefined)};
@@ -10365,14 +10582,14 @@ describe('plugin-meetings', () => {
10365
10582
  meeting.locusInfo.emit(
10366
10583
  {function: 'test', file: 'test'},
10367
10584
  'LOCUS_INFO_UPDATE_URL',
10368
- newLocusUrl
10585
+ payload
10369
10586
  );
10370
10587
  assert.calledWith(meeting.members.locusUrlUpdate, newLocusUrl);
10371
10588
  assert.calledOnceWithExactly(meeting.breakouts.locusUrlUpdate, newLocusUrl);
10372
10589
  assert.calledOnceWithExactly(meeting.annotation.locusUrlUpdate, newLocusUrl);
10373
10590
  assert.calledWith(meeting.members.locusUrlUpdate, newLocusUrl);
10374
10591
  assert.calledWith(meeting.recordingController.setLocusUrl, newLocusUrl);
10375
- assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl);
10592
+ assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl, false);
10376
10593
  assert.calledWith(meeting.simultaneousInterpretation.locusUrlUpdate, newLocusUrl);
10377
10594
  assert.calledWith(meeting.webinar.locusUrlUpdate, newLocusUrl);
10378
10595
  assert.equal(meeting.locusUrl, newLocusUrl);
@@ -10390,6 +10607,22 @@ describe('plugin-meetings', () => {
10390
10607
  {locusUrl: 'newLocusUrl/12345'}
10391
10608
  );
10392
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
+
10393
10626
  done();
10394
10627
  });
10395
10628
  });
@@ -11227,6 +11460,8 @@ describe('plugin-meetings', () => {
11227
11460
  let canUserRenameOthersSpy;
11228
11461
  let canShareWhiteBoardSpy;
11229
11462
  let canMoveToLobbySpy;
11463
+ let isSpokenLanguageAutoDetectionEnabledSpy;
11464
+ let showAutoEndMeetingWarningSpy;
11230
11465
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
11231
11466
 
11232
11467
  beforeEach(() => {
@@ -11258,11 +11493,15 @@ describe('plugin-meetings', () => {
11258
11493
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
11259
11494
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
11260
11495
  canMoveToLobbySpy = sinon.spy(MeetingUtil, 'canMoveToLobby');
11496
+ showAutoEndMeetingWarningSpy = sinon.spy(MeetingUtil, 'showAutoEndMeetingWarning');
11497
+ isSpokenLanguageAutoDetectionEnabledSpy = sinon.spy(MeetingUtil, 'isSpokenLanguageAutoDetectionEnabled');
11498
+
11261
11499
  });
11262
11500
 
11263
11501
  afterEach(() => {
11264
11502
  inMeetingActionsSetSpy.restore();
11265
11503
  waitingForOthersToJoinSpy.restore();
11504
+ showAutoEndMeetingWarningSpy.restore();
11266
11505
  });
11267
11506
 
11268
11507
  forEach(
@@ -11810,6 +12049,8 @@ describe('plugin-meetings', () => {
11810
12049
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11811
12050
  assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11812
12051
  assert.calledWith(canMoveToLobbySpy, userDisplayHints);
12052
+ assert.calledWith(showAutoEndMeetingWarningSpy, userDisplayHints);
12053
+ assert.calledWith(isSpokenLanguageAutoDetectionEnabledSpy, userDisplayHints);
11813
12054
 
11814
12055
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11815
12056
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -12262,6 +12503,7 @@ describe('plugin-meetings', () => {
12262
12503
  meeting.deviceUrl = 'deviceUrl.com';
12263
12504
  webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12264
12505
  webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
12506
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
12265
12507
  });
12266
12508
  it('should stop the whiteboard share', async () => {
12267
12509
  const whiteboardShare = meeting.stopWhiteboardShare();
@@ -12363,6 +12605,9 @@ describe('plugin-meetings', () => {
12363
12605
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
12364
12606
  meeting.deviceUrl = 'my-web-url';
12365
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();
12366
12611
  });
12367
12612
 
12368
12613
  const USER_IDS = {
@@ -12594,7 +12839,7 @@ describe('plugin-meetings', () => {
12594
12839
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12595
12840
  functionName: 'remoteShare',
12596
12841
  eventPayload: {
12597
- memberId: null,
12842
+ memberId: meeting.webinar.selfIsAttendee ? beneficiaryId : null,
12598
12843
  url,
12599
12844
  shareInstanceId,
12600
12845
  annotationInfo: undefined,
@@ -12649,7 +12894,7 @@ describe('plugin-meetings', () => {
12649
12894
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12650
12895
  functionName: 'remoteShare',
12651
12896
  eventPayload: {
12652
- memberId: null,
12897
+ memberId: beneficiaryId,
12653
12898
  url,
12654
12899
  shareInstanceId,
12655
12900
  annotationInfo: undefined,
@@ -13508,7 +13753,54 @@ describe('plugin-meetings', () => {
13508
13753
  payloadTestHelper([data1, data2, data3]);
13509
13754
  });
13510
13755
  });
13511
- });
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
+ });
13512
13804
 
13513
13805
  describe('handleShareVideoStreamMuteStateChange', () => {
13514
13806
  it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
@@ -13535,6 +13827,7 @@ describe('plugin-meetings', () => {
13535
13827
  });
13536
13828
  });
13537
13829
  });
13830
+ });
13538
13831
 
13539
13832
  describe('#startKeepAlive', () => {
13540
13833
  let clock;
@@ -14740,4 +15033,79 @@ describe('plugin-meetings', () => {
14740
15033
  );
14741
15034
  });
14742
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
+ });
14743
15111
  });