@webex/plugin-meetings 3.8.0-next.6 → 3.8.0-next.60

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 (156) 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 +1 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/constants.js +16 -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 +8 -16
  23. package/dist/media/index.js.map +1 -1
  24. package/dist/meeting/in-meeting-actions.js +13 -1
  25. package/dist/meeting/in-meeting-actions.js.map +1 -1
  26. package/dist/meeting/index.js +548 -288
  27. package/dist/meeting/index.js.map +1 -1
  28. package/dist/meeting/locusMediaRequest.js +0 -17
  29. package/dist/meeting/locusMediaRequest.js.map +1 -1
  30. package/dist/meeting/muteState.js +0 -2
  31. package/dist/meeting/muteState.js.map +1 -1
  32. package/dist/meeting/request.js +30 -0
  33. package/dist/meeting/request.js.map +1 -1
  34. package/dist/meeting/request.type.js.map +1 -1
  35. package/dist/meeting/util.js +13 -2
  36. package/dist/meeting/util.js.map +1 -1
  37. package/dist/meeting-info/meeting-info-v2.js +359 -60
  38. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  39. package/dist/meetings/index.js +91 -1
  40. package/dist/meetings/index.js.map +1 -1
  41. package/dist/meetings/util.js +14 -0
  42. package/dist/meetings/util.js.map +1 -1
  43. package/dist/member/index.js +10 -0
  44. package/dist/member/index.js.map +1 -1
  45. package/dist/member/util.js +3 -0
  46. package/dist/member/util.js.map +1 -1
  47. package/dist/members/index.js +23 -0
  48. package/dist/members/index.js.map +1 -1
  49. package/dist/members/request.js +21 -0
  50. package/dist/members/request.js.map +1 -1
  51. package/dist/members/util.js +15 -0
  52. package/dist/members/util.js.map +1 -1
  53. package/dist/metrics/constants.js +9 -0
  54. package/dist/metrics/constants.js.map +1 -1
  55. package/dist/reachability/clusterReachability.js +63 -27
  56. package/dist/reachability/clusterReachability.js.map +1 -1
  57. package/dist/reachability/index.js +112 -47
  58. package/dist/reachability/index.js.map +1 -1
  59. package/dist/reachability/reachability.types.js +14 -0
  60. package/dist/reachability/reachability.types.js.map +1 -1
  61. package/dist/reachability/request.js +19 -3
  62. package/dist/reachability/request.js.map +1 -1
  63. package/dist/reconnection-manager/index.js +2 -2
  64. package/dist/reconnection-manager/index.js.map +1 -1
  65. package/dist/recording-controller/util.js +5 -5
  66. package/dist/recording-controller/util.js.map +1 -1
  67. package/dist/roap/index.js.map +1 -1
  68. package/dist/roap/turnDiscovery.js +45 -27
  69. package/dist/roap/turnDiscovery.js.map +1 -1
  70. package/dist/roap/types.js +17 -0
  71. package/dist/roap/types.js.map +1 -0
  72. package/dist/types/common/errors/webex-errors.d.ts +7 -1
  73. package/dist/types/config.d.ts +1 -0
  74. package/dist/types/constants.d.ts +11 -85
  75. package/dist/types/controls-options-manager/enums.d.ts +3 -1
  76. package/dist/types/controls-options-manager/types.d.ts +7 -1
  77. package/dist/types/locus-info/index.d.ts +1 -0
  78. package/dist/types/locus-info/selfUtils.d.ts +247 -1
  79. package/dist/types/meeting/in-meeting-actions.d.ts +12 -0
  80. package/dist/types/meeting/index.d.ts +54 -1
  81. package/dist/types/meeting/muteState.d.ts +0 -1
  82. package/dist/types/meeting/request.d.ts +12 -1
  83. package/dist/types/meeting/request.type.d.ts +6 -0
  84. package/dist/types/meeting/util.d.ts +3 -1
  85. package/dist/types/meeting-info/meeting-info-v2.d.ts +80 -0
  86. package/dist/types/meetings/index.d.ts +38 -0
  87. package/dist/types/member/index.d.ts +1 -0
  88. package/dist/types/members/index.d.ts +8 -0
  89. package/dist/types/members/request.d.ts +19 -0
  90. package/dist/types/members/util.d.ts +13 -0
  91. package/dist/types/metrics/constants.d.ts +9 -0
  92. package/dist/types/reachability/clusterReachability.d.ts +15 -7
  93. package/dist/types/reachability/index.d.ts +10 -1
  94. package/dist/types/reachability/reachability.types.d.ts +5 -0
  95. package/dist/types/roap/index.d.ts +3 -2
  96. package/dist/types/roap/turnDiscovery.d.ts +5 -17
  97. package/dist/types/roap/types.d.ts +16 -0
  98. package/dist/webinar/index.js +1 -1
  99. package/package.json +23 -23
  100. package/src/common/errors/webex-errors.ts +8 -1
  101. package/src/config.ts +1 -0
  102. package/src/constants.ts +18 -90
  103. package/src/controls-options-manager/enums.ts +2 -0
  104. package/src/controls-options-manager/types.ts +11 -1
  105. package/src/controls-options-manager/util.ts +62 -0
  106. package/src/locus-info/controlsUtils.ts +44 -14
  107. package/src/locus-info/index.ts +23 -1
  108. package/src/locus-info/selfUtils.ts +451 -447
  109. package/src/media/index.ts +11 -21
  110. package/src/meeting/in-meeting-actions.ts +24 -0
  111. package/src/meeting/index.ts +364 -92
  112. package/src/meeting/locusMediaRequest.ts +0 -18
  113. package/src/meeting/muteState.ts +0 -2
  114. package/src/meeting/request.ts +36 -1
  115. package/src/meeting/request.type.ts +7 -0
  116. package/src/meeting/util.ts +11 -2
  117. package/src/meeting-info/meeting-info-v2.ts +247 -6
  118. package/src/meetings/index.ts +107 -1
  119. package/src/meetings/util.ts +18 -0
  120. package/src/member/index.ts +11 -0
  121. package/src/member/util.ts +3 -0
  122. package/src/members/index.ts +25 -0
  123. package/src/members/request.ts +26 -0
  124. package/src/members/util.ts +16 -0
  125. package/src/metrics/constants.ts +9 -0
  126. package/src/reachability/clusterReachability.ts +73 -26
  127. package/src/reachability/index.ts +70 -1
  128. package/src/reachability/reachability.types.ts +6 -0
  129. package/src/reachability/request.ts +7 -0
  130. package/src/reconnection-manager/index.ts +2 -2
  131. package/src/recording-controller/util.ts +17 -13
  132. package/src/roap/index.ts +3 -7
  133. package/src/roap/turnDiscovery.ts +34 -39
  134. package/src/roap/types.ts +23 -0
  135. package/test/unit/spec/controls-options-manager/util.js +120 -0
  136. package/test/unit/spec/locus-info/controlsUtils.js +103 -9
  137. package/test/unit/spec/locus-info/index.js +28 -0
  138. package/test/unit/spec/media/index.ts +36 -16
  139. package/test/unit/spec/meeting/in-meeting-actions.ts +15 -4
  140. package/test/unit/spec/meeting/index.js +518 -34
  141. package/test/unit/spec/meeting/locusMediaRequest.ts +0 -30
  142. package/test/unit/spec/meeting/muteState.js +0 -2
  143. package/test/unit/spec/meeting/request.js +32 -1
  144. package/test/unit/spec/meeting/utils.js +119 -18
  145. package/test/unit/spec/meeting-info/meetinginfov2.js +443 -114
  146. package/test/unit/spec/meetings/index.js +120 -2
  147. package/test/unit/spec/member/index.js +7 -0
  148. package/test/unit/spec/member/util.js +24 -0
  149. package/test/unit/spec/members/index.js +103 -26
  150. package/test/unit/spec/members/request.js +45 -22
  151. package/test/unit/spec/members/utils.js +33 -0
  152. package/test/unit/spec/reachability/clusterReachability.ts +88 -56
  153. package/test/unit/spec/reachability/index.ts +101 -0
  154. package/test/unit/spec/reachability/request.js +47 -2
  155. package/test/unit/spec/reconnection-manager/index.js +4 -4
  156. 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;
@@ -2110,6 +2110,7 @@ describe('plugin-meetings', () => {
2110
2110
  someReachabilityMetric2: 'some value2',
2111
2111
  }),
2112
2112
  stopReachability: sinon.stub(),
2113
+ isSubnetReachable: sinon.stub().returns(false),
2113
2114
  };
2114
2115
 
2115
2116
  const forceRtcMetricsSend = sinon.stub().resolves();
@@ -2165,6 +2166,7 @@ describe('plugin-meetings', () => {
2165
2166
  someReachabilityMetric1: 'some value1',
2166
2167
  someReachabilityMetric2: 'some value2',
2167
2168
  selectedCandidatePairChanges: 2,
2169
+ isSubnetReachable: null,
2168
2170
  numTransports: 1,
2169
2171
  iceCandidatesCount: 0,
2170
2172
  }
@@ -2211,6 +2213,7 @@ describe('plugin-meetings', () => {
2211
2213
  signalingState: 'unknown',
2212
2214
  connectionState: 'unknown',
2213
2215
  iceConnectionState: 'unknown',
2216
+ isSubnetReachable: null,
2214
2217
  })
2215
2218
  );
2216
2219
 
@@ -2225,6 +2228,7 @@ describe('plugin-meetings', () => {
2225
2228
  someReachabilityMetric1: 'some value1',
2226
2229
  someReachabilityMetric2: 'some value2',
2227
2230
  }),
2231
+ isSubnetReachable: sinon.stub().returns(true),
2228
2232
  };
2229
2233
 
2230
2234
  meeting.waitForRemoteSDPAnswer = sinon.stub().rejects();
@@ -2275,6 +2279,7 @@ describe('plugin-meetings', () => {
2275
2279
  selectedCandidatePairChanges: 2,
2276
2280
  numTransports: 1,
2277
2281
  iceCandidatesCount: 0,
2282
+ isSubnetReachable: null,
2278
2283
  }
2279
2284
  );
2280
2285
  });
@@ -2332,6 +2337,7 @@ describe('plugin-meetings', () => {
2332
2337
  signalingState: 'have-local-offer',
2333
2338
  connectionState: 'connecting',
2334
2339
  iceConnectionState: 'checking',
2340
+ isSubnetReachable: null,
2335
2341
  })
2336
2342
  );
2337
2343
 
@@ -2389,6 +2395,7 @@ describe('plugin-meetings', () => {
2389
2395
  signalingState: 'have-local-offer',
2390
2396
  connectionState: 'connecting',
2391
2397
  iceConnectionState: 'checking',
2398
+ isSubnetReachable: null,
2392
2399
  })
2393
2400
  );
2394
2401
 
@@ -2667,7 +2674,7 @@ describe('plugin-meetings', () => {
2667
2674
 
2668
2675
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
2669
2676
  turnServerInfo: {
2670
- url: FAKE_TURN_URL,
2677
+ urls: [FAKE_TURN_URL],
2671
2678
  username: FAKE_TURN_USER,
2672
2679
  password: FAKE_TURN_PASSWORD,
2673
2680
  },
@@ -2689,7 +2696,7 @@ describe('plugin-meetings', () => {
2689
2696
  meeting.id,
2690
2697
  sinon.match({
2691
2698
  turnServerInfo: {
2692
- url: FAKE_TURN_URL,
2699
+ urls: [FAKE_TURN_URL],
2693
2700
  username: FAKE_TURN_USER,
2694
2701
  password: FAKE_TURN_PASSWORD,
2695
2702
  },
@@ -2724,8 +2731,9 @@ describe('plugin-meetings', () => {
2724
2731
  sinon.stub().returns(FAKE_ERROR));
2725
2732
  webex.meetings.reachability = {
2726
2733
  isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2727
- getReachabilityMetrics: sinon.stub().resolves(),
2734
+ getReachabilityMetrics: sinon.stub().resolves({}),
2728
2735
  stopReachability: sinon.stub(),
2736
+ isSubnetReachable: sinon.stub().returns(true),
2729
2737
  };
2730
2738
  const MOCK_CLIENT_ERROR_CODE = 2004;
2731
2739
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -2747,14 +2755,15 @@ describe('plugin-meetings', () => {
2747
2755
  .onSecondCall()
2748
2756
  .returns({
2749
2757
  turnServerInfo: {
2750
- url: FAKE_TURN_URL,
2758
+ urls: [FAKE_TURN_URL],
2751
2759
  username: FAKE_TURN_USER,
2752
2760
  password: FAKE_TURN_PASSWORD,
2753
2761
  },
2754
2762
  turnDiscoverySkippedReason: undefined,
2755
2763
  });
2756
2764
  meeting.meetingState = 'ACTIVE';
2757
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2765
+ const error = {iceConnected: false};
2766
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects(error);
2758
2767
 
2759
2768
  const forceRtcMetricsSend = sinon.stub().resolves();
2760
2769
  const closeMediaConnectionStub = sinon.stub();
@@ -2772,6 +2781,7 @@ describe('plugin-meetings', () => {
2772
2781
  })
2773
2782
  .catch((err) => {
2774
2783
  errorThrown = err;
2784
+ assert.instanceOf(err.cause, Error);
2775
2785
  assert.instanceOf(err, AddMediaFailed);
2776
2786
  });
2777
2787
 
@@ -2828,6 +2838,7 @@ describe('plugin-meetings', () => {
2828
2838
  },
2829
2839
  options: {
2830
2840
  meetingId: meeting.id,
2841
+ rawError: error,
2831
2842
  },
2832
2843
  });
2833
2844
  assert.calledWith(webex.internal.newMetrics.submitClientEvent.thirdCall, {
@@ -2839,6 +2850,7 @@ describe('plugin-meetings', () => {
2839
2850
  },
2840
2851
  options: {
2841
2852
  meetingId: meeting.id,
2853
+ rawError: error,
2842
2854
  },
2843
2855
  });
2844
2856
 
@@ -2905,6 +2917,7 @@ describe('plugin-meetings', () => {
2905
2917
  selectedCandidatePairChanges: 2,
2906
2918
  numTransports: 1,
2907
2919
  iceCandidatesCount: 0,
2920
+ isSubnetReachable: null,
2908
2921
  },
2909
2922
  ]);
2910
2923
 
@@ -2935,6 +2948,7 @@ describe('plugin-meetings', () => {
2935
2948
  .resolves(false),
2936
2949
  getReachabilityMetrics: sinon.stub().resolves({}),
2937
2950
  stopReachability: sinon.stub(),
2951
+ isSubnetReachable: sinon.stub().returns(true),
2938
2952
  };
2939
2953
  const getErrorPayloadForClientErrorCodeStub =
2940
2954
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
@@ -2959,16 +2973,19 @@ describe('plugin-meetings', () => {
2959
2973
  .onSecondCall()
2960
2974
  .returns({
2961
2975
  turnServerInfo: {
2962
- url: FAKE_TURN_URL,
2976
+ urls: [FAKE_TURN_URL],
2963
2977
  username: FAKE_TURN_USER,
2964
2978
  password: FAKE_TURN_PASSWORD,
2965
2979
  },
2966
2980
  turnDiscoverySkippedReason: undefined,
2967
2981
  });
2982
+
2983
+ const mediaConnectionError = new Error('fake error');
2984
+
2968
2985
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon
2969
2986
  .stub()
2970
2987
  .onFirstCall()
2971
- .rejects()
2988
+ .rejects(mediaConnectionError)
2972
2989
  .onSecondCall()
2973
2990
  .resolves();
2974
2991
 
@@ -3037,6 +3054,7 @@ describe('plugin-meetings', () => {
3037
3054
  },
3038
3055
  options: {
3039
3056
  meetingId: meeting.id,
3057
+ rawError: mediaConnectionError,
3040
3058
  },
3041
3059
  });
3042
3060
  assert.calledWith(webex.internal.newMetrics.submitClientEvent.thirdCall, {
@@ -3102,6 +3120,7 @@ describe('plugin-meetings', () => {
3102
3120
  retriedWithTurnServer: true,
3103
3121
  isJoinWithMediaRetry: false,
3104
3122
  iceCandidatesCount: 0,
3123
+ isSubnetReachable: null,
3105
3124
  },
3106
3125
  ]);
3107
3126
  meeting.roap.doTurnDiscovery;
@@ -3136,7 +3155,7 @@ describe('plugin-meetings', () => {
3136
3155
  .onSecondCall()
3137
3156
  .returns({
3138
3157
  turnServerInfo: {
3139
- url: FAKE_TURN_URL,
3158
+ urls: [FAKE_TURN_URL],
3140
3159
  username: FAKE_TURN_USER,
3141
3160
  password: FAKE_TURN_PASSWORD,
3142
3161
  },
@@ -3188,7 +3207,7 @@ describe('plugin-meetings', () => {
3188
3207
  .onSecondCall()
3189
3208
  .returns({
3190
3209
  turnServerInfo: {
3191
- url: FAKE_TURN_URL,
3210
+ urls: [FAKE_TURN_URL],
3192
3211
  username: FAKE_TURN_USER,
3193
3212
  password: FAKE_TURN_PASSWORD,
3194
3213
  },
@@ -3230,6 +3249,7 @@ describe('plugin-meetings', () => {
3230
3249
  someReachabilityMetric2: 'some value2',
3231
3250
  }),
3232
3251
  stopReachability: sinon.stub(),
3252
+ isSubnetReachable: sinon.stub().returns(true),
3233
3253
  };
3234
3254
  meeting.iceCandidatesCount = 3;
3235
3255
  meeting.iceCandidateErrors.set('701_error', 3);
@@ -3257,6 +3277,7 @@ describe('plugin-meetings', () => {
3257
3277
  iceCandidatesCount: 3,
3258
3278
  '701_error': 3,
3259
3279
  '701_turn_host_lookup_received_error': 1,
3280
+ isSubnetReachable: null,
3260
3281
  }
3261
3282
  );
3262
3283
 
@@ -3319,6 +3340,7 @@ describe('plugin-meetings', () => {
3319
3340
  iceConnectionState: 'unknown',
3320
3341
  selectedCandidatePairChanges: 2,
3321
3342
  numTransports: 1,
3343
+ isSubnetReachable: null,
3322
3344
  iceCandidatesCount: 0,
3323
3345
  }
3324
3346
  );
@@ -3380,6 +3402,116 @@ describe('plugin-meetings', () => {
3380
3402
  numTransports: 1,
3381
3403
  '701_error': 2,
3382
3404
  '701_turn_host_lookup_received_error': 1,
3405
+ isSubnetReachable: null,
3406
+ iceCandidatesCount: 0,
3407
+ }
3408
+ );
3409
+
3410
+ assert.isOk(errorThrown);
3411
+ });
3412
+
3413
+ it('should send valid isSubnetReachability if media connection success', async () => {
3414
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3415
+ turnServerInfo: undefined,
3416
+ turnDiscoverySkippedReason: undefined,
3417
+ });
3418
+ meeting.meetingState = 'ACTIVE';
3419
+ meeting.mediaProperties.waitForMediaConnectionConnected.resolves();
3420
+ meeting.webex.meetings.reachability = {
3421
+ getReachabilityMetrics: sinon.stub().resolves({
3422
+ reachability_public_udp_success: 5,
3423
+ }),
3424
+ stopReachability: sinon.stub(),
3425
+ isSubnetReachable: sinon.stub().returns(false),
3426
+ };
3427
+
3428
+ const forceRtcMetricsSend = sinon.stub().resolves();
3429
+ const closeMediaConnectionStub = sinon.stub();
3430
+ Media.createMediaConnection = sinon.stub().returns({
3431
+ close: closeMediaConnectionStub,
3432
+ forceRtcMetricsSend,
3433
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3434
+ initiateOffer: sinon.stub().resolves({}),
3435
+ on: sinon.stub(),
3436
+ });
3437
+
3438
+ await meeting.addMedia({
3439
+ mediaSettings: {},
3440
+ });
3441
+
3442
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
3443
+ correlation_id: meeting.correlationId,
3444
+ locus_id: meeting.locusUrl.split('/').pop(),
3445
+ connectionType: 'udp',
3446
+ selectedCandidatePairChanges: 2,
3447
+ numTransports: 1,
3448
+ isMultistream: false,
3449
+ retriedWithTurnServer: false,
3450
+ isJoinWithMediaRetry: false,
3451
+ iceCandidatesCount: 0,
3452
+ reachability_public_udp_success: 5,
3453
+ isSubnetReachable: false,
3454
+ });
3455
+ });
3456
+
3457
+ it('should send valid isSubnetReachability if media connection fails', async () => {
3458
+ let errorThrown = undefined;
3459
+
3460
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3461
+ turnServerInfo: undefined,
3462
+ turnDiscoverySkippedReason: undefined,
3463
+ });
3464
+ meeting.meetingState = 'ACTIVE';
3465
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
3466
+ meeting.webex.meetings.reachability = {
3467
+ getReachabilityMetrics: sinon.stub().resolves({
3468
+ reachability_public_udp_success: 5,
3469
+ }),
3470
+ stopReachability: sinon.stub(),
3471
+ isSubnetReachable: sinon.stub().returns(true),
3472
+ };
3473
+
3474
+ const forceRtcMetricsSend = sinon.stub().resolves();
3475
+ const closeMediaConnectionStub = sinon.stub();
3476
+ Media.createMediaConnection = sinon.stub().returns({
3477
+ close: closeMediaConnectionStub,
3478
+ forceRtcMetricsSend,
3479
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3480
+ initiateOffer: sinon.stub().resolves({}),
3481
+ on: sinon.stub(),
3482
+ });
3483
+
3484
+ await meeting
3485
+ .addMedia({
3486
+ mediaSettings: {},
3487
+ })
3488
+ .catch((err) => {
3489
+ errorThrown = err;
3490
+ assert.instanceOf(err, AddMediaFailed);
3491
+ });
3492
+
3493
+ // Check that the only metric sent is ADD_MEDIA_FAILURE
3494
+ assert.calledOnceWithExactly(
3495
+ Metrics.sendBehavioralMetric,
3496
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
3497
+ {
3498
+ correlation_id: meeting.correlationId,
3499
+ locus_id: meeting.locusUrl.split('/').pop(),
3500
+ reason: errorThrown.message,
3501
+ stack: errorThrown.stack,
3502
+ code: errorThrown.code,
3503
+ turnDiscoverySkippedReason: undefined,
3504
+ turnServerUsed: true,
3505
+ retriedWithTurnServer: false,
3506
+ isMultistream: false,
3507
+ isJoinWithMediaRetry: false,
3508
+ signalingState: 'unknown',
3509
+ connectionState: 'unknown',
3510
+ iceConnectionState: 'unknown',
3511
+ selectedCandidatePairChanges: 2,
3512
+ numTransports: 1,
3513
+ reachability_public_udp_success: 5,
3514
+ isSubnetReachable: true,
3383
3515
  iceCandidatesCount: 0,
3384
3516
  }
3385
3517
  );
@@ -3399,6 +3531,8 @@ describe('plugin-meetings', () => {
3399
3531
  meeting.config.stats.enableStatsAnalyzer = true;
3400
3532
 
3401
3533
  statsAnalyzerStub = new EventsScope();
3534
+ statsAnalyzerStub.getNetworkType = sinon.stub().returns('wifi');
3535
+
3402
3536
  // mock the StatsAnalyzer constructor
3403
3537
  sinon.stub(InternalMediaCoreModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
3404
3538
 
@@ -3439,6 +3573,40 @@ describe('plugin-meetings', () => {
3439
3573
  });
3440
3574
  });
3441
3575
 
3576
+ it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and does not send metric because we already have', async () => {
3577
+ meeting.shareCAEventSentStatus = {
3578
+ transmitStart: true,
3579
+ transmitStop: false,
3580
+ receiveStart: false,
3581
+ receiveStop: false,
3582
+ };
3583
+ statsAnalyzerStub.emit(
3584
+ {file: 'test', function: 'test'},
3585
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED,
3586
+ {mediaType: 'share'}
3587
+ );
3588
+
3589
+ assert.calledWith(
3590
+ TriggerProxy.trigger,
3591
+ sinon.match.instanceOf(Meeting),
3592
+ {
3593
+ file: 'meeting/index',
3594
+ function: 'addMedia',
3595
+ },
3596
+ EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED,
3597
+ {
3598
+ mediaType: 'share',
3599
+ }
3600
+ );
3601
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3602
+ name: 'client.media.tx.start',
3603
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3604
+ options: {
3605
+ meetingId: meeting.id,
3606
+ },
3607
+ });
3608
+ });
3609
+
3442
3610
  it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => {
3443
3611
  statsAnalyzerStub.emit(
3444
3612
  {file: 'test', function: 'test'},
@@ -3455,6 +3623,28 @@ describe('plugin-meetings', () => {
3455
3623
  });
3456
3624
  });
3457
3625
 
3626
+ it('LOCAL_MEDIA_STOPPED does not send metric because we already have', async () => {
3627
+ meeting.shareCAEventSentStatus = {
3628
+ transmitStart: false,
3629
+ transmitStop: true,
3630
+ receiveStart: false,
3631
+ receiveStop: false,
3632
+ };
3633
+ statsAnalyzerStub.emit(
3634
+ {file: 'test', function: 'test'},
3635
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED,
3636
+ {mediaType: 'share'}
3637
+ );
3638
+
3639
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3640
+ name: 'client.media.tx.stop',
3641
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3642
+ options: {
3643
+ meetingId: meeting.id,
3644
+ },
3645
+ });
3646
+ });
3647
+
3458
3648
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => {
3459
3649
  statsAnalyzerStub.emit(
3460
3650
  {file: 'test', function: 'test'},
@@ -3535,6 +3725,47 @@ describe('plugin-meetings', () => {
3535
3725
  });
3536
3726
  });
3537
3727
 
3728
+ it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and does not send metric because we already have', async () => {
3729
+ meeting.shareCAEventSentStatus = {
3730
+ transmitStart: false,
3731
+ transmitStop: false,
3732
+ receiveStart: true,
3733
+ receiveStop: false,
3734
+ };
3735
+ statsAnalyzerStub.emit(
3736
+ {file: 'test', function: 'test'},
3737
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3738
+ {mediaType: 'share'}
3739
+ );
3740
+
3741
+ assert.calledWith(
3742
+ TriggerProxy.trigger,
3743
+ sinon.match.instanceOf(Meeting),
3744
+ {
3745
+ file: 'meeting/index',
3746
+ function: 'addMedia',
3747
+ },
3748
+ EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
3749
+ {
3750
+ mediaType: 'share',
3751
+ }
3752
+ );
3753
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3754
+ name: 'client.media.render.start',
3755
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3756
+ options: {
3757
+ meetingId: meeting.id,
3758
+ },
3759
+ });
3760
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3761
+ name: 'client.media.rx.start',
3762
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3763
+ options: {
3764
+ meetingId: meeting.id,
3765
+ },
3766
+ });
3767
+ });
3768
+
3538
3769
  it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
3539
3770
  statsAnalyzerStub.emit(
3540
3771
  {file: 'test', function: 'test'},
@@ -3559,6 +3790,34 @@ describe('plugin-meetings', () => {
3559
3790
  });
3560
3791
  });
3561
3792
 
3793
+ it('REMOTE_MEDIA_STOPPED does not send metric because we already have', async () => {
3794
+ meeting.shareCAEventSentStatus = {
3795
+ transmitStart: false,
3796
+ transmitStop: false,
3797
+ receiveStart: true,
3798
+ receiveStop: true,
3799
+ };
3800
+ statsAnalyzerStub.emit(
3801
+ {file: 'test', function: 'test'},
3802
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3803
+ {mediaType: 'share'}
3804
+ );
3805
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3806
+ name: 'client.media.render.stop',
3807
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3808
+ options: {
3809
+ meetingId: meeting.id,
3810
+ },
3811
+ });
3812
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3813
+ name: 'client.media.rx.stop',
3814
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3815
+ options: {
3816
+ meetingId: meeting.id,
3817
+ },
3818
+ });
3819
+ });
3820
+
3562
3821
  it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
3563
3822
  let fakeMembersCollection = {
3564
3823
  members: {
@@ -3568,7 +3827,7 @@ describe('plugin-meetings', () => {
3568
3827
  },
3569
3828
  };
3570
3829
  sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
3571
- const fakeData = {intervalMetadata: {}, networkType: 'wifi'};
3830
+ const fakeData = {intervalMetadata: {}};
3572
3831
 
3573
3832
  statsAnalyzerStub.emit(
3574
3833
  {file: 'test', function: 'test'},
@@ -3609,7 +3868,7 @@ describe('plugin-meetings', () => {
3609
3868
  });
3610
3869
 
3611
3870
  it('calls submitMQE correctly', async () => {
3612
- const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
3871
+ const fakeData = {intervalMetadata: {bla: 'bla'}};
3613
3872
 
3614
3873
  statsAnalyzerStub.emit(
3615
3874
  {file: 'test', function: 'test'},
@@ -3640,7 +3899,7 @@ describe('plugin-meetings', () => {
3640
3899
 
3641
3900
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3642
3901
  turnServerInfo: {
3643
- url: FAKE_TURN_URL,
3902
+ urls: [FAKE_TURN_URL],
3644
3903
  username: FAKE_TURN_USER,
3645
3904
  password: FAKE_TURN_PASSWORD,
3646
3905
  },
@@ -3666,7 +3925,7 @@ describe('plugin-meetings', () => {
3666
3925
  meeting.id,
3667
3926
  sinon.match({
3668
3927
  turnServerInfo: {
3669
- url: FAKE_TURN_URL,
3928
+ urls: [FAKE_TURN_URL],
3670
3929
  username: FAKE_TURN_USER,
3671
3930
  password: FAKE_TURN_PASSWORD,
3672
3931
  },
@@ -3817,6 +4076,9 @@ describe('plugin-meetings', () => {
3817
4076
  },
3818
4077
  options: {
3819
4078
  meetingId: meeting.id,
4079
+ rawError: {
4080
+ iceConnected: false,
4081
+ },
3820
4082
  },
3821
4083
  },
3822
4084
  ]);
@@ -3887,9 +4149,29 @@ describe('plugin-meetings', () => {
3887
4149
  } catch (err) {
3888
4150
  assert.instanceOf(err, Error);
3889
4151
  assert.equal(err.message, 'setBrb failed');
3890
- assert.isRejected((Promise.reject()));
4152
+ assert.isRejected(Promise.reject());
3891
4153
  }
3892
4154
  });
4155
+
4156
+ it('updates remote mute state when brb is enabled', async () => {
4157
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4158
+
4159
+ await meeting.beRightBack(true);
4160
+
4161
+ sinon.assert.calledOnceWithExactly(
4162
+ meeting.audio.handleServerRemoteMuteUpdate,
4163
+ meeting,
4164
+ true
4165
+ );
4166
+ });
4167
+
4168
+ it('does not update remote mute state when brb is disabled', async () => {
4169
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4170
+
4171
+ await meeting.beRightBack(false);
4172
+
4173
+ assert.notCalled(meeting.audio.handleServerRemoteMuteUpdate);
4174
+ });
3893
4175
  });
3894
4176
  });
3895
4177
 
@@ -3943,7 +4225,10 @@ describe('plugin-meetings', () => {
3943
4225
  .resolves({id: 'fake clientMediaPreferences'});
3944
4226
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3945
4227
  turnServerInfo: {
3946
- url: 'turns:turn-server-url:443?transport=tcp',
4228
+ urls: [
4229
+ 'turns:turn-server-url1:443?transport=tcp',
4230
+ 'turns:turn-server-url2:443?transport=tcp',
4231
+ ],
3947
4232
  username: 'turn user',
3948
4233
  password: 'turn password',
3949
4234
  },
@@ -3961,12 +4246,10 @@ describe('plugin-meetings', () => {
3961
4246
  expectedMediaConnectionConfig = {
3962
4247
  iceServers: [
3963
4248
  {
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',
4249
+ urls: [
4250
+ 'turns:turn-server-url1:443?transport=tcp',
4251
+ 'turns:turn-server-url2:443?transport=tcp',
4252
+ ],
3970
4253
  username: 'turn user',
3971
4254
  credential: 'turn password',
3972
4255
  },
@@ -4048,9 +4331,11 @@ describe('plugin-meetings', () => {
4048
4331
  .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
4049
4332
  .returns(fakeMultistreamRoapMediaConnection);
4050
4333
 
4051
- locusMediaRequestStub = sinon
4052
- .stub(WebexPlugin.prototype, 'request')
4053
- .resolves({body: {locus: {fullState: {}}}, upload: sinon.match.instanceOf(EventEmitter), download: sinon.match.instanceOf(EventEmitter)});
4334
+ locusMediaRequestStub = sinon.stub(WebexPlugin.prototype, 'request').resolves({
4335
+ body: {locus: {fullState: {}}},
4336
+ upload: sinon.match.instanceOf(EventEmitter),
4337
+ download: sinon.match.instanceOf(EventEmitter),
4338
+ });
4054
4339
 
4055
4340
  // setup some things and mocks so that the call to join() works
4056
4341
  // (we need to call join() because it creates the LocusMediaRequest instance
@@ -5234,7 +5519,10 @@ describe('plugin-meetings', () => {
5234
5519
  // and check that when we fallback to transcoded we still do another TURN discovery
5235
5520
  await runCheck(
5236
5521
  {
5237
- url: 'turns:turn-server-url:443?transport=tcp',
5522
+ urls: [
5523
+ 'turns:turn-server-url1:443?transport=tcp',
5524
+ 'turns:turn-server-url2:443?transport=tcp',
5525
+ ],
5238
5526
  username: 'turn user',
5239
5527
  password: 'turn password',
5240
5528
  },
@@ -5248,7 +5536,10 @@ describe('plugin-meetings', () => {
5248
5536
  // but doing it just for completeness
5249
5537
  await runCheck(
5250
5538
  {
5251
- url: 'turns:turn-server-url:443?transport=tcp',
5539
+ urls: [
5540
+ 'turns:turn-server-url1:443?transport=tcp',
5541
+ 'turns:turn-server-url2:443?transport=tcp',
5542
+ ],
5252
5543
  username: 'turn user',
5253
5544
  password: 'turn password',
5254
5545
  },
@@ -7530,6 +7821,27 @@ describe('plugin-meetings', () => {
7530
7821
  });
7531
7822
  });
7532
7823
 
7824
+ describe('#setIsoLocalClientMeetingJoinTime', () => {
7825
+ it('should fallback to system clock ISO string when given an undefined value', () => {
7826
+ const currentSystemTime = new Date().toISOString();
7827
+ meeting.isoLocalClientMeetingJoinTime = undefined;
7828
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7829
+ });
7830
+
7831
+ it('should fallback to system clock ISO string when given an invalid value', () => {
7832
+ const currentSystemTime = new Date().toISOString();
7833
+ meeting.isoLocalClientMeetingJoinTime = 'invalid-date';
7834
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7835
+ });
7836
+
7837
+ it('should set the isoLocalClientMeetingJoinTime correctly for a valid date string', () => {
7838
+ const validDateString = 'Tue, 01 Apr 2025 13:00:36 GMT';
7839
+ const expectedISOString = new Date(validDateString).toISOString();
7840
+ meeting.isoLocalClientMeetingJoinTime = validDateString;
7841
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, expectedISOString);
7842
+ });
7843
+ });
7844
+
7533
7845
  describe('#updateCallStateForMetrics', () => {
7534
7846
  it('should update the callState, overriding existing values', () => {
7535
7847
  assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''});
@@ -7611,6 +7923,12 @@ describe('plugin-meetings', () => {
7611
7923
  meeting.audio = {handleLocalStreamChange: sinon.stub()};
7612
7924
  meeting.video = {handleLocalStreamChange: sinon.stub()};
7613
7925
  meeting.statsAnalyzer = {updateMediaStatus: sinon.stub()};
7926
+ meeting.shareCAEventSentStatus = {
7927
+ transmitStart: false,
7928
+ transmitStop: false,
7929
+ receiveStart: false,
7930
+ receiveStop: false,
7931
+ };
7614
7932
  fakeMultistreamRoapMediaConnection = {
7615
7933
  createSendSlot: () => {
7616
7934
  return {
@@ -7678,6 +7996,9 @@ describe('plugin-meetings', () => {
7678
7996
  });
7679
7997
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7680
7998
 
7999
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
8000
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
8001
+
7681
8002
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7682
8003
  expected: {sendShare: true},
7683
8004
  });
@@ -7698,18 +8019,23 @@ describe('plugin-meetings', () => {
7698
8019
  assert.equal(meeting.mediaProperties.shareAudioStream, stream);
7699
8020
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7700
8021
 
8022
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
8023
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
8024
+
7701
8025
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7702
8026
  expected: {sendShare: true},
7703
8027
  });
7704
8028
  };
7705
8029
 
7706
8030
  it('requests screen share floor and publishes the screen share video stream', async () => {
8031
+ meeting.shareCAEventSentStatus.transmitStart = true;
7707
8032
  await meeting.publishStreams({screenShare: {video: videoShareStream}});
7708
8033
 
7709
8034
  checkScreenShareVideoPublished(videoShareStream);
7710
8035
  });
7711
8036
 
7712
8037
  it('requests screen share floor and publishes the screen share audio stream', async () => {
8038
+ meeting.shareCAEventSentStatus.transmitStart = true;
7713
8039
  await meeting.publishStreams({screenShare: {audio: audioShareStream}});
7714
8040
 
7715
8041
  checkScreenShareAudioPublished(audioShareStream);
@@ -8596,13 +8922,19 @@ describe('plugin-meetings', () => {
8596
8922
  const fakeErrorMessage = 'test error';
8597
8923
  const fakeRootCauseName = 'root cause name';
8598
8924
  const fakeErrorName = 'test error name';
8925
+ let clock;
8599
8926
 
8600
8927
  beforeEach(() => {
8928
+ clock = sinon.useFakeTimers();
8601
8929
  meeting.setupMediaConnectionListeners();
8602
8930
  webex.internal.newMetrics.submitClientEvent.resetHistory();
8603
8931
  Metrics.sendBehavioralMetric.resetHistory();
8604
8932
  });
8605
8933
 
8934
+ afterEach(() => {
8935
+ clock.restore();
8936
+ });
8937
+
8606
8938
  const checkMetricSent = (event, error) => {
8607
8939
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
8608
8940
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -8671,6 +9003,13 @@ describe('plugin-meetings', () => {
8671
9003
  });
8672
9004
 
8673
9005
  it('should send metrics for SdpAnswerHandlingError error', () => {
9006
+ meeting.sdpResponseTimer = '1234';
9007
+ meeting.deferSDPAnswer = {
9008
+ reject: sinon.stub(),
9009
+ };
9010
+
9011
+ const clearTimeoutSpy = sinon.spy(clock, 'clearTimeout');
9012
+
8674
9013
  const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
8675
9014
  name: fakeErrorName,
8676
9015
  cause: {name: fakeRootCauseName},
@@ -8685,6 +9024,8 @@ describe('plugin-meetings', () => {
8685
9024
  fakeErrorMessage,
8686
9025
  fakeRootCauseName
8687
9026
  );
9027
+ assert.calledOnce(meeting.deferSDPAnswer.reject);
9028
+ assert.calledOnce(clearTimeoutSpy);
8688
9029
  });
8689
9030
 
8690
9031
  it('should send metrics for SdpError error', () => {
@@ -9590,6 +9931,42 @@ describe('plugin-meetings', () => {
9590
9931
  );
9591
9932
  });
9592
9933
 
9934
+ it('listens to CONTROLS_ANNOTATION_CHANGED', async () => {
9935
+ const state = {example: 'value'};
9936
+
9937
+ await meeting.locusInfo.emitScoped(
9938
+ {function: 'test', file: 'test'},
9939
+ LOCUSINFO.EVENTS.CONTROLS_ANNOTATION_CHANGED,
9940
+ {state}
9941
+ );
9942
+
9943
+ assert.calledWith(
9944
+ TriggerProxy.trigger,
9945
+ meeting,
9946
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
9947
+ EVENT_TRIGGERS.MEETING_CONTROLS_ANNOTATION_UPDATED,
9948
+ {state}
9949
+ );
9950
+ });
9951
+
9952
+ it('listens to CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED', async () => {
9953
+ const state = {example: 'value'};
9954
+
9955
+ await meeting.locusInfo.emitScoped(
9956
+ {function: 'test', file: 'test'},
9957
+ LOCUSINFO.EVENTS.CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED,
9958
+ {state}
9959
+ );
9960
+
9961
+ assert.calledWith(
9962
+ TriggerProxy.trigger,
9963
+ meeting,
9964
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
9965
+ EVENT_TRIGGERS.MEETING_CONTROLS_REMOTE_DESKTOP_CONTROL_UPDATED,
9966
+ {state}
9967
+ );
9968
+ });
9969
+
9593
9970
  it('listens to the locus interpretation update event', () => {
9594
9971
  const interpretation = {
9595
9972
  siLanguages: [{languageCode: 20, languageName: 'en'}],
@@ -9928,6 +10305,22 @@ describe('plugin-meetings', () => {
9928
10305
  });
9929
10306
  });
9930
10307
 
10308
+ describe('#emailInput', () => {
10309
+ it('should set the email input', () => {
10310
+ assert.notOk(meeting.emailInput);
10311
+ meeting.emailInput = 'current';
10312
+ assert.equal(meeting.emailInput, 'current');
10313
+ });
10314
+ });
10315
+
10316
+ describe('#userNameInput', () => {
10317
+ it('should set the user name input', () => {
10318
+ assert.notOk(meeting.userNameInput);
10319
+ meeting.userNameInput = 'current';
10320
+ assert.equal(meeting.userNameInput, 'current');
10321
+ });
10322
+ });
10323
+
9931
10324
  describe('#setPermissionTokenPayload', () => {
9932
10325
  let now;
9933
10326
  let clock;
@@ -10469,9 +10862,11 @@ describe('plugin-meetings', () => {
10469
10862
  let canUserLowerSomeoneElsesHandSpy;
10470
10863
  let waitingForOthersToJoinSpy;
10471
10864
  let canSendReactionsSpy;
10865
+ let requiresPostMeetingDataConsentPromptSpy;
10472
10866
  let canUserRenameSelfAndObservedSpy;
10473
10867
  let canUserRenameOthersSpy;
10474
10868
  let canShareWhiteBoardSpy;
10869
+ let canMoveToLobbySpy;
10475
10870
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
10476
10871
 
10477
10872
  beforeEach(() => {
@@ -10496,8 +10891,13 @@ describe('plugin-meetings', () => {
10496
10891
  waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
10497
10892
  canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
10498
10893
  canUserRenameSelfAndObservedSpy = sinon.spy(MeetingUtil, 'canUserRenameSelfAndObserved');
10894
+ requiresPostMeetingDataConsentPromptSpy = sinon.spy(
10895
+ MeetingUtil,
10896
+ 'requiresPostMeetingDataConsentPrompt'
10897
+ );
10499
10898
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
10500
10899
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
10900
+ canMoveToLobbySpy = sinon.spy(MeetingUtil, 'canMoveToLobby');
10501
10901
  });
10502
10902
 
10503
10903
  afterEach(() => {
@@ -10624,6 +11024,11 @@ describe('plugin-meetings', () => {
10624
11024
  requiredDisplayHints: [],
10625
11025
  requiredPolicies: [SELF_POLICY.SUPPORT_POLLING_AND_QA],
10626
11026
  },
11027
+ {
11028
+ actionName: 'canShareWhiteBoard',
11029
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_WHITEBOARD],
11030
+ requiredPolicies: [SELF_POLICY.SUPPORT_WHITEBOARD],
11031
+ },
10627
11032
  ],
10628
11033
  ({
10629
11034
  actionName,
@@ -11031,8 +11436,10 @@ describe('plugin-meetings', () => {
11031
11436
  assert.calledWith(waitingForOthersToJoinSpy, userDisplayHints);
11032
11437
  assert.calledWith(canSendReactionsSpy, null, userDisplayHints);
11033
11438
  assert.calledWith(canUserRenameSelfAndObservedSpy, userDisplayHints);
11439
+ assert.calledWith(requiresPostMeetingDataConsentPromptSpy, userDisplayHints);
11034
11440
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11035
- assert.calledWith(canShareWhiteBoardSpy, userDisplayHints);
11441
+ assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11442
+ assert.calledWith(canMoveToLobbySpy, userDisplayHints);
11036
11443
 
11037
11444
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11038
11445
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -11126,6 +11533,22 @@ describe('plugin-meetings', () => {
11126
11533
  requiredPolicies: [SELF_POLICY.SUPPORT_VOIP],
11127
11534
  policies: selfUserPolicies,
11128
11535
  });
11536
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11537
+ requiredHints: [DISPLAY_HINTS.ENABLE_ANNOTATION_MEETING_OPTION],
11538
+ displayHints: userDisplayHints,
11539
+ });
11540
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11541
+ requiredHints: [DISPLAY_HINTS.DISABLE_ANNOTATION_MEETING_OPTION],
11542
+ displayHints: userDisplayHints,
11543
+ });
11544
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11545
+ requiredHints: [DISPLAY_HINTS.ENABLE_RDC_MEETING_OPTION],
11546
+ displayHints: userDisplayHints,
11547
+ });
11548
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11549
+ requiredHints: [DISPLAY_HINTS.DISABLE_RDC_MEETING_OPTION],
11550
+ displayHints: userDisplayHints,
11551
+ });
11129
11552
 
11130
11553
  assert.calledWith(
11131
11554
  TriggerProxy.trigger,
@@ -11972,6 +12395,8 @@ describe('plugin-meetings', () => {
11972
12395
  // Set the webinar attendee flag
11973
12396
  meeting.webinar = {selfIsAttendee: true};
11974
12397
  meeting.locusInfo.info.isWebinar = true;
12398
+ meeting.shareCAEventSentStatus.receiveStart = true;
12399
+ meeting.shareCAEventSentStatus.receiveStop = true;
11975
12400
 
11976
12401
  // Step 1: Start sharing whiteboard A
11977
12402
  const data1 = generateData(
@@ -11995,6 +12420,8 @@ describe('plugin-meetings', () => {
11995
12420
 
11996
12421
  // Specific assertions for webinar attendee status
11997
12422
  assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
12423
+ assert.equal(meeting.shareCAEventSentStatus.receiveStart, false);
12424
+ assert.equal(meeting.shareCAEventSentStatus.receiveStop, false);
11998
12425
  });
11999
12426
  });
12000
12427
 
@@ -12650,6 +13077,31 @@ describe('plugin-meetings', () => {
12650
13077
  });
12651
13078
  });
12652
13079
  });
13080
+
13081
+ describe('handleShareVideoStreamMuteStateChange', () => {
13082
+ it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
13083
+ meeting.isMultistream = true;
13084
+ meeting.statsAnalyzer = {shareVideoEncoderImplementation: 'OpenH264'};
13085
+ meeting.mediaProperties.shareVideoStream = {
13086
+ getSettings: sinon.stub().returns({displaySurface: 'monitor', frameRate: 30}),
13087
+ };
13088
+
13089
+ meeting.handleShareVideoStreamMuteStateChange(true);
13090
+
13091
+ assert.calledOnceWithExactly(
13092
+ Metrics.sendBehavioralMetric,
13093
+ BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE,
13094
+ {
13095
+ correlationId: meeting.correlationId,
13096
+ muted: true,
13097
+ encoderImplementation: 'OpenH264',
13098
+ displaySurface: 'monitor',
13099
+ isMultistream: true,
13100
+ frameRate: 30,
13101
+ }
13102
+ );
13103
+ });
13104
+ });
12653
13105
  });
12654
13106
 
12655
13107
  describe('#startKeepAlive', () => {
@@ -12817,6 +13269,38 @@ describe('plugin-meetings', () => {
12817
13269
  });
12818
13270
  });
12819
13271
 
13272
+ describe('#setPostMeetingDataConsent', () => {
13273
+ it('should have #setPostMeetingDataConsent', () => {
13274
+ assert.exists(meeting.setPostMeetingDataConsent);
13275
+ });
13276
+
13277
+ beforeEach(() => {
13278
+ meeting.meetingRequest.setPostMeetingDataConsent = sinon
13279
+ .stub()
13280
+ .returns(Promise.resolve());
13281
+ });
13282
+
13283
+ [true, false].forEach((accept) => {
13284
+ it(`should send consent with ${accept}`, async () => {
13285
+ const id = uuidv4();
13286
+ meeting.locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`;
13287
+ meeting.deviceUrl = `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`;
13288
+ meeting.members.selfId = id;
13289
+
13290
+ const consentPromise = meeting.setPostMeetingDataConsent(accept);
13291
+
13292
+ assert.exists(consentPromise.then);
13293
+ await consentPromise;
13294
+ assert.calledOnceWithExactly(meeting.meetingRequest.setPostMeetingDataConsent, {
13295
+ locusUrl: `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`,
13296
+ postMeetingDataConsent: accept,
13297
+ selfId: id,
13298
+ deviceUrl: `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`,
13299
+ });
13300
+ });
13301
+ });
13302
+ });
13303
+
12820
13304
  describe('#sendReaction', () => {
12821
13305
  it('should have #sendReaction', () => {
12822
13306
  assert.exists(meeting.sendReaction);