@webex/plugin-meetings 2.60.0-next.9 → 2.60.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 (55) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.js +2 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.js +5 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/interceptors/index.d.ts +2 -0
  9. package/dist/interceptors/index.js +15 -0
  10. package/dist/interceptors/index.js.map +1 -0
  11. package/dist/interceptors/locusRetry.d.ts +27 -0
  12. package/dist/interceptors/locusRetry.js +94 -0
  13. package/dist/interceptors/locusRetry.js.map +1 -0
  14. package/dist/interpretation/index.js +1 -1
  15. package/dist/interpretation/siLanguage.js +1 -1
  16. package/dist/meeting/index.js +6 -2
  17. package/dist/meeting/index.js.map +1 -1
  18. package/dist/meetings/index.d.ts +1 -11
  19. package/dist/meetings/index.js +4 -3
  20. package/dist/meetings/index.js.map +1 -1
  21. package/dist/reachability/clusterReachability.d.ts +109 -0
  22. package/dist/reachability/clusterReachability.js +357 -0
  23. package/dist/reachability/clusterReachability.js.map +1 -0
  24. package/dist/reachability/index.d.ts +32 -121
  25. package/dist/reachability/index.js +173 -459
  26. package/dist/reachability/index.js.map +1 -1
  27. package/dist/reachability/util.d.ts +8 -0
  28. package/dist/reachability/util.js +29 -0
  29. package/dist/reachability/util.js.map +1 -0
  30. package/dist/statsAnalyzer/index.d.ts +22 -0
  31. package/dist/statsAnalyzer/index.js +60 -0
  32. package/dist/statsAnalyzer/index.js.map +1 -1
  33. package/dist/statsAnalyzer/mqaUtil.js +4 -0
  34. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  35. package/dist/webinar/index.js +1 -1
  36. package/package.json +25 -22
  37. package/src/config.ts +1 -0
  38. package/src/index.ts +4 -0
  39. package/src/interceptors/index.ts +3 -0
  40. package/src/interceptors/locusRetry.ts +67 -0
  41. package/src/meeting/index.ts +10 -2
  42. package/src/meetings/index.ts +4 -3
  43. package/src/reachability/clusterReachability.ts +320 -0
  44. package/src/reachability/index.ts +124 -421
  45. package/src/reachability/util.ts +24 -0
  46. package/src/statsAnalyzer/index.ts +64 -1
  47. package/src/statsAnalyzer/mqaUtil.ts +4 -0
  48. package/test/unit/spec/interceptors/locusRetry.ts +131 -0
  49. package/test/unit/spec/meeting/index.js +8 -1
  50. package/test/unit/spec/meetings/index.js +28 -25
  51. package/test/unit/spec/reachability/clusterReachability.ts +279 -0
  52. package/test/unit/spec/reachability/index.ts +159 -226
  53. package/test/unit/spec/reachability/util.ts +40 -0
  54. package/test/unit/spec/roap/request.ts +26 -3
  55. package/test/unit/spec/stats-analyzer/index.js +100 -20
package/package.json CHANGED
@@ -44,13 +44,13 @@
44
44
  "@webex/eslint-config-legacy": "0.0.0",
45
45
  "@webex/jest-config-legacy": "0.0.0",
46
46
  "@webex/legacy-tools": "0.0.0",
47
- "@webex/plugin-meetings": "2.60.0-next.9",
48
- "@webex/plugin-rooms": "2.60.0-next.6",
49
- "@webex/test-helper-chai": "2.60.0-next.6",
50
- "@webex/test-helper-mocha": "2.60.0-next.6",
51
- "@webex/test-helper-mock-webex": "2.60.0-next.6",
52
- "@webex/test-helper-retry": "2.60.0-next.6",
53
- "@webex/test-helper-test-users": "2.60.0-next.6",
47
+ "@webex/plugin-meetings": "2.60.1-next.1",
48
+ "@webex/plugin-rooms": "2.60.1-next.1",
49
+ "@webex/test-helper-chai": "2.60.1-next.1",
50
+ "@webex/test-helper-mocha": "2.60.1-next.1",
51
+ "@webex/test-helper-mock-webex": "2.60.1-next.1",
52
+ "@webex/test-helper-retry": "2.60.1-next.1",
53
+ "@webex/test-helper-test-users": "2.60.1-next.1",
54
54
  "chai": "^4.3.4",
55
55
  "chai-as-promised": "^7.1.1",
56
56
  "eslint": "^8.24.0",
@@ -62,19 +62,19 @@
62
62
  "typescript": "^4.7.4"
63
63
  },
64
64
  "dependencies": {
65
- "@webex/common": "2.60.0-next.6",
66
- "@webex/internal-media-core": "2.2.3",
67
- "@webex/internal-plugin-conversation": "2.60.0-next.6",
68
- "@webex/internal-plugin-device": "2.60.0-next.6",
69
- "@webex/internal-plugin-llm": "2.60.0-next.6",
70
- "@webex/internal-plugin-mercury": "2.60.0-next.6",
71
- "@webex/internal-plugin-metrics": "2.60.0-next.6",
72
- "@webex/internal-plugin-support": "2.60.0-next.6",
73
- "@webex/internal-plugin-user": "2.60.0-next.6",
74
- "@webex/media-helpers": "3.0.0-next.17",
75
- "@webex/plugin-people": "2.60.0-next.6",
76
- "@webex/plugin-rooms": "2.60.0-next.6",
77
- "@webex/webex-core": "2.60.0-next.6",
65
+ "@webex/common": "2.60.1-next.1",
66
+ "@webex/internal-media-core": "2.2.6",
67
+ "@webex/internal-plugin-conversation": "2.60.1-next.1",
68
+ "@webex/internal-plugin-device": "2.60.1-next.1",
69
+ "@webex/internal-plugin-llm": "2.60.1-next.1",
70
+ "@webex/internal-plugin-mercury": "2.60.1-next.1",
71
+ "@webex/internal-plugin-metrics": "2.60.1-next.1",
72
+ "@webex/internal-plugin-support": "2.60.1-next.1",
73
+ "@webex/internal-plugin-user": "2.60.1-next.1",
74
+ "@webex/media-helpers": "3.0.0-next.20",
75
+ "@webex/plugin-people": "2.60.1-next.1",
76
+ "@webex/plugin-rooms": "2.60.1-next.1",
77
+ "@webex/webex-core": "2.60.1-next.1",
78
78
  "ampersand-collection": "^2.0.2",
79
79
  "bowser": "^2.11.0",
80
80
  "btoa": "^1.2.1",
@@ -82,11 +82,14 @@
82
82
  "global": "^4.4.0",
83
83
  "ip-anonymize": "^0.1.0",
84
84
  "javascript-state-machine": "^3.1.0",
85
- "jwt-decode": "^4.0.0",
85
+ "jwt-decode": "3.1.2",
86
86
  "lodash": "^4.17.21",
87
87
  "sdp-transform": "^2.12.0",
88
88
  "uuid": "^3.3.2",
89
89
  "webrtc-adapter": "^8.1.2"
90
90
  },
91
- "version": "2.60.0-next.9"
91
+ "//": [
92
+ "TODO: upgrade jwt-decode when moving to node 18"
93
+ ],
94
+ "version": "2.60.1-next.1"
92
95
  }
package/src/config.ts CHANGED
@@ -86,6 +86,7 @@ export default {
86
86
  enableMediaNegotiatedEvent: false,
87
87
  enableUnifiedMeetings: true,
88
88
  enableAdhocMeetings: true,
89
+ enableTcpReachability: false,
89
90
  },
90
91
  degradationPreferences: {
91
92
  maxMacroblocksLimit: 8192,
package/src/index.ts CHANGED
@@ -3,9 +3,13 @@ import {registerPlugin} from '@webex/webex-core';
3
3
 
4
4
  import Meetings from './meetings';
5
5
  import config from './config';
6
+ import {LocusRetryStatusInterceptor} from './interceptors';
6
7
 
7
8
  registerPlugin('meetings', Meetings, {
8
9
  config,
10
+ interceptors: {
11
+ LocusRetryStatusInterceptor: LocusRetryStatusInterceptor.create,
12
+ },
9
13
  });
10
14
 
11
15
  export {
@@ -0,0 +1,3 @@
1
+ import LocusRetryStatusInterceptor from './locusRetry';
2
+
3
+ export {LocusRetryStatusInterceptor};
@@ -0,0 +1,67 @@
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import {Interceptor} from '@webex/http-core';
6
+
7
+ const rateLimitExpiryTime = new WeakMap();
8
+ /**
9
+ * @class
10
+ */
11
+ export default class LocusRetryStatusInterceptor extends Interceptor {
12
+ /**
13
+ * @returns {LocusRetryStatusInterceptor}
14
+ */
15
+ static create() {
16
+ // @ts-ignore
17
+ return new LocusRetryStatusInterceptor({webex: this});
18
+ }
19
+
20
+ /**
21
+ * Handle response errors
22
+ * @param {Object} options
23
+ * @param {WebexHttpError} reason
24
+ * @returns {Promise<WebexHttpError>}
25
+ */
26
+ onResponseError(options, reason) {
27
+ if ((reason.statusCode === 503 || reason.statusCode === 429) && options.uri.includes('locus')) {
28
+ const hasRetriedLocusRequest = rateLimitExpiryTime.get(this);
29
+ const retryAfterTime = options.headers['retry-after'] || 2000;
30
+
31
+ if (hasRetriedLocusRequest) {
32
+ rateLimitExpiryTime.set(this, false);
33
+
34
+ return Promise.reject(options);
35
+ }
36
+ rateLimitExpiryTime.set(this, true);
37
+
38
+ return this.handleRetryRequestLocusServiceError(options, retryAfterTime);
39
+ }
40
+
41
+ return Promise.reject(reason);
42
+ }
43
+
44
+ /**
45
+ * Handle retries for locus service unavailable errors
46
+ * @param {Object} options associated with the request
47
+ * @param {number} retryAfterTime retry after time in milliseconds
48
+ * @returns {Promise}
49
+ */
50
+ handleRetryRequestLocusServiceError(options, retryAfterTime) {
51
+ return new Promise((resolve, reject) => {
52
+ const timeout = setTimeout(() => {
53
+ clearTimeout(timeout);
54
+
55
+ // @ts-ignore
56
+ this.webex
57
+ .request({
58
+ method: options.method,
59
+ uri: options.uri,
60
+ body: options.body,
61
+ })
62
+ .then(resolve)
63
+ .catch(reject);
64
+ }, retryAfterTime);
65
+ });
66
+ }
67
+ }
@@ -1,6 +1,6 @@
1
1
  import uuid from 'uuid';
2
2
  import {cloneDeep, isEqual, isEmpty} from 'lodash';
3
- import {jwtDecode as decode} from 'jwt-decode';
3
+ import jwtDecode from 'jwt-decode';
4
4
  // @ts-ignore - Fix this
5
5
  import {StatelessWebexPlugin} from '@webex/webex-core';
6
6
  // @ts-ignore - Types not available for @webex/common
@@ -3549,7 +3549,7 @@ export default class Meeting extends StatelessWebexPlugin {
3549
3549
  * @returns {void}
3550
3550
  */
3551
3551
  public setPermissionTokenPayload(permissionToken: string) {
3552
- this.permissionTokenPayload = decode(permissionToken);
3552
+ this.permissionTokenPayload = jwtDecode(permissionToken);
3553
3553
  this.permissionTokenReceivedLocalTime = new Date().getTime();
3554
3554
  }
3555
3555
 
@@ -4260,6 +4260,14 @@ export default class Meeting extends StatelessWebexPlugin {
4260
4260
  ) {
4261
4261
  const {mediaOptions, joinOptions} = options;
4262
4262
 
4263
+ if (!mediaOptions?.allowMediaInLobby) {
4264
+ return Promise.reject(
4265
+ new ParameterError('joinWithMedia() can only be used with allowMediaInLobby set to true')
4266
+ );
4267
+ }
4268
+
4269
+ LoggerProxy.logger.info('Meeting:index#joinWithMedia called');
4270
+
4263
4271
  return this.join(joinOptions)
4264
4272
  .then((joinResponse) =>
4265
4273
  this.addMedia(mediaOptions).then((mediaResponse) => ({
@@ -1369,11 +1369,12 @@ export default class Meetings extends WebexPlugin {
1369
1369
  const meetingsCollection = this.meetingCollection.getAll();
1370
1370
 
1371
1371
  if (Object.keys(meetingsCollection).length > 0) {
1372
- // Some time the mercury event is missed after mercury reconnect
1373
- // if sync returns no locus then clear all the meetings
1372
+ // Sometimes the mercury events are lost after mercury reconnect
1373
+ // Remove any Locus meetings that are not returned by Locus
1374
+ // (they had a locusUrl previously but are no longer active) in the sync
1374
1375
  for (const meeting of Object.values(meetingsCollection)) {
1375
1376
  // @ts-ignore
1376
- if (!activeLocusUrl.includes(meeting.locusUrl)) {
1377
+ if (meeting.locusUrl && !activeLocusUrl.includes(meeting.locusUrl)) {
1377
1378
  // destroy function also uploads logs
1378
1379
  // @ts-ignore
1379
1380
  this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
@@ -0,0 +1,320 @@
1
+ import {Defer} from '@webex/common';
2
+
3
+ import LoggerProxy from '../common/logs/logger-proxy';
4
+ import {ClusterNode} from './request';
5
+ import {convertStunUrlToTurn} from './util';
6
+
7
+ import {ICE_GATHERING_STATE, CONNECTION_STATE} from '../constants';
8
+
9
+ const DEFAULT_TIMEOUT = 3000;
10
+ const VIDEO_MESH_TIMEOUT = 1000;
11
+
12
+ // result for a specific transport protocol (like udp or tcp)
13
+ export type TransportResult = {
14
+ result: 'reachable' | 'unreachable' | 'untested';
15
+ latencyInMilliseconds?: number; // amount of time it took to get the first ICE candidate
16
+ clientMediaIPs?: string[];
17
+ };
18
+
19
+ // reachability result for a specific media cluster
20
+ export type ClusterReachabilityResult = {
21
+ udp: TransportResult;
22
+ tcp: TransportResult;
23
+ xtls: TransportResult;
24
+ };
25
+
26
+ /**
27
+ * A class that handles reachability checks for a single cluster.
28
+ */
29
+ export class ClusterReachability {
30
+ private numUdpUrls: number;
31
+ private numTcpUrls: number;
32
+ private result: ClusterReachabilityResult;
33
+ private pc?: RTCPeerConnection;
34
+ private defer: Defer; // this defer is resolved once reachability checks for this cluster are completed
35
+ private startTimestamp: number;
36
+ public readonly isVideoMesh: boolean;
37
+ public readonly name;
38
+
39
+ /**
40
+ * Constructor for ClusterReachability
41
+ * @param {string} name cluster name
42
+ * @param {ClusterNode} clusterInfo information about the media cluster
43
+ */
44
+ constructor(name: string, clusterInfo: ClusterNode) {
45
+ this.name = name;
46
+ this.isVideoMesh = clusterInfo.isVideoMesh;
47
+ this.numUdpUrls = clusterInfo.udp.length;
48
+ this.numTcpUrls = clusterInfo.tcp.length;
49
+
50
+ this.pc = this.createPeerConnection(clusterInfo);
51
+
52
+ this.defer = new Defer();
53
+ this.result = {
54
+ udp: {
55
+ result: 'untested',
56
+ },
57
+ tcp: {
58
+ result: 'untested',
59
+ },
60
+ xtls: {
61
+ result: 'untested',
62
+ },
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Gets total elapsed time, can be called only after start() is called
68
+ * @returns {Number} Milliseconds
69
+ */
70
+ private getElapsedTime() {
71
+ return Math.round(performance.now() - this.startTimestamp);
72
+ }
73
+
74
+ /**
75
+ * Generate peerConnection config settings
76
+ * @param {ClusterNode} cluster
77
+ * @returns {RTCConfiguration} peerConnectionConfig
78
+ */
79
+ private buildPeerConnectionConfig(cluster: ClusterNode): RTCConfiguration {
80
+ const udpIceServers = cluster.udp.map((url) => ({
81
+ username: '',
82
+ credential: '',
83
+ urls: [url],
84
+ }));
85
+
86
+ // STUN servers are contacted only using UDP, so in order to test TCP reachability
87
+ // we pretend that Linus is a TURN server, because we can explicitly say "transport=tcp" in TURN urls.
88
+ // We then check for relay candidates to know if TURN-TCP worked (see registerIceCandidateListener()).
89
+ const tcpIceServers = cluster.tcp.map((urlString: string) => {
90
+ return {
91
+ username: 'webexturnreachuser',
92
+ credential: 'webexturnreachpwd',
93
+ urls: [convertStunUrlToTurn(urlString, 'tcp')],
94
+ };
95
+ });
96
+
97
+ return {
98
+ iceServers: [...udpIceServers, ...tcpIceServers],
99
+ iceCandidatePoolSize: 0,
100
+ iceTransportPolicy: 'all',
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Creates an RTCPeerConnection
106
+ * @param {ClusterNode} clusterInfo information about the media cluster
107
+ * @returns {RTCPeerConnection} peerConnection
108
+ */
109
+ private createPeerConnection(clusterInfo: ClusterNode) {
110
+ try {
111
+ const config = this.buildPeerConnectionConfig(clusterInfo);
112
+
113
+ const peerConnection = new RTCPeerConnection(config);
114
+
115
+ return peerConnection;
116
+ } catch (peerConnectionError) {
117
+ LoggerProxy.logger.warn(
118
+ `Reachability:index#createPeerConnection --> Error creating peerConnection:`,
119
+ peerConnectionError
120
+ );
121
+
122
+ return undefined;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @returns {ClusterReachabilityResult} reachability result for this cluster
128
+ */
129
+ getResult() {
130
+ return this.result;
131
+ }
132
+
133
+ /**
134
+ * Closes the peerConnection
135
+ *
136
+ * @returns {void}
137
+ */
138
+ private closePeerConnection() {
139
+ if (this.pc) {
140
+ this.pc.onicecandidate = null;
141
+ this.pc.onicegatheringstatechange = null;
142
+ this.pc.close();
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Resolves the defer, indicating that reachability checks for this cluster are completed
148
+ *
149
+ * @returns {void}
150
+ */
151
+ private finishReachabilityCheck() {
152
+ this.defer.resolve();
153
+ }
154
+
155
+ /**
156
+ * Adds public IP (client media IPs)
157
+ * @param {string} protocol
158
+ * @param {string} publicIP
159
+ * @returns {void}
160
+ */
161
+ private addPublicIP(protocol: 'udp' | 'tcp', publicIP?: string | null) {
162
+ const result = this.result[protocol];
163
+
164
+ if (publicIP) {
165
+ if (result.clientMediaIPs) {
166
+ if (!result.clientMediaIPs.includes(publicIP)) {
167
+ result.clientMediaIPs.push(publicIP);
168
+ }
169
+ } else {
170
+ result.clientMediaIPs = [publicIP];
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Registers a listener for the iceGatheringStateChange event
177
+ *
178
+ * @returns {void}
179
+ */
180
+ private registerIceGatheringStateChangeListener() {
181
+ this.pc.onicegatheringstatechange = () => {
182
+ const {COMPLETE} = ICE_GATHERING_STATE;
183
+
184
+ if (this.pc.iceConnectionState === COMPLETE) {
185
+ this.closePeerConnection();
186
+ this.finishReachabilityCheck();
187
+ }
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Checks if we have the results for all the protocols (UDP and TCP)
193
+ *
194
+ * @returns {boolean} true if we have all results, false otherwise
195
+ */
196
+ private haveWeGotAllResults(): boolean {
197
+ return ['udp', 'tcp'].every(
198
+ (protocol) =>
199
+ this.result[protocol].result === 'reachable' || this.result[protocol].result === 'untested'
200
+ );
201
+ }
202
+
203
+ /**
204
+ * Stores the latency in the result for the given protocol and marks it as reachable
205
+ *
206
+ * @param {string} protocol
207
+ * @param {number} latency
208
+ * @returns {void}
209
+ */
210
+ private storeLatencyResult(protocol: 'udp' | 'tcp', latency: number) {
211
+ const result = this.result[protocol];
212
+
213
+ if (result.latencyInMilliseconds === undefined) {
214
+ LoggerProxy.logger.log(
215
+ // @ts-ignore
216
+ `Reachability:index#storeLatencyResult --> Successfully reached ${this.name} over ${protocol}: ${latency}ms`
217
+ );
218
+ result.latencyInMilliseconds = latency;
219
+ result.result = 'reachable';
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Registers a listener for the icecandidate event
225
+ *
226
+ * @returns {void}
227
+ */
228
+ private registerIceCandidateListener() {
229
+ this.pc.onicecandidate = (e) => {
230
+ const CANDIDATE_TYPES = {
231
+ SERVER_REFLEXIVE: 'srflx',
232
+ RELAY: 'relay',
233
+ };
234
+
235
+ if (e.candidate) {
236
+ if (e.candidate.type === CANDIDATE_TYPES.SERVER_REFLEXIVE) {
237
+ this.storeLatencyResult('udp', this.getElapsedTime());
238
+ this.addPublicIP('udp', e.candidate.address);
239
+ }
240
+
241
+ if (e.candidate.type === CANDIDATE_TYPES.RELAY) {
242
+ this.storeLatencyResult('tcp', this.getElapsedTime());
243
+ // we don't add public IP for TCP, because in the case of relay candidates
244
+ // e.candidate.address is the TURN server address, not the client's public IP
245
+ }
246
+
247
+ if (this.haveWeGotAllResults()) {
248
+ this.closePeerConnection();
249
+ this.finishReachabilityCheck();
250
+ }
251
+ }
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Starts the process of doing UDP and TCP reachability checks on the media cluster.
257
+ * XTLS reachability checking is not supported.
258
+ *
259
+ * @returns {Promise}
260
+ */
261
+ async start(): Promise<ClusterReachabilityResult> {
262
+ if (!this.pc) {
263
+ LoggerProxy.logger.warn(
264
+ `Reachability:ClusterReachability#start --> Error: peerConnection is undefined`
265
+ );
266
+
267
+ return this.result;
268
+ }
269
+
270
+ // Initialize this.result as saying that nothing is reachable.
271
+ // It will get updated as we go along and successfully gather ICE candidates.
272
+ this.result.udp = {
273
+ result: this.numUdpUrls > 0 ? 'unreachable' : 'untested',
274
+ };
275
+ this.result.tcp = {
276
+ result: this.numTcpUrls > 0 ? 'unreachable' : 'untested',
277
+ };
278
+
279
+ try {
280
+ const offer = await this.pc.createOffer({offerToReceiveAudio: true});
281
+
282
+ this.startTimestamp = performance.now();
283
+
284
+ // not awaiting the next call on purpose, because we're not sending the offer anywhere and there won't be any answer
285
+ // we just need to make this call to trigger the ICE gathering process
286
+ this.pc.setLocalDescription(offer);
287
+
288
+ await this.gatherIceCandidates();
289
+ } catch (error) {
290
+ LoggerProxy.logger.warn(`Reachability:ClusterReachability#start --> Error: `, error);
291
+ }
292
+
293
+ return this.result;
294
+ }
295
+
296
+ /**
297
+ * Starts the process of gathering ICE candidates
298
+ *
299
+ * @returns {Promise} promise that's resolved once reachability checks for this cluster are completed or timeout is reached
300
+ */
301
+ private gatherIceCandidates() {
302
+ const timeout = this.isVideoMesh ? VIDEO_MESH_TIMEOUT : DEFAULT_TIMEOUT;
303
+
304
+ this.registerIceGatheringStateChangeListener();
305
+ this.registerIceCandidateListener();
306
+
307
+ // Set maximum timeout
308
+ setTimeout(() => {
309
+ const {CLOSED} = CONNECTION_STATE;
310
+
311
+ // Close any open peerConnections
312
+ if (this.pc.connectionState !== CLOSED) {
313
+ this.closePeerConnection();
314
+ this.finishReachabilityCheck();
315
+ }
316
+ }, timeout);
317
+
318
+ return this.defer.promise;
319
+ }
320
+ }