@webex/plugin-meetings 3.0.0-next.23 → 3.0.0-next.25

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.
@@ -2,7 +2,7 @@ import {Defer} from '@webex/common';
2
2
 
3
3
  import LoggerProxy from '../common/logs/logger-proxy';
4
4
  import {ClusterNode} from './request';
5
- import {convertStunUrlToTurn} from './util';
5
+ import {convertStunUrlToTurn, convertStunUrlToTurnTls} from './util';
6
6
 
7
7
  import {ICE_GATHERING_STATE, CONNECTION_STATE} from '../constants';
8
8
 
@@ -29,6 +29,7 @@ export type ClusterReachabilityResult = {
29
29
  export class ClusterReachability {
30
30
  private numUdpUrls: number;
31
31
  private numTcpUrls: number;
32
+ private numXTlsUrls: number;
32
33
  private result: ClusterReachabilityResult;
33
34
  private pc?: RTCPeerConnection;
34
35
  private defer: Defer; // this defer is resolved once reachability checks for this cluster are completed
@@ -46,6 +47,7 @@ export class ClusterReachability {
46
47
  this.isVideoMesh = clusterInfo.isVideoMesh;
47
48
  this.numUdpUrls = clusterInfo.udp.length;
48
49
  this.numTcpUrls = clusterInfo.tcp.length;
50
+ this.numXTlsUrls = clusterInfo.xtls.length;
49
51
 
50
52
  this.pc = this.createPeerConnection(clusterInfo);
51
53
 
@@ -94,8 +96,16 @@ export class ClusterReachability {
94
96
  };
95
97
  });
96
98
 
99
+ const turnTlsIceServers = cluster.xtls.map((urlString: string) => {
100
+ return {
101
+ username: 'webexturnreachuser',
102
+ credential: 'webexturnreachpwd',
103
+ urls: [convertStunUrlToTurnTls(urlString)],
104
+ };
105
+ });
106
+
97
107
  return {
98
- iceServers: [...udpIceServers, ...tcpIceServers],
108
+ iceServers: [...udpIceServers, ...tcpIceServers, ...turnTlsIceServers],
99
109
  iceCandidatePoolSize: 0,
100
110
  iceTransportPolicy: 'all',
101
111
  };
@@ -194,7 +204,7 @@ export class ClusterReachability {
194
204
  * @returns {boolean} true if we have all results, false otherwise
195
205
  */
196
206
  private haveWeGotAllResults(): boolean {
197
- return ['udp', 'tcp'].every(
207
+ return ['udp', 'tcp', 'xtls'].every(
198
208
  (protocol) =>
199
209
  this.result[protocol].result === 'reachable' || this.result[protocol].result === 'untested'
200
210
  );
@@ -207,7 +217,7 @@ export class ClusterReachability {
207
217
  * @param {number} latency
208
218
  * @returns {void}
209
219
  */
210
- private storeLatencyResult(protocol: 'udp' | 'tcp', latency: number) {
220
+ private storeLatencyResult(protocol: 'udp' | 'tcp' | 'xtls', latency: number) {
211
221
  const result = this.result[protocol];
212
222
 
213
223
  if (result.latencyInMilliseconds === undefined) {
@@ -227,6 +237,7 @@ export class ClusterReachability {
227
237
  */
228
238
  private registerIceCandidateListener() {
229
239
  this.pc.onicecandidate = (e) => {
240
+ const TURN_TLS_PORT = 443;
230
241
  const CANDIDATE_TYPES = {
231
242
  SERVER_REFLEXIVE: 'srflx',
232
243
  RELAY: 'relay',
@@ -239,7 +250,8 @@ export class ClusterReachability {
239
250
  }
240
251
 
241
252
  if (e.candidate.type === CANDIDATE_TYPES.RELAY) {
242
- this.storeLatencyResult('tcp', this.getElapsedTime());
253
+ const protocol = e.candidate.port === TURN_TLS_PORT ? 'xtls' : 'tcp';
254
+ this.storeLatencyResult(protocol, this.getElapsedTime());
243
255
  // we don't add public IP for TCP, because in the case of relay candidates
244
256
  // e.candidate.address is the TURN server address, not the client's public IP
245
257
  }
@@ -275,6 +287,9 @@ export class ClusterReachability {
275
287
  this.result.tcp = {
276
288
  result: this.numTcpUrls > 0 ? 'unreachable' : 'untested',
277
289
  };
290
+ this.result.xtls = {
291
+ result: this.numXTlsUrls > 0 ? 'unreachable' : 'untested',
292
+ };
278
293
 
279
294
  try {
280
295
  const offer = await this.pc.createOffer({offerToReceiveAudio: true});
@@ -22,10 +22,14 @@ export type ReachabilityMetrics = {
22
22
  reachability_public_udp_failed: number;
23
23
  reachability_public_tcp_success: number;
24
24
  reachability_public_tcp_failed: number;
25
+ reachability_public_xtls_success: number;
26
+ reachability_public_xtls_failed: number;
25
27
  reachability_vmn_udp_success: number;
26
28
  reachability_vmn_udp_failed: number;
27
29
  reachability_vmn_tcp_success: number;
28
30
  reachability_vmn_tcp_failed: number;
31
+ reachability_vmn_xtls_success: number;
32
+ reachability_vmn_xtls_failed: number;
29
33
  };
30
34
 
31
35
  /**
@@ -141,10 +145,14 @@ export default class Reachability {
141
145
  reachability_public_udp_failed: 0,
142
146
  reachability_public_tcp_success: 0,
143
147
  reachability_public_tcp_failed: 0,
148
+ reachability_public_xtls_success: 0,
149
+ reachability_public_xtls_failed: 0,
144
150
  reachability_vmn_udp_success: 0,
145
151
  reachability_vmn_udp_failed: 0,
146
152
  reachability_vmn_tcp_success: 0,
147
153
  reachability_vmn_tcp_failed: 0,
154
+ reachability_vmn_xtls_success: 0,
155
+ reachability_vmn_xtls_failed: 0,
148
156
  };
149
157
 
150
158
  const updateStats = (clusterType: 'public' | 'vmn', result: ClusterReachabilityResult) => {
@@ -156,6 +164,10 @@ export default class Reachability {
156
164
  const outcome = result.tcp.result === 'reachable' ? 'success' : 'failed';
157
165
  stats[`reachability_${clusterType}_tcp_${outcome}`] += 1;
158
166
  }
167
+ if (result.xtls && result.xtls.result !== 'untested') {
168
+ const outcome = result.xtls.result === 'reachable' ? 'success' : 'failed';
169
+ stats[`reachability_${clusterType}_xtls_${outcome}`] += 1;
170
+ }
159
171
  };
160
172
 
161
173
  try {
@@ -338,7 +350,10 @@ export default class Reachability {
338
350
  LoggerProxy.logger.log(
339
351
  `Reachability:index#performReachabilityChecks --> doing UDP${
340
352
  // @ts-ignore
341
- this.webex.config.meetings.experimental.enableTcpReachability ? ' and TCP' : ''
353
+ this.webex.config.meetings.experimental.enableTcpReachability ? ',TCP' : ''
354
+ }${
355
+ // @ts-ignore
356
+ this.webex.config.meetings.experimental.enableTlsReachability ? ',TLS' : ''
342
357
  } reachability checks`
343
358
  );
344
359
 
@@ -354,6 +369,14 @@ export default class Reachability {
354
369
  cluster.tcp = [];
355
370
  }
356
371
 
372
+ const includeTlsReachability =
373
+ // @ts-ignore
374
+ this.webex.config.meetings.experimental.enableTlsReachability && !cluster.isVideoMesh;
375
+
376
+ if (!includeTlsReachability) {
377
+ cluster.xtls = [];
378
+ }
379
+
357
380
  this.clusterReachability[key] = new ClusterReachability(key, cluster);
358
381
 
359
382
  return this.clusterReachability[key].start().then((result) => {
@@ -22,3 +22,24 @@ export function convertStunUrlToTurn(stunUrl: string, protocol: 'udp' | 'tcp') {
22
22
 
23
23
  return url.toString();
24
24
  }
25
+
26
+ /**
27
+ * Converts a stun url to a turns url
28
+ *
29
+ * @param {string} stunUrl url of a stun server
30
+ * @returns {string} url of a turns server
31
+ */
32
+ export function convertStunUrlToTurnTls(stunUrl: string) {
33
+ // stunUrl looks like this: "stun:external-media1.public.wjfkm-a-15.prod.infra.webex.com:443"
34
+ // and we need it to be like this: "turns:external-media1.public.wjfkm-a-15.prod.infra.webex.com:443?transport=tcp"
35
+ const url = new URL(stunUrl);
36
+
37
+ if (url.protocol !== 'stun:') {
38
+ throw new Error(`Not a STUN URL: ${stunUrl}`);
39
+ }
40
+
41
+ url.protocol = 'turns:';
42
+ url.searchParams.append('transport', 'tcp');
43
+
44
+ return url.toString();
45
+ }
@@ -1011,15 +1011,14 @@ describe('plugin-meetings', () => {
1011
1011
  });
1012
1012
 
1013
1013
  describe('transcription events', () => {
1014
+ beforeEach(() => {
1015
+ meeting.trigger = sinon.stub();
1016
+ });
1017
+
1014
1018
  it('should trigger meeting:caption-received event', () => {
1015
1019
  meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({});
1016
1020
  assert.calledWith(
1017
- TriggerProxy.trigger,
1018
- sinon.match.instanceOf(Meeting),
1019
- {
1020
- file: 'meeting/index',
1021
- function: 'setUpVoiceaListeners',
1022
- },
1021
+ meeting.trigger,
1023
1022
  EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED
1024
1023
  );
1025
1024
  });
@@ -1027,12 +1026,7 @@ describe('plugin-meetings', () => {
1027
1026
  it('should trigger meeting:receiveTranscription:started event', () => {
1028
1027
  meeting.voiceaListenerCallbacks[VOICEAEVENTS.VOICEA_ANNOUNCEMENT]({});
1029
1028
  assert.calledWith(
1030
- TriggerProxy.trigger,
1031
- sinon.match.instanceOf(Meeting),
1032
- {
1033
- file: 'meeting/index',
1034
- function: 'setUpVoiceaListeners',
1035
- },
1029
+ meeting.trigger,
1036
1030
  EVENT_TRIGGERS.MEETING_STARTED_RECEIVING_TRANSCRIPTION
1037
1031
  );
1038
1032
  });
@@ -1040,12 +1034,7 @@ describe('plugin-meetings', () => {
1040
1034
  it('should trigger meeting:caption-received event', () => {
1041
1035
  meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({});
1042
1036
  assert.calledWith(
1043
- TriggerProxy.trigger,
1044
- sinon.match.instanceOf(Meeting),
1045
- {
1046
- file: 'meeting/index',
1047
- function: 'setUpVoiceaListeners',
1048
- },
1037
+ meeting.trigger,
1049
1038
  EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED
1050
1039
  );
1051
1040
  });
@@ -253,6 +253,19 @@ describe('plugin-meetings', () => {
253
253
  });
254
254
  });
255
255
 
256
+ describe('#_toggleTlsReachability', () => {
257
+ it('should have _toggleTlsReachability', () => {
258
+ assert.equal(typeof webex.meetings._toggleTlsReachability, 'function');
259
+ });
260
+
261
+ describe('success', () => {
262
+ it('should update meetings to do TLS reachability', () => {
263
+ webex.meetings._toggleTlsReachability(true);
264
+ assert.equal(webex.meetings.config.experimental.enableTlsReachability, true);
265
+ });
266
+ });
267
+ });
268
+
256
269
  describe('Public API Contracts', () => {
257
270
  describe('#register', () => {
258
271
  it('emits an event and resolves when register succeeds', async () => {
@@ -4,7 +4,7 @@ import sinon from 'sinon';
4
4
  import testUtils from '../../../utils/testUtils';
5
5
 
6
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
7
+ import {ClusterReachability} from '@webex/plugin-meetings/src/reachability/clusterReachability'; // replace with actual path
8
8
 
9
9
  describe('ClusterReachability', () => {
10
10
  let previousRTCPeerConnection;
@@ -28,9 +28,8 @@ describe('ClusterReachability', () => {
28
28
  isVideoMesh: false,
29
29
  udp: ['stun:udp1', 'stun:udp2'],
30
30
  tcp: ['stun:tcp1.webex.com', 'stun:tcp2.webex.com:5004'],
31
- xtls: ['xtls1', 'xtls2'],
31
+ xtls: ['stun:xtls1.webex.com', 'stun:xtls2.webex.com:443'],
32
32
  });
33
-
34
33
  });
35
34
 
36
35
  afterEach(() => {
@@ -50,8 +49,26 @@ describe('ClusterReachability', () => {
50
49
  iceServers: [
51
50
  {username: '', credential: '', urls: ['stun:udp1']},
52
51
  {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']}
52
+ {
53
+ username: 'webexturnreachuser',
54
+ credential: 'webexturnreachpwd',
55
+ urls: ['turn:tcp1.webex.com?transport=tcp'],
56
+ },
57
+ {
58
+ username: 'webexturnreachuser',
59
+ credential: 'webexturnreachpwd',
60
+ urls: ['turn:tcp2.webex.com:5004?transport=tcp'],
61
+ },
62
+ {
63
+ username: 'webexturnreachuser',
64
+ credential: 'webexturnreachpwd',
65
+ urls: ['turns:xtls1.webex.com?transport=tcp'],
66
+ },
67
+ {
68
+ username: 'webexturnreachuser',
69
+ credential: 'webexturnreachpwd',
70
+ urls: ['turns:xtls2.webex.com:443?transport=tcp'],
71
+ },
55
72
  ],
56
73
  iceCandidatePoolSize: 0,
57
74
  iceTransportPolicy: 'all',
@@ -79,7 +96,7 @@ describe('ClusterReachability', () => {
79
96
  assert.deepEqual(clusterReachability.getResult(), {
80
97
  udp: {result: 'untested'},
81
98
  tcp: {result: 'untested'},
82
- xtls: {result: 'untested'}
99
+ xtls: {result: 'untested'},
83
100
  });
84
101
  });
85
102
 
@@ -92,7 +109,7 @@ describe('ClusterReachability', () => {
92
109
 
93
110
  afterEach(() => {
94
111
  clock.restore();
95
- })
112
+ });
96
113
 
97
114
  it('should initiate the ICE gathering process', async () => {
98
115
  const promise = clusterReachability.start();
@@ -107,11 +124,11 @@ describe('ClusterReachability', () => {
107
124
  assert.calledOnceWithExactly(fakePeerConnection.createOffer, {offerToReceiveAudio: true});
108
125
  assert.calledOnce(fakePeerConnection.setLocalDescription);
109
126
 
110
- await clock.tickAsync(3000);// move the clock so that reachability times out
127
+ await clock.tickAsync(3000); // move the clock so that reachability times out
111
128
  await promise;
112
129
  });
113
130
 
114
- it('resolves and has correct result as soon as it finds that both udp and tcp is reachable', async () => {
131
+ it('resolves and has correct result as soon as it finds that all udp, tcp and tls are reachable', async () => {
115
132
  const promise = clusterReachability.start();
116
133
 
117
134
  await clock.tickAsync(100);
@@ -120,12 +137,17 @@ describe('ClusterReachability', () => {
120
137
  await clock.tickAsync(100);
121
138
  fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
122
139
 
140
+ await clock.tickAsync(100);
141
+ fakePeerConnection.onicecandidate({
142
+ candidate: {type: 'relay', address: 'someTurnRelayIp', port: 443},
143
+ });
144
+
123
145
  await promise;
124
146
 
125
147
  assert.deepEqual(clusterReachability.getResult(), {
126
148
  udp: {result: 'reachable', latencyInMilliseconds: 100, clientMediaIPs: ['somePublicIp']},
127
149
  tcp: {result: 'reachable', latencyInMilliseconds: 200},
128
- xtls: {result: 'untested'}
150
+ xtls: {result: 'reachable', latencyInMilliseconds: 300},
129
151
  });
130
152
  });
131
153
 
@@ -139,7 +161,7 @@ describe('ClusterReachability', () => {
139
161
  assert.deepEqual(clusterReachability.getResult(), {
140
162
  udp: {result: 'unreachable'},
141
163
  tcp: {result: 'unreachable'},
142
- xtls: {result: 'untested'}
164
+ xtls: {result: 'unreachable'},
143
165
  });
144
166
  });
145
167
 
@@ -148,7 +170,7 @@ describe('ClusterReachability', () => {
148
170
  isVideoMesh: true,
149
171
  udp: ['stun:udp1', 'stun:udp2'],
150
172
  tcp: ['stun:tcp1.webex.com', 'stun:tcp2.webex.com:5004'],
151
- xtls: ['xtls1', 'xtls2'],
173
+ xtls: ['stun:xtls1.webex.com', 'stun:xtls1.webex.com:443'],
152
174
  });
153
175
 
154
176
  const promise = clusterReachability.start();
@@ -160,7 +182,7 @@ describe('ClusterReachability', () => {
160
182
  assert.deepEqual(clusterReachability.getResult(), {
161
183
  udp: {result: 'unreachable'},
162
184
  tcp: {result: 'unreachable'},
163
- xtls: {result: 'untested'}
185
+ xtls: {result: 'unreachable'},
164
186
  });
165
187
  });
166
188
 
@@ -176,7 +198,7 @@ describe('ClusterReachability', () => {
176
198
  assert.deepEqual(clusterReachability.getResult(), {
177
199
  udp: {result: 'unreachable'},
178
200
  tcp: {result: 'unreachable'},
179
- xtls: {result: 'untested'}
201
+ xtls: {result: 'unreachable'},
180
202
  });
181
203
  });
182
204
 
@@ -194,7 +216,7 @@ describe('ClusterReachability', () => {
194
216
  assert.deepEqual(clusterReachability.getResult(), {
195
217
  udp: {result: 'reachable', latencyInMilliseconds: 30, clientMediaIPs: ['somePublicIp1']},
196
218
  tcp: {result: 'unreachable'},
197
- xtls: {result: 'untested'}
219
+ xtls: {result: 'unreachable'},
198
220
  });
199
221
  });
200
222
 
@@ -211,15 +233,19 @@ describe('ClusterReachability', () => {
211
233
  await clock.tickAsync(10);
212
234
  fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp3'}});
213
235
 
214
- await clock.tickAsync(3000);// move the clock so that reachability times out
236
+ await clock.tickAsync(3000); // move the clock so that reachability times out
215
237
 
216
238
  await promise;
217
239
 
218
240
  // latency should be from only the first candidates, but the clientMediaIps should be from all UDP candidates (not TCP)
219
241
  assert.deepEqual(clusterReachability.getResult(), {
220
- udp: {result: 'reachable', latencyInMilliseconds: 10, clientMediaIPs: ['somePublicIp1', 'somePublicIp2', 'somePublicIp3']},
242
+ udp: {
243
+ result: 'reachable',
244
+ latencyInMilliseconds: 10,
245
+ clientMediaIPs: ['somePublicIp1', 'somePublicIp2', 'somePublicIp3'],
246
+ },
221
247
  tcp: {result: 'unreachable'},
222
- xtls: {result: 'untested'}
248
+ xtls: {result: 'unreachable'},
223
249
  });
224
250
  });
225
251
 
@@ -236,7 +262,7 @@ describe('ClusterReachability', () => {
236
262
  await clock.tickAsync(10);
237
263
  fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp3'}});
238
264
 
239
- await clock.tickAsync(3000);// move the clock so that reachability times out
265
+ await clock.tickAsync(3000); // move the clock so that reachability times out
240
266
 
241
267
  await promise;
242
268
 
@@ -244,7 +270,38 @@ describe('ClusterReachability', () => {
244
270
  assert.deepEqual(clusterReachability.getResult(), {
245
271
  udp: {result: 'unreachable'},
246
272
  tcp: {result: 'reachable', latencyInMilliseconds: 10},
247
- xtls: {result: 'untested'}
273
+ xtls: {result: 'unreachable'},
274
+ });
275
+ });
276
+
277
+ it('should store latency only for the first tls relay candidate', async () => {
278
+ const promise = clusterReachability.start();
279
+
280
+ await clock.tickAsync(10);
281
+ fakePeerConnection.onicecandidate({
282
+ candidate: {type: 'relay', address: 'someTurnRelayIp1', port: 443},
283
+ });
284
+
285
+ // generate more candidates
286
+ await clock.tickAsync(10);
287
+ fakePeerConnection.onicecandidate({
288
+ candidate: {type: 'relay', address: 'someTurnRelayIp2', port: 443},
289
+ });
290
+
291
+ await clock.tickAsync(10);
292
+ fakePeerConnection.onicecandidate({
293
+ candidate: {type: 'relay', address: 'someTurnRelayIp3', port: 443},
294
+ });
295
+
296
+ await clock.tickAsync(3000); // move the clock so that reachability times out
297
+
298
+ await promise;
299
+
300
+ // latency should be from only the first candidates, but the clientMediaIps should be from only from UDP candidates
301
+ assert.deepEqual(clusterReachability.getResult(), {
302
+ udp: {result: 'unreachable'},
303
+ tcp: {result: 'unreachable'},
304
+ xtls: {result: 'reachable', latencyInMilliseconds: 10},
248
305
  });
249
306
  });
250
307
 
@@ -266,13 +323,20 @@ describe('ClusterReachability', () => {
266
323
 
267
324
  // send also a relay candidate so that the reachability check finishes
268
325
  fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
326
+ fakePeerConnection.onicecandidate({
327
+ candidate: {type: 'relay', address: 'someTurnRelayIp', port: 443},
328
+ });
269
329
 
270
330
  await promise;
271
331
 
272
332
  assert.deepEqual(clusterReachability.getResult(), {
273
- udp: {result: 'reachable', latencyInMilliseconds: 10, clientMediaIPs: ['somePublicIp1', 'somePublicIp2']},
333
+ udp: {
334
+ result: 'reachable',
335
+ latencyInMilliseconds: 10,
336
+ clientMediaIPs: ['somePublicIp1', 'somePublicIp2'],
337
+ },
274
338
  tcp: {result: 'reachable', latencyInMilliseconds: 40},
275
- xtls: {result: 'untested'}
339
+ xtls: {result: 'reachable', latencyInMilliseconds: 40},
276
340
  });
277
341
  });
278
342
  });