@webex/plugin-meetings 3.8.0-next.8 → 3.8.0-next.81

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 (171) 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 +5 -1
  7. package/dist/config.js.map +1 -1
  8. package/dist/constants.js +20 -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 +28 -10
  18. package/dist/locus-info/controlsUtils.js.map +1 -1
  19. package/dist/locus-info/index.js +62 -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 +6 -0
  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 +570 -302
  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 +0 -2
  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 +13 -2
  41. package/dist/meeting/util.js.map +1 -1
  42. package/dist/meeting-info/meeting-info-v2.js +373 -68
  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 +136 -1
  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 +10 -0
  51. package/dist/member/index.js.map +1 -1
  52. package/dist/member/util.js +330 -353
  53. package/dist/member/util.js.map +1 -1
  54. package/dist/members/index.js +42 -0
  55. package/dist/members/index.js.map +1 -1
  56. package/dist/members/request.js +38 -0
  57. package/dist/members/request.js.map +1 -1
  58. package/dist/members/util.js +36 -1
  59. package/dist/members/util.js.map +1 -1
  60. package/dist/metrics/constants.js +9 -0
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/reachability/clusterReachability.js +63 -27
  63. package/dist/reachability/clusterReachability.js.map +1 -1
  64. package/dist/reachability/index.js +112 -47
  65. package/dist/reachability/index.js.map +1 -1
  66. package/dist/reachability/reachability.types.js +14 -0
  67. package/dist/reachability/reachability.types.js.map +1 -1
  68. package/dist/reachability/request.js +19 -3
  69. package/dist/reachability/request.js.map +1 -1
  70. package/dist/reconnection-manager/index.js +2 -2
  71. package/dist/reconnection-manager/index.js.map +1 -1
  72. package/dist/roap/index.js.map +1 -1
  73. package/dist/roap/turnDiscovery.js +45 -27
  74. package/dist/roap/turnDiscovery.js.map +1 -1
  75. package/dist/roap/types.js +17 -0
  76. package/dist/roap/types.js.map +1 -0
  77. package/dist/types/common/errors/webex-errors.d.ts +7 -1
  78. package/dist/types/config.d.ts +3 -0
  79. package/dist/types/constants.d.ts +13 -85
  80. package/dist/types/controls-options-manager/enums.d.ts +3 -1
  81. package/dist/types/controls-options-manager/types.d.ts +7 -1
  82. package/dist/types/locus-info/index.d.ts +3 -3
  83. package/dist/types/locus-info/selfUtils.d.ts +216 -1
  84. package/dist/types/media/properties.d.ts +15 -0
  85. package/dist/types/meeting/in-meeting-actions.d.ts +16 -0
  86. package/dist/types/meeting/index.d.ts +43 -1
  87. package/dist/types/meeting/muteState.d.ts +0 -1
  88. package/dist/types/meeting/request.d.ts +12 -1
  89. package/dist/types/meeting/request.type.d.ts +6 -0
  90. package/dist/types/meeting/util.d.ts +3 -1
  91. package/dist/types/meeting-info/meeting-info-v2.d.ts +82 -1
  92. package/dist/types/meetings/index.d.ts +57 -0
  93. package/dist/types/member/index.d.ts +1 -0
  94. package/dist/types/member/util.d.ts +159 -1
  95. package/dist/types/members/index.d.ts +15 -0
  96. package/dist/types/members/request.d.ts +26 -0
  97. package/dist/types/members/util.d.ts +27 -0
  98. package/dist/types/metrics/constants.d.ts +9 -0
  99. package/dist/types/reachability/clusterReachability.d.ts +15 -7
  100. package/dist/types/reachability/index.d.ts +10 -1
  101. package/dist/types/reachability/reachability.types.d.ts +5 -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 +3 -0
  110. package/src/constants.ts +20 -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 +44 -14
  115. package/src/locus-info/index.ts +56 -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 +7 -0
  120. package/src/meeting/in-meeting-actions.ts +32 -0
  121. package/src/meeting/index.ts +382 -93
  122. package/src/meeting/locusMediaRequest.ts +0 -18
  123. package/src/meeting/muteState.ts +0 -2
  124. package/src/meeting/request.ts +36 -1
  125. package/src/meeting/request.type.ts +7 -0
  126. package/src/meeting/util.ts +11 -2
  127. package/src/meeting-info/meeting-info-v2.ts +254 -8
  128. package/src/meeting-info/utilv2.ts +5 -0
  129. package/src/meetings/index.ts +148 -1
  130. package/src/meetings/util.ts +18 -0
  131. package/src/member/index.ts +13 -2
  132. package/src/member/util.ts +351 -348
  133. package/src/members/index.ts +47 -0
  134. package/src/members/request.ts +44 -0
  135. package/src/members/util.ts +43 -1
  136. package/src/metrics/constants.ts +9 -0
  137. package/src/reachability/clusterReachability.ts +73 -26
  138. package/src/reachability/index.ts +70 -1
  139. package/src/reachability/reachability.types.ts +6 -0
  140. package/src/reachability/request.ts +7 -0
  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 +103 -9
  148. package/test/unit/spec/locus-info/index.js +167 -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 +19 -0
  153. package/test/unit/spec/meeting/in-meeting-actions.ts +19 -4
  154. package/test/unit/spec/meeting/index.js +557 -35
  155. package/test/unit/spec/meeting/locusMediaRequest.ts +0 -30
  156. package/test/unit/spec/meeting/muteState.js +0 -2
  157. package/test/unit/spec/meeting/request.js +32 -1
  158. package/test/unit/spec/meeting/utils.js +119 -18
  159. package/test/unit/spec/meeting-info/meetinginfov2.js +484 -114
  160. package/test/unit/spec/meeting-info/utilv2.js +19 -0
  161. package/test/unit/spec/meetings/index.js +146 -2
  162. package/test/unit/spec/member/index.js +7 -0
  163. package/test/unit/spec/member/util.js +24 -0
  164. package/test/unit/spec/members/index.js +140 -26
  165. package/test/unit/spec/members/request.js +68 -22
  166. package/test/unit/spec/members/utils.js +75 -0
  167. package/test/unit/spec/reachability/clusterReachability.ts +88 -56
  168. package/test/unit/spec/reachability/index.ts +101 -0
  169. package/test/unit/spec/reachability/request.js +47 -2
  170. package/test/unit/spec/reconnection-manager/index.js +4 -4
  171. package/test/unit/spec/roap/turnDiscovery.ts +110 -28
@@ -773,6 +773,28 @@ export default class Members extends StatelessWebexPlugin {
773
773
  return this.membersRequest.cancelPhoneInvite(options);
774
774
  }
775
775
 
776
+ /**
777
+ * Cancels an SIP call to the associated meeting
778
+ * @param {String} invitee
779
+ * @returns {Promise}
780
+ * @memberof Members
781
+ */
782
+ cancelSIPInvite(invitee: any) {
783
+ if (!this.locusUrl) {
784
+ return Promise.reject(
785
+ new ParameterError('The associated locus url for this meeting object must be defined.')
786
+ );
787
+ }
788
+ if (!invitee?.memberId) {
789
+ return Promise.reject(
790
+ new ParameterError('The invitee must be defined with a memberId property.')
791
+ );
792
+ }
793
+ const options = MembersUtil.cancelSIPInviteOptions(invitee, this.locusUrl);
794
+
795
+ return this.membersRequest.cancelSIPInvite(options);
796
+ }
797
+
776
798
  /**
777
799
  * Admits waiting members (invited guests to meeting)
778
800
  * @param {Array} memberIds
@@ -886,6 +908,31 @@ export default class Members extends StatelessWebexPlugin {
886
908
  });
887
909
  }
888
910
 
911
+ /**
912
+ * Moves a meeting member into the lobby.
913
+ * @param {String} memberId -- The ID of the member to move.
914
+ * @returns {Promise} -- Resolves with the lobby‐move response.
915
+ * @public
916
+ * @memberof Members
917
+ */
918
+ public moveToLobby(memberId: string) {
919
+ if (!this.locusUrl) {
920
+ return Promise.reject(
921
+ new ParameterError(
922
+ 'The associated locus url for this meetings members object must be defined.'
923
+ )
924
+ );
925
+ }
926
+ if (!memberId) {
927
+ return Promise.reject(
928
+ new ParameterError('The member id must be defined to move the member to lobby.')
929
+ );
930
+ }
931
+ const body = MembersUtil.getMoveMemberToLobbyRequestBody(memberId);
932
+
933
+ return this.membersRequest.moveToLobbyMember({locusUrl: this.locusUrl, memberId}, body);
934
+ }
935
+
889
936
  /**
890
937
  * Raise or lower the hand of a member in a meeting
891
938
  * @param {String} memberId
@@ -129,6 +129,32 @@ export default class MembersRequest extends StatelessWebexPlugin {
129
129
  return this.locusDeltaRequest(requestParams);
130
130
  }
131
131
 
132
+ /**
133
+ * Sends a request to move a meeting member into the lobby.
134
+ * *
135
+ * @param {Object} options - Request options.
136
+ * @param {string} options.locusUrl - The locus URL for the meeting.
137
+ * @param {string} options.memberId - The ID of the member to move.
138
+ * @param {Object} body - The request payload.
139
+ * @param {Object} body.moveToLobby - Container for move‐to‐lobby data.
140
+ * @param {string[]} body.moveToLobby.participantIds - Array of participant IDs to move.
141
+ * @returns {Promise} - Resolves with the locus‐delta response.
142
+ */
143
+ moveToLobbyMember(
144
+ options: {locusUrl: string; memberId: string},
145
+ body: {moveToLobby: {participantIds: string[]}}
146
+ ) {
147
+ if (!options || !options.locusUrl || !options.memberId) {
148
+ throw new ParameterError(
149
+ 'memberId must be defined, and the associated locus url for this meeting object must be defined.'
150
+ );
151
+ }
152
+
153
+ const requestParams = MembersUtil.getMoveMemberToLobbyRequestParams(options, body);
154
+
155
+ return this.locusDeltaRequest(requestParams);
156
+ }
157
+
132
158
  /**
133
159
  * Sends a request to raise or lower a member's hand
134
160
  * @param {Object} options
@@ -252,4 +278,22 @@ export default class MembersRequest extends StatelessWebexPlugin {
252
278
 
253
279
  return this.locusDeltaRequest(requestParams);
254
280
  }
281
+
282
+ /**
283
+ * @param {Object} options with format of {invitee: object, locusUrl: string}
284
+ * @returns {Promise}
285
+ * @throws {Error} if the options are not valid and complete, must have invitee with memberId AND locusUrl
286
+ * @memberof MembersRequest
287
+ */
288
+ cancelSIPInvite(options: any) {
289
+ if (!options?.invitee?.memberId || !options?.locusUrl) {
290
+ throw new ParameterError(
291
+ 'invitee must be passed and the associated locus url for this meeting object must be defined.'
292
+ );
293
+ }
294
+
295
+ const requestParams = MembersUtil.generateCancelSIPInviteRequestParams(options);
296
+
297
+ return this.locusDeltaRequest(requestParams);
298
+ }
255
299
  }
@@ -110,7 +110,10 @@ const MembersUtil = {
110
110
  return !DIALER_REGEX.E164_FORMAT.test(invitee.phoneNumber);
111
111
  }
112
112
 
113
- return !VALID_EMAIL_ADDRESS.test(invitee.email || invitee.emailAddress);
113
+ return !(
114
+ VALID_EMAIL_ADDRESS.test(invitee.email || invitee.emailAddress) ||
115
+ DIALER_REGEX.SIP_ADDRESS.test(invitee.email || invitee.emailAddress)
116
+ );
114
117
  },
115
118
 
116
119
  getRemoveMemberRequestParams: (options) => {
@@ -203,6 +206,22 @@ const MembersUtil = {
203
206
  };
204
207
  },
205
208
 
209
+ getMoveMemberToLobbyRequestBody: (memberId: string) => ({
210
+ moveToLobby: {
211
+ participantIds: [memberId],
212
+ },
213
+ }),
214
+
215
+ getMoveMemberToLobbyRequestParams: (options: {memberId: string; locusUrl: string}, body) => {
216
+ const uri = `${options.locusUrl}/${PARTICIPANT}/${options.memberId}/${CONTROLS}`;
217
+
218
+ return {
219
+ method: HTTP_VERBS.PATCH,
220
+ uri,
221
+ body,
222
+ };
223
+ },
224
+
206
225
  /**
207
226
  * @param {ServerRoleShape} role
208
227
  * @returns {ServerRoleShape} the role shape to be added to the body
@@ -351,6 +370,29 @@ const MembersUtil = {
351
370
 
352
371
  return requestParams;
353
372
  },
373
+
374
+ cancelSIPInviteOptions: (invitee, locusUrl) => ({
375
+ invitee,
376
+ locusUrl,
377
+ }),
378
+
379
+ generateCancelSIPInviteRequestParams: (options) => {
380
+ const body = {
381
+ actionType: _REMOVE_,
382
+ invitees: [
383
+ {
384
+ address: options.invitee.memberId,
385
+ },
386
+ ],
387
+ };
388
+ const requestParams = {
389
+ method: HTTP_VERBS.PUT,
390
+ uri: options.locusUrl,
391
+ body,
392
+ };
393
+
394
+ return requestParams;
395
+ },
354
396
  };
355
397
 
356
398
  export default MembersUtil;
@@ -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
  });
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