@webex/plugin-meetings 3.12.0-next.91 → 3.12.0-next.93
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/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/common/errors/webex-errors.js +47 -5
- package/dist/common/errors/webex-errors.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +14 -5
- package/dist/locus-info/index.js.map +1 -1
- package/dist/media/MediaConnectionAwaiter.js +15 -2
- package/dist/media/MediaConnectionAwaiter.js.map +1 -1
- package/dist/media/index.js +5 -1
- package/dist/media/index.js.map +1 -1
- package/dist/meeting/index.js +458 -365
- package/dist/meeting/index.js.map +1 -1
- package/dist/reachability/clusterReachability.js.map +1 -1
- package/dist/reachability/index.js +111 -70
- package/dist/reachability/index.js.map +1 -1
- package/dist/types/common/errors/webex-errors.d.ts +31 -2
- package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
- package/dist/types/meeting/index.d.ts +18 -3
- package/dist/types/reachability/clusterReachability.d.ts +3 -3
- package/dist/types/reachability/index.d.ts +9 -2
- package/dist/webinar/index.js +1 -1
- package/package.json +3 -3
- package/src/common/errors/webex-errors.ts +47 -3
- package/src/locus-info/index.ts +15 -5
- package/src/media/MediaConnectionAwaiter.ts +21 -4
- package/src/media/index.ts +7 -1
- package/src/meeting/index.ts +100 -27
- package/src/reachability/clusterReachability.ts +3 -2
- package/src/reachability/index.ts +28 -1
- package/test/unit/spec/common/errors/webex-errors.ts +62 -0
- package/test/unit/spec/locus-info/index.js +44 -0
- package/test/unit/spec/media/MediaConnectionAwaiter.ts +55 -0
- package/test/unit/spec/media/index.ts +61 -0
- package/test/unit/spec/meeting/index.js +290 -8
- package/test/unit/spec/reachability/index.ts +69 -0
|
@@ -157,19 +157,63 @@ WebExMeetingsErrors[IceGatheringFailed.CODE] = IceGatheringFailed;
|
|
|
157
157
|
class AddMediaFailed extends WebexMeetingsError {
|
|
158
158
|
static CODE = 30203;
|
|
159
159
|
cause?: Error;
|
|
160
|
+
connectionType?: string;
|
|
161
|
+
selectedCandidatePairChanges?: number;
|
|
162
|
+
numTransports?: number;
|
|
163
|
+
iceConnected?: boolean;
|
|
160
164
|
|
|
161
165
|
/**
|
|
162
166
|
* Creates a new AddMediaFailed error
|
|
163
|
-
* @param {
|
|
167
|
+
* @param {Object} options
|
|
168
|
+
* @param {Error} [options.cause] - The underlying error that caused the media addition to fail
|
|
169
|
+
* @param {string} [options.connectionType] - The ICE connection type at the time of failure (e.g. 'UDP', 'TURN-TLS')
|
|
170
|
+
* @param {number} [options.selectedCandidatePairChanges] - Number of times the selected candidate pair changed
|
|
171
|
+
* @param {number} [options.numTransports] - Number of ICE transports
|
|
172
|
+
* @param {boolean} [options.iceConnected] - Whether ICE connection was established before the failure
|
|
164
173
|
*/
|
|
165
|
-
constructor(
|
|
174
|
+
constructor(
|
|
175
|
+
options: {
|
|
176
|
+
cause?: Error;
|
|
177
|
+
connectionType?: string;
|
|
178
|
+
selectedCandidatePairChanges?: number;
|
|
179
|
+
numTransports?: number;
|
|
180
|
+
iceConnected?: boolean;
|
|
181
|
+
} = {}
|
|
182
|
+
) {
|
|
166
183
|
super(AddMediaFailed.CODE, 'Failed to add media');
|
|
167
|
-
this.cause = cause;
|
|
184
|
+
this.cause = options.cause;
|
|
185
|
+
this.connectionType = options.connectionType;
|
|
186
|
+
this.selectedCandidatePairChanges = options.selectedCandidatePairChanges;
|
|
187
|
+
this.numTransports = options.numTransports;
|
|
188
|
+
this.iceConnected = options.iceConnected;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Returns true if the failure was due to a DTLS handshake failure
|
|
193
|
+
* (ICE connected successfully but the overall media connection failed).
|
|
194
|
+
*/
|
|
195
|
+
get isDtlsHandshakeFailure(): boolean {
|
|
196
|
+
return this.iceConnected === true;
|
|
168
197
|
}
|
|
169
198
|
}
|
|
170
199
|
export {AddMediaFailed};
|
|
171
200
|
WebExMeetingsErrors[AddMediaFailed.CODE] = AddMediaFailed;
|
|
172
201
|
|
|
202
|
+
/**
|
|
203
|
+
* @class MediaConnectionTimedOutError
|
|
204
|
+
* @classdesc Internal error thrown when the media connection times out waiting to connect.
|
|
205
|
+
*/
|
|
206
|
+
class MediaConnectionTimedOutError extends Error {
|
|
207
|
+
iceConnected: boolean;
|
|
208
|
+
|
|
209
|
+
constructor(message: string, iceConnected: boolean) {
|
|
210
|
+
super(message);
|
|
211
|
+
this.name = 'MediaConnectionTimedOutError';
|
|
212
|
+
this.iceConnected = iceConnected;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export {MediaConnectionTimedOutError};
|
|
216
|
+
|
|
173
217
|
/**
|
|
174
218
|
* @class SdpResponseTimeoutError
|
|
175
219
|
* @classdesc Raised whenever we timeout waiting for remote SDP answer
|
package/src/locus-info/index.ts
CHANGED
|
@@ -518,7 +518,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
518
518
|
this.updateControls(locus.controls, locus.self);
|
|
519
519
|
this.updateLocusUrl(locus.url, ControlsUtils.isMainSessionDTO(locus));
|
|
520
520
|
this.updateFullState(locus.fullState);
|
|
521
|
-
this.updateMeetingInfo(locus.info);
|
|
521
|
+
this.updateMeetingInfo(locus.info, locus.self);
|
|
522
522
|
this.updateEmbeddedApps(locus.embeddedApps);
|
|
523
523
|
// self and participants generate sipUrl for 1:1 meeting
|
|
524
524
|
this.updateSelf(locus.self);
|
|
@@ -2491,9 +2491,19 @@ export default class LocusInfo extends EventsScope {
|
|
|
2491
2491
|
*/
|
|
2492
2492
|
updateMeetingInfo(info: object, self?: object) {
|
|
2493
2493
|
const roles = self ? SelfUtils.getRoles(self) : this.parsedLocus.self?.roles || [];
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2494
|
+
const isJoined = SelfUtils.isJoined(self || this.parsedLocus.self);
|
|
2495
|
+
|
|
2496
|
+
// The parsed userDisplayHints depend on info, roles and isJoined, so we must recompute
|
|
2497
|
+
// whenever any of them changes. A common case is self transitioning to JOINED via a delta
|
|
2498
|
+
// that doesn't carry an info section - in that case we fall back to the previously stored
|
|
2499
|
+
// info so the hints get reparsed with the new joined state (e.g. VIEW_THE_PARTICIPANT_LIST).
|
|
2500
|
+
const infoToParse = info || this.info;
|
|
2501
|
+
const infoChanged = info && !isEqual(this.info, info);
|
|
2502
|
+
const rolesChanged = !isEqual(this.roles, roles);
|
|
2503
|
+
const isJoinedChanged = SelfUtils.isJoined(this.parsedLocus.self) !== isJoined;
|
|
2504
|
+
|
|
2505
|
+
if (infoToParse && (infoChanged || rolesChanged || isJoinedChanged)) {
|
|
2506
|
+
const parsedInfo = InfoUtils.getInfos(this.parsedLocus.info, infoToParse, roles, isJoined);
|
|
2497
2507
|
|
|
2498
2508
|
if (parsedInfo.updates.isLocked) {
|
|
2499
2509
|
this.emitScoped(
|
|
@@ -2516,7 +2526,7 @@ export default class LocusInfo extends EventsScope {
|
|
|
2516
2526
|
);
|
|
2517
2527
|
}
|
|
2518
2528
|
|
|
2519
|
-
this.info =
|
|
2529
|
+
this.info = infoToParse;
|
|
2520
2530
|
this.parsedLocus.info = parsedInfo.current;
|
|
2521
2531
|
// Parses the info and adds necessary values
|
|
2522
2532
|
this.updateMeeting(parsedInfo.current);
|
|
@@ -10,6 +10,10 @@ export interface MediaConnectionAwaiterProps {
|
|
|
10
10
|
correlationId: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export interface FailureResult {
|
|
14
|
+
iceConnected: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* @class MediaConnectionAwaiter
|
|
15
19
|
*/
|
|
@@ -58,6 +62,17 @@ export default class MediaConnectionAwaiter {
|
|
|
58
62
|
return this.webrtcMediaConnection.getConnectionState() === ConnectionState.Failed;
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Returns true if ICE connection state indicates connectivity.
|
|
67
|
+
*
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
private isIceConnected(): boolean {
|
|
71
|
+
const state = this.webrtcMediaConnection.getIceConnectionState();
|
|
72
|
+
|
|
73
|
+
return state === 'connected' || state === 'completed';
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
/**
|
|
62
77
|
* Returns true if the ICE Gathering is completed, false otherwise.
|
|
63
78
|
*
|
|
@@ -105,7 +120,7 @@ export default class MediaConnectionAwaiter {
|
|
|
105
120
|
|
|
106
121
|
this.defer.reject({
|
|
107
122
|
iceConnected: this.iceConnected,
|
|
108
|
-
});
|
|
123
|
+
} satisfies FailureResult);
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
if (!this.isConnected()) {
|
|
@@ -148,7 +163,7 @@ export default class MediaConnectionAwaiter {
|
|
|
148
163
|
`Media:MediaConnectionAwaiter#iceConnectionStateHandler --> ICE connection state change -> ${iceConnectionState}`
|
|
149
164
|
);
|
|
150
165
|
|
|
151
|
-
if (
|
|
166
|
+
if (this.isIceConnected() && !this.iceConnected) {
|
|
152
167
|
this.iceConnected = true;
|
|
153
168
|
}
|
|
154
169
|
|
|
@@ -249,13 +264,13 @@ export default class MediaConnectionAwaiter {
|
|
|
249
264
|
|
|
250
265
|
this.defer.reject({
|
|
251
266
|
iceConnected: this.iceConnected,
|
|
252
|
-
});
|
|
267
|
+
} satisfies FailureResult);
|
|
253
268
|
}
|
|
254
269
|
|
|
255
270
|
/**
|
|
256
271
|
* Waits for the webrtc media connection to be connected.
|
|
257
272
|
*
|
|
258
|
-
* @returns {Promise}
|
|
273
|
+
* @returns {Promise<void>} In case of failure, the promise is rejected with FailureResult
|
|
259
274
|
*/
|
|
260
275
|
waitForMediaConnectionConnected(): Promise<void> {
|
|
261
276
|
if (this.isConnected()) {
|
|
@@ -269,6 +284,8 @@ export default class MediaConnectionAwaiter {
|
|
|
269
284
|
'Media:MediaConnectionAwaiter#waitForMediaConnectionConnected --> Waiting for media connection to be connected'
|
|
270
285
|
);
|
|
271
286
|
|
|
287
|
+
this.iceConnected = this.isIceConnected();
|
|
288
|
+
|
|
272
289
|
this.webrtcMediaConnection.on(
|
|
273
290
|
MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED,
|
|
274
291
|
this.peerConnectionStateCallback
|
package/src/media/index.ts
CHANGED
|
@@ -142,6 +142,7 @@ Media.createMediaConnection = (
|
|
|
142
142
|
enableExtmap?: boolean;
|
|
143
143
|
turnServerInfo?: TurnServerInfo;
|
|
144
144
|
bundlePolicy?: BundlePolicy;
|
|
145
|
+
iceTransportPolicy?: RTCIceTransportPolicy;
|
|
145
146
|
iceCandidatesTimeout?: number;
|
|
146
147
|
disableAudioMainDtx?: boolean;
|
|
147
148
|
enableAudioTwcc?: boolean;
|
|
@@ -157,6 +158,7 @@ Media.createMediaConnection = (
|
|
|
157
158
|
enableExtmap,
|
|
158
159
|
turnServerInfo,
|
|
159
160
|
bundlePolicy,
|
|
161
|
+
iceTransportPolicy,
|
|
160
162
|
iceCandidatesTimeout,
|
|
161
163
|
disableAudioMainDtx,
|
|
162
164
|
enableAudioTwcc,
|
|
@@ -167,7 +169,7 @@ Media.createMediaConnection = (
|
|
|
167
169
|
const iceServers = [];
|
|
168
170
|
|
|
169
171
|
// we might not have any TURN server if TURN discovery failed or wasn't done or we land on a video mesh node
|
|
170
|
-
if (turnServerInfo
|
|
172
|
+
if (turnServerInfo && turnServerInfo.urls.length > 0) {
|
|
171
173
|
// TURN-TLS server
|
|
172
174
|
iceServers.push({
|
|
173
175
|
urls: turnServerInfo.urls,
|
|
@@ -187,6 +189,10 @@ Media.createMediaConnection = (
|
|
|
187
189
|
config.bundlePolicy = bundlePolicy;
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
if (iceTransportPolicy) {
|
|
193
|
+
config.iceTransportPolicy = iceTransportPolicy;
|
|
194
|
+
}
|
|
195
|
+
|
|
190
196
|
if (disableAudioMainDtx !== undefined) {
|
|
191
197
|
config.disableAudioMainDtx = disableAudioMainDtx;
|
|
192
198
|
}
|
package/src/meeting/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
NoMediaEstablishedYetError,
|
|
67
67
|
UserNotJoinedError,
|
|
68
68
|
AddMediaFailed,
|
|
69
|
+
MediaConnectionTimedOutError,
|
|
69
70
|
SdpResponseTimeoutError,
|
|
70
71
|
} from '../common/errors/webex-errors';
|
|
71
72
|
|
|
@@ -76,6 +77,7 @@ import Roap, {type TurnDiscoveryResult, type TurnDiscoverySkipReason} from '../r
|
|
|
76
77
|
import {type TurnServerInfo} from '../roap/types';
|
|
77
78
|
import Media, {type BundlePolicy} from '../media';
|
|
78
79
|
import MediaProperties from '../media/properties';
|
|
80
|
+
import {type FailureResult as MediaConnectionAwaiterFailureResult} from '../media/MediaConnectionAwaiter';
|
|
79
81
|
import MeetingStateMachine from './state';
|
|
80
82
|
import {createMuteState} from './muteState';
|
|
81
83
|
import LocusInfo, {LocusLLMEvent} from '../locus-info';
|
|
@@ -790,6 +792,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
790
792
|
private addMediaData: {
|
|
791
793
|
retriedWithTurnServer: boolean;
|
|
792
794
|
icePhaseCallback: () => string;
|
|
795
|
+
iceTransportPolicy?: RTCIceTransportPolicy;
|
|
793
796
|
};
|
|
794
797
|
|
|
795
798
|
private sendSlotManager: SendSlotManager = new SendSlotManager(LoggerProxy);
|
|
@@ -5685,6 +5688,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5685
5688
|
joined = true;
|
|
5686
5689
|
}
|
|
5687
5690
|
|
|
5691
|
+
const shouldRetryMediaWithOnlyTurnTLS = await this.shouldRetryMediaWithOnlyTurnTLS(prevError);
|
|
5692
|
+
|
|
5688
5693
|
const mediaResponse = await this.addMediaInternal(
|
|
5689
5694
|
() => {
|
|
5690
5695
|
// callback is not called when UserNotJoinedError is thrown
|
|
@@ -5692,8 +5697,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5692
5697
|
? 'JOIN_MEETING_FINAL'
|
|
5693
5698
|
: 'JOIN_MEETING_RETRY';
|
|
5694
5699
|
},
|
|
5695
|
-
turnServerInfo,
|
|
5696
5700
|
forceTurnDiscovery,
|
|
5701
|
+
turnServerInfo,
|
|
5702
|
+
shouldRetryMediaWithOnlyTurnTLS ? 'relay' : undefined,
|
|
5697
5703
|
mediaOptions
|
|
5698
5704
|
);
|
|
5699
5705
|
|
|
@@ -7907,17 +7913,54 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7907
7913
|
return `MC-${this.id.substring(0, 4)}`;
|
|
7908
7914
|
}
|
|
7909
7915
|
|
|
7916
|
+
/**
|
|
7917
|
+
* Determines if the next media attempt should use only TURN-TLS (iceTransportPolicy='relay').
|
|
7918
|
+
* We do this in the hope that it will minimize the chance of Homer sending DTLS packets on a wrong
|
|
7919
|
+
* transport (as there will be only 1 transport) and dealing with firewalls that let STUN packets through, but block
|
|
7920
|
+
* other UDP traffic.
|
|
7921
|
+
*
|
|
7922
|
+
* For clients landing on video mesh nodes (VMN), reachability might still have successful TLS (to public nodes)
|
|
7923
|
+
* but TURN discovery will result in empty TURN urls array and iceTransportPolicy='relay' will still be overriden to undefined,
|
|
7924
|
+
* see the code in establishMediaConnection()
|
|
7925
|
+
*
|
|
7926
|
+
* @param {Error} prevError - The error from the previous addMedia attempt
|
|
7927
|
+
* @returns {Promise<boolean>}
|
|
7928
|
+
*/
|
|
7929
|
+
private async shouldRetryMediaWithOnlyTurnTLS(prevError: Error | undefined): Promise<boolean> {
|
|
7930
|
+
if (
|
|
7931
|
+
this.isMultistream &&
|
|
7932
|
+
prevError instanceof AddMediaFailed &&
|
|
7933
|
+
prevError.isDtlsHandshakeFailure &&
|
|
7934
|
+
prevError.connectionType === 'UDP'
|
|
7935
|
+
) {
|
|
7936
|
+
const hasTlsReachability =
|
|
7937
|
+
// @ts-ignore
|
|
7938
|
+
await this.webex.meetings.reachability.isAnyClusterReachableViaProtocol('xtls');
|
|
7939
|
+
|
|
7940
|
+
if (hasTlsReachability) {
|
|
7941
|
+
LoggerProxy.logger.info(
|
|
7942
|
+
'Meeting:index#shouldRetryMediaWithOnlyTurnTLS --> previous attempt failed due to DTLS handshake failure over UDP and TLS reachability is available'
|
|
7943
|
+
);
|
|
7944
|
+
|
|
7945
|
+
return true;
|
|
7946
|
+
}
|
|
7947
|
+
}
|
|
7948
|
+
|
|
7949
|
+
return false;
|
|
7950
|
+
}
|
|
7951
|
+
|
|
7910
7952
|
/**
|
|
7911
7953
|
* Creates a webrtc media connection and publishes streams to it
|
|
7912
7954
|
*
|
|
7913
7955
|
* @param {Object} turnServerInfo TURN server information
|
|
7914
7956
|
* @param {BundlePolicy} [bundlePolicy] Bundle policy settings
|
|
7915
|
-
* @param {
|
|
7957
|
+
* @param {RTCIceTransportPolicy} [iceTransportPolicy] ICE transport policy override
|
|
7916
7958
|
* @returns {RoapMediaConnection | MultistreamRoapMediaConnection}
|
|
7917
7959
|
*/
|
|
7918
7960
|
private async createMediaConnection(
|
|
7919
7961
|
turnServerInfo?: TurnServerInfo,
|
|
7920
|
-
bundlePolicy?: BundlePolicy
|
|
7962
|
+
bundlePolicy?: BundlePolicy,
|
|
7963
|
+
iceTransportPolicy?: RTCIceTransportPolicy
|
|
7921
7964
|
) {
|
|
7922
7965
|
this.rtcMetrics = this.isMultistream
|
|
7923
7966
|
? // @ts-ignore
|
|
@@ -7941,6 +7984,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
7941
7984
|
enableExtmap: this.config.enableExtmap,
|
|
7942
7985
|
turnServerInfo,
|
|
7943
7986
|
bundlePolicy,
|
|
7987
|
+
iceTransportPolicy,
|
|
7944
7988
|
// @ts-ignore - config coming from registerPlugin
|
|
7945
7989
|
iceCandidatesTimeout: this.config.iceCandidatesGatheringTimeout,
|
|
7946
7990
|
// @ts-ignore - config coming from registerPlugin
|
|
@@ -8067,7 +8111,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8067
8111
|
try {
|
|
8068
8112
|
await this.mediaProperties.waitForMediaConnectionConnected(this.correlationId);
|
|
8069
8113
|
} catch (error) {
|
|
8070
|
-
const {iceConnected} = error;
|
|
8114
|
+
const {iceConnected} = error as MediaConnectionAwaiterFailureResult;
|
|
8071
8115
|
|
|
8072
8116
|
if (!this.hasMediaConnectionConnectedAtLeastOnce) {
|
|
8073
8117
|
// Only send CA event for join flow if we haven't successfully connected media yet
|
|
@@ -8107,13 +8151,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8107
8151
|
});
|
|
8108
8152
|
}
|
|
8109
8153
|
|
|
8110
|
-
|
|
8111
|
-
`Timed out waiting for media connection to be connected, correlationId=${this.correlationId}
|
|
8154
|
+
throw new MediaConnectionTimedOutError(
|
|
8155
|
+
`Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`,
|
|
8156
|
+
iceConnected
|
|
8112
8157
|
);
|
|
8113
|
-
|
|
8114
|
-
timedOutError.cause = error;
|
|
8115
|
-
|
|
8116
|
-
throw timedOutError;
|
|
8117
8158
|
}
|
|
8118
8159
|
}
|
|
8119
8160
|
|
|
@@ -8326,7 +8367,18 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8326
8367
|
error
|
|
8327
8368
|
);
|
|
8328
8369
|
|
|
8329
|
-
|
|
8370
|
+
const {connectionType, selectedCandidatePairChanges, numTransports} =
|
|
8371
|
+
await this.mediaProperties.getCurrentConnectionInfo();
|
|
8372
|
+
|
|
8373
|
+
const iceConnected = error instanceof MediaConnectionTimedOutError && error.iceConnected;
|
|
8374
|
+
|
|
8375
|
+
throw new AddMediaFailed({
|
|
8376
|
+
cause: error,
|
|
8377
|
+
connectionType,
|
|
8378
|
+
selectedCandidatePairChanges,
|
|
8379
|
+
numTransports,
|
|
8380
|
+
iceConnected,
|
|
8381
|
+
});
|
|
8330
8382
|
}
|
|
8331
8383
|
}
|
|
8332
8384
|
|
|
@@ -8401,7 +8453,21 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8401
8453
|
({turnServerInfo} = await this.doTurnDiscovery(isReconnecting, isForced));
|
|
8402
8454
|
}
|
|
8403
8455
|
|
|
8404
|
-
|
|
8456
|
+
if (
|
|
8457
|
+
(!turnServerInfo || !turnServerInfo.urls?.length) &&
|
|
8458
|
+
this.addMediaData.iceTransportPolicy === 'relay'
|
|
8459
|
+
) {
|
|
8460
|
+
LoggerProxy.logger.info(
|
|
8461
|
+
`${LOG_HEADER} cannot do iceTransportPolicy=relay, because there is no turn server info available`
|
|
8462
|
+
);
|
|
8463
|
+
// drop the forced relay policy and use the default, because we don't have a TURN server
|
|
8464
|
+
this.addMediaData.iceTransportPolicy = undefined;
|
|
8465
|
+
}
|
|
8466
|
+
const mediaConnection = await this.createMediaConnection(
|
|
8467
|
+
turnServerInfo,
|
|
8468
|
+
bundlePolicy,
|
|
8469
|
+
this.addMediaData.iceTransportPolicy
|
|
8470
|
+
);
|
|
8405
8471
|
|
|
8406
8472
|
LoggerProxy.logger.info(
|
|
8407
8473
|
`${LOG_HEADER} media connection created this.isMultistream=${this.isMultistream}`
|
|
@@ -8438,7 +8504,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8438
8504
|
await this.remoteMediaManager.start();
|
|
8439
8505
|
}
|
|
8440
8506
|
|
|
8441
|
-
await
|
|
8507
|
+
await mediaConnection.initiateOffer();
|
|
8442
8508
|
|
|
8443
8509
|
await this.waitForRemoteSDPAnswer();
|
|
8444
8510
|
|
|
@@ -8506,6 +8572,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8506
8572
|
|
|
8507
8573
|
this.isMultistream = false;
|
|
8508
8574
|
|
|
8575
|
+
// reset iceTransportPolicy, because it's only applied for multistream media connections
|
|
8576
|
+
this.addMediaData.iceTransportPolicy = undefined;
|
|
8577
|
+
|
|
8509
8578
|
if (this.mediaProperties.webrtcMediaConnection) {
|
|
8510
8579
|
// close peer connection, but don't reset mute state information, because we will want to use it on the retry
|
|
8511
8580
|
this.closePeerConnections(false);
|
|
@@ -8630,8 +8699,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8630
8699
|
addMedia(options: AddMediaOptions = {}): Promise<void> {
|
|
8631
8700
|
return this.addMediaInternal(
|
|
8632
8701
|
() => (this.turnServerUsed ? 'JOIN_MEETING_FINAL' : 'JOIN_MEETING_RETRY'),
|
|
8633
|
-
undefined,
|
|
8634
8702
|
false,
|
|
8703
|
+
undefined /* turnServerInfo - it will be fetched via doTurnDiscovery() */,
|
|
8704
|
+
undefined /* iceTransportPolicy - we're relying on the default value */,
|
|
8635
8705
|
options
|
|
8636
8706
|
);
|
|
8637
8707
|
}
|
|
@@ -8640,8 +8710,9 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8640
8710
|
* Internal version of addMedia() with some more arguments for finer control of its behavior
|
|
8641
8711
|
*
|
|
8642
8712
|
* @param {Function} icePhaseCallback - callback to determine the icePhase for CA "client.ice.end" failure events
|
|
8643
|
-
* @param {TurnServerInfo} turnServerInfo - TURN server information
|
|
8644
8713
|
* @param {boolean} forceTurnDiscovery - if true, TURN discovery will be done
|
|
8714
|
+
* @param {TurnServerInfo} turnServerInfo - TURN server information
|
|
8715
|
+
* @param {RTCIceTransportPolicy} [iceTransportPolicy] - ICE transport policy override
|
|
8645
8716
|
* @param {AddMediaOptions} options - same as options of the public addMedia() method
|
|
8646
8717
|
* @returns {Promise<void>}
|
|
8647
8718
|
* @protected
|
|
@@ -8649,12 +8720,14 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8649
8720
|
*/
|
|
8650
8721
|
protected async addMediaInternal(
|
|
8651
8722
|
icePhaseCallback: () => string,
|
|
8652
|
-
|
|
8653
|
-
|
|
8723
|
+
forceTurnDiscovery: boolean,
|
|
8724
|
+
turnServerInfo?: TurnServerInfo,
|
|
8725
|
+
iceTransportPolicy?: RTCIceTransportPolicy,
|
|
8654
8726
|
options: AddMediaOptions = {}
|
|
8655
8727
|
): Promise<void> {
|
|
8656
8728
|
this.addMediaData.retriedWithTurnServer = false;
|
|
8657
8729
|
this.addMediaData.icePhaseCallback = icePhaseCallback;
|
|
8730
|
+
this.addMediaData.iceTransportPolicy = iceTransportPolicy;
|
|
8658
8731
|
|
|
8659
8732
|
this.hasMediaConnectionConnectedAtLeastOnce = false;
|
|
8660
8733
|
const LOG_HEADER = 'Meeting:index#addMedia -->';
|
|
@@ -8663,7 +8736,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8663
8736
|
options
|
|
8664
8737
|
)}, turnServerInfo=${JSON.stringify(
|
|
8665
8738
|
turnServerInfo
|
|
8666
|
-
)}, forceTurnDiscovery=${forceTurnDiscovery}`
|
|
8739
|
+
)}, forceTurnDiscovery=${forceTurnDiscovery}, iceTransportPolicy=${iceTransportPolicy}`
|
|
8667
8740
|
);
|
|
8668
8741
|
|
|
8669
8742
|
if (options.allowMediaInLobby !== true && this.meetingState !== FULL_STATE.ACTIVE) {
|
|
@@ -8777,12 +8850,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8777
8850
|
|
|
8778
8851
|
// Establish new media connection with forced TURN discovery
|
|
8779
8852
|
// We need to do TURN discovery again, because backend will be creating a new confluence, so it might land on a different node or cluster
|
|
8780
|
-
await this.establishMediaConnection(
|
|
8781
|
-
remoteMediaManagerConfig,
|
|
8782
|
-
bundlePolicy,
|
|
8783
|
-
true,
|
|
8784
|
-
undefined
|
|
8785
|
-
);
|
|
8853
|
+
await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy, true);
|
|
8786
8854
|
} else {
|
|
8787
8855
|
throw error;
|
|
8788
8856
|
}
|
|
@@ -8812,6 +8880,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8812
8880
|
isMultistream: this.isMultistream,
|
|
8813
8881
|
retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
|
|
8814
8882
|
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.retryCount > 0,
|
|
8883
|
+
iceTransportPolicy: this.addMediaData.iceTransportPolicy || 'all',
|
|
8815
8884
|
...reachabilityMetrics,
|
|
8816
8885
|
...iceCandidateErrors,
|
|
8817
8886
|
iceCandidatesCount: this.iceCandidatesCount,
|
|
@@ -8833,14 +8902,16 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8833
8902
|
// We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
|
|
8834
8903
|
this.remoteMediaManager?.logAllReceiveSlots();
|
|
8835
8904
|
this.startPeriodicLogUpload();
|
|
8836
|
-
} catch (error) {
|
|
8905
|
+
} catch (error: any) {
|
|
8837
8906
|
LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
|
|
8838
8907
|
|
|
8839
8908
|
// @ts-ignore
|
|
8840
8909
|
const reachabilityMetrics = await this.getMediaReachabilityMetricFields();
|
|
8841
8910
|
|
|
8842
|
-
const {selectedCandidatePairChanges, numTransports} =
|
|
8843
|
-
|
|
8911
|
+
const {connectionType, selectedCandidatePairChanges, numTransports} =
|
|
8912
|
+
error instanceof AddMediaFailed
|
|
8913
|
+
? error
|
|
8914
|
+
: await this.mediaProperties.getCurrentConnectionInfo();
|
|
8844
8915
|
|
|
8845
8916
|
const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);
|
|
8846
8917
|
|
|
@@ -8857,6 +8928,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8857
8928
|
retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
|
|
8858
8929
|
isMultistream: this.isMultistream,
|
|
8859
8930
|
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.retryCount > 0,
|
|
8931
|
+
iceTransportPolicy: this.addMediaData.iceTransportPolicy || 'all',
|
|
8860
8932
|
signalingState:
|
|
8861
8933
|
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
8862
8934
|
?.signalingState ||
|
|
@@ -8872,6 +8944,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8872
8944
|
?.iceConnectionState ||
|
|
8873
8945
|
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
|
|
8874
8946
|
'unknown',
|
|
8947
|
+
connectionType,
|
|
8875
8948
|
...reachabilityMetrics,
|
|
8876
8949
|
...iceCandidateErrors,
|
|
8877
8950
|
iceCandidatesCount: this.iceCandidatesCount,
|
|
@@ -6,13 +6,14 @@ import {Enum} from '../constants';
|
|
|
6
6
|
import {
|
|
7
7
|
ClusterReachabilityResult,
|
|
8
8
|
NatType,
|
|
9
|
+
Protocol,
|
|
9
10
|
ReachabilityPeerConnectionEvents,
|
|
10
11
|
} from './reachability.types';
|
|
11
12
|
import {ReachabilityPeerConnection} from './reachabilityPeerConnection';
|
|
12
13
|
|
|
13
14
|
// data for the Events.resultReady event
|
|
14
15
|
export type ResultEventData = {
|
|
15
|
-
protocol:
|
|
16
|
+
protocol: Protocol;
|
|
16
17
|
result: 'reachable' | 'unreachable' | 'untested';
|
|
17
18
|
latencyInMilliseconds: number; // amount of time it took to get the ICE candidate
|
|
18
19
|
clientMediaIPs?: string[];
|
|
@@ -20,7 +21,7 @@ export type ResultEventData = {
|
|
|
20
21
|
|
|
21
22
|
// data for the Events.clientMediaIpsUpdated event
|
|
22
23
|
export type ClientMediaIpsUpdatedEventData = {
|
|
23
|
-
protocol:
|
|
24
|
+
protocol: Protocol;
|
|
24
25
|
clientMediaIPs: string[];
|
|
25
26
|
};
|
|
26
27
|
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
TransportResultForBackend,
|
|
26
26
|
GetClustersTrigger,
|
|
27
27
|
NatType,
|
|
28
|
+
Protocol,
|
|
28
29
|
} from './reachability.types';
|
|
29
30
|
import {
|
|
30
31
|
ClientMediaIpsUpdatedEventData,
|
|
@@ -569,6 +570,32 @@ export default class Reachability extends EventsScope {
|
|
|
569
570
|
return unreachable;
|
|
570
571
|
}
|
|
571
572
|
|
|
573
|
+
/**
|
|
574
|
+
* Checks if any cluster is reachable via the given protocol based on stored reachability results.
|
|
575
|
+
*
|
|
576
|
+
* @param {string} protocol - the protocol to check ('udp', 'tcp', or 'xtls')
|
|
577
|
+
* @returns {Promise<boolean>} true if at least one cluster has a 'reachable' result for the given protocol
|
|
578
|
+
*/
|
|
579
|
+
async isAnyClusterReachableViaProtocol(protocol: Protocol): Promise<boolean> {
|
|
580
|
+
try {
|
|
581
|
+
// @ts-ignore
|
|
582
|
+
const resultsJson = await this.webex.boundedStorage.get(
|
|
583
|
+
this.namespace,
|
|
584
|
+
REACHABILITY.localStorageResult
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
const results: ReachabilityResults = JSON.parse(resultsJson);
|
|
588
|
+
|
|
589
|
+
return Object.values(results).some((result) => result[protocol]?.result === 'reachable');
|
|
590
|
+
} catch (e) {
|
|
591
|
+
LoggerProxy.logger.warn(
|
|
592
|
+
`Reachability:index#isAnyClusterReachableViaProtocol --> Error reading reachability data: ${e}`
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
572
599
|
/**
|
|
573
600
|
* Get list of all unreachable clusters
|
|
574
601
|
* @returns {array} Unreachable clusters
|
|
@@ -701,7 +728,7 @@ export default class Reachability extends EventsScope {
|
|
|
701
728
|
*/
|
|
702
729
|
protected getStatistics(
|
|
703
730
|
results: Array<ClusterReachabilityResult & {isVideoMesh: boolean}>,
|
|
704
|
-
protocol:
|
|
731
|
+
protocol: Protocol,
|
|
705
732
|
isVideoMesh: boolean
|
|
706
733
|
) {
|
|
707
734
|
const values = results
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {assert} from '@webex/test-helper-chai';
|
|
2
|
+
import {AddMediaFailed, MediaConnectionTimedOutError} from '@webex/plugin-meetings/src/common/errors/webex-errors';
|
|
3
|
+
|
|
4
|
+
describe('MediaConnectionTimedOutError', () => {
|
|
5
|
+
[
|
|
6
|
+
{iceConnected: true, message: 'timeout with ice connected'},
|
|
7
|
+
{iceConnected: false, message: 'timeout without ice connected'},
|
|
8
|
+
].forEach(({iceConnected, message}) => {
|
|
9
|
+
it(`stores message and iceConnected=${iceConnected}`, () => {
|
|
10
|
+
const error = new MediaConnectionTimedOutError(message, iceConnected);
|
|
11
|
+
|
|
12
|
+
assert.equal(error.message, message);
|
|
13
|
+
assert.equal(error.iceConnected, iceConnected);
|
|
14
|
+
assert.equal(error.name, 'MediaConnectionTimedOutError');
|
|
15
|
+
assert.instanceOf(error, Error);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('AddMediaFailed', () => {
|
|
21
|
+
it('stores all options properties', () => {
|
|
22
|
+
const cause = new Error('underlying');
|
|
23
|
+
const error = new AddMediaFailed({
|
|
24
|
+
cause,
|
|
25
|
+
connectionType: 'UDP',
|
|
26
|
+
selectedCandidatePairChanges: 3,
|
|
27
|
+
numTransports: 2,
|
|
28
|
+
iceConnected: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
assert.equal(error.cause, cause);
|
|
32
|
+
assert.equal(error.connectionType, 'UDP');
|
|
33
|
+
assert.equal(error.selectedCandidatePairChanges, 3);
|
|
34
|
+
assert.equal(error.numTransports, 2);
|
|
35
|
+
assert.equal(error.iceConnected, true);
|
|
36
|
+
assert.equal(error.message, 'Failed to add media');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('works with default empty options', () => {
|
|
40
|
+
const error = new AddMediaFailed();
|
|
41
|
+
|
|
42
|
+
assert.isUndefined(error.cause);
|
|
43
|
+
assert.isUndefined(error.connectionType);
|
|
44
|
+
assert.isUndefined(error.selectedCandidatePairChanges);
|
|
45
|
+
assert.isUndefined(error.numTransports);
|
|
46
|
+
assert.isUndefined(error.iceConnected);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('isDtlsHandshakeFailure', () => {
|
|
50
|
+
[
|
|
51
|
+
{iceConnected: true, expected: true},
|
|
52
|
+
{iceConnected: false, expected: false},
|
|
53
|
+
{iceConnected: undefined, expected: false},
|
|
54
|
+
].forEach(({iceConnected, expected}) => {
|
|
55
|
+
it(`returns ${expected} when iceConnected=${iceConnected}`, () => {
|
|
56
|
+
const error = new AddMediaFailed({iceConnected});
|
|
57
|
+
|
|
58
|
+
assert.equal(error.isDtlsHandshakeFailure, expected);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|