@webex/plugin-meetings 3.12.0-next.92 → 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/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/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/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
|
@@ -509,6 +509,67 @@ describe('createMediaConnection', () => {
|
|
|
509
509
|
);
|
|
510
510
|
});
|
|
511
511
|
|
|
512
|
+
it('passes iceTransportPolicy to MultistreamRoapMediaConnection when provided', () => {
|
|
513
|
+
const multistreamRoapMediaConnectionConstructorStub = sinon
|
|
514
|
+
.stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
|
|
515
|
+
.returns(fakeRoapMediaConnection);
|
|
516
|
+
|
|
517
|
+
Media.createMediaConnection(true, 'debug string', 'meeting id', {
|
|
518
|
+
mediaProperties: {
|
|
519
|
+
mediaDirection: {
|
|
520
|
+
sendAudio: true,
|
|
521
|
+
sendVideo: true,
|
|
522
|
+
sendShare: false,
|
|
523
|
+
receiveAudio: true,
|
|
524
|
+
receiveVideo: true,
|
|
525
|
+
receiveShare: true,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
iceTransportPolicy: 'relay',
|
|
529
|
+
});
|
|
530
|
+
assert.calledOnce(multistreamRoapMediaConnectionConstructorStub);
|
|
531
|
+
assert.calledWith(
|
|
532
|
+
multistreamRoapMediaConnectionConstructorStub,
|
|
533
|
+
{
|
|
534
|
+
iceServers: [],
|
|
535
|
+
iceTransportPolicy: 'relay',
|
|
536
|
+
disableAudioTwcc: true,
|
|
537
|
+
enableAV1SlidesSupport: false,
|
|
538
|
+
},
|
|
539
|
+
'meeting id'
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('does not pass iceTransportPolicy to MultistreamRoapMediaConnection when undefined', () => {
|
|
544
|
+
const multistreamRoapMediaConnectionConstructorStub = sinon
|
|
545
|
+
.stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
|
|
546
|
+
.returns(fakeRoapMediaConnection);
|
|
547
|
+
|
|
548
|
+
Media.createMediaConnection(true, 'debug string', 'meeting id', {
|
|
549
|
+
mediaProperties: {
|
|
550
|
+
mediaDirection: {
|
|
551
|
+
sendAudio: true,
|
|
552
|
+
sendVideo: true,
|
|
553
|
+
sendShare: false,
|
|
554
|
+
receiveAudio: true,
|
|
555
|
+
receiveVideo: true,
|
|
556
|
+
receiveShare: true,
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
iceTransportPolicy: undefined,
|
|
560
|
+
});
|
|
561
|
+
assert.calledOnce(multistreamRoapMediaConnectionConstructorStub);
|
|
562
|
+
assert.calledWith(
|
|
563
|
+
multistreamRoapMediaConnectionConstructorStub,
|
|
564
|
+
{
|
|
565
|
+
iceServers: [],
|
|
566
|
+
disableAudioTwcc: true,
|
|
567
|
+
enableAV1SlidesSupport: false,
|
|
568
|
+
},
|
|
569
|
+
'meeting id'
|
|
570
|
+
);
|
|
571
|
+
});
|
|
572
|
+
|
|
512
573
|
it('MultistreamRoapMediaConnection disable audio twcc by default', () => {
|
|
513
574
|
const multistreamRoapMediaConnectionConstructorStub = sinon
|
|
514
575
|
.stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
|
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
MeetingNotActiveError,
|
|
95
95
|
UserInLobbyError,
|
|
96
96
|
AddMediaFailed,
|
|
97
|
+
MediaConnectionTimedOutError,
|
|
97
98
|
} from '../../../../src/common/errors/webex-errors';
|
|
98
99
|
import WebExMeetingsErrors from '../../../../src/common/errors/webex-meetings-error';
|
|
99
100
|
import ParameterError from '../../../../src/common/errors/parameter';
|
|
@@ -264,6 +265,7 @@ describe('plugin-meetings', () => {
|
|
|
264
265
|
webex.meetings.uploadLogs = sinon.stub().returns(Promise.resolve());
|
|
265
266
|
webex.meetings.reachability = {
|
|
266
267
|
isAnyPublicClusterReachable: sinon.stub().resolves(true),
|
|
268
|
+
isAnyClusterReachableViaProtocol: sinon.stub().resolves(false),
|
|
267
269
|
getReachabilityResults: sinon.stub().resolves(undefined),
|
|
268
270
|
getReachabilityMetrics: sinon.stub().resolves({}),
|
|
269
271
|
stopReachability: sinon.stub(),
|
|
@@ -513,6 +515,7 @@ describe('plugin-meetings', () => {
|
|
|
513
515
|
localWebex.meetings.uploadLogs = sinon.stub().returns(Promise.resolve());
|
|
514
516
|
localWebex.meetings.reachability = {
|
|
515
517
|
isAnyPublicClusterReachable: sinon.stub().resolves(true),
|
|
518
|
+
isAnyClusterReachableViaProtocol: sinon.stub().resolves(false),
|
|
516
519
|
getReachabilityResults: sinon.stub().resolves(undefined),
|
|
517
520
|
getReachabilityMetrics: sinon.stub().resolves({}),
|
|
518
521
|
stopReachability: sinon.stub(),
|
|
@@ -1015,8 +1018,9 @@ describe('plugin-meetings', () => {
|
|
|
1015
1018
|
assert.calledOnceWithExactly(
|
|
1016
1019
|
meeting.addMediaInternal,
|
|
1017
1020
|
sinon.match.any,
|
|
1018
|
-
fakeTurnServerInfo,
|
|
1019
1021
|
false,
|
|
1022
|
+
fakeTurnServerInfo,
|
|
1023
|
+
undefined,
|
|
1020
1024
|
mediaOptions
|
|
1021
1025
|
);
|
|
1022
1026
|
|
|
@@ -1048,8 +1052,9 @@ describe('plugin-meetings', () => {
|
|
|
1048
1052
|
assert.calledOnceWithExactly(
|
|
1049
1053
|
meeting.addMediaInternal,
|
|
1050
1054
|
sinon.match.any,
|
|
1051
|
-
undefined,
|
|
1052
1055
|
false,
|
|
1056
|
+
undefined,
|
|
1057
|
+
undefined,
|
|
1053
1058
|
mediaOptions
|
|
1054
1059
|
);
|
|
1055
1060
|
|
|
@@ -1083,8 +1088,9 @@ describe('plugin-meetings', () => {
|
|
|
1083
1088
|
assert.calledOnceWithExactly(
|
|
1084
1089
|
meeting.addMediaInternal,
|
|
1085
1090
|
sinon.match.any,
|
|
1086
|
-
undefined,
|
|
1087
1091
|
false,
|
|
1092
|
+
undefined,
|
|
1093
|
+
undefined,
|
|
1088
1094
|
mediaOptions
|
|
1089
1095
|
);
|
|
1090
1096
|
|
|
@@ -1638,8 +1644,9 @@ describe('plugin-meetings', () => {
|
|
|
1638
1644
|
assert.calledWith(
|
|
1639
1645
|
meeting.addMediaInternal.firstCall,
|
|
1640
1646
|
sinon.match.any,
|
|
1641
|
-
fakeTurnServerInfo,
|
|
1642
1647
|
false,
|
|
1648
|
+
fakeTurnServerInfo,
|
|
1649
|
+
undefined,
|
|
1643
1650
|
mediaOptions
|
|
1644
1651
|
);
|
|
1645
1652
|
|
|
@@ -1647,8 +1654,9 @@ describe('plugin-meetings', () => {
|
|
|
1647
1654
|
assert.calledWith(
|
|
1648
1655
|
meeting.addMediaInternal.secondCall,
|
|
1649
1656
|
sinon.match.any,
|
|
1650
|
-
undefined,
|
|
1651
1657
|
true,
|
|
1658
|
+
undefined,
|
|
1659
|
+
undefined,
|
|
1652
1660
|
mediaOptions
|
|
1653
1661
|
);
|
|
1654
1662
|
|
|
@@ -1666,7 +1674,7 @@ describe('plugin-meetings', () => {
|
|
|
1666
1674
|
|
|
1667
1675
|
meeting.addMediaInternal = sinon
|
|
1668
1676
|
.stub()
|
|
1669
|
-
.callsFake((icePhaseCallback,
|
|
1677
|
+
.callsFake((icePhaseCallback, _forceTurnDiscovery, _turnServerInfo) => {
|
|
1670
1678
|
const defer = new Defer();
|
|
1671
1679
|
|
|
1672
1680
|
icePhaseCallbacks.push(icePhaseCallback);
|
|
@@ -1880,6 +1888,7 @@ describe('plugin-meetings', () => {
|
|
|
1880
1888
|
sinon.match.any,
|
|
1881
1889
|
sinon.match.any,
|
|
1882
1890
|
sinon.match.any,
|
|
1891
|
+
sinon.match.any,
|
|
1883
1892
|
sinon.match.has('videoEnabled', false).and(sinon.match.has('allowMediaInLobby', true))
|
|
1884
1893
|
);
|
|
1885
1894
|
});
|
|
@@ -1900,6 +1909,7 @@ describe('plugin-meetings', () => {
|
|
|
1900
1909
|
sinon.match.any,
|
|
1901
1910
|
sinon.match.any,
|
|
1902
1911
|
sinon.match.any,
|
|
1912
|
+
sinon.match.any,
|
|
1903
1913
|
sinon.match.has('audioEnabled', false).and(sinon.match.has('allowMediaInLobby', true))
|
|
1904
1914
|
);
|
|
1905
1915
|
});
|
|
@@ -1921,6 +1931,7 @@ describe('plugin-meetings', () => {
|
|
|
1921
1931
|
sinon.match.any,
|
|
1922
1932
|
sinon.match.any,
|
|
1923
1933
|
sinon.match.any,
|
|
1934
|
+
sinon.match.any,
|
|
1924
1935
|
sinon.match({
|
|
1925
1936
|
sendVideo: true,
|
|
1926
1937
|
receiveVideo: false,
|
|
@@ -1930,6 +1941,97 @@ describe('plugin-meetings', () => {
|
|
|
1930
1941
|
);
|
|
1931
1942
|
});
|
|
1932
1943
|
|
|
1944
|
+
describe('shouldRetryMediaWithOnlyTurnTLS', () => {
|
|
1945
|
+
it('should pass iceTransportPolicy=relay for multistream when previous error is DTLS failure over UDP and TLS is reachable', async () => {
|
|
1946
|
+
meeting.isMultistream = true;
|
|
1947
|
+
const dtlsError = new AddMediaFailed({iceConnected: true, connectionType: 'UDP'});
|
|
1948
|
+
|
|
1949
|
+
webex.meetings.reachability.isAnyClusterReachableViaProtocol = sinon.stub().resolves(true);
|
|
1950
|
+
|
|
1951
|
+
addMediaInternalStub.onFirstCall().rejects(dtlsError);
|
|
1952
|
+
addMediaInternalStub.onSecondCall().resolves(test4);
|
|
1953
|
+
|
|
1954
|
+
await meeting.joinWithMedia({joinOptions, mediaOptions});
|
|
1955
|
+
|
|
1956
|
+
assert.calledWith(
|
|
1957
|
+
meeting.addMediaInternal.secondCall,
|
|
1958
|
+
sinon.match.any,
|
|
1959
|
+
sinon.match.any,
|
|
1960
|
+
sinon.match.any,
|
|
1961
|
+
'relay',
|
|
1962
|
+
mediaOptions
|
|
1963
|
+
);
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
[
|
|
1967
|
+
{
|
|
1968
|
+
title: 'TLS is not reachable',
|
|
1969
|
+
prevError: new AddMediaFailed({iceConnected: true, connectionType: 'UDP'}),
|
|
1970
|
+
tlsReachable: false,
|
|
1971
|
+
isMultistream: true,
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
title: 'error is not AddMediaFailed',
|
|
1975
|
+
prevError: new Error('generic error'),
|
|
1976
|
+
tlsReachable: true,
|
|
1977
|
+
isMultistream: true,
|
|
1978
|
+
},
|
|
1979
|
+
{
|
|
1980
|
+
title: 'error is AddMediaFailed but not DTLS failure',
|
|
1981
|
+
prevError: new AddMediaFailed({iceConnected: false, connectionType: 'UDP'}),
|
|
1982
|
+
tlsReachable: true,
|
|
1983
|
+
isMultistream: true,
|
|
1984
|
+
},
|
|
1985
|
+
{
|
|
1986
|
+
title: 'error is DTLS failure but connectionType is not UDP',
|
|
1987
|
+
prevError: new AddMediaFailed({iceConnected: true, connectionType: 'TURN-TLS'}),
|
|
1988
|
+
tlsReachable: true,
|
|
1989
|
+
isMultistream: true,
|
|
1990
|
+
},
|
|
1991
|
+
{
|
|
1992
|
+
title: 'isMultistream is false',
|
|
1993
|
+
prevError: new AddMediaFailed({iceConnected: true, connectionType: 'UDP'}),
|
|
1994
|
+
tlsReachable: true,
|
|
1995
|
+
isMultistream: false,
|
|
1996
|
+
},
|
|
1997
|
+
].forEach(({title, prevError, tlsReachable, isMultistream}) => {
|
|
1998
|
+
it(`should pass iceTransportPolicy=undefined when ${title}`, async () => {
|
|
1999
|
+
meeting.join = sinon.stub().callsFake(() => {
|
|
2000
|
+
meeting.isMultistream = isMultistream;
|
|
2001
|
+
return Promise.resolve(fakeJoinResult);
|
|
2002
|
+
});
|
|
2003
|
+
webex.meetings.reachability.isAnyClusterReachableViaProtocol = sinon.stub().resolves(tlsReachable);
|
|
2004
|
+
|
|
2005
|
+
addMediaInternalStub.onFirstCall().rejects(prevError);
|
|
2006
|
+
addMediaInternalStub.onSecondCall().resolves(test4);
|
|
2007
|
+
|
|
2008
|
+
await meeting.joinWithMedia({joinOptions, mediaOptions});
|
|
2009
|
+
|
|
2010
|
+
assert.calledWith(
|
|
2011
|
+
meeting.addMediaInternal.secondCall,
|
|
2012
|
+
sinon.match.any,
|
|
2013
|
+
sinon.match.any,
|
|
2014
|
+
sinon.match.any,
|
|
2015
|
+
undefined,
|
|
2016
|
+
mediaOptions
|
|
2017
|
+
);
|
|
2018
|
+
});
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
it('should pass iceTransportPolicy=undefined on first attempt (no prevError)', async () => {
|
|
2022
|
+
await meeting.joinWithMedia({joinOptions, mediaOptions});
|
|
2023
|
+
|
|
2024
|
+
assert.calledWith(
|
|
2025
|
+
meeting.addMediaInternal.firstCall,
|
|
2026
|
+
sinon.match.any,
|
|
2027
|
+
sinon.match.any,
|
|
2028
|
+
sinon.match.any,
|
|
2029
|
+
undefined,
|
|
2030
|
+
mediaOptions
|
|
2031
|
+
);
|
|
2032
|
+
});
|
|
2033
|
+
});
|
|
2034
|
+
|
|
1933
2035
|
it('should throw immediately if RTCPeerConnection is not available', async () => {
|
|
1934
2036
|
supportsRTCPeerConnectionStub.returns(CapabilityState.NOT_CAPABLE);
|
|
1935
2037
|
|
|
@@ -3270,9 +3372,11 @@ describe('plugin-meetings', () => {
|
|
|
3270
3372
|
retriedWithTurnServer: false,
|
|
3271
3373
|
isMultistream: false,
|
|
3272
3374
|
isJoinWithMediaRetry: false,
|
|
3375
|
+
iceTransportPolicy: 'all',
|
|
3273
3376
|
signalingState: 'unknown',
|
|
3274
3377
|
connectionState: 'unknown',
|
|
3275
3378
|
iceConnectionState: 'unknown',
|
|
3379
|
+
connectionType: 'udp',
|
|
3276
3380
|
someReachabilityMetric1: 'some value1',
|
|
3277
3381
|
someReachabilityMetric2: 'some value2',
|
|
3278
3382
|
selectedCandidatePairChanges: 2,
|
|
@@ -3398,9 +3502,11 @@ describe('plugin-meetings', () => {
|
|
|
3398
3502
|
retriedWithTurnServer: false,
|
|
3399
3503
|
isMultistream: false,
|
|
3400
3504
|
isJoinWithMediaRetry: false,
|
|
3505
|
+
iceTransportPolicy: 'all',
|
|
3401
3506
|
signalingState: 'unknown',
|
|
3402
3507
|
connectionState: 'unknown',
|
|
3403
3508
|
iceConnectionState: 'unknown',
|
|
3509
|
+
connectionType: 'udp',
|
|
3404
3510
|
someReachabilityMetric1: 'some value1',
|
|
3405
3511
|
someReachabilityMetric2: 'some value2',
|
|
3406
3512
|
selectedCandidatePairChanges: 2,
|
|
@@ -4168,9 +4274,11 @@ describe('plugin-meetings', () => {
|
|
|
4168
4274
|
retriedWithTurnServer: true,
|
|
4169
4275
|
isMultistream: false,
|
|
4170
4276
|
isJoinWithMediaRetry: false,
|
|
4277
|
+
iceTransportPolicy: 'all',
|
|
4171
4278
|
signalingState: 'unknown',
|
|
4172
4279
|
connectionState: 'unknown',
|
|
4173
4280
|
iceConnectionState: 'unknown',
|
|
4281
|
+
connectionType: 'udp',
|
|
4174
4282
|
selectedCandidatePairChanges: 2,
|
|
4175
4283
|
numTransports: 1,
|
|
4176
4284
|
iceCandidatesCount: 0,
|
|
@@ -4195,6 +4303,161 @@ describe('plugin-meetings', () => {
|
|
|
4195
4303
|
assert.isOk(errorThrown);
|
|
4196
4304
|
});
|
|
4197
4305
|
|
|
4306
|
+
[
|
|
4307
|
+
{iceConnected: true, title: 'iceConnected=true'},
|
|
4308
|
+
{iceConnected: false, title: 'iceConnected=false'},
|
|
4309
|
+
].forEach(({iceConnected, title}) => {
|
|
4310
|
+
it(`should propagate ${title} from waitForMediaConnectionConnected rejection to AddMediaFailed`, async () => {
|
|
4311
|
+
webex.meetings.reachability = {
|
|
4312
|
+
isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
|
|
4313
|
+
getReachabilityMetrics: sinon.stub().resolves({}),
|
|
4314
|
+
stopReachability: sinon.stub(),
|
|
4315
|
+
isSubnetReachable: sinon.stub().returns(true),
|
|
4316
|
+
};
|
|
4317
|
+
webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
|
|
4318
|
+
sinon.stub().returns({fatal: true});
|
|
4319
|
+
sinon.stub(CallDiagnosticUtils, 'generateClientErrorCodeForIceFailure').returns(2004);
|
|
4320
|
+
|
|
4321
|
+
meeting.meetingState = 'ACTIVE';
|
|
4322
|
+
meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().rejects({iceConnected});
|
|
4323
|
+
|
|
4324
|
+
meeting.roap.doTurnDiscovery = sinon.stub().returns({
|
|
4325
|
+
turnServerInfo: {urls: ['turns:fake:443'], username: 'u', password: 'p'},
|
|
4326
|
+
turnDiscoverySkippedReason: undefined,
|
|
4327
|
+
});
|
|
4328
|
+
|
|
4329
|
+
const forceRtcMetricsSend = sinon.stub().resolves();
|
|
4330
|
+
Media.createMediaConnection = sinon.stub().returns({
|
|
4331
|
+
close: sinon.stub(),
|
|
4332
|
+
forceRtcMetricsSend,
|
|
4333
|
+
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
|
|
4334
|
+
initiateOffer: sinon.stub().resolves({}),
|
|
4335
|
+
on: sinon.stub(),
|
|
4336
|
+
});
|
|
4337
|
+
|
|
4338
|
+
let thrownError;
|
|
4339
|
+
await meeting.addMedia({mediaSettings: {}}).catch((err) => {
|
|
4340
|
+
thrownError = err;
|
|
4341
|
+
});
|
|
4342
|
+
|
|
4343
|
+
assert.instanceOf(thrownError, AddMediaFailed);
|
|
4344
|
+
assert.equal(thrownError.iceConnected, iceConnected);
|
|
4345
|
+
});
|
|
4346
|
+
});
|
|
4347
|
+
|
|
4348
|
+
describe('iceTransportPolicy=relay handling', () => {
|
|
4349
|
+
let createMediaConnectionStub;
|
|
4350
|
+
|
|
4351
|
+
beforeEach(() => {
|
|
4352
|
+
webex.meetings.reachability = {
|
|
4353
|
+
isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
|
|
4354
|
+
isAnyClusterReachableViaProtocol: sinon.stub().resolves(true),
|
|
4355
|
+
getReachabilityMetrics: sinon.stub().resolves({}),
|
|
4356
|
+
stopReachability: sinon.stub(),
|
|
4357
|
+
isSubnetReachable: sinon.stub().returns(true),
|
|
4358
|
+
};
|
|
4359
|
+
webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
|
|
4360
|
+
sinon.stub().returns({fatal: true});
|
|
4361
|
+
sinon.stub(CallDiagnosticUtils, 'generateClientErrorCodeForIceFailure').returns(2004);
|
|
4362
|
+
|
|
4363
|
+
meeting.meetingState = 'ACTIVE';
|
|
4364
|
+
meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
|
|
4365
|
+
|
|
4366
|
+
createMediaConnectionStub = sinon.stub().returns({
|
|
4367
|
+
close: sinon.stub(),
|
|
4368
|
+
forceRtcMetricsSend: sinon.stub().resolves(),
|
|
4369
|
+
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
|
|
4370
|
+
initiateOffer: sinon.stub().resolves({}),
|
|
4371
|
+
on: sinon.stub(),
|
|
4372
|
+
});
|
|
4373
|
+
Media.createMediaConnection = createMediaConnectionStub;
|
|
4374
|
+
});
|
|
4375
|
+
|
|
4376
|
+
[
|
|
4377
|
+
{
|
|
4378
|
+
title: 'should drop iceTransportPolicy=relay when TURN discovery returns no turn server info',
|
|
4379
|
+
turnServerInfo: undefined,
|
|
4380
|
+
turnDiscoverySkippedReason: 'reachability',
|
|
4381
|
+
},
|
|
4382
|
+
{
|
|
4383
|
+
title: 'should drop iceTransportPolicy=relay when TURN discovery returns turnServerInfo with empty urls',
|
|
4384
|
+
turnServerInfo: {urls: [], username: 'u', password: 'p'},
|
|
4385
|
+
turnDiscoverySkippedReason: undefined,
|
|
4386
|
+
},
|
|
4387
|
+
].forEach(({title, turnServerInfo, turnDiscoverySkippedReason}) => {
|
|
4388
|
+
it(title, async () => {
|
|
4389
|
+
meeting.roap.doTurnDiscovery = sinon.stub().returns({
|
|
4390
|
+
turnServerInfo,
|
|
4391
|
+
turnDiscoverySkippedReason,
|
|
4392
|
+
});
|
|
4393
|
+
|
|
4394
|
+
await meeting.addMediaInternal(
|
|
4395
|
+
() => 'JOIN_MEETING_FINAL',
|
|
4396
|
+
false,
|
|
4397
|
+
undefined,
|
|
4398
|
+
'relay',
|
|
4399
|
+
{mediaSettings: {}}
|
|
4400
|
+
);
|
|
4401
|
+
|
|
4402
|
+
assert.calledOnce(createMediaConnectionStub);
|
|
4403
|
+
const config = createMediaConnectionStub.firstCall.args[3];
|
|
4404
|
+
assert.isUndefined(config.iceTransportPolicy);
|
|
4405
|
+
|
|
4406
|
+
const successCall = Metrics.sendBehavioralMetric.getCalls().find(
|
|
4407
|
+
(call) => call.args[0] === BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS
|
|
4408
|
+
);
|
|
4409
|
+
assert.isDefined(successCall);
|
|
4410
|
+
assert.equal(successCall.args[1].iceTransportPolicy, 'all');
|
|
4411
|
+
});
|
|
4412
|
+
});
|
|
4413
|
+
|
|
4414
|
+
it('should send ADD_MEDIA_SUCCESS metric with iceTransportPolicy=relay when relay is used', async () => {
|
|
4415
|
+
meeting.roap.doTurnDiscovery = sinon.stub().returns({
|
|
4416
|
+
turnServerInfo: {urls: ['turns:turn-server:443?transport=tcp'], username: 'u', password: 'p'},
|
|
4417
|
+
turnDiscoverySkippedReason: undefined,
|
|
4418
|
+
});
|
|
4419
|
+
|
|
4420
|
+
await meeting.addMediaInternal(
|
|
4421
|
+
() => 'JOIN_MEETING_FINAL',
|
|
4422
|
+
false,
|
|
4423
|
+
undefined,
|
|
4424
|
+
'relay',
|
|
4425
|
+
{mediaSettings: {}}
|
|
4426
|
+
);
|
|
4427
|
+
|
|
4428
|
+
const successCall = Metrics.sendBehavioralMetric.getCalls().find(
|
|
4429
|
+
(call) => call.args[0] === BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS
|
|
4430
|
+
);
|
|
4431
|
+
assert.isDefined(successCall);
|
|
4432
|
+
assert.equal(successCall.args[1].iceTransportPolicy, 'relay');
|
|
4433
|
+
});
|
|
4434
|
+
|
|
4435
|
+
it('should send ADD_MEDIA_FAILURE metric with iceTransportPolicy=relay when relay attempt fails', async () => {
|
|
4436
|
+
meeting.roap.doTurnDiscovery = sinon.stub().returns({
|
|
4437
|
+
turnServerInfo: {urls: ['turns:turn-server:443?transport=tcp'], username: 'u', password: 'p'},
|
|
4438
|
+
turnDiscoverySkippedReason: undefined,
|
|
4439
|
+
});
|
|
4440
|
+
|
|
4441
|
+
meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().rejects(
|
|
4442
|
+
new MediaConnectionTimedOutError('timed out', true)
|
|
4443
|
+
);
|
|
4444
|
+
|
|
4445
|
+
await assert.isRejected(meeting.addMediaInternal(
|
|
4446
|
+
() => 'JOIN_MEETING_FINAL',
|
|
4447
|
+
false,
|
|
4448
|
+
undefined,
|
|
4449
|
+
'relay',
|
|
4450
|
+
{mediaSettings: {}}
|
|
4451
|
+
));
|
|
4452
|
+
|
|
4453
|
+
const failureCall = Metrics.sendBehavioralMetric.getCalls().find(
|
|
4454
|
+
(call) => call.args[0] === BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE
|
|
4455
|
+
);
|
|
4456
|
+
assert.isDefined(failureCall);
|
|
4457
|
+
assert.equal(failureCall.args[1].iceTransportPolicy, 'relay');
|
|
4458
|
+
});
|
|
4459
|
+
});
|
|
4460
|
+
|
|
4198
4461
|
it('should resolve if waitForMediaConnectionConnected() rejects the first time but resolves the second time', async () => {
|
|
4199
4462
|
const FAKE_ERROR = {fatal: true};
|
|
4200
4463
|
webex.meetings.reachability = {
|
|
@@ -4384,6 +4647,7 @@ describe('plugin-meetings', () => {
|
|
|
4384
4647
|
isMultistream: false,
|
|
4385
4648
|
retriedWithTurnServer: true,
|
|
4386
4649
|
isJoinWithMediaRetry: false,
|
|
4650
|
+
iceTransportPolicy: 'all',
|
|
4387
4651
|
iceCandidatesCount: 0,
|
|
4388
4652
|
subnet_reachable: null,
|
|
4389
4653
|
selected_cluster: null,
|
|
@@ -4547,6 +4811,7 @@ describe('plugin-meetings', () => {
|
|
|
4547
4811
|
isMultistream: false,
|
|
4548
4812
|
retriedWithTurnServer: false,
|
|
4549
4813
|
isJoinWithMediaRetry: false,
|
|
4814
|
+
iceTransportPolicy: 'all',
|
|
4550
4815
|
someReachabilityMetric1: 'some value1',
|
|
4551
4816
|
someReachabilityMetric2: 'some value2',
|
|
4552
4817
|
iceCandidatesCount: 3,
|
|
@@ -4612,9 +4877,11 @@ describe('plugin-meetings', () => {
|
|
|
4612
4877
|
retriedWithTurnServer: false,
|
|
4613
4878
|
isMultistream: false,
|
|
4614
4879
|
isJoinWithMediaRetry: false,
|
|
4880
|
+
iceTransportPolicy: 'all',
|
|
4615
4881
|
signalingState: 'unknown',
|
|
4616
4882
|
connectionState: 'unknown',
|
|
4617
4883
|
iceConnectionState: 'unknown',
|
|
4884
|
+
connectionType: 'udp',
|
|
4618
4885
|
selectedCandidatePairChanges: 2,
|
|
4619
4886
|
numTransports: 1,
|
|
4620
4887
|
subnet_reachable: null,
|
|
@@ -4675,9 +4942,11 @@ describe('plugin-meetings', () => {
|
|
|
4675
4942
|
retriedWithTurnServer: false,
|
|
4676
4943
|
isMultistream: false,
|
|
4677
4944
|
isJoinWithMediaRetry: false,
|
|
4945
|
+
iceTransportPolicy: 'all',
|
|
4678
4946
|
signalingState: 'unknown',
|
|
4679
4947
|
connectionState: 'unknown',
|
|
4680
4948
|
iceConnectionState: 'unknown',
|
|
4949
|
+
connectionType: 'udp',
|
|
4681
4950
|
selectedCandidatePairChanges: 2,
|
|
4682
4951
|
numTransports: 1,
|
|
4683
4952
|
'701_error': 2,
|
|
@@ -4739,6 +5008,7 @@ describe('plugin-meetings', () => {
|
|
|
4739
5008
|
isMultistream: false,
|
|
4740
5009
|
retriedWithTurnServer: false,
|
|
4741
5010
|
isJoinWithMediaRetry: false,
|
|
5011
|
+
iceTransportPolicy: 'all',
|
|
4742
5012
|
iceCandidatesCount: 0,
|
|
4743
5013
|
reachability_public_udp_success: 5,
|
|
4744
5014
|
subnet_reachable: false,
|
|
@@ -4804,9 +5074,11 @@ describe('plugin-meetings', () => {
|
|
|
4804
5074
|
retriedWithTurnServer: false,
|
|
4805
5075
|
isMultistream: false,
|
|
4806
5076
|
isJoinWithMediaRetry: false,
|
|
5077
|
+
iceTransportPolicy: 'all',
|
|
4807
5078
|
signalingState: 'unknown',
|
|
4808
5079
|
connectionState: 'unknown',
|
|
4809
5080
|
iceConnectionState: 'unknown',
|
|
5081
|
+
connectionType: 'udp',
|
|
4810
5082
|
selectedCandidatePairChanges: 2,
|
|
4811
5083
|
numTransports: 1,
|
|
4812
5084
|
reachability_public_udp_success: 5,
|
|
@@ -7139,13 +7411,14 @@ describe('plugin-meetings', () => {
|
|
|
7139
7411
|
sinon.stub(meeting.locusMediaRequest, 'downgradeFromMultistreamToTranscoded');
|
|
7140
7412
|
});
|
|
7141
7413
|
|
|
7142
|
-
const runCheck = async (turnServerInfo, forceTurnDiscovery) => {
|
|
7414
|
+
const runCheck = async (turnServerInfo, forceTurnDiscovery, iceTransportPolicy) => {
|
|
7143
7415
|
// we're calling addMediaInternal() with mic stream,
|
|
7144
7416
|
// so that we also verify that audioMute, videoMute info is correctly sent to backend
|
|
7145
7417
|
const addMediaPromise = meeting.addMediaInternal(
|
|
7146
7418
|
() => '',
|
|
7147
|
-
turnServerInfo,
|
|
7148
7419
|
forceTurnDiscovery,
|
|
7420
|
+
turnServerInfo,
|
|
7421
|
+
iceTransportPolicy,
|
|
7149
7422
|
{
|
|
7150
7423
|
localStreams: {microphone: fakeMicrophoneStream},
|
|
7151
7424
|
}
|
|
@@ -7199,6 +7472,10 @@ describe('plugin-meetings', () => {
|
|
|
7199
7472
|
// at this point the meeting should have been downgraded to transcoded
|
|
7200
7473
|
assert.equal(meeting.isMultistream, false);
|
|
7201
7474
|
|
|
7475
|
+
// iceTransportPolicy must be reset to undefined on downgrade to transcoded,
|
|
7476
|
+
// otherwise the transcoded retry could gather direct candidates while metrics report relay
|
|
7477
|
+
assert.isUndefined(meeting.addMediaData.iceTransportPolicy);
|
|
7478
|
+
|
|
7202
7479
|
// old stats analyzer stopped and new one created
|
|
7203
7480
|
assert.calledOnce(initialStatsAnalyzer.stopAnalyzer);
|
|
7204
7481
|
assert.calledOnceWithExactly(
|
|
@@ -7289,6 +7566,11 @@ describe('plugin-meetings', () => {
|
|
|
7289
7566
|
true
|
|
7290
7567
|
);
|
|
7291
7568
|
});
|
|
7569
|
+
|
|
7570
|
+
it('resets iceTransportPolicy to undefined when falling back from multistream (relay) to transcoded', async () => {
|
|
7571
|
+
// simulate a relay-only retry falling back to transcoded and verify iceTransportPolicy is reset
|
|
7572
|
+
await runCheck(undefined, false, 'relay');
|
|
7573
|
+
});
|
|
7292
7574
|
});
|
|
7293
7575
|
}
|
|
7294
7576
|
})
|
|
@@ -160,6 +160,75 @@ describe('isAnyPublicClusterReachable', () => {
|
|
|
160
160
|
});
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
+
describe('isAnyClusterReachableViaProtocol', () => {
|
|
164
|
+
let webex;
|
|
165
|
+
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
webex = new MockWebex();
|
|
168
|
+
sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.unknown);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
afterEach(() => {
|
|
172
|
+
sinon.restore();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const checkReachableViaProtocol = async (
|
|
176
|
+
mockStorage: any,
|
|
177
|
+
protocol: 'udp' | 'tcp' | 'xtls',
|
|
178
|
+
expectedValue: boolean
|
|
179
|
+
) => {
|
|
180
|
+
if (mockStorage) {
|
|
181
|
+
await webex.boundedStorage.put(
|
|
182
|
+
'Reachability',
|
|
183
|
+
'reachability.result',
|
|
184
|
+
JSON.stringify(mockStorage)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const reachability = new Reachability(webex);
|
|
188
|
+
|
|
189
|
+
const result = await reachability.isAnyClusterReachableViaProtocol(protocol);
|
|
190
|
+
|
|
191
|
+
assert.equal(result, expectedValue);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
['udp', 'tcp', 'xtls'].forEach((protocol: 'udp' | 'tcp' | 'xtls') => {
|
|
195
|
+
it(`returns true when at least one cluster is reachable via ${protocol}`, async () => {
|
|
196
|
+
await checkReachableViaProtocol(
|
|
197
|
+
{
|
|
198
|
+
clusterA: {[protocol]: {result: 'reachable'}},
|
|
199
|
+
clusterB: {[protocol]: {result: 'unreachable'}},
|
|
200
|
+
},
|
|
201
|
+
protocol,
|
|
202
|
+
true
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it(`returns false when no cluster is reachable via ${protocol}`, async () => {
|
|
207
|
+
await checkReachableViaProtocol(
|
|
208
|
+
{
|
|
209
|
+
clusterA: {[protocol]: {result: 'unreachable'}},
|
|
210
|
+
clusterB: {[protocol]: {result: 'unreachable'}},
|
|
211
|
+
},
|
|
212
|
+
protocol,
|
|
213
|
+
false
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('returns false when storage read throws an error', async () => {
|
|
219
|
+
// don't put anything in storage so .get() rejects
|
|
220
|
+
const reachability = new Reachability(webex);
|
|
221
|
+
|
|
222
|
+
const result = await reachability.isAnyClusterReachableViaProtocol('xtls');
|
|
223
|
+
|
|
224
|
+
assert.equal(result, false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns false when stored data is empty', async () => {
|
|
228
|
+
await checkReachableViaProtocol({}, 'xtls', false);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
163
232
|
describe('isWebexMediaBackendUnreachable', () => {
|
|
164
233
|
let webex;
|
|
165
234
|
|