@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.
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/interceptors/index.d.ts +2 -0
- package/dist/interceptors/index.js +15 -0
- package/dist/interceptors/index.js.map +1 -0
- package/dist/interceptors/locusRetry.d.ts +27 -0
- package/dist/interceptors/locusRetry.js +94 -0
- package/dist/interceptors/locusRetry.js.map +1 -0
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/meeting/index.js +6 -2
- package/dist/meeting/index.js.map +1 -1
- package/dist/meetings/index.d.ts +1 -11
- package/dist/meetings/index.js +4 -3
- package/dist/meetings/index.js.map +1 -1
- package/dist/reachability/clusterReachability.d.ts +109 -0
- package/dist/reachability/clusterReachability.js +357 -0
- package/dist/reachability/clusterReachability.js.map +1 -0
- package/dist/reachability/index.d.ts +32 -121
- package/dist/reachability/index.js +173 -459
- package/dist/reachability/index.js.map +1 -1
- package/dist/reachability/util.d.ts +8 -0
- package/dist/reachability/util.js +29 -0
- package/dist/reachability/util.js.map +1 -0
- package/dist/statsAnalyzer/index.d.ts +22 -0
- package/dist/statsAnalyzer/index.js +60 -0
- package/dist/statsAnalyzer/index.js.map +1 -1
- package/dist/statsAnalyzer/mqaUtil.js +4 -0
- package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
- package/dist/webinar/index.js +1 -1
- package/package.json +25 -22
- package/src/config.ts +1 -0
- package/src/index.ts +4 -0
- package/src/interceptors/index.ts +3 -0
- package/src/interceptors/locusRetry.ts +67 -0
- package/src/meeting/index.ts +10 -2
- package/src/meetings/index.ts +4 -3
- package/src/reachability/clusterReachability.ts +320 -0
- package/src/reachability/index.ts +124 -421
- package/src/reachability/util.ts +24 -0
- package/src/statsAnalyzer/index.ts +64 -1
- package/src/statsAnalyzer/mqaUtil.ts +4 -0
- package/test/unit/spec/interceptors/locusRetry.ts +131 -0
- package/test/unit/spec/meeting/index.js +8 -1
- package/test/unit/spec/meetings/index.js +28 -25
- package/test/unit/spec/reachability/clusterReachability.ts +279 -0
- package/test/unit/spec/reachability/index.ts +159 -226
- package/test/unit/spec/reachability/util.ts +40 -0
- package/test/unit/spec/roap/request.ts +26 -3
- 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.
|
|
48
|
-
"@webex/plugin-rooms": "2.60.
|
|
49
|
-
"@webex/test-helper-chai": "2.60.
|
|
50
|
-
"@webex/test-helper-mocha": "2.60.
|
|
51
|
-
"@webex/test-helper-mock-webex": "2.60.
|
|
52
|
-
"@webex/test-helper-retry": "2.60.
|
|
53
|
-
"@webex/test-helper-test-users": "2.60.
|
|
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.
|
|
66
|
-
"@webex/internal-media-core": "2.2.
|
|
67
|
-
"@webex/internal-plugin-conversation": "2.60.
|
|
68
|
-
"@webex/internal-plugin-device": "2.60.
|
|
69
|
-
"@webex/internal-plugin-llm": "2.60.
|
|
70
|
-
"@webex/internal-plugin-mercury": "2.60.
|
|
71
|
-
"@webex/internal-plugin-metrics": "2.60.
|
|
72
|
-
"@webex/internal-plugin-support": "2.60.
|
|
73
|
-
"@webex/internal-plugin-user": "2.60.
|
|
74
|
-
"@webex/media-helpers": "3.0.0-next.
|
|
75
|
-
"@webex/plugin-people": "2.60.
|
|
76
|
-
"@webex/plugin-rooms": "2.60.
|
|
77
|
-
"@webex/webex-core": "2.60.
|
|
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": "
|
|
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
|
-
"
|
|
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
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,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
|
+
}
|
package/src/meeting/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import uuid from 'uuid';
|
|
2
2
|
import {cloneDeep, isEqual, isEmpty} from 'lodash';
|
|
3
|
-
import
|
|
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 =
|
|
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) => ({
|
package/src/meetings/index.ts
CHANGED
|
@@ -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
|
-
//
|
|
1373
|
-
//
|
|
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
|
+
}
|