@webex/plugin-meetings 3.8.0-next.2 → 3.8.0-next.20
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 +1 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/interpretation/index.js +4 -4
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +1 -1
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/meeting/index.js +43 -3
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/locusMediaRequest.js +21 -5
- package/dist/meeting/locusMediaRequest.js.map +1 -1
- package/dist/meetings/index.js +1 -1
- package/dist/meetings/index.js.map +1 -1
- package/dist/multistream/remoteMediaManager.js +40 -8
- package/dist/multistream/remoteMediaManager.js.map +1 -1
- package/dist/reachability/clusterReachability.js +52 -8
- package/dist/reachability/clusterReachability.js.map +1 -1
- package/dist/reachability/index.js +70 -45
- package/dist/reachability/index.js.map +1 -1
- package/dist/reachability/reachability.types.js +14 -0
- package/dist/reachability/reachability.types.js.map +1 -1
- package/dist/recording-controller/util.js +5 -5
- package/dist/recording-controller/util.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/constants.d.ts +1 -0
- package/dist/types/meeting/index.d.ts +22 -0
- package/dist/types/multistream/remoteMediaManager.d.ts +10 -1
- package/dist/types/reachability/clusterReachability.d.ts +13 -1
- package/dist/types/reachability/index.d.ts +2 -1
- package/dist/types/reachability/reachability.types.d.ts +5 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +22 -22
- package/src/config.ts +1 -0
- package/src/constants.ts +1 -0
- package/src/interpretation/index.ts +3 -3
- package/src/locus-info/controlsUtils.ts +2 -2
- package/src/meeting/index.ts +39 -1
- package/src/meeting/locusMediaRequest.ts +27 -4
- package/src/meetings/index.ts +1 -1
- package/src/multistream/remoteMediaManager.ts +32 -10
- package/src/reachability/clusterReachability.ts +47 -1
- package/src/reachability/index.ts +15 -0
- package/src/reachability/reachability.types.ts +6 -0
- package/src/recording-controller/util.ts +17 -13
- package/test/unit/spec/interpretation/index.ts +39 -1
- package/test/unit/spec/locus-info/controlsUtils.js +8 -0
- package/test/unit/spec/meeting/index.js +163 -104
- package/test/unit/spec/meeting/locusMediaRequest.ts +96 -58
- package/test/unit/spec/meetings/index.js +13 -0
- package/test/unit/spec/multistream/remoteMediaManager.ts +397 -118
- package/test/unit/spec/reachability/clusterReachability.ts +47 -1
- package/test/unit/spec/reachability/index.ts +12 -0
|
@@ -4,6 +4,10 @@ export type TransportResult = {
|
|
|
4
4
|
latencyInMilliseconds?: number;
|
|
5
5
|
clientMediaIPs?: string[];
|
|
6
6
|
};
|
|
7
|
+
export declare enum NatType {
|
|
8
|
+
Unknown = "unknown",
|
|
9
|
+
SymmetricNat = "symmetric-nat"
|
|
10
|
+
}
|
|
7
11
|
export type ClusterReachabilityResult = {
|
|
8
12
|
udp: TransportResult;
|
|
9
13
|
tcp: TransportResult;
|
|
@@ -22,6 +26,7 @@ export type ReachabilityMetrics = {
|
|
|
22
26
|
reachability_vmn_tcp_failed: number;
|
|
23
27
|
reachability_vmn_xtls_success: number;
|
|
24
28
|
reachability_vmn_xtls_failed: number;
|
|
29
|
+
natType: NatType;
|
|
25
30
|
};
|
|
26
31
|
/**
|
|
27
32
|
* This is the type that matches what backend expects us to send to them. It is a bit weird, because
|
package/dist/webinar/index.js
CHANGED
package/package.json
CHANGED
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"@webex/eslint-config-legacy": "0.0.0",
|
|
44
44
|
"@webex/jest-config-legacy": "0.0.0",
|
|
45
45
|
"@webex/legacy-tools": "0.0.0",
|
|
46
|
-
"@webex/plugin-meetings": "3.8.0-next.
|
|
47
|
-
"@webex/plugin-rooms": "3.
|
|
48
|
-
"@webex/test-helper-chai": "3.
|
|
49
|
-
"@webex/test-helper-mocha": "3.
|
|
50
|
-
"@webex/test-helper-mock-webex": "3.
|
|
51
|
-
"@webex/test-helper-retry": "3.
|
|
52
|
-
"@webex/test-helper-test-users": "3.
|
|
46
|
+
"@webex/plugin-meetings": "3.8.0-next.20",
|
|
47
|
+
"@webex/plugin-rooms": "3.8.0-next.10",
|
|
48
|
+
"@webex/test-helper-chai": "3.8.0-next.9",
|
|
49
|
+
"@webex/test-helper-mocha": "3.8.0-next.9",
|
|
50
|
+
"@webex/test-helper-mock-webex": "3.8.0-next.9",
|
|
51
|
+
"@webex/test-helper-retry": "3.8.0-next.9",
|
|
52
|
+
"@webex/test-helper-test-users": "3.8.0-next.9",
|
|
53
53
|
"chai": "^4.3.4",
|
|
54
54
|
"chai-as-promised": "^7.1.1",
|
|
55
55
|
"eslint": "^8.24.0",
|
|
@@ -61,22 +61,22 @@
|
|
|
61
61
|
"typescript": "^4.7.4"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@webex/common": "3.
|
|
64
|
+
"@webex/common": "3.8.0-next.9",
|
|
65
65
|
"@webex/event-dictionary-ts": "^1.0.1688",
|
|
66
|
-
"@webex/internal-media-core": "2.14.
|
|
67
|
-
"@webex/internal-plugin-conversation": "3.
|
|
68
|
-
"@webex/internal-plugin-device": "3.
|
|
69
|
-
"@webex/internal-plugin-llm": "3.8.0-next.
|
|
70
|
-
"@webex/internal-plugin-mercury": "3.
|
|
71
|
-
"@webex/internal-plugin-metrics": "3.
|
|
72
|
-
"@webex/internal-plugin-support": "3.
|
|
73
|
-
"@webex/internal-plugin-user": "3.
|
|
74
|
-
"@webex/internal-plugin-voicea": "3.8.0-next.
|
|
75
|
-
"@webex/media-helpers": "3.
|
|
76
|
-
"@webex/plugin-people": "3.
|
|
77
|
-
"@webex/plugin-rooms": "3.
|
|
66
|
+
"@webex/internal-media-core": "2.14.6",
|
|
67
|
+
"@webex/internal-plugin-conversation": "3.8.0-next.10",
|
|
68
|
+
"@webex/internal-plugin-device": "3.8.0-next.9",
|
|
69
|
+
"@webex/internal-plugin-llm": "3.8.0-next.11",
|
|
70
|
+
"@webex/internal-plugin-mercury": "3.8.0-next.10",
|
|
71
|
+
"@webex/internal-plugin-metrics": "3.8.0-next.9",
|
|
72
|
+
"@webex/internal-plugin-support": "3.8.0-next.10",
|
|
73
|
+
"@webex/internal-plugin-user": "3.8.0-next.9",
|
|
74
|
+
"@webex/internal-plugin-voicea": "3.8.0-next.20",
|
|
75
|
+
"@webex/media-helpers": "3.8.0-next.10",
|
|
76
|
+
"@webex/plugin-people": "3.8.0-next.10",
|
|
77
|
+
"@webex/plugin-rooms": "3.8.0-next.10",
|
|
78
78
|
"@webex/web-capabilities": "^1.4.0",
|
|
79
|
-
"@webex/webex-core": "3.
|
|
79
|
+
"@webex/webex-core": "3.8.0-next.9",
|
|
80
80
|
"ampersand-collection": "^2.0.2",
|
|
81
81
|
"bowser": "^2.11.0",
|
|
82
82
|
"btoa": "^1.2.1",
|
|
@@ -92,5 +92,5 @@
|
|
|
92
92
|
"//": [
|
|
93
93
|
"TODO: upgrade jwt-decode when moving to node 18"
|
|
94
94
|
],
|
|
95
|
-
"version": "3.8.0-next.
|
|
95
|
+
"version": "3.8.0-next.20"
|
|
96
96
|
}
|
package/src/config.ts
CHANGED
|
@@ -95,6 +95,7 @@ export default {
|
|
|
95
95
|
// This only applies to non-multistream meetings
|
|
96
96
|
iceCandidatesGatheringTimeout: undefined,
|
|
97
97
|
backendIpv6NativeSupport: false,
|
|
98
|
+
enableReachabilityChecks: true,
|
|
98
99
|
reachabilityGetClusterTimeout: 5000,
|
|
99
100
|
logUploadIntervalMultiplicationFactor: 0, // if set to 0 or undefined, logs won't be uploaded periodically, if you want periodic logs, recommended value is 1
|
|
100
101
|
},
|
package/src/constants.ts
CHANGED
|
@@ -904,6 +904,7 @@ export enum SELF_POLICY {
|
|
|
904
904
|
ENFORCE_VIRTUAL_BACKGROUND = 'enforceVirtualBackground',
|
|
905
905
|
SUPPORT_LOCAL_RECORD = 'supportLocalRecord',
|
|
906
906
|
SUPPORT_NETWORK_BASED_RECORD = 'supportNetworkBasedRecord',
|
|
907
|
+
SUPPORT_PREMISE_RECORD = 'supportPremiseRecord',
|
|
907
908
|
SUPPORT_REALTIME_CLOSE_CAPTION = 'supportRealtimeCloseCaption',
|
|
908
909
|
SUPPORT_CHAT = 'supportChat',
|
|
909
910
|
SUPPORT_DESKTOP_SHARE_REMOTE = 'supportDesktopShareRemote',
|
|
@@ -35,13 +35,13 @@ const SimultaneousInterpretation = WebexPlugin.extend({
|
|
|
35
35
|
derived: {
|
|
36
36
|
shouldQuerySupportLanguages: {
|
|
37
37
|
cache: false,
|
|
38
|
-
deps: ['canManageInterpreters', 'hostSIEnabled'],
|
|
38
|
+
deps: ['canManageInterpreters', 'hostSIEnabled', 'locusUrl'],
|
|
39
39
|
/**
|
|
40
40
|
* Returns should query support languages or not
|
|
41
41
|
* @returns {boolean}
|
|
42
42
|
*/
|
|
43
43
|
fn() {
|
|
44
|
-
return !!(this.canManageInterpreters && this.hostSIEnabled);
|
|
44
|
+
return !!(this.canManageInterpreters && this.hostSIEnabled && this.locusUrl);
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
47
|
},
|
|
@@ -51,7 +51,7 @@ const SimultaneousInterpretation = WebexPlugin.extend({
|
|
|
51
51
|
*/
|
|
52
52
|
initialize() {
|
|
53
53
|
this.listenTo(this, 'change:shouldQuerySupportLanguages', () => {
|
|
54
|
-
if (this.
|
|
54
|
+
if (this.shouldQuerySupportLanguages && !this.supportLanguages) {
|
|
55
55
|
this.querySupportLanguages();
|
|
56
56
|
}
|
|
57
57
|
});
|
|
@@ -206,8 +206,8 @@ ControlsUtils.getControls = (oldControls: any, newControls: any) => {
|
|
|
206
206
|
),
|
|
207
207
|
|
|
208
208
|
hasPracticeSessionEnabledChanged: !isEqual(
|
|
209
|
-
previous?.practiceSession?.enabled,
|
|
210
|
-
current?.practiceSession?.enabled
|
|
209
|
+
!!previous?.practiceSession?.enabled,
|
|
210
|
+
!!current?.practiceSession?.enabled
|
|
211
211
|
),
|
|
212
212
|
|
|
213
213
|
hasStageViewChanged: !isEqual(previous?.videoLayout, current?.videoLayout),
|
package/src/meeting/index.ts
CHANGED
|
@@ -241,6 +241,8 @@ export type CallStateForMetrics = {
|
|
|
241
241
|
sessionCorrelationId?: string;
|
|
242
242
|
joinTrigger?: string;
|
|
243
243
|
loginType?: string;
|
|
244
|
+
userNameInput?: string;
|
|
245
|
+
emailInput?: string;
|
|
244
246
|
};
|
|
245
247
|
|
|
246
248
|
export const MEDIA_UPDATE_TYPE = {
|
|
@@ -1627,6 +1629,38 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1627
1629
|
this.callStateForMetrics.correlationId = correlationId;
|
|
1628
1630
|
}
|
|
1629
1631
|
|
|
1632
|
+
/**
|
|
1633
|
+
* Getter - Returns callStateForMetrics.userNameInput
|
|
1634
|
+
* @returns {string}
|
|
1635
|
+
*/
|
|
1636
|
+
get userNameInput() {
|
|
1637
|
+
return this.callStateForMetrics?.userNameInput;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Setter - sets callStateForMetrics.userNameInput
|
|
1642
|
+
* @param {string} userNameInput
|
|
1643
|
+
*/
|
|
1644
|
+
set userNameInput(userNameInput: string) {
|
|
1645
|
+
this.callStateForMetrics.userNameInput = userNameInput;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Getter - Returns callStateForMetrics.emailInput
|
|
1650
|
+
* @returns {string}
|
|
1651
|
+
*/
|
|
1652
|
+
get emailInput() {
|
|
1653
|
+
return this.callStateForMetrics?.emailInput;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Setter - sets callStateForMetrics.emailInput
|
|
1658
|
+
* @param {string} emailInput
|
|
1659
|
+
*/
|
|
1660
|
+
set emailInput(emailInput: string) {
|
|
1661
|
+
this.callStateForMetrics.emailInput = emailInput;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1630
1664
|
/**
|
|
1631
1665
|
* Getter - Returns callStateForMetrics.sessionCorrelationId
|
|
1632
1666
|
* @returns {string}
|
|
@@ -8681,6 +8715,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8681
8715
|
LoggerProxy.logger.log(
|
|
8682
8716
|
`Meeting:index#handleShareVideoStreamMuteStateChange --> Share video stream mute state changed to muted ${muted}`
|
|
8683
8717
|
);
|
|
8718
|
+
|
|
8719
|
+
const shareVideoStreamSettings = this.mediaProperties?.shareVideoStream?.getSettings();
|
|
8720
|
+
|
|
8684
8721
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE, {
|
|
8685
8722
|
correlationId: this.correlationId,
|
|
8686
8723
|
muted,
|
|
@@ -8689,8 +8726,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8689
8726
|
// SDK to TypeScript 5, which may affect other packages, use bracket notation for now, since
|
|
8690
8727
|
// all we're doing here is adding metrics.
|
|
8691
8728
|
// eslint-disable-next-line dot-notation
|
|
8692
|
-
displaySurface:
|
|
8729
|
+
displaySurface: shareVideoStreamSettings?.['displaySurface'],
|
|
8693
8730
|
isMultistream: this.isMultistream,
|
|
8731
|
+
frameRate: shareVideoStreamSettings?.frameRate,
|
|
8694
8732
|
});
|
|
8695
8733
|
};
|
|
8696
8734
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable valid-jsdoc */
|
|
2
2
|
import {defer} from 'lodash';
|
|
3
|
-
import {Defer} from '@webex/common';
|
|
3
|
+
import {Defer, transferEvents} from '@webex/common';
|
|
4
|
+
import {EventEmitter} from 'events';
|
|
4
5
|
import {WebexPlugin} from '@webex/webex-core';
|
|
5
6
|
import {MEDIA, HTTP_VERBS, ROAP} from '../constants';
|
|
6
7
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
@@ -250,12 +251,19 @@ export class LocusMediaRequest extends WebexPlugin {
|
|
|
250
251
|
this.confluenceState = 'creation in progress';
|
|
251
252
|
}
|
|
252
253
|
|
|
253
|
-
|
|
254
|
-
|
|
254
|
+
const upload = new EventEmitter();
|
|
255
|
+
const download = new EventEmitter();
|
|
256
|
+
|
|
257
|
+
const options = {
|
|
255
258
|
method: HTTP_VERBS.PUT,
|
|
256
259
|
uri,
|
|
257
260
|
body,
|
|
258
|
-
|
|
261
|
+
upload,
|
|
262
|
+
download,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// @ts-ignore
|
|
266
|
+
const promise = this.request(options)
|
|
259
267
|
.then((result) => {
|
|
260
268
|
if (isRequestAffectingConfluenceState(request)) {
|
|
261
269
|
this.confluenceState = 'created';
|
|
@@ -294,6 +302,21 @@ export class LocusMediaRequest extends WebexPlugin {
|
|
|
294
302
|
|
|
295
303
|
throw e;
|
|
296
304
|
});
|
|
305
|
+
|
|
306
|
+
if (request.type === 'RoapMessage') {
|
|
307
|
+
const setupProgressListener = (direction: string, eventEmitter: EventEmitter) => {
|
|
308
|
+
eventEmitter.on('progress', (progressEvent: ProgressEvent) => {
|
|
309
|
+
LoggerProxy.logger.info(
|
|
310
|
+
`${request.type}: ${direction} Progress, Timestamp: ${progressEvent.timeStamp}, Progress: ${progressEvent.loaded}/${progressEvent.total}`
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
setupProgressListener('Upload', options.upload);
|
|
316
|
+
setupProgressListener('Download', options.download);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return promise;
|
|
297
320
|
}
|
|
298
321
|
|
|
299
322
|
/**
|
package/src/meetings/index.ts
CHANGED
|
@@ -854,7 +854,7 @@ export default class Meetings extends WebexPlugin {
|
|
|
854
854
|
this.executeRegistrationStep(
|
|
855
855
|
() =>
|
|
856
856
|
this.startReachability('registration').catch((error) => {
|
|
857
|
-
LoggerProxy.logger.
|
|
857
|
+
LoggerProxy.logger.warn(`Meetings:index#register --> startReachability failed:`, error);
|
|
858
858
|
}),
|
|
859
859
|
'startReachability'
|
|
860
860
|
),
|
|
@@ -1054,22 +1054,44 @@ export class RemoteMediaManager extends EventsScope {
|
|
|
1054
1054
|
);
|
|
1055
1055
|
}
|
|
1056
1056
|
|
|
1057
|
+
/**
|
|
1058
|
+
* Set multiple remote video CSIs at once
|
|
1059
|
+
* @param remoteMediaCsis The remote medias and CSIs to set them to
|
|
1060
|
+
* @returns {void}
|
|
1061
|
+
*/
|
|
1062
|
+
public setRemoteVideoCsis(remoteMediaCsis: {remoteMedia: RemoteMedia; csi?: CSI | null}[]) {
|
|
1063
|
+
if (!remoteMediaCsis.length) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Check all remote medias are known
|
|
1068
|
+
remoteMediaCsis.forEach(({remoteMedia}) => {
|
|
1069
|
+
if (!Object.values(this.media.video.memberPanes).includes(remoteMedia)) {
|
|
1070
|
+
throw new Error(`remoteMedia ${remoteMedia.id} not found`);
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
// Set remote video CSIs
|
|
1075
|
+
remoteMediaCsis.forEach(({remoteMedia, csi}) => {
|
|
1076
|
+
if (csi) {
|
|
1077
|
+
remoteMedia.sendMediaRequest(csi, false);
|
|
1078
|
+
} else {
|
|
1079
|
+
remoteMedia.cancelMediaRequest(false);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// Commit the changes
|
|
1084
|
+
this.mediaRequestManagers.video.commit();
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1057
1087
|
/**
|
|
1058
1088
|
* Sets a new CSI on a given remote media object
|
|
1059
1089
|
*
|
|
1060
1090
|
* @param {RemoteMedia} remoteMedia remote Media object to modify
|
|
1061
1091
|
* @param {CSI} csi new CSI value, can be null if we want to stop receiving media
|
|
1062
1092
|
*/
|
|
1063
|
-
public setRemoteVideoCsi(remoteMedia: RemoteMedia, csi
|
|
1064
|
-
|
|
1065
|
-
throw new Error('remoteMedia not found');
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
if (csi) {
|
|
1069
|
-
remoteMedia.sendMediaRequest(csi, true);
|
|
1070
|
-
} else {
|
|
1071
|
-
remoteMedia.cancelMediaRequest(true);
|
|
1072
|
-
}
|
|
1093
|
+
public setRemoteVideoCsi(remoteMedia: RemoteMedia, csi?: CSI | null) {
|
|
1094
|
+
this.setRemoteVideoCsis([{remoteMedia, csi}]);
|
|
1073
1095
|
}
|
|
1074
1096
|
|
|
1075
1097
|
/**
|
|
@@ -6,7 +6,7 @@ import {convertStunUrlToTurn, convertStunUrlToTurnTls} from './util';
|
|
|
6
6
|
import EventsScope from '../common/events/events-scope';
|
|
7
7
|
|
|
8
8
|
import {CONNECTION_STATE, Enum, ICE_GATHERING_STATE} from '../constants';
|
|
9
|
-
import {ClusterReachabilityResult} from './reachability.types';
|
|
9
|
+
import {ClusterReachabilityResult, NatType} from './reachability.types';
|
|
10
10
|
|
|
11
11
|
// data for the Events.resultReady event
|
|
12
12
|
export type ResultEventData = {
|
|
@@ -22,9 +22,14 @@ export type ClientMediaIpsUpdatedEventData = {
|
|
|
22
22
|
clientMediaIPs: string[];
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
export type NatTypeUpdatedEventData = {
|
|
26
|
+
natType: NatType;
|
|
27
|
+
};
|
|
28
|
+
|
|
25
29
|
export const Events = {
|
|
26
30
|
resultReady: 'resultReady', // emitted when a cluster is reached successfully using specific protocol
|
|
27
31
|
clientMediaIpsUpdated: 'clientMediaIpsUpdated', // emitted when more public IPs are found after resultReady was already sent for a given protocol
|
|
32
|
+
natTypeUpdated: 'natTypeUpdated', // emitted when NAT type is determined
|
|
28
33
|
} as const;
|
|
29
34
|
|
|
30
35
|
export type Events = Enum<typeof Events>;
|
|
@@ -41,6 +46,7 @@ export class ClusterReachability extends EventsScope {
|
|
|
41
46
|
private pc?: RTCPeerConnection;
|
|
42
47
|
private defer: Defer; // this defer is resolved once reachability checks for this cluster are completed
|
|
43
48
|
private startTimestamp: number;
|
|
49
|
+
private srflxIceCandidates: RTCIceCandidate[] = [];
|
|
44
50
|
public readonly isVideoMesh: boolean;
|
|
45
51
|
public readonly name;
|
|
46
52
|
|
|
@@ -290,6 +296,44 @@ export class ClusterReachability extends EventsScope {
|
|
|
290
296
|
}
|
|
291
297
|
}
|
|
292
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Determines NAT Type.
|
|
301
|
+
*
|
|
302
|
+
* @param {RTCIceCandidate} candidate
|
|
303
|
+
* @returns {void}
|
|
304
|
+
*/
|
|
305
|
+
private determineNatType(candidate: RTCIceCandidate) {
|
|
306
|
+
this.srflxIceCandidates.push(candidate);
|
|
307
|
+
|
|
308
|
+
if (this.srflxIceCandidates.length > 1) {
|
|
309
|
+
const portsFound: Record<string, Set<number>> = {};
|
|
310
|
+
|
|
311
|
+
this.srflxIceCandidates.forEach((c) => {
|
|
312
|
+
const key = `${c.address}:${c.relatedPort}`;
|
|
313
|
+
if (!portsFound[key]) {
|
|
314
|
+
portsFound[key] = new Set();
|
|
315
|
+
}
|
|
316
|
+
portsFound[key].add(c.port);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
Object.entries(portsFound).forEach(([, ports]) => {
|
|
320
|
+
if (ports.size > 1) {
|
|
321
|
+
// Found candidates with the same address and relatedPort, but different ports
|
|
322
|
+
this.emit(
|
|
323
|
+
{
|
|
324
|
+
file: 'clusterReachability',
|
|
325
|
+
function: 'determineNatType',
|
|
326
|
+
},
|
|
327
|
+
Events.natTypeUpdated,
|
|
328
|
+
{
|
|
329
|
+
natType: NatType.SymmetricNat,
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
293
337
|
/**
|
|
294
338
|
* Registers a listener for the icecandidate event
|
|
295
339
|
*
|
|
@@ -308,6 +352,8 @@ export class ClusterReachability extends EventsScope {
|
|
|
308
352
|
if (e.candidate) {
|
|
309
353
|
if (e.candidate.type === CANDIDATE_TYPES.SERVER_REFLEXIVE) {
|
|
310
354
|
this.saveResult('udp', latencyInMilliseconds, e.candidate.address);
|
|
355
|
+
|
|
356
|
+
this.determineNatType(e.candidate);
|
|
311
357
|
}
|
|
312
358
|
|
|
313
359
|
if (e.candidate.type === CANDIDATE_TYPES.RELAY) {
|
|
@@ -23,11 +23,13 @@ import {
|
|
|
23
23
|
ReachabilityResultsForBackend,
|
|
24
24
|
TransportResultForBackend,
|
|
25
25
|
GetClustersTrigger,
|
|
26
|
+
NatType,
|
|
26
27
|
} from './reachability.types';
|
|
27
28
|
import {
|
|
28
29
|
ClientMediaIpsUpdatedEventData,
|
|
29
30
|
ClusterReachability,
|
|
30
31
|
Events,
|
|
32
|
+
NatTypeUpdatedEventData,
|
|
31
33
|
ResultEventData,
|
|
32
34
|
} from './clusterReachability';
|
|
33
35
|
import EventsScope from '../common/events/events-scope';
|
|
@@ -64,6 +66,7 @@ export default class Reachability extends EventsScope {
|
|
|
64
66
|
resultsCount = {videoMesh: {udp: 0}, public: {udp: 0, tcp: 0, xtls: 0}};
|
|
65
67
|
startTime = undefined;
|
|
66
68
|
totalDuration = undefined;
|
|
69
|
+
natType = NatType.Unknown;
|
|
67
70
|
|
|
68
71
|
protected lastTrigger?: string;
|
|
69
72
|
|
|
@@ -143,6 +146,10 @@ export default class Reachability extends EventsScope {
|
|
|
143
146
|
* @memberof Reachability
|
|
144
147
|
*/
|
|
145
148
|
public async gatherReachability(trigger: string): Promise<ReachabilityResults> {
|
|
149
|
+
// @ts-ignore
|
|
150
|
+
if (!this.webex.config.meetings.enableReachabilityChecks) {
|
|
151
|
+
throw new Error('enableReachabilityChecks is disabled in config');
|
|
152
|
+
}
|
|
146
153
|
// Fetch clusters and measure latency
|
|
147
154
|
try {
|
|
148
155
|
this.lastTrigger = trigger;
|
|
@@ -305,6 +312,7 @@ export default class Reachability extends EventsScope {
|
|
|
305
312
|
reachability_vmn_tcp_failed: 0,
|
|
306
313
|
reachability_vmn_xtls_success: 0,
|
|
307
314
|
reachability_vmn_xtls_failed: 0,
|
|
315
|
+
natType: this.natType,
|
|
308
316
|
};
|
|
309
317
|
|
|
310
318
|
const updateStats = (clusterType: 'public' | 'vmn', result: ClusterReachabilityResult) => {
|
|
@@ -963,6 +971,13 @@ export default class Reachability extends EventsScope {
|
|
|
963
971
|
}
|
|
964
972
|
);
|
|
965
973
|
|
|
974
|
+
this.clusterReachability[key].on(
|
|
975
|
+
Events.natTypeUpdated,
|
|
976
|
+
async (data: NatTypeUpdatedEventData) => {
|
|
977
|
+
this.natType = data.natType;
|
|
978
|
+
}
|
|
979
|
+
);
|
|
980
|
+
|
|
966
981
|
this.clusterReachability[key].start(); // not awaiting on purpose
|
|
967
982
|
});
|
|
968
983
|
}
|
|
@@ -7,6 +7,11 @@ export type TransportResult = {
|
|
|
7
7
|
clientMediaIPs?: string[];
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
export enum NatType {
|
|
11
|
+
Unknown = 'unknown',
|
|
12
|
+
SymmetricNat = 'symmetric-nat',
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
// reachability result for a specific media cluster
|
|
11
16
|
export type ClusterReachabilityResult = {
|
|
12
17
|
udp: TransportResult;
|
|
@@ -27,6 +32,7 @@ export type ReachabilityMetrics = {
|
|
|
27
32
|
reachability_vmn_tcp_failed: number;
|
|
28
33
|
reachability_vmn_xtls_success: number;
|
|
29
34
|
reachability_vmn_xtls_failed: number;
|
|
35
|
+
natType: NatType;
|
|
30
36
|
};
|
|
31
37
|
|
|
32
38
|
/**
|
|
@@ -6,33 +6,37 @@ const canUserStart = (
|
|
|
6
6
|
displayHints: Array<string>,
|
|
7
7
|
userPolicies: Record<SELF_POLICY, boolean>
|
|
8
8
|
): boolean =>
|
|
9
|
-
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_START)
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_START) &&
|
|
10
|
+
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies)) ||
|
|
11
|
+
(displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_START) &&
|
|
12
|
+
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_PREMISE_RECORD, userPolicies));
|
|
12
13
|
|
|
13
14
|
const canUserPause = (
|
|
14
15
|
displayHints: Array<string>,
|
|
15
16
|
userPolicies: Record<SELF_POLICY, boolean>
|
|
16
17
|
): boolean =>
|
|
17
|
-
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_PAUSE)
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_PAUSE) &&
|
|
19
|
+
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies)) ||
|
|
20
|
+
(displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_PAUSE) &&
|
|
21
|
+
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_PREMISE_RECORD, userPolicies));
|
|
20
22
|
|
|
21
23
|
const canUserResume = (
|
|
22
24
|
displayHints: Array<string>,
|
|
23
25
|
userPolicies: Record<SELF_POLICY, boolean>
|
|
24
26
|
): boolean =>
|
|
25
|
-
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_RESUME)
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_RESUME) &&
|
|
28
|
+
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies)) ||
|
|
29
|
+
(displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_RESUME) &&
|
|
30
|
+
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_PREMISE_RECORD, userPolicies));
|
|
28
31
|
|
|
29
32
|
const canUserStop = (
|
|
30
33
|
displayHints: Array<string>,
|
|
31
34
|
userPolicies: Record<SELF_POLICY, boolean>
|
|
32
35
|
): boolean =>
|
|
33
|
-
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_STOP)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_STOP) &&
|
|
37
|
+
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies)) ||
|
|
38
|
+
(displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_STOP) &&
|
|
39
|
+
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_PREMISE_RECORD, userPolicies));
|
|
36
40
|
|
|
37
41
|
const isPremiseRecordingEnabled = (
|
|
38
42
|
displayHints: Array<string>,
|
|
@@ -42,7 +46,7 @@ const isPremiseRecordingEnabled = (
|
|
|
42
46
|
displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_PAUSE) ||
|
|
43
47
|
displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_STOP) ||
|
|
44
48
|
displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_RESUME)) &&
|
|
45
|
-
MeetingUtil.selfSupportsFeature(SELF_POLICY.
|
|
49
|
+
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_PREMISE_RECORD, userPolicies);
|
|
46
50
|
|
|
47
51
|
const extractLocusId = (url: string) => {
|
|
48
52
|
return url?.split('/').pop();
|
|
@@ -22,16 +22,54 @@ describe('plugin-meetings', () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
describe('#initialize', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
interpretation.querySupportLanguages = sinon.stub();
|
|
27
|
+
interpretation.set({
|
|
28
|
+
canManageInterpreters: undefined,
|
|
29
|
+
hostSIEnabled: undefined,
|
|
30
|
+
locusUrl: undefined
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
interpretation.querySupportLanguages.reset();
|
|
36
|
+
});
|
|
37
|
+
|
|
25
38
|
it('creates SimultaneousInterpretation as expected', () => {
|
|
26
39
|
assert.equal(interpretation.namespace, 'Meetings');
|
|
27
40
|
});
|
|
28
41
|
it('call querySupportLanguages correctly when meet the conditions', () => {
|
|
29
|
-
interpretation.querySupportLanguages = sinon.stub();
|
|
30
42
|
interpretation.set({
|
|
31
43
|
canManageInterpreters: true,
|
|
44
|
+
hostSIEnabled: true,
|
|
45
|
+
locusUrl: "MOCK_LOCUS_URL"
|
|
32
46
|
});
|
|
33
47
|
assert.called(interpretation.querySupportLanguages);
|
|
34
48
|
});
|
|
49
|
+
|
|
50
|
+
it('does not call querySupportLanguages when canManageInterpreters is not set', () => {
|
|
51
|
+
interpretation.set({
|
|
52
|
+
hostSIEnabled: true,
|
|
53
|
+
locusUrl: "MOCK_LOCUS_URL"
|
|
54
|
+
});
|
|
55
|
+
assert.notCalled(interpretation.querySupportLanguages);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not call querySupportLanguages when hostSIEnabled is not set', () => {
|
|
59
|
+
interpretation.set({
|
|
60
|
+
canManageInterpreters: true,
|
|
61
|
+
locusUrl: "MOCK_LOCUS_URL"
|
|
62
|
+
});
|
|
63
|
+
assert.notCalled(interpretation.querySupportLanguages);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not call querySupportLanguages when locusUrl is not set', () => {
|
|
67
|
+
interpretation.set({
|
|
68
|
+
canManageInterpreters: true,
|
|
69
|
+
hostSIEnabled: true,
|
|
70
|
+
});
|
|
71
|
+
assert.notCalled(interpretation.querySupportLanguages);
|
|
72
|
+
});
|
|
35
73
|
});
|
|
36
74
|
|
|
37
75
|
describe('#cleanUp', () => {
|
|
@@ -269,6 +269,14 @@ describe('plugin-meetings', () => {
|
|
|
269
269
|
assert.equal(updates.hasPracticeSessionEnabledChanged, true);
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
+
it('returns hasPracticeSessionEnabledChanged = false when enabled is false and previous state is false', () => {
|
|
273
|
+
const newControls = {practiceSession: {enabled: false}};
|
|
274
|
+
|
|
275
|
+
const {updates} = ControlsUtils.getControls(defaultControls, newControls);
|
|
276
|
+
|
|
277
|
+
assert.equal(updates.hasPracticeSessionEnabledChanged, false);
|
|
278
|
+
});
|
|
279
|
+
|
|
272
280
|
it('returns hasEntryExitToneChanged = true when mode changed', () => {
|
|
273
281
|
const newControls = {
|
|
274
282
|
entryExitTone: {
|