@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
@@ -23,6 +23,7 @@ export default class Member {
23
23
  isInMeeting: any;
24
24
  isModerator: any;
25
25
  isModeratorAssignmentProhibited: any;
26
+ isPresenterAssignmentProhibited: any;
26
27
  isMutable: any;
27
28
  isNotAdmitted: any;
28
29
  isRecording: any;
@@ -257,6 +258,14 @@ export default class Member {
257
258
  */
258
259
  this.isModeratorAssignmentProhibited = null;
259
260
 
261
+ /**
262
+ * @instance
263
+ * @type {Boolean}
264
+ * @public
265
+ * @memberof Member
266
+ */
267
+ this.isPresenterAssignmentProhibited = null;
268
+
260
269
  /**
261
270
  * @instance
262
271
  * @type {IExternalRoles}
@@ -309,6 +318,8 @@ export default class Member {
309
318
  this.isModerator = MemberUtil.isModerator(participant);
310
319
  this.isModeratorAssignmentProhibited =
311
320
  MemberUtil.isModeratorAssignmentProhibited(participant);
321
+ this.isPresenterAssignmentProhibited =
322
+ MemberUtil.isPresenterAssignmentProhibited(participant);
312
323
  this.processStatus(participant);
313
324
  this.processRoles(participant as ParticipantWithRoles);
314
325
  // must be done last
@@ -127,6 +127,9 @@ MemberUtil.isDevice = (participant: any) => participant && participant.type ===
127
127
  MemberUtil.isModeratorAssignmentProhibited = (participant) =>
128
128
  participant && participant.moderatorAssignmentNotAllowed;
129
129
 
130
+ MemberUtil.isPresenterAssignmentProhibited = (participant) =>
131
+ participant && participant.presenterAssignmentNotAllowed;
132
+
130
133
  /**
131
134
  * checks to see if the participant id is the same as the passed id
132
135
  * there are multiple ids that can be used
@@ -48,10 +48,19 @@ const BEHAVIORAL_METRICS = {
48
48
  UPLOAD_LOGS_FAILURE: 'js_sdk_upload_logs_failure',
49
49
  UPLOAD_LOGS_SUCCESS: 'js_sdk_upload_logs_success',
50
50
  RECEIVE_TRANSCRIPTION_FAILURE: 'js_sdk_receive_transcription_failure',
51
+ MEETING_IS_IN_PROGRESS_ERROR: 'js_sdk_meeting_is_in_progress_error',
52
+ STATIC_MEETING_LINK_ALREADY_EXISTS_ERROR: 'js_sdk_static_meeting_link_already_exists_error',
51
53
  FETCH_MEETING_INFO_V1_SUCCESS: 'js_sdk_fetch_meeting_info_v1_success',
52
54
  FETCH_MEETING_INFO_V1_FAILURE: 'js_sdk_fetch_meeting_info_v1_failure',
55
+ ENABLE_STATIC_METTING_LINK_SUCCESS: 'js_sdk_enable_static_meeting_link_success',
56
+ ENABLE_STATIC_METTING_LINK_FAILURE: 'js_sdk_enable_static_meeting_link_failure',
57
+ DISABLE_STATIC_MEETING_LINK_SUCCESS: 'js_sdk_disable_static_meeting_link_success',
58
+ DISABLE_STATIC_MEETING_LINK_FAILURE: 'js_sdk_disable_static_meeting_link_failure',
53
59
  ADHOC_MEETING_SUCCESS: 'js_sdk_adhoc_meeting_success',
54
60
  ADHOC_MEETING_FAILURE: 'js_sdk_adhoc_meeting_failure',
61
+ FETCH_STATIC_MEETING_LINK_SUCCESS: 'js_sdk_fetch_static_meeting_link_success',
62
+ FETCH_STATIC_MEETING_LINK_FAILURE: 'js_sdk_fetch_static_meeting_link_failure',
63
+ MEETING_LINK_DOES_NOT_EXIST_ERROR: 'js_sdk_meeting_link_does_not_exist_error',
55
64
  VERIFY_PASSWORD_SUCCESS: 'js_sdk_verify_password_success',
56
65
  VERIFY_PASSWORD_ERROR: 'js_sdk_verify_password_error',
57
66
  VERIFY_CAPTCHA_ERROR: 'js_sdk_verify_captcha_error',
@@ -6,7 +6,7 @@ import {convertStunUrlToTurn, convertStunUrlToTurnTls} from './util';
6
6
  import EventsScope from '../common/events/events-scope';
7
7
 
8
8
  import {CONNECTION_STATE, Enum, ICE_GATHERING_STATE} from '../constants';
9
- import {ClusterReachabilityResult} from './reachability.types';
9
+ import {ClusterReachabilityResult, NatType} from './reachability.types';
10
10
 
11
11
  // data for the Events.resultReady event
12
12
  export type ResultEventData = {
@@ -22,9 +22,14 @@ export type ClientMediaIpsUpdatedEventData = {
22
22
  clientMediaIPs: string[];
23
23
  };
24
24
 
25
+ export type NatTypeUpdatedEventData = {
26
+ natType: NatType;
27
+ };
28
+
25
29
  export const Events = {
26
30
  resultReady: 'resultReady', // emitted when a cluster is reached successfully using specific protocol
27
31
  clientMediaIpsUpdated: 'clientMediaIpsUpdated', // emitted when more public IPs are found after resultReady was already sent for a given protocol
32
+ natTypeUpdated: 'natTypeUpdated', // emitted when NAT type is determined
28
33
  } as const;
29
34
 
30
35
  export type Events = Enum<typeof Events>;
@@ -41,8 +46,10 @@ export class ClusterReachability extends EventsScope {
41
46
  private pc?: RTCPeerConnection;
42
47
  private defer: Defer; // this defer is resolved once reachability checks for this cluster are completed
43
48
  private startTimestamp: number;
49
+ private srflxIceCandidates: RTCIceCandidate[] = [];
44
50
  public readonly isVideoMesh: boolean;
45
51
  public readonly name;
52
+ public readonly reachedSubnets: Set<string> = new Set();
46
53
 
47
54
  /**
48
55
  * Constructor for ClusterReachability
@@ -228,27 +235,13 @@ export class ClusterReachability extends EventsScope {
228
235
  */
229
236
  private registerIceGatheringStateChangeListener() {
230
237
  this.pc.onicegatheringstatechange = () => {
231
- const {COMPLETE} = ICE_GATHERING_STATE;
232
-
233
- if (this.pc.iceConnectionState === COMPLETE) {
238
+ if (this.pc.iceGatheringState === ICE_GATHERING_STATE.COMPLETE) {
234
239
  this.closePeerConnection();
235
240
  this.finishReachabilityCheck();
236
241
  }
237
242
  };
238
243
  }
239
244
 
240
- /**
241
- * Checks if we have the results for all the protocols (UDP and TCP)
242
- *
243
- * @returns {boolean} true if we have all results, false otherwise
244
- */
245
- private haveWeGotAllResults(): boolean {
246
- return ['udp', 'tcp', 'xtls'].every(
247
- (protocol) =>
248
- this.result[protocol].result === 'reachable' || this.result[protocol].result === 'untested'
249
- );
250
- }
251
-
252
245
  /**
253
246
  * Saves the latency in the result for the given protocol and marks it as reachable,
254
247
  * emits the "resultReady" event if this is the first result for that protocol,
@@ -258,9 +251,15 @@ export class ClusterReachability extends EventsScope {
258
251
  * @param {string} protocol
259
252
  * @param {number} latency
260
253
  * @param {string|null} [publicIp]
254
+ * @param {string|null} [serverIp]
261
255
  * @returns {void}
262
256
  */
263
- private saveResult(protocol: 'udp' | 'tcp' | 'xtls', latency: number, publicIp?: string | null) {
257
+ private saveResult(
258
+ protocol: 'udp' | 'tcp' | 'xtls',
259
+ latency: number,
260
+ publicIp?: string | null,
261
+ serverIp?: string | null
262
+ ) {
264
263
  const result = this.result[protocol];
265
264
 
266
265
  if (result.latencyInMilliseconds === undefined) {
@@ -288,6 +287,48 @@ export class ClusterReachability extends EventsScope {
288
287
  } else {
289
288
  this.addPublicIP(protocol, publicIp);
290
289
  }
290
+
291
+ if (serverIp) {
292
+ this.reachedSubnets.add(serverIp);
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Determines NAT Type.
298
+ *
299
+ * @param {RTCIceCandidate} candidate
300
+ * @returns {void}
301
+ */
302
+ private determineNatType(candidate: RTCIceCandidate) {
303
+ this.srflxIceCandidates.push(candidate);
304
+
305
+ if (this.srflxIceCandidates.length > 1) {
306
+ const portsFound: Record<string, Set<number>> = {};
307
+
308
+ this.srflxIceCandidates.forEach((c) => {
309
+ const key = `${c.address}:${c.relatedPort}`;
310
+ if (!portsFound[key]) {
311
+ portsFound[key] = new Set();
312
+ }
313
+ portsFound[key].add(c.port);
314
+ });
315
+
316
+ Object.entries(portsFound).forEach(([, ports]) => {
317
+ if (ports.size > 1) {
318
+ // Found candidates with the same address and relatedPort, but different ports
319
+ this.emit(
320
+ {
321
+ file: 'clusterReachability',
322
+ function: 'determineNatType',
323
+ },
324
+ Events.natTypeUpdated,
325
+ {
326
+ natType: NatType.SymmetricNat,
327
+ }
328
+ );
329
+ }
330
+ });
331
+ }
291
332
  }
292
333
 
293
334
  /**
@@ -307,19 +348,25 @@ export class ClusterReachability extends EventsScope {
307
348
 
308
349
  if (e.candidate) {
309
350
  if (e.candidate.type === CANDIDATE_TYPES.SERVER_REFLEXIVE) {
310
- this.saveResult('udp', latencyInMilliseconds, e.candidate.address);
351
+ let serverIp = null;
352
+ if ('url' in e.candidate) {
353
+ const stunServerUrlRegex = /stun:([\d.]+):\d+/;
354
+
355
+ const match = (e.candidate as any).url.match(stunServerUrlRegex);
356
+ if (match) {
357
+ // eslint-disable-next-line prefer-destructuring
358
+ serverIp = match[1];
359
+ }
360
+ }
361
+
362
+ this.saveResult('udp', latencyInMilliseconds, e.candidate.address, serverIp);
363
+
364
+ this.determineNatType(e.candidate);
311
365
  }
312
366
 
313
367
  if (e.candidate.type === CANDIDATE_TYPES.RELAY) {
314
368
  const protocol = e.candidate.port === TURN_TLS_PORT ? 'xtls' : 'tcp';
315
- this.saveResult(protocol, latencyInMilliseconds);
316
- // we don't add public IP for TCP, because in the case of relay candidates
317
- // e.candidate.address is the TURN server address, not the client's public IP
318
- }
319
-
320
- if (this.haveWeGotAllResults()) {
321
- this.closePeerConnection();
322
- this.finishReachabilityCheck();
369
+ this.saveResult(protocol, latencyInMilliseconds, null, e.candidate.address);
323
370
  }
324
371
  }
325
372
  };
@@ -23,11 +23,13 @@ import {
23
23
  ReachabilityResultsForBackend,
24
24
  TransportResultForBackend,
25
25
  GetClustersTrigger,
26
+ NatType,
26
27
  } from './reachability.types';
27
28
  import {
28
29
  ClientMediaIpsUpdatedEventData,
29
30
  ClusterReachability,
30
31
  Events,
32
+ NatTypeUpdatedEventData,
31
33
  ResultEventData,
32
34
  } from './clusterReachability';
33
35
  import EventsScope from '../common/events/events-scope';
@@ -64,6 +66,7 @@ export default class Reachability extends EventsScope {
64
66
  resultsCount = {videoMesh: {udp: 0}, public: {udp: 0, tcp: 0, xtls: 0}};
65
67
  startTime = undefined;
66
68
  totalDuration = undefined;
69
+ natType = NatType.Unknown;
67
70
 
68
71
  protected lastTrigger?: string;
69
72
 
@@ -135,6 +138,60 @@ export default class Reachability extends EventsScope {
135
138
  }
136
139
  }
137
140
 
141
+ /**
142
+ * Checks if the given subnet is reachable
143
+ * @param {string} mediaServerIp - media server ip
144
+ * @returns {boolean | null} true if reachable, false if not reachable, null if mediaServerIp is not provided
145
+ * @public
146
+ * @memberof Reachability
147
+ */
148
+ public isSubnetReachable(mediaServerIp?: string): boolean | null {
149
+ if (!mediaServerIp) {
150
+ LoggerProxy.logger.error(`Reachability:index#isSubnetReachable --> mediaServerIp is null`);
151
+
152
+ return null;
153
+ }
154
+
155
+ const subnetFirstOctet = mediaServerIp.split('.')[0];
156
+
157
+ LoggerProxy.logger.info(
158
+ `Reachability:index#isSubnetReachable --> Looking for subnet: ${subnetFirstOctet}.X.X.X`
159
+ );
160
+
161
+ const matchingReachedClusters = Object.values(this.clusterReachability).reduce(
162
+ (acc, cluster) => {
163
+ const reachedSubnetsArray = Array.from(cluster.reachedSubnets);
164
+
165
+ let logMessage = `Reachability:index#isSubnetReachable --> Cluster ${cluster.name} reached [`;
166
+ for (let i = 0; i < reachedSubnetsArray.length; i += 1) {
167
+ const subnet = reachedSubnetsArray[i];
168
+ const reachedSubnetFirstOctet = subnet.split('.')[0];
169
+
170
+ if (subnetFirstOctet === reachedSubnetFirstOctet) {
171
+ acc.add(cluster.name);
172
+ }
173
+
174
+ logMessage += `${subnet}`;
175
+ if (i < reachedSubnetsArray.length - 1) {
176
+ logMessage += ',';
177
+ }
178
+ }
179
+ logMessage += `]`;
180
+
181
+ LoggerProxy.logger.info(logMessage);
182
+
183
+ return acc;
184
+ },
185
+ new Set<string>()
186
+ );
187
+
188
+ LoggerProxy.logger.info(
189
+ `Reachability:index#isSubnetReachable --> Found ${matchingReachedClusters.size} clusters that use the subnet ${subnetFirstOctet}.X.X.X`
190
+ );
191
+
192
+ return matchingReachedClusters.size > 0;
193
+ }
194
+
138
195
  /**
139
196
  * Gets a list of media clusters from the backend and performs reachability checks on all the clusters
140
197
  * @param {string} trigger - explains the reason for starting reachability
@@ -143,6 +200,10 @@ export default class Reachability extends EventsScope {
143
200
  * @memberof Reachability
144
201
  */
145
202
  public async gatherReachability(trigger: string): Promise<ReachabilityResults> {
203
+ // @ts-ignore
204
+ if (!this.webex.config.meetings.enableReachabilityChecks) {
205
+ throw new Error('enableReachabilityChecks is disabled in config');
206
+ }
146
207
  // Fetch clusters and measure latency
147
208
  try {
148
209
  this.lastTrigger = trigger;
@@ -281,7 +342,7 @@ export default class Reachability extends EventsScope {
281
342
  {}
282
343
  );
283
344
  this.sendMetric(true);
284
- this.resolveReachabilityPromise();
345
+ this.resolveReachabilityPromise(false);
285
346
  }
286
347
  }
287
348
 
@@ -305,6 +366,7 @@ export default class Reachability extends EventsScope {
305
366
  reachability_vmn_tcp_failed: 0,
306
367
  reachability_vmn_xtls_success: 0,
307
368
  reachability_vmn_xtls_failed: 0,
369
+ natType: this.natType,
308
370
  };
309
371
 
310
372
  const updateStats = (clusterType: 'public' | 'vmn', result: ClusterReachabilityResult) => {
@@ -963,6 +1025,13 @@ export default class Reachability extends EventsScope {
963
1025
  }
964
1026
  );
965
1027
 
1028
+ this.clusterReachability[key].on(
1029
+ Events.natTypeUpdated,
1030
+ async (data: NatTypeUpdatedEventData) => {
1031
+ this.natType = data.natType;
1032
+ }
1033
+ );
1034
+
966
1035
  this.clusterReachability[key].start(); // not awaiting on purpose
967
1036
  });
968
1037
  }
@@ -7,6 +7,11 @@ export type TransportResult = {
7
7
  clientMediaIPs?: string[];
8
8
  };
9
9
 
10
+ export enum NatType {
11
+ Unknown = 'unknown',
12
+ SymmetricNat = 'symmetric-nat',
13
+ }
14
+
10
15
  // reachability result for a specific media cluster
11
16
  export type ClusterReachabilityResult = {
12
17
  udp: TransportResult;
@@ -27,6 +32,7 @@ export type ReachabilityMetrics = {
27
32
  reachability_vmn_tcp_failed: number;
28
33
  reachability_vmn_xtls_success: number;
29
34
  reachability_vmn_xtls_failed: number;
35
+ natType: NatType;
30
36
  };
31
37
 
32
38
  /**
@@ -45,6 +45,9 @@ class ReachabilityRequest {
45
45
  joinCookie: any;
46
46
  discoveryOptions?: Record<string, any>;
47
47
  }> => {
48
+ const appType = this.webex?.config?.support?.appType;
49
+ const appVersion = this.webex?.config?.support?.appVersion;
50
+
48
51
  // we only measure latency for the initial startup call, not for other triggers
49
52
  const callWrapper =
50
53
  trigger === 'startup'
@@ -67,6 +70,10 @@ class ReachabilityRequest {
67
70
  'early-call-min-clusters': true,
68
71
  },
69
72
  'previous-report': previousReport,
73
+ ...(appType &&
74
+ appVersion && {
75
+ 'client-environment': {components: {[appType]: appVersion}},
76
+ }),
70
77
  trigger,
71
78
  },
72
79
  timeout: this.webex.config.meetings.reachabilityGetClusterTimeout,
@@ -591,9 +591,9 @@ export default class ReconnectionManager {
591
591
 
592
592
  const iceServers = [];
593
593
 
594
- if (turnServerResult.turnServerInfo?.url) {
594
+ if (turnServerResult.turnServerInfo?.urls.length > 0) {
595
595
  iceServers.push({
596
- urls: turnServerResult.turnServerInfo.url,
596
+ urls: turnServerResult.turnServerInfo.urls,
597
597
  username: turnServerResult.turnServerInfo.username || '',
598
598
  credential: turnServerResult.turnServerInfo.password || '',
599
599
  });
@@ -6,33 +6,37 @@ const canUserStart = (
6
6
  displayHints: Array<string>,
7
7
  userPolicies: Record<SELF_POLICY, boolean>
8
8
  ): boolean =>
9
- (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_START) ||
10
- displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_START)) &&
11
- MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
9
+ (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_START) &&
10
+ MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies)) ||
11
+ (displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_START) &&
12
+ MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_PREMISE_RECORD, userPolicies));
12
13
 
13
14
  const canUserPause = (
14
15
  displayHints: Array<string>,
15
16
  userPolicies: Record<SELF_POLICY, boolean>
16
17
  ): boolean =>
17
- (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_PAUSE) ||
18
- displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_PAUSE)) &&
19
- MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
18
+ (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_PAUSE) &&
19
+ MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies)) ||
20
+ (displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_PAUSE) &&
21
+ MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_PREMISE_RECORD, userPolicies));
20
22
 
21
23
  const canUserResume = (
22
24
  displayHints: Array<string>,
23
25
  userPolicies: Record<SELF_POLICY, boolean>
24
26
  ): boolean =>
25
- (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_RESUME) ||
26
- displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_RESUME)) &&
27
- MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
27
+ (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_RESUME) &&
28
+ MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies)) ||
29
+ (displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_RESUME) &&
30
+ MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_PREMISE_RECORD, userPolicies));
28
31
 
29
32
  const canUserStop = (
30
33
  displayHints: Array<string>,
31
34
  userPolicies: Record<SELF_POLICY, boolean>
32
35
  ): boolean =>
33
- (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_STOP) ||
34
- displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_STOP)) &&
35
- MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
36
+ (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_STOP) &&
37
+ MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies)) ||
38
+ (displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_STOP) &&
39
+ MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_PREMISE_RECORD, userPolicies));
36
40
 
37
41
  const isPremiseRecordingEnabled = (
38
42
  displayHints: Array<string>,
@@ -42,7 +46,7 @@ const isPremiseRecordingEnabled = (
42
46
  displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_PAUSE) ||
43
47
  displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_STOP) ||
44
48
  displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_RESUME)) &&
45
- MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
49
+ MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_PREMISE_RECORD, userPolicies);
46
50
 
47
51
  const extractLocusId = (url: string) => {
48
52
  return url?.split('/').pop();
package/src/roap/index.ts CHANGED
@@ -5,17 +5,13 @@ import {ROAP} from '../constants';
5
5
  import LoggerProxy from '../common/logs/logger-proxy';
6
6
 
7
7
  import RoapRequest from './request';
8
- import TurnDiscovery, {TurnDiscoveryResult} from './turnDiscovery';
8
+ import TurnDiscovery from './turnDiscovery';
9
+ import {TurnDiscoveryResult} from './types';
9
10
  import Meeting from '../meeting';
10
- import MeetingUtil from '../meeting/util';
11
11
  import Metrics from '../metrics';
12
12
  import BEHAVIORAL_METRICS from '../metrics/constants';
13
13
 
14
- export {
15
- type TurnDiscoveryResult,
16
- type TurnServerInfo,
17
- type TurnDiscoverySkipReason,
18
- } from './turnDiscovery';
14
+ export {type TurnDiscoveryResult, type TurnServerInfo, type TurnDiscoverySkipReason} from './types';
19
15
 
20
16
  /**
21
17
  * Roap options
@@ -4,11 +4,11 @@ import {Defer} from '@webex/common';
4
4
  import Metrics from '../metrics';
5
5
  import BEHAVIORAL_METRICS from '../metrics/constants';
6
6
  import LoggerProxy from '../common/logs/logger-proxy';
7
- import {ROAP, Enum} from '../constants';
7
+ import {ROAP} from '../constants';
8
8
 
9
9
  import RoapRequest from './request';
10
10
  import Meeting from '../meeting';
11
- import MeetingUtil from '../meeting/util';
11
+ import {TurnDiscoverySkipReason, TurnServerInfo, TurnDiscoveryResult} from './types';
12
12
 
13
13
  const TURN_DISCOVERY_TIMEOUT = 10; // in seconds
14
14
 
@@ -18,28 +18,6 @@ const TURN_DISCOVERY_TIMEOUT = 10; // in seconds
18
18
  // and do the SDP offer with seq=1
19
19
  const TURN_DISCOVERY_SEQ = 0;
20
20
 
21
- const TurnDiscoverySkipReason = {
22
- missingHttpResponse: 'missing http response', // when we asked for the TURN discovery response to be in the http response, but it wasn't there
23
- reachability: 'reachability', // when udp reachability to public clusters is ok, so we don't need TURN (this doens't apply when joinWithMedia() is used)
24
- alreadyInProgress: 'already in progress', // when we try to start TURN discovery while it's already in progress
25
- } as const;
26
-
27
- export type TurnDiscoverySkipReason =
28
- | Enum<typeof TurnDiscoverySkipReason> // this is a kind of FYI, because in practice typescript will infer the type of TurnDiscoverySkipReason as a string
29
- | string // used in case of errors, contains the error message
30
- | undefined; // used when TURN discovery is not skipped
31
-
32
- export type TurnServerInfo = {
33
- url: string;
34
- username: string;
35
- password: string;
36
- };
37
-
38
- export type TurnDiscoveryResult = {
39
- turnServerInfo?: TurnServerInfo;
40
- turnDiscoverySkippedReason: TurnDiscoverySkipReason;
41
- };
42
-
43
21
  /**
44
22
  * Handles the process of finding out TURN server information from Linus.
45
23
  * This is achieved by sending a TURN_DISCOVERY_REQUEST.
@@ -53,6 +31,17 @@ export default class TurnDiscovery {
53
31
 
54
32
  private responseTimer?: ReturnType<typeof setTimeout>;
55
33
 
34
+ /** Resets the turnInfo structure to the defaults
35
+ * @returns {void}
36
+ */
37
+ private resetTurnInfo() {
38
+ this.turnInfo = {
39
+ urls: [],
40
+ username: '',
41
+ password: '',
42
+ };
43
+ }
44
+
56
45
  /**
57
46
  * Constructor
58
47
  *
@@ -60,11 +49,7 @@ export default class TurnDiscovery {
60
49
  */
61
50
  constructor(roapRequest: RoapRequest) {
62
51
  this.roapRequest = roapRequest;
63
- this.turnInfo = {
64
- url: '',
65
- username: '',
66
- password: '',
67
- };
52
+ this.resetTurnInfo();
68
53
  }
69
54
 
70
55
  /**
@@ -134,21 +119,29 @@ export default class TurnDiscovery {
134
119
  }
135
120
 
136
121
  const expectedHeaders = [
137
- {headerName: 'x-cisco-turn-url', field: 'url'},
138
- {headerName: 'x-cisco-turn-username', field: 'username'},
139
- {headerName: 'x-cisco-turn-password', field: 'password'},
122
+ {headerName: 'x-cisco-turn-url', field: 'urls', multipleAllowed: true},
123
+ {headerName: 'x-cisco-turn-username', field: 'username', multipleAllowed: false},
124
+ {headerName: 'x-cisco-turn-password', field: 'password', multipleAllowed: false},
140
125
  ];
141
126
 
142
- let foundHeaders = 0;
127
+ const foundHeaders = {};
128
+
129
+ this.resetTurnInfo();
143
130
 
144
131
  headers?.forEach((receivedHeader) => {
145
132
  // check if it matches any of our expected headers
146
133
  expectedHeaders.forEach((expectedHeader) => {
147
134
  if (receivedHeader.startsWith(`${expectedHeader.headerName}=`)) {
148
- this.turnInfo[expectedHeader.field] = receivedHeader.substring(
149
- expectedHeader.headerName.length + 1
150
- );
151
- foundHeaders += 1;
135
+ foundHeaders[expectedHeader.headerName] = true;
136
+
137
+ const headerValue = receivedHeader.substring(expectedHeader.headerName.length + 1);
138
+
139
+ if (expectedHeader.multipleAllowed) {
140
+ this.turnInfo[expectedHeader.field].push(headerValue);
141
+ } else {
142
+ // just store the last one we find
143
+ this.turnInfo[expectedHeader.field] = headerValue;
144
+ }
152
145
  }
153
146
  });
154
147
  });
@@ -156,7 +149,7 @@ export default class TurnDiscovery {
156
149
  clearTimeout(this.responseTimer);
157
150
  this.responseTimer = undefined;
158
151
 
159
- if (foundHeaders !== expectedHeaders.length) {
152
+ if (expectedHeaders.some((header) => !foundHeaders[header.headerName])) {
160
153
  LoggerProxy.logger.warn(
161
154
  `Roap:turnDiscovery#handleTurnDiscoveryResponse --> missing some headers, received ${from}: ${JSON.stringify(
162
155
  headers
@@ -169,9 +162,11 @@ export default class TurnDiscovery {
169
162
  );
170
163
  } else {
171
164
  LoggerProxy.logger.info(
172
- `Roap:turnDiscovery#handleTurnDiscoveryResponse --> received a valid response ${from}, url=${this.turnInfo.url}`
165
+ `Roap:turnDiscovery#handleTurnDiscoveryResponse --> received a valid response ${from}, urls=${this.turnInfo.urls}`
173
166
  );
174
167
 
168
+ this.turnInfo.urls = this.turnInfo.urls.filter((url) => url !== ''); // remove empty urls, we might get them if we land on video-mesh nodes (VMN)
169
+
175
170
  this.defer.resolve({isOkRequired: !headers?.includes('noOkInTransaction')});
176
171
  }
177
172
  }
@@ -0,0 +1,23 @@
1
+ import {Enum} from '../constants';
2
+
3
+ export const TurnDiscoverySkipReason = {
4
+ missingHttpResponse: 'missing http response', // when we asked for the TURN discovery response to be in the http response, but it wasn't there
5
+ reachability: 'reachability', // when udp reachability to public clusters is ok, so we don't need TURN (this doens't apply when joinWithMedia() is used)
6
+ alreadyInProgress: 'already in progress', // when we try to start TURN discovery while it's already in progress
7
+ } as const;
8
+
9
+ export type TurnDiscoverySkipReason =
10
+ | Enum<typeof TurnDiscoverySkipReason> // this is a kind of FYI, because in practice typescript will infer the type of TurnDiscoverySkipReason as a string
11
+ | string // used in case of errors, contains the error message
12
+ | undefined; // used when TURN discovery is not skipped
13
+
14
+ export type TurnServerInfo = {
15
+ urls: string[];
16
+ username: string;
17
+ password: string;
18
+ };
19
+
20
+ export type TurnDiscoveryResult = {
21
+ turnServerInfo?: TurnServerInfo;
22
+ turnDiscoverySkippedReason: TurnDiscoverySkipReason;
23
+ };