@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
|
@@ -2846,6 +2846,10 @@ describe('plugin-meetings', () => {
|
|
|
2846
2846
|
|
|
2847
2847
|
let expectedMeeting;
|
|
2848
2848
|
|
|
2849
|
+
// simulate that updateSelf has been called previously (as happens in production)
|
|
2850
|
+
// so that parsedLocus.self reflects the joined state
|
|
2851
|
+
locusInfo.parsedLocus.self = {state: 'JOINED'};
|
|
2852
|
+
|
|
2849
2853
|
/*
|
|
2850
2854
|
When the event is triggered, it is required that the meeting has already
|
|
2851
2855
|
been updated. This is why the meeting is being checked within the stubbed event emitter
|
|
@@ -2997,6 +3001,46 @@ describe('plugin-meetings', () => {
|
|
|
2997
3001
|
// since self is not passed to updateMeetingInfo, MEETING_INFO_UPDATED should be triggered with isIntializing: true
|
|
2998
3002
|
checkMeetingInfoUpdatedCalledForRoles(true, {isInitializing: true});
|
|
2999
3003
|
});
|
|
3004
|
+
|
|
3005
|
+
// joined-section hints (like ROSTER_IN_MEETING) are filtered out while not joined, so they
|
|
3006
|
+
// are a good proxy for verifying that userDisplayHints get recomputed on a join transition
|
|
3007
|
+
[
|
|
3008
|
+
{
|
|
3009
|
+
name: 'the JOINED delta carries the info section',
|
|
3010
|
+
getSecondInfo: (info) => info,
|
|
3011
|
+
},
|
|
3012
|
+
{
|
|
3013
|
+
name: 'the JOINED delta omits the info section (falls back to stored info)',
|
|
3014
|
+
getSecondInfo: () => undefined,
|
|
3015
|
+
},
|
|
3016
|
+
].forEach(({name, getSecondInfo}) => {
|
|
3017
|
+
it(`recomputes userDisplayHints when self transitions to JOINED with unchanged roles and ${name}`, () => {
|
|
3018
|
+
const info = cloneDeep(meetingInfo); // joined: ['ROSTER_IN_MEETING', 'LOCK_STATUS_UNLOCKED']
|
|
3019
|
+
|
|
3020
|
+
const notJoinedSelf = cloneDeep(self);
|
|
3021
|
+
notJoinedSelf.state = 'IDLE';
|
|
3022
|
+
notJoinedSelf.controls.role.roles = [];
|
|
3023
|
+
|
|
3024
|
+
const joinedSelf = cloneDeep(self);
|
|
3025
|
+
joinedSelf.state = 'JOINED';
|
|
3026
|
+
joinedSelf.controls.role.roles = [];
|
|
3027
|
+
|
|
3028
|
+
sinon.stub(locusInfo, 'emitScoped');
|
|
3029
|
+
|
|
3030
|
+
// first update while not joined: joined-section hints are filtered out
|
|
3031
|
+
locusInfo.updateMeetingInfo(info, notJoinedSelf);
|
|
3032
|
+
assert.notInclude(locusInfo.parsedLocus.info.userDisplayHints, 'ROSTER_IN_MEETING');
|
|
3033
|
+
assert.notInclude(locusInfo.parsedLocus.info.userDisplayHints, 'LOCK_STATUS_UNLOCKED');
|
|
3034
|
+
|
|
3035
|
+
// self transitions to JOINED - info and roles are unchanged
|
|
3036
|
+
locusInfo.updateMeetingInfo(getSecondInfo(info), joinedSelf);
|
|
3037
|
+
|
|
3038
|
+
// the hints must be recomputed with the new joined state
|
|
3039
|
+
assert.include(locusInfo.parsedLocus.info.userDisplayHints, 'ROSTER_IN_MEETING');
|
|
3040
|
+
assert.include(locusInfo.parsedLocus.info.userDisplayHints, 'LOCK_STATUS_UNLOCKED');
|
|
3041
|
+
checkMeetingInfoUpdatedCalled(true, {isInitializing: false});
|
|
3042
|
+
});
|
|
3043
|
+
});
|
|
3000
3044
|
});
|
|
3001
3045
|
|
|
3002
3046
|
describe('#updateMediaShares', () => {
|
|
@@ -204,6 +204,61 @@ describe('MediaConnectionAwaiter', () => {
|
|
|
204
204
|
assert.calledThrice(mockMC.off);
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
+
it('sets iceConnected=true when ice connection state reaches "completed"', async () => {
|
|
208
|
+
mockMC.getConnectionState.returns(ConnectionState.Connecting);
|
|
209
|
+
mockMC.getIceGatheringState.returns('complete');
|
|
210
|
+
|
|
211
|
+
let promiseRejected = false;
|
|
212
|
+
let rejectedIceConnected;
|
|
213
|
+
|
|
214
|
+
mediaConnectionAwaiter
|
|
215
|
+
.waitForMediaConnectionConnected()
|
|
216
|
+
.then(() => {})
|
|
217
|
+
.catch((error) => {
|
|
218
|
+
promiseRejected = true;
|
|
219
|
+
rejectedIceConnected = error.iceConnected;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await testUtils.flushPromises();
|
|
223
|
+
|
|
224
|
+
const iceConnectionListener = mockMC.on.getCall(1).args[1];
|
|
225
|
+
|
|
226
|
+
mockMC.getIceConnectionState.returns('completed');
|
|
227
|
+
iceConnectionListener();
|
|
228
|
+
|
|
229
|
+
await clock.tickAsync(ICE_AND_DTLS_CONNECTION_TIMEOUT);
|
|
230
|
+
await testUtils.flushPromises();
|
|
231
|
+
|
|
232
|
+
assert.equal(promiseRejected, true);
|
|
233
|
+
assert.equal(rejectedIceConnected, true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
['connected', 'completed'].forEach((iceState) => {
|
|
237
|
+
it(`initializes iceConnected=true from current ICE state "${iceState}" when starting the await`, async () => {
|
|
238
|
+
mockMC.getConnectionState.returns(ConnectionState.Connecting);
|
|
239
|
+
mockMC.getIceGatheringState.returns('complete');
|
|
240
|
+
mockMC.getIceConnectionState.returns(iceState);
|
|
241
|
+
|
|
242
|
+
let promiseRejected = false;
|
|
243
|
+
let rejectedIceConnected;
|
|
244
|
+
|
|
245
|
+
mediaConnectionAwaiter
|
|
246
|
+
.waitForMediaConnectionConnected()
|
|
247
|
+
.then(() => {})
|
|
248
|
+
.catch((error) => {
|
|
249
|
+
promiseRejected = true;
|
|
250
|
+
rejectedIceConnected = error.iceConnected;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// no ICE connection state events fired - the flag should be set from the initial state
|
|
254
|
+
await clock.tickAsync(ICE_AND_DTLS_CONNECTION_TIMEOUT);
|
|
255
|
+
await testUtils.flushPromises();
|
|
256
|
+
|
|
257
|
+
assert.equal(promiseRejected, true);
|
|
258
|
+
assert.equal(rejectedIceConnected, true);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
207
262
|
it('resolves after timeout if connection state reach connected/completed', async () => {
|
|
208
263
|
mockMC.getConnectionState.returns(ConnectionState.Connecting);
|
|
209
264
|
mockMC.getIceGatheringState.returns('gathering');
|
|
@@ -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
|
})
|