@webex/plugin-meetings 3.0.0-beta.337 → 3.0.0-beta.339
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.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/reachability/clusterReachability.js +356 -0
- package/dist/reachability/clusterReachability.js.map +1 -0
- package/dist/reachability/index.js +167 -451
- package/dist/reachability/index.js.map +1 -1
- package/dist/reachability/util.js +29 -0
- package/dist/reachability/util.js.map +1 -0
- package/dist/statsAnalyzer/mqaUtil.js +4 -0
- package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/meetings/index.d.ts +1 -11
- package/dist/types/reachability/clusterReachability.d.ts +109 -0
- package/dist/types/reachability/index.d.ts +32 -121
- package/dist/types/reachability/util.d.ts +8 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +20 -20
- package/src/config.ts +1 -0
- package/src/reachability/clusterReachability.ts +320 -0
- package/src/reachability/index.ts +124 -421
- package/src/reachability/util.ts +24 -0
- package/src/statsAnalyzer/mqaUtil.ts +4 -0
- 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 +47 -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
|
+
}
|
|
@@ -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,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
|
+
});
|