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

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 (87) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +2 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/interpretation/index.js +1 -1
  6. package/dist/interpretation/siLanguage.js +1 -1
  7. package/dist/locus-info/index.js +38 -10
  8. package/dist/locus-info/index.js.map +1 -1
  9. package/dist/locus-info/parser.js +4 -1
  10. package/dist/locus-info/parser.js.map +1 -1
  11. package/dist/media/properties.js +53 -5
  12. package/dist/media/properties.js.map +1 -1
  13. package/dist/meeting/in-meeting-actions.js +2 -0
  14. package/dist/meeting/in-meeting-actions.js.map +1 -1
  15. package/dist/meeting/index.js +189 -122
  16. package/dist/meeting/index.js.map +1 -1
  17. package/dist/meeting/muteState.js +2 -5
  18. package/dist/meeting/muteState.js.map +1 -1
  19. package/dist/meeting/request.js +25 -0
  20. package/dist/meeting/request.js.map +1 -1
  21. package/dist/meeting/util.js +30 -11
  22. package/dist/meeting/util.js.map +1 -1
  23. package/dist/meeting-info/meeting-info-v2.js +29 -21
  24. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  25. package/dist/meetings/index.js +31 -25
  26. package/dist/meetings/index.js.map +1 -1
  27. package/dist/member/types.js.map +1 -1
  28. package/dist/members/collection.js +13 -0
  29. package/dist/members/collection.js.map +1 -1
  30. package/dist/members/index.js +42 -20
  31. package/dist/members/index.js.map +1 -1
  32. package/dist/members/util.js +7 -2
  33. package/dist/members/util.js.map +1 -1
  34. package/dist/metrics/constants.js +2 -1
  35. package/dist/metrics/constants.js.map +1 -1
  36. package/dist/reachability/index.js +3 -3
  37. package/dist/reachability/index.js.map +1 -1
  38. package/dist/types/constants.d.ts +2 -0
  39. package/dist/types/locus-info/index.d.ts +54 -1
  40. package/dist/types/media/properties.d.ts +21 -0
  41. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  42. package/dist/types/meeting/index.d.ts +11 -1
  43. package/dist/types/meeting/request.d.ts +9 -0
  44. package/dist/types/meeting/util.d.ts +10 -3
  45. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  46. package/dist/types/meetings/index.d.ts +3 -1
  47. package/dist/types/member/types.d.ts +1 -0
  48. package/dist/types/members/collection.d.ts +6 -0
  49. package/dist/types/members/index.d.ts +12 -2
  50. package/dist/types/members/util.d.ts +6 -3
  51. package/dist/types/metrics/constants.d.ts +1 -0
  52. package/dist/webinar/index.js +1 -1
  53. package/package.json +17 -17
  54. package/src/constants.ts +2 -0
  55. package/src/locus-info/index.ts +84 -9
  56. package/src/locus-info/parser.ts +5 -1
  57. package/src/media/properties.ts +43 -0
  58. package/src/meeting/in-meeting-actions.ts +4 -0
  59. package/src/meeting/index.ts +91 -4
  60. package/src/meeting/muteState.ts +2 -6
  61. package/src/meeting/request.ts +23 -0
  62. package/src/meeting/util.ts +41 -20
  63. package/src/meeting-info/meeting-info-v2.ts +24 -5
  64. package/src/meetings/index.ts +9 -3
  65. package/src/member/types.ts +1 -0
  66. package/src/members/collection.ts +11 -0
  67. package/src/members/index.ts +38 -5
  68. package/src/members/util.ts +18 -2
  69. package/src/metrics/constants.ts +1 -0
  70. package/src/reachability/index.ts +3 -3
  71. package/test/unit/spec/common/browser-detection.js +0 -24
  72. package/test/unit/spec/locus-info/index.js +30 -15
  73. package/test/unit/spec/locus-info/parser.js +3 -2
  74. package/test/unit/spec/media/properties.ts +137 -0
  75. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  76. package/test/unit/spec/meeting/index.js +255 -27
  77. package/test/unit/spec/meeting/muteState.js +32 -6
  78. package/test/unit/spec/meeting/request.js +21 -0
  79. package/test/unit/spec/meeting/utils.js +45 -16
  80. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  81. package/test/unit/spec/meetings/index.js +10 -5
  82. package/test/unit/spec/members/collection.js +120 -0
  83. package/test/unit/spec/members/index.js +72 -3
  84. package/test/unit/spec/members/request.js +55 -0
  85. package/test/unit/spec/members/utils.js +116 -14
  86. package/test/unit/spec/reachability/index.ts +158 -3
  87. package/test/unit/spec/roap/turnDiscovery.ts +3 -3
@@ -38,6 +38,7 @@ describe('plugin-meetings', () => {
38
38
  isManualCaptionActive: null,
39
39
  isPremiseRecordingEnabled: null,
40
40
  isSaveTranscriptsEnabled: null,
41
+ isSpokenLanguageAutoDetectionEnabled: null,
41
42
  isWebexAssistantActive: null,
42
43
  canViewCaptionPanel: null,
43
44
  isRealTimeTranslationEnabled: null,
@@ -151,6 +152,7 @@ describe('plugin-meetings', () => {
151
152
  'isManualCaptionActive',
152
153
  'isPremiseRecordingEnabled',
153
154
  'isSaveTranscriptsEnabled',
155
+ 'isSpokenLanguageAutoDetectionEnabled',
154
156
  'isWebexAssistantActive',
155
157
  'canViewCaptionPanel',
156
158
  'isRealTimeTranslationEnabled',
@@ -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
  },
@@ -11227,6 +11425,7 @@ describe('plugin-meetings', () => {
11227
11425
  let canUserRenameOthersSpy;
11228
11426
  let canShareWhiteBoardSpy;
11229
11427
  let canMoveToLobbySpy;
11428
+ let isSpokenLanguageAutoDetectionEnabledSpy;
11230
11429
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
11231
11430
 
11232
11431
  beforeEach(() => {
@@ -11258,6 +11457,8 @@ describe('plugin-meetings', () => {
11258
11457
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
11259
11458
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
11260
11459
  canMoveToLobbySpy = sinon.spy(MeetingUtil, 'canMoveToLobby');
11460
+ isSpokenLanguageAutoDetectionEnabledSpy = sinon.spy(MeetingUtil, 'isSpokenLanguageAutoDetectionEnabled');
11461
+
11261
11462
  });
11262
11463
 
11263
11464
  afterEach(() => {
@@ -11810,6 +12011,7 @@ describe('plugin-meetings', () => {
11810
12011
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11811
12012
  assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11812
12013
  assert.calledWith(canMoveToLobbySpy, userDisplayHints);
12014
+ assert.calledWith(isSpokenLanguageAutoDetectionEnabledSpy, userDisplayHints);
11813
12015
 
11814
12016
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11815
12017
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -12598,7 +12800,7 @@ describe('plugin-meetings', () => {
12598
12800
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12599
12801
  functionName: 'remoteShare',
12600
12802
  eventPayload: {
12601
- memberId: null,
12803
+ memberId: meeting.webinar.selfIsAttendee ? beneficiaryId : null,
12602
12804
  url,
12603
12805
  shareInstanceId,
12604
12806
  annotationInfo: undefined,
@@ -12653,7 +12855,7 @@ describe('plugin-meetings', () => {
12653
12855
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12654
12856
  functionName: 'remoteShare',
12655
12857
  eventPayload: {
12656
- memberId: null,
12858
+ memberId: beneficiaryId,
12657
12859
  url,
12658
12860
  shareInstanceId,
12659
12861
  annotationInfo: undefined,
@@ -14792,4 +14994,30 @@ describe('plugin-meetings', () => {
14792
14994
  );
14793
14995
  });
14794
14996
  });
14997
+
14998
+ describe('#notifyHost', () => {
14999
+ beforeEach(() => {
15000
+ meeting.meetingRequest.notifyHost = sinon.stub().returns(Promise.resolve());
15001
+ });
15002
+
15003
+ it('sends the expected request', async () => {
15004
+ meeting.meetingInfo.siteFullUrl = `convergedats.webex.com`;
15005
+ const meetingUuid = 'meeting-uuid';
15006
+ const displayName = ['Test', 'User'];
15007
+ meeting.locusId = 'locusId';
15008
+
15009
+ const notifyHostPromise = meeting.notifyHost(meetingUuid, displayName);
15010
+
15011
+ assert.exists(notifyHostPromise.then);
15012
+ await notifyHostPromise;
15013
+
15014
+ assert.calledOnceWithExactly(
15015
+ meeting.meetingRequest.notifyHost,
15016
+ meeting.meetingInfo.siteFullUrl,
15017
+ meeting.locusId,
15018
+ meetingUuid,
15019
+ displayName,
15020
+ );
15021
+ });
15022
+ });
14795
15023
  });