@webex/plugin-meetings 3.8.0-next.8 → 3.8.0-next.81

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 (171) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +70 -6
  3. package/dist/breakouts/index.js.map +1 -1
  4. package/dist/common/errors/webex-errors.js +12 -2
  5. package/dist/common/errors/webex-errors.js.map +1 -1
  6. package/dist/config.js +5 -1
  7. package/dist/config.js.map +1 -1
  8. package/dist/constants.js +20 -123
  9. package/dist/constants.js.map +1 -1
  10. package/dist/controls-options-manager/enums.js +2 -0
  11. package/dist/controls-options-manager/enums.js.map +1 -1
  12. package/dist/controls-options-manager/types.js.map +1 -1
  13. package/dist/controls-options-manager/util.js +52 -0
  14. package/dist/controls-options-manager/util.js.map +1 -1
  15. package/dist/interpretation/index.js +1 -1
  16. package/dist/interpretation/siLanguage.js +1 -1
  17. package/dist/locus-info/controlsUtils.js +28 -10
  18. package/dist/locus-info/controlsUtils.js.map +1 -1
  19. package/dist/locus-info/index.js +62 -12
  20. package/dist/locus-info/index.js.map +1 -1
  21. package/dist/locus-info/selfUtils.js +432 -418
  22. package/dist/locus-info/selfUtils.js.map +1 -1
  23. package/dist/media/index.js +17 -17
  24. package/dist/media/index.js.map +1 -1
  25. package/dist/media/properties.js +94 -6
  26. package/dist/media/properties.js.map +1 -1
  27. package/dist/meeting/brbState.js +6 -0
  28. package/dist/meeting/brbState.js.map +1 -1
  29. package/dist/meeting/in-meeting-actions.js +17 -1
  30. package/dist/meeting/in-meeting-actions.js.map +1 -1
  31. package/dist/meeting/index.js +570 -302
  32. package/dist/meeting/index.js.map +1 -1
  33. package/dist/meeting/locusMediaRequest.js +0 -17
  34. package/dist/meeting/locusMediaRequest.js.map +1 -1
  35. package/dist/meeting/muteState.js +0 -2
  36. package/dist/meeting/muteState.js.map +1 -1
  37. package/dist/meeting/request.js +30 -0
  38. package/dist/meeting/request.js.map +1 -1
  39. package/dist/meeting/request.type.js.map +1 -1
  40. package/dist/meeting/util.js +13 -2
  41. package/dist/meeting/util.js.map +1 -1
  42. package/dist/meeting-info/meeting-info-v2.js +373 -68
  43. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  44. package/dist/meeting-info/utilv2.js +5 -1
  45. package/dist/meeting-info/utilv2.js.map +1 -1
  46. package/dist/meetings/index.js +136 -1
  47. package/dist/meetings/index.js.map +1 -1
  48. package/dist/meetings/util.js +14 -0
  49. package/dist/meetings/util.js.map +1 -1
  50. package/dist/member/index.js +10 -0
  51. package/dist/member/index.js.map +1 -1
  52. package/dist/member/util.js +330 -353
  53. package/dist/member/util.js.map +1 -1
  54. package/dist/members/index.js +42 -0
  55. package/dist/members/index.js.map +1 -1
  56. package/dist/members/request.js +38 -0
  57. package/dist/members/request.js.map +1 -1
  58. package/dist/members/util.js +36 -1
  59. package/dist/members/util.js.map +1 -1
  60. package/dist/metrics/constants.js +9 -0
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/reachability/clusterReachability.js +63 -27
  63. package/dist/reachability/clusterReachability.js.map +1 -1
  64. package/dist/reachability/index.js +112 -47
  65. package/dist/reachability/index.js.map +1 -1
  66. package/dist/reachability/reachability.types.js +14 -0
  67. package/dist/reachability/reachability.types.js.map +1 -1
  68. package/dist/reachability/request.js +19 -3
  69. package/dist/reachability/request.js.map +1 -1
  70. package/dist/reconnection-manager/index.js +2 -2
  71. package/dist/reconnection-manager/index.js.map +1 -1
  72. package/dist/roap/index.js.map +1 -1
  73. package/dist/roap/turnDiscovery.js +45 -27
  74. package/dist/roap/turnDiscovery.js.map +1 -1
  75. package/dist/roap/types.js +17 -0
  76. package/dist/roap/types.js.map +1 -0
  77. package/dist/types/common/errors/webex-errors.d.ts +7 -1
  78. package/dist/types/config.d.ts +3 -0
  79. package/dist/types/constants.d.ts +13 -85
  80. package/dist/types/controls-options-manager/enums.d.ts +3 -1
  81. package/dist/types/controls-options-manager/types.d.ts +7 -1
  82. package/dist/types/locus-info/index.d.ts +3 -3
  83. package/dist/types/locus-info/selfUtils.d.ts +216 -1
  84. package/dist/types/media/properties.d.ts +15 -0
  85. package/dist/types/meeting/in-meeting-actions.d.ts +16 -0
  86. package/dist/types/meeting/index.d.ts +43 -1
  87. package/dist/types/meeting/muteState.d.ts +0 -1
  88. package/dist/types/meeting/request.d.ts +12 -1
  89. package/dist/types/meeting/request.type.d.ts +6 -0
  90. package/dist/types/meeting/util.d.ts +3 -1
  91. package/dist/types/meeting-info/meeting-info-v2.d.ts +82 -1
  92. package/dist/types/meetings/index.d.ts +57 -0
  93. package/dist/types/member/index.d.ts +1 -0
  94. package/dist/types/member/util.d.ts +159 -1
  95. package/dist/types/members/index.d.ts +15 -0
  96. package/dist/types/members/request.d.ts +26 -0
  97. package/dist/types/members/util.d.ts +27 -0
  98. package/dist/types/metrics/constants.d.ts +9 -0
  99. package/dist/types/reachability/clusterReachability.d.ts +15 -7
  100. package/dist/types/reachability/index.d.ts +10 -1
  101. package/dist/types/reachability/reachability.types.d.ts +5 -0
  102. package/dist/types/roap/index.d.ts +3 -2
  103. package/dist/types/roap/turnDiscovery.d.ts +5 -17
  104. package/dist/types/roap/types.d.ts +16 -0
  105. package/dist/webinar/index.js +1 -1
  106. package/package.json +24 -23
  107. package/src/breakouts/index.ts +69 -0
  108. package/src/common/errors/webex-errors.ts +8 -1
  109. package/src/config.ts +3 -0
  110. package/src/constants.ts +20 -90
  111. package/src/controls-options-manager/enums.ts +2 -0
  112. package/src/controls-options-manager/types.ts +11 -1
  113. package/src/controls-options-manager/util.ts +62 -0
  114. package/src/locus-info/controlsUtils.ts +44 -14
  115. package/src/locus-info/index.ts +56 -13
  116. package/src/locus-info/selfUtils.ts +496 -442
  117. package/src/media/index.ts +23 -21
  118. package/src/media/properties.ts +96 -0
  119. package/src/meeting/brbState.ts +7 -0
  120. package/src/meeting/in-meeting-actions.ts +32 -0
  121. package/src/meeting/index.ts +382 -93
  122. package/src/meeting/locusMediaRequest.ts +0 -18
  123. package/src/meeting/muteState.ts +0 -2
  124. package/src/meeting/request.ts +36 -1
  125. package/src/meeting/request.type.ts +7 -0
  126. package/src/meeting/util.ts +11 -2
  127. package/src/meeting-info/meeting-info-v2.ts +254 -8
  128. package/src/meeting-info/utilv2.ts +5 -0
  129. package/src/meetings/index.ts +148 -1
  130. package/src/meetings/util.ts +18 -0
  131. package/src/member/index.ts +13 -2
  132. package/src/member/util.ts +351 -348
  133. package/src/members/index.ts +47 -0
  134. package/src/members/request.ts +44 -0
  135. package/src/members/util.ts +43 -1
  136. package/src/metrics/constants.ts +9 -0
  137. package/src/reachability/clusterReachability.ts +73 -26
  138. package/src/reachability/index.ts +70 -1
  139. package/src/reachability/reachability.types.ts +6 -0
  140. package/src/reachability/request.ts +7 -0
  141. package/src/reconnection-manager/index.ts +2 -2
  142. package/src/roap/index.ts +3 -7
  143. package/src/roap/turnDiscovery.ts +34 -39
  144. package/src/roap/types.ts +23 -0
  145. package/test/unit/spec/breakouts/index.ts +167 -95
  146. package/test/unit/spec/controls-options-manager/util.js +120 -0
  147. package/test/unit/spec/locus-info/controlsUtils.js +103 -9
  148. package/test/unit/spec/locus-info/index.js +167 -73
  149. package/test/unit/spec/locus-info/selfUtils.js +98 -24
  150. package/test/unit/spec/media/index.ts +150 -18
  151. package/test/unit/spec/media/properties.ts +130 -0
  152. package/test/unit/spec/meeting/brbState.ts +19 -0
  153. package/test/unit/spec/meeting/in-meeting-actions.ts +19 -4
  154. package/test/unit/spec/meeting/index.js +557 -35
  155. package/test/unit/spec/meeting/locusMediaRequest.ts +0 -30
  156. package/test/unit/spec/meeting/muteState.js +0 -2
  157. package/test/unit/spec/meeting/request.js +32 -1
  158. package/test/unit/spec/meeting/utils.js +119 -18
  159. package/test/unit/spec/meeting-info/meetinginfov2.js +484 -114
  160. package/test/unit/spec/meeting-info/utilv2.js +19 -0
  161. package/test/unit/spec/meetings/index.js +146 -2
  162. package/test/unit/spec/member/index.js +7 -0
  163. package/test/unit/spec/member/util.js +24 -0
  164. package/test/unit/spec/members/index.js +140 -26
  165. package/test/unit/spec/members/request.js +68 -22
  166. package/test/unit/spec/members/utils.js +75 -0
  167. package/test/unit/spec/reachability/clusterReachability.ts +88 -56
  168. package/test/unit/spec/reachability/index.ts +101 -0
  169. package/test/unit/spec/reachability/request.js +47 -2
  170. package/test/unit/spec/reconnection-manager/index.js +4 -4
  171. package/test/unit/spec/roap/turnDiscovery.ts +110 -28
@@ -1,6 +1,7 @@
1
1
  /*!
2
2
  * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
3
  */
4
+ import {v4 as uuidv4} from 'uuid';
4
5
  import 'jsdom-global/register';
5
6
  import {cloneDeep, forEach, isEqual, isUndefined} from 'lodash';
6
7
  import sinon from 'sinon';
@@ -115,9 +116,9 @@ import {ERROR_DESCRIPTIONS} from '@webex/internal-plugin-metrics/src/call-diagno
115
116
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
116
117
 
117
118
  import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
118
- import { createBrbState } from '@webex/plugin-meetings/src/meeting/brbState';
119
+ import {createBrbState} from '@webex/plugin-meetings/src/meeting/brbState';
119
120
  import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
120
- import { EventEmitter } from 'stream';
121
+ import {EventEmitter} from 'stream';
121
122
 
122
123
  describe('plugin-meetings', () => {
123
124
  const logger = {
@@ -210,6 +211,7 @@ describe('plugin-meetings', () => {
210
211
  let membersSpy;
211
212
  let meetingRequestSpy;
212
213
  let correlationId;
214
+ let isoLocalClientMeetingJoinTime;
213
215
  let uploadEvent;
214
216
 
215
217
  beforeEach(() => {
@@ -241,6 +243,7 @@ describe('plugin-meetings', () => {
241
243
  },
242
244
  });
243
245
 
246
+ webex.internal.newMetrics.callDiagnosticMetrics.clearErrorCache = sinon.stub();
244
247
  webex.internal.support.submitLogs = sinon.stub().returns(Promise.resolve());
245
248
  webex.internal.services = {get: sinon.stub().returns('locus-url')};
246
249
  webex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
@@ -251,6 +254,7 @@ describe('plugin-meetings', () => {
251
254
  getReachabilityResults: sinon.stub().resolves(undefined),
252
255
  getReachabilityMetrics: sinon.stub().resolves({}),
253
256
  stopReachability: sinon.stub(),
257
+ isSubnetReachable: sinon.stub().returns(true),
254
258
  };
255
259
  webex.internal.llm.on = sinon.stub();
256
260
  webex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
@@ -281,7 +285,7 @@ describe('plugin-meetings', () => {
281
285
  testDestination = `testDestination-${uuid.v4()}`;
282
286
  correlationId = uuid.v4();
283
287
  uploadEvent = new EventEmitter();
284
- uploadEvent.addListener('progress', () => {})
288
+ uploadEvent.addListener('progress', () => {});
285
289
 
286
290
  meeting = new Meeting(
287
291
  {
@@ -610,6 +614,22 @@ describe('plugin-meetings', () => {
610
614
  assert.calledWith(meeting.members.cancelPhoneInvite, uuid1);
611
615
  });
612
616
  });
617
+ describe('#cancelSIPInvite', () => {
618
+ it('should have #cancelSIPInvite', () => {
619
+ assert.exists(meeting.cancelSIPInvite);
620
+ });
621
+ beforeEach(() => {
622
+ meeting.members.cancelSIPInvite = sinon.stub().returns(Promise.resolve(test1));
623
+ });
624
+ it('should proxy members #cancelSIPInvite and return a promise', async () => {
625
+ const cancel = meeting.cancelSIPInvite({memberId: uuid1});
626
+
627
+ assert.exists(cancel.then);
628
+ await cancel;
629
+ assert.calledOnce(meeting.members.cancelSIPInvite);
630
+ assert.calledWith(meeting.members.cancelSIPInvite, {memberId: uuid1});
631
+ });
632
+ });
613
633
  describe('#admit', () => {
614
634
  it('should have #admit', () => {
615
635
  assert.exists(meeting.admit);
@@ -1692,10 +1712,6 @@ describe('plugin-meetings', () => {
1692
1712
  sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult));
1693
1713
  });
1694
1714
 
1695
- afterEach(() => {
1696
- assert.exists(meeting.isoLocalClientMeetingJoinTime);
1697
- });
1698
-
1699
1715
  it('should join the meeting and return promise', async () => {
1700
1716
  const join = meeting.join({pstnAudioType: 'dial-in'});
1701
1717
  meeting.config.enableAutomaticLLM = true;
@@ -2047,7 +2063,12 @@ describe('plugin-meetings', () => {
2047
2063
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
2048
2064
  meeting.mediaProperties.getCurrentConnectionInfo = sinon
2049
2065
  .stub()
2050
- .resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
2066
+ .resolves({
2067
+ connectionType: 'udp',
2068
+ selectedCandidatePairChanges: 2,
2069
+ numTransports: 1,
2070
+ ipVersion: 'IPv6',
2071
+ });
2051
2072
  meeting.audio = muteStateStub;
2052
2073
  meeting.video = muteStateStub;
2053
2074
  sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
@@ -2110,6 +2131,7 @@ describe('plugin-meetings', () => {
2110
2131
  someReachabilityMetric2: 'some value2',
2111
2132
  }),
2112
2133
  stopReachability: sinon.stub(),
2134
+ isSubnetReachable: sinon.stub().returns(false),
2113
2135
  };
2114
2136
 
2115
2137
  const forceRtcMetricsSend = sinon.stub().resolves();
@@ -2165,6 +2187,8 @@ describe('plugin-meetings', () => {
2165
2187
  someReachabilityMetric1: 'some value1',
2166
2188
  someReachabilityMetric2: 'some value2',
2167
2189
  selectedCandidatePairChanges: 2,
2190
+ isSubnetReachable: null,
2191
+ selectedCluster: null,
2168
2192
  numTransports: 1,
2169
2193
  iceCandidatesCount: 0,
2170
2194
  }
@@ -2211,6 +2235,8 @@ describe('plugin-meetings', () => {
2211
2235
  signalingState: 'unknown',
2212
2236
  connectionState: 'unknown',
2213
2237
  iceConnectionState: 'unknown',
2238
+ isSubnetReachable: null,
2239
+ selectedCluster: null,
2214
2240
  })
2215
2241
  );
2216
2242
 
@@ -2225,6 +2251,7 @@ describe('plugin-meetings', () => {
2225
2251
  someReachabilityMetric1: 'some value1',
2226
2252
  someReachabilityMetric2: 'some value2',
2227
2253
  }),
2254
+ isSubnetReachable: sinon.stub().returns(true),
2228
2255
  };
2229
2256
 
2230
2257
  meeting.waitForRemoteSDPAnswer = sinon.stub().rejects();
@@ -2275,6 +2302,8 @@ describe('plugin-meetings', () => {
2275
2302
  selectedCandidatePairChanges: 2,
2276
2303
  numTransports: 1,
2277
2304
  iceCandidatesCount: 0,
2305
+ isSubnetReachable: null,
2306
+ selectedCluster: null,
2278
2307
  }
2279
2308
  );
2280
2309
  });
@@ -2332,6 +2361,8 @@ describe('plugin-meetings', () => {
2332
2361
  signalingState: 'have-local-offer',
2333
2362
  connectionState: 'connecting',
2334
2363
  iceConnectionState: 'checking',
2364
+ isSubnetReachable: null,
2365
+ selectedCluster: null,
2335
2366
  })
2336
2367
  );
2337
2368
 
@@ -2389,6 +2420,8 @@ describe('plugin-meetings', () => {
2389
2420
  signalingState: 'have-local-offer',
2390
2421
  connectionState: 'connecting',
2391
2422
  iceConnectionState: 'checking',
2423
+ isSubnetReachable: null,
2424
+ selectedCluster: null,
2392
2425
  })
2393
2426
  );
2394
2427
 
@@ -2667,7 +2700,7 @@ describe('plugin-meetings', () => {
2667
2700
 
2668
2701
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
2669
2702
  turnServerInfo: {
2670
- url: FAKE_TURN_URL,
2703
+ urls: [FAKE_TURN_URL],
2671
2704
  username: FAKE_TURN_USER,
2672
2705
  password: FAKE_TURN_PASSWORD,
2673
2706
  },
@@ -2689,7 +2722,7 @@ describe('plugin-meetings', () => {
2689
2722
  meeting.id,
2690
2723
  sinon.match({
2691
2724
  turnServerInfo: {
2692
- url: FAKE_TURN_URL,
2725
+ urls: [FAKE_TURN_URL],
2693
2726
  username: FAKE_TURN_USER,
2694
2727
  password: FAKE_TURN_PASSWORD,
2695
2728
  },
@@ -2724,8 +2757,9 @@ describe('plugin-meetings', () => {
2724
2757
  sinon.stub().returns(FAKE_ERROR));
2725
2758
  webex.meetings.reachability = {
2726
2759
  isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2727
- getReachabilityMetrics: sinon.stub().resolves(),
2760
+ getReachabilityMetrics: sinon.stub().resolves({}),
2728
2761
  stopReachability: sinon.stub(),
2762
+ isSubnetReachable: sinon.stub().returns(true),
2729
2763
  };
2730
2764
  const MOCK_CLIENT_ERROR_CODE = 2004;
2731
2765
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -2747,14 +2781,15 @@ describe('plugin-meetings', () => {
2747
2781
  .onSecondCall()
2748
2782
  .returns({
2749
2783
  turnServerInfo: {
2750
- url: FAKE_TURN_URL,
2784
+ urls: [FAKE_TURN_URL],
2751
2785
  username: FAKE_TURN_USER,
2752
2786
  password: FAKE_TURN_PASSWORD,
2753
2787
  },
2754
2788
  turnDiscoverySkippedReason: undefined,
2755
2789
  });
2756
2790
  meeting.meetingState = 'ACTIVE';
2757
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2791
+ const error = {iceConnected: false};
2792
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects(error);
2758
2793
 
2759
2794
  const forceRtcMetricsSend = sinon.stub().resolves();
2760
2795
  const closeMediaConnectionStub = sinon.stub();
@@ -2772,6 +2807,7 @@ describe('plugin-meetings', () => {
2772
2807
  })
2773
2808
  .catch((err) => {
2774
2809
  errorThrown = err;
2810
+ assert.instanceOf(err.cause, Error);
2775
2811
  assert.instanceOf(err, AddMediaFailed);
2776
2812
  });
2777
2813
 
@@ -2828,6 +2864,7 @@ describe('plugin-meetings', () => {
2828
2864
  },
2829
2865
  options: {
2830
2866
  meetingId: meeting.id,
2867
+ rawError: error,
2831
2868
  },
2832
2869
  });
2833
2870
  assert.calledWith(webex.internal.newMetrics.submitClientEvent.thirdCall, {
@@ -2839,6 +2876,7 @@ describe('plugin-meetings', () => {
2839
2876
  },
2840
2877
  options: {
2841
2878
  meetingId: meeting.id,
2879
+ rawError: error,
2842
2880
  },
2843
2881
  });
2844
2882
 
@@ -2905,6 +2943,8 @@ describe('plugin-meetings', () => {
2905
2943
  selectedCandidatePairChanges: 2,
2906
2944
  numTransports: 1,
2907
2945
  iceCandidatesCount: 0,
2946
+ isSubnetReachable: null,
2947
+ selectedCluster: null,
2908
2948
  },
2909
2949
  ]);
2910
2950
 
@@ -2935,6 +2975,7 @@ describe('plugin-meetings', () => {
2935
2975
  .resolves(false),
2936
2976
  getReachabilityMetrics: sinon.stub().resolves({}),
2937
2977
  stopReachability: sinon.stub(),
2978
+ isSubnetReachable: sinon.stub().returns(true),
2938
2979
  };
2939
2980
  const getErrorPayloadForClientErrorCodeStub =
2940
2981
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
@@ -2959,16 +3000,19 @@ describe('plugin-meetings', () => {
2959
3000
  .onSecondCall()
2960
3001
  .returns({
2961
3002
  turnServerInfo: {
2962
- url: FAKE_TURN_URL,
3003
+ urls: [FAKE_TURN_URL],
2963
3004
  username: FAKE_TURN_USER,
2964
3005
  password: FAKE_TURN_PASSWORD,
2965
3006
  },
2966
3007
  turnDiscoverySkippedReason: undefined,
2967
3008
  });
3009
+
3010
+ const mediaConnectionError = new Error('fake error');
3011
+
2968
3012
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon
2969
3013
  .stub()
2970
3014
  .onFirstCall()
2971
- .rejects()
3015
+ .rejects(mediaConnectionError)
2972
3016
  .onSecondCall()
2973
3017
  .resolves();
2974
3018
 
@@ -3037,10 +3081,14 @@ describe('plugin-meetings', () => {
3037
3081
  },
3038
3082
  options: {
3039
3083
  meetingId: meeting.id,
3084
+ rawError: mediaConnectionError,
3040
3085
  },
3041
3086
  });
3042
3087
  assert.calledWith(webex.internal.newMetrics.submitClientEvent.thirdCall, {
3043
3088
  name: 'client.media-engine.ready',
3089
+ payload: {
3090
+ ipVersion: 'IPv6',
3091
+ },
3044
3092
  options: {
3045
3093
  meetingId: meeting.id,
3046
3094
  },
@@ -3097,11 +3145,14 @@ describe('plugin-meetings', () => {
3097
3145
  locus_id: meeting.locusUrl.split('/').pop(),
3098
3146
  connectionType: 'udp',
3099
3147
  selectedCandidatePairChanges: 2,
3148
+ ipVersion: 'IPv6',
3100
3149
  numTransports: 1,
3101
3150
  isMultistream: false,
3102
3151
  retriedWithTurnServer: true,
3103
3152
  isJoinWithMediaRetry: false,
3104
3153
  iceCandidatesCount: 0,
3154
+ isSubnetReachable: null,
3155
+ selectedCluster: null,
3105
3156
  },
3106
3157
  ]);
3107
3158
  meeting.roap.doTurnDiscovery;
@@ -3136,7 +3187,7 @@ describe('plugin-meetings', () => {
3136
3187
  .onSecondCall()
3137
3188
  .returns({
3138
3189
  turnServerInfo: {
3139
- url: FAKE_TURN_URL,
3190
+ urls: [FAKE_TURN_URL],
3140
3191
  username: FAKE_TURN_USER,
3141
3192
  password: FAKE_TURN_PASSWORD,
3142
3193
  },
@@ -3188,7 +3239,7 @@ describe('plugin-meetings', () => {
3188
3239
  .onSecondCall()
3189
3240
  .returns({
3190
3241
  turnServerInfo: {
3191
- url: FAKE_TURN_URL,
3242
+ urls: [FAKE_TURN_URL],
3192
3243
  username: FAKE_TURN_USER,
3193
3244
  password: FAKE_TURN_PASSWORD,
3194
3245
  },
@@ -3230,7 +3281,13 @@ describe('plugin-meetings', () => {
3230
3281
  someReachabilityMetric2: 'some value2',
3231
3282
  }),
3232
3283
  stopReachability: sinon.stub(),
3284
+ isSubnetReachable: sinon.stub().returns(true),
3233
3285
  };
3286
+ meeting.mediaConnections = [
3287
+ {
3288
+ mediaAgentCluster: 'some.cluster',
3289
+ }
3290
+ ]
3234
3291
  meeting.iceCandidatesCount = 3;
3235
3292
  meeting.iceCandidateErrors.set('701_error', 3);
3236
3293
  meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
@@ -3248,6 +3305,7 @@ describe('plugin-meetings', () => {
3248
3305
  locus_id: meeting.locusUrl.split('/').pop(),
3249
3306
  connectionType: 'udp',
3250
3307
  selectedCandidatePairChanges: 2,
3308
+ ipVersion: 'IPv6',
3251
3309
  numTransports: 1,
3252
3310
  isMultistream: false,
3253
3311
  retriedWithTurnServer: false,
@@ -3257,6 +3315,8 @@ describe('plugin-meetings', () => {
3257
3315
  iceCandidatesCount: 3,
3258
3316
  '701_error': 3,
3259
3317
  '701_turn_host_lookup_received_error': 1,
3318
+ isSubnetReachable: null,
3319
+ selectedCluster: 'some.cluster',
3260
3320
  }
3261
3321
  );
3262
3322
 
@@ -3319,6 +3379,8 @@ describe('plugin-meetings', () => {
3319
3379
  iceConnectionState: 'unknown',
3320
3380
  selectedCandidatePairChanges: 2,
3321
3381
  numTransports: 1,
3382
+ isSubnetReachable: null,
3383
+ selectedCluster: null,
3322
3384
  iceCandidatesCount: 0,
3323
3385
  }
3324
3386
  );
@@ -3380,6 +3442,120 @@ describe('plugin-meetings', () => {
3380
3442
  numTransports: 1,
3381
3443
  '701_error': 2,
3382
3444
  '701_turn_host_lookup_received_error': 1,
3445
+ isSubnetReachable: null,
3446
+ selectedCluster: null,
3447
+ iceCandidatesCount: 0,
3448
+ }
3449
+ );
3450
+
3451
+ assert.isOk(errorThrown);
3452
+ });
3453
+
3454
+ it('should send valid isSubnetReachability if media connection success', async () => {
3455
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3456
+ turnServerInfo: undefined,
3457
+ turnDiscoverySkippedReason: undefined,
3458
+ });
3459
+ meeting.meetingState = 'ACTIVE';
3460
+ meeting.mediaProperties.waitForMediaConnectionConnected.resolves();
3461
+ meeting.webex.meetings.reachability = {
3462
+ getReachabilityMetrics: sinon.stub().resolves({
3463
+ reachability_public_udp_success: 5,
3464
+ }),
3465
+ stopReachability: sinon.stub(),
3466
+ isSubnetReachable: sinon.stub().returns(false),
3467
+ };
3468
+
3469
+ const forceRtcMetricsSend = sinon.stub().resolves();
3470
+ const closeMediaConnectionStub = sinon.stub();
3471
+ Media.createMediaConnection = sinon.stub().returns({
3472
+ close: closeMediaConnectionStub,
3473
+ forceRtcMetricsSend,
3474
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3475
+ initiateOffer: sinon.stub().resolves({}),
3476
+ on: sinon.stub(),
3477
+ });
3478
+
3479
+ await meeting.addMedia({
3480
+ mediaSettings: {},
3481
+ });
3482
+
3483
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
3484
+ correlation_id: meeting.correlationId,
3485
+ locus_id: meeting.locusUrl.split('/').pop(),
3486
+ connectionType: 'udp',
3487
+ ipVersion: 'IPv6',
3488
+ selectedCandidatePairChanges: 2,
3489
+ numTransports: 1,
3490
+ isMultistream: false,
3491
+ retriedWithTurnServer: false,
3492
+ isJoinWithMediaRetry: false,
3493
+ iceCandidatesCount: 0,
3494
+ reachability_public_udp_success: 5,
3495
+ isSubnetReachable: false,
3496
+ selectedCluster: null,
3497
+ });
3498
+ });
3499
+
3500
+ it('should send valid isSubnetReachability if media connection fails', async () => {
3501
+ let errorThrown = undefined;
3502
+
3503
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3504
+ turnServerInfo: undefined,
3505
+ turnDiscoverySkippedReason: undefined,
3506
+ });
3507
+ meeting.meetingState = 'ACTIVE';
3508
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
3509
+ meeting.webex.meetings.reachability = {
3510
+ getReachabilityMetrics: sinon.stub().resolves({
3511
+ reachability_public_udp_success: 5,
3512
+ }),
3513
+ stopReachability: sinon.stub(),
3514
+ isSubnetReachable: sinon.stub().returns(true),
3515
+ };
3516
+
3517
+ const forceRtcMetricsSend = sinon.stub().resolves();
3518
+ const closeMediaConnectionStub = sinon.stub();
3519
+ Media.createMediaConnection = sinon.stub().returns({
3520
+ close: closeMediaConnectionStub,
3521
+ forceRtcMetricsSend,
3522
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3523
+ initiateOffer: sinon.stub().resolves({}),
3524
+ on: sinon.stub(),
3525
+ });
3526
+
3527
+ await meeting
3528
+ .addMedia({
3529
+ mediaSettings: {},
3530
+ })
3531
+ .catch((err) => {
3532
+ errorThrown = err;
3533
+ assert.instanceOf(err, AddMediaFailed);
3534
+ });
3535
+
3536
+ // Check that the only metric sent is ADD_MEDIA_FAILURE
3537
+ assert.calledOnceWithExactly(
3538
+ Metrics.sendBehavioralMetric,
3539
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
3540
+ {
3541
+ correlation_id: meeting.correlationId,
3542
+ locus_id: meeting.locusUrl.split('/').pop(),
3543
+ reason: errorThrown.message,
3544
+ stack: errorThrown.stack,
3545
+ code: errorThrown.code,
3546
+ turnDiscoverySkippedReason: undefined,
3547
+ turnServerUsed: true,
3548
+ retriedWithTurnServer: false,
3549
+ isMultistream: false,
3550
+ isJoinWithMediaRetry: false,
3551
+ signalingState: 'unknown',
3552
+ connectionState: 'unknown',
3553
+ iceConnectionState: 'unknown',
3554
+ selectedCandidatePairChanges: 2,
3555
+ numTransports: 1,
3556
+ reachability_public_udp_success: 5,
3557
+ isSubnetReachable: true,
3558
+ selectedCluster: null,
3383
3559
  iceCandidatesCount: 0,
3384
3560
  }
3385
3561
  );
@@ -3399,6 +3575,8 @@ describe('plugin-meetings', () => {
3399
3575
  meeting.config.stats.enableStatsAnalyzer = true;
3400
3576
 
3401
3577
  statsAnalyzerStub = new EventsScope();
3578
+ statsAnalyzerStub.getNetworkType = sinon.stub().returns('wifi');
3579
+
3402
3580
  // mock the StatsAnalyzer constructor
3403
3581
  sinon.stub(InternalMediaCoreModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
3404
3582
 
@@ -3439,6 +3617,40 @@ describe('plugin-meetings', () => {
3439
3617
  });
3440
3618
  });
3441
3619
 
3620
+ it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and does not send metric because we already have', async () => {
3621
+ meeting.shareCAEventSentStatus = {
3622
+ transmitStart: true,
3623
+ transmitStop: false,
3624
+ receiveStart: false,
3625
+ receiveStop: false,
3626
+ };
3627
+ statsAnalyzerStub.emit(
3628
+ {file: 'test', function: 'test'},
3629
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED,
3630
+ {mediaType: 'share'}
3631
+ );
3632
+
3633
+ assert.calledWith(
3634
+ TriggerProxy.trigger,
3635
+ sinon.match.instanceOf(Meeting),
3636
+ {
3637
+ file: 'meeting/index',
3638
+ function: 'addMedia',
3639
+ },
3640
+ EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED,
3641
+ {
3642
+ mediaType: 'share',
3643
+ }
3644
+ );
3645
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3646
+ name: 'client.media.tx.start',
3647
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3648
+ options: {
3649
+ meetingId: meeting.id,
3650
+ },
3651
+ });
3652
+ });
3653
+
3442
3654
  it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => {
3443
3655
  statsAnalyzerStub.emit(
3444
3656
  {file: 'test', function: 'test'},
@@ -3455,6 +3667,28 @@ describe('plugin-meetings', () => {
3455
3667
  });
3456
3668
  });
3457
3669
 
3670
+ it('LOCAL_MEDIA_STOPPED does not send metric because we already have', async () => {
3671
+ meeting.shareCAEventSentStatus = {
3672
+ transmitStart: false,
3673
+ transmitStop: true,
3674
+ receiveStart: false,
3675
+ receiveStop: false,
3676
+ };
3677
+ statsAnalyzerStub.emit(
3678
+ {file: 'test', function: 'test'},
3679
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED,
3680
+ {mediaType: 'share'}
3681
+ );
3682
+
3683
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3684
+ name: 'client.media.tx.stop',
3685
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3686
+ options: {
3687
+ meetingId: meeting.id,
3688
+ },
3689
+ });
3690
+ });
3691
+
3458
3692
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => {
3459
3693
  statsAnalyzerStub.emit(
3460
3694
  {file: 'test', function: 'test'},
@@ -3535,6 +3769,47 @@ describe('plugin-meetings', () => {
3535
3769
  });
3536
3770
  });
3537
3771
 
3772
+ it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and does not send metric because we already have', async () => {
3773
+ meeting.shareCAEventSentStatus = {
3774
+ transmitStart: false,
3775
+ transmitStop: false,
3776
+ receiveStart: true,
3777
+ receiveStop: false,
3778
+ };
3779
+ statsAnalyzerStub.emit(
3780
+ {file: 'test', function: 'test'},
3781
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3782
+ {mediaType: 'share'}
3783
+ );
3784
+
3785
+ assert.calledWith(
3786
+ TriggerProxy.trigger,
3787
+ sinon.match.instanceOf(Meeting),
3788
+ {
3789
+ file: 'meeting/index',
3790
+ function: 'addMedia',
3791
+ },
3792
+ EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
3793
+ {
3794
+ mediaType: 'share',
3795
+ }
3796
+ );
3797
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3798
+ name: 'client.media.render.start',
3799
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3800
+ options: {
3801
+ meetingId: meeting.id,
3802
+ },
3803
+ });
3804
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3805
+ name: 'client.media.rx.start',
3806
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3807
+ options: {
3808
+ meetingId: meeting.id,
3809
+ },
3810
+ });
3811
+ });
3812
+
3538
3813
  it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
3539
3814
  statsAnalyzerStub.emit(
3540
3815
  {file: 'test', function: 'test'},
@@ -3559,6 +3834,34 @@ describe('plugin-meetings', () => {
3559
3834
  });
3560
3835
  });
3561
3836
 
3837
+ it('REMOTE_MEDIA_STOPPED does not send metric because we already have', async () => {
3838
+ meeting.shareCAEventSentStatus = {
3839
+ transmitStart: false,
3840
+ transmitStop: false,
3841
+ receiveStart: true,
3842
+ receiveStop: true,
3843
+ };
3844
+ statsAnalyzerStub.emit(
3845
+ {file: 'test', function: 'test'},
3846
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3847
+ {mediaType: 'share'}
3848
+ );
3849
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3850
+ name: 'client.media.render.stop',
3851
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3852
+ options: {
3853
+ meetingId: meeting.id,
3854
+ },
3855
+ });
3856
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3857
+ name: 'client.media.rx.stop',
3858
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3859
+ options: {
3860
+ meetingId: meeting.id,
3861
+ },
3862
+ });
3863
+ });
3864
+
3562
3865
  it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
3563
3866
  let fakeMembersCollection = {
3564
3867
  members: {
@@ -3568,7 +3871,7 @@ describe('plugin-meetings', () => {
3568
3871
  },
3569
3872
  };
3570
3873
  sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
3571
- const fakeData = {intervalMetadata: {}, networkType: 'wifi'};
3874
+ const fakeData = {intervalMetadata: {}};
3572
3875
 
3573
3876
  statsAnalyzerStub.emit(
3574
3877
  {file: 'test', function: 'test'},
@@ -3609,7 +3912,7 @@ describe('plugin-meetings', () => {
3609
3912
  });
3610
3913
 
3611
3914
  it('calls submitMQE correctly', async () => {
3612
- const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
3915
+ const fakeData = {intervalMetadata: {bla: 'bla'}};
3613
3916
 
3614
3917
  statsAnalyzerStub.emit(
3615
3918
  {file: 'test', function: 'test'},
@@ -3640,7 +3943,7 @@ describe('plugin-meetings', () => {
3640
3943
 
3641
3944
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3642
3945
  turnServerInfo: {
3643
- url: FAKE_TURN_URL,
3946
+ urls: [FAKE_TURN_URL],
3644
3947
  username: FAKE_TURN_USER,
3645
3948
  password: FAKE_TURN_PASSWORD,
3646
3949
  },
@@ -3666,7 +3969,7 @@ describe('plugin-meetings', () => {
3666
3969
  meeting.id,
3667
3970
  sinon.match({
3668
3971
  turnServerInfo: {
3669
- url: FAKE_TURN_URL,
3972
+ urls: [FAKE_TURN_URL],
3670
3973
  username: FAKE_TURN_USER,
3671
3974
  password: FAKE_TURN_PASSWORD,
3672
3975
  },
@@ -3817,6 +4120,9 @@ describe('plugin-meetings', () => {
3817
4120
  },
3818
4121
  options: {
3819
4122
  meetingId: meeting.id,
4123
+ rawError: {
4124
+ iceConnected: false,
4125
+ },
3820
4126
  },
3821
4127
  },
3822
4128
  ]);
@@ -3887,9 +4193,29 @@ describe('plugin-meetings', () => {
3887
4193
  } catch (err) {
3888
4194
  assert.instanceOf(err, Error);
3889
4195
  assert.equal(err.message, 'setBrb failed');
3890
- assert.isRejected((Promise.reject()));
4196
+ assert.isRejected(Promise.reject());
3891
4197
  }
3892
4198
  });
4199
+
4200
+ it('updates remote mute state when brb is enabled', async () => {
4201
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4202
+
4203
+ await meeting.beRightBack(true);
4204
+
4205
+ sinon.assert.calledOnceWithExactly(
4206
+ meeting.audio.handleServerRemoteMuteUpdate,
4207
+ meeting,
4208
+ true
4209
+ );
4210
+ });
4211
+
4212
+ it('does not update remote mute state when brb is disabled', async () => {
4213
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4214
+
4215
+ await meeting.beRightBack(false);
4216
+
4217
+ assert.notCalled(meeting.audio.handleServerRemoteMuteUpdate);
4218
+ });
3893
4219
  });
3894
4220
  });
3895
4221
 
@@ -3943,7 +4269,10 @@ describe('plugin-meetings', () => {
3943
4269
  .resolves({id: 'fake clientMediaPreferences'});
3944
4270
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3945
4271
  turnServerInfo: {
3946
- url: 'turns:turn-server-url:443?transport=tcp',
4272
+ urls: [
4273
+ 'turns:turn-server-url1:443?transport=tcp',
4274
+ 'turns:turn-server-url2:443?transport=tcp',
4275
+ ],
3947
4276
  username: 'turn user',
3948
4277
  password: 'turn password',
3949
4278
  },
@@ -3961,12 +4290,10 @@ describe('plugin-meetings', () => {
3961
4290
  expectedMediaConnectionConfig = {
3962
4291
  iceServers: [
3963
4292
  {
3964
- urls: 'turn:turn-server-url:5004?transport=tcp',
3965
- username: 'turn user',
3966
- credential: 'turn password',
3967
- },
3968
- {
3969
- urls: 'turns:turn-server-url:443?transport=tcp',
4293
+ urls: [
4294
+ 'turns:turn-server-url1:443?transport=tcp',
4295
+ 'turns:turn-server-url2:443?transport=tcp',
4296
+ ],
3970
4297
  username: 'turn user',
3971
4298
  credential: 'turn password',
3972
4299
  },
@@ -4048,9 +4375,11 @@ describe('plugin-meetings', () => {
4048
4375
  .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
4049
4376
  .returns(fakeMultistreamRoapMediaConnection);
4050
4377
 
4051
- locusMediaRequestStub = sinon
4052
- .stub(WebexPlugin.prototype, 'request')
4053
- .resolves({body: {locus: {fullState: {}}}, upload: sinon.match.instanceOf(EventEmitter), download: sinon.match.instanceOf(EventEmitter)});
4378
+ locusMediaRequestStub = sinon.stub(WebexPlugin.prototype, 'request').resolves({
4379
+ body: {locus: {fullState: {}}},
4380
+ upload: sinon.match.instanceOf(EventEmitter),
4381
+ download: sinon.match.instanceOf(EventEmitter),
4382
+ });
4054
4383
 
4055
4384
  // setup some things and mocks so that the call to join() works
4056
4385
  // (we need to call join() because it creates the LocusMediaRequest instance
@@ -5234,7 +5563,10 @@ describe('plugin-meetings', () => {
5234
5563
  // and check that when we fallback to transcoded we still do another TURN discovery
5235
5564
  await runCheck(
5236
5565
  {
5237
- url: 'turns:turn-server-url:443?transport=tcp',
5566
+ urls: [
5567
+ 'turns:turn-server-url1:443?transport=tcp',
5568
+ 'turns:turn-server-url2:443?transport=tcp',
5569
+ ],
5238
5570
  username: 'turn user',
5239
5571
  password: 'turn password',
5240
5572
  },
@@ -5248,7 +5580,10 @@ describe('plugin-meetings', () => {
5248
5580
  // but doing it just for completeness
5249
5581
  await runCheck(
5250
5582
  {
5251
- url: 'turns:turn-server-url:443?transport=tcp',
5583
+ urls: [
5584
+ 'turns:turn-server-url1:443?transport=tcp',
5585
+ 'turns:turn-server-url2:443?transport=tcp',
5586
+ ],
5252
5587
  username: 'turn user',
5253
5588
  password: 'turn password',
5254
5589
  },
@@ -7530,6 +7865,27 @@ describe('plugin-meetings', () => {
7530
7865
  });
7531
7866
  });
7532
7867
 
7868
+ describe('#setIsoLocalClientMeetingJoinTime', () => {
7869
+ it('should fallback to system clock ISO string when given an undefined value', () => {
7870
+ const currentSystemTime = new Date().toISOString();
7871
+ meeting.isoLocalClientMeetingJoinTime = undefined;
7872
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7873
+ });
7874
+
7875
+ it('should fallback to system clock ISO string when given an invalid value', () => {
7876
+ const currentSystemTime = new Date().toISOString();
7877
+ meeting.isoLocalClientMeetingJoinTime = 'invalid-date';
7878
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7879
+ });
7880
+
7881
+ it('should set the isoLocalClientMeetingJoinTime correctly for a valid date string', () => {
7882
+ const validDateString = 'Tue, 01 Apr 2025 13:00:36 GMT';
7883
+ const expectedISOString = new Date(validDateString).toISOString();
7884
+ meeting.isoLocalClientMeetingJoinTime = validDateString;
7885
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, expectedISOString);
7886
+ });
7887
+ });
7888
+
7533
7889
  describe('#updateCallStateForMetrics', () => {
7534
7890
  it('should update the callState, overriding existing values', () => {
7535
7891
  assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''});
@@ -7611,6 +7967,12 @@ describe('plugin-meetings', () => {
7611
7967
  meeting.audio = {handleLocalStreamChange: sinon.stub()};
7612
7968
  meeting.video = {handleLocalStreamChange: sinon.stub()};
7613
7969
  meeting.statsAnalyzer = {updateMediaStatus: sinon.stub()};
7970
+ meeting.shareCAEventSentStatus = {
7971
+ transmitStart: false,
7972
+ transmitStop: false,
7973
+ receiveStart: false,
7974
+ receiveStop: false,
7975
+ };
7614
7976
  fakeMultistreamRoapMediaConnection = {
7615
7977
  createSendSlot: () => {
7616
7978
  return {
@@ -7678,6 +8040,9 @@ describe('plugin-meetings', () => {
7678
8040
  });
7679
8041
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7680
8042
 
8043
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
8044
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
8045
+
7681
8046
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7682
8047
  expected: {sendShare: true},
7683
8048
  });
@@ -7698,18 +8063,23 @@ describe('plugin-meetings', () => {
7698
8063
  assert.equal(meeting.mediaProperties.shareAudioStream, stream);
7699
8064
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7700
8065
 
8066
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
8067
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
8068
+
7701
8069
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7702
8070
  expected: {sendShare: true},
7703
8071
  });
7704
8072
  };
7705
8073
 
7706
8074
  it('requests screen share floor and publishes the screen share video stream', async () => {
8075
+ meeting.shareCAEventSentStatus.transmitStart = true;
7707
8076
  await meeting.publishStreams({screenShare: {video: videoShareStream}});
7708
8077
 
7709
8078
  checkScreenShareVideoPublished(videoShareStream);
7710
8079
  });
7711
8080
 
7712
8081
  it('requests screen share floor and publishes the screen share audio stream', async () => {
8082
+ meeting.shareCAEventSentStatus.transmitStart = true;
7713
8083
  await meeting.publishStreams({screenShare: {audio: audioShareStream}});
7714
8084
 
7715
8085
  checkScreenShareAudioPublished(audioShareStream);
@@ -8596,13 +8966,19 @@ describe('plugin-meetings', () => {
8596
8966
  const fakeErrorMessage = 'test error';
8597
8967
  const fakeRootCauseName = 'root cause name';
8598
8968
  const fakeErrorName = 'test error name';
8969
+ let clock;
8599
8970
 
8600
8971
  beforeEach(() => {
8972
+ clock = sinon.useFakeTimers();
8601
8973
  meeting.setupMediaConnectionListeners();
8602
8974
  webex.internal.newMetrics.submitClientEvent.resetHistory();
8603
8975
  Metrics.sendBehavioralMetric.resetHistory();
8604
8976
  });
8605
8977
 
8978
+ afterEach(() => {
8979
+ clock.restore();
8980
+ });
8981
+
8606
8982
  const checkMetricSent = (event, error) => {
8607
8983
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
8608
8984
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -8671,6 +9047,13 @@ describe('plugin-meetings', () => {
8671
9047
  });
8672
9048
 
8673
9049
  it('should send metrics for SdpAnswerHandlingError error', () => {
9050
+ meeting.sdpResponseTimer = '1234';
9051
+ meeting.deferSDPAnswer = {
9052
+ reject: sinon.stub(),
9053
+ };
9054
+
9055
+ const clearTimeoutSpy = sinon.spy(clock, 'clearTimeout');
9056
+
8674
9057
  const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
8675
9058
  name: fakeErrorName,
8676
9059
  cause: {name: fakeRootCauseName},
@@ -8685,6 +9068,8 @@ describe('plugin-meetings', () => {
8685
9068
  fakeErrorMessage,
8686
9069
  fakeRootCauseName
8687
9070
  );
9071
+ assert.calledOnce(meeting.deferSDPAnswer.reject);
9072
+ assert.calledOnce(clearTimeoutSpy);
8688
9073
  });
8689
9074
 
8690
9075
  it('should send metrics for SdpError error', () => {
@@ -9590,6 +9975,42 @@ describe('plugin-meetings', () => {
9590
9975
  );
9591
9976
  });
9592
9977
 
9978
+ it('listens to CONTROLS_ANNOTATION_CHANGED', async () => {
9979
+ const state = {example: 'value'};
9980
+
9981
+ await meeting.locusInfo.emitScoped(
9982
+ {function: 'test', file: 'test'},
9983
+ LOCUSINFO.EVENTS.CONTROLS_ANNOTATION_CHANGED,
9984
+ {state}
9985
+ );
9986
+
9987
+ assert.calledWith(
9988
+ TriggerProxy.trigger,
9989
+ meeting,
9990
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
9991
+ EVENT_TRIGGERS.MEETING_CONTROLS_ANNOTATION_UPDATED,
9992
+ {state}
9993
+ );
9994
+ });
9995
+
9996
+ it('listens to CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED', async () => {
9997
+ const state = {example: 'value'};
9998
+
9999
+ await meeting.locusInfo.emitScoped(
10000
+ {function: 'test', file: 'test'},
10001
+ LOCUSINFO.EVENTS.CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED,
10002
+ {state}
10003
+ );
10004
+
10005
+ assert.calledWith(
10006
+ TriggerProxy.trigger,
10007
+ meeting,
10008
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
10009
+ EVENT_TRIGGERS.MEETING_CONTROLS_REMOTE_DESKTOP_CONTROL_UPDATED,
10010
+ {state}
10011
+ );
10012
+ });
10013
+
9593
10014
  it('listens to the locus interpretation update event', () => {
9594
10015
  const interpretation = {
9595
10016
  siLanguages: [{languageCode: 20, languageName: 'en'}],
@@ -10485,9 +10906,11 @@ describe('plugin-meetings', () => {
10485
10906
  let canUserLowerSomeoneElsesHandSpy;
10486
10907
  let waitingForOthersToJoinSpy;
10487
10908
  let canSendReactionsSpy;
10909
+ let requiresPostMeetingDataConsentPromptSpy;
10488
10910
  let canUserRenameSelfAndObservedSpy;
10489
10911
  let canUserRenameOthersSpy;
10490
10912
  let canShareWhiteBoardSpy;
10913
+ let canMoveToLobbySpy;
10491
10914
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
10492
10915
 
10493
10916
  beforeEach(() => {
@@ -10512,8 +10935,13 @@ describe('plugin-meetings', () => {
10512
10935
  waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
10513
10936
  canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
10514
10937
  canUserRenameSelfAndObservedSpy = sinon.spy(MeetingUtil, 'canUserRenameSelfAndObserved');
10938
+ requiresPostMeetingDataConsentPromptSpy = sinon.spy(
10939
+ MeetingUtil,
10940
+ 'requiresPostMeetingDataConsentPrompt'
10941
+ );
10515
10942
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
10516
10943
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
10944
+ canMoveToLobbySpy = sinon.spy(MeetingUtil, 'canMoveToLobby');
10517
10945
  });
10518
10946
 
10519
10947
  afterEach(() => {
@@ -10611,6 +11039,16 @@ describe('plugin-meetings', () => {
10611
11039
  requiredDisplayHints: [],
10612
11040
  requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
10613
11041
  },
11042
+ {
11043
+ actionName: 'canRealtimeCloseCaption',
11044
+ requiredDisplayHints: [],
11045
+ requiredPolicies: [SELF_POLICY.SUPPORT_REALTIME_CLOSE_CAPTION],
11046
+ },
11047
+ {
11048
+ actionName: 'canRealtimeCloseCaptionManual',
11049
+ requiredDisplayHints: [],
11050
+ requiredPolicies: [SELF_POLICY.SUPPORT_REALTIME_CLOSE_CAPTION_MANUAL],
11051
+ },
10614
11052
  {
10615
11053
  actionName: 'canChat',
10616
11054
  requiredDisplayHints: [],
@@ -10640,6 +11078,11 @@ describe('plugin-meetings', () => {
10640
11078
  requiredDisplayHints: [],
10641
11079
  requiredPolicies: [SELF_POLICY.SUPPORT_POLLING_AND_QA],
10642
11080
  },
11081
+ {
11082
+ actionName: 'canShareWhiteBoard',
11083
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_WHITEBOARD],
11084
+ requiredPolicies: [SELF_POLICY.SUPPORT_WHITEBOARD],
11085
+ },
10643
11086
  ],
10644
11087
  ({
10645
11088
  actionName,
@@ -11047,8 +11490,10 @@ describe('plugin-meetings', () => {
11047
11490
  assert.calledWith(waitingForOthersToJoinSpy, userDisplayHints);
11048
11491
  assert.calledWith(canSendReactionsSpy, null, userDisplayHints);
11049
11492
  assert.calledWith(canUserRenameSelfAndObservedSpy, userDisplayHints);
11493
+ assert.calledWith(requiresPostMeetingDataConsentPromptSpy, userDisplayHints);
11050
11494
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11051
- assert.calledWith(canShareWhiteBoardSpy, userDisplayHints);
11495
+ assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11496
+ assert.calledWith(canMoveToLobbySpy, userDisplayHints);
11052
11497
 
11053
11498
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11054
11499
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -11142,6 +11587,22 @@ describe('plugin-meetings', () => {
11142
11587
  requiredPolicies: [SELF_POLICY.SUPPORT_VOIP],
11143
11588
  policies: selfUserPolicies,
11144
11589
  });
11590
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11591
+ requiredHints: [DISPLAY_HINTS.ENABLE_ANNOTATION_MEETING_OPTION],
11592
+ displayHints: userDisplayHints,
11593
+ });
11594
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11595
+ requiredHints: [DISPLAY_HINTS.DISABLE_ANNOTATION_MEETING_OPTION],
11596
+ displayHints: userDisplayHints,
11597
+ });
11598
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11599
+ requiredHints: [DISPLAY_HINTS.ENABLE_RDC_MEETING_OPTION],
11600
+ displayHints: userDisplayHints,
11601
+ });
11602
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11603
+ requiredHints: [DISPLAY_HINTS.DISABLE_RDC_MEETING_OPTION],
11604
+ displayHints: userDisplayHints,
11605
+ });
11145
11606
 
11146
11607
  assert.calledWith(
11147
11608
  TriggerProxy.trigger,
@@ -11988,6 +12449,8 @@ describe('plugin-meetings', () => {
11988
12449
  // Set the webinar attendee flag
11989
12450
  meeting.webinar = {selfIsAttendee: true};
11990
12451
  meeting.locusInfo.info.isWebinar = true;
12452
+ meeting.shareCAEventSentStatus.receiveStart = true;
12453
+ meeting.shareCAEventSentStatus.receiveStop = true;
11991
12454
 
11992
12455
  // Step 1: Start sharing whiteboard A
11993
12456
  const data1 = generateData(
@@ -12011,6 +12474,8 @@ describe('plugin-meetings', () => {
12011
12474
 
12012
12475
  // Specific assertions for webinar attendee status
12013
12476
  assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
12477
+ assert.equal(meeting.shareCAEventSentStatus.receiveStart, false);
12478
+ assert.equal(meeting.shareCAEventSentStatus.receiveStop, false);
12014
12479
  });
12015
12480
  });
12016
12481
 
@@ -12666,6 +13131,31 @@ describe('plugin-meetings', () => {
12666
13131
  });
12667
13132
  });
12668
13133
  });
13134
+
13135
+ describe('handleShareVideoStreamMuteStateChange', () => {
13136
+ it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
13137
+ meeting.isMultistream = true;
13138
+ meeting.statsAnalyzer = {shareVideoEncoderImplementation: 'OpenH264'};
13139
+ meeting.mediaProperties.shareVideoStream = {
13140
+ getSettings: sinon.stub().returns({displaySurface: 'monitor', frameRate: 30}),
13141
+ };
13142
+
13143
+ meeting.handleShareVideoStreamMuteStateChange(true);
13144
+
13145
+ assert.calledOnceWithExactly(
13146
+ Metrics.sendBehavioralMetric,
13147
+ BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE,
13148
+ {
13149
+ correlationId: meeting.correlationId,
13150
+ muted: true,
13151
+ encoderImplementation: 'OpenH264',
13152
+ displaySurface: 'monitor',
13153
+ isMultistream: true,
13154
+ frameRate: 30,
13155
+ }
13156
+ );
13157
+ });
13158
+ });
12669
13159
  });
12670
13160
 
12671
13161
  describe('#startKeepAlive', () => {
@@ -12833,6 +13323,38 @@ describe('plugin-meetings', () => {
12833
13323
  });
12834
13324
  });
12835
13325
 
13326
+ describe('#setPostMeetingDataConsent', () => {
13327
+ it('should have #setPostMeetingDataConsent', () => {
13328
+ assert.exists(meeting.setPostMeetingDataConsent);
13329
+ });
13330
+
13331
+ beforeEach(() => {
13332
+ meeting.meetingRequest.setPostMeetingDataConsent = sinon
13333
+ .stub()
13334
+ .returns(Promise.resolve());
13335
+ });
13336
+
13337
+ [true, false].forEach((accept) => {
13338
+ it(`should send consent with ${accept}`, async () => {
13339
+ const id = uuidv4();
13340
+ meeting.locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`;
13341
+ meeting.deviceUrl = `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`;
13342
+ meeting.members.selfId = id;
13343
+
13344
+ const consentPromise = meeting.setPostMeetingDataConsent(accept);
13345
+
13346
+ assert.exists(consentPromise.then);
13347
+ await consentPromise;
13348
+ assert.calledOnceWithExactly(meeting.meetingRequest.setPostMeetingDataConsent, {
13349
+ locusUrl: `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`,
13350
+ postMeetingDataConsent: accept,
13351
+ selfId: id,
13352
+ deviceUrl: `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`,
13353
+ });
13354
+ });
13355
+ });
13356
+ });
13357
+
12836
13358
  describe('#sendReaction', () => {
12837
13359
  it('should have #sendReaction', () => {
12838
13360
  assert.exists(meeting.sendReaction);