@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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* eslint-disable import/prefer-default-export */
|
|
2
|
+
/**
|
|
3
|
+
* Converts a stun url to a turn url
|
|
4
|
+
*
|
|
5
|
+
* @param {string} stunUrl url of a stun server
|
|
6
|
+
* @param {'tcp'|'udp'} protocol what protocol to use for the turn server
|
|
7
|
+
* @returns {string} url of a turn server
|
|
8
|
+
*/
|
|
9
|
+
export function convertStunUrlToTurn(stunUrl: string, protocol: 'udp' | 'tcp') {
|
|
10
|
+
// stunUrl looks like this: "stun:external-media91.public.wjfkm-a-10.prod.infra.webex.com:5004"
|
|
11
|
+
// and we need it to be like this: "turn:external-media91.public.wjfkm-a-10.prod.infra.webex.com:5004?transport=tcp"
|
|
12
|
+
const url = new URL(stunUrl);
|
|
13
|
+
|
|
14
|
+
if (url.protocol !== 'stun:') {
|
|
15
|
+
throw new Error(`Not a STUN URL: ${stunUrl}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
url.protocol = 'turn:';
|
|
19
|
+
if (protocol === 'tcp') {
|
|
20
|
+
url.searchParams.append('transport', 'tcp');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return url.toString();
|
|
24
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable prefer-destructuring */
|
|
2
2
|
|
|
3
|
-
import {cloneDeep} from 'lodash';
|
|
3
|
+
import {cloneDeep, isEmpty} from 'lodash';
|
|
4
4
|
import {ConnectionState} from '@webex/internal-media-core';
|
|
5
5
|
|
|
6
6
|
import EventsScope from '../common/events/events-scope';
|
|
@@ -78,6 +78,7 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
78
78
|
statsResults: any;
|
|
79
79
|
statsStarted: any;
|
|
80
80
|
successfulCandidatePair: any;
|
|
81
|
+
localIpAddress: string; // Returns the local IP address for diagnostics. this is the local IP of the interface used for the current media connection a host can have many local Ip Addresses
|
|
81
82
|
receiveSlotCallback: ReceiveSlotCallback;
|
|
82
83
|
|
|
83
84
|
/**
|
|
@@ -107,6 +108,7 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
107
108
|
this.lastEmittedStartStopEvent = {};
|
|
108
109
|
this.receiveSlotCallback = receiveSlotCallback;
|
|
109
110
|
this.successfulCandidatePair = {};
|
|
111
|
+
this.localIpAddress = '';
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
/**
|
|
@@ -266,6 +268,16 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
266
268
|
this.mediaConnection = mediaConnection;
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Returns the local IP address for diagnostics.
|
|
273
|
+
* this is the local IP of the interface used for the current media connection
|
|
274
|
+
* a host can have many local Ip Addresses
|
|
275
|
+
* @returns {string | undefined} The local IP address.
|
|
276
|
+
*/
|
|
277
|
+
getLocalIpAddress(): string {
|
|
278
|
+
return this.localIpAddress;
|
|
279
|
+
}
|
|
280
|
+
|
|
269
281
|
/**
|
|
270
282
|
* Starts the stats analyzer on interval
|
|
271
283
|
*
|
|
@@ -405,6 +417,10 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
405
417
|
this.statsResults[type].direction = statsItem.currentDirection;
|
|
406
418
|
this.statsResults[type].trackLabel = statsItem.localTrackLabel;
|
|
407
419
|
this.statsResults[type].csi = statsItem.csi;
|
|
420
|
+
this.extractAndSetLocalIpAddressInfoForDiagnostics(
|
|
421
|
+
this.successfulCandidatePair?.localCandidateId,
|
|
422
|
+
this.statsResults?.candidates
|
|
423
|
+
);
|
|
408
424
|
// reset the successful candidate pair.
|
|
409
425
|
this.successfulCandidatePair = {};
|
|
410
426
|
}
|
|
@@ -983,6 +999,48 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
983
999
|
}
|
|
984
1000
|
}
|
|
985
1001
|
|
|
1002
|
+
/**
|
|
1003
|
+
* extracts the local Ip address from the statsResult object by looking at stats results candidates
|
|
1004
|
+
* and matches that ID with the successful candidate pair. It looks at the type of local candidate it is
|
|
1005
|
+
* and then extracts the IP address from the relatedAddress or address property based on conditions known in webrtc
|
|
1006
|
+
* note, there are known incompatibilities and it is possible for this to set undefined, or for the IP address to be the public IP address
|
|
1007
|
+
* for example, firefox does not set the relayProtocol, and if the user is behind a NAT it might be the public IP
|
|
1008
|
+
* @private
|
|
1009
|
+
* @param {string} successfulCandidatePairId - The ID of the successful candidate pair.
|
|
1010
|
+
* @param {Object} candidates - the stats result candidates
|
|
1011
|
+
* @returns {void}
|
|
1012
|
+
*/
|
|
1013
|
+
extractAndSetLocalIpAddressInfoForDiagnostics = (
|
|
1014
|
+
successfulCandidatePairId: string,
|
|
1015
|
+
candidates: {[key: string]: Record<string, unknown>}
|
|
1016
|
+
) => {
|
|
1017
|
+
let newIpAddress = '';
|
|
1018
|
+
if (successfulCandidatePairId && !isEmpty(candidates)) {
|
|
1019
|
+
const localCandidate = candidates[successfulCandidatePairId];
|
|
1020
|
+
if (localCandidate) {
|
|
1021
|
+
if (localCandidate.candidateType === 'host') {
|
|
1022
|
+
// if it's a host candidate, use the address property - it will be the local IP
|
|
1023
|
+
newIpAddress = `${localCandidate.address}`;
|
|
1024
|
+
} else if (localCandidate.candidateType === 'prflx') {
|
|
1025
|
+
// if it's a peer reflexive candidate and we're not using a relay (there is no relayProtocol set)
|
|
1026
|
+
// then look at the relatedAddress - it will be the local
|
|
1027
|
+
//
|
|
1028
|
+
// Firefox doesn't populate the relayProtocol property
|
|
1029
|
+
if (!localCandidate.relayProtocol) {
|
|
1030
|
+
newIpAddress = `${localCandidate.relatedAddress}`;
|
|
1031
|
+
} else {
|
|
1032
|
+
// if it's a peer reflexive candidate and we are using a relay -
|
|
1033
|
+
// in that case the relatedAddress will be the IP of the TURN server (Linus),
|
|
1034
|
+
// so we can only look at the address, but it might be local IP or public IP,
|
|
1035
|
+
// depending on if the user is behind a NAT or not
|
|
1036
|
+
newIpAddress = `${localCandidate.address}`;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
this.localIpAddress = newIpAddress;
|
|
1042
|
+
};
|
|
1043
|
+
|
|
986
1044
|
/**
|
|
987
1045
|
* Processes remote and local candidate result and stores
|
|
988
1046
|
* @private
|
|
@@ -1020,6 +1078,11 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
1020
1078
|
this.statsResults.candidates[result.id] = {
|
|
1021
1079
|
candidateType: result.candidateType,
|
|
1022
1080
|
ipAddress: result.ip, // TODO: add ports
|
|
1081
|
+
relatedAddress: result.relatedAddress,
|
|
1082
|
+
relatedPort: result.relatedPort,
|
|
1083
|
+
relayProtocol: result.relayProtocol,
|
|
1084
|
+
protocol: result.protocol,
|
|
1085
|
+
address: result.address,
|
|
1023
1086
|
portNumber: result.port,
|
|
1024
1087
|
networkType: result.networkType,
|
|
1025
1088
|
priority: result.priority,
|
|
@@ -23,6 +23,7 @@ export const getAudioReceiverMqa = ({audioReceiver, statsResults, lastMqaDataSen
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
audioReceiver.common.common.direction = statsResults[mediaType].direction;
|
|
26
|
+
audioReceiver.common.common.isMain = !mediaType.includes('-share');
|
|
26
27
|
audioReceiver.common.transportType = statsResults.connectionType.local.transport;
|
|
27
28
|
|
|
28
29
|
// add rtpPacket info inside common as also for call analyzer
|
|
@@ -83,6 +84,7 @@ export const getAudioSenderMqa = ({audioSender, statsResults, lastMqaDataSent, m
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
audioSender.common.common.direction = statsResults[mediaType].direction;
|
|
87
|
+
audioSender.common.common.isMain = !mediaType.includes('-share');
|
|
86
88
|
audioSender.common.transportType = statsResults.connectionType.local.transport;
|
|
87
89
|
|
|
88
90
|
audioSender.common.maxRemoteJitter =
|
|
@@ -146,6 +148,7 @@ export const getVideoReceiverMqa = ({videoReceiver, statsResults, lastMqaDataSen
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
videoReceiver.common.common.direction = statsResults[mediaType].direction;
|
|
151
|
+
videoReceiver.common.common.isMain = !mediaType.includes('-share');
|
|
149
152
|
videoReceiver.common.transportType = statsResults.connectionType.local.transport;
|
|
150
153
|
|
|
151
154
|
// collect the packets received for the last min
|
|
@@ -226,6 +229,7 @@ export const getVideoSenderMqa = ({videoSender, statsResults, lastMqaDataSent, m
|
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
videoSender.common.common.direction = statsResults[mediaType].direction;
|
|
232
|
+
videoSender.common.common.isMain = !mediaType.includes('-share');
|
|
229
233
|
videoSender.common.transportType = statsResults.connectionType.local.transport;
|
|
230
234
|
|
|
231
235
|
// @ts-ignore
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* eslint-disable camelcase */
|
|
6
|
+
import {assert} from '@webex/test-helper-chai';
|
|
7
|
+
import { expect } from "@webex/test-helper-chai";
|
|
8
|
+
import MockWebex from '@webex/test-helper-mock-webex';
|
|
9
|
+
import {LocusRetryStatusInterceptor} from "@webex/plugin-meetings/src/interceptors";
|
|
10
|
+
import {WebexHttpError} from '@webex/webex-core';
|
|
11
|
+
import Meetings from '@webex/plugin-meetings';
|
|
12
|
+
import sinon from 'sinon';
|
|
13
|
+
|
|
14
|
+
describe('plugin-meetings', () => {
|
|
15
|
+
describe('Interceptors', () => {
|
|
16
|
+
describe('LocusRetryStatusInterceptor', () => {
|
|
17
|
+
let interceptor, webex;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
webex = new MockWebex({
|
|
20
|
+
children: {
|
|
21
|
+
meeting: Meetings,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
interceptor = Reflect.apply(LocusRetryStatusInterceptor.create, {
|
|
25
|
+
sessionId: 'mock-webex_uuid',
|
|
26
|
+
}, []);
|
|
27
|
+
});
|
|
28
|
+
describe('#onResponseError', () => {
|
|
29
|
+
const options = {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
trackingid: 'test',
|
|
33
|
+
'retry-after': 1000,
|
|
34
|
+
},
|
|
35
|
+
uri: `https://locus-test.webex.com/locus/api/v1/loci/call`,
|
|
36
|
+
body: 'foo'
|
|
37
|
+
};
|
|
38
|
+
const reason1 = new WebexHttpError.MethodNotAllowed({
|
|
39
|
+
statusCode: 403,
|
|
40
|
+
options: {
|
|
41
|
+
headers: {
|
|
42
|
+
trackingid: 'test',
|
|
43
|
+
'retry-after': 1000,
|
|
44
|
+
},
|
|
45
|
+
uri: `https://locus-test.webex.com/locus/api/v1/loci/call`,
|
|
46
|
+
},
|
|
47
|
+
body: {
|
|
48
|
+
error: 'POST not allwed',
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const reason2 = new WebexHttpError.MethodNotAllowed({
|
|
52
|
+
statusCode: 503,
|
|
53
|
+
options: {
|
|
54
|
+
headers: {
|
|
55
|
+
trackingid: 'test',
|
|
56
|
+
'retry-after': 1000,
|
|
57
|
+
},
|
|
58
|
+
uri: `https://locus-test.webex.com/locus/api/v1/loci/call`,
|
|
59
|
+
},
|
|
60
|
+
body: {
|
|
61
|
+
error: 'Service Unavailable',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('rejects when not locus service unavailable error', () => {
|
|
66
|
+
return assert.isRejected(interceptor.onResponseError(options, reason1));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('calls handleRetryRequestLocusServiceError with correct retry time when locus service unavailable error', () => {
|
|
70
|
+
interceptor.webex.request = sinon.stub().returns(Promise.resolve());
|
|
71
|
+
const handleRetryStub = sinon.stub(interceptor, 'handleRetryRequestLocusServiceError');
|
|
72
|
+
handleRetryStub.returns(Promise.resolve());
|
|
73
|
+
|
|
74
|
+
return interceptor.onResponseError(options, reason2).then(() => {
|
|
75
|
+
expect(handleRetryStub.calledWith(options, 1000)).to.be.true;
|
|
76
|
+
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('#handleRetryRequestLocusServiceError', () => {
|
|
82
|
+
const options = {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: {
|
|
85
|
+
trackingid: 'test',
|
|
86
|
+
},
|
|
87
|
+
uri: `https://locus-test.webex.com/locus/api/v1/loci/call`,
|
|
88
|
+
body: 'foo'
|
|
89
|
+
};
|
|
90
|
+
const retryAfterTime = 2000;
|
|
91
|
+
|
|
92
|
+
it('returns the correct resolved value when the request is successful', () => {
|
|
93
|
+
const mockResponse = 'mock response'
|
|
94
|
+
interceptor.webex.request = sinon.stub().returns(Promise.resolve(mockResponse));
|
|
95
|
+
|
|
96
|
+
return interceptor.handleRetryRequestLocusServiceError(options, retryAfterTime)
|
|
97
|
+
.then((response) => {
|
|
98
|
+
expect(response).to.equal(mockResponse);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('rejects the promise when the request is unsuccessful', () => {
|
|
103
|
+
const rejectionReason = 'Service Unavaialble after retry';
|
|
104
|
+
|
|
105
|
+
interceptor.webex.request = sinon.stub().returns(Promise.reject(rejectionReason));
|
|
106
|
+
|
|
107
|
+
return interceptor.handleRetryRequestLocusServiceError(options, retryAfterTime)
|
|
108
|
+
.catch((error) => {
|
|
109
|
+
expect(error).to.equal(rejectionReason);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('retries the request after the specified time', () => {
|
|
114
|
+
let clock;
|
|
115
|
+
clock = sinon.useFakeTimers();
|
|
116
|
+
const mockResponse = 'mock response'
|
|
117
|
+
|
|
118
|
+
interceptor.webex.request = sinon.stub().returns(Promise.resolve(mockResponse));
|
|
119
|
+
const promise = interceptor.handleRetryRequestLocusServiceError(options, retryAfterTime);
|
|
120
|
+
|
|
121
|
+
clock.tick(retryAfterTime);
|
|
122
|
+
|
|
123
|
+
return promise.then(() => {
|
|
124
|
+
expect(interceptor.webex.request.calledOnce).to.be.true;
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
@@ -635,7 +635,14 @@ describe('plugin-meetings', () => {
|
|
|
635
635
|
describe('rejection', () => {
|
|
636
636
|
it('should error out and return a promise', async () => {
|
|
637
637
|
meeting.join = sinon.stub().returns(Promise.reject());
|
|
638
|
-
assert.isRejected(meeting.joinWithMedia({}));
|
|
638
|
+
assert.isRejected(meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: true}}));
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('should fail if called with allowMediaInLobby:false', async () => {
|
|
642
|
+
meeting.join = sinon.stub().returns(Promise.resolve(test1));
|
|
643
|
+
meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
|
|
644
|
+
|
|
645
|
+
assert.isRejected(meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: false}}));
|
|
639
646
|
});
|
|
640
647
|
});
|
|
641
648
|
});
|
|
@@ -559,43 +559,46 @@ describe('plugin-meetings', () => {
|
|
|
559
559
|
});
|
|
560
560
|
});
|
|
561
561
|
});
|
|
562
|
-
describe('
|
|
563
|
-
let initialSetup;
|
|
564
|
-
let parse;
|
|
562
|
+
describe('destroy non active locus meetings', () => {
|
|
565
563
|
let destroySpy;
|
|
566
564
|
|
|
565
|
+
const meetingCollectionMeetings = {
|
|
566
|
+
stillValidLocusMeeting: {
|
|
567
|
+
locusUrl: 'still-valid-locus-url',
|
|
568
|
+
sendCallAnalyzerMetrics: sinon.stub(),
|
|
569
|
+
},
|
|
570
|
+
noLongerValidLocusMeeting: {
|
|
571
|
+
locusUrl: 'no-longer-valid-locus-url',
|
|
572
|
+
sendCallAnalyzerMetrics: sinon.stub(),
|
|
573
|
+
},
|
|
574
|
+
otherNonLocusMeeting1: {
|
|
575
|
+
locusUrl: null,
|
|
576
|
+
sendCallAnalyzerMetrics: sinon.stub(),
|
|
577
|
+
},
|
|
578
|
+
otherNonLocusMeeting2: {
|
|
579
|
+
locusUrl: undefined,
|
|
580
|
+
sendCallAnalyzerMetrics: sinon.stub(),
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
|
|
567
584
|
beforeEach(() => {
|
|
568
585
|
destroySpy = sinon.spy(webex.meetings, 'destroy');
|
|
569
|
-
|
|
570
|
-
webex.meetings.meetingCollection.getByKey = sinon.stub().returns({
|
|
571
|
-
locusInfo,
|
|
572
|
-
sendCallAnalyzerMetrics: sinon.stub(),
|
|
573
|
-
});
|
|
574
|
-
webex.meetings.meetingCollection.getAll = sinon.stub().returns({
|
|
575
|
-
meetingutk: {
|
|
576
|
-
locusUrl: 'fdfdjfdhj',
|
|
577
|
-
sendCallAnalyzerMetrics: sinon.stub(),
|
|
578
|
-
},
|
|
579
|
-
});
|
|
580
|
-
webex.meetings.create = sinon.stub().returns(
|
|
581
|
-
Promise.resolve({
|
|
582
|
-
locusInfo: {
|
|
583
|
-
initialSetup,
|
|
584
|
-
},
|
|
585
|
-
sendCallAnalyzerMetrics: sinon.stub(),
|
|
586
|
-
})
|
|
587
|
-
);
|
|
586
|
+
webex.meetings.meetingCollection.getAll = sinon.stub().returns(meetingCollectionMeetings);
|
|
588
587
|
webex.meetings.request.getActiveMeetings = sinon.stub().returns(
|
|
589
588
|
Promise.resolve({
|
|
590
|
-
loci: [
|
|
589
|
+
loci: [
|
|
590
|
+
{url: 'still-valid-locus-url'}
|
|
591
|
+
],
|
|
591
592
|
})
|
|
592
593
|
);
|
|
593
594
|
MeetingUtil.cleanUp = sinon.stub().returns(Promise.resolve());
|
|
594
595
|
});
|
|
595
|
-
it('destroy non active meetings', async () => {
|
|
596
|
+
it('destroy only non active locus meetings and keep active locus meetings and any other non-locus meeting', async () => {
|
|
596
597
|
await webex.meetings.syncMeetings();
|
|
597
598
|
assert.calledOnce(webex.meetings.request.getActiveMeetings);
|
|
598
|
-
assert.calledOnce(
|
|
599
|
+
assert.calledOnce(webex.meetings.meetingCollection.getAll);
|
|
600
|
+
assert.calledWith(destroySpy, meetingCollectionMeetings.noLongerValidLocusMeeting);
|
|
601
|
+
assert.callCount(destroySpy, 1);
|
|
599
602
|
|
|
600
603
|
assert.calledOnce(MeetingUtil.cleanUp);
|
|
601
604
|
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import {assert} from '@webex/test-helper-chai';
|
|
2
|
+
import MockWebex from '@webex/test-helper-mock-webex';
|
|
3
|
+
import sinon from 'sinon';
|
|
4
|
+
import testUtils from '../../../utils/testUtils';
|
|
5
|
+
|
|
6
|
+
// packages/@webex/plugin-meetings/test/unit/spec/reachability/clusterReachability.ts
|
|
7
|
+
import { ClusterReachability } from '@webex/plugin-meetings/src/reachability/clusterReachability'; // replace with actual path
|
|
8
|
+
|
|
9
|
+
describe('ClusterReachability', () => {
|
|
10
|
+
let previousRTCPeerConnection;
|
|
11
|
+
let clusterReachability;
|
|
12
|
+
let fakePeerConnection;
|
|
13
|
+
|
|
14
|
+
const FAKE_OFFER = {type: 'offer', sdp: 'fake sdp'};
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
fakePeerConnection = {
|
|
18
|
+
createOffer: sinon.stub().resolves(FAKE_OFFER),
|
|
19
|
+
setLocalDescription: sinon.stub().resolves(),
|
|
20
|
+
close: sinon.stub(),
|
|
21
|
+
iceGatheringState: 'new',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
previousRTCPeerConnection = global.RTCPeerConnection;
|
|
25
|
+
global.RTCPeerConnection = sinon.stub().returns(fakePeerConnection);
|
|
26
|
+
|
|
27
|
+
clusterReachability = new ClusterReachability('testName', {
|
|
28
|
+
isVideoMesh: false,
|
|
29
|
+
udp: ['stun:udp1', 'stun:udp2'],
|
|
30
|
+
tcp: ['stun:tcp1.webex.com', 'stun:tcp2.webex.com:5004'],
|
|
31
|
+
xtls: ['xtls1', 'xtls2'],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
global.RTCPeerConnection = previousRTCPeerConnection;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should create an instance correctly', () => {
|
|
41
|
+
assert.instanceOf(clusterReachability, ClusterReachability);
|
|
42
|
+
assert.equal(clusterReachability.name, 'testName');
|
|
43
|
+
assert.equal(clusterReachability.isVideoMesh, false);
|
|
44
|
+
assert.equal(clusterReachability.numUdpUrls, 2);
|
|
45
|
+
assert.equal(clusterReachability.numTcpUrls, 2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should create a peer connection with the right config', () => {
|
|
49
|
+
assert.calledOnceWithExactly(global.RTCPeerConnection, {
|
|
50
|
+
iceServers: [
|
|
51
|
+
{username: '', credential: '', urls: ['stun:udp1']},
|
|
52
|
+
{username: '', credential: '', urls: ['stun:udp2']},
|
|
53
|
+
{username: 'webexturnreachuser', credential: 'webexturnreachpwd', urls: ['turn:tcp1.webex.com?transport=tcp']},
|
|
54
|
+
{username: 'webexturnreachuser', credential: 'webexturnreachpwd', urls: ['turn:tcp2.webex.com:5004?transport=tcp']}
|
|
55
|
+
],
|
|
56
|
+
iceCandidatePoolSize: 0,
|
|
57
|
+
iceTransportPolicy: 'all',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should create a peer connection with the right config even if lists of urls are empty', () => {
|
|
62
|
+
(global.RTCPeerConnection as any).resetHistory();
|
|
63
|
+
|
|
64
|
+
clusterReachability = new ClusterReachability('testName', {
|
|
65
|
+
isVideoMesh: false,
|
|
66
|
+
udp: [],
|
|
67
|
+
tcp: [],
|
|
68
|
+
xtls: [],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert.calledOnceWithExactly(global.RTCPeerConnection, {
|
|
72
|
+
iceServers: [],
|
|
73
|
+
iceCandidatePoolSize: 0,
|
|
74
|
+
iceTransportPolicy: 'all',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns correct results before start() is called', () => {
|
|
79
|
+
assert.deepEqual(clusterReachability.getResult(), {
|
|
80
|
+
udp: {result: 'untested'},
|
|
81
|
+
tcp: {result: 'untested'},
|
|
82
|
+
xtls: {result: 'untested'}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('#start', () => {
|
|
87
|
+
let clock;
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
clock = sinon.useFakeTimers();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
clock.restore();
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should initiate the ICE gathering process', async () => {
|
|
98
|
+
const promise = clusterReachability.start();
|
|
99
|
+
|
|
100
|
+
await testUtils.flushPromises();
|
|
101
|
+
|
|
102
|
+
// check that the right listeners are setup
|
|
103
|
+
assert.isFunction(fakePeerConnection.onicecandidate);
|
|
104
|
+
assert.isFunction(fakePeerConnection.onicegatheringstatechange);
|
|
105
|
+
|
|
106
|
+
// check that the right webrtc APIs are called
|
|
107
|
+
assert.calledOnceWithExactly(fakePeerConnection.createOffer, {offerToReceiveAudio: true});
|
|
108
|
+
assert.calledOnce(fakePeerConnection.setLocalDescription);
|
|
109
|
+
|
|
110
|
+
await clock.tickAsync(3000);// move the clock so that reachability times out
|
|
111
|
+
await promise;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('resolves and has correct result as soon as it finds that both udp and tcp is reachable', async () => {
|
|
115
|
+
const promise = clusterReachability.start();
|
|
116
|
+
|
|
117
|
+
await clock.tickAsync(100);
|
|
118
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp'}});
|
|
119
|
+
|
|
120
|
+
await clock.tickAsync(100);
|
|
121
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
|
|
122
|
+
|
|
123
|
+
await promise;
|
|
124
|
+
|
|
125
|
+
assert.deepEqual(clusterReachability.getResult(), {
|
|
126
|
+
udp: {result: 'reachable', latencyInMilliseconds: 100, clientMediaIPs: ['somePublicIp']},
|
|
127
|
+
tcp: {result: 'reachable', latencyInMilliseconds: 200},
|
|
128
|
+
xtls: {result: 'untested'}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('times out correctly', async () => {
|
|
133
|
+
const promise = clusterReachability.start();
|
|
134
|
+
|
|
135
|
+
// progress time without any candidates
|
|
136
|
+
await clock.tickAsync(3000);
|
|
137
|
+
await promise;
|
|
138
|
+
|
|
139
|
+
assert.deepEqual(clusterReachability.getResult(), {
|
|
140
|
+
udp: {result: 'unreachable'},
|
|
141
|
+
tcp: {result: 'unreachable'},
|
|
142
|
+
xtls: {result: 'untested'}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('times out correctly for video mesh nodes', async () => {
|
|
147
|
+
clusterReachability = new ClusterReachability('testName', {
|
|
148
|
+
isVideoMesh: true,
|
|
149
|
+
udp: ['stun:udp1', 'stun:udp2'],
|
|
150
|
+
tcp: ['stun:tcp1.webex.com', 'stun:tcp2.webex.com:5004'],
|
|
151
|
+
xtls: ['xtls1', 'xtls2'],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const promise = clusterReachability.start();
|
|
155
|
+
|
|
156
|
+
// video mesh nodes have shorter timeout of just 1s
|
|
157
|
+
await clock.tickAsync(1000);
|
|
158
|
+
await promise;
|
|
159
|
+
|
|
160
|
+
assert.deepEqual(clusterReachability.getResult(), {
|
|
161
|
+
udp: {result: 'unreachable'},
|
|
162
|
+
tcp: {result: 'unreachable'},
|
|
163
|
+
xtls: {result: 'untested'}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('resolves when ICE gathering is completed', async () => {
|
|
168
|
+
const promise = clusterReachability.start();
|
|
169
|
+
|
|
170
|
+
await testUtils.flushPromises();
|
|
171
|
+
|
|
172
|
+
fakePeerConnection.iceConnectionState = 'complete';
|
|
173
|
+
fakePeerConnection.onicegatheringstatechange();
|
|
174
|
+
await promise;
|
|
175
|
+
|
|
176
|
+
assert.deepEqual(clusterReachability.getResult(), {
|
|
177
|
+
udp: {result: 'unreachable'},
|
|
178
|
+
tcp: {result: 'unreachable'},
|
|
179
|
+
xtls: {result: 'untested'}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('resolves with the right result when ICE gathering is completed', async () => {
|
|
184
|
+
const promise = clusterReachability.start();
|
|
185
|
+
|
|
186
|
+
// send 1 candidate
|
|
187
|
+
await clock.tickAsync(30);
|
|
188
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1'}});
|
|
189
|
+
|
|
190
|
+
fakePeerConnection.iceConnectionState = 'complete';
|
|
191
|
+
fakePeerConnection.onicegatheringstatechange();
|
|
192
|
+
await promise;
|
|
193
|
+
|
|
194
|
+
assert.deepEqual(clusterReachability.getResult(), {
|
|
195
|
+
udp: {result: 'reachable', latencyInMilliseconds: 30, clientMediaIPs: ['somePublicIp1']},
|
|
196
|
+
tcp: {result: 'unreachable'},
|
|
197
|
+
xtls: {result: 'untested'}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should store latency only for the first srflx candidate, but IPs from all of them', async () => {
|
|
202
|
+
const promise = clusterReachability.start();
|
|
203
|
+
|
|
204
|
+
await clock.tickAsync(10);
|
|
205
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1'}});
|
|
206
|
+
|
|
207
|
+
// generate more candidates
|
|
208
|
+
await clock.tickAsync(10);
|
|
209
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp2'}});
|
|
210
|
+
|
|
211
|
+
await clock.tickAsync(10);
|
|
212
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp3'}});
|
|
213
|
+
|
|
214
|
+
await clock.tickAsync(3000);// move the clock so that reachability times out
|
|
215
|
+
|
|
216
|
+
await promise;
|
|
217
|
+
|
|
218
|
+
// latency should be from only the first candidates, but the clientMediaIps should be from all UDP candidates (not TCP)
|
|
219
|
+
assert.deepEqual(clusterReachability.getResult(), {
|
|
220
|
+
udp: {result: 'reachable', latencyInMilliseconds: 10, clientMediaIPs: ['somePublicIp1', 'somePublicIp2', 'somePublicIp3']},
|
|
221
|
+
tcp: {result: 'unreachable'},
|
|
222
|
+
xtls: {result: 'untested'}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should store latency only for the first relay candidate', async () => {
|
|
227
|
+
const promise = clusterReachability.start();
|
|
228
|
+
|
|
229
|
+
await clock.tickAsync(10);
|
|
230
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp1'}});
|
|
231
|
+
|
|
232
|
+
// generate more candidates
|
|
233
|
+
await clock.tickAsync(10);
|
|
234
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp2'}});
|
|
235
|
+
|
|
236
|
+
await clock.tickAsync(10);
|
|
237
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp3'}});
|
|
238
|
+
|
|
239
|
+
await clock.tickAsync(3000);// move the clock so that reachability times out
|
|
240
|
+
|
|
241
|
+
await promise;
|
|
242
|
+
|
|
243
|
+
// latency should be from only the first candidates, but the clientMediaIps should be from only from UDP candidates
|
|
244
|
+
assert.deepEqual(clusterReachability.getResult(), {
|
|
245
|
+
udp: {result: 'unreachable'},
|
|
246
|
+
tcp: {result: 'reachable', latencyInMilliseconds: 10},
|
|
247
|
+
xtls: {result: 'untested'}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('ignores duplicate clientMediaIps', async () => {
|
|
252
|
+
const promise = clusterReachability.start();
|
|
253
|
+
|
|
254
|
+
// generate candidates with duplicate addresses
|
|
255
|
+
await clock.tickAsync(10);
|
|
256
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1'}});
|
|
257
|
+
|
|
258
|
+
await clock.tickAsync(10);
|
|
259
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1'}});
|
|
260
|
+
|
|
261
|
+
await clock.tickAsync(10);
|
|
262
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp2'}});
|
|
263
|
+
|
|
264
|
+
await clock.tickAsync(10);
|
|
265
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp2'}});
|
|
266
|
+
|
|
267
|
+
// send also a relay candidate so that the reachability check finishes
|
|
268
|
+
fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
|
|
269
|
+
|
|
270
|
+
await promise;
|
|
271
|
+
|
|
272
|
+
assert.deepEqual(clusterReachability.getResult(), {
|
|
273
|
+
udp: {result: 'reachable', latencyInMilliseconds: 10, clientMediaIPs: ['somePublicIp1', 'somePublicIp2']},
|
|
274
|
+
tcp: {result: 'reachable', latencyInMilliseconds: 40},
|
|
275
|
+
xtls: {result: 'untested'}
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|