@webex/plugin-meetings 3.0.0-next.2 → 3.0.0-next.21
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/constants.d.ts +3 -9
- package/dist/constants.js +4 -9
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/interpretation/index.js +3 -3
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/mediaSharesUtils.js +15 -1
- package/dist/locus-info/mediaSharesUtils.js.map +1 -1
- package/dist/media/index.js +4 -1
- package/dist/media/index.js.map +1 -1
- package/dist/mediaQualityMetrics/config.d.ts +9 -1
- package/dist/mediaQualityMetrics/config.js +10 -1
- package/dist/mediaQualityMetrics/config.js.map +1 -1
- package/dist/meeting/index.d.ts +18 -7
- package/dist/meeting/index.js +745 -567
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/muteState.d.ts +2 -8
- package/dist/meeting/muteState.js +37 -25
- package/dist/meeting/muteState.js.map +1 -1
- package/dist/meeting/request.d.ts +3 -0
- package/dist/meeting/request.js +32 -23
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/util.js +1 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/multistream/mediaRequestManager.d.ts +1 -2
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/remoteMediaGroup.d.ts +1 -1
- package/dist/multistream/remoteMediaGroup.js.map +1 -1
- package/dist/multistream/remoteMediaManager.d.ts +1 -2
- package/dist/multistream/remoteMediaManager.js.map +1 -1
- package/dist/multistream/sendSlotManager.d.ts +1 -2
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/reachability/request.js +12 -10
- package/dist/reachability/request.js.map +1 -1
- package/dist/reconnection-manager/index.js +2 -1
- package/dist/reconnection-manager/index.js.map +1 -1
- package/dist/roap/index.d.ts +10 -2
- package/dist/roap/index.js +15 -0
- package/dist/roap/index.js.map +1 -1
- package/dist/roap/request.js +3 -3
- package/dist/roap/request.js.map +1 -1
- package/dist/roap/turnDiscovery.d.ts +64 -17
- package/dist/roap/turnDiscovery.js +307 -126
- package/dist/roap/turnDiscovery.js.map +1 -1
- package/dist/statsAnalyzer/index.js +61 -41
- package/dist/statsAnalyzer/index.js.map +1 -1
- package/dist/webinar/index.js +1 -1
- package/package.json +22 -22
- package/src/constants.ts +3 -9
- package/src/index.ts +1 -0
- package/src/interpretation/index.ts +2 -2
- package/src/locus-info/mediaSharesUtils.ts +16 -0
- package/src/media/index.ts +3 -1
- package/src/mediaQualityMetrics/config.ts +11 -1
- package/src/meeting/index.ts +264 -90
- package/src/meeting/muteState.ts +34 -20
- package/src/meeting/request.ts +18 -2
- package/src/meeting/util.ts +1 -0
- package/src/multistream/mediaRequestManager.ts +1 -1
- package/src/multistream/remoteMediaGroup.ts +1 -1
- package/src/multistream/remoteMediaManager.ts +1 -2
- package/src/multistream/sendSlotManager.ts +1 -2
- package/src/reachability/request.ts +15 -11
- package/src/reconnection-manager/index.ts +1 -1
- package/src/roap/index.ts +25 -3
- package/src/roap/request.ts +3 -3
- package/src/roap/turnDiscovery.ts +244 -78
- package/src/statsAnalyzer/index.ts +72 -43
- package/test/integration/spec/journey.js +14 -14
- package/test/integration/spec/space-meeting.js +1 -1
- package/test/unit/spec/interpretation/index.ts +4 -1
- package/test/unit/spec/locus-info/mediaSharesUtils.ts +9 -0
- package/test/unit/spec/media/index.ts +89 -78
- package/test/unit/spec/meeting/index.js +611 -125
- package/test/unit/spec/meeting/muteState.js +219 -67
- package/test/unit/spec/meeting/request.js +21 -0
- package/test/unit/spec/meeting/utils.js +6 -1
- package/test/unit/spec/meetings/index.js +27 -20
- package/test/unit/spec/multistream/remoteMediaGroup.ts +0 -1
- package/test/unit/spec/multistream/remoteMediaManager.ts +0 -1
- package/test/unit/spec/reachability/request.js +15 -7
- package/test/unit/spec/reconnection-manager/index.js +28 -0
- package/test/unit/spec/roap/index.ts +61 -6
- package/test/unit/spec/roap/turnDiscovery.ts +298 -16
- package/test/unit/spec/stats-analyzer/index.js +183 -8
- package/dist/member/member.types.d.ts +0 -11
- package/dist/member/member.types.js +0 -17
- package/dist/member/member.types.js.map +0 -1
- package/src/member/member.types.ts +0 -13
|
@@ -4,7 +4,7 @@ import {Defer} from '@webex/common';
|
|
|
4
4
|
import Metrics from '../metrics';
|
|
5
5
|
import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
6
6
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
7
|
-
import {ROAP} from '../constants';
|
|
7
|
+
import {ROAP, Enum} from '../constants';
|
|
8
8
|
|
|
9
9
|
import RoapRequest from './request';
|
|
10
10
|
import Meeting from '../meeting';
|
|
@@ -18,6 +18,28 @@ const TURN_DISCOVERY_TIMEOUT = 10; // in seconds
|
|
|
18
18
|
// and do the SDP offer with seq=1
|
|
19
19
|
const TURN_DISCOVERY_SEQ = 0;
|
|
20
20
|
|
|
21
|
+
const TurnDiscoverySkipReason = {
|
|
22
|
+
missingHttpResponse: 'missing http response', // when we asked for the TURN discovery response to be in the http response, but it wasn't there
|
|
23
|
+
reachability: 'reachability', // when udp reachability to public clusters is ok, so we don't need TURN (this doens't apply when joinWithMedia() is used)
|
|
24
|
+
alreadyInProgress: 'already in progress', // when we try to start TURN discovery while it's already in progress
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
export type TurnDiscoverySkipReason =
|
|
28
|
+
| Enum<typeof TurnDiscoverySkipReason> // this is a kind of FYI, because in practice typescript will infer the type of TurnDiscoverySkipReason as a string
|
|
29
|
+
| string // used in case of errors, contains the error message
|
|
30
|
+
| undefined; // used when TURN discovery is not skipped
|
|
31
|
+
|
|
32
|
+
export type TurnServerInfo = {
|
|
33
|
+
url: string;
|
|
34
|
+
username: string;
|
|
35
|
+
password: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type TurnDiscoveryResult = {
|
|
39
|
+
turnServerInfo?: TurnServerInfo;
|
|
40
|
+
turnDiscoverySkippedReason: TurnDiscoverySkipReason;
|
|
41
|
+
};
|
|
42
|
+
|
|
21
43
|
/**
|
|
22
44
|
* Handles the process of finding out TURN server information from Linus.
|
|
23
45
|
* This is achieved by sending a TURN_DISCOVERY_REQUEST.
|
|
@@ -27,11 +49,7 @@ export default class TurnDiscovery {
|
|
|
27
49
|
|
|
28
50
|
private defer?: Defer; // used for waiting for the response
|
|
29
51
|
|
|
30
|
-
private turnInfo:
|
|
31
|
-
url: string;
|
|
32
|
-
username: string;
|
|
33
|
-
password: string;
|
|
34
|
-
};
|
|
52
|
+
private turnInfo: TurnServerInfo;
|
|
35
53
|
|
|
36
54
|
private responseTimer?: ReturnType<typeof setTimeout>;
|
|
37
55
|
|
|
@@ -85,7 +103,8 @@ export default class TurnDiscovery {
|
|
|
85
103
|
}
|
|
86
104
|
|
|
87
105
|
/**
|
|
88
|
-
*
|
|
106
|
+
* Handles TURN_DISCOVERY_RESPONSE roap message. Use it if the roap message comes over the websocket,
|
|
107
|
+
* otherwise use handleTurnDiscoveryHttpResponse() if it comes in the http response.
|
|
89
108
|
*
|
|
90
109
|
* @param {Object} roapMessage
|
|
91
110
|
* @param {string} from string to indicate how we got the response (used just for logging)
|
|
@@ -158,18 +177,191 @@ export default class TurnDiscovery {
|
|
|
158
177
|
}
|
|
159
178
|
|
|
160
179
|
/**
|
|
161
|
-
*
|
|
180
|
+
* Generates TURN_DISCOVERY_REQUEST roap message. When this method returns a roapMessage, it means that a TURN discovery process has started.
|
|
181
|
+
* It needs be ended by calling handleTurnDiscoveryHttpResponse() once you get a response from the backend. If you don't get any response
|
|
182
|
+
* or want to abort, you need to call abort().
|
|
162
183
|
*
|
|
163
|
-
* @param {
|
|
164
|
-
* @
|
|
184
|
+
* @param {Meeting} meeting
|
|
185
|
+
* @param {boolean} isForced
|
|
186
|
+
* @returns {Object}
|
|
187
|
+
*/
|
|
188
|
+
public async generateTurnDiscoveryRequestMessage(
|
|
189
|
+
meeting: Meeting,
|
|
190
|
+
isForced: boolean
|
|
191
|
+
): Promise<{roapMessage?: object; turnDiscoverySkippedReason: TurnDiscoverySkipReason}> {
|
|
192
|
+
if (this.defer) {
|
|
193
|
+
LoggerProxy.logger.warn(
|
|
194
|
+
'Roap:turnDiscovery#generateTurnDiscoveryRequestMessage --> TURN discovery already in progress'
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
roapMessage: undefined,
|
|
199
|
+
turnDiscoverySkippedReason: TurnDiscoverySkipReason.alreadyInProgress,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let turnDiscoverySkippedReason: TurnDiscoverySkipReason;
|
|
204
|
+
|
|
205
|
+
if (!isForced) {
|
|
206
|
+
turnDiscoverySkippedReason = await this.getSkipReason(meeting);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (turnDiscoverySkippedReason) {
|
|
210
|
+
return {roapMessage: undefined, turnDiscoverySkippedReason};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.defer = new Defer();
|
|
214
|
+
|
|
215
|
+
const roapMessage = {
|
|
216
|
+
messageType: ROAP.ROAP_TYPES.TURN_DISCOVERY_REQUEST,
|
|
217
|
+
version: ROAP.ROAP_VERSION,
|
|
218
|
+
seq: TURN_DISCOVERY_SEQ,
|
|
219
|
+
headers: ['includeAnswerInHttpResponse', 'noOkInTransaction'],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
LoggerProxy.logger.info(
|
|
223
|
+
'Roap:turnDiscovery#generateTurnDiscoveryRequestMessage --> generated TURN_DISCOVERY_REQUEST message'
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return {roapMessage, turnDiscoverySkippedReason: undefined};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Handles any errors that occur during TURN discovery without re-throwing them.
|
|
231
|
+
*
|
|
232
|
+
* @param {Meeting} meeting
|
|
233
|
+
* @param {Error} error
|
|
234
|
+
* @returns {TurnDiscoveryResult}
|
|
235
|
+
*/
|
|
236
|
+
private handleTurnDiscoveryFailure(meeting: Meeting, error: Error): TurnDiscoveryResult {
|
|
237
|
+
// we catch any errors and resolve with no turn information so that the normal call join flow can continue without TURN
|
|
238
|
+
LoggerProxy.logger.info(
|
|
239
|
+
`Roap:turnDiscovery#doTurnDiscovery --> TURN discovery failed, continuing without TURN: ${error}`
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_FAILURE, {
|
|
243
|
+
correlation_id: meeting.correlationId,
|
|
244
|
+
locus_id: meeting.locusUrl.split('/').pop(),
|
|
245
|
+
reason: error.message,
|
|
246
|
+
stack: error.stack,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {turnServerInfo: undefined, turnDiscoverySkippedReason: `failure: ${error.message}`};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Handles TURN_DISCOVERY_RESPONSE roap message that came in http response. If the response is not valid,
|
|
254
|
+
* it returns an object with turnServerInfo set to undefined. In that case you need to call abort()
|
|
255
|
+
* to end the TURN discovery process.
|
|
256
|
+
*
|
|
257
|
+
* @param {Meeting} meeting
|
|
258
|
+
* @param {Object|undefined} httpResponse can be undefined to indicate that we didn't get the response
|
|
259
|
+
* @returns {Promise<TurnDiscoveryResult>}
|
|
165
260
|
* @memberof Roap
|
|
166
261
|
*/
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
262
|
+
public async handleTurnDiscoveryHttpResponse(
|
|
263
|
+
meeting: Meeting,
|
|
264
|
+
httpResponse?: object
|
|
265
|
+
): Promise<TurnDiscoveryResult> {
|
|
266
|
+
if (!this.defer) {
|
|
267
|
+
LoggerProxy.logger.warn(
|
|
268
|
+
'Roap:turnDiscovery#handleTurnDiscoveryHttpResponse --> unexpected http response, TURN discovery is not in progress'
|
|
269
|
+
);
|
|
171
270
|
|
|
172
|
-
|
|
271
|
+
throw new Error(
|
|
272
|
+
'handleTurnDiscoveryHttpResponse() called before generateTurnDiscoveryRequestMessage()'
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (httpResponse === undefined) {
|
|
277
|
+
return {
|
|
278
|
+
turnServerInfo: undefined,
|
|
279
|
+
turnDiscoverySkippedReason: TurnDiscoverySkipReason.missingHttpResponse,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const roapMessage = this.parseHttpTurnDiscoveryResponse(meeting, httpResponse);
|
|
285
|
+
|
|
286
|
+
if (!roapMessage) {
|
|
287
|
+
return {
|
|
288
|
+
turnServerInfo: undefined,
|
|
289
|
+
turnDiscoverySkippedReason: TurnDiscoverySkipReason.missingHttpResponse,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.handleTurnDiscoveryResponse(roapMessage, 'in http response');
|
|
294
|
+
|
|
295
|
+
const {isOkRequired} = await this.defer.promise;
|
|
296
|
+
|
|
297
|
+
if (isOkRequired) {
|
|
298
|
+
await this.sendRoapOK(meeting);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this.defer = undefined;
|
|
302
|
+
|
|
303
|
+
LoggerProxy.logger.info('Roap:turnDiscovery#doTurnDiscovery --> TURN discovery completed');
|
|
304
|
+
|
|
305
|
+
return {turnServerInfo: this.turnInfo, turnDiscoverySkippedReason: undefined};
|
|
306
|
+
} catch (error) {
|
|
307
|
+
this.abort();
|
|
308
|
+
|
|
309
|
+
return this.handleTurnDiscoveryFailure(meeting, error);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Aborts current TURN discovery. This method needs to be called if you called generateTurnDiscoveryRequestMessage(),
|
|
315
|
+
* but then never got any response from the server.
|
|
316
|
+
* @returns {void}
|
|
317
|
+
*/
|
|
318
|
+
public abort() {
|
|
319
|
+
if (this.defer) {
|
|
320
|
+
this.defer.reject(new Error('TURN discovery aborted'));
|
|
321
|
+
this.defer = undefined;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Parses the TURN_DISCOVERY_RESPONSE roap message out of the http response
|
|
327
|
+
* and returns it.
|
|
328
|
+
*
|
|
329
|
+
* @param {Meeting} meeting
|
|
330
|
+
* @param {any} httpResponse
|
|
331
|
+
* @returns {any}
|
|
332
|
+
*/
|
|
333
|
+
private parseHttpTurnDiscoveryResponse(
|
|
334
|
+
meeting: Meeting,
|
|
335
|
+
httpResponse: {mediaConnections?: Array<{remoteSdp?: string}>}
|
|
336
|
+
) {
|
|
337
|
+
let turnDiscoveryResponse;
|
|
338
|
+
|
|
339
|
+
if (httpResponse.mediaConnections?.[0]?.remoteSdp) {
|
|
340
|
+
const remoteSdp = JSON.parse(httpResponse.mediaConnections[0].remoteSdp);
|
|
341
|
+
|
|
342
|
+
if (remoteSdp.roapMessage) {
|
|
343
|
+
// yes, it's misleading that remoteSdp actually contains a TURN discovery response, but that's how the backend works...
|
|
344
|
+
const {seq, messageType, errorType, errorCause, headers} = remoteSdp.roapMessage;
|
|
345
|
+
|
|
346
|
+
turnDiscoveryResponse = {
|
|
347
|
+
seq,
|
|
348
|
+
messageType,
|
|
349
|
+
errorType,
|
|
350
|
+
errorCause,
|
|
351
|
+
headers,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!turnDiscoveryResponse) {
|
|
357
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_HTTP_RESPONSE_MISSING, {
|
|
358
|
+
correlationId: meeting.correlationId,
|
|
359
|
+
messageType: 'TURN_DISCOVERY_RESPONSE',
|
|
360
|
+
isMultistream: meeting.isMultistream,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return turnDiscoveryResponse;
|
|
173
365
|
}
|
|
174
366
|
|
|
175
367
|
/**
|
|
@@ -181,13 +373,19 @@ export default class TurnDiscovery {
|
|
|
181
373
|
* @private
|
|
182
374
|
* @memberof Roap
|
|
183
375
|
*/
|
|
184
|
-
sendRoapTurnDiscoveryRequest(
|
|
376
|
+
private sendRoapTurnDiscoveryRequest(
|
|
377
|
+
meeting: Meeting,
|
|
378
|
+
isReconnecting: boolean
|
|
379
|
+
): Promise<TurnDiscoveryResult> {
|
|
185
380
|
if (this.defer) {
|
|
186
381
|
LoggerProxy.logger.warn(
|
|
187
382
|
'Roap:turnDiscovery#sendRoapTurnDiscoveryRequest --> already in progress'
|
|
188
383
|
);
|
|
189
384
|
|
|
190
|
-
return Promise.resolve(
|
|
385
|
+
return Promise.resolve({
|
|
386
|
+
turnServerInfo: undefined,
|
|
387
|
+
turnDiscoverySkippedReason: TurnDiscoverySkipReason.alreadyInProgress,
|
|
388
|
+
});
|
|
191
389
|
}
|
|
192
390
|
|
|
193
391
|
this.defer = new Defer();
|
|
@@ -215,41 +413,14 @@ export default class TurnDiscovery {
|
|
|
215
413
|
// @ts-ignore - because of meeting.webex
|
|
216
414
|
ipVersion: MeetingUtil.getIpVersion(meeting.webex),
|
|
217
415
|
})
|
|
218
|
-
.then((response) => {
|
|
416
|
+
.then(async (response) => {
|
|
219
417
|
const {mediaConnections} = response;
|
|
220
418
|
|
|
221
|
-
let turnDiscoveryResponse;
|
|
222
|
-
|
|
223
419
|
if (mediaConnections) {
|
|
224
420
|
meeting.updateMediaConnections(mediaConnections);
|
|
225
|
-
|
|
226
|
-
if (mediaConnections[0]?.remoteSdp) {
|
|
227
|
-
const remoteSdp = JSON.parse(mediaConnections[0].remoteSdp);
|
|
228
|
-
|
|
229
|
-
if (remoteSdp.roapMessage) {
|
|
230
|
-
// yes, it's misleading that remoteSdp actually contains a TURN discovery response, but that's how the backend works...
|
|
231
|
-
const {seq, messageType, errorType, errorCause, headers} = remoteSdp.roapMessage;
|
|
232
|
-
|
|
233
|
-
turnDiscoveryResponse = {
|
|
234
|
-
seq,
|
|
235
|
-
messageType,
|
|
236
|
-
errorType,
|
|
237
|
-
errorCause,
|
|
238
|
-
headers,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
421
|
}
|
|
243
422
|
|
|
244
|
-
|
|
245
|
-
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_HTTP_RESPONSE_MISSING, {
|
|
246
|
-
correlationId: meeting.correlationId,
|
|
247
|
-
messageType: 'TURN_DISCOVERY_RESPONSE',
|
|
248
|
-
isMultistream: meeting.isMultistream,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return turnDiscoveryResponse;
|
|
423
|
+
return this.handleTurnDiscoveryHttpResponse(meeting, response);
|
|
253
424
|
});
|
|
254
425
|
}
|
|
255
426
|
|
|
@@ -261,7 +432,14 @@ export default class TurnDiscovery {
|
|
|
261
432
|
* @returns {Promise}
|
|
262
433
|
*/
|
|
263
434
|
sendRoapOK(meeting: Meeting) {
|
|
264
|
-
LoggerProxy.logger.info(
|
|
435
|
+
LoggerProxy.logger.info(
|
|
436
|
+
'Roap:turnDiscovery#sendRoapOK --> TURN discovery response requires OK, sending it...'
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_REQUIRES_OK, {
|
|
440
|
+
correlation_id: meeting.correlationId,
|
|
441
|
+
locus_id: meeting.locusUrl.split('/').pop(),
|
|
442
|
+
});
|
|
265
443
|
|
|
266
444
|
return this.roapRequest.sendRoap({
|
|
267
445
|
roapMessage: {
|
|
@@ -284,7 +462,7 @@ export default class TurnDiscovery {
|
|
|
284
462
|
* @param {Meeting} meeting
|
|
285
463
|
* @returns {Promise<string>} Promise with empty string if reachability is not skipped or a reason if it is skipped
|
|
286
464
|
*/
|
|
287
|
-
private async getSkipReason(meeting: Meeting): Promise<
|
|
465
|
+
private async getSkipReason(meeting: Meeting): Promise<TurnDiscoverySkipReason> {
|
|
288
466
|
const isAnyPublicClusterReachable =
|
|
289
467
|
// @ts-ignore - fix type
|
|
290
468
|
await meeting.webex.meetings.reachability.isAnyPublicClusterReachable();
|
|
@@ -294,10 +472,10 @@ export default class TurnDiscovery {
|
|
|
294
472
|
'Roap:turnDiscovery#getSkipReason --> reachability has not failed, skipping TURN discovery'
|
|
295
473
|
);
|
|
296
474
|
|
|
297
|
-
return
|
|
475
|
+
return TurnDiscoverySkipReason.reachability;
|
|
298
476
|
}
|
|
299
477
|
|
|
300
|
-
return
|
|
478
|
+
return undefined;
|
|
301
479
|
}
|
|
302
480
|
|
|
303
481
|
/**
|
|
@@ -330,8 +508,12 @@ export default class TurnDiscovery {
|
|
|
330
508
|
* @param {Boolean} [isForced]
|
|
331
509
|
* @returns {Promise}
|
|
332
510
|
*/
|
|
333
|
-
async doTurnDiscovery(
|
|
334
|
-
|
|
511
|
+
async doTurnDiscovery(
|
|
512
|
+
meeting: Meeting,
|
|
513
|
+
isReconnecting?: boolean,
|
|
514
|
+
isForced?: boolean
|
|
515
|
+
): Promise<TurnDiscoveryResult> {
|
|
516
|
+
let turnDiscoverySkippedReason: TurnDiscoverySkipReason;
|
|
335
517
|
|
|
336
518
|
if (!isForced) {
|
|
337
519
|
turnDiscoverySkippedReason = await this.getSkipReason(meeting);
|
|
@@ -345,24 +527,20 @@ export default class TurnDiscovery {
|
|
|
345
527
|
}
|
|
346
528
|
|
|
347
529
|
try {
|
|
348
|
-
const
|
|
530
|
+
const turnDiscoveryResult = await this.sendRoapTurnDiscoveryRequest(meeting, isReconnecting);
|
|
531
|
+
|
|
532
|
+
if (
|
|
533
|
+
turnDiscoveryResult.turnDiscoverySkippedReason !==
|
|
534
|
+
TurnDiscoverySkipReason.missingHttpResponse
|
|
535
|
+
) {
|
|
536
|
+
return turnDiscoveryResult;
|
|
537
|
+
}
|
|
349
538
|
|
|
350
539
|
// if we haven't got the response over http, we need to wait for it to come over the websocket via Mercury
|
|
351
|
-
const {isOkRequired} =
|
|
352
|
-
? await this.handleTurnDiscoveryResponseInHttpResponse(httpResponse)
|
|
353
|
-
: await this.waitForTurnDiscoveryResponse();
|
|
540
|
+
const {isOkRequired} = await this.waitForTurnDiscoveryResponse();
|
|
354
541
|
|
|
355
542
|
if (isOkRequired) {
|
|
356
543
|
await this.sendRoapOK(meeting);
|
|
357
|
-
|
|
358
|
-
LoggerProxy.logger.info(
|
|
359
|
-
'Roap:turnDiscovery#doTurnDiscovery --> TURN discovery response requires OK'
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_REQUIRES_OK, {
|
|
363
|
-
correlation_id: meeting.correlationId,
|
|
364
|
-
locus_id: meeting.locusUrl.split('/').pop(),
|
|
365
|
-
});
|
|
366
544
|
}
|
|
367
545
|
|
|
368
546
|
this.defer = undefined;
|
|
@@ -371,19 +549,7 @@ export default class TurnDiscovery {
|
|
|
371
549
|
|
|
372
550
|
return {turnServerInfo: this.turnInfo, turnDiscoverySkippedReason: undefined};
|
|
373
551
|
} catch (e) {
|
|
374
|
-
|
|
375
|
-
LoggerProxy.logger.info(
|
|
376
|
-
`Roap:turnDiscovery#doTurnDiscovery --> TURN discovery failed, continuing without TURN: ${e}`
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_FAILURE, {
|
|
380
|
-
correlation_id: meeting.correlationId,
|
|
381
|
-
locus_id: meeting.locusUrl.split('/').pop(),
|
|
382
|
-
reason: e.message,
|
|
383
|
-
stack: e.stack,
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
return {turnServerInfo: undefined, turnDiscoverySkippedReason: undefined};
|
|
552
|
+
return this.handleTurnDiscoveryFailure(meeting, e);
|
|
387
553
|
}
|
|
388
554
|
}
|
|
389
555
|
}
|
|
@@ -4,14 +4,7 @@ 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';
|
|
7
|
-
import {
|
|
8
|
-
DEFAULT_GET_STATS_FILTER,
|
|
9
|
-
STATS,
|
|
10
|
-
MQA_INTERVAL,
|
|
11
|
-
NETWORK_TYPE,
|
|
12
|
-
MEDIA_DEVICES,
|
|
13
|
-
_UNKNOWN_,
|
|
14
|
-
} from '../constants';
|
|
7
|
+
import {DEFAULT_GET_STATS_FILTER, STATS, MQA_INTERVAL, NETWORK_TYPE, _UNKNOWN_} from '../constants';
|
|
15
8
|
import {
|
|
16
9
|
emptyAudioReceive,
|
|
17
10
|
emptyAudioTransmit,
|
|
@@ -264,7 +257,7 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
264
257
|
|
|
265
258
|
// Add stats for individual streams
|
|
266
259
|
Object.keys(this.statsResults).forEach((mediaType) => {
|
|
267
|
-
if (mediaType.
|
|
260
|
+
if (mediaType.startsWith('audio-send')) {
|
|
268
261
|
const audioSenderStream = cloneDeep(emptyAudioTransmitStream);
|
|
269
262
|
|
|
270
263
|
getAudioSenderStreamMqa({
|
|
@@ -276,7 +269,7 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
276
269
|
newMqa.audioTransmit[0].streams.push(audioSenderStream);
|
|
277
270
|
|
|
278
271
|
this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
|
|
279
|
-
} else if (mediaType.
|
|
272
|
+
} else if (mediaType.startsWith('audio-share-send')) {
|
|
280
273
|
const audioSenderStream = cloneDeep(emptyAudioTransmitStream);
|
|
281
274
|
|
|
282
275
|
getAudioSenderStreamMqa({
|
|
@@ -288,7 +281,7 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
288
281
|
newMqa.audioTransmit[1].streams.push(audioSenderStream);
|
|
289
282
|
|
|
290
283
|
this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
|
|
291
|
-
} else if (mediaType.
|
|
284
|
+
} else if (mediaType.startsWith('audio-recv')) {
|
|
292
285
|
const audioReceiverStream = cloneDeep(emptyAudioReceiveStream);
|
|
293
286
|
|
|
294
287
|
getAudioReceiverStreamMqa({
|
|
@@ -300,7 +293,7 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
300
293
|
newMqa.audioReceive[0].streams.push(audioReceiverStream);
|
|
301
294
|
|
|
302
295
|
this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
|
|
303
|
-
} else if (mediaType.
|
|
296
|
+
} else if (mediaType.startsWith('audio-share-recv')) {
|
|
304
297
|
const audioReceiverStream = cloneDeep(emptyAudioReceiveStream);
|
|
305
298
|
|
|
306
299
|
getAudioReceiverStreamMqa({
|
|
@@ -312,7 +305,8 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
312
305
|
newMqa.audioReceive[1].streams.push(audioReceiverStream);
|
|
313
306
|
|
|
314
307
|
this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
|
|
315
|
-
} else if (mediaType.
|
|
308
|
+
} else if (mediaType.startsWith('video-send-layer')) {
|
|
309
|
+
// We only want the stream-specific stats we get with video-send-layer-0, video-send-layer-1, etc.
|
|
316
310
|
const videoSenderStream = cloneDeep(emptyVideoTransmitStream);
|
|
317
311
|
|
|
318
312
|
getVideoSenderStreamMqa({
|
|
@@ -324,7 +318,7 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
324
318
|
newMqa.videoTransmit[0].streams.push(videoSenderStream);
|
|
325
319
|
|
|
326
320
|
this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
|
|
327
|
-
} else if (mediaType.
|
|
321
|
+
} else if (mediaType.startsWith('video-share-send')) {
|
|
328
322
|
const videoSenderStream = cloneDeep(emptyVideoTransmitStream);
|
|
329
323
|
|
|
330
324
|
getVideoSenderStreamMqa({
|
|
@@ -336,7 +330,7 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
336
330
|
newMqa.videoTransmit[1].streams.push(videoSenderStream);
|
|
337
331
|
|
|
338
332
|
this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
|
|
339
|
-
} else if (mediaType.
|
|
333
|
+
} else if (mediaType.startsWith('video-recv')) {
|
|
340
334
|
const videoReceiverStream = cloneDeep(emptyVideoReceiveStream);
|
|
341
335
|
|
|
342
336
|
getVideoReceiverStreamMqa({
|
|
@@ -348,7 +342,7 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
348
342
|
newMqa.videoReceive[0].streams.push(videoReceiverStream);
|
|
349
343
|
|
|
350
344
|
this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
|
|
351
|
-
} else if (mediaType.
|
|
345
|
+
} else if (mediaType.startsWith('video-share-recv')) {
|
|
352
346
|
const videoReceiverStream = cloneDeep(emptyVideoReceiveStream);
|
|
353
347
|
|
|
354
348
|
getVideoReceiverStreamMqa({
|
|
@@ -366,18 +360,23 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
366
360
|
newMqa.intervalMetadata.peerReflexiveIP = this.statsResults.connectionType.local.ipAddress;
|
|
367
361
|
|
|
368
362
|
// Adding peripheral information
|
|
369
|
-
newMqa.intervalMetadata.
|
|
363
|
+
newMqa.intervalMetadata.speakerInfo = {
|
|
364
|
+
deviceName: _UNKNOWN_,
|
|
365
|
+
};
|
|
370
366
|
if (this.statsResults['audio-send']) {
|
|
371
|
-
newMqa.intervalMetadata.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
});
|
|
367
|
+
newMqa.intervalMetadata.microphoneInfo = {
|
|
368
|
+
deviceName: this.statsResults['audio-send'].trackLabel || _UNKNOWN_,
|
|
369
|
+
};
|
|
375
370
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
371
|
+
|
|
372
|
+
const existingVideoSender = Object.keys(this.statsResults).find((item) =>
|
|
373
|
+
item.includes('video-send')
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (existingVideoSender) {
|
|
377
|
+
newMqa.intervalMetadata.cameraInfo = {
|
|
378
|
+
deviceName: this.statsResults[existingVideoSender].trackLabel || _UNKNOWN_,
|
|
379
|
+
};
|
|
381
380
|
}
|
|
382
381
|
|
|
383
382
|
newMqa.networkType = this.statsResults.connectionType.local.networkType;
|
|
@@ -552,9 +551,21 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
552
551
|
}
|
|
553
552
|
});
|
|
554
553
|
|
|
554
|
+
let videoSenderIndex = 0;
|
|
555
555
|
statsItem.report.forEach((result) => {
|
|
556
556
|
if (types.includes(result.type)) {
|
|
557
|
-
|
|
557
|
+
// if the video sender has multiple streams in the report, it is a new stream object.
|
|
558
|
+
if (type === 'video-send' && result.type === 'outbound-rtp') {
|
|
559
|
+
const newType = `video-send-layer-${videoSenderIndex}`;
|
|
560
|
+
this.parseGetStatsResult(result, newType, isSender);
|
|
561
|
+
videoSenderIndex += 1;
|
|
562
|
+
|
|
563
|
+
this.statsResults[newType].direction = statsItem.currentDirection;
|
|
564
|
+
this.statsResults[newType].trackLabel = statsItem.localTrackLabel;
|
|
565
|
+
this.statsResults[newType].csi = statsItem.csi;
|
|
566
|
+
} else {
|
|
567
|
+
this.parseGetStatsResult(result, type, isSender);
|
|
568
|
+
}
|
|
558
569
|
}
|
|
559
570
|
});
|
|
560
571
|
|
|
@@ -664,12 +675,23 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
664
675
|
const getCurrentStatsTotals = (keyPrefix: string, value: string): number =>
|
|
665
676
|
Object.keys(this.statsResults)
|
|
666
677
|
.filter((key) => key.startsWith(keyPrefix))
|
|
667
|
-
.reduce(
|
|
678
|
+
.reduce(
|
|
679
|
+
(prev, cur) =>
|
|
680
|
+
prev +
|
|
681
|
+
(this.statsResults[cur]?.[keyPrefix.includes('send') ? 'send' : 'recv'][value] || 0),
|
|
682
|
+
0
|
|
683
|
+
);
|
|
668
684
|
|
|
669
685
|
const getPreviousStatsTotals = (keyPrefix: string, value: string): number =>
|
|
670
686
|
Object.keys(this.statsResults)
|
|
671
687
|
.filter((key) => key.startsWith(keyPrefix))
|
|
672
|
-
.reduce(
|
|
688
|
+
.reduce(
|
|
689
|
+
(prev, cur) =>
|
|
690
|
+
prev +
|
|
691
|
+
(this.lastStatsResults[cur]?.[keyPrefix.includes('send') ? 'send' : 'recv'][value] ||
|
|
692
|
+
0),
|
|
693
|
+
0
|
|
694
|
+
);
|
|
673
695
|
|
|
674
696
|
// Audio Transmit
|
|
675
697
|
if (this.lastStatsResults['audio-send']) {
|
|
@@ -731,47 +753,54 @@ export class StatsAnalyzer extends EventsScope {
|
|
|
731
753
|
false
|
|
732
754
|
);
|
|
733
755
|
|
|
756
|
+
const currentTotalPacketsSent = getCurrentStatsTotals('video-send', 'totalPacketsSent');
|
|
757
|
+
const previousTotalPacketsSent = getPreviousStatsTotals('video-send', 'totalPacketsSent');
|
|
758
|
+
|
|
759
|
+
const currentFramesEncoded = getCurrentStatsTotals('video-send', 'framesEncoded');
|
|
760
|
+
const previousFramesEncoded = getPreviousStatsTotals('video-send', 'framesEncoded');
|
|
761
|
+
|
|
762
|
+
const currentFramesSent = getCurrentStatsTotals('video-send', 'framesSent');
|
|
763
|
+
const previousFramesSent = getPreviousStatsTotals('video-send', 'framesSent');
|
|
764
|
+
|
|
765
|
+
const doesVideoSendExist = Object.keys(this.lastStatsResults).some((item) =>
|
|
766
|
+
item.includes('video-send')
|
|
767
|
+
);
|
|
768
|
+
|
|
734
769
|
// Video Transmit
|
|
735
|
-
if (
|
|
770
|
+
if (doesVideoSendExist) {
|
|
736
771
|
// compare video stats sent
|
|
737
|
-
const currentStats = this.statsResults['video-send'].send;
|
|
738
|
-
const previousStats = this.lastStatsResults['video-send'].send;
|
|
739
772
|
|
|
740
773
|
if (
|
|
741
774
|
this.meetingMediaStatus.expected.sendVideo &&
|
|
742
|
-
(
|
|
743
|
-
currentStats.totalPacketsSent === 0)
|
|
775
|
+
(currentTotalPacketsSent === previousTotalPacketsSent || currentTotalPacketsSent === 0)
|
|
744
776
|
) {
|
|
745
777
|
LoggerProxy.logger.info(
|
|
746
778
|
`StatsAnalyzer:index#compareLastStatsResult --> No video RTP packets sent`,
|
|
747
|
-
|
|
779
|
+
currentTotalPacketsSent
|
|
748
780
|
);
|
|
749
781
|
} else {
|
|
750
782
|
if (
|
|
751
783
|
this.meetingMediaStatus.expected.sendVideo &&
|
|
752
|
-
(
|
|
753
|
-
currentStats.framesEncoded === 0)
|
|
784
|
+
(currentFramesEncoded === previousFramesEncoded || currentFramesEncoded === 0)
|
|
754
785
|
) {
|
|
755
786
|
LoggerProxy.logger.info(
|
|
756
787
|
`StatsAnalyzer:index#compareLastStatsResult --> No video Frames Encoded`,
|
|
757
|
-
|
|
788
|
+
currentFramesEncoded
|
|
758
789
|
);
|
|
759
790
|
}
|
|
760
791
|
|
|
761
792
|
if (
|
|
762
793
|
this.meetingMediaStatus.expected.sendVideo &&
|
|
763
|
-
(
|
|
764
|
-
this.lastStatsResults['video-send'].send.framesSent ||
|
|
765
|
-
this.statsResults['video-send'].send.framesSent === 0)
|
|
794
|
+
(currentFramesSent === previousFramesSent || currentFramesSent === 0)
|
|
766
795
|
) {
|
|
767
796
|
LoggerProxy.logger.info(
|
|
768
797
|
`StatsAnalyzer:index#compareLastStatsResult --> No video Frames sent`,
|
|
769
|
-
|
|
798
|
+
currentFramesSent
|
|
770
799
|
);
|
|
771
800
|
}
|
|
772
801
|
}
|
|
773
802
|
|
|
774
|
-
this.emitStartStopEvents('video',
|
|
803
|
+
this.emitStartStopEvents('video', previousFramesSent, currentFramesSent, true);
|
|
775
804
|
}
|
|
776
805
|
|
|
777
806
|
// Video Receive
|