@webex/plugin-meetings 3.8.0-web-workers-keepalive.1 → 3.8.1-next.1

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 (168) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +70 -6
  3. package/dist/breakouts/index.js.map +1 -1
  4. package/dist/common/errors/webex-errors.js +12 -2
  5. package/dist/common/errors/webex-errors.js.map +1 -1
  6. package/dist/config.js +4 -1
  7. package/dist/config.js.map +1 -1
  8. package/dist/constants.js +22 -123
  9. package/dist/constants.js.map +1 -1
  10. package/dist/controls-options-manager/enums.js +2 -0
  11. package/dist/controls-options-manager/enums.js.map +1 -1
  12. package/dist/controls-options-manager/types.js.map +1 -1
  13. package/dist/controls-options-manager/util.js +52 -0
  14. package/dist/controls-options-manager/util.js.map +1 -1
  15. package/dist/interpretation/index.js +1 -1
  16. package/dist/interpretation/siLanguage.js +1 -1
  17. package/dist/locus-info/controlsUtils.js +30 -10
  18. package/dist/locus-info/controlsUtils.js.map +1 -1
  19. package/dist/locus-info/index.js +83 -12
  20. package/dist/locus-info/index.js.map +1 -1
  21. package/dist/locus-info/selfUtils.js +432 -418
  22. package/dist/locus-info/selfUtils.js.map +1 -1
  23. package/dist/media/index.js +17 -17
  24. package/dist/media/index.js.map +1 -1
  25. package/dist/media/properties.js +94 -6
  26. package/dist/media/properties.js.map +1 -1
  27. package/dist/meeting/brbState.js +9 -2
  28. package/dist/meeting/brbState.js.map +1 -1
  29. package/dist/meeting/in-meeting-actions.js +17 -1
  30. package/dist/meeting/in-meeting-actions.js.map +1 -1
  31. package/dist/meeting/index.js +568 -328
  32. package/dist/meeting/index.js.map +1 -1
  33. package/dist/meeting/locusMediaRequest.js +0 -17
  34. package/dist/meeting/locusMediaRequest.js.map +1 -1
  35. package/dist/meeting/muteState.js +4 -4
  36. package/dist/meeting/muteState.js.map +1 -1
  37. package/dist/meeting/request.js +30 -0
  38. package/dist/meeting/request.js.map +1 -1
  39. package/dist/meeting/request.type.js.map +1 -1
  40. package/dist/meeting/util.js +9 -1
  41. package/dist/meeting/util.js.map +1 -1
  42. package/dist/meeting-info/meeting-info-v2.js +19 -13
  43. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  44. package/dist/meeting-info/utilv2.js +5 -1
  45. package/dist/meeting-info/utilv2.js.map +1 -1
  46. package/dist/meetings/index.js +76 -0
  47. package/dist/meetings/index.js.map +1 -1
  48. package/dist/meetings/util.js +14 -0
  49. package/dist/meetings/util.js.map +1 -1
  50. package/dist/member/index.js +45 -9
  51. package/dist/member/index.js.map +1 -1
  52. package/dist/member/types.js +3 -0
  53. package/dist/member/types.js.map +1 -1
  54. package/dist/member/util.js +335 -356
  55. package/dist/member/util.js.map +1 -1
  56. package/dist/members/collection.js.map +1 -1
  57. package/dist/members/index.js +137 -29
  58. package/dist/members/index.js.map +1 -1
  59. package/dist/members/request.js +38 -0
  60. package/dist/members/request.js.map +1 -1
  61. package/dist/members/util.js +36 -1
  62. package/dist/members/util.js.map +1 -1
  63. package/dist/metrics/constants.js +1 -0
  64. package/dist/metrics/constants.js.map +1 -1
  65. package/dist/reachability/clusterReachability.js +23 -31
  66. package/dist/reachability/clusterReachability.js.map +1 -1
  67. package/dist/reachability/index.js +42 -2
  68. package/dist/reachability/index.js.map +1 -1
  69. package/dist/reconnection-manager/index.js +2 -2
  70. package/dist/reconnection-manager/index.js.map +1 -1
  71. package/dist/roap/index.js.map +1 -1
  72. package/dist/roap/turnDiscovery.js +45 -27
  73. package/dist/roap/turnDiscovery.js.map +1 -1
  74. package/dist/roap/types.js +17 -0
  75. package/dist/roap/types.js.map +1 -0
  76. package/dist/types/common/errors/webex-errors.d.ts +7 -1
  77. package/dist/types/config.d.ts +2 -0
  78. package/dist/types/constants.d.ts +15 -85
  79. package/dist/types/controls-options-manager/enums.d.ts +3 -1
  80. package/dist/types/controls-options-manager/types.d.ts +7 -1
  81. package/dist/types/locus-info/index.d.ts +3 -3
  82. package/dist/types/locus-info/selfUtils.d.ts +216 -1
  83. package/dist/types/media/properties.d.ts +15 -0
  84. package/dist/types/meeting/in-meeting-actions.d.ts +16 -0
  85. package/dist/types/meeting/index.d.ts +35 -1
  86. package/dist/types/meeting/muteState.d.ts +0 -1
  87. package/dist/types/meeting/request.d.ts +12 -1
  88. package/dist/types/meeting/request.type.d.ts +6 -0
  89. package/dist/types/meeting/util.d.ts +3 -1
  90. package/dist/types/meeting-info/meeting-info-v2.d.ts +2 -1
  91. package/dist/types/meetings/index.d.ts +28 -0
  92. package/dist/types/member/index.d.ts +20 -6
  93. package/dist/types/member/types.d.ts +73 -14
  94. package/dist/types/member/util.d.ts +156 -1
  95. package/dist/types/members/collection.d.ts +6 -5
  96. package/dist/types/members/index.d.ts +32 -43
  97. package/dist/types/members/request.d.ts +26 -0
  98. package/dist/types/members/util.d.ts +27 -0
  99. package/dist/types/metrics/constants.d.ts +1 -0
  100. package/dist/types/reachability/clusterReachability.d.ts +2 -6
  101. package/dist/types/reachability/index.d.ts +8 -0
  102. package/dist/types/roap/index.d.ts +3 -2
  103. package/dist/types/roap/turnDiscovery.d.ts +5 -17
  104. package/dist/types/roap/types.d.ts +16 -0
  105. package/dist/webinar/index.js +1 -1
  106. package/package.json +24 -23
  107. package/src/breakouts/index.ts +69 -0
  108. package/src/common/errors/webex-errors.ts +8 -1
  109. package/src/config.ts +2 -0
  110. package/src/constants.ts +23 -90
  111. package/src/controls-options-manager/enums.ts +2 -0
  112. package/src/controls-options-manager/types.ts +11 -1
  113. package/src/controls-options-manager/util.ts +62 -0
  114. package/src/locus-info/controlsUtils.ts +48 -12
  115. package/src/locus-info/index.ts +88 -13
  116. package/src/locus-info/selfUtils.ts +496 -442
  117. package/src/media/index.ts +23 -21
  118. package/src/media/properties.ts +96 -0
  119. package/src/meeting/brbState.ts +11 -2
  120. package/src/meeting/in-meeting-actions.ts +32 -0
  121. package/src/meeting/index.ts +356 -87
  122. package/src/meeting/locusMediaRequest.ts +0 -18
  123. package/src/meeting/muteState.ts +4 -4
  124. package/src/meeting/request.ts +36 -1
  125. package/src/meeting/request.type.ts +7 -0
  126. package/src/meeting/util.ts +9 -1
  127. package/src/meeting-info/meeting-info-v2.ts +7 -2
  128. package/src/meeting-info/utilv2.ts +5 -0
  129. package/src/meetings/index.ts +76 -0
  130. package/src/meetings/util.ts +18 -0
  131. package/src/member/index.ts +57 -22
  132. package/src/member/types.ts +82 -16
  133. package/src/member/util.ts +357 -353
  134. package/src/members/collection.ts +4 -3
  135. package/src/members/index.ts +137 -18
  136. package/src/members/request.ts +44 -0
  137. package/src/members/util.ts +43 -1
  138. package/src/metrics/constants.ts +1 -0
  139. package/src/reachability/clusterReachability.ts +26 -25
  140. package/src/reachability/index.ts +55 -1
  141. package/src/reconnection-manager/index.ts +2 -2
  142. package/src/roap/index.ts +3 -7
  143. package/src/roap/turnDiscovery.ts +34 -39
  144. package/src/roap/types.ts +23 -0
  145. package/test/unit/spec/breakouts/index.ts +167 -95
  146. package/test/unit/spec/controls-options-manager/util.js +120 -0
  147. package/test/unit/spec/locus-info/controlsUtils.js +131 -9
  148. package/test/unit/spec/locus-info/index.js +195 -73
  149. package/test/unit/spec/locus-info/selfUtils.js +98 -24
  150. package/test/unit/spec/media/index.ts +150 -18
  151. package/test/unit/spec/media/properties.ts +130 -0
  152. package/test/unit/spec/meeting/brbState.ts +40 -2
  153. package/test/unit/spec/meeting/in-meeting-actions.ts +19 -4
  154. package/test/unit/spec/meeting/index.js +553 -36
  155. package/test/unit/spec/meeting/locusMediaRequest.ts +0 -30
  156. package/test/unit/spec/meeting/muteState.js +73 -2
  157. package/test/unit/spec/meeting/request.js +32 -1
  158. package/test/unit/spec/meeting/utils.js +79 -33
  159. package/test/unit/spec/meeting-info/meetinginfov2.js +41 -0
  160. package/test/unit/spec/meeting-info/utilv2.js +19 -0
  161. package/test/unit/spec/meetings/index.js +68 -1
  162. package/test/unit/spec/members/index.js +304 -78
  163. package/test/unit/spec/members/request.js +68 -22
  164. package/test/unit/spec/members/utils.js +75 -0
  165. package/test/unit/spec/reachability/clusterReachability.ts +41 -55
  166. package/test/unit/spec/reachability/index.ts +89 -0
  167. package/test/unit/spec/reconnection-manager/index.js +4 -4
  168. 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 = {
@@ -242,6 +243,7 @@ describe('plugin-meetings', () => {
242
243
  },
243
244
  });
244
245
 
246
+ webex.internal.newMetrics.callDiagnosticMetrics.clearErrorCache = sinon.stub();
245
247
  webex.internal.support.submitLogs = sinon.stub().returns(Promise.resolve());
246
248
  webex.internal.services = {get: sinon.stub().returns('locus-url')};
247
249
  webex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
@@ -252,6 +254,7 @@ describe('plugin-meetings', () => {
252
254
  getReachabilityResults: sinon.stub().resolves(undefined),
253
255
  getReachabilityMetrics: sinon.stub().resolves({}),
254
256
  stopReachability: sinon.stub(),
257
+ isSubnetReachable: sinon.stub().returns(true),
255
258
  };
256
259
  webex.internal.llm.on = sinon.stub();
257
260
  webex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
@@ -282,7 +285,7 @@ describe('plugin-meetings', () => {
282
285
  testDestination = `testDestination-${uuid.v4()}`;
283
286
  correlationId = uuid.v4();
284
287
  uploadEvent = new EventEmitter();
285
- uploadEvent.addListener('progress', () => {})
288
+ uploadEvent.addListener('progress', () => {});
286
289
 
287
290
  meeting = new Meeting(
288
291
  {
@@ -611,6 +614,22 @@ describe('plugin-meetings', () => {
611
614
  assert.calledWith(meeting.members.cancelPhoneInvite, uuid1);
612
615
  });
613
616
  });
617
+ describe('#cancelSIPInvite', () => {
618
+ it('should have #cancelSIPInvite', () => {
619
+ assert.exists(meeting.cancelSIPInvite);
620
+ });
621
+ beforeEach(() => {
622
+ meeting.members.cancelSIPInvite = sinon.stub().returns(Promise.resolve(test1));
623
+ });
624
+ it('should proxy members #cancelSIPInvite and return a promise', async () => {
625
+ const cancel = meeting.cancelSIPInvite({memberId: uuid1});
626
+
627
+ assert.exists(cancel.then);
628
+ await cancel;
629
+ assert.calledOnce(meeting.members.cancelSIPInvite);
630
+ assert.calledWith(meeting.members.cancelSIPInvite, {memberId: uuid1});
631
+ });
632
+ });
614
633
  describe('#admit', () => {
615
634
  it('should have #admit', () => {
616
635
  assert.exists(meeting.admit);
@@ -1204,6 +1223,46 @@ describe('plugin-meetings', () => {
1204
1223
  });
1205
1224
  });
1206
1225
 
1226
+ describe('#update spoken language', () => {
1227
+ beforeEach(() => {
1228
+ webex.internal.voicea.onSpokenLanguageUpdate = sinon.stub();
1229
+ meeting.transcription = {languageOptions: {currentSpokenLanguage: 'en'}};
1230
+ });
1231
+ afterEach(() => {
1232
+ // Restore the original methods after each test
1233
+ sinon.restore();
1234
+ });
1235
+ it('should call voicea.onSpokenLanguageUpdate when joined', async () => {
1236
+
1237
+ meeting.joinedWith = {state: 'JOINED'};
1238
+ await meeting.locusInfo.emitScoped(
1239
+ {function: 'test', file: 'test'},
1240
+ LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIPTION_SPOKEN_LANGUAGE_UPDATED,
1241
+ {spokenLanguage: 'fr'},
1242
+ );
1243
+ assert.calledWith(webex.internal.voicea.onSpokenLanguageUpdate, 'fr');
1244
+ assert.equal(meeting.transcription.languageOptions.currentSpokenLanguage, 'fr');
1245
+ assert.calledWith(
1246
+ TriggerProxy.trigger,
1247
+ meeting,
1248
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
1249
+ EVENT_TRIGGERS.MEETING_TRANSCRIPTION_SPOKEN_LANGUAGE_UPDATED
1250
+ );
1251
+ });
1252
+
1253
+ it('should also call voicea.onSpokenLanguageUpdate when not joined', async () => {
1254
+
1255
+ meeting.joinedWith = {state: 'NOT_JOINED'};
1256
+ await meeting.locusInfo.emitScoped(
1257
+ {function: 'test', file: 'test'},
1258
+ LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIPTION_SPOKEN_LANGUAGE_UPDATED,
1259
+ {spokenLanguage: 'de'},
1260
+ );
1261
+ assert.calledWith(webex.internal.voicea.onSpokenLanguageUpdate, 'de');
1262
+ assert.equal(meeting.transcription.languageOptions.currentSpokenLanguage, 'de');
1263
+ });
1264
+ });
1265
+
1207
1266
  describe('#startTranscription', () => {
1208
1267
  beforeEach(() => {
1209
1268
  webex.internal.voicea.on = sinon.stub();
@@ -2044,7 +2103,12 @@ describe('plugin-meetings', () => {
2044
2103
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
2045
2104
  meeting.mediaProperties.getCurrentConnectionInfo = sinon
2046
2105
  .stub()
2047
- .resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
2106
+ .resolves({
2107
+ connectionType: 'udp',
2108
+ selectedCandidatePairChanges: 2,
2109
+ numTransports: 1,
2110
+ ipVersion: 'IPv6',
2111
+ });
2048
2112
  meeting.audio = muteStateStub;
2049
2113
  meeting.video = muteStateStub;
2050
2114
  sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
@@ -2107,6 +2171,7 @@ describe('plugin-meetings', () => {
2107
2171
  someReachabilityMetric2: 'some value2',
2108
2172
  }),
2109
2173
  stopReachability: sinon.stub(),
2174
+ isSubnetReachable: sinon.stub().returns(false),
2110
2175
  };
2111
2176
 
2112
2177
  const forceRtcMetricsSend = sinon.stub().resolves();
@@ -2162,6 +2227,8 @@ describe('plugin-meetings', () => {
2162
2227
  someReachabilityMetric1: 'some value1',
2163
2228
  someReachabilityMetric2: 'some value2',
2164
2229
  selectedCandidatePairChanges: 2,
2230
+ isSubnetReachable: null,
2231
+ selectedCluster: null,
2165
2232
  numTransports: 1,
2166
2233
  iceCandidatesCount: 0,
2167
2234
  }
@@ -2208,6 +2275,8 @@ describe('plugin-meetings', () => {
2208
2275
  signalingState: 'unknown',
2209
2276
  connectionState: 'unknown',
2210
2277
  iceConnectionState: 'unknown',
2278
+ isSubnetReachable: null,
2279
+ selectedCluster: null,
2211
2280
  })
2212
2281
  );
2213
2282
 
@@ -2222,6 +2291,7 @@ describe('plugin-meetings', () => {
2222
2291
  someReachabilityMetric1: 'some value1',
2223
2292
  someReachabilityMetric2: 'some value2',
2224
2293
  }),
2294
+ isSubnetReachable: sinon.stub().returns(true),
2225
2295
  };
2226
2296
 
2227
2297
  meeting.waitForRemoteSDPAnswer = sinon.stub().rejects();
@@ -2272,6 +2342,8 @@ describe('plugin-meetings', () => {
2272
2342
  selectedCandidatePairChanges: 2,
2273
2343
  numTransports: 1,
2274
2344
  iceCandidatesCount: 0,
2345
+ isSubnetReachable: null,
2346
+ selectedCluster: null,
2275
2347
  }
2276
2348
  );
2277
2349
  });
@@ -2329,6 +2401,8 @@ describe('plugin-meetings', () => {
2329
2401
  signalingState: 'have-local-offer',
2330
2402
  connectionState: 'connecting',
2331
2403
  iceConnectionState: 'checking',
2404
+ isSubnetReachable: null,
2405
+ selectedCluster: null,
2332
2406
  })
2333
2407
  );
2334
2408
 
@@ -2386,6 +2460,8 @@ describe('plugin-meetings', () => {
2386
2460
  signalingState: 'have-local-offer',
2387
2461
  connectionState: 'connecting',
2388
2462
  iceConnectionState: 'checking',
2463
+ isSubnetReachable: null,
2464
+ selectedCluster: null,
2389
2465
  })
2390
2466
  );
2391
2467
 
@@ -2664,7 +2740,7 @@ describe('plugin-meetings', () => {
2664
2740
 
2665
2741
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
2666
2742
  turnServerInfo: {
2667
- url: FAKE_TURN_URL,
2743
+ urls: [FAKE_TURN_URL],
2668
2744
  username: FAKE_TURN_USER,
2669
2745
  password: FAKE_TURN_PASSWORD,
2670
2746
  },
@@ -2686,7 +2762,7 @@ describe('plugin-meetings', () => {
2686
2762
  meeting.id,
2687
2763
  sinon.match({
2688
2764
  turnServerInfo: {
2689
- url: FAKE_TURN_URL,
2765
+ urls: [FAKE_TURN_URL],
2690
2766
  username: FAKE_TURN_USER,
2691
2767
  password: FAKE_TURN_PASSWORD,
2692
2768
  },
@@ -2721,8 +2797,9 @@ describe('plugin-meetings', () => {
2721
2797
  sinon.stub().returns(FAKE_ERROR));
2722
2798
  webex.meetings.reachability = {
2723
2799
  isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2724
- getReachabilityMetrics: sinon.stub().resolves(),
2800
+ getReachabilityMetrics: sinon.stub().resolves({}),
2725
2801
  stopReachability: sinon.stub(),
2802
+ isSubnetReachable: sinon.stub().returns(true),
2726
2803
  };
2727
2804
  const MOCK_CLIENT_ERROR_CODE = 2004;
2728
2805
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -2744,14 +2821,15 @@ describe('plugin-meetings', () => {
2744
2821
  .onSecondCall()
2745
2822
  .returns({
2746
2823
  turnServerInfo: {
2747
- url: FAKE_TURN_URL,
2824
+ urls: [FAKE_TURN_URL],
2748
2825
  username: FAKE_TURN_USER,
2749
2826
  password: FAKE_TURN_PASSWORD,
2750
2827
  },
2751
2828
  turnDiscoverySkippedReason: undefined,
2752
2829
  });
2753
2830
  meeting.meetingState = 'ACTIVE';
2754
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2831
+ const error = {iceConnected: false};
2832
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects(error);
2755
2833
 
2756
2834
  const forceRtcMetricsSend = sinon.stub().resolves();
2757
2835
  const closeMediaConnectionStub = sinon.stub();
@@ -2769,6 +2847,7 @@ describe('plugin-meetings', () => {
2769
2847
  })
2770
2848
  .catch((err) => {
2771
2849
  errorThrown = err;
2850
+ assert.instanceOf(err.cause, Error);
2772
2851
  assert.instanceOf(err, AddMediaFailed);
2773
2852
  });
2774
2853
 
@@ -2825,6 +2904,7 @@ describe('plugin-meetings', () => {
2825
2904
  },
2826
2905
  options: {
2827
2906
  meetingId: meeting.id,
2907
+ rawError: error,
2828
2908
  },
2829
2909
  });
2830
2910
  assert.calledWith(webex.internal.newMetrics.submitClientEvent.thirdCall, {
@@ -2836,6 +2916,7 @@ describe('plugin-meetings', () => {
2836
2916
  },
2837
2917
  options: {
2838
2918
  meetingId: meeting.id,
2919
+ rawError: error,
2839
2920
  },
2840
2921
  });
2841
2922
 
@@ -2902,6 +2983,8 @@ describe('plugin-meetings', () => {
2902
2983
  selectedCandidatePairChanges: 2,
2903
2984
  numTransports: 1,
2904
2985
  iceCandidatesCount: 0,
2986
+ isSubnetReachable: null,
2987
+ selectedCluster: null,
2905
2988
  },
2906
2989
  ]);
2907
2990
 
@@ -2932,6 +3015,7 @@ describe('plugin-meetings', () => {
2932
3015
  .resolves(false),
2933
3016
  getReachabilityMetrics: sinon.stub().resolves({}),
2934
3017
  stopReachability: sinon.stub(),
3018
+ isSubnetReachable: sinon.stub().returns(true),
2935
3019
  };
2936
3020
  const getErrorPayloadForClientErrorCodeStub =
2937
3021
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
@@ -2956,16 +3040,19 @@ describe('plugin-meetings', () => {
2956
3040
  .onSecondCall()
2957
3041
  .returns({
2958
3042
  turnServerInfo: {
2959
- url: FAKE_TURN_URL,
3043
+ urls: [FAKE_TURN_URL],
2960
3044
  username: FAKE_TURN_USER,
2961
3045
  password: FAKE_TURN_PASSWORD,
2962
3046
  },
2963
3047
  turnDiscoverySkippedReason: undefined,
2964
3048
  });
3049
+
3050
+ const mediaConnectionError = new Error('fake error');
3051
+
2965
3052
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon
2966
3053
  .stub()
2967
3054
  .onFirstCall()
2968
- .rejects()
3055
+ .rejects(mediaConnectionError)
2969
3056
  .onSecondCall()
2970
3057
  .resolves();
2971
3058
 
@@ -3034,10 +3121,14 @@ describe('plugin-meetings', () => {
3034
3121
  },
3035
3122
  options: {
3036
3123
  meetingId: meeting.id,
3124
+ rawError: mediaConnectionError,
3037
3125
  },
3038
3126
  });
3039
3127
  assert.calledWith(webex.internal.newMetrics.submitClientEvent.thirdCall, {
3040
3128
  name: 'client.media-engine.ready',
3129
+ payload: {
3130
+ ipVersion: 'IPv6',
3131
+ },
3041
3132
  options: {
3042
3133
  meetingId: meeting.id,
3043
3134
  },
@@ -3094,11 +3185,14 @@ describe('plugin-meetings', () => {
3094
3185
  locus_id: meeting.locusUrl.split('/').pop(),
3095
3186
  connectionType: 'udp',
3096
3187
  selectedCandidatePairChanges: 2,
3188
+ ipVersion: 'IPv6',
3097
3189
  numTransports: 1,
3098
3190
  isMultistream: false,
3099
3191
  retriedWithTurnServer: true,
3100
3192
  isJoinWithMediaRetry: false,
3101
3193
  iceCandidatesCount: 0,
3194
+ isSubnetReachable: null,
3195
+ selectedCluster: null,
3102
3196
  },
3103
3197
  ]);
3104
3198
  meeting.roap.doTurnDiscovery;
@@ -3133,7 +3227,7 @@ describe('plugin-meetings', () => {
3133
3227
  .onSecondCall()
3134
3228
  .returns({
3135
3229
  turnServerInfo: {
3136
- url: FAKE_TURN_URL,
3230
+ urls: [FAKE_TURN_URL],
3137
3231
  username: FAKE_TURN_USER,
3138
3232
  password: FAKE_TURN_PASSWORD,
3139
3233
  },
@@ -3185,7 +3279,7 @@ describe('plugin-meetings', () => {
3185
3279
  .onSecondCall()
3186
3280
  .returns({
3187
3281
  turnServerInfo: {
3188
- url: FAKE_TURN_URL,
3282
+ urls: [FAKE_TURN_URL],
3189
3283
  username: FAKE_TURN_USER,
3190
3284
  password: FAKE_TURN_PASSWORD,
3191
3285
  },
@@ -3227,7 +3321,13 @@ describe('plugin-meetings', () => {
3227
3321
  someReachabilityMetric2: 'some value2',
3228
3322
  }),
3229
3323
  stopReachability: sinon.stub(),
3324
+ isSubnetReachable: sinon.stub().returns(true),
3230
3325
  };
3326
+ meeting.mediaConnections = [
3327
+ {
3328
+ mediaAgentCluster: 'some.cluster',
3329
+ }
3330
+ ]
3231
3331
  meeting.iceCandidatesCount = 3;
3232
3332
  meeting.iceCandidateErrors.set('701_error', 3);
3233
3333
  meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
@@ -3245,6 +3345,7 @@ describe('plugin-meetings', () => {
3245
3345
  locus_id: meeting.locusUrl.split('/').pop(),
3246
3346
  connectionType: 'udp',
3247
3347
  selectedCandidatePairChanges: 2,
3348
+ ipVersion: 'IPv6',
3248
3349
  numTransports: 1,
3249
3350
  isMultistream: false,
3250
3351
  retriedWithTurnServer: false,
@@ -3254,6 +3355,8 @@ describe('plugin-meetings', () => {
3254
3355
  iceCandidatesCount: 3,
3255
3356
  '701_error': 3,
3256
3357
  '701_turn_host_lookup_received_error': 1,
3358
+ isSubnetReachable: null,
3359
+ selectedCluster: 'some.cluster',
3257
3360
  }
3258
3361
  );
3259
3362
 
@@ -3316,6 +3419,8 @@ describe('plugin-meetings', () => {
3316
3419
  iceConnectionState: 'unknown',
3317
3420
  selectedCandidatePairChanges: 2,
3318
3421
  numTransports: 1,
3422
+ isSubnetReachable: null,
3423
+ selectedCluster: null,
3319
3424
  iceCandidatesCount: 0,
3320
3425
  }
3321
3426
  );
@@ -3377,6 +3482,120 @@ describe('plugin-meetings', () => {
3377
3482
  numTransports: 1,
3378
3483
  '701_error': 2,
3379
3484
  '701_turn_host_lookup_received_error': 1,
3485
+ isSubnetReachable: null,
3486
+ selectedCluster: null,
3487
+ iceCandidatesCount: 0,
3488
+ }
3489
+ );
3490
+
3491
+ assert.isOk(errorThrown);
3492
+ });
3493
+
3494
+ it('should send valid isSubnetReachability if media connection success', async () => {
3495
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3496
+ turnServerInfo: undefined,
3497
+ turnDiscoverySkippedReason: undefined,
3498
+ });
3499
+ meeting.meetingState = 'ACTIVE';
3500
+ meeting.mediaProperties.waitForMediaConnectionConnected.resolves();
3501
+ meeting.webex.meetings.reachability = {
3502
+ getReachabilityMetrics: sinon.stub().resolves({
3503
+ reachability_public_udp_success: 5,
3504
+ }),
3505
+ stopReachability: sinon.stub(),
3506
+ isSubnetReachable: sinon.stub().returns(false),
3507
+ };
3508
+
3509
+ const forceRtcMetricsSend = sinon.stub().resolves();
3510
+ const closeMediaConnectionStub = sinon.stub();
3511
+ Media.createMediaConnection = sinon.stub().returns({
3512
+ close: closeMediaConnectionStub,
3513
+ forceRtcMetricsSend,
3514
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3515
+ initiateOffer: sinon.stub().resolves({}),
3516
+ on: sinon.stub(),
3517
+ });
3518
+
3519
+ await meeting.addMedia({
3520
+ mediaSettings: {},
3521
+ });
3522
+
3523
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
3524
+ correlation_id: meeting.correlationId,
3525
+ locus_id: meeting.locusUrl.split('/').pop(),
3526
+ connectionType: 'udp',
3527
+ ipVersion: 'IPv6',
3528
+ selectedCandidatePairChanges: 2,
3529
+ numTransports: 1,
3530
+ isMultistream: false,
3531
+ retriedWithTurnServer: false,
3532
+ isJoinWithMediaRetry: false,
3533
+ iceCandidatesCount: 0,
3534
+ reachability_public_udp_success: 5,
3535
+ isSubnetReachable: false,
3536
+ selectedCluster: null,
3537
+ });
3538
+ });
3539
+
3540
+ it('should send valid isSubnetReachability if media connection fails', async () => {
3541
+ let errorThrown = undefined;
3542
+
3543
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3544
+ turnServerInfo: undefined,
3545
+ turnDiscoverySkippedReason: undefined,
3546
+ });
3547
+ meeting.meetingState = 'ACTIVE';
3548
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
3549
+ meeting.webex.meetings.reachability = {
3550
+ getReachabilityMetrics: sinon.stub().resolves({
3551
+ reachability_public_udp_success: 5,
3552
+ }),
3553
+ stopReachability: sinon.stub(),
3554
+ isSubnetReachable: sinon.stub().returns(true),
3555
+ };
3556
+
3557
+ const forceRtcMetricsSend = sinon.stub().resolves();
3558
+ const closeMediaConnectionStub = sinon.stub();
3559
+ Media.createMediaConnection = sinon.stub().returns({
3560
+ close: closeMediaConnectionStub,
3561
+ forceRtcMetricsSend,
3562
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3563
+ initiateOffer: sinon.stub().resolves({}),
3564
+ on: sinon.stub(),
3565
+ });
3566
+
3567
+ await meeting
3568
+ .addMedia({
3569
+ mediaSettings: {},
3570
+ })
3571
+ .catch((err) => {
3572
+ errorThrown = err;
3573
+ assert.instanceOf(err, AddMediaFailed);
3574
+ });
3575
+
3576
+ // Check that the only metric sent is ADD_MEDIA_FAILURE
3577
+ assert.calledOnceWithExactly(
3578
+ Metrics.sendBehavioralMetric,
3579
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
3580
+ {
3581
+ correlation_id: meeting.correlationId,
3582
+ locus_id: meeting.locusUrl.split('/').pop(),
3583
+ reason: errorThrown.message,
3584
+ stack: errorThrown.stack,
3585
+ code: errorThrown.code,
3586
+ turnDiscoverySkippedReason: undefined,
3587
+ turnServerUsed: true,
3588
+ retriedWithTurnServer: false,
3589
+ isMultistream: false,
3590
+ isJoinWithMediaRetry: false,
3591
+ signalingState: 'unknown',
3592
+ connectionState: 'unknown',
3593
+ iceConnectionState: 'unknown',
3594
+ selectedCandidatePairChanges: 2,
3595
+ numTransports: 1,
3596
+ reachability_public_udp_success: 5,
3597
+ isSubnetReachable: true,
3598
+ selectedCluster: null,
3380
3599
  iceCandidatesCount: 0,
3381
3600
  }
3382
3601
  );
@@ -3396,6 +3615,8 @@ describe('plugin-meetings', () => {
3396
3615
  meeting.config.stats.enableStatsAnalyzer = true;
3397
3616
 
3398
3617
  statsAnalyzerStub = new EventsScope();
3618
+ statsAnalyzerStub.getNetworkType = sinon.stub().returns('wifi');
3619
+
3399
3620
  // mock the StatsAnalyzer constructor
3400
3621
  sinon.stub(InternalMediaCoreModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
3401
3622
 
@@ -3436,6 +3657,40 @@ describe('plugin-meetings', () => {
3436
3657
  });
3437
3658
  });
3438
3659
 
3660
+ it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and does not send metric because we already have', async () => {
3661
+ meeting.shareCAEventSentStatus = {
3662
+ transmitStart: true,
3663
+ transmitStop: false,
3664
+ receiveStart: false,
3665
+ receiveStop: false,
3666
+ };
3667
+ statsAnalyzerStub.emit(
3668
+ {file: 'test', function: 'test'},
3669
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED,
3670
+ {mediaType: 'share'}
3671
+ );
3672
+
3673
+ assert.calledWith(
3674
+ TriggerProxy.trigger,
3675
+ sinon.match.instanceOf(Meeting),
3676
+ {
3677
+ file: 'meeting/index',
3678
+ function: 'addMedia',
3679
+ },
3680
+ EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED,
3681
+ {
3682
+ mediaType: 'share',
3683
+ }
3684
+ );
3685
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3686
+ name: 'client.media.tx.start',
3687
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3688
+ options: {
3689
+ meetingId: meeting.id,
3690
+ },
3691
+ });
3692
+ });
3693
+
3439
3694
  it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => {
3440
3695
  statsAnalyzerStub.emit(
3441
3696
  {file: 'test', function: 'test'},
@@ -3452,6 +3707,28 @@ describe('plugin-meetings', () => {
3452
3707
  });
3453
3708
  });
3454
3709
 
3710
+ it('LOCAL_MEDIA_STOPPED does not send metric because we already have', async () => {
3711
+ meeting.shareCAEventSentStatus = {
3712
+ transmitStart: false,
3713
+ transmitStop: true,
3714
+ receiveStart: false,
3715
+ receiveStop: false,
3716
+ };
3717
+ statsAnalyzerStub.emit(
3718
+ {file: 'test', function: 'test'},
3719
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED,
3720
+ {mediaType: 'share'}
3721
+ );
3722
+
3723
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3724
+ name: 'client.media.tx.stop',
3725
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3726
+ options: {
3727
+ meetingId: meeting.id,
3728
+ },
3729
+ });
3730
+ });
3731
+
3455
3732
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => {
3456
3733
  statsAnalyzerStub.emit(
3457
3734
  {file: 'test', function: 'test'},
@@ -3532,6 +3809,47 @@ describe('plugin-meetings', () => {
3532
3809
  });
3533
3810
  });
3534
3811
 
3812
+ it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and does not send metric because we already have', async () => {
3813
+ meeting.shareCAEventSentStatus = {
3814
+ transmitStart: false,
3815
+ transmitStop: false,
3816
+ receiveStart: true,
3817
+ receiveStop: false,
3818
+ };
3819
+ statsAnalyzerStub.emit(
3820
+ {file: 'test', function: 'test'},
3821
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3822
+ {mediaType: 'share'}
3823
+ );
3824
+
3825
+ assert.calledWith(
3826
+ TriggerProxy.trigger,
3827
+ sinon.match.instanceOf(Meeting),
3828
+ {
3829
+ file: 'meeting/index',
3830
+ function: 'addMedia',
3831
+ },
3832
+ EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
3833
+ {
3834
+ mediaType: 'share',
3835
+ }
3836
+ );
3837
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3838
+ name: 'client.media.render.start',
3839
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3840
+ options: {
3841
+ meetingId: meeting.id,
3842
+ },
3843
+ });
3844
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3845
+ name: 'client.media.rx.start',
3846
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3847
+ options: {
3848
+ meetingId: meeting.id,
3849
+ },
3850
+ });
3851
+ });
3852
+
3535
3853
  it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
3536
3854
  statsAnalyzerStub.emit(
3537
3855
  {file: 'test', function: 'test'},
@@ -3556,6 +3874,34 @@ describe('plugin-meetings', () => {
3556
3874
  });
3557
3875
  });
3558
3876
 
3877
+ it('REMOTE_MEDIA_STOPPED does not send metric because we already have', async () => {
3878
+ meeting.shareCAEventSentStatus = {
3879
+ transmitStart: false,
3880
+ transmitStop: false,
3881
+ receiveStart: true,
3882
+ receiveStop: true,
3883
+ };
3884
+ statsAnalyzerStub.emit(
3885
+ {file: 'test', function: 'test'},
3886
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3887
+ {mediaType: 'share'}
3888
+ );
3889
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3890
+ name: 'client.media.render.stop',
3891
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3892
+ options: {
3893
+ meetingId: meeting.id,
3894
+ },
3895
+ });
3896
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3897
+ name: 'client.media.rx.stop',
3898
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3899
+ options: {
3900
+ meetingId: meeting.id,
3901
+ },
3902
+ });
3903
+ });
3904
+
3559
3905
  it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
3560
3906
  let fakeMembersCollection = {
3561
3907
  members: {
@@ -3565,7 +3911,7 @@ describe('plugin-meetings', () => {
3565
3911
  },
3566
3912
  };
3567
3913
  sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
3568
- const fakeData = {intervalMetadata: {}, networkType: 'wifi'};
3914
+ const fakeData = {intervalMetadata: {}};
3569
3915
 
3570
3916
  statsAnalyzerStub.emit(
3571
3917
  {file: 'test', function: 'test'},
@@ -3606,7 +3952,7 @@ describe('plugin-meetings', () => {
3606
3952
  });
3607
3953
 
3608
3954
  it('calls submitMQE correctly', async () => {
3609
- const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
3955
+ const fakeData = {intervalMetadata: {bla: 'bla'}};
3610
3956
 
3611
3957
  statsAnalyzerStub.emit(
3612
3958
  {file: 'test', function: 'test'},
@@ -3637,7 +3983,7 @@ describe('plugin-meetings', () => {
3637
3983
 
3638
3984
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3639
3985
  turnServerInfo: {
3640
- url: FAKE_TURN_URL,
3986
+ urls: [FAKE_TURN_URL],
3641
3987
  username: FAKE_TURN_USER,
3642
3988
  password: FAKE_TURN_PASSWORD,
3643
3989
  },
@@ -3663,7 +4009,7 @@ describe('plugin-meetings', () => {
3663
4009
  meeting.id,
3664
4010
  sinon.match({
3665
4011
  turnServerInfo: {
3666
- url: FAKE_TURN_URL,
4012
+ urls: [FAKE_TURN_URL],
3667
4013
  username: FAKE_TURN_USER,
3668
4014
  password: FAKE_TURN_PASSWORD,
3669
4015
  },
@@ -3814,6 +4160,9 @@ describe('plugin-meetings', () => {
3814
4160
  },
3815
4161
  options: {
3816
4162
  meetingId: meeting.id,
4163
+ rawError: {
4164
+ iceConnected: false,
4165
+ },
3817
4166
  },
3818
4167
  },
3819
4168
  ]);
@@ -3843,7 +4192,7 @@ describe('plugin-meetings', () => {
3843
4192
  meeting.deviceUrl = 'device url';
3844
4193
  meeting.selfId = 'self id';
3845
4194
  meeting.brbState = createBrbState(meeting, false);
3846
- meeting.brbState.enable = sinon.stub().resolves();
4195
+ sinon.stub(meeting.brbState, 'enable').resolves();
3847
4196
  });
3848
4197
 
3849
4198
  afterEach(() => {
@@ -3884,9 +4233,42 @@ describe('plugin-meetings', () => {
3884
4233
  } catch (err) {
3885
4234
  assert.instanceOf(err, Error);
3886
4235
  assert.equal(err.message, 'setBrb failed');
3887
- assert.isRejected((Promise.reject()));
4236
+ assert.isRejected(Promise.reject());
3888
4237
  }
3889
4238
  });
4239
+
4240
+ it('updates remote mute state when brb is enabled', async () => {
4241
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4242
+
4243
+ await meeting.beRightBack(true);
4244
+
4245
+ sinon.assert.calledOnceWithExactly(
4246
+ meeting.audio.handleServerRemoteMuteUpdate,
4247
+ meeting,
4248
+ true
4249
+ );
4250
+ });
4251
+
4252
+ it('does not update remote mute state when brb is disabled', async () => {
4253
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4254
+
4255
+ await meeting.beRightBack(false);
4256
+
4257
+ assert.notCalled(meeting.audio.handleServerRemoteMuteUpdate);
4258
+ });
4259
+
4260
+ it('should reject when brb enable fails', async () => {
4261
+ meeting.brbState.enable.restore();
4262
+
4263
+ const error = new Error();
4264
+ meeting.meetingRequest.setBrb = sinon.stub().rejects(error);
4265
+
4266
+ await expect(
4267
+ meeting.beRightBack(true)
4268
+ ).to.be.rejectedWith(error);
4269
+
4270
+ assert.isFalse(meeting.brbState.state.syncToServerInProgress);
4271
+ });
3890
4272
  });
3891
4273
  });
3892
4274
 
@@ -3940,7 +4322,10 @@ describe('plugin-meetings', () => {
3940
4322
  .resolves({id: 'fake clientMediaPreferences'});
3941
4323
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3942
4324
  turnServerInfo: {
3943
- url: 'turns:turn-server-url:443?transport=tcp',
4325
+ urls: [
4326
+ 'turns:turn-server-url1:443?transport=tcp',
4327
+ 'turns:turn-server-url2:443?transport=tcp',
4328
+ ],
3944
4329
  username: 'turn user',
3945
4330
  password: 'turn password',
3946
4331
  },
@@ -3958,12 +4343,10 @@ describe('plugin-meetings', () => {
3958
4343
  expectedMediaConnectionConfig = {
3959
4344
  iceServers: [
3960
4345
  {
3961
- urls: 'turn:turn-server-url:5004?transport=tcp',
3962
- username: 'turn user',
3963
- credential: 'turn password',
3964
- },
3965
- {
3966
- urls: 'turns:turn-server-url:443?transport=tcp',
4346
+ urls: [
4347
+ 'turns:turn-server-url1:443?transport=tcp',
4348
+ 'turns:turn-server-url2:443?transport=tcp',
4349
+ ],
3967
4350
  username: 'turn user',
3968
4351
  credential: 'turn password',
3969
4352
  },
@@ -4045,9 +4428,11 @@ describe('plugin-meetings', () => {
4045
4428
  .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
4046
4429
  .returns(fakeMultistreamRoapMediaConnection);
4047
4430
 
4048
- locusMediaRequestStub = sinon
4049
- .stub(WebexPlugin.prototype, 'request')
4050
- .resolves({body: {locus: {fullState: {}}}, upload: sinon.match.instanceOf(EventEmitter), download: sinon.match.instanceOf(EventEmitter)});
4431
+ locusMediaRequestStub = sinon.stub(WebexPlugin.prototype, 'request').resolves({
4432
+ body: {locus: {fullState: {}}},
4433
+ upload: sinon.match.instanceOf(EventEmitter),
4434
+ download: sinon.match.instanceOf(EventEmitter),
4435
+ });
4051
4436
 
4052
4437
  // setup some things and mocks so that the call to join() works
4053
4438
  // (we need to call join() because it creates the LocusMediaRequest instance
@@ -5231,7 +5616,10 @@ describe('plugin-meetings', () => {
5231
5616
  // and check that when we fallback to transcoded we still do another TURN discovery
5232
5617
  await runCheck(
5233
5618
  {
5234
- url: 'turns:turn-server-url:443?transport=tcp',
5619
+ urls: [
5620
+ 'turns:turn-server-url1:443?transport=tcp',
5621
+ 'turns:turn-server-url2:443?transport=tcp',
5622
+ ],
5235
5623
  username: 'turn user',
5236
5624
  password: 'turn password',
5237
5625
  },
@@ -5245,7 +5633,10 @@ describe('plugin-meetings', () => {
5245
5633
  // but doing it just for completeness
5246
5634
  await runCheck(
5247
5635
  {
5248
- url: 'turns:turn-server-url:443?transport=tcp',
5636
+ urls: [
5637
+ 'turns:turn-server-url1:443?transport=tcp',
5638
+ 'turns:turn-server-url2:443?transport=tcp',
5639
+ ],
5249
5640
  username: 'turn user',
5250
5641
  password: 'turn password',
5251
5642
  },
@@ -7527,19 +7918,19 @@ describe('plugin-meetings', () => {
7527
7918
  });
7528
7919
  });
7529
7920
 
7530
- describe('#setIsoLocalClientMeetingJoinTime', () => {
7921
+ describe('#setIsoLocalClientMeetingJoinTime', () => {
7531
7922
  it('should fallback to system clock ISO string when given an undefined value', () => {
7532
7923
  const currentSystemTime = new Date().toISOString();
7533
7924
  meeting.isoLocalClientMeetingJoinTime = undefined;
7534
7925
  assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7535
7926
  });
7536
-
7927
+
7537
7928
  it('should fallback to system clock ISO string when given an invalid value', () => {
7538
7929
  const currentSystemTime = new Date().toISOString();
7539
7930
  meeting.isoLocalClientMeetingJoinTime = 'invalid-date';
7540
7931
  assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7541
7932
  });
7542
-
7933
+
7543
7934
  it('should set the isoLocalClientMeetingJoinTime correctly for a valid date string', () => {
7544
7935
  const validDateString = 'Tue, 01 Apr 2025 13:00:36 GMT';
7545
7936
  const expectedISOString = new Date(validDateString).toISOString();
@@ -7629,6 +8020,12 @@ describe('plugin-meetings', () => {
7629
8020
  meeting.audio = {handleLocalStreamChange: sinon.stub()};
7630
8021
  meeting.video = {handleLocalStreamChange: sinon.stub()};
7631
8022
  meeting.statsAnalyzer = {updateMediaStatus: sinon.stub()};
8023
+ meeting.shareCAEventSentStatus = {
8024
+ transmitStart: false,
8025
+ transmitStop: false,
8026
+ receiveStart: false,
8027
+ receiveStop: false,
8028
+ };
7632
8029
  fakeMultistreamRoapMediaConnection = {
7633
8030
  createSendSlot: () => {
7634
8031
  return {
@@ -7696,6 +8093,9 @@ describe('plugin-meetings', () => {
7696
8093
  });
7697
8094
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7698
8095
 
8096
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
8097
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
8098
+
7699
8099
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7700
8100
  expected: {sendShare: true},
7701
8101
  });
@@ -7716,18 +8116,23 @@ describe('plugin-meetings', () => {
7716
8116
  assert.equal(meeting.mediaProperties.shareAudioStream, stream);
7717
8117
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7718
8118
 
8119
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
8120
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
8121
+
7719
8122
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7720
8123
  expected: {sendShare: true},
7721
8124
  });
7722
8125
  };
7723
8126
 
7724
8127
  it('requests screen share floor and publishes the screen share video stream', async () => {
8128
+ meeting.shareCAEventSentStatus.transmitStart = true;
7725
8129
  await meeting.publishStreams({screenShare: {video: videoShareStream}});
7726
8130
 
7727
8131
  checkScreenShareVideoPublished(videoShareStream);
7728
8132
  });
7729
8133
 
7730
8134
  it('requests screen share floor and publishes the screen share audio stream', async () => {
8135
+ meeting.shareCAEventSentStatus.transmitStart = true;
7731
8136
  await meeting.publishStreams({screenShare: {audio: audioShareStream}});
7732
8137
 
7733
8138
  checkScreenShareAudioPublished(audioShareStream);
@@ -8699,7 +9104,7 @@ describe('plugin-meetings', () => {
8699
9104
  meeting.deferSDPAnswer = {
8700
9105
  reject: sinon.stub(),
8701
9106
  };
8702
-
9107
+
8703
9108
  const clearTimeoutSpy = sinon.spy(clock, 'clearTimeout');
8704
9109
 
8705
9110
  const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
@@ -9623,6 +10028,42 @@ describe('plugin-meetings', () => {
9623
10028
  );
9624
10029
  });
9625
10030
 
10031
+ it('listens to CONTROLS_ANNOTATION_CHANGED', async () => {
10032
+ const state = {example: 'value'};
10033
+
10034
+ await meeting.locusInfo.emitScoped(
10035
+ {function: 'test', file: 'test'},
10036
+ LOCUSINFO.EVENTS.CONTROLS_ANNOTATION_CHANGED,
10037
+ {state}
10038
+ );
10039
+
10040
+ assert.calledWith(
10041
+ TriggerProxy.trigger,
10042
+ meeting,
10043
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
10044
+ EVENT_TRIGGERS.MEETING_CONTROLS_ANNOTATION_UPDATED,
10045
+ {state}
10046
+ );
10047
+ });
10048
+
10049
+ it('listens to CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED', async () => {
10050
+ const state = {example: 'value'};
10051
+
10052
+ await meeting.locusInfo.emitScoped(
10053
+ {function: 'test', file: 'test'},
10054
+ LOCUSINFO.EVENTS.CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED,
10055
+ {state}
10056
+ );
10057
+
10058
+ assert.calledWith(
10059
+ TriggerProxy.trigger,
10060
+ meeting,
10061
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
10062
+ EVENT_TRIGGERS.MEETING_CONTROLS_REMOTE_DESKTOP_CONTROL_UPDATED,
10063
+ {state}
10064
+ );
10065
+ });
10066
+
9626
10067
  it('listens to the locus interpretation update event', () => {
9627
10068
  const interpretation = {
9628
10069
  siLanguages: [{languageCode: 20, languageName: 'en'}],
@@ -10518,9 +10959,11 @@ describe('plugin-meetings', () => {
10518
10959
  let canUserLowerSomeoneElsesHandSpy;
10519
10960
  let waitingForOthersToJoinSpy;
10520
10961
  let canSendReactionsSpy;
10962
+ let requiresPostMeetingDataConsentPromptSpy;
10521
10963
  let canUserRenameSelfAndObservedSpy;
10522
10964
  let canUserRenameOthersSpy;
10523
10965
  let canShareWhiteBoardSpy;
10966
+ let canMoveToLobbySpy;
10524
10967
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
10525
10968
 
10526
10969
  beforeEach(() => {
@@ -10545,8 +10988,13 @@ describe('plugin-meetings', () => {
10545
10988
  waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
10546
10989
  canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
10547
10990
  canUserRenameSelfAndObservedSpy = sinon.spy(MeetingUtil, 'canUserRenameSelfAndObserved');
10991
+ requiresPostMeetingDataConsentPromptSpy = sinon.spy(
10992
+ MeetingUtil,
10993
+ 'requiresPostMeetingDataConsentPrompt'
10994
+ );
10548
10995
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
10549
10996
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
10997
+ canMoveToLobbySpy = sinon.spy(MeetingUtil, 'canMoveToLobby');
10550
10998
  });
10551
10999
 
10552
11000
  afterEach(() => {
@@ -10644,6 +11092,16 @@ describe('plugin-meetings', () => {
10644
11092
  requiredDisplayHints: [],
10645
11093
  requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
10646
11094
  },
11095
+ {
11096
+ actionName: 'canRealtimeCloseCaption',
11097
+ requiredDisplayHints: [],
11098
+ requiredPolicies: [SELF_POLICY.SUPPORT_REALTIME_CLOSE_CAPTION],
11099
+ },
11100
+ {
11101
+ actionName: 'canRealtimeCloseCaptionManual',
11102
+ requiredDisplayHints: [],
11103
+ requiredPolicies: [SELF_POLICY.SUPPORT_REALTIME_CLOSE_CAPTION_MANUAL],
11104
+ },
10647
11105
  {
10648
11106
  actionName: 'canChat',
10649
11107
  requiredDisplayHints: [],
@@ -10673,6 +11131,11 @@ describe('plugin-meetings', () => {
10673
11131
  requiredDisplayHints: [],
10674
11132
  requiredPolicies: [SELF_POLICY.SUPPORT_POLLING_AND_QA],
10675
11133
  },
11134
+ {
11135
+ actionName: 'canShareWhiteBoard',
11136
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_WHITEBOARD],
11137
+ requiredPolicies: [SELF_POLICY.SUPPORT_WHITEBOARD],
11138
+ },
10676
11139
  ],
10677
11140
  ({
10678
11141
  actionName,
@@ -11080,8 +11543,10 @@ describe('plugin-meetings', () => {
11080
11543
  assert.calledWith(waitingForOthersToJoinSpy, userDisplayHints);
11081
11544
  assert.calledWith(canSendReactionsSpy, null, userDisplayHints);
11082
11545
  assert.calledWith(canUserRenameSelfAndObservedSpy, userDisplayHints);
11546
+ assert.calledWith(requiresPostMeetingDataConsentPromptSpy, userDisplayHints);
11083
11547
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11084
- assert.calledWith(canShareWhiteBoardSpy, userDisplayHints);
11548
+ assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11549
+ assert.calledWith(canMoveToLobbySpy, userDisplayHints);
11085
11550
 
11086
11551
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11087
11552
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -11175,6 +11640,22 @@ describe('plugin-meetings', () => {
11175
11640
  requiredPolicies: [SELF_POLICY.SUPPORT_VOIP],
11176
11641
  policies: selfUserPolicies,
11177
11642
  });
11643
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11644
+ requiredHints: [DISPLAY_HINTS.ENABLE_ANNOTATION_MEETING_OPTION],
11645
+ displayHints: userDisplayHints,
11646
+ });
11647
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11648
+ requiredHints: [DISPLAY_HINTS.DISABLE_ANNOTATION_MEETING_OPTION],
11649
+ displayHints: userDisplayHints,
11650
+ });
11651
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11652
+ requiredHints: [DISPLAY_HINTS.ENABLE_RDC_MEETING_OPTION],
11653
+ displayHints: userDisplayHints,
11654
+ });
11655
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11656
+ requiredHints: [DISPLAY_HINTS.DISABLE_RDC_MEETING_OPTION],
11657
+ displayHints: userDisplayHints,
11658
+ });
11178
11659
 
11179
11660
  assert.calledWith(
11180
11661
  TriggerProxy.trigger,
@@ -12021,6 +12502,8 @@ describe('plugin-meetings', () => {
12021
12502
  // Set the webinar attendee flag
12022
12503
  meeting.webinar = {selfIsAttendee: true};
12023
12504
  meeting.locusInfo.info.isWebinar = true;
12505
+ meeting.shareCAEventSentStatus.receiveStart = true;
12506
+ meeting.shareCAEventSentStatus.receiveStop = true;
12024
12507
 
12025
12508
  // Step 1: Start sharing whiteboard A
12026
12509
  const data1 = generateData(
@@ -12044,6 +12527,8 @@ describe('plugin-meetings', () => {
12044
12527
 
12045
12528
  // Specific assertions for webinar attendee status
12046
12529
  assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
12530
+ assert.equal(meeting.shareCAEventSentStatus.receiveStart, false);
12531
+ assert.equal(meeting.shareCAEventSentStatus.receiveStop, false);
12047
12532
  });
12048
12533
  });
12049
12534
 
@@ -12891,6 +13376,38 @@ describe('plugin-meetings', () => {
12891
13376
  });
12892
13377
  });
12893
13378
 
13379
+ describe('#setPostMeetingDataConsent', () => {
13380
+ it('should have #setPostMeetingDataConsent', () => {
13381
+ assert.exists(meeting.setPostMeetingDataConsent);
13382
+ });
13383
+
13384
+ beforeEach(() => {
13385
+ meeting.meetingRequest.setPostMeetingDataConsent = sinon
13386
+ .stub()
13387
+ .returns(Promise.resolve());
13388
+ });
13389
+
13390
+ [true, false].forEach((accept) => {
13391
+ it(`should send consent with ${accept}`, async () => {
13392
+ const id = uuidv4();
13393
+ meeting.locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`;
13394
+ meeting.deviceUrl = `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`;
13395
+ meeting.members.selfId = id;
13396
+
13397
+ const consentPromise = meeting.setPostMeetingDataConsent(accept);
13398
+
13399
+ assert.exists(consentPromise.then);
13400
+ await consentPromise;
13401
+ assert.calledOnceWithExactly(meeting.meetingRequest.setPostMeetingDataConsent, {
13402
+ locusUrl: `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`,
13403
+ postMeetingDataConsent: accept,
13404
+ selfId: id,
13405
+ deviceUrl: `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`,
13406
+ });
13407
+ });
13408
+ });
13409
+ });
13410
+
12894
13411
  describe('#sendReaction', () => {
12895
13412
  it('should have #sendReaction', () => {
12896
13413
  assert.exists(meeting.sendReaction);