@webex/plugin-meetings 3.8.0-next.7 → 3.8.0-next.70

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 (166) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/common/errors/webex-errors.js +12 -2
  4. package/dist/common/errors/webex-errors.js.map +1 -1
  5. package/dist/config.js +4 -1
  6. package/dist/config.js.map +1 -1
  7. package/dist/constants.js +17 -121
  8. package/dist/constants.js.map +1 -1
  9. package/dist/controls-options-manager/enums.js +2 -0
  10. package/dist/controls-options-manager/enums.js.map +1 -1
  11. package/dist/controls-options-manager/types.js.map +1 -1
  12. package/dist/controls-options-manager/util.js +52 -0
  13. package/dist/controls-options-manager/util.js.map +1 -1
  14. package/dist/interpretation/index.js +1 -1
  15. package/dist/interpretation/siLanguage.js +1 -1
  16. package/dist/locus-info/controlsUtils.js +28 -10
  17. package/dist/locus-info/controlsUtils.js.map +1 -1
  18. package/dist/locus-info/index.js +20 -1
  19. package/dist/locus-info/index.js.map +1 -1
  20. package/dist/locus-info/selfUtils.js +405 -418
  21. package/dist/locus-info/selfUtils.js.map +1 -1
  22. package/dist/media/index.js +14 -16
  23. package/dist/media/index.js.map +1 -1
  24. package/dist/media/properties.js +94 -6
  25. package/dist/media/properties.js.map +1 -1
  26. package/dist/meeting/brbState.js +6 -0
  27. package/dist/meeting/brbState.js.map +1 -1
  28. package/dist/meeting/in-meeting-actions.js +17 -1
  29. package/dist/meeting/in-meeting-actions.js.map +1 -1
  30. package/dist/meeting/index.js +541 -302
  31. package/dist/meeting/index.js.map +1 -1
  32. package/dist/meeting/locusMediaRequest.js +0 -17
  33. package/dist/meeting/locusMediaRequest.js.map +1 -1
  34. package/dist/meeting/muteState.js +0 -2
  35. package/dist/meeting/muteState.js.map +1 -1
  36. package/dist/meeting/request.js +30 -0
  37. package/dist/meeting/request.js.map +1 -1
  38. package/dist/meeting/request.type.js.map +1 -1
  39. package/dist/meeting/util.js +13 -2
  40. package/dist/meeting/util.js.map +1 -1
  41. package/dist/meeting-info/meeting-info-v2.js +359 -60
  42. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  43. package/dist/meetings/index.js +114 -1
  44. package/dist/meetings/index.js.map +1 -1
  45. package/dist/meetings/util.js +14 -0
  46. package/dist/meetings/util.js.map +1 -1
  47. package/dist/member/index.js +10 -0
  48. package/dist/member/index.js.map +1 -1
  49. package/dist/member/util.js +330 -353
  50. package/dist/member/util.js.map +1 -1
  51. package/dist/members/index.js +23 -0
  52. package/dist/members/index.js.map +1 -1
  53. package/dist/members/request.js +21 -0
  54. package/dist/members/request.js.map +1 -1
  55. package/dist/members/util.js +15 -0
  56. package/dist/members/util.js.map +1 -1
  57. package/dist/metrics/constants.js +9 -0
  58. package/dist/metrics/constants.js.map +1 -1
  59. package/dist/reachability/clusterReachability.js +63 -27
  60. package/dist/reachability/clusterReachability.js.map +1 -1
  61. package/dist/reachability/index.js +112 -47
  62. package/dist/reachability/index.js.map +1 -1
  63. package/dist/reachability/reachability.types.js +14 -0
  64. package/dist/reachability/reachability.types.js.map +1 -1
  65. package/dist/reachability/request.js +19 -3
  66. package/dist/reachability/request.js.map +1 -1
  67. package/dist/reconnection-manager/index.js +2 -2
  68. package/dist/reconnection-manager/index.js.map +1 -1
  69. package/dist/recording-controller/util.js +5 -5
  70. package/dist/recording-controller/util.js.map +1 -1
  71. package/dist/roap/index.js.map +1 -1
  72. package/dist/roap/turnDiscovery.js +45 -27
  73. package/dist/roap/turnDiscovery.js.map +1 -1
  74. package/dist/roap/types.js +17 -0
  75. package/dist/roap/types.js.map +1 -0
  76. package/dist/types/common/errors/webex-errors.d.ts +7 -1
  77. package/dist/types/config.d.ts +2 -0
  78. package/dist/types/constants.d.ts +12 -85
  79. package/dist/types/controls-options-manager/enums.d.ts +3 -1
  80. package/dist/types/controls-options-manager/types.d.ts +7 -1
  81. package/dist/types/locus-info/index.d.ts +1 -0
  82. package/dist/types/locus-info/selfUtils.d.ts +247 -1
  83. package/dist/types/media/properties.d.ts +15 -0
  84. package/dist/types/meeting/in-meeting-actions.d.ts +16 -0
  85. package/dist/types/meeting/index.d.ts +32 -1
  86. package/dist/types/meeting/muteState.d.ts +0 -1
  87. package/dist/types/meeting/request.d.ts +12 -1
  88. package/dist/types/meeting/request.type.d.ts +6 -0
  89. package/dist/types/meeting/util.d.ts +3 -1
  90. package/dist/types/meeting-info/meeting-info-v2.d.ts +80 -0
  91. package/dist/types/meetings/index.d.ts +48 -0
  92. package/dist/types/member/index.d.ts +1 -0
  93. package/dist/types/member/util.d.ts +159 -1
  94. package/dist/types/members/index.d.ts +8 -0
  95. package/dist/types/members/request.d.ts +19 -0
  96. package/dist/types/members/util.d.ts +13 -0
  97. package/dist/types/metrics/constants.d.ts +9 -0
  98. package/dist/types/reachability/clusterReachability.d.ts +15 -7
  99. package/dist/types/reachability/index.d.ts +10 -1
  100. package/dist/types/reachability/reachability.types.d.ts +5 -0
  101. package/dist/types/roap/index.d.ts +3 -2
  102. package/dist/types/roap/turnDiscovery.d.ts +5 -17
  103. package/dist/types/roap/types.d.ts +16 -0
  104. package/dist/webinar/index.js +1 -1
  105. package/package.json +24 -23
  106. package/src/common/errors/webex-errors.ts +8 -1
  107. package/src/config.ts +2 -0
  108. package/src/constants.ts +19 -90
  109. package/src/controls-options-manager/enums.ts +2 -0
  110. package/src/controls-options-manager/types.ts +11 -1
  111. package/src/controls-options-manager/util.ts +62 -0
  112. package/src/locus-info/controlsUtils.ts +44 -14
  113. package/src/locus-info/index.ts +23 -1
  114. package/src/locus-info/selfUtils.ts +451 -447
  115. package/src/media/index.ts +20 -21
  116. package/src/media/properties.ts +96 -0
  117. package/src/meeting/brbState.ts +7 -0
  118. package/src/meeting/in-meeting-actions.ts +32 -0
  119. package/src/meeting/index.ts +346 -93
  120. package/src/meeting/locusMediaRequest.ts +0 -18
  121. package/src/meeting/muteState.ts +0 -2
  122. package/src/meeting/request.ts +36 -1
  123. package/src/meeting/request.type.ts +7 -0
  124. package/src/meeting/util.ts +11 -2
  125. package/src/meeting-info/meeting-info-v2.ts +247 -6
  126. package/src/meetings/index.ts +128 -1
  127. package/src/meetings/util.ts +18 -0
  128. package/src/member/index.ts +13 -2
  129. package/src/member/util.ts +351 -348
  130. package/src/members/index.ts +25 -0
  131. package/src/members/request.ts +26 -0
  132. package/src/members/util.ts +16 -0
  133. package/src/metrics/constants.ts +9 -0
  134. package/src/reachability/clusterReachability.ts +73 -26
  135. package/src/reachability/index.ts +70 -1
  136. package/src/reachability/reachability.types.ts +6 -0
  137. package/src/reachability/request.ts +7 -0
  138. package/src/reconnection-manager/index.ts +2 -2
  139. package/src/recording-controller/util.ts +17 -13
  140. package/src/roap/index.ts +3 -7
  141. package/src/roap/turnDiscovery.ts +34 -39
  142. package/src/roap/types.ts +23 -0
  143. package/test/unit/spec/controls-options-manager/util.js +120 -0
  144. package/test/unit/spec/locus-info/controlsUtils.js +103 -9
  145. package/test/unit/spec/locus-info/index.js +28 -0
  146. package/test/unit/spec/media/index.ts +98 -16
  147. package/test/unit/spec/media/properties.ts +130 -0
  148. package/test/unit/spec/meeting/brbState.ts +19 -0
  149. package/test/unit/spec/meeting/in-meeting-actions.ts +19 -4
  150. package/test/unit/spec/meeting/index.js +524 -35
  151. package/test/unit/spec/meeting/locusMediaRequest.ts +0 -30
  152. package/test/unit/spec/meeting/muteState.js +0 -2
  153. package/test/unit/spec/meeting/request.js +32 -1
  154. package/test/unit/spec/meeting/utils.js +119 -18
  155. package/test/unit/spec/meeting-info/meetinginfov2.js +443 -114
  156. package/test/unit/spec/meetings/index.js +133 -2
  157. package/test/unit/spec/member/index.js +7 -0
  158. package/test/unit/spec/member/util.js +24 -0
  159. package/test/unit/spec/members/index.js +103 -26
  160. package/test/unit/spec/members/request.js +45 -22
  161. package/test/unit/spec/members/utils.js +33 -0
  162. package/test/unit/spec/reachability/clusterReachability.ts +88 -56
  163. package/test/unit/spec/reachability/index.ts +101 -0
  164. package/test/unit/spec/reachability/request.js +47 -2
  165. package/test/unit/spec/reconnection-manager/index.js +4 -4
  166. 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
  {
@@ -1692,10 +1696,6 @@ describe('plugin-meetings', () => {
1692
1696
  sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult));
1693
1697
  });
1694
1698
 
1695
- afterEach(() => {
1696
- assert.exists(meeting.isoLocalClientMeetingJoinTime);
1697
- });
1698
-
1699
1699
  it('should join the meeting and return promise', async () => {
1700
1700
  const join = meeting.join({pstnAudioType: 'dial-in'});
1701
1701
  meeting.config.enableAutomaticLLM = true;
@@ -2047,7 +2047,12 @@ describe('plugin-meetings', () => {
2047
2047
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
2048
2048
  meeting.mediaProperties.getCurrentConnectionInfo = sinon
2049
2049
  .stub()
2050
- .resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
2050
+ .resolves({
2051
+ connectionType: 'udp',
2052
+ selectedCandidatePairChanges: 2,
2053
+ numTransports: 1,
2054
+ ipVersion: 'IPv6',
2055
+ });
2051
2056
  meeting.audio = muteStateStub;
2052
2057
  meeting.video = muteStateStub;
2053
2058
  sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
@@ -2110,6 +2115,7 @@ describe('plugin-meetings', () => {
2110
2115
  someReachabilityMetric2: 'some value2',
2111
2116
  }),
2112
2117
  stopReachability: sinon.stub(),
2118
+ isSubnetReachable: sinon.stub().returns(false),
2113
2119
  };
2114
2120
 
2115
2121
  const forceRtcMetricsSend = sinon.stub().resolves();
@@ -2165,6 +2171,7 @@ describe('plugin-meetings', () => {
2165
2171
  someReachabilityMetric1: 'some value1',
2166
2172
  someReachabilityMetric2: 'some value2',
2167
2173
  selectedCandidatePairChanges: 2,
2174
+ isSubnetReachable: null,
2168
2175
  numTransports: 1,
2169
2176
  iceCandidatesCount: 0,
2170
2177
  }
@@ -2211,6 +2218,7 @@ describe('plugin-meetings', () => {
2211
2218
  signalingState: 'unknown',
2212
2219
  connectionState: 'unknown',
2213
2220
  iceConnectionState: 'unknown',
2221
+ isSubnetReachable: null,
2214
2222
  })
2215
2223
  );
2216
2224
 
@@ -2225,6 +2233,7 @@ describe('plugin-meetings', () => {
2225
2233
  someReachabilityMetric1: 'some value1',
2226
2234
  someReachabilityMetric2: 'some value2',
2227
2235
  }),
2236
+ isSubnetReachable: sinon.stub().returns(true),
2228
2237
  };
2229
2238
 
2230
2239
  meeting.waitForRemoteSDPAnswer = sinon.stub().rejects();
@@ -2275,6 +2284,7 @@ describe('plugin-meetings', () => {
2275
2284
  selectedCandidatePairChanges: 2,
2276
2285
  numTransports: 1,
2277
2286
  iceCandidatesCount: 0,
2287
+ isSubnetReachable: null,
2278
2288
  }
2279
2289
  );
2280
2290
  });
@@ -2332,6 +2342,7 @@ describe('plugin-meetings', () => {
2332
2342
  signalingState: 'have-local-offer',
2333
2343
  connectionState: 'connecting',
2334
2344
  iceConnectionState: 'checking',
2345
+ isSubnetReachable: null,
2335
2346
  })
2336
2347
  );
2337
2348
 
@@ -2389,6 +2400,7 @@ describe('plugin-meetings', () => {
2389
2400
  signalingState: 'have-local-offer',
2390
2401
  connectionState: 'connecting',
2391
2402
  iceConnectionState: 'checking',
2403
+ isSubnetReachable: null,
2392
2404
  })
2393
2405
  );
2394
2406
 
@@ -2667,7 +2679,7 @@ describe('plugin-meetings', () => {
2667
2679
 
2668
2680
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
2669
2681
  turnServerInfo: {
2670
- url: FAKE_TURN_URL,
2682
+ urls: [FAKE_TURN_URL],
2671
2683
  username: FAKE_TURN_USER,
2672
2684
  password: FAKE_TURN_PASSWORD,
2673
2685
  },
@@ -2689,7 +2701,7 @@ describe('plugin-meetings', () => {
2689
2701
  meeting.id,
2690
2702
  sinon.match({
2691
2703
  turnServerInfo: {
2692
- url: FAKE_TURN_URL,
2704
+ urls: [FAKE_TURN_URL],
2693
2705
  username: FAKE_TURN_USER,
2694
2706
  password: FAKE_TURN_PASSWORD,
2695
2707
  },
@@ -2724,8 +2736,9 @@ describe('plugin-meetings', () => {
2724
2736
  sinon.stub().returns(FAKE_ERROR));
2725
2737
  webex.meetings.reachability = {
2726
2738
  isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2727
- getReachabilityMetrics: sinon.stub().resolves(),
2739
+ getReachabilityMetrics: sinon.stub().resolves({}),
2728
2740
  stopReachability: sinon.stub(),
2741
+ isSubnetReachable: sinon.stub().returns(true),
2729
2742
  };
2730
2743
  const MOCK_CLIENT_ERROR_CODE = 2004;
2731
2744
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -2747,14 +2760,15 @@ describe('plugin-meetings', () => {
2747
2760
  .onSecondCall()
2748
2761
  .returns({
2749
2762
  turnServerInfo: {
2750
- url: FAKE_TURN_URL,
2763
+ urls: [FAKE_TURN_URL],
2751
2764
  username: FAKE_TURN_USER,
2752
2765
  password: FAKE_TURN_PASSWORD,
2753
2766
  },
2754
2767
  turnDiscoverySkippedReason: undefined,
2755
2768
  });
2756
2769
  meeting.meetingState = 'ACTIVE';
2757
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2770
+ const error = {iceConnected: false};
2771
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects(error);
2758
2772
 
2759
2773
  const forceRtcMetricsSend = sinon.stub().resolves();
2760
2774
  const closeMediaConnectionStub = sinon.stub();
@@ -2772,6 +2786,7 @@ describe('plugin-meetings', () => {
2772
2786
  })
2773
2787
  .catch((err) => {
2774
2788
  errorThrown = err;
2789
+ assert.instanceOf(err.cause, Error);
2775
2790
  assert.instanceOf(err, AddMediaFailed);
2776
2791
  });
2777
2792
 
@@ -2828,6 +2843,7 @@ describe('plugin-meetings', () => {
2828
2843
  },
2829
2844
  options: {
2830
2845
  meetingId: meeting.id,
2846
+ rawError: error,
2831
2847
  },
2832
2848
  });
2833
2849
  assert.calledWith(webex.internal.newMetrics.submitClientEvent.thirdCall, {
@@ -2839,6 +2855,7 @@ describe('plugin-meetings', () => {
2839
2855
  },
2840
2856
  options: {
2841
2857
  meetingId: meeting.id,
2858
+ rawError: error,
2842
2859
  },
2843
2860
  });
2844
2861
 
@@ -2905,6 +2922,7 @@ describe('plugin-meetings', () => {
2905
2922
  selectedCandidatePairChanges: 2,
2906
2923
  numTransports: 1,
2907
2924
  iceCandidatesCount: 0,
2925
+ isSubnetReachable: null,
2908
2926
  },
2909
2927
  ]);
2910
2928
 
@@ -2935,6 +2953,7 @@ describe('plugin-meetings', () => {
2935
2953
  .resolves(false),
2936
2954
  getReachabilityMetrics: sinon.stub().resolves({}),
2937
2955
  stopReachability: sinon.stub(),
2956
+ isSubnetReachable: sinon.stub().returns(true),
2938
2957
  };
2939
2958
  const getErrorPayloadForClientErrorCodeStub =
2940
2959
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
@@ -2959,16 +2978,19 @@ describe('plugin-meetings', () => {
2959
2978
  .onSecondCall()
2960
2979
  .returns({
2961
2980
  turnServerInfo: {
2962
- url: FAKE_TURN_URL,
2981
+ urls: [FAKE_TURN_URL],
2963
2982
  username: FAKE_TURN_USER,
2964
2983
  password: FAKE_TURN_PASSWORD,
2965
2984
  },
2966
2985
  turnDiscoverySkippedReason: undefined,
2967
2986
  });
2987
+
2988
+ const mediaConnectionError = new Error('fake error');
2989
+
2968
2990
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon
2969
2991
  .stub()
2970
2992
  .onFirstCall()
2971
- .rejects()
2993
+ .rejects(mediaConnectionError)
2972
2994
  .onSecondCall()
2973
2995
  .resolves();
2974
2996
 
@@ -3037,10 +3059,14 @@ describe('plugin-meetings', () => {
3037
3059
  },
3038
3060
  options: {
3039
3061
  meetingId: meeting.id,
3062
+ rawError: mediaConnectionError,
3040
3063
  },
3041
3064
  });
3042
3065
  assert.calledWith(webex.internal.newMetrics.submitClientEvent.thirdCall, {
3043
3066
  name: 'client.media-engine.ready',
3067
+ payload: {
3068
+ ipVersion: 'IPv6',
3069
+ },
3044
3070
  options: {
3045
3071
  meetingId: meeting.id,
3046
3072
  },
@@ -3097,11 +3123,13 @@ describe('plugin-meetings', () => {
3097
3123
  locus_id: meeting.locusUrl.split('/').pop(),
3098
3124
  connectionType: 'udp',
3099
3125
  selectedCandidatePairChanges: 2,
3126
+ ipVersion: 'IPv6',
3100
3127
  numTransports: 1,
3101
3128
  isMultistream: false,
3102
3129
  retriedWithTurnServer: true,
3103
3130
  isJoinWithMediaRetry: false,
3104
3131
  iceCandidatesCount: 0,
3132
+ isSubnetReachable: null,
3105
3133
  },
3106
3134
  ]);
3107
3135
  meeting.roap.doTurnDiscovery;
@@ -3136,7 +3164,7 @@ describe('plugin-meetings', () => {
3136
3164
  .onSecondCall()
3137
3165
  .returns({
3138
3166
  turnServerInfo: {
3139
- url: FAKE_TURN_URL,
3167
+ urls: [FAKE_TURN_URL],
3140
3168
  username: FAKE_TURN_USER,
3141
3169
  password: FAKE_TURN_PASSWORD,
3142
3170
  },
@@ -3188,7 +3216,7 @@ describe('plugin-meetings', () => {
3188
3216
  .onSecondCall()
3189
3217
  .returns({
3190
3218
  turnServerInfo: {
3191
- url: FAKE_TURN_URL,
3219
+ urls: [FAKE_TURN_URL],
3192
3220
  username: FAKE_TURN_USER,
3193
3221
  password: FAKE_TURN_PASSWORD,
3194
3222
  },
@@ -3230,6 +3258,7 @@ describe('plugin-meetings', () => {
3230
3258
  someReachabilityMetric2: 'some value2',
3231
3259
  }),
3232
3260
  stopReachability: sinon.stub(),
3261
+ isSubnetReachable: sinon.stub().returns(true),
3233
3262
  };
3234
3263
  meeting.iceCandidatesCount = 3;
3235
3264
  meeting.iceCandidateErrors.set('701_error', 3);
@@ -3248,6 +3277,7 @@ describe('plugin-meetings', () => {
3248
3277
  locus_id: meeting.locusUrl.split('/').pop(),
3249
3278
  connectionType: 'udp',
3250
3279
  selectedCandidatePairChanges: 2,
3280
+ ipVersion: 'IPv6',
3251
3281
  numTransports: 1,
3252
3282
  isMultistream: false,
3253
3283
  retriedWithTurnServer: false,
@@ -3257,6 +3287,7 @@ describe('plugin-meetings', () => {
3257
3287
  iceCandidatesCount: 3,
3258
3288
  '701_error': 3,
3259
3289
  '701_turn_host_lookup_received_error': 1,
3290
+ isSubnetReachable: null,
3260
3291
  }
3261
3292
  );
3262
3293
 
@@ -3319,6 +3350,7 @@ describe('plugin-meetings', () => {
3319
3350
  iceConnectionState: 'unknown',
3320
3351
  selectedCandidatePairChanges: 2,
3321
3352
  numTransports: 1,
3353
+ isSubnetReachable: null,
3322
3354
  iceCandidatesCount: 0,
3323
3355
  }
3324
3356
  );
@@ -3380,6 +3412,117 @@ describe('plugin-meetings', () => {
3380
3412
  numTransports: 1,
3381
3413
  '701_error': 2,
3382
3414
  '701_turn_host_lookup_received_error': 1,
3415
+ isSubnetReachable: null,
3416
+ iceCandidatesCount: 0,
3417
+ }
3418
+ );
3419
+
3420
+ assert.isOk(errorThrown);
3421
+ });
3422
+
3423
+ it('should send valid isSubnetReachability if media connection success', async () => {
3424
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3425
+ turnServerInfo: undefined,
3426
+ turnDiscoverySkippedReason: undefined,
3427
+ });
3428
+ meeting.meetingState = 'ACTIVE';
3429
+ meeting.mediaProperties.waitForMediaConnectionConnected.resolves();
3430
+ meeting.webex.meetings.reachability = {
3431
+ getReachabilityMetrics: sinon.stub().resolves({
3432
+ reachability_public_udp_success: 5,
3433
+ }),
3434
+ stopReachability: sinon.stub(),
3435
+ isSubnetReachable: sinon.stub().returns(false),
3436
+ };
3437
+
3438
+ const forceRtcMetricsSend = sinon.stub().resolves();
3439
+ const closeMediaConnectionStub = sinon.stub();
3440
+ Media.createMediaConnection = sinon.stub().returns({
3441
+ close: closeMediaConnectionStub,
3442
+ forceRtcMetricsSend,
3443
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3444
+ initiateOffer: sinon.stub().resolves({}),
3445
+ on: sinon.stub(),
3446
+ });
3447
+
3448
+ await meeting.addMedia({
3449
+ mediaSettings: {},
3450
+ });
3451
+
3452
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
3453
+ correlation_id: meeting.correlationId,
3454
+ locus_id: meeting.locusUrl.split('/').pop(),
3455
+ connectionType: 'udp',
3456
+ ipVersion: 'IPv6',
3457
+ selectedCandidatePairChanges: 2,
3458
+ numTransports: 1,
3459
+ isMultistream: false,
3460
+ retriedWithTurnServer: false,
3461
+ isJoinWithMediaRetry: false,
3462
+ iceCandidatesCount: 0,
3463
+ reachability_public_udp_success: 5,
3464
+ isSubnetReachable: false,
3465
+ });
3466
+ });
3467
+
3468
+ it('should send valid isSubnetReachability if media connection fails', async () => {
3469
+ let errorThrown = undefined;
3470
+
3471
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3472
+ turnServerInfo: undefined,
3473
+ turnDiscoverySkippedReason: undefined,
3474
+ });
3475
+ meeting.meetingState = 'ACTIVE';
3476
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
3477
+ meeting.webex.meetings.reachability = {
3478
+ getReachabilityMetrics: sinon.stub().resolves({
3479
+ reachability_public_udp_success: 5,
3480
+ }),
3481
+ stopReachability: sinon.stub(),
3482
+ isSubnetReachable: sinon.stub().returns(true),
3483
+ };
3484
+
3485
+ const forceRtcMetricsSend = sinon.stub().resolves();
3486
+ const closeMediaConnectionStub = sinon.stub();
3487
+ Media.createMediaConnection = sinon.stub().returns({
3488
+ close: closeMediaConnectionStub,
3489
+ forceRtcMetricsSend,
3490
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3491
+ initiateOffer: sinon.stub().resolves({}),
3492
+ on: sinon.stub(),
3493
+ });
3494
+
3495
+ await meeting
3496
+ .addMedia({
3497
+ mediaSettings: {},
3498
+ })
3499
+ .catch((err) => {
3500
+ errorThrown = err;
3501
+ assert.instanceOf(err, AddMediaFailed);
3502
+ });
3503
+
3504
+ // Check that the only metric sent is ADD_MEDIA_FAILURE
3505
+ assert.calledOnceWithExactly(
3506
+ Metrics.sendBehavioralMetric,
3507
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
3508
+ {
3509
+ correlation_id: meeting.correlationId,
3510
+ locus_id: meeting.locusUrl.split('/').pop(),
3511
+ reason: errorThrown.message,
3512
+ stack: errorThrown.stack,
3513
+ code: errorThrown.code,
3514
+ turnDiscoverySkippedReason: undefined,
3515
+ turnServerUsed: true,
3516
+ retriedWithTurnServer: false,
3517
+ isMultistream: false,
3518
+ isJoinWithMediaRetry: false,
3519
+ signalingState: 'unknown',
3520
+ connectionState: 'unknown',
3521
+ iceConnectionState: 'unknown',
3522
+ selectedCandidatePairChanges: 2,
3523
+ numTransports: 1,
3524
+ reachability_public_udp_success: 5,
3525
+ isSubnetReachable: true,
3383
3526
  iceCandidatesCount: 0,
3384
3527
  }
3385
3528
  );
@@ -3399,6 +3542,8 @@ describe('plugin-meetings', () => {
3399
3542
  meeting.config.stats.enableStatsAnalyzer = true;
3400
3543
 
3401
3544
  statsAnalyzerStub = new EventsScope();
3545
+ statsAnalyzerStub.getNetworkType = sinon.stub().returns('wifi');
3546
+
3402
3547
  // mock the StatsAnalyzer constructor
3403
3548
  sinon.stub(InternalMediaCoreModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
3404
3549
 
@@ -3439,6 +3584,40 @@ describe('plugin-meetings', () => {
3439
3584
  });
3440
3585
  });
3441
3586
 
3587
+ it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and does not send metric because we already have', async () => {
3588
+ meeting.shareCAEventSentStatus = {
3589
+ transmitStart: true,
3590
+ transmitStop: false,
3591
+ receiveStart: false,
3592
+ receiveStop: false,
3593
+ };
3594
+ statsAnalyzerStub.emit(
3595
+ {file: 'test', function: 'test'},
3596
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED,
3597
+ {mediaType: 'share'}
3598
+ );
3599
+
3600
+ assert.calledWith(
3601
+ TriggerProxy.trigger,
3602
+ sinon.match.instanceOf(Meeting),
3603
+ {
3604
+ file: 'meeting/index',
3605
+ function: 'addMedia',
3606
+ },
3607
+ EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED,
3608
+ {
3609
+ mediaType: 'share',
3610
+ }
3611
+ );
3612
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3613
+ name: 'client.media.tx.start',
3614
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3615
+ options: {
3616
+ meetingId: meeting.id,
3617
+ },
3618
+ });
3619
+ });
3620
+
3442
3621
  it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => {
3443
3622
  statsAnalyzerStub.emit(
3444
3623
  {file: 'test', function: 'test'},
@@ -3455,6 +3634,28 @@ describe('plugin-meetings', () => {
3455
3634
  });
3456
3635
  });
3457
3636
 
3637
+ it('LOCAL_MEDIA_STOPPED does not send metric because we already have', async () => {
3638
+ meeting.shareCAEventSentStatus = {
3639
+ transmitStart: false,
3640
+ transmitStop: true,
3641
+ receiveStart: false,
3642
+ receiveStop: false,
3643
+ };
3644
+ statsAnalyzerStub.emit(
3645
+ {file: 'test', function: 'test'},
3646
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED,
3647
+ {mediaType: 'share'}
3648
+ );
3649
+
3650
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3651
+ name: 'client.media.tx.stop',
3652
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3653
+ options: {
3654
+ meetingId: meeting.id,
3655
+ },
3656
+ });
3657
+ });
3658
+
3458
3659
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => {
3459
3660
  statsAnalyzerStub.emit(
3460
3661
  {file: 'test', function: 'test'},
@@ -3535,6 +3736,47 @@ describe('plugin-meetings', () => {
3535
3736
  });
3536
3737
  });
3537
3738
 
3739
+ it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and does not send metric because we already have', async () => {
3740
+ meeting.shareCAEventSentStatus = {
3741
+ transmitStart: false,
3742
+ transmitStop: false,
3743
+ receiveStart: true,
3744
+ receiveStop: false,
3745
+ };
3746
+ statsAnalyzerStub.emit(
3747
+ {file: 'test', function: 'test'},
3748
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3749
+ {mediaType: 'share'}
3750
+ );
3751
+
3752
+ assert.calledWith(
3753
+ TriggerProxy.trigger,
3754
+ sinon.match.instanceOf(Meeting),
3755
+ {
3756
+ file: 'meeting/index',
3757
+ function: 'addMedia',
3758
+ },
3759
+ EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
3760
+ {
3761
+ mediaType: 'share',
3762
+ }
3763
+ );
3764
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3765
+ name: 'client.media.render.start',
3766
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3767
+ options: {
3768
+ meetingId: meeting.id,
3769
+ },
3770
+ });
3771
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3772
+ name: 'client.media.rx.start',
3773
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3774
+ options: {
3775
+ meetingId: meeting.id,
3776
+ },
3777
+ });
3778
+ });
3779
+
3538
3780
  it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
3539
3781
  statsAnalyzerStub.emit(
3540
3782
  {file: 'test', function: 'test'},
@@ -3559,6 +3801,34 @@ describe('plugin-meetings', () => {
3559
3801
  });
3560
3802
  });
3561
3803
 
3804
+ it('REMOTE_MEDIA_STOPPED does not send metric because we already have', async () => {
3805
+ meeting.shareCAEventSentStatus = {
3806
+ transmitStart: false,
3807
+ transmitStop: false,
3808
+ receiveStart: true,
3809
+ receiveStop: true,
3810
+ };
3811
+ statsAnalyzerStub.emit(
3812
+ {file: 'test', function: 'test'},
3813
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3814
+ {mediaType: 'share'}
3815
+ );
3816
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3817
+ name: 'client.media.render.stop',
3818
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3819
+ options: {
3820
+ meetingId: meeting.id,
3821
+ },
3822
+ });
3823
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3824
+ name: 'client.media.rx.stop',
3825
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3826
+ options: {
3827
+ meetingId: meeting.id,
3828
+ },
3829
+ });
3830
+ });
3831
+
3562
3832
  it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
3563
3833
  let fakeMembersCollection = {
3564
3834
  members: {
@@ -3568,7 +3838,7 @@ describe('plugin-meetings', () => {
3568
3838
  },
3569
3839
  };
3570
3840
  sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
3571
- const fakeData = {intervalMetadata: {}, networkType: 'wifi'};
3841
+ const fakeData = {intervalMetadata: {}};
3572
3842
 
3573
3843
  statsAnalyzerStub.emit(
3574
3844
  {file: 'test', function: 'test'},
@@ -3609,7 +3879,7 @@ describe('plugin-meetings', () => {
3609
3879
  });
3610
3880
 
3611
3881
  it('calls submitMQE correctly', async () => {
3612
- const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
3882
+ const fakeData = {intervalMetadata: {bla: 'bla'}};
3613
3883
 
3614
3884
  statsAnalyzerStub.emit(
3615
3885
  {file: 'test', function: 'test'},
@@ -3640,7 +3910,7 @@ describe('plugin-meetings', () => {
3640
3910
 
3641
3911
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3642
3912
  turnServerInfo: {
3643
- url: FAKE_TURN_URL,
3913
+ urls: [FAKE_TURN_URL],
3644
3914
  username: FAKE_TURN_USER,
3645
3915
  password: FAKE_TURN_PASSWORD,
3646
3916
  },
@@ -3666,7 +3936,7 @@ describe('plugin-meetings', () => {
3666
3936
  meeting.id,
3667
3937
  sinon.match({
3668
3938
  turnServerInfo: {
3669
- url: FAKE_TURN_URL,
3939
+ urls: [FAKE_TURN_URL],
3670
3940
  username: FAKE_TURN_USER,
3671
3941
  password: FAKE_TURN_PASSWORD,
3672
3942
  },
@@ -3817,6 +4087,9 @@ describe('plugin-meetings', () => {
3817
4087
  },
3818
4088
  options: {
3819
4089
  meetingId: meeting.id,
4090
+ rawError: {
4091
+ iceConnected: false,
4092
+ },
3820
4093
  },
3821
4094
  },
3822
4095
  ]);
@@ -3887,9 +4160,29 @@ describe('plugin-meetings', () => {
3887
4160
  } catch (err) {
3888
4161
  assert.instanceOf(err, Error);
3889
4162
  assert.equal(err.message, 'setBrb failed');
3890
- assert.isRejected((Promise.reject()));
4163
+ assert.isRejected(Promise.reject());
3891
4164
  }
3892
4165
  });
4166
+
4167
+ it('updates remote mute state when brb is enabled', async () => {
4168
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4169
+
4170
+ await meeting.beRightBack(true);
4171
+
4172
+ sinon.assert.calledOnceWithExactly(
4173
+ meeting.audio.handleServerRemoteMuteUpdate,
4174
+ meeting,
4175
+ true
4176
+ );
4177
+ });
4178
+
4179
+ it('does not update remote mute state when brb is disabled', async () => {
4180
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4181
+
4182
+ await meeting.beRightBack(false);
4183
+
4184
+ assert.notCalled(meeting.audio.handleServerRemoteMuteUpdate);
4185
+ });
3893
4186
  });
3894
4187
  });
3895
4188
 
@@ -3943,7 +4236,10 @@ describe('plugin-meetings', () => {
3943
4236
  .resolves({id: 'fake clientMediaPreferences'});
3944
4237
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3945
4238
  turnServerInfo: {
3946
- url: 'turns:turn-server-url:443?transport=tcp',
4239
+ urls: [
4240
+ 'turns:turn-server-url1:443?transport=tcp',
4241
+ 'turns:turn-server-url2:443?transport=tcp',
4242
+ ],
3947
4243
  username: 'turn user',
3948
4244
  password: 'turn password',
3949
4245
  },
@@ -3961,12 +4257,10 @@ describe('plugin-meetings', () => {
3961
4257
  expectedMediaConnectionConfig = {
3962
4258
  iceServers: [
3963
4259
  {
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',
4260
+ urls: [
4261
+ 'turns:turn-server-url1:443?transport=tcp',
4262
+ 'turns:turn-server-url2:443?transport=tcp',
4263
+ ],
3970
4264
  username: 'turn user',
3971
4265
  credential: 'turn password',
3972
4266
  },
@@ -4048,9 +4342,11 @@ describe('plugin-meetings', () => {
4048
4342
  .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
4049
4343
  .returns(fakeMultistreamRoapMediaConnection);
4050
4344
 
4051
- locusMediaRequestStub = sinon
4052
- .stub(WebexPlugin.prototype, 'request')
4053
- .resolves({body: {locus: {fullState: {}}}, upload: sinon.match.instanceOf(EventEmitter), download: sinon.match.instanceOf(EventEmitter)});
4345
+ locusMediaRequestStub = sinon.stub(WebexPlugin.prototype, 'request').resolves({
4346
+ body: {locus: {fullState: {}}},
4347
+ upload: sinon.match.instanceOf(EventEmitter),
4348
+ download: sinon.match.instanceOf(EventEmitter),
4349
+ });
4054
4350
 
4055
4351
  // setup some things and mocks so that the call to join() works
4056
4352
  // (we need to call join() because it creates the LocusMediaRequest instance
@@ -5234,7 +5530,10 @@ describe('plugin-meetings', () => {
5234
5530
  // and check that when we fallback to transcoded we still do another TURN discovery
5235
5531
  await runCheck(
5236
5532
  {
5237
- url: 'turns:turn-server-url:443?transport=tcp',
5533
+ urls: [
5534
+ 'turns:turn-server-url1:443?transport=tcp',
5535
+ 'turns:turn-server-url2:443?transport=tcp',
5536
+ ],
5238
5537
  username: 'turn user',
5239
5538
  password: 'turn password',
5240
5539
  },
@@ -5248,7 +5547,10 @@ describe('plugin-meetings', () => {
5248
5547
  // but doing it just for completeness
5249
5548
  await runCheck(
5250
5549
  {
5251
- url: 'turns:turn-server-url:443?transport=tcp',
5550
+ urls: [
5551
+ 'turns:turn-server-url1:443?transport=tcp',
5552
+ 'turns:turn-server-url2:443?transport=tcp',
5553
+ ],
5252
5554
  username: 'turn user',
5253
5555
  password: 'turn password',
5254
5556
  },
@@ -7530,6 +7832,27 @@ describe('plugin-meetings', () => {
7530
7832
  });
7531
7833
  });
7532
7834
 
7835
+ describe('#setIsoLocalClientMeetingJoinTime', () => {
7836
+ it('should fallback to system clock ISO string when given an undefined value', () => {
7837
+ const currentSystemTime = new Date().toISOString();
7838
+ meeting.isoLocalClientMeetingJoinTime = undefined;
7839
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7840
+ });
7841
+
7842
+ it('should fallback to system clock ISO string when given an invalid value', () => {
7843
+ const currentSystemTime = new Date().toISOString();
7844
+ meeting.isoLocalClientMeetingJoinTime = 'invalid-date';
7845
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7846
+ });
7847
+
7848
+ it('should set the isoLocalClientMeetingJoinTime correctly for a valid date string', () => {
7849
+ const validDateString = 'Tue, 01 Apr 2025 13:00:36 GMT';
7850
+ const expectedISOString = new Date(validDateString).toISOString();
7851
+ meeting.isoLocalClientMeetingJoinTime = validDateString;
7852
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, expectedISOString);
7853
+ });
7854
+ });
7855
+
7533
7856
  describe('#updateCallStateForMetrics', () => {
7534
7857
  it('should update the callState, overriding existing values', () => {
7535
7858
  assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''});
@@ -7611,6 +7934,12 @@ describe('plugin-meetings', () => {
7611
7934
  meeting.audio = {handleLocalStreamChange: sinon.stub()};
7612
7935
  meeting.video = {handleLocalStreamChange: sinon.stub()};
7613
7936
  meeting.statsAnalyzer = {updateMediaStatus: sinon.stub()};
7937
+ meeting.shareCAEventSentStatus = {
7938
+ transmitStart: false,
7939
+ transmitStop: false,
7940
+ receiveStart: false,
7941
+ receiveStop: false,
7942
+ };
7614
7943
  fakeMultistreamRoapMediaConnection = {
7615
7944
  createSendSlot: () => {
7616
7945
  return {
@@ -7678,6 +8007,9 @@ describe('plugin-meetings', () => {
7678
8007
  });
7679
8008
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7680
8009
 
8010
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
8011
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
8012
+
7681
8013
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7682
8014
  expected: {sendShare: true},
7683
8015
  });
@@ -7698,18 +8030,23 @@ describe('plugin-meetings', () => {
7698
8030
  assert.equal(meeting.mediaProperties.shareAudioStream, stream);
7699
8031
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7700
8032
 
8033
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
8034
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
8035
+
7701
8036
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7702
8037
  expected: {sendShare: true},
7703
8038
  });
7704
8039
  };
7705
8040
 
7706
8041
  it('requests screen share floor and publishes the screen share video stream', async () => {
8042
+ meeting.shareCAEventSentStatus.transmitStart = true;
7707
8043
  await meeting.publishStreams({screenShare: {video: videoShareStream}});
7708
8044
 
7709
8045
  checkScreenShareVideoPublished(videoShareStream);
7710
8046
  });
7711
8047
 
7712
8048
  it('requests screen share floor and publishes the screen share audio stream', async () => {
8049
+ meeting.shareCAEventSentStatus.transmitStart = true;
7713
8050
  await meeting.publishStreams({screenShare: {audio: audioShareStream}});
7714
8051
 
7715
8052
  checkScreenShareAudioPublished(audioShareStream);
@@ -8596,13 +8933,19 @@ describe('plugin-meetings', () => {
8596
8933
  const fakeErrorMessage = 'test error';
8597
8934
  const fakeRootCauseName = 'root cause name';
8598
8935
  const fakeErrorName = 'test error name';
8936
+ let clock;
8599
8937
 
8600
8938
  beforeEach(() => {
8939
+ clock = sinon.useFakeTimers();
8601
8940
  meeting.setupMediaConnectionListeners();
8602
8941
  webex.internal.newMetrics.submitClientEvent.resetHistory();
8603
8942
  Metrics.sendBehavioralMetric.resetHistory();
8604
8943
  });
8605
8944
 
8945
+ afterEach(() => {
8946
+ clock.restore();
8947
+ });
8948
+
8606
8949
  const checkMetricSent = (event, error) => {
8607
8950
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
8608
8951
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -8671,6 +9014,13 @@ describe('plugin-meetings', () => {
8671
9014
  });
8672
9015
 
8673
9016
  it('should send metrics for SdpAnswerHandlingError error', () => {
9017
+ meeting.sdpResponseTimer = '1234';
9018
+ meeting.deferSDPAnswer = {
9019
+ reject: sinon.stub(),
9020
+ };
9021
+
9022
+ const clearTimeoutSpy = sinon.spy(clock, 'clearTimeout');
9023
+
8674
9024
  const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
8675
9025
  name: fakeErrorName,
8676
9026
  cause: {name: fakeRootCauseName},
@@ -8685,6 +9035,8 @@ describe('plugin-meetings', () => {
8685
9035
  fakeErrorMessage,
8686
9036
  fakeRootCauseName
8687
9037
  );
9038
+ assert.calledOnce(meeting.deferSDPAnswer.reject);
9039
+ assert.calledOnce(clearTimeoutSpy);
8688
9040
  });
8689
9041
 
8690
9042
  it('should send metrics for SdpError error', () => {
@@ -9590,6 +9942,42 @@ describe('plugin-meetings', () => {
9590
9942
  );
9591
9943
  });
9592
9944
 
9945
+ it('listens to CONTROLS_ANNOTATION_CHANGED', async () => {
9946
+ const state = {example: 'value'};
9947
+
9948
+ await meeting.locusInfo.emitScoped(
9949
+ {function: 'test', file: 'test'},
9950
+ LOCUSINFO.EVENTS.CONTROLS_ANNOTATION_CHANGED,
9951
+ {state}
9952
+ );
9953
+
9954
+ assert.calledWith(
9955
+ TriggerProxy.trigger,
9956
+ meeting,
9957
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
9958
+ EVENT_TRIGGERS.MEETING_CONTROLS_ANNOTATION_UPDATED,
9959
+ {state}
9960
+ );
9961
+ });
9962
+
9963
+ it('listens to CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED', async () => {
9964
+ const state = {example: 'value'};
9965
+
9966
+ await meeting.locusInfo.emitScoped(
9967
+ {function: 'test', file: 'test'},
9968
+ LOCUSINFO.EVENTS.CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED,
9969
+ {state}
9970
+ );
9971
+
9972
+ assert.calledWith(
9973
+ TriggerProxy.trigger,
9974
+ meeting,
9975
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
9976
+ EVENT_TRIGGERS.MEETING_CONTROLS_REMOTE_DESKTOP_CONTROL_UPDATED,
9977
+ {state}
9978
+ );
9979
+ });
9980
+
9593
9981
  it('listens to the locus interpretation update event', () => {
9594
9982
  const interpretation = {
9595
9983
  siLanguages: [{languageCode: 20, languageName: 'en'}],
@@ -10485,9 +10873,11 @@ describe('plugin-meetings', () => {
10485
10873
  let canUserLowerSomeoneElsesHandSpy;
10486
10874
  let waitingForOthersToJoinSpy;
10487
10875
  let canSendReactionsSpy;
10876
+ let requiresPostMeetingDataConsentPromptSpy;
10488
10877
  let canUserRenameSelfAndObservedSpy;
10489
10878
  let canUserRenameOthersSpy;
10490
10879
  let canShareWhiteBoardSpy;
10880
+ let canMoveToLobbySpy;
10491
10881
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
10492
10882
 
10493
10883
  beforeEach(() => {
@@ -10512,8 +10902,13 @@ describe('plugin-meetings', () => {
10512
10902
  waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
10513
10903
  canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
10514
10904
  canUserRenameSelfAndObservedSpy = sinon.spy(MeetingUtil, 'canUserRenameSelfAndObserved');
10905
+ requiresPostMeetingDataConsentPromptSpy = sinon.spy(
10906
+ MeetingUtil,
10907
+ 'requiresPostMeetingDataConsentPrompt'
10908
+ );
10515
10909
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
10516
10910
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
10911
+ canMoveToLobbySpy = sinon.spy(MeetingUtil, 'canMoveToLobby');
10517
10912
  });
10518
10913
 
10519
10914
  afterEach(() => {
@@ -10611,6 +11006,16 @@ describe('plugin-meetings', () => {
10611
11006
  requiredDisplayHints: [],
10612
11007
  requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
10613
11008
  },
11009
+ {
11010
+ actionName: 'canRealtimeCloseCaption',
11011
+ requiredDisplayHints: [],
11012
+ requiredPolicies: [SELF_POLICY.SUPPORT_REALTIME_CLOSE_CAPTION],
11013
+ },
11014
+ {
11015
+ actionName: 'canRealtimeCloseCaptionManual',
11016
+ requiredDisplayHints: [],
11017
+ requiredPolicies: [SELF_POLICY.SUPPORT_REALTIME_CLOSE_CAPTION_MANUAL],
11018
+ },
10614
11019
  {
10615
11020
  actionName: 'canChat',
10616
11021
  requiredDisplayHints: [],
@@ -10640,6 +11045,11 @@ describe('plugin-meetings', () => {
10640
11045
  requiredDisplayHints: [],
10641
11046
  requiredPolicies: [SELF_POLICY.SUPPORT_POLLING_AND_QA],
10642
11047
  },
11048
+ {
11049
+ actionName: 'canShareWhiteBoard',
11050
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_WHITEBOARD],
11051
+ requiredPolicies: [SELF_POLICY.SUPPORT_WHITEBOARD],
11052
+ },
10643
11053
  ],
10644
11054
  ({
10645
11055
  actionName,
@@ -11047,8 +11457,10 @@ describe('plugin-meetings', () => {
11047
11457
  assert.calledWith(waitingForOthersToJoinSpy, userDisplayHints);
11048
11458
  assert.calledWith(canSendReactionsSpy, null, userDisplayHints);
11049
11459
  assert.calledWith(canUserRenameSelfAndObservedSpy, userDisplayHints);
11460
+ assert.calledWith(requiresPostMeetingDataConsentPromptSpy, userDisplayHints);
11050
11461
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11051
- assert.calledWith(canShareWhiteBoardSpy, userDisplayHints);
11462
+ assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11463
+ assert.calledWith(canMoveToLobbySpy, userDisplayHints);
11052
11464
 
11053
11465
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11054
11466
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -11142,6 +11554,22 @@ describe('plugin-meetings', () => {
11142
11554
  requiredPolicies: [SELF_POLICY.SUPPORT_VOIP],
11143
11555
  policies: selfUserPolicies,
11144
11556
  });
11557
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11558
+ requiredHints: [DISPLAY_HINTS.ENABLE_ANNOTATION_MEETING_OPTION],
11559
+ displayHints: userDisplayHints,
11560
+ });
11561
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11562
+ requiredHints: [DISPLAY_HINTS.DISABLE_ANNOTATION_MEETING_OPTION],
11563
+ displayHints: userDisplayHints,
11564
+ });
11565
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11566
+ requiredHints: [DISPLAY_HINTS.ENABLE_RDC_MEETING_OPTION],
11567
+ displayHints: userDisplayHints,
11568
+ });
11569
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11570
+ requiredHints: [DISPLAY_HINTS.DISABLE_RDC_MEETING_OPTION],
11571
+ displayHints: userDisplayHints,
11572
+ });
11145
11573
 
11146
11574
  assert.calledWith(
11147
11575
  TriggerProxy.trigger,
@@ -11988,6 +12416,8 @@ describe('plugin-meetings', () => {
11988
12416
  // Set the webinar attendee flag
11989
12417
  meeting.webinar = {selfIsAttendee: true};
11990
12418
  meeting.locusInfo.info.isWebinar = true;
12419
+ meeting.shareCAEventSentStatus.receiveStart = true;
12420
+ meeting.shareCAEventSentStatus.receiveStop = true;
11991
12421
 
11992
12422
  // Step 1: Start sharing whiteboard A
11993
12423
  const data1 = generateData(
@@ -12011,6 +12441,8 @@ describe('plugin-meetings', () => {
12011
12441
 
12012
12442
  // Specific assertions for webinar attendee status
12013
12443
  assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
12444
+ assert.equal(meeting.shareCAEventSentStatus.receiveStart, false);
12445
+ assert.equal(meeting.shareCAEventSentStatus.receiveStop, false);
12014
12446
  });
12015
12447
  });
12016
12448
 
@@ -12666,6 +13098,31 @@ describe('plugin-meetings', () => {
12666
13098
  });
12667
13099
  });
12668
13100
  });
13101
+
13102
+ describe('handleShareVideoStreamMuteStateChange', () => {
13103
+ it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
13104
+ meeting.isMultistream = true;
13105
+ meeting.statsAnalyzer = {shareVideoEncoderImplementation: 'OpenH264'};
13106
+ meeting.mediaProperties.shareVideoStream = {
13107
+ getSettings: sinon.stub().returns({displaySurface: 'monitor', frameRate: 30}),
13108
+ };
13109
+
13110
+ meeting.handleShareVideoStreamMuteStateChange(true);
13111
+
13112
+ assert.calledOnceWithExactly(
13113
+ Metrics.sendBehavioralMetric,
13114
+ BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE,
13115
+ {
13116
+ correlationId: meeting.correlationId,
13117
+ muted: true,
13118
+ encoderImplementation: 'OpenH264',
13119
+ displaySurface: 'monitor',
13120
+ isMultistream: true,
13121
+ frameRate: 30,
13122
+ }
13123
+ );
13124
+ });
13125
+ });
12669
13126
  });
12670
13127
 
12671
13128
  describe('#startKeepAlive', () => {
@@ -12833,6 +13290,38 @@ describe('plugin-meetings', () => {
12833
13290
  });
12834
13291
  });
12835
13292
 
13293
+ describe('#setPostMeetingDataConsent', () => {
13294
+ it('should have #setPostMeetingDataConsent', () => {
13295
+ assert.exists(meeting.setPostMeetingDataConsent);
13296
+ });
13297
+
13298
+ beforeEach(() => {
13299
+ meeting.meetingRequest.setPostMeetingDataConsent = sinon
13300
+ .stub()
13301
+ .returns(Promise.resolve());
13302
+ });
13303
+
13304
+ [true, false].forEach((accept) => {
13305
+ it(`should send consent with ${accept}`, async () => {
13306
+ const id = uuidv4();
13307
+ meeting.locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`;
13308
+ meeting.deviceUrl = `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`;
13309
+ meeting.members.selfId = id;
13310
+
13311
+ const consentPromise = meeting.setPostMeetingDataConsent(accept);
13312
+
13313
+ assert.exists(consentPromise.then);
13314
+ await consentPromise;
13315
+ assert.calledOnceWithExactly(meeting.meetingRequest.setPostMeetingDataConsent, {
13316
+ locusUrl: `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`,
13317
+ postMeetingDataConsent: accept,
13318
+ selfId: id,
13319
+ deviceUrl: `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`,
13320
+ });
13321
+ });
13322
+ });
13323
+ });
13324
+
12836
13325
  describe('#sendReaction', () => {
12837
13326
  it('should have #sendReaction', () => {
12838
13327
  assert.exists(meeting.sendReaction);