@webex/plugin-meetings 3.8.0-next.5 → 3.8.0-next.51

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 (133) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/config.js +1 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/constants.js +14 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/controls-options-manager/enums.js +2 -0
  8. package/dist/controls-options-manager/enums.js.map +1 -1
  9. package/dist/controls-options-manager/types.js.map +1 -1
  10. package/dist/controls-options-manager/util.js +52 -0
  11. package/dist/controls-options-manager/util.js.map +1 -1
  12. package/dist/interpretation/index.js +1 -1
  13. package/dist/interpretation/siLanguage.js +1 -1
  14. package/dist/locus-info/controlsUtils.js +28 -10
  15. package/dist/locus-info/controlsUtils.js.map +1 -1
  16. package/dist/locus-info/index.js +20 -1
  17. package/dist/locus-info/index.js.map +1 -1
  18. package/dist/media/index.js +3 -15
  19. package/dist/media/index.js.map +1 -1
  20. package/dist/meeting/in-meeting-actions.js +11 -1
  21. package/dist/meeting/in-meeting-actions.js.map +1 -1
  22. package/dist/meeting/index.js +544 -324
  23. package/dist/meeting/index.js.map +1 -1
  24. package/dist/meeting/locusMediaRequest.js +26 -23
  25. package/dist/meeting/locusMediaRequest.js.map +1 -1
  26. package/dist/meeting/muteState.js +0 -2
  27. package/dist/meeting/muteState.js.map +1 -1
  28. package/dist/meeting/request.js +30 -0
  29. package/dist/meeting/request.js.map +1 -1
  30. package/dist/meeting/request.type.js.map +1 -1
  31. package/dist/meeting/util.js +27 -2
  32. package/dist/meeting/util.js.map +1 -1
  33. package/dist/meeting-info/meeting-info-v2.js +359 -60
  34. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  35. package/dist/meetings/index.js +69 -1
  36. package/dist/meetings/index.js.map +1 -1
  37. package/dist/meetings/util.js +14 -0
  38. package/dist/meetings/util.js.map +1 -1
  39. package/dist/member/index.js +10 -0
  40. package/dist/member/index.js.map +1 -1
  41. package/dist/member/util.js +3 -0
  42. package/dist/member/util.js.map +1 -1
  43. package/dist/metrics/constants.js +9 -0
  44. package/dist/metrics/constants.js.map +1 -1
  45. package/dist/reachability/clusterReachability.js +63 -27
  46. package/dist/reachability/clusterReachability.js.map +1 -1
  47. package/dist/reachability/index.js +112 -47
  48. package/dist/reachability/index.js.map +1 -1
  49. package/dist/reachability/reachability.types.js +14 -0
  50. package/dist/reachability/reachability.types.js.map +1 -1
  51. package/dist/reachability/request.js +19 -3
  52. package/dist/reachability/request.js.map +1 -1
  53. package/dist/reconnection-manager/index.js +2 -2
  54. package/dist/reconnection-manager/index.js.map +1 -1
  55. package/dist/recording-controller/util.js +5 -5
  56. package/dist/recording-controller/util.js.map +1 -1
  57. package/dist/roap/index.js.map +1 -1
  58. package/dist/roap/turnDiscovery.js +45 -27
  59. package/dist/roap/turnDiscovery.js.map +1 -1
  60. package/dist/roap/types.js +17 -0
  61. package/dist/roap/types.js.map +1 -0
  62. package/dist/types/config.d.ts +1 -0
  63. package/dist/types/constants.d.ts +10 -0
  64. package/dist/types/controls-options-manager/enums.d.ts +3 -1
  65. package/dist/types/controls-options-manager/types.d.ts +7 -1
  66. package/dist/types/locus-info/index.d.ts +1 -0
  67. package/dist/types/meeting/in-meeting-actions.d.ts +10 -0
  68. package/dist/types/meeting/index.d.ts +50 -3
  69. package/dist/types/meeting/muteState.d.ts +0 -1
  70. package/dist/types/meeting/request.d.ts +12 -1
  71. package/dist/types/meeting/request.type.d.ts +6 -0
  72. package/dist/types/meeting/util.d.ts +8 -1
  73. package/dist/types/meeting-info/meeting-info-v2.d.ts +80 -0
  74. package/dist/types/meetings/index.d.ts +29 -0
  75. package/dist/types/member/index.d.ts +1 -0
  76. package/dist/types/metrics/constants.d.ts +9 -0
  77. package/dist/types/reachability/clusterReachability.d.ts +15 -7
  78. package/dist/types/reachability/index.d.ts +10 -1
  79. package/dist/types/reachability/reachability.types.d.ts +5 -0
  80. package/dist/types/roap/index.d.ts +3 -2
  81. package/dist/types/roap/turnDiscovery.d.ts +5 -17
  82. package/dist/types/roap/types.d.ts +16 -0
  83. package/dist/webinar/index.js +1 -1
  84. package/package.json +22 -22
  85. package/src/config.ts +1 -0
  86. package/src/constants.ts +17 -0
  87. package/src/controls-options-manager/enums.ts +2 -0
  88. package/src/controls-options-manager/types.ts +11 -1
  89. package/src/controls-options-manager/util.ts +62 -0
  90. package/src/locus-info/controlsUtils.ts +44 -14
  91. package/src/locus-info/index.ts +23 -1
  92. package/src/media/index.ts +5 -21
  93. package/src/meeting/in-meeting-actions.ts +20 -0
  94. package/src/meeting/index.ts +351 -99
  95. package/src/meeting/locusMediaRequest.ts +33 -23
  96. package/src/meeting/muteState.ts +0 -2
  97. package/src/meeting/request.ts +36 -1
  98. package/src/meeting/request.type.ts +7 -0
  99. package/src/meeting/util.ts +27 -2
  100. package/src/meeting-info/meeting-info-v2.ts +247 -6
  101. package/src/meetings/index.ts +87 -1
  102. package/src/meetings/util.ts +18 -0
  103. package/src/member/index.ts +11 -0
  104. package/src/member/util.ts +3 -0
  105. package/src/metrics/constants.ts +9 -0
  106. package/src/reachability/clusterReachability.ts +73 -26
  107. package/src/reachability/index.ts +70 -1
  108. package/src/reachability/reachability.types.ts +6 -0
  109. package/src/reachability/request.ts +7 -0
  110. package/src/reconnection-manager/index.ts +2 -2
  111. package/src/recording-controller/util.ts +17 -13
  112. package/src/roap/index.ts +3 -7
  113. package/src/roap/turnDiscovery.ts +34 -39
  114. package/src/roap/types.ts +23 -0
  115. package/test/unit/spec/controls-options-manager/util.js +120 -0
  116. package/test/unit/spec/locus-info/controlsUtils.js +103 -9
  117. package/test/unit/spec/locus-info/index.js +28 -0
  118. package/test/unit/spec/media/index.ts +6 -16
  119. package/test/unit/spec/meeting/in-meeting-actions.ts +13 -4
  120. package/test/unit/spec/meeting/index.js +558 -145
  121. package/test/unit/spec/meeting/locusMediaRequest.ts +101 -88
  122. package/test/unit/spec/meeting/muteState.js +0 -2
  123. package/test/unit/spec/meeting/request.js +32 -1
  124. package/test/unit/spec/meeting/utils.js +123 -18
  125. package/test/unit/spec/meeting-info/meetinginfov2.js +443 -114
  126. package/test/unit/spec/meetings/index.js +96 -1
  127. package/test/unit/spec/member/index.js +7 -0
  128. package/test/unit/spec/member/util.js +24 -0
  129. package/test/unit/spec/reachability/clusterReachability.ts +88 -56
  130. package/test/unit/spec/reachability/index.ts +101 -0
  131. package/test/unit/spec/reachability/request.js +47 -2
  132. package/test/unit/spec/reconnection-manager/index.js +4 -4
  133. 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';
@@ -93,13 +94,14 @@ import CaptchaError from '../../../../src/common/errors/captcha-error';
93
94
  import PermissionError from '../../../../src/common/errors/permission';
94
95
  import JoinWebinarError from '../../../../src/common/errors/join-webinar-error';
95
96
  import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
96
- import MultistreamNotSupportedError from '../../../../src/common/errors/multistream-not-supported-error';;
97
+ import MultistreamNotSupportedError from '../../../../src/common/errors/multistream-not-supported-error';
97
98
  import testUtils from '../../../utils/testUtils';
98
99
  import {
99
100
  MeetingInfoV2CaptchaError,
100
101
  MeetingInfoV2PasswordError,
101
102
  MeetingInfoV2PolicyError,
102
- MeetingInfoV2JoinWebinarError, MeetingInfoV2JoinForbiddenError,
103
+ MeetingInfoV2JoinWebinarError,
104
+ MeetingInfoV2JoinForbiddenError,
103
105
  } from '../../../../src/meeting-info/meeting-info-v2';
104
106
  import {
105
107
  DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
@@ -114,8 +116,9 @@ import {ERROR_DESCRIPTIONS} from '@webex/internal-plugin-metrics/src/call-diagno
114
116
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
115
117
 
116
118
  import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
117
- import { createBrbState } from '@webex/plugin-meetings/src/meeting/brbState';
118
- import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
119
+ import {createBrbState} from '@webex/plugin-meetings/src/meeting/brbState';
120
+ import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
121
+ import {EventEmitter} from 'stream';
119
122
 
120
123
  describe('plugin-meetings', () => {
121
124
  const logger = {
@@ -208,6 +211,8 @@ describe('plugin-meetings', () => {
208
211
  let membersSpy;
209
212
  let meetingRequestSpy;
210
213
  let correlationId;
214
+ let isoLocalClientMeetingJoinTime;
215
+ let uploadEvent;
211
216
 
212
217
  beforeEach(() => {
213
218
  webex = new MockWebex({
@@ -248,6 +253,7 @@ describe('plugin-meetings', () => {
248
253
  getReachabilityResults: sinon.stub().resolves(undefined),
249
254
  getReachabilityMetrics: sinon.stub().resolves({}),
250
255
  stopReachability: sinon.stub(),
256
+ isSubnetReachable: sinon.stub().returns(true),
251
257
  };
252
258
  webex.internal.llm.on = sinon.stub();
253
259
  webex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
@@ -277,6 +283,8 @@ describe('plugin-meetings', () => {
277
283
  test4 = `test4-${uuid.v4()}`;
278
284
  testDestination = `testDestination-${uuid.v4()}`;
279
285
  correlationId = uuid.v4();
286
+ uploadEvent = new EventEmitter();
287
+ uploadEvent.addListener('progress', () => {});
280
288
 
281
289
  meeting = new Meeting(
282
290
  {
@@ -667,7 +675,7 @@ describe('plugin-meetings', () => {
667
675
  beforeEach(() => {
668
676
  meeting.join = sinon.stub().callsFake((joinOptions) => {
669
677
  meeting.isMultistream = joinOptions.enableMultistream;
670
- return Promise.resolve(fakeJoinResult)
678
+ return Promise.resolve(fakeJoinResult);
671
679
  });
672
680
  addMediaInternalStub = sinon
673
681
  .stub(meeting, 'addMediaInternal')
@@ -1004,13 +1012,19 @@ describe('plugin-meetings', () => {
1004
1012
  .stub()
1005
1013
  .returns(fakeClientError);
1006
1014
 
1007
- // call joinWithMedia() - it should fail
1008
- await assert.isRejected(
1009
- meeting.joinWithMedia({
1015
+ const promise = meeting.joinWithMedia({
1010
1016
  joinOptions,
1011
1017
  mediaOptions,
1012
1018
  })
1013
- );
1019
+
1020
+ // call joinWithMedia() - it should fail
1021
+ await assert.isRejected(promise);
1022
+
1023
+ const rejectedError = await promise.catch((error) => error);
1024
+
1025
+ // Since the SDK has sent the CA events, we need to mark this error as handled
1026
+ // so the client doesn't try and send CA events again
1027
+ assert.isTrue(rejectedError.handledBySdk);
1014
1028
 
1015
1029
  // check the right CA events have been sent:
1016
1030
  // calls at index 0 and 2 to submitClientEvent are for "client.media.capabilities" which we don't care about in this test
@@ -1070,7 +1084,11 @@ describe('plugin-meetings', () => {
1070
1084
  mediaOptions,
1071
1085
  });
1072
1086
 
1073
- assert.deepEqual(result, {join: fakeJoinResult, media: undefined, multistreamEnabled: false});
1087
+ assert.deepEqual(result, {
1088
+ join: fakeJoinResult,
1089
+ media: undefined,
1090
+ multistreamEnabled: false,
1091
+ });
1074
1092
 
1075
1093
  assert.calledOnce(meeting.join);
1076
1094
 
@@ -1174,7 +1192,10 @@ describe('plugin-meetings', () => {
1174
1192
  type: addMediaError.name,
1175
1193
  }
1176
1194
  );
1177
- assert.calledOnceWithExactly(meeting.leave, {resourceId: undefined, reason: 'joinWithMedia failure'})
1195
+ assert.calledOnceWithExactly(meeting.leave, {
1196
+ resourceId: undefined,
1197
+ reason: 'joinWithMedia failure',
1198
+ });
1178
1199
  });
1179
1200
  });
1180
1201
 
@@ -1680,10 +1701,6 @@ describe('plugin-meetings', () => {
1680
1701
  sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult));
1681
1702
  });
1682
1703
 
1683
- afterEach(() => {
1684
- assert.exists(meeting.isoLocalClientMeetingJoinTime);
1685
- });
1686
-
1687
1704
  it('should join the meeting and return promise', async () => {
1688
1705
  const join = meeting.join({pstnAudioType: 'dial-in'});
1689
1706
  meeting.config.enableAutomaticLLM = true;
@@ -1799,6 +1816,7 @@ describe('plugin-meetings', () => {
1799
1816
  await meeting.join();
1800
1817
  joinSucceeded = true;
1801
1818
  } catch (e) {
1819
+ assert.isTrue(e.handledBySdk)
1802
1820
  assert.instanceOf(e, IntentToJoinError);
1803
1821
  }
1804
1822
  assert.isFalse(joinSucceeded);
@@ -1851,7 +1869,20 @@ describe('plugin-meetings', () => {
1851
1869
  });
1852
1870
  });
1853
1871
  it('should try to join the meeting and return promise reject', async () => {
1854
- await meeting.join().catch(() => {
1872
+ await meeting.join().catch((e) => {
1873
+ assert.isTrue(e.handledBySdk);
1874
+ assert.calledOnce(MeetingUtil.joinMeeting);
1875
+ });
1876
+ });
1877
+
1878
+ it('should try to join the meeting and return deferred promise reject', async () => {
1879
+
1880
+ // call first
1881
+ meeting.join();
1882
+
1883
+ // call 2nd time will get the deferred promise
1884
+ await meeting.join().catch((e) => {
1885
+ assert.isTrue(e.handledBySdk);
1855
1886
  assert.calledOnce(MeetingUtil.joinMeeting);
1856
1887
  });
1857
1888
  });
@@ -2098,6 +2129,7 @@ describe('plugin-meetings', () => {
2098
2129
  someReachabilityMetric2: 'some value2',
2099
2130
  }),
2100
2131
  stopReachability: sinon.stub(),
2132
+ isSubnetReachable: sinon.stub().returns(false),
2101
2133
  };
2102
2134
 
2103
2135
  const forceRtcMetricsSend = sinon.stub().resolves();
@@ -2153,6 +2185,7 @@ describe('plugin-meetings', () => {
2153
2185
  someReachabilityMetric1: 'some value1',
2154
2186
  someReachabilityMetric2: 'some value2',
2155
2187
  selectedCandidatePairChanges: 2,
2188
+ isSubnetReachable: false,
2156
2189
  numTransports: 1,
2157
2190
  iceCandidatesCount: 0,
2158
2191
  }
@@ -2199,6 +2232,7 @@ describe('plugin-meetings', () => {
2199
2232
  signalingState: 'unknown',
2200
2233
  connectionState: 'unknown',
2201
2234
  iceConnectionState: 'unknown',
2235
+ isSubnetReachable: true,
2202
2236
  })
2203
2237
  );
2204
2238
 
@@ -2213,6 +2247,7 @@ describe('plugin-meetings', () => {
2213
2247
  someReachabilityMetric1: 'some value1',
2214
2248
  someReachabilityMetric2: 'some value2',
2215
2249
  }),
2250
+ isSubnetReachable: sinon.stub().returns(true),
2216
2251
  };
2217
2252
 
2218
2253
  meeting.waitForRemoteSDPAnswer = sinon.stub().rejects();
@@ -2263,6 +2298,7 @@ describe('plugin-meetings', () => {
2263
2298
  selectedCandidatePairChanges: 2,
2264
2299
  numTransports: 1,
2265
2300
  iceCandidatesCount: 0,
2301
+ isSubnetReachable: true,
2266
2302
  }
2267
2303
  );
2268
2304
  });
@@ -2320,6 +2356,7 @@ describe('plugin-meetings', () => {
2320
2356
  signalingState: 'have-local-offer',
2321
2357
  connectionState: 'connecting',
2322
2358
  iceConnectionState: 'checking',
2359
+ isSubnetReachable: true,
2323
2360
  })
2324
2361
  );
2325
2362
 
@@ -2377,6 +2414,7 @@ describe('plugin-meetings', () => {
2377
2414
  signalingState: 'have-local-offer',
2378
2415
  connectionState: 'connecting',
2379
2416
  iceConnectionState: 'checking',
2417
+ isSubnetReachable: true,
2380
2418
  })
2381
2419
  );
2382
2420
 
@@ -2655,7 +2693,7 @@ describe('plugin-meetings', () => {
2655
2693
 
2656
2694
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
2657
2695
  turnServerInfo: {
2658
- url: FAKE_TURN_URL,
2696
+ urls: [FAKE_TURN_URL],
2659
2697
  username: FAKE_TURN_USER,
2660
2698
  password: FAKE_TURN_PASSWORD,
2661
2699
  },
@@ -2677,7 +2715,7 @@ describe('plugin-meetings', () => {
2677
2715
  meeting.id,
2678
2716
  sinon.match({
2679
2717
  turnServerInfo: {
2680
- url: FAKE_TURN_URL,
2718
+ urls: [FAKE_TURN_URL],
2681
2719
  username: FAKE_TURN_USER,
2682
2720
  password: FAKE_TURN_PASSWORD,
2683
2721
  },
@@ -2714,6 +2752,7 @@ describe('plugin-meetings', () => {
2714
2752
  isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2715
2753
  getReachabilityMetrics: sinon.stub().resolves(),
2716
2754
  stopReachability: sinon.stub(),
2755
+ isSubnetReachable: sinon.stub().returns(true),
2717
2756
  };
2718
2757
  const MOCK_CLIENT_ERROR_CODE = 2004;
2719
2758
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -2735,7 +2774,7 @@ describe('plugin-meetings', () => {
2735
2774
  .onSecondCall()
2736
2775
  .returns({
2737
2776
  turnServerInfo: {
2738
- url: FAKE_TURN_URL,
2777
+ urls: [FAKE_TURN_URL],
2739
2778
  username: FAKE_TURN_USER,
2740
2779
  password: FAKE_TURN_PASSWORD,
2741
2780
  },
@@ -2893,6 +2932,7 @@ describe('plugin-meetings', () => {
2893
2932
  selectedCandidatePairChanges: 2,
2894
2933
  numTransports: 1,
2895
2934
  iceCandidatesCount: 0,
2935
+ isSubnetReachable: true,
2896
2936
  },
2897
2937
  ]);
2898
2938
 
@@ -2923,6 +2963,7 @@ describe('plugin-meetings', () => {
2923
2963
  .resolves(false),
2924
2964
  getReachabilityMetrics: sinon.stub().resolves({}),
2925
2965
  stopReachability: sinon.stub(),
2966
+ isSubnetReachable: sinon.stub().returns(true),
2926
2967
  };
2927
2968
  const getErrorPayloadForClientErrorCodeStub =
2928
2969
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
@@ -2947,7 +2988,7 @@ describe('plugin-meetings', () => {
2947
2988
  .onSecondCall()
2948
2989
  .returns({
2949
2990
  turnServerInfo: {
2950
- url: FAKE_TURN_URL,
2991
+ urls: [FAKE_TURN_URL],
2951
2992
  username: FAKE_TURN_USER,
2952
2993
  password: FAKE_TURN_PASSWORD,
2953
2994
  },
@@ -3090,6 +3131,7 @@ describe('plugin-meetings', () => {
3090
3131
  retriedWithTurnServer: true,
3091
3132
  isJoinWithMediaRetry: false,
3092
3133
  iceCandidatesCount: 0,
3134
+ isSubnetReachable: true,
3093
3135
  },
3094
3136
  ]);
3095
3137
  meeting.roap.doTurnDiscovery;
@@ -3124,7 +3166,7 @@ describe('plugin-meetings', () => {
3124
3166
  .onSecondCall()
3125
3167
  .returns({
3126
3168
  turnServerInfo: {
3127
- url: FAKE_TURN_URL,
3169
+ urls: [FAKE_TURN_URL],
3128
3170
  username: FAKE_TURN_USER,
3129
3171
  password: FAKE_TURN_PASSWORD,
3130
3172
  },
@@ -3176,7 +3218,7 @@ describe('plugin-meetings', () => {
3176
3218
  .onSecondCall()
3177
3219
  .returns({
3178
3220
  turnServerInfo: {
3179
- url: FAKE_TURN_URL,
3221
+ urls: [FAKE_TURN_URL],
3180
3222
  username: FAKE_TURN_USER,
3181
3223
  password: FAKE_TURN_PASSWORD,
3182
3224
  },
@@ -3218,6 +3260,7 @@ describe('plugin-meetings', () => {
3218
3260
  someReachabilityMetric2: 'some value2',
3219
3261
  }),
3220
3262
  stopReachability: sinon.stub(),
3263
+ isSubnetReachable: sinon.stub().returns(true),
3221
3264
  };
3222
3265
  meeting.iceCandidatesCount = 3;
3223
3266
  meeting.iceCandidateErrors.set('701_error', 3);
@@ -3245,6 +3288,7 @@ describe('plugin-meetings', () => {
3245
3288
  iceCandidatesCount: 3,
3246
3289
  '701_error': 3,
3247
3290
  '701_turn_host_lookup_received_error': 1,
3291
+ isSubnetReachable: true,
3248
3292
  }
3249
3293
  );
3250
3294
 
@@ -3307,6 +3351,7 @@ describe('plugin-meetings', () => {
3307
3351
  iceConnectionState: 'unknown',
3308
3352
  selectedCandidatePairChanges: 2,
3309
3353
  numTransports: 1,
3354
+ isSubnetReachable: true,
3310
3355
  iceCandidatesCount: 0,
3311
3356
  }
3312
3357
  );
@@ -3368,6 +3413,7 @@ describe('plugin-meetings', () => {
3368
3413
  numTransports: 1,
3369
3414
  '701_error': 2,
3370
3415
  '701_turn_host_lookup_received_error': 1,
3416
+ isSubnetReachable: true,
3371
3417
  iceCandidatesCount: 0,
3372
3418
  }
3373
3419
  );
@@ -3427,6 +3473,40 @@ describe('plugin-meetings', () => {
3427
3473
  });
3428
3474
  });
3429
3475
 
3476
+ it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and does not send metric because we already have', async () => {
3477
+ meeting.shareCAEventSentStatus = {
3478
+ transmitStart: true,
3479
+ transmitStop: false,
3480
+ receiveStart: false,
3481
+ receiveStop: false,
3482
+ };
3483
+ statsAnalyzerStub.emit(
3484
+ {file: 'test', function: 'test'},
3485
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED,
3486
+ {mediaType: 'share'}
3487
+ );
3488
+
3489
+ assert.calledWith(
3490
+ TriggerProxy.trigger,
3491
+ sinon.match.instanceOf(Meeting),
3492
+ {
3493
+ file: 'meeting/index',
3494
+ function: 'addMedia',
3495
+ },
3496
+ EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED,
3497
+ {
3498
+ mediaType: 'share',
3499
+ }
3500
+ );
3501
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3502
+ name: 'client.media.tx.start',
3503
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3504
+ options: {
3505
+ meetingId: meeting.id,
3506
+ },
3507
+ });
3508
+ });
3509
+
3430
3510
  it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => {
3431
3511
  statsAnalyzerStub.emit(
3432
3512
  {file: 'test', function: 'test'},
@@ -3443,6 +3523,28 @@ describe('plugin-meetings', () => {
3443
3523
  });
3444
3524
  });
3445
3525
 
3526
+ it('LOCAL_MEDIA_STOPPED does not send metric because we already have', async () => {
3527
+ meeting.shareCAEventSentStatus = {
3528
+ transmitStart: false,
3529
+ transmitStop: true,
3530
+ receiveStart: false,
3531
+ receiveStop: false,
3532
+ };
3533
+ statsAnalyzerStub.emit(
3534
+ {file: 'test', function: 'test'},
3535
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED,
3536
+ {mediaType: 'share'}
3537
+ );
3538
+
3539
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3540
+ name: 'client.media.tx.stop',
3541
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3542
+ options: {
3543
+ meetingId: meeting.id,
3544
+ },
3545
+ });
3546
+ });
3547
+
3446
3548
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => {
3447
3549
  statsAnalyzerStub.emit(
3448
3550
  {file: 'test', function: 'test'},
@@ -3523,6 +3625,47 @@ describe('plugin-meetings', () => {
3523
3625
  });
3524
3626
  });
3525
3627
 
3628
+ it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and does not send metric because we already have', async () => {
3629
+ meeting.shareCAEventSentStatus = {
3630
+ transmitStart: false,
3631
+ transmitStop: false,
3632
+ receiveStart: true,
3633
+ receiveStop: false,
3634
+ };
3635
+ statsAnalyzerStub.emit(
3636
+ {file: 'test', function: 'test'},
3637
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3638
+ {mediaType: 'share'}
3639
+ );
3640
+
3641
+ assert.calledWith(
3642
+ TriggerProxy.trigger,
3643
+ sinon.match.instanceOf(Meeting),
3644
+ {
3645
+ file: 'meeting/index',
3646
+ function: 'addMedia',
3647
+ },
3648
+ EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
3649
+ {
3650
+ mediaType: 'share',
3651
+ }
3652
+ );
3653
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3654
+ name: 'client.media.render.start',
3655
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3656
+ options: {
3657
+ meetingId: meeting.id,
3658
+ },
3659
+ });
3660
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3661
+ name: 'client.media.rx.start',
3662
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3663
+ options: {
3664
+ meetingId: meeting.id,
3665
+ },
3666
+ });
3667
+ });
3668
+
3526
3669
  it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
3527
3670
  statsAnalyzerStub.emit(
3528
3671
  {file: 'test', function: 'test'},
@@ -3547,21 +3690,49 @@ describe('plugin-meetings', () => {
3547
3690
  });
3548
3691
  });
3549
3692
 
3693
+ it('REMOTE_MEDIA_STOPPED does not send metric because we already have', async () => {
3694
+ meeting.shareCAEventSentStatus = {
3695
+ transmitStart: false,
3696
+ transmitStop: false,
3697
+ receiveStart: true,
3698
+ receiveStop: true,
3699
+ };
3700
+ statsAnalyzerStub.emit(
3701
+ {file: 'test', function: 'test'},
3702
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3703
+ {mediaType: 'share'}
3704
+ );
3705
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3706
+ name: 'client.media.render.stop',
3707
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3708
+ options: {
3709
+ meetingId: meeting.id,
3710
+ },
3711
+ });
3712
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3713
+ name: 'client.media.rx.stop',
3714
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3715
+ options: {
3716
+ meetingId: meeting.id,
3717
+ },
3718
+ });
3719
+ });
3720
+
3550
3721
  it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
3551
3722
  let fakeMembersCollection = {
3552
3723
  members: {
3553
- member1: { isInMeeting: true },
3554
- member2: { isInMeeting: true },
3555
- member3: { isInMeeting: false },
3724
+ member1: {isInMeeting: true},
3725
+ member2: {isInMeeting: true},
3726
+ member3: {isInMeeting: false},
3556
3727
  },
3557
3728
  };
3558
- sinon.stub(meeting, 'getMembers').returns({ membersCollection: fakeMembersCollection });
3559
- const fakeData = { intervalMetadata: {}, networkType: 'wifi' };
3729
+ sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
3730
+ const fakeData = {intervalMetadata: {}, networkType: 'wifi'};
3560
3731
 
3561
3732
  statsAnalyzerStub.emit(
3562
- { file: 'test', function: 'test' },
3733
+ {file: 'test', function: 'test'},
3563
3734
  StatsAnalyzerEventNames.MEDIA_QUALITY,
3564
- { data: fakeData }
3735
+ {data: fakeData}
3565
3736
  );
3566
3737
 
3567
3738
  assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
@@ -3570,15 +3741,17 @@ describe('plugin-meetings', () => {
3570
3741
  meetingId: meeting.id,
3571
3742
  },
3572
3743
  payload: {
3573
- intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2))],
3744
+ intervals: [
3745
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
3746
+ ],
3574
3747
  },
3575
3748
  });
3576
3749
  fakeMembersCollection.members.member2.isInMeeting = false;
3577
3750
 
3578
3751
  statsAnalyzerStub.emit(
3579
- { file: 'test', function: 'test' },
3752
+ {file: 'test', function: 'test'},
3580
3753
  StatsAnalyzerEventNames.MEDIA_QUALITY,
3581
- { data: fakeData }
3754
+ {data: fakeData}
3582
3755
  );
3583
3756
 
3584
3757
  assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
@@ -3587,7 +3760,9 @@ describe('plugin-meetings', () => {
3587
3760
  meetingId: meeting.id,
3588
3761
  },
3589
3762
  payload: {
3590
- intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1))],
3763
+ intervals: [
3764
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1)),
3765
+ ],
3591
3766
  },
3592
3767
  });
3593
3768
  });
@@ -3624,7 +3799,7 @@ describe('plugin-meetings', () => {
3624
3799
 
3625
3800
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3626
3801
  turnServerInfo: {
3627
- url: FAKE_TURN_URL,
3802
+ urls: [FAKE_TURN_URL],
3628
3803
  username: FAKE_TURN_USER,
3629
3804
  password: FAKE_TURN_PASSWORD,
3630
3805
  },
@@ -3650,7 +3825,7 @@ describe('plugin-meetings', () => {
3650
3825
  meeting.id,
3651
3826
  sinon.match({
3652
3827
  turnServerInfo: {
3653
- url: FAKE_TURN_URL,
3828
+ urls: [FAKE_TURN_URL],
3654
3829
  username: FAKE_TURN_USER,
3655
3830
  password: FAKE_TURN_PASSWORD,
3656
3831
  },
@@ -3842,7 +4017,6 @@ describe('plugin-meetings', () => {
3842
4017
  });
3843
4018
 
3844
4019
  describe('when in a multistream meeting', () => {
3845
-
3846
4020
  beforeEach(() => {
3847
4021
  meeting.isMultistream = true;
3848
4022
  });
@@ -3853,7 +4027,7 @@ describe('plugin-meetings', () => {
3853
4027
  await brbResult;
3854
4028
  assert.exists(brbResult.then);
3855
4029
  assert.calledOnce(meeting.brbState.enable);
3856
- })
4030
+ });
3857
4031
 
3858
4032
  it('should disable #beRightBack and return a promise', async () => {
3859
4033
  const brbResult = meeting.beRightBack(false);
@@ -3861,7 +4035,7 @@ describe('plugin-meetings', () => {
3861
4035
  await brbResult;
3862
4036
  assert.exists(brbResult.then);
3863
4037
  assert.calledOnce(meeting.brbState.enable);
3864
- })
4038
+ });
3865
4039
 
3866
4040
  it('should throw an error and reject the promise if setBrb fails', async () => {
3867
4041
  const error = new Error('setBrb failed');
@@ -3872,9 +4046,29 @@ describe('plugin-meetings', () => {
3872
4046
  } catch (err) {
3873
4047
  assert.instanceOf(err, Error);
3874
4048
  assert.equal(err.message, 'setBrb failed');
3875
- assert.isRejected((Promise.reject()));
4049
+ assert.isRejected(Promise.reject());
3876
4050
  }
3877
- })
4051
+ });
4052
+
4053
+ it('updates remote mute state when brb is enabled', async () => {
4054
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4055
+
4056
+ await meeting.beRightBack(true);
4057
+
4058
+ sinon.assert.calledOnceWithExactly(
4059
+ meeting.audio.handleServerRemoteMuteUpdate,
4060
+ meeting,
4061
+ true
4062
+ );
4063
+ });
4064
+
4065
+ it('does not update remote mute state when brb is disabled', async () => {
4066
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4067
+
4068
+ await meeting.beRightBack(false);
4069
+
4070
+ assert.notCalled(meeting.audio.handleServerRemoteMuteUpdate);
4071
+ });
3878
4072
  });
3879
4073
  });
3880
4074
 
@@ -3928,7 +4122,10 @@ describe('plugin-meetings', () => {
3928
4122
  .resolves({id: 'fake clientMediaPreferences'});
3929
4123
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3930
4124
  turnServerInfo: {
3931
- url: 'turns:turn-server-url:443?transport=tcp',
4125
+ urls: [
4126
+ 'turns:turn-server-url1:443?transport=tcp',
4127
+ 'turns:turn-server-url2:443?transport=tcp',
4128
+ ],
3932
4129
  username: 'turn user',
3933
4130
  password: 'turn password',
3934
4131
  },
@@ -3946,12 +4143,10 @@ describe('plugin-meetings', () => {
3946
4143
  expectedMediaConnectionConfig = {
3947
4144
  iceServers: [
3948
4145
  {
3949
- urls: 'turn:turn-server-url:5004?transport=tcp',
3950
- username: 'turn user',
3951
- credential: 'turn password',
3952
- },
3953
- {
3954
- urls: 'turns:turn-server-url:443?transport=tcp',
4146
+ urls: [
4147
+ 'turns:turn-server-url1:443?transport=tcp',
4148
+ 'turns:turn-server-url2:443?transport=tcp',
4149
+ ],
3955
4150
  username: 'turn user',
3956
4151
  credential: 'turn password',
3957
4152
  },
@@ -4006,7 +4201,7 @@ describe('plugin-meetings', () => {
4006
4201
  initiateOffer: sinon.stub().resolves({}),
4007
4202
  update: sinon.stub().resolves({}),
4008
4203
  on: sinon.stub(),
4009
- roapMessageReceived: sinon.stub()
4204
+ roapMessageReceived: sinon.stub(),
4010
4205
  };
4011
4206
 
4012
4207
  fakeMultistreamRoapMediaConnection = {
@@ -4033,9 +4228,11 @@ describe('plugin-meetings', () => {
4033
4228
  .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
4034
4229
  .returns(fakeMultistreamRoapMediaConnection);
4035
4230
 
4036
- locusMediaRequestStub = sinon
4037
- .stub(WebexPlugin.prototype, 'request')
4038
- .resolves({body: {locus: {fullState: {}}}});
4231
+ locusMediaRequestStub = sinon.stub(WebexPlugin.prototype, 'request').resolves({
4232
+ body: {locus: {fullState: {}}},
4233
+ upload: sinon.match.instanceOf(EventEmitter),
4234
+ download: sinon.match.instanceOf(EventEmitter),
4235
+ });
4039
4236
 
4040
4237
  // setup some things and mocks so that the call to join() works
4041
4238
  // (we need to call join() because it creates the LocusMediaRequest instance
@@ -4144,6 +4341,8 @@ describe('plugin-meetings', () => {
4144
4341
  id: 'fake clientMediaPreferences',
4145
4342
  },
4146
4343
  },
4344
+ upload: sinon.match.instanceOf(EventEmitter),
4345
+ download: sinon.match.instanceOf(EventEmitter),
4147
4346
  });
4148
4347
  };
4149
4348
 
@@ -4171,6 +4370,8 @@ describe('plugin-meetings', () => {
4171
4370
  },
4172
4371
  ],
4173
4372
  },
4373
+ upload: sinon.match.instanceOf(EventEmitter),
4374
+ download: sinon.match.instanceOf(EventEmitter),
4174
4375
  });
4175
4376
  };
4176
4377
 
@@ -4195,6 +4396,8 @@ describe('plugin-meetings', () => {
4195
4396
  respOnlySdp: true,
4196
4397
  usingResource: null,
4197
4398
  },
4399
+ upload: sinon.match.instanceOf(EventEmitter),
4400
+ download: sinon.match.instanceOf(EventEmitter),
4198
4401
  });
4199
4402
  };
4200
4403
 
@@ -5213,7 +5416,10 @@ describe('plugin-meetings', () => {
5213
5416
  // and check that when we fallback to transcoded we still do another TURN discovery
5214
5417
  await runCheck(
5215
5418
  {
5216
- url: 'turns:turn-server-url:443?transport=tcp',
5419
+ urls: [
5420
+ 'turns:turn-server-url1:443?transport=tcp',
5421
+ 'turns:turn-server-url2:443?transport=tcp',
5422
+ ],
5217
5423
  username: 'turn user',
5218
5424
  password: 'turn password',
5219
5425
  },
@@ -5227,7 +5433,10 @@ describe('plugin-meetings', () => {
5227
5433
  // but doing it just for completeness
5228
5434
  await runCheck(
5229
5435
  {
5230
- url: 'turns:turn-server-url:443?transport=tcp',
5436
+ urls: [
5437
+ 'turns:turn-server-url1:443?transport=tcp',
5438
+ 'turns:turn-server-url2:443?transport=tcp',
5439
+ ],
5231
5440
  username: 'turn user',
5232
5441
  password: 'turn password',
5233
5442
  },
@@ -6337,7 +6546,10 @@ describe('plugin-meetings', () => {
6337
6546
  .throws(new MeetingInfoV2JoinForbiddenError(403003, FAKE_MEETING_INFO)),
6338
6547
  };
6339
6548
 
6340
- await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinForbiddenError);
6549
+ await assert.isRejected(
6550
+ meeting.fetchMeetingInfo({sendCAevents: true}),
6551
+ JoinForbiddenError
6552
+ );
6341
6553
 
6342
6554
  assert.calledWith(
6343
6555
  meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
@@ -6353,10 +6565,7 @@ describe('plugin-meetings', () => {
6353
6565
 
6354
6566
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6355
6567
  assert.equal(meeting.meetingInfoFailureCode, 403003);
6356
- assert.equal(
6357
- meeting.meetingInfoFailureReason,
6358
- MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH
6359
- );
6568
+ assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH);
6360
6569
  assert.equal(meeting.requiredCaptcha, null);
6361
6570
  });
6362
6571
 
@@ -6733,15 +6942,10 @@ describe('plugin-meetings', () => {
6733
6942
  meeting.attrs.meetingInfoProvider = {
6734
6943
  fetchMeetingInfo: sinon
6735
6944
  .stub()
6736
- .throws(
6737
- new MeetingInfoV2JoinWebinarError(403021, FAKE_MEETING_INFO, 'a message')
6738
- ),
6945
+ .throws(new MeetingInfoV2JoinWebinarError(403021, FAKE_MEETING_INFO, 'a message')),
6739
6946
  };
6740
6947
 
6741
- await assert.isRejected(
6742
- meeting.fetchMeetingInfo({sendCAevents: true}),
6743
- JoinWebinarError
6744
- );
6948
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
6745
6949
 
6746
6950
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6747
6951
  assert.equal(
@@ -6756,15 +6960,10 @@ describe('plugin-meetings', () => {
6756
6960
  meeting.attrs.meetingInfoProvider = {
6757
6961
  fetchMeetingInfo: sinon
6758
6962
  .stub()
6759
- .throws(
6760
- new MeetingInfoV2JoinWebinarError(403026, FAKE_MEETING_INFO, 'a message')
6761
- ),
6963
+ .throws(new MeetingInfoV2JoinWebinarError(403026, FAKE_MEETING_INFO, 'a message')),
6762
6964
  };
6763
6965
 
6764
- await assert.isRejected(
6765
- meeting.fetchMeetingInfo({sendCAevents: true}),
6766
- JoinWebinarError
6767
- );
6966
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
6768
6967
 
6769
6968
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6770
6969
  assert.equal(
@@ -6779,15 +6978,10 @@ describe('plugin-meetings', () => {
6779
6978
  meeting.attrs.meetingInfoProvider = {
6780
6979
  fetchMeetingInfo: sinon
6781
6980
  .stub()
6782
- .throws(
6783
- new MeetingInfoV2JoinWebinarError(403037, FAKE_MEETING_INFO, 'a message')
6784
- ),
6981
+ .throws(new MeetingInfoV2JoinWebinarError(403037, FAKE_MEETING_INFO, 'a message')),
6785
6982
  };
6786
6983
 
6787
- await assert.isRejected(
6788
- meeting.fetchMeetingInfo({sendCAevents: true}),
6789
- JoinWebinarError
6790
- );
6984
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
6791
6985
 
6792
6986
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6793
6987
  assert.equal(
@@ -7524,6 +7718,27 @@ describe('plugin-meetings', () => {
7524
7718
  });
7525
7719
  });
7526
7720
 
7721
+ describe('#setIsoLocalClientMeetingJoinTime', () => {
7722
+ it('should fallback to system clock ISO string when given an undefined value', () => {
7723
+ const currentSystemTime = new Date().toISOString();
7724
+ meeting.isoLocalClientMeetingJoinTime = undefined;
7725
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7726
+ });
7727
+
7728
+ it('should fallback to system clock ISO string when given an invalid value', () => {
7729
+ const currentSystemTime = new Date().toISOString();
7730
+ meeting.isoLocalClientMeetingJoinTime = 'invalid-date';
7731
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7732
+ });
7733
+
7734
+ it('should set the isoLocalClientMeetingJoinTime correctly for a valid date string', () => {
7735
+ const validDateString = 'Tue, 01 Apr 2025 13:00:36 GMT';
7736
+ const expectedISOString = new Date(validDateString).toISOString();
7737
+ meeting.isoLocalClientMeetingJoinTime = validDateString;
7738
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, expectedISOString);
7739
+ });
7740
+ });
7741
+
7527
7742
  describe('#updateCallStateForMetrics', () => {
7528
7743
  it('should update the callState, overriding existing values', () => {
7529
7744
  assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''});
@@ -7605,6 +7820,12 @@ describe('plugin-meetings', () => {
7605
7820
  meeting.audio = {handleLocalStreamChange: sinon.stub()};
7606
7821
  meeting.video = {handleLocalStreamChange: sinon.stub()};
7607
7822
  meeting.statsAnalyzer = {updateMediaStatus: sinon.stub()};
7823
+ meeting.shareCAEventSentStatus = {
7824
+ transmitStart: false,
7825
+ transmitStop: false,
7826
+ receiveStart: false,
7827
+ receiveStop: false,
7828
+ };
7608
7829
  fakeMultistreamRoapMediaConnection = {
7609
7830
  createSendSlot: () => {
7610
7831
  return {
@@ -7672,6 +7893,9 @@ describe('plugin-meetings', () => {
7672
7893
  });
7673
7894
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7674
7895
 
7896
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
7897
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
7898
+
7675
7899
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7676
7900
  expected: {sendShare: true},
7677
7901
  });
@@ -7692,18 +7916,23 @@ describe('plugin-meetings', () => {
7692
7916
  assert.equal(meeting.mediaProperties.shareAudioStream, stream);
7693
7917
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7694
7918
 
7919
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
7920
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
7921
+
7695
7922
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7696
7923
  expected: {sendShare: true},
7697
7924
  });
7698
7925
  };
7699
7926
 
7700
7927
  it('requests screen share floor and publishes the screen share video stream', async () => {
7928
+ meeting.shareCAEventSentStatus.transmitStart = true;
7701
7929
  await meeting.publishStreams({screenShare: {video: videoShareStream}});
7702
7930
 
7703
7931
  checkScreenShareVideoPublished(videoShareStream);
7704
7932
  });
7705
7933
 
7706
7934
  it('requests screen share floor and publishes the screen share audio stream', async () => {
7935
+ meeting.shareCAEventSentStatus.transmitStart = true;
7707
7936
  await meeting.publishStreams({screenShare: {audio: audioShareStream}});
7708
7937
 
7709
7938
  checkScreenShareAudioPublished(audioShareStream);
@@ -8590,21 +8819,34 @@ describe('plugin-meetings', () => {
8590
8819
  const fakeErrorMessage = 'test error';
8591
8820
  const fakeRootCauseName = 'root cause name';
8592
8821
  const fakeErrorName = 'test error name';
8822
+ let clock;
8593
8823
 
8594
8824
  beforeEach(() => {
8825
+ clock = sinon.useFakeTimers();
8595
8826
  meeting.setupMediaConnectionListeners();
8596
8827
  webex.internal.newMetrics.submitClientEvent.resetHistory();
8597
8828
  Metrics.sendBehavioralMetric.resetHistory();
8598
8829
  });
8599
8830
 
8600
- const checkMetricSent = (event, error) => {
8831
+ afterEach(() => {
8832
+ clock.restore();
8833
+ });
8834
+
8835
+ const checkMetricSent = (event, error, expectedErrorCode) => {
8601
8836
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
8602
- assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8837
+ assert.deepEqual(webex.internal.newMetrics.submitClientEvent.getCall(0).args[0], {
8603
8838
  name: event,
8604
8839
  payload: {
8605
8840
  canProceed: false,
8606
8841
  },
8607
- options: {rawError: error, meetingId: meeting.id},
8842
+ options: {
8843
+ rawError: {
8844
+ ...(error.cause ? {cause: {name: error.cause.name}} : {cause: undefined}),
8845
+ code: expectedErrorCode,
8846
+ name: error.name,
8847
+ },
8848
+ meetingId: meeting.id,
8849
+ },
8608
8850
  });
8609
8851
  };
8610
8852
 
@@ -8638,7 +8880,7 @@ describe('plugin-meetings', () => {
8638
8880
 
8639
8881
  eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
8640
8882
 
8641
- checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
8883
+ checkMetricSent('client.media-engine.local-sdp-generated', fakeError, 30005);
8642
8884
  checkBehavioralMetricSent(
8643
8885
  BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
8644
8886
  Errors.ErrorCode.SdpOfferCreationError,
@@ -8655,7 +8897,7 @@ describe('plugin-meetings', () => {
8655
8897
 
8656
8898
  eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
8657
8899
 
8658
- checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
8900
+ checkMetricSent('client.media-engine.remote-sdp-received', fakeError, 30006);
8659
8901
  checkBehavioralMetricSent(
8660
8902
  BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
8661
8903
  Errors.ErrorCode.SdpOfferHandlingError,
@@ -8665,6 +8907,13 @@ describe('plugin-meetings', () => {
8665
8907
  });
8666
8908
 
8667
8909
  it('should send metrics for SdpAnswerHandlingError error', () => {
8910
+ meeting.sdpResponseTimer = '1234';
8911
+ meeting.deferSDPAnswer = {
8912
+ reject: sinon.stub(),
8913
+ };
8914
+
8915
+ const clearTimeoutSpy = sinon.spy(clock, 'clearTimeout');
8916
+
8668
8917
  const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
8669
8918
  name: fakeErrorName,
8670
8919
  cause: {name: fakeRootCauseName},
@@ -8672,13 +8921,16 @@ describe('plugin-meetings', () => {
8672
8921
 
8673
8922
  eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
8674
8923
 
8675
- checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
8924
+ checkMetricSent('client.media-engine.remote-sdp-received', fakeError, 30004);
8676
8925
  checkBehavioralMetricSent(
8677
8926
  BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
8678
8927
  Errors.ErrorCode.SdpAnswerHandlingError,
8679
8928
  fakeErrorMessage,
8680
8929
  fakeRootCauseName
8681
8930
  );
8931
+ assert.calledOnce(meeting.deferSDPAnswer.reject);
8932
+ assert.isTrue(meeting.deferSDPAnswer.reject.getCall(0).args[0].handledBySdk);
8933
+ assert.calledOnce(clearTimeoutSpy);
8682
8934
  });
8683
8935
 
8684
8936
  it('should send metrics for SdpError error', () => {
@@ -8687,7 +8939,7 @@ describe('plugin-meetings', () => {
8687
8939
 
8688
8940
  eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
8689
8941
 
8690
- checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
8942
+ checkMetricSent('client.media-engine.local-sdp-generated', fakeError, 30002);
8691
8943
  // expectedMetadataType is the error name in this case
8692
8944
  checkBehavioralMetricSent(
8693
8945
  BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE,
@@ -8705,7 +8957,7 @@ describe('plugin-meetings', () => {
8705
8957
 
8706
8958
  eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
8707
8959
 
8708
- checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
8960
+ checkMetricSent('client.media-engine.local-sdp-generated', fakeError, 30003);
8709
8961
  // expectedMetadataType is the error name in this case
8710
8962
  checkBehavioralMetricSent(
8711
8963
  BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE,
@@ -8899,7 +9151,7 @@ describe('plugin-meetings', () => {
8899
9151
  assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
8900
9152
  clientErrorCode: expectedErrorCode,
8901
9153
  });
8902
- assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
9154
+ assert.deepEqual(webex.internal.newMetrics.submitClientEvent.getCall(0).args[0], {
8903
9155
  name: 'client.media-engine.remote-sdp-received',
8904
9156
  payload: {
8905
9157
  canProceed,
@@ -8907,9 +9159,18 @@ describe('plugin-meetings', () => {
8907
9159
  },
8908
9160
  options: {
8909
9161
  meetingId: meeting.id,
8910
- rawError: fakeError,
9162
+ rawError: fakeError instanceof MultistreamNotSupportedError ? {
9163
+ code: fakeError.code,
9164
+ name: fakeError.name,
9165
+ sdkMessage: fakeError.sdkMessage,
9166
+ error: fakeError.error,
9167
+ } : {},
8911
9168
  },
8912
9169
  });
9170
+ const actualError = webex.internal.newMetrics.submitClientEvent.getCall(0).args[0].options.rawError;
9171
+
9172
+ assert.isTrue(actualError.handledBySdk);
9173
+ assert.equal(actualError.message, fakeError.message);
8913
9174
  };
8914
9175
 
8915
9176
  it('handles OFFER message correctly when request fails', async () => {
@@ -9223,22 +9484,22 @@ describe('plugin-meetings', () => {
9223
9484
  const assertBrb = (enabled) => {
9224
9485
  meeting.brbState = createBrbState(meeting, false);
9225
9486
  meeting.locusInfo.emit(
9226
- { function: 'test', file: 'test' },
9487
+ {function: 'test', file: 'test'},
9227
9488
  LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
9228
- { brb: { enabled } },
9229
- )
9489
+ {brb: {enabled}}
9490
+ );
9230
9491
  assert.calledWithExactly(
9231
9492
  TriggerProxy.trigger,
9232
9493
  meeting,
9233
9494
  {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
9234
9495
  EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
9235
- { payload: { brb: { enabled } } },
9496
+ {payload: {brb: {enabled}}}
9236
9497
  );
9237
- }
9498
+ };
9238
9499
 
9239
9500
  assertBrb(true);
9240
9501
  assertBrb(false);
9241
- })
9502
+ });
9242
9503
 
9243
9504
  it('listens to the interpretation changed event', () => {
9244
9505
  meeting.simultaneousInterpretation.updateSelfInterpretation = sinon.stub();
@@ -9584,6 +9845,42 @@ describe('plugin-meetings', () => {
9584
9845
  );
9585
9846
  });
9586
9847
 
9848
+ it('listens to CONTROLS_ANNOTATION_CHANGED', async () => {
9849
+ const state = {example: 'value'};
9850
+
9851
+ await meeting.locusInfo.emitScoped(
9852
+ {function: 'test', file: 'test'},
9853
+ LOCUSINFO.EVENTS.CONTROLS_ANNOTATION_CHANGED,
9854
+ {state}
9855
+ );
9856
+
9857
+ assert.calledWith(
9858
+ TriggerProxy.trigger,
9859
+ meeting,
9860
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
9861
+ EVENT_TRIGGERS.MEETING_CONTROLS_ANNOTATION_UPDATED,
9862
+ {state}
9863
+ );
9864
+ });
9865
+
9866
+ it('listens to CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED', async () => {
9867
+ const state = {example: 'value'};
9868
+
9869
+ await meeting.locusInfo.emitScoped(
9870
+ {function: 'test', file: 'test'},
9871
+ LOCUSINFO.EVENTS.CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED,
9872
+ {state}
9873
+ );
9874
+
9875
+ assert.calledWith(
9876
+ TriggerProxy.trigger,
9877
+ meeting,
9878
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
9879
+ EVENT_TRIGGERS.MEETING_CONTROLS_REMOTE_DESKTOP_CONTROL_UPDATED,
9880
+ {state}
9881
+ );
9882
+ });
9883
+
9587
9884
  it('listens to the locus interpretation update event', () => {
9588
9885
  const interpretation = {
9589
9886
  siLanguages: [{languageCode: 20, languageName: 'en'}],
@@ -9922,6 +10219,22 @@ describe('plugin-meetings', () => {
9922
10219
  });
9923
10220
  });
9924
10221
 
10222
+ describe('#emailInput', () => {
10223
+ it('should set the email input', () => {
10224
+ assert.notOk(meeting.emailInput);
10225
+ meeting.emailInput = 'current';
10226
+ assert.equal(meeting.emailInput, 'current');
10227
+ });
10228
+ });
10229
+
10230
+ describe('#userNameInput', () => {
10231
+ it('should set the user name input', () => {
10232
+ assert.notOk(meeting.userNameInput);
10233
+ meeting.userNameInput = 'current';
10234
+ assert.equal(meeting.userNameInput, 'current');
10235
+ });
10236
+ });
10237
+
9925
10238
  describe('#setPermissionTokenPayload', () => {
9926
10239
  let now;
9927
10240
  let clock;
@@ -10463,6 +10776,7 @@ describe('plugin-meetings', () => {
10463
10776
  let canUserLowerSomeoneElsesHandSpy;
10464
10777
  let waitingForOthersToJoinSpy;
10465
10778
  let canSendReactionsSpy;
10779
+ let requiresPostMeetingDataConsentPromptSpy;
10466
10780
  let canUserRenameSelfAndObservedSpy;
10467
10781
  let canUserRenameOthersSpy;
10468
10782
  let canShareWhiteBoardSpy;
@@ -10490,6 +10804,10 @@ describe('plugin-meetings', () => {
10490
10804
  waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
10491
10805
  canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
10492
10806
  canUserRenameSelfAndObservedSpy = sinon.spy(MeetingUtil, 'canUserRenameSelfAndObserved');
10807
+ requiresPostMeetingDataConsentPromptSpy = sinon.spy(
10808
+ MeetingUtil,
10809
+ 'requiresPostMeetingDataConsentPrompt'
10810
+ );
10493
10811
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
10494
10812
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
10495
10813
  });
@@ -10618,6 +10936,11 @@ describe('plugin-meetings', () => {
10618
10936
  requiredDisplayHints: [],
10619
10937
  requiredPolicies: [SELF_POLICY.SUPPORT_POLLING_AND_QA],
10620
10938
  },
10939
+ {
10940
+ actionName: 'canShareWhiteBoard',
10941
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_WHITEBOARD],
10942
+ requiredPolicies: [SELF_POLICY.SUPPORT_WHITEBOARD],
10943
+ },
10621
10944
  ],
10622
10945
  ({
10623
10946
  actionName,
@@ -11025,8 +11348,9 @@ describe('plugin-meetings', () => {
11025
11348
  assert.calledWith(waitingForOthersToJoinSpy, userDisplayHints);
11026
11349
  assert.calledWith(canSendReactionsSpy, null, userDisplayHints);
11027
11350
  assert.calledWith(canUserRenameSelfAndObservedSpy, userDisplayHints);
11351
+ assert.calledWith(requiresPostMeetingDataConsentPromptSpy, userDisplayHints);
11028
11352
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11029
- assert.calledWith(canShareWhiteBoardSpy, userDisplayHints);
11353
+ assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11030
11354
 
11031
11355
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11032
11356
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -11120,6 +11444,22 @@ describe('plugin-meetings', () => {
11120
11444
  requiredPolicies: [SELF_POLICY.SUPPORT_VOIP],
11121
11445
  policies: selfUserPolicies,
11122
11446
  });
11447
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11448
+ requiredHints: [DISPLAY_HINTS.ENABLE_ANNOTATION_MEETING_OPTION],
11449
+ displayHints: userDisplayHints,
11450
+ });
11451
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11452
+ requiredHints: [DISPLAY_HINTS.DISABLE_ANNOTATION_MEETING_OPTION],
11453
+ displayHints: userDisplayHints,
11454
+ });
11455
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11456
+ requiredHints: [DISPLAY_HINTS.ENABLE_RDC_MEETING_OPTION],
11457
+ displayHints: userDisplayHints,
11458
+ });
11459
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11460
+ requiredHints: [DISPLAY_HINTS.DISABLE_RDC_MEETING_OPTION],
11461
+ displayHints: userDisplayHints,
11462
+ });
11123
11463
 
11124
11464
  assert.calledWith(
11125
11465
  TriggerProxy.trigger,
@@ -11326,18 +11666,21 @@ describe('plugin-meetings', () => {
11326
11666
  );
11327
11667
  });
11328
11668
 
11329
-
11330
11669
  it('connect ps data channel if ps started in webinar', async () => {
11331
11670
  meeting.joinedWith = {state: 'JOINED'};
11332
- meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url', practiceSessionDatachannelUrl: 'a ps datachannel url'}};
11671
+ meeting.locusInfo = {
11672
+ url: 'a url',
11673
+ info: {
11674
+ datachannelUrl: 'a datachannel url',
11675
+ practiceSessionDatachannelUrl: 'a ps datachannel url',
11676
+ },
11677
+ };
11333
11678
  meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
11334
11679
  await meeting.updateLLMConnection();
11335
11680
 
11336
11681
  assert.notCalled(webex.internal.llm.disconnectLLM);
11337
11682
  assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
11338
-
11339
11683
  });
11340
-
11341
11684
  });
11342
11685
 
11343
11686
  describe('#setLocus', () => {
@@ -11755,24 +12098,29 @@ describe('plugin-meetings', () => {
11755
12098
 
11756
12099
  activeSharingId.whiteboard = beneficiaryId;
11757
12100
 
11758
- eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11759
- eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11760
- functionName: 'remoteShare',
11761
- eventPayload: {
11762
- memberId: null,
11763
- url,
11764
- shareInstanceId,
11765
- annotationInfo: undefined,
11766
- resourceType: undefined,
11767
- },
11768
- } : {
11769
- eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11770
- functionName: 'startWhiteboardShare',
11771
- eventPayload: {resourceUrl, memberId: beneficiaryId},
11772
- });
11773
-
11774
- shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
12101
+ eventTrigger.share.push(
12102
+ meeting.webinar.selfIsAttendee
12103
+ ? {
12104
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12105
+ functionName: 'remoteShare',
12106
+ eventPayload: {
12107
+ memberId: null,
12108
+ url,
12109
+ shareInstanceId,
12110
+ annotationInfo: undefined,
12111
+ resourceType: undefined,
12112
+ },
12113
+ }
12114
+ : {
12115
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
12116
+ functionName: 'startWhiteboardShare',
12117
+ eventPayload: {resourceUrl, memberId: beneficiaryId},
12118
+ }
12119
+ );
11775
12120
 
12121
+ shareStatus = meeting.webinar.selfIsAttendee
12122
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
12123
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11776
12124
  }
11777
12125
 
11778
12126
  if (eventTrigger.member) {
@@ -11804,24 +12152,29 @@ describe('plugin-meetings', () => {
11804
12152
  newPayload.current.content.disposition = FLOOR_ACTION.ACCEPTED;
11805
12153
  newPayload.current.content.beneficiaryId = otherBeneficiaryId;
11806
12154
 
11807
- eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11808
- eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11809
- functionName: 'remoteShare',
11810
- eventPayload: {
11811
- memberId: null,
11812
- url,
11813
- shareInstanceId,
11814
- annotationInfo: undefined,
11815
- resourceType: undefined,
11816
- },
11817
- } : {
11818
- eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11819
- functionName: 'startWhiteboardShare',
11820
- eventPayload: {resourceUrl, memberId: beneficiaryId},
11821
- });
11822
-
11823
- shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
12155
+ eventTrigger.share.push(
12156
+ meeting.webinar.selfIsAttendee
12157
+ ? {
12158
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12159
+ functionName: 'remoteShare',
12160
+ eventPayload: {
12161
+ memberId: null,
12162
+ url,
12163
+ shareInstanceId,
12164
+ annotationInfo: undefined,
12165
+ resourceType: undefined,
12166
+ },
12167
+ }
12168
+ : {
12169
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
12170
+ functionName: 'startWhiteboardShare',
12171
+ eventPayload: {resourceUrl, memberId: beneficiaryId},
12172
+ }
12173
+ );
11824
12174
 
12175
+ shareStatus = meeting.webinar.selfIsAttendee
12176
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
12177
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11825
12178
  } else {
11826
12179
  eventTrigger.share.push({
11827
12180
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD,
@@ -11951,24 +12304,26 @@ describe('plugin-meetings', () => {
11951
12304
  describe('Whiteboard Share - Webinar Attendee', () => {
11952
12305
  it('Scenario #1: Whiteboard sharing as a webinar attendee', () => {
11953
12306
  // Set the webinar attendee flag
11954
- meeting.webinar = { selfIsAttendee: true };
12307
+ meeting.webinar = {selfIsAttendee: true};
11955
12308
  meeting.locusInfo.info.isWebinar = true;
12309
+ meeting.shareCAEventSentStatus.receiveStart = true;
12310
+ meeting.shareCAEventSentStatus.receiveStop = true;
11956
12311
 
11957
12312
  // Step 1: Start sharing whiteboard A
11958
12313
  const data1 = generateData(
11959
- blankPayload, // Initial payload
11960
- true, // isGranting: Granting share
11961
- false, // isContent: Whiteboard (not content)
11962
- USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
12314
+ blankPayload, // Initial payload
12315
+ true, // isGranting: Granting share
12316
+ false, // isContent: Whiteboard (not content)
12317
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
11963
12318
  RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
11964
12319
  );
11965
12320
 
11966
12321
  // Step 2: Stop sharing whiteboard A
11967
12322
  const data2 = generateData(
11968
- data1.payload, // Updated payload from Step 1
11969
- false, // isGranting: Stopping share
11970
- false, // isContent: Whiteboard
11971
- USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
12323
+ data1.payload, // Updated payload from Step 1
12324
+ false, // isGranting: Stopping share
12325
+ false, // isContent: Whiteboard
12326
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
11972
12327
  );
11973
12328
 
11974
12329
  // Validate the payload changes and status updates
@@ -11976,10 +12331,11 @@ describe('plugin-meetings', () => {
11976
12331
 
11977
12332
  // Specific assertions for webinar attendee status
11978
12333
  assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
12334
+ assert.equal(meeting.shareCAEventSentStatus.receiveStart, false);
12335
+ assert.equal(meeting.shareCAEventSentStatus.receiveStop, false);
11979
12336
  });
11980
12337
  });
11981
12338
 
11982
-
11983
12339
  describe('Whiteboard A --> Whiteboard B', () => {
11984
12340
  it('Scenario #1: you share both whiteboards', () => {
11985
12341
  const data1 = generateData(
@@ -12632,6 +12988,31 @@ describe('plugin-meetings', () => {
12632
12988
  });
12633
12989
  });
12634
12990
  });
12991
+
12992
+ describe('handleShareVideoStreamMuteStateChange', () => {
12993
+ it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
12994
+ meeting.isMultistream = true;
12995
+ meeting.statsAnalyzer = {shareVideoEncoderImplementation: 'OpenH264'};
12996
+ meeting.mediaProperties.shareVideoStream = {
12997
+ getSettings: sinon.stub().returns({displaySurface: 'monitor', frameRate: 30}),
12998
+ };
12999
+
13000
+ meeting.handleShareVideoStreamMuteStateChange(true);
13001
+
13002
+ assert.calledOnceWithExactly(
13003
+ Metrics.sendBehavioralMetric,
13004
+ BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE,
13005
+ {
13006
+ correlationId: meeting.correlationId,
13007
+ muted: true,
13008
+ encoderImplementation: 'OpenH264',
13009
+ displaySurface: 'monitor',
13010
+ isMultistream: true,
13011
+ frameRate: 30,
13012
+ }
13013
+ );
13014
+ });
13015
+ });
12635
13016
  });
12636
13017
 
12637
13018
  describe('#startKeepAlive', () => {
@@ -12799,6 +13180,38 @@ describe('plugin-meetings', () => {
12799
13180
  });
12800
13181
  });
12801
13182
 
13183
+ describe('#setPostMeetingDataConsent', () => {
13184
+ it('should have #setPostMeetingDataConsent', () => {
13185
+ assert.exists(meeting.setPostMeetingDataConsent);
13186
+ });
13187
+
13188
+ beforeEach(() => {
13189
+ meeting.meetingRequest.setPostMeetingDataConsent = sinon
13190
+ .stub()
13191
+ .returns(Promise.resolve());
13192
+ });
13193
+
13194
+ [true, false].forEach((accept) => {
13195
+ it(`should send consent with ${accept}`, async () => {
13196
+ const id = uuidv4();
13197
+ meeting.locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`;
13198
+ meeting.deviceUrl = `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`;
13199
+ meeting.members.selfId = id;
13200
+
13201
+ const consentPromise = meeting.setPostMeetingDataConsent(accept);
13202
+
13203
+ assert.exists(consentPromise.then);
13204
+ await consentPromise;
13205
+ assert.calledOnceWithExactly(meeting.meetingRequest.setPostMeetingDataConsent, {
13206
+ locusUrl: `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`,
13207
+ postMeetingDataConsent: accept,
13208
+ selfId: id,
13209
+ deviceUrl: `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`,
13210
+ });
13211
+ });
13212
+ });
13213
+ });
13214
+
12802
13215
  describe('#sendReaction', () => {
12803
13216
  it('should have #sendReaction', () => {
12804
13217
  assert.exists(meeting.sendReaction);
@@ -13290,7 +13703,7 @@ describe('plugin-meetings', () => {
13290
13703
  await meeting.roapMessageReceived(fakeMessage);
13291
13704
 
13292
13705
  assert.fail('Expected MultistreamNotSupportedError to be thrown');
13293
- } catch(e) {
13706
+ } catch (e) {
13294
13707
  assert.isTrue(e instanceof MultistreamNotSupportedError);
13295
13708
  }
13296
13709