@webex/plugin-meetings 3.3.1 → 3.4.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +7 -2
- package/dist/breakouts/index.js.map +1 -1
- package/dist/constants.js +11 -4
- package/dist/constants.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/selfUtils.js +0 -5
- package/dist/locus-info/selfUtils.js.map +1 -1
- package/dist/media/MediaConnectionAwaiter.js +70 -15
- package/dist/media/MediaConnectionAwaiter.js.map +1 -1
- package/dist/media/index.js +12 -0
- package/dist/media/index.js.map +1 -1
- package/dist/meeting/connectionStateHandler.js +67 -0
- package/dist/meeting/connectionStateHandler.js.map +1 -0
- package/dist/meeting/index.js +554 -358
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/locusMediaRequest.js +7 -0
- package/dist/meeting/locusMediaRequest.js.map +1 -1
- package/dist/meeting/muteState.js +6 -1
- package/dist/meeting/muteState.js.map +1 -1
- package/dist/meeting/util.js +1 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/index.js +4 -4
- package/dist/meeting-info/index.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +2 -2
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meeting-info/util.js +17 -17
- package/dist/meeting-info/util.js.map +1 -1
- package/dist/meeting-info/utilv2.js +16 -16
- package/dist/meeting-info/utilv2.js.map +1 -1
- package/dist/meetings/collection.js +1 -1
- package/dist/meetings/collection.js.map +1 -1
- package/dist/meetings/index.js +37 -33
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/meetings.types.js +8 -0
- package/dist/meetings/meetings.types.js.map +1 -1
- package/dist/meetings/util.js +3 -2
- package/dist/meetings/util.js.map +1 -1
- package/dist/metrics/constants.js +2 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/metrics/index.js +57 -0
- package/dist/metrics/index.js.map +1 -1
- package/dist/personal-meeting-room/index.js +1 -1
- package/dist/personal-meeting-room/index.js.map +1 -1
- package/dist/reachability/clusterReachability.js +108 -53
- package/dist/reachability/clusterReachability.js.map +1 -1
- package/dist/reachability/index.js +415 -56
- package/dist/reachability/index.js.map +1 -1
- package/dist/types/constants.d.ts +11 -3
- package/dist/types/media/MediaConnectionAwaiter.d.ts +24 -4
- package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
- package/dist/types/meeting/index.d.ts +27 -8
- package/dist/types/meeting/locusMediaRequest.d.ts +2 -0
- package/dist/types/meeting-info/index.d.ts +3 -2
- package/dist/types/meeting-info/meeting-info-v2.d.ts +3 -2
- package/dist/types/meeting-info/util.d.ts +5 -4
- package/dist/types/meeting-info/utilv2.d.ts +3 -2
- package/dist/types/meetings/collection.d.ts +3 -2
- package/dist/types/meetings/index.d.ts +4 -3
- package/dist/types/meetings/meetings.types.d.ts +9 -0
- package/dist/types/metrics/constants.d.ts +1 -0
- package/dist/types/metrics/index.d.ts +15 -0
- package/dist/types/reachability/clusterReachability.d.ts +31 -3
- package/dist/types/reachability/index.d.ts +93 -2
- package/dist/webinar/index.js +1 -1
- package/package.json +23 -23
- package/src/breakouts/index.ts +7 -1
- package/src/constants.ts +13 -17
- package/src/locus-info/selfUtils.ts +0 -5
- package/src/media/MediaConnectionAwaiter.ts +89 -14
- package/src/media/index.ts +13 -0
- package/src/meeting/connectionStateHandler.ts +65 -0
- package/src/meeting/index.ts +532 -295
- package/src/meeting/locusMediaRequest.ts +5 -0
- package/src/meeting/muteState.ts +6 -1
- package/src/meeting/util.ts +1 -0
- package/src/meeting-info/index.ts +9 -6
- package/src/meeting-info/meeting-info-v2.ts +4 -4
- package/src/meeting-info/util.ts +23 -28
- package/src/meeting-info/utilv2.ts +18 -24
- package/src/meetings/collection.ts +3 -3
- package/src/meetings/index.ts +39 -40
- package/src/meetings/meetings.types.ts +11 -0
- package/src/meetings/util.ts +5 -4
- package/src/metrics/constants.ts +1 -0
- package/src/metrics/index.ts +44 -0
- package/src/personal-meeting-room/index.ts +2 -2
- package/src/reachability/clusterReachability.ts +86 -25
- package/src/reachability/index.ts +316 -27
- package/test/unit/spec/breakouts/index.ts +51 -32
- package/test/unit/spec/locus-info/selfUtils.js +25 -23
- package/test/unit/spec/media/MediaConnectionAwaiter.ts +131 -32
- package/test/unit/spec/media/index.ts +42 -27
- package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
- package/test/unit/spec/meeting/index.js +762 -179
- package/test/unit/spec/meeting/locusMediaRequest.ts +7 -0
- package/test/unit/spec/meeting/muteState.js +24 -0
- package/test/unit/spec/meeting-info/index.js +4 -4
- package/test/unit/spec/meeting-info/meetinginfov2.js +24 -28
- package/test/unit/spec/meeting-info/request.js +2 -2
- package/test/unit/spec/meeting-info/utilv2.js +41 -49
- package/test/unit/spec/meetings/index.js +14 -0
- package/test/unit/spec/metrics/index.js +126 -0
- package/test/unit/spec/multistream/mediaRequestManager.ts +2 -2
- package/test/unit/spec/personal-meeting-room/personal-meeting-room.js +2 -2
- package/test/unit/spec/reachability/clusterReachability.ts +116 -22
- package/test/unit/spec/reachability/index.ts +1153 -84
- package/test/unit/spec/rtcMetrics/index.ts +1 -0
- package/dist/mediaQualityMetrics/config.js +0 -321
- package/dist/mediaQualityMetrics/config.js.map +0 -1
- package/dist/networkQualityMonitor/index.js +0 -227
- package/dist/networkQualityMonitor/index.js.map +0 -1
- package/dist/statsAnalyzer/global.js +0 -44
- package/dist/statsAnalyzer/global.js.map +0 -1
- package/dist/statsAnalyzer/index.js +0 -1072
- package/dist/statsAnalyzer/index.js.map +0 -1
- package/dist/statsAnalyzer/mqaUtil.js +0 -368
- package/dist/statsAnalyzer/mqaUtil.js.map +0 -1
- package/dist/types/mediaQualityMetrics/config.d.ts +0 -247
- package/dist/types/networkQualityMonitor/index.d.ts +0 -70
- package/dist/types/statsAnalyzer/global.d.ts +0 -36
- package/dist/types/statsAnalyzer/index.d.ts +0 -217
- package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -48
- package/src/mediaQualityMetrics/config.ts +0 -255
- package/src/networkQualityMonitor/index.ts +0 -211
- package/src/statsAnalyzer/global.ts +0 -37
- package/src/statsAnalyzer/index.ts +0 -1318
- package/src/statsAnalyzer/mqaUtil.ts +0 -463
- package/test/unit/spec/networkQualityMonitor/index.js +0 -99
- package/test/unit/spec/stats-analyzer/index.js +0 -1819
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
import 'jsdom-global/register';
|
|
5
5
|
import {cloneDeep, forEach, isEqual, isUndefined} from 'lodash';
|
|
6
6
|
import sinon from 'sinon';
|
|
7
|
-
import * as
|
|
7
|
+
import * as InternalMediaCoreModule from '@webex/internal-media-core';
|
|
8
8
|
import StateMachine from 'javascript-state-machine';
|
|
9
9
|
import uuid from 'uuid';
|
|
10
10
|
import {assert, expect} from '@webex/test-helper-chai';
|
|
11
|
-
import {Credentials,
|
|
11
|
+
import {Credentials, WebexPlugin} from '@webex/webex-core';
|
|
12
12
|
import Support from '@webex/internal-plugin-support';
|
|
13
13
|
import MockWebex from '@webex/test-helper-mock-webex';
|
|
14
14
|
import StaticConfig from '@webex/plugin-meetings/src/common/config';
|
|
@@ -21,31 +21,28 @@ import {
|
|
|
21
21
|
PASSWORD_STATUS,
|
|
22
22
|
EVENTS,
|
|
23
23
|
EVENT_TRIGGERS,
|
|
24
|
-
|
|
25
|
-
_MEETING_ID_,
|
|
24
|
+
DESTINATION_TYPE,
|
|
26
25
|
MEETING_REMOVED_REASON,
|
|
27
26
|
LOCUSINFO,
|
|
28
27
|
ICE_AND_DTLS_CONNECTION_TIMEOUT,
|
|
29
28
|
DISPLAY_HINTS,
|
|
30
29
|
SELF_POLICY,
|
|
31
30
|
IP_VERSION,
|
|
32
|
-
ERROR_DICTIONARY,
|
|
33
31
|
NETWORK_STATUS,
|
|
34
32
|
ONLINE,
|
|
35
33
|
OFFLINE,
|
|
36
|
-
|
|
34
|
+
ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
|
|
37
35
|
} from '@webex/plugin-meetings/src/constants';
|
|
38
|
-
import * as InternalMediaCoreModule from '@webex/internal-media-core';
|
|
39
36
|
import {
|
|
40
37
|
ConnectionState,
|
|
41
|
-
|
|
38
|
+
MediaConnectionEventNames,
|
|
39
|
+
StatsAnalyzerEventNames,
|
|
42
40
|
Errors,
|
|
43
41
|
ErrorType,
|
|
44
42
|
RemoteTrackType,
|
|
45
43
|
MediaType,
|
|
46
44
|
} from '@webex/internal-media-core';
|
|
47
45
|
import {LocalStreamEventNames} from '@webex/media-helpers';
|
|
48
|
-
import * as StatsAnalyzerModule from '@webex/plugin-meetings/src/statsAnalyzer';
|
|
49
46
|
import EventsScope from '@webex/plugin-meetings/src/common/events/events-scope';
|
|
50
47
|
import Meetings, {CONSTANTS} from '@webex/plugin-meetings';
|
|
51
48
|
import Meeting from '@webex/plugin-meetings/src/meeting';
|
|
@@ -72,6 +69,7 @@ import {MediaRequestManager} from '@webex/plugin-meetings/src/multistream/mediaR
|
|
|
72
69
|
import * as ReceiveSlotManagerModule from '@webex/plugin-meetings/src/multistream/receiveSlotManager';
|
|
73
70
|
import * as SendSlotManagerModule from '@webex/plugin-meetings/src/multistream/sendSlotManager';
|
|
74
71
|
import {CallDiagnosticUtils} from '@webex/internal-plugin-metrics';
|
|
72
|
+
import * as LocusMediaRequestModule from '@webex/plugin-meetings/src/meeting/locusMediaRequest';
|
|
75
73
|
|
|
76
74
|
import CallDiagnosticLatencies from '@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics-latencies';
|
|
77
75
|
import LLM from '@webex/internal-plugin-llm';
|
|
@@ -102,6 +100,7 @@ import {
|
|
|
102
100
|
import {
|
|
103
101
|
DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
|
|
104
102
|
ICE_FAILED_WITHOUT_TURN_TLS_CLIENT_CODE,
|
|
103
|
+
ICE_AND_REACHABILITY_FAILED_CLIENT_CODE,
|
|
105
104
|
ICE_FAILED_WITH_TURN_TLS_CLIENT_CODE,
|
|
106
105
|
ICE_FAILURE_CLIENT_CODE,
|
|
107
106
|
MISSING_ROAP_ANSWER_CLIENT_CODE,
|
|
@@ -279,7 +278,7 @@ describe('plugin-meetings', () => {
|
|
|
279
278
|
deviceUrl: uuid3,
|
|
280
279
|
locus: {url: url1},
|
|
281
280
|
destination: testDestination,
|
|
282
|
-
destinationType:
|
|
281
|
+
destinationType: DESTINATION_TYPE.MEETING_ID,
|
|
283
282
|
correlationId,
|
|
284
283
|
selfId: uuid1,
|
|
285
284
|
},
|
|
@@ -347,7 +346,7 @@ describe('plugin-meetings', () => {
|
|
|
347
346
|
assert.equal(meeting.requiredCaptcha, null);
|
|
348
347
|
assert.equal(meeting.meetingInfoFailureReason, undefined);
|
|
349
348
|
assert.equal(meeting.destination, testDestination);
|
|
350
|
-
assert.equal(meeting.destinationType,
|
|
349
|
+
assert.equal(meeting.destinationType, DESTINATION_TYPE.MEETING_ID);
|
|
351
350
|
assert.instanceOf(meeting.breakouts, Breakouts);
|
|
352
351
|
assert.instanceOf(meeting.simultaneousInterpretation, SimultaneousInterpretation);
|
|
353
352
|
assert.instanceOf(meeting.webinar, Webinar);
|
|
@@ -367,7 +366,7 @@ describe('plugin-meetings', () => {
|
|
|
367
366
|
deviceUrl: uuid3,
|
|
368
367
|
locus: {url: url1},
|
|
369
368
|
destination: testDestination,
|
|
370
|
-
destinationType:
|
|
369
|
+
destinationType: DESTINATION_TYPE.MEETING_ID,
|
|
371
370
|
},
|
|
372
371
|
{
|
|
373
372
|
parent: webex,
|
|
@@ -385,7 +384,7 @@ describe('plugin-meetings', () => {
|
|
|
385
384
|
deviceUrl: uuid3,
|
|
386
385
|
locus: {url: url1},
|
|
387
386
|
destination: testDestination,
|
|
388
|
-
destinationType:
|
|
387
|
+
destinationType: DESTINATION_TYPE.MEETING_ID,
|
|
389
388
|
callStateForMetrics: {
|
|
390
389
|
correlationId: uuid4,
|
|
391
390
|
joinTrigger: 'fake-join-trigger',
|
|
@@ -426,7 +425,7 @@ describe('plugin-meetings', () => {
|
|
|
426
425
|
deviceUrl: uuid3,
|
|
427
426
|
locus: {url: url1},
|
|
428
427
|
destination: testDestination,
|
|
429
|
-
destinationType:
|
|
428
|
+
destinationType: DESTINATION_TYPE.MEETING_ID,
|
|
430
429
|
},
|
|
431
430
|
{
|
|
432
431
|
parent: webex,
|
|
@@ -502,7 +501,7 @@ describe('plugin-meetings', () => {
|
|
|
502
501
|
deviceUrl: uuid3,
|
|
503
502
|
locus: {url: url1},
|
|
504
503
|
destination: testDestination,
|
|
505
|
-
destinationType:
|
|
504
|
+
destinationType: DESTINATION_TYPE.MEETING_ID,
|
|
506
505
|
},
|
|
507
506
|
{
|
|
508
507
|
parent: webex,
|
|
@@ -622,10 +621,13 @@ describe('plugin-meetings', () => {
|
|
|
622
621
|
let generateTurnDiscoveryRequestMessageStub;
|
|
623
622
|
let handleTurnDiscoveryHttpResponseStub;
|
|
624
623
|
let abortTurnDiscoveryStub;
|
|
624
|
+
let addMediaInternalStub;
|
|
625
625
|
|
|
626
626
|
beforeEach(() => {
|
|
627
627
|
meeting.join = sinon.stub().returns(Promise.resolve(fakeJoinResult));
|
|
628
|
-
|
|
628
|
+
addMediaInternalStub = sinon
|
|
629
|
+
.stub(meeting, 'addMediaInternal')
|
|
630
|
+
.returns(Promise.resolve(test4));
|
|
629
631
|
|
|
630
632
|
webex.meetings.reachability.getReachabilityResults.resolves(fakeReachabilityResults);
|
|
631
633
|
|
|
@@ -644,7 +646,7 @@ describe('plugin-meetings', () => {
|
|
|
644
646
|
mediaOptions,
|
|
645
647
|
});
|
|
646
648
|
|
|
647
|
-
// check that TURN discovery is done with join and
|
|
649
|
+
// check that TURN discovery is done with join and addMediaInternal() called
|
|
648
650
|
assert.calledOnceWithExactly(meeting.join, {
|
|
649
651
|
...joinOptions,
|
|
650
652
|
roapMessage: fakeRoapMessage,
|
|
@@ -656,12 +658,21 @@ describe('plugin-meetings', () => {
|
|
|
656
658
|
meeting,
|
|
657
659
|
fakeJoinResult
|
|
658
660
|
);
|
|
659
|
-
assert.calledOnceWithExactly(
|
|
661
|
+
assert.calledOnceWithExactly(
|
|
662
|
+
meeting.addMediaInternal,
|
|
663
|
+
sinon.match.any,
|
|
664
|
+
fakeTurnServerInfo,
|
|
665
|
+
false,
|
|
666
|
+
mediaOptions
|
|
667
|
+
);
|
|
660
668
|
|
|
661
669
|
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
662
670
|
|
|
663
671
|
// resets joinWithMediaRetryInfo
|
|
664
|
-
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
672
|
+
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
673
|
+
isRetry: false,
|
|
674
|
+
prevJoinResponse: undefined,
|
|
675
|
+
});
|
|
665
676
|
});
|
|
666
677
|
|
|
667
678
|
it("should not call handleTurnDiscoveryHttpResponse if we don't send a TURN discovery request with join", async () => {
|
|
@@ -672,7 +683,7 @@ describe('plugin-meetings', () => {
|
|
|
672
683
|
mediaOptions,
|
|
673
684
|
});
|
|
674
685
|
|
|
675
|
-
// check that TURN discovery is done with join and
|
|
686
|
+
// check that TURN discovery is done with join and addMediaInternal() called
|
|
676
687
|
assert.calledOnceWithExactly(meeting.join, {
|
|
677
688
|
...joinOptions,
|
|
678
689
|
roapMessage: undefined,
|
|
@@ -681,7 +692,13 @@ describe('plugin-meetings', () => {
|
|
|
681
692
|
assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
|
|
682
693
|
assert.notCalled(handleTurnDiscoveryHttpResponseStub);
|
|
683
694
|
assert.notCalled(abortTurnDiscoveryStub);
|
|
684
|
-
assert.calledOnceWithExactly(
|
|
695
|
+
assert.calledOnceWithExactly(
|
|
696
|
+
meeting.addMediaInternal,
|
|
697
|
+
sinon.match.any,
|
|
698
|
+
undefined,
|
|
699
|
+
false,
|
|
700
|
+
mediaOptions
|
|
701
|
+
);
|
|
685
702
|
|
|
686
703
|
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
687
704
|
assert.equal(meeting.turnServerUsed, false);
|
|
@@ -698,7 +715,7 @@ describe('plugin-meetings', () => {
|
|
|
698
715
|
mediaOptions,
|
|
699
716
|
});
|
|
700
717
|
|
|
701
|
-
// check that TURN discovery is done with join and
|
|
718
|
+
// check that TURN discovery is done with join and addMediaInternal() called
|
|
702
719
|
assert.calledOnceWithExactly(meeting.join, {
|
|
703
720
|
...joinOptions,
|
|
704
721
|
roapMessage: fakeRoapMessage,
|
|
@@ -711,7 +728,13 @@ describe('plugin-meetings', () => {
|
|
|
711
728
|
fakeJoinResult
|
|
712
729
|
);
|
|
713
730
|
assert.calledOnceWithExactly(abortTurnDiscoveryStub);
|
|
714
|
-
assert.calledOnceWithExactly(
|
|
731
|
+
assert.calledOnceWithExactly(
|
|
732
|
+
meeting.addMediaInternal,
|
|
733
|
+
sinon.match.any,
|
|
734
|
+
undefined,
|
|
735
|
+
false,
|
|
736
|
+
mediaOptions
|
|
737
|
+
);
|
|
715
738
|
|
|
716
739
|
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
717
740
|
});
|
|
@@ -758,12 +781,20 @@ describe('plugin-meetings', () => {
|
|
|
758
781
|
);
|
|
759
782
|
|
|
760
783
|
// resets joinWithMediaRetryInfo
|
|
761
|
-
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
784
|
+
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
785
|
+
isRetry: false,
|
|
786
|
+
prevJoinResponse: undefined,
|
|
787
|
+
});
|
|
762
788
|
});
|
|
763
789
|
|
|
764
790
|
it('should resolve if join() fails the first time but succeeds the second time', async () => {
|
|
765
791
|
const error = new Error('fake');
|
|
766
|
-
meeting.join = sinon
|
|
792
|
+
meeting.join = sinon
|
|
793
|
+
.stub()
|
|
794
|
+
.onFirstCall()
|
|
795
|
+
.returns(Promise.reject(error))
|
|
796
|
+
.onSecondCall()
|
|
797
|
+
.returns(Promise.resolve(fakeJoinResult));
|
|
767
798
|
const leaveStub = sinon.stub(meeting, 'leave').resolves();
|
|
768
799
|
|
|
769
800
|
const result = await meeting.joinWithMedia({
|
|
@@ -795,24 +826,27 @@ describe('plugin-meetings', () => {
|
|
|
795
826
|
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
796
827
|
|
|
797
828
|
// resets joinWithMediaRetryInfo
|
|
798
|
-
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
829
|
+
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
830
|
+
isRetry: false,
|
|
831
|
+
prevJoinResponse: undefined,
|
|
832
|
+
});
|
|
799
833
|
});
|
|
800
834
|
|
|
801
835
|
it('should fail if called with allowMediaInLobby:false', async () => {
|
|
802
836
|
meeting.join = sinon.stub().returns(Promise.resolve(test1));
|
|
803
|
-
meeting.
|
|
837
|
+
meeting.addMediaInternal = sinon.stub().returns(Promise.resolve(test4));
|
|
804
838
|
|
|
805
839
|
await assert.isRejected(
|
|
806
840
|
meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: false}})
|
|
807
841
|
);
|
|
808
842
|
});
|
|
809
843
|
|
|
810
|
-
it('should call leave() if
|
|
844
|
+
it('should call leave() if addMediaInternal() fails and ignore leave() failure', async () => {
|
|
811
845
|
const leaveError = new Error('leave error');
|
|
812
846
|
const addMediaError = new Error('fake addMedia error');
|
|
813
847
|
|
|
814
848
|
const leaveStub = sinon.stub(meeting, 'leave').rejects(leaveError);
|
|
815
|
-
meeting.
|
|
849
|
+
meeting.addMediaInternal = sinon.stub().rejects(addMediaError);
|
|
816
850
|
|
|
817
851
|
await assert.isRejected(
|
|
818
852
|
meeting.joinWithMedia({
|
|
@@ -828,7 +862,6 @@ describe('plugin-meetings', () => {
|
|
|
828
862
|
reason: 'joinWithMedia failure',
|
|
829
863
|
});
|
|
830
864
|
|
|
831
|
-
|
|
832
865
|
// Behavioral metric is sent on both calls of joinWithMedia
|
|
833
866
|
assert.calledTwice(Metrics.sendBehavioralMetric);
|
|
834
867
|
assert.calledWith(
|
|
@@ -863,12 +896,11 @@ describe('plugin-meetings', () => {
|
|
|
863
896
|
);
|
|
864
897
|
});
|
|
865
898
|
|
|
866
|
-
it('should not call leave() if
|
|
899
|
+
it('should not call leave() if addMediaInternal() fails the first time and succeeds the second time and should only call join() once', async () => {
|
|
867
900
|
const addMediaError = new Error('fake addMedia error');
|
|
868
|
-
const
|
|
869
|
-
const leaveStub = sinon.stub(meeting, 'leave').rejects(leaveError);
|
|
901
|
+
const leaveStub = sinon.stub(meeting, 'leave');
|
|
870
902
|
|
|
871
|
-
meeting.
|
|
903
|
+
meeting.addMediaInternal = sinon
|
|
872
904
|
.stub()
|
|
873
905
|
.onFirstCall()
|
|
874
906
|
.rejects(addMediaError)
|
|
@@ -902,6 +934,203 @@ describe('plugin-meetings', () => {
|
|
|
902
934
|
}
|
|
903
935
|
);
|
|
904
936
|
});
|
|
937
|
+
|
|
938
|
+
it('should send the right CA events when media connection fails', async () => {
|
|
939
|
+
const fakeClientError = {id: 'error'};
|
|
940
|
+
|
|
941
|
+
const fakeMediaConnection = {
|
|
942
|
+
close: sinon.stub(),
|
|
943
|
+
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
|
|
944
|
+
initiateOffer: sinon.stub().resolves({}),
|
|
945
|
+
on: sinon.stub(),
|
|
946
|
+
forceRtcMetricsSend: sinon.stub().resolves(),
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// setup the stubs so that media connection always fails on waitForMediaConnectionConnected()
|
|
950
|
+
addMediaInternalStub.restore();
|
|
951
|
+
meeting.join.returns(
|
|
952
|
+
Promise.resolve({id: 'join result', roapMessage: 'fake TURN discovery response'})
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
|
|
956
|
+
sinon.stub(meeting, 'waitForRemoteSDPAnswer').resolves();
|
|
957
|
+
sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
|
|
958
|
+
sinon
|
|
959
|
+
.stub(meeting.mediaProperties, 'waitForMediaConnectionConnected')
|
|
960
|
+
.rejects(new Error('fake error'));
|
|
961
|
+
|
|
962
|
+
webex.meetings.reachability.isWebexMediaBackendUnreachable = sinon.stub().resolves(false);
|
|
963
|
+
webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode = sinon
|
|
964
|
+
.stub()
|
|
965
|
+
.returns(fakeClientError);
|
|
966
|
+
|
|
967
|
+
// call joinWithMedia() - it should fail
|
|
968
|
+
await assert.isRejected(
|
|
969
|
+
meeting.joinWithMedia({
|
|
970
|
+
joinOptions,
|
|
971
|
+
mediaOptions,
|
|
972
|
+
})
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
// check the right CA events have been sent:
|
|
976
|
+
// calls at index 0 and 2 to submitClientEvent are for "client.media.capabilities" which we don't care about in this test
|
|
977
|
+
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent.getCall(1), {
|
|
978
|
+
name: 'client.ice.end',
|
|
979
|
+
payload: {
|
|
980
|
+
canProceed: false,
|
|
981
|
+
icePhase: 'JOIN_MEETING_RETRY',
|
|
982
|
+
errors: [fakeClientError],
|
|
983
|
+
},
|
|
984
|
+
options: {
|
|
985
|
+
meetingId: meeting.id,
|
|
986
|
+
},
|
|
987
|
+
});
|
|
988
|
+
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent.getCall(3), {
|
|
989
|
+
name: 'client.ice.end',
|
|
990
|
+
payload: {
|
|
991
|
+
canProceed: false,
|
|
992
|
+
icePhase: 'JOIN_MEETING_FINAL',
|
|
993
|
+
errors: [fakeClientError],
|
|
994
|
+
},
|
|
995
|
+
options: {
|
|
996
|
+
meetingId: meeting.id,
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it('should force TURN discovery on the 2nd attempt, if addMediaInternal() fails the first time', async () => {
|
|
1002
|
+
const addMediaError = new Error('fake addMedia error');
|
|
1003
|
+
|
|
1004
|
+
const fakeMediaConnection = {
|
|
1005
|
+
close: sinon.stub(),
|
|
1006
|
+
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
|
|
1007
|
+
initiateOffer: sinon.stub().resolves({}),
|
|
1008
|
+
on: sinon.stub(),
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
/* Setup the stubs so that the first call to addMediaInternal() fails
|
|
1012
|
+
and the 2nd call calls the real implementation - so that we can check that
|
|
1013
|
+
addMediaInternal() eventually calls meeting.roap.doTurnDiscovery() with isForced=true.
|
|
1014
|
+
As a result we need to also stub a few other methods like createMediaConnection() and waitForRemoteSDPAnswer() */
|
|
1015
|
+
sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
|
|
1016
|
+
sinon.stub(meeting, 'waitForRemoteSDPAnswer').resolves();
|
|
1017
|
+
|
|
1018
|
+
addMediaInternalStub.onFirstCall().rejects(addMediaError);
|
|
1019
|
+
addMediaInternalStub.onSecondCall().callsFake((...args) => {
|
|
1020
|
+
return addMediaInternalStub.wrappedMethod.bind(meeting)(...args);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
|
|
1024
|
+
|
|
1025
|
+
const result = await meeting.joinWithMedia({
|
|
1026
|
+
joinOptions,
|
|
1027
|
+
mediaOptions,
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: undefined});
|
|
1031
|
+
|
|
1032
|
+
assert.calledOnce(meeting.join);
|
|
1033
|
+
|
|
1034
|
+
// first addMediaInternal() call without forcing TURN
|
|
1035
|
+
assert.calledWith(
|
|
1036
|
+
meeting.addMediaInternal.firstCall,
|
|
1037
|
+
sinon.match.any,
|
|
1038
|
+
fakeTurnServerInfo,
|
|
1039
|
+
false,
|
|
1040
|
+
mediaOptions
|
|
1041
|
+
);
|
|
1042
|
+
|
|
1043
|
+
// second addMediaInternal() call with forcing TURN
|
|
1044
|
+
assert.calledWith(
|
|
1045
|
+
meeting.addMediaInternal.secondCall,
|
|
1046
|
+
sinon.match.any,
|
|
1047
|
+
undefined,
|
|
1048
|
+
true,
|
|
1049
|
+
mediaOptions
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
// now check that TURN is actually forced by addMediaInternal(),
|
|
1053
|
+
// we're not checking the isReconnecting param value, because it depends on the full sequence of things
|
|
1054
|
+
// being done correctly (like SDP offer creation) and some of these are stubbed in this test
|
|
1055
|
+
assert.calledWith(meeting.roap.doTurnDiscovery, meeting, sinon.match.any, true);
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
it('should return the right icePhase in icePhaseCallback on 1st attempt and retry', async () => {
|
|
1059
|
+
const addMediaError = new Error('fake addMedia error');
|
|
1060
|
+
|
|
1061
|
+
const icePhaseCallbacks = [];
|
|
1062
|
+
const addMediaInternalResults = [];
|
|
1063
|
+
|
|
1064
|
+
meeting.addMediaInternal = sinon
|
|
1065
|
+
.stub()
|
|
1066
|
+
.callsFake((icePhaseCallback, _turnServerInfo, _forceTurnDiscovery) => {
|
|
1067
|
+
const defer = new Defer();
|
|
1068
|
+
|
|
1069
|
+
icePhaseCallbacks.push(icePhaseCallback);
|
|
1070
|
+
addMediaInternalResults.push(defer);
|
|
1071
|
+
return defer.promise;
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
const result = meeting.joinWithMedia({
|
|
1075
|
+
joinOptions,
|
|
1076
|
+
mediaOptions,
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
await testUtils.flushPromises();
|
|
1080
|
+
|
|
1081
|
+
// check the callback works correctly on the 1st attempt
|
|
1082
|
+
assert.equal(icePhaseCallbacks.length, 1);
|
|
1083
|
+
assert.equal(icePhaseCallbacks[0](), 'JOIN_MEETING_RETRY');
|
|
1084
|
+
|
|
1085
|
+
// now trigger the failure, so that joinWithMedia() does a retry
|
|
1086
|
+
addMediaInternalResults[0].reject(addMediaError);
|
|
1087
|
+
|
|
1088
|
+
await testUtils.flushPromises();
|
|
1089
|
+
|
|
1090
|
+
// check the callback works correctly on the 2nd attempt
|
|
1091
|
+
assert.equal(icePhaseCallbacks.length, 2);
|
|
1092
|
+
assert.equal(icePhaseCallbacks[1](), 'JOIN_MEETING_FINAL');
|
|
1093
|
+
|
|
1094
|
+
// trigger 2nd failure
|
|
1095
|
+
addMediaInternalResults[1].reject(addMediaError);
|
|
1096
|
+
|
|
1097
|
+
await assert.isRejected(result);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
it('should not attempt a retry if we fail to create the offer on first atttempt', async () => {
|
|
1101
|
+
const addMediaError = new Error('fake addMedia error');
|
|
1102
|
+
addMediaError.name = 'SdpOfferCreationError';
|
|
1103
|
+
|
|
1104
|
+
meeting.addMediaInternal.rejects(addMediaError);
|
|
1105
|
+
|
|
1106
|
+
await assert.isRejected(
|
|
1107
|
+
meeting.joinWithMedia({
|
|
1108
|
+
joinOptions,
|
|
1109
|
+
mediaOptions,
|
|
1110
|
+
}),
|
|
1111
|
+
addMediaError
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
// check that only 1 attempt was done
|
|
1115
|
+
assert.calledOnce(meeting.join);
|
|
1116
|
+
assert.calledOnce(meeting.addMediaInternal);
|
|
1117
|
+
assert.calledOnce(Metrics.sendBehavioralMetric);
|
|
1118
|
+
assert.calledWith(
|
|
1119
|
+
Metrics.sendBehavioralMetric.firstCall,
|
|
1120
|
+
BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
|
|
1121
|
+
{
|
|
1122
|
+
correlation_id: meeting.correlationId,
|
|
1123
|
+
locus_id: meeting.locusUrl.split('/').pop(),
|
|
1124
|
+
reason: addMediaError.message,
|
|
1125
|
+
stack: addMediaError.stack,
|
|
1126
|
+
leaveErrorReason: undefined,
|
|
1127
|
+
isRetry: false,
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
type: addMediaError.name,
|
|
1131
|
+
}
|
|
1132
|
+
);
|
|
1133
|
+
});
|
|
905
1134
|
});
|
|
906
1135
|
|
|
907
1136
|
describe('#isTranscriptionSupported', () => {
|
|
@@ -946,19 +1175,18 @@ describe('plugin-meetings', () => {
|
|
|
946
1175
|
assert.calledTwice(webex.internal.voicea.turnOnCaptions);
|
|
947
1176
|
});
|
|
948
1177
|
|
|
949
|
-
it('should listen to events and
|
|
1178
|
+
it('should listen to events and turnOnCaptions for all users', async () => {
|
|
950
1179
|
meeting.joinedWith = {
|
|
951
1180
|
state: 'JOINED',
|
|
952
1181
|
};
|
|
953
1182
|
meeting.areVoiceaEventsSetup = false;
|
|
954
|
-
meeting.roles = ['COHOST'];
|
|
955
1183
|
|
|
956
1184
|
await meeting.startTranscription();
|
|
957
1185
|
|
|
958
1186
|
assert.equal(webex.internal.voicea.on.callCount, 4);
|
|
959
1187
|
assert.equal(meeting.areVoiceaEventsSetup, true);
|
|
960
1188
|
assert.equal(webex.internal.voicea.listenToEvents.callCount, 1);
|
|
961
|
-
assert.
|
|
1189
|
+
assert.calledOnce(webex.internal.voicea.turnOnCaptions);
|
|
962
1190
|
});
|
|
963
1191
|
|
|
964
1192
|
it("should throw error if request doesn't work", async () => {
|
|
@@ -1075,6 +1303,7 @@ describe('plugin-meetings', () => {
|
|
|
1075
1303
|
webex.internal.voicea.on = sinon.stub();
|
|
1076
1304
|
webex.internal.voicea.off = sinon.stub();
|
|
1077
1305
|
webex.internal.voicea.setSpokenLanguage = sinon.stub();
|
|
1306
|
+
meeting.roles = ['MODERATOR'];
|
|
1078
1307
|
});
|
|
1079
1308
|
|
|
1080
1309
|
afterEach(() => {
|
|
@@ -1091,6 +1320,16 @@ describe('plugin-meetings', () => {
|
|
|
1091
1320
|
});
|
|
1092
1321
|
});
|
|
1093
1322
|
|
|
1323
|
+
it('should reject if current user is not a host', (done) => {
|
|
1324
|
+
meeting.isTranscriptionSupported.returns(true);
|
|
1325
|
+
meeting.roles = ['COHOST'];
|
|
1326
|
+
|
|
1327
|
+
meeting.setSpokenLanguage('fr').catch((error) => {
|
|
1328
|
+
assert.equal(error.message, 'Only host can set spoken language');
|
|
1329
|
+
done();
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1094
1333
|
it('should resolve with the language code on successful language update', (done) => {
|
|
1095
1334
|
meeting.isTranscriptionSupported.returns(true);
|
|
1096
1335
|
const languageCode = 'fr';
|
|
@@ -1136,10 +1375,7 @@ describe('plugin-meetings', () => {
|
|
|
1136
1375
|
|
|
1137
1376
|
it('should trigger meeting:caption-received event', () => {
|
|
1138
1377
|
meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({});
|
|
1139
|
-
assert.calledWith(
|
|
1140
|
-
meeting.trigger,
|
|
1141
|
-
EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED
|
|
1142
|
-
);
|
|
1378
|
+
assert.calledWith(meeting.trigger, EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED);
|
|
1143
1379
|
});
|
|
1144
1380
|
|
|
1145
1381
|
it('should trigger meeting:receiveTranscription:started event', () => {
|
|
@@ -1152,10 +1388,7 @@ describe('plugin-meetings', () => {
|
|
|
1152
1388
|
|
|
1153
1389
|
it('should trigger meeting:caption-received event', () => {
|
|
1154
1390
|
meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({});
|
|
1155
|
-
assert.calledWith(
|
|
1156
|
-
meeting.trigger,
|
|
1157
|
-
EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED
|
|
1158
|
-
);
|
|
1391
|
+
assert.calledWith(meeting.trigger, EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED);
|
|
1159
1392
|
});
|
|
1160
1393
|
});
|
|
1161
1394
|
|
|
@@ -1310,11 +1543,7 @@ describe('plugin-meetings', () => {
|
|
|
1310
1543
|
|
|
1311
1544
|
it('turns off llm online, emits transcription connected events', () => {
|
|
1312
1545
|
meeting.handleLLMOnline();
|
|
1313
|
-
assert.calledOnceWithExactly(
|
|
1314
|
-
webex.internal.llm.off,
|
|
1315
|
-
'online',
|
|
1316
|
-
meeting.handleLLMOnline
|
|
1317
|
-
);
|
|
1546
|
+
assert.calledOnceWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
|
|
1318
1547
|
assert.calledWith(
|
|
1319
1548
|
TriggerProxy.trigger,
|
|
1320
1549
|
sinon.match.instanceOf(Meeting),
|
|
@@ -1376,11 +1605,40 @@ describe('plugin-meetings', () => {
|
|
|
1376
1605
|
assert.calledOnce(MeetingUtil.joinMeeting);
|
|
1377
1606
|
assert.calledOnce(meeting.setLocus);
|
|
1378
1607
|
assert.equal(result, joinMeetingResult);
|
|
1379
|
-
assert.calledWith(
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
)
|
|
1608
|
+
assert.calledWith(webex.internal.llm.on, 'online', meeting.handleLLMOnline);
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
[true, false].forEach((enableMultistream) => {
|
|
1612
|
+
it(`should instantiate LocusMediaRequest with correct parameters (enableMultistream=${enableMultistream})`, async () => {
|
|
1613
|
+
meeting.config.deviceType = 'web';
|
|
1614
|
+
meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
|
|
1615
|
+
|
|
1616
|
+
const mockLocusMediaRequestCtor = sinon
|
|
1617
|
+
.stub(LocusMediaRequestModule, 'LocusMediaRequest')
|
|
1618
|
+
.returns({
|
|
1619
|
+
id: 'fake LocusMediaRequest instance',
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
await meeting.join({enableMultistream});
|
|
1623
|
+
|
|
1624
|
+
assert.calledOnceWithExactly(
|
|
1625
|
+
mockLocusMediaRequestCtor,
|
|
1626
|
+
{
|
|
1627
|
+
correlationId: meeting.correlationId,
|
|
1628
|
+
meetingId: meeting.id,
|
|
1629
|
+
device: {
|
|
1630
|
+
url: meeting.deviceUrl,
|
|
1631
|
+
deviceType: meeting.config.deviceType,
|
|
1632
|
+
countryCode: 'UK',
|
|
1633
|
+
regionCode: 'EU',
|
|
1634
|
+
},
|
|
1635
|
+
preferTranscoding: !enableMultistream,
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
parent: meeting.webex,
|
|
1639
|
+
}
|
|
1640
|
+
);
|
|
1641
|
+
});
|
|
1384
1642
|
});
|
|
1385
1643
|
|
|
1386
1644
|
it('should take trigger from meeting joinTrigger if available', () => {
|
|
@@ -1661,7 +1919,7 @@ describe('plugin-meetings', () => {
|
|
|
1661
1919
|
|
|
1662
1920
|
let fakeMediaConnection;
|
|
1663
1921
|
|
|
1664
|
-
beforeEach(() => {
|
|
1922
|
+
beforeEach(async () => {
|
|
1665
1923
|
fakeMediaConnection = {
|
|
1666
1924
|
close: sinon.stub(),
|
|
1667
1925
|
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
|
|
@@ -1670,17 +1928,29 @@ describe('plugin-meetings', () => {
|
|
|
1670
1928
|
};
|
|
1671
1929
|
meeting.mediaProperties.setMediaDirection = sinon.stub().returns(true);
|
|
1672
1930
|
meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
|
|
1673
|
-
meeting.mediaProperties.getCurrentConnectionInfo = sinon
|
|
1931
|
+
meeting.mediaProperties.getCurrentConnectionInfo = sinon
|
|
1932
|
+
.stub()
|
|
1933
|
+
.resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
|
|
1674
1934
|
meeting.audio = muteStateStub;
|
|
1675
1935
|
meeting.video = muteStateStub;
|
|
1676
1936
|
sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
.stub()
|
|
1937
|
+
sinon.stub(meeting, 'setupMediaConnectionListeners');
|
|
1938
|
+
sinon.stub(meeting, 'setMercuryListener');
|
|
1939
|
+
sinon
|
|
1940
|
+
.stub(meeting.roap, 'doTurnDiscovery')
|
|
1682
1941
|
.resolves({turnServerInfo: {}, turnDiscoverySkippedReason: undefined});
|
|
1683
|
-
|
|
1942
|
+
sinon.stub(meeting, 'waitForRemoteSDPAnswer').resolves();
|
|
1943
|
+
|
|
1944
|
+
// normally the first Roap message we send is creating confluence, so mock LocusMediaRequest.isConfluenceCreated()
|
|
1945
|
+
// to return false the first time it's called and true the 2nd time, to simulate how it would happen for real
|
|
1946
|
+
meeting.locusMediaRequest = {
|
|
1947
|
+
isConfluenceCreated: sinon
|
|
1948
|
+
.stub()
|
|
1949
|
+
.onFirstCall()
|
|
1950
|
+
.returns(false)
|
|
1951
|
+
.onSecondCall()
|
|
1952
|
+
.returns(true),
|
|
1953
|
+
};
|
|
1684
1954
|
});
|
|
1685
1955
|
|
|
1686
1956
|
it('should have #addMedia', () => {
|
|
@@ -1778,6 +2048,7 @@ describe('plugin-meetings', () => {
|
|
|
1778
2048
|
someReachabilityMetric2: 'some value2',
|
|
1779
2049
|
selectedCandidatePairChanges: 2,
|
|
1780
2050
|
numTransports: 1,
|
|
2051
|
+
iceCandidatesCount: 0,
|
|
1781
2052
|
}
|
|
1782
2053
|
);
|
|
1783
2054
|
});
|
|
@@ -1885,6 +2156,7 @@ describe('plugin-meetings', () => {
|
|
|
1885
2156
|
someReachabilityMetric2: 'some value2',
|
|
1886
2157
|
selectedCandidatePairChanges: 2,
|
|
1887
2158
|
numTransports: 1,
|
|
2159
|
+
iceCandidatesCount: 0,
|
|
1888
2160
|
}
|
|
1889
2161
|
);
|
|
1890
2162
|
});
|
|
@@ -2028,6 +2300,61 @@ describe('plugin-meetings', () => {
|
|
|
2028
2300
|
}
|
|
2029
2301
|
});
|
|
2030
2302
|
|
|
2303
|
+
it('sends correct CA event when times out waiting for SDP answer', async () => {
|
|
2304
|
+
const eventListeners = {};
|
|
2305
|
+
const clock = sinon.useFakeTimers();
|
|
2306
|
+
|
|
2307
|
+
// these 2 are stubbed, we need the real versions:
|
|
2308
|
+
meeting.waitForRemoteSDPAnswer.restore();
|
|
2309
|
+
meeting.setupMediaConnectionListeners.restore();
|
|
2310
|
+
|
|
2311
|
+
meeting.meetingState = 'ACTIVE';
|
|
2312
|
+
|
|
2313
|
+
// setup a mock media connection that will trigger an offer when initiateOffer() is called
|
|
2314
|
+
Media.createMediaConnection = sinon.stub().returns({
|
|
2315
|
+
initiateOffer: sinon.stub().callsFake(() => {
|
|
2316
|
+
// simulate offer being generated
|
|
2317
|
+
eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
|
|
2318
|
+
|
|
2319
|
+
return Promise.resolve();
|
|
2320
|
+
}),
|
|
2321
|
+
close: sinon.stub(),
|
|
2322
|
+
on: (event, listener) => {
|
|
2323
|
+
eventListeners[event] = listener;
|
|
2324
|
+
},
|
|
2325
|
+
forceRtcMetricsSend: sinon.stub().resolves(),
|
|
2326
|
+
});
|
|
2327
|
+
|
|
2328
|
+
const getErrorPayloadForClientErrorCodeStub =
|
|
2329
|
+
(webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
|
|
2330
|
+
sinon
|
|
2331
|
+
.stub()
|
|
2332
|
+
.callsFake(({clientErrorCode}) => ({errorCode: clientErrorCode, fatal: true})));
|
|
2333
|
+
|
|
2334
|
+
const result = meeting.addMedia();
|
|
2335
|
+
await testUtils.flushPromises();
|
|
2336
|
+
|
|
2337
|
+
// simulate timeout waiting for the SDP answer that never comes
|
|
2338
|
+
await clock.tickAsync(ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT);
|
|
2339
|
+
|
|
2340
|
+
await assert.isRejected(result);
|
|
2341
|
+
|
|
2342
|
+
assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
|
|
2343
|
+
clientErrorCode: 2007,
|
|
2344
|
+
});
|
|
2345
|
+
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
2346
|
+
name: 'client.media-engine.remote-sdp-received',
|
|
2347
|
+
payload: {
|
|
2348
|
+
canProceed: false,
|
|
2349
|
+
errors: [{errorCode: 2007, fatal: true}],
|
|
2350
|
+
},
|
|
2351
|
+
options: {
|
|
2352
|
+
meetingId: meeting.id,
|
|
2353
|
+
rawError: sinon.match.instanceOf(Error),
|
|
2354
|
+
},
|
|
2355
|
+
});
|
|
2356
|
+
});
|
|
2357
|
+
|
|
2031
2358
|
it('if an error occurs after media request has already been sent, and the user waits until the server kicks them out, a UserNotJoinedError should be thrown when attempting to addMedia again', async () => {
|
|
2032
2359
|
meeting.meetingState = 'ACTIVE';
|
|
2033
2360
|
// setup the mock to cause addMedia() to fail
|
|
@@ -2189,6 +2516,10 @@ describe('plugin-meetings', () => {
|
|
|
2189
2516
|
const getErrorPayloadForClientErrorCodeStub =
|
|
2190
2517
|
(webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
|
|
2191
2518
|
sinon.stub().returns(FAKE_ERROR));
|
|
2519
|
+
webex.meetings.reachability = {
|
|
2520
|
+
isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
|
|
2521
|
+
getReachabilityMetrics: sinon.stub().resolves(),
|
|
2522
|
+
};
|
|
2192
2523
|
const MOCK_CLIENT_ERROR_CODE = 2004;
|
|
2193
2524
|
const generateClientErrorCodeForIceFailureStub = sinon
|
|
2194
2525
|
.stub(CallDiagnosticUtils, 'generateClientErrorCodeForIceFailure')
|
|
@@ -2216,7 +2547,7 @@ describe('plugin-meetings', () => {
|
|
|
2216
2547
|
turnDiscoverySkippedReason: undefined,
|
|
2217
2548
|
});
|
|
2218
2549
|
meeting.meetingState = 'ACTIVE';
|
|
2219
|
-
meeting.mediaProperties.waitForMediaConnectionConnected.rejects(
|
|
2550
|
+
meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
|
|
2220
2551
|
|
|
2221
2552
|
const forceRtcMetricsSend = sinon.stub().resolves();
|
|
2222
2553
|
const closeMediaConnectionStub = sinon.stub();
|
|
@@ -2240,13 +2571,15 @@ describe('plugin-meetings', () => {
|
|
|
2240
2571
|
assert.calledTwice(generateClientErrorCodeForIceFailureStub);
|
|
2241
2572
|
assert.calledWith(generateClientErrorCodeForIceFailureStub, {
|
|
2242
2573
|
signalingState: 'unknown',
|
|
2243
|
-
|
|
2574
|
+
iceConnected: false,
|
|
2244
2575
|
turnServerUsed: false,
|
|
2576
|
+
unreachable: false,
|
|
2245
2577
|
});
|
|
2246
2578
|
assert.calledWith(generateClientErrorCodeForIceFailureStub, {
|
|
2247
2579
|
signalingState: 'unknown',
|
|
2248
|
-
|
|
2580
|
+
iceConnected: false,
|
|
2249
2581
|
turnServerUsed: true,
|
|
2582
|
+
unreachable: false,
|
|
2250
2583
|
});
|
|
2251
2584
|
|
|
2252
2585
|
assert.calledTwice(getErrorPayloadForClientErrorCodeStub);
|
|
@@ -2364,6 +2697,7 @@ describe('plugin-meetings', () => {
|
|
|
2364
2697
|
iceConnectionState: 'unknown',
|
|
2365
2698
|
selectedCandidatePairChanges: 2,
|
|
2366
2699
|
numTransports: 1,
|
|
2700
|
+
iceCandidatesCount: 0,
|
|
2367
2701
|
},
|
|
2368
2702
|
]);
|
|
2369
2703
|
|
|
@@ -2371,7 +2705,7 @@ describe('plugin-meetings', () => {
|
|
|
2371
2705
|
const doTurnDiscoveryCalls = meeting.roap.doTurnDiscovery.getCalls();
|
|
2372
2706
|
assert.equal(doTurnDiscoveryCalls.length, 2);
|
|
2373
2707
|
assert.deepEqual(doTurnDiscoveryCalls[0].args, [meeting, false, false]);
|
|
2374
|
-
assert.deepEqual(doTurnDiscoveryCalls[1].args, [
|
|
2708
|
+
assert.deepEqual(doTurnDiscoveryCalls[1].args.slice(1), [true, true]);
|
|
2375
2709
|
|
|
2376
2710
|
// Some clean up steps happens twice
|
|
2377
2711
|
assert.calledTwice(forceRtcMetricsSend);
|
|
@@ -2383,6 +2717,17 @@ describe('plugin-meetings', () => {
|
|
|
2383
2717
|
|
|
2384
2718
|
it('should resolve if waitForMediaConnectionConnected() rejects the first time but resolves the second time', async () => {
|
|
2385
2719
|
const FAKE_ERROR = {fatal: true};
|
|
2720
|
+
webex.meetings.reachability = {
|
|
2721
|
+
isWebexMediaBackendUnreachable: sinon
|
|
2722
|
+
.stub()
|
|
2723
|
+
.onCall(0)
|
|
2724
|
+
.rejects()
|
|
2725
|
+
.onCall(1)
|
|
2726
|
+
.resolves(true)
|
|
2727
|
+
.onCall(2)
|
|
2728
|
+
.resolves(false),
|
|
2729
|
+
getReachabilityMetrics: sinon.stub().resolves({}),
|
|
2730
|
+
};
|
|
2386
2731
|
const getErrorPayloadForClientErrorCodeStub =
|
|
2387
2732
|
(webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
|
|
2388
2733
|
sinon.stub().returns(FAKE_ERROR));
|
|
@@ -2440,8 +2785,9 @@ describe('plugin-meetings', () => {
|
|
|
2440
2785
|
assert.calledOnce(generateClientErrorCodeForIceFailureStub);
|
|
2441
2786
|
assert.calledWith(generateClientErrorCodeForIceFailureStub, {
|
|
2442
2787
|
signalingState: 'unknown',
|
|
2443
|
-
|
|
2788
|
+
iceConnected: undefined,
|
|
2444
2789
|
turnServerUsed: false,
|
|
2790
|
+
unreachable: false,
|
|
2445
2791
|
});
|
|
2446
2792
|
|
|
2447
2793
|
assert.calledOnce(getErrorPayloadForClientErrorCodeStub);
|
|
@@ -2547,6 +2893,7 @@ describe('plugin-meetings', () => {
|
|
|
2547
2893
|
isMultistream: false,
|
|
2548
2894
|
retriedWithTurnServer: true,
|
|
2549
2895
|
isJoinWithMediaRetry: false,
|
|
2896
|
+
iceCandidatesCount: 0,
|
|
2550
2897
|
},
|
|
2551
2898
|
]);
|
|
2552
2899
|
meeting.roap.doTurnDiscovery;
|
|
@@ -2675,6 +3022,10 @@ describe('plugin-meetings', () => {
|
|
|
2675
3022
|
someReachabilityMetric2: 'some value2',
|
|
2676
3023
|
}),
|
|
2677
3024
|
};
|
|
3025
|
+
meeting.iceCandidatesCount = 3;
|
|
3026
|
+
meeting.iceCandidateErrors.set('701_error', 3);
|
|
3027
|
+
meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
|
|
3028
|
+
|
|
2678
3029
|
await meeting.addMedia({
|
|
2679
3030
|
mediaSettings: {},
|
|
2680
3031
|
});
|
|
@@ -2694,6 +3045,9 @@ describe('plugin-meetings', () => {
|
|
|
2694
3045
|
isJoinWithMediaRetry: false,
|
|
2695
3046
|
someReachabilityMetric1: 'some value1',
|
|
2696
3047
|
someReachabilityMetric2: 'some value2',
|
|
3048
|
+
iceCandidatesCount: 3,
|
|
3049
|
+
'701_error': 3,
|
|
3050
|
+
'701_turn_host_lookup_received_error': 1,
|
|
2697
3051
|
}
|
|
2698
3052
|
);
|
|
2699
3053
|
|
|
@@ -2715,7 +3069,63 @@ describe('plugin-meetings', () => {
|
|
|
2715
3069
|
turnDiscoverySkippedReason: undefined,
|
|
2716
3070
|
});
|
|
2717
3071
|
meeting.meetingState = 'ACTIVE';
|
|
2718
|
-
meeting.mediaProperties.waitForMediaConnectionConnected.rejects(
|
|
3072
|
+
meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
|
|
3073
|
+
|
|
3074
|
+
const forceRtcMetricsSend = sinon.stub().resolves();
|
|
3075
|
+
const closeMediaConnectionStub = sinon.stub();
|
|
3076
|
+
Media.createMediaConnection = sinon.stub().returns({
|
|
3077
|
+
close: closeMediaConnectionStub,
|
|
3078
|
+
forceRtcMetricsSend,
|
|
3079
|
+
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
|
|
3080
|
+
initiateOffer: sinon.stub().resolves({}),
|
|
3081
|
+
on: sinon.stub(),
|
|
3082
|
+
});
|
|
3083
|
+
|
|
3084
|
+
await meeting
|
|
3085
|
+
.addMedia({
|
|
3086
|
+
mediaSettings: {},
|
|
3087
|
+
})
|
|
3088
|
+
.catch((err) => {
|
|
3089
|
+
errorThrown = err;
|
|
3090
|
+
assert.instanceOf(err, AddMediaFailed);
|
|
3091
|
+
});
|
|
3092
|
+
|
|
3093
|
+
// Check that the only metric sent is ADD_MEDIA_FAILURE
|
|
3094
|
+
assert.calledOnceWithExactly(
|
|
3095
|
+
Metrics.sendBehavioralMetric,
|
|
3096
|
+
BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
|
|
3097
|
+
{
|
|
3098
|
+
correlation_id: meeting.correlationId,
|
|
3099
|
+
locus_id: meeting.locusUrl.split('/').pop(),
|
|
3100
|
+
reason: errorThrown.message,
|
|
3101
|
+
stack: errorThrown.stack,
|
|
3102
|
+
code: errorThrown.code,
|
|
3103
|
+
turnDiscoverySkippedReason: undefined,
|
|
3104
|
+
turnServerUsed: true,
|
|
3105
|
+
retriedWithTurnServer: false,
|
|
3106
|
+
isMultistream: false,
|
|
3107
|
+
isJoinWithMediaRetry: false,
|
|
3108
|
+
signalingState: 'unknown',
|
|
3109
|
+
connectionState: 'unknown',
|
|
3110
|
+
iceConnectionState: 'unknown',
|
|
3111
|
+
selectedCandidatePairChanges: 2,
|
|
3112
|
+
numTransports: 1,
|
|
3113
|
+
iceCandidatesCount: 0,
|
|
3114
|
+
}
|
|
3115
|
+
);
|
|
3116
|
+
|
|
3117
|
+
assert.isOk(errorThrown);
|
|
3118
|
+
});
|
|
3119
|
+
|
|
3120
|
+
it('should send ICE_CANDIDATE_ERROR metric if media connection fails and ice candidate errors have been gathered', async () => {
|
|
3121
|
+
let errorThrown = undefined;
|
|
3122
|
+
|
|
3123
|
+
meeting.roap.doTurnDiscovery = sinon.stub().returns({
|
|
3124
|
+
turnServerInfo: undefined,
|
|
3125
|
+
turnDiscoverySkippedReason: undefined,
|
|
3126
|
+
});
|
|
3127
|
+
meeting.meetingState = 'ACTIVE';
|
|
3128
|
+
meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
|
|
2719
3129
|
|
|
2720
3130
|
const forceRtcMetricsSend = sinon.stub().resolves();
|
|
2721
3131
|
const closeMediaConnectionStub = sinon.stub();
|
|
@@ -2727,6 +3137,9 @@ describe('plugin-meetings', () => {
|
|
|
2727
3137
|
on: sinon.stub(),
|
|
2728
3138
|
});
|
|
2729
3139
|
|
|
3140
|
+
meeting.iceCandidateErrors.set('701_error', 2);
|
|
3141
|
+
meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
|
|
3142
|
+
|
|
2730
3143
|
await meeting
|
|
2731
3144
|
.addMedia({
|
|
2732
3145
|
mediaSettings: {},
|
|
@@ -2756,6 +3169,9 @@ describe('plugin-meetings', () => {
|
|
|
2756
3169
|
iceConnectionState: 'unknown',
|
|
2757
3170
|
selectedCandidatePairChanges: 2,
|
|
2758
3171
|
numTransports: 1,
|
|
3172
|
+
'701_error': 2,
|
|
3173
|
+
'701_turn_host_lookup_received_error': 1,
|
|
3174
|
+
iceCandidatesCount: 0,
|
|
2759
3175
|
}
|
|
2760
3176
|
);
|
|
2761
3177
|
|
|
@@ -2775,7 +3191,7 @@ describe('plugin-meetings', () => {
|
|
|
2775
3191
|
|
|
2776
3192
|
statsAnalyzerStub = new EventsScope();
|
|
2777
3193
|
// mock the StatsAnalyzer constructor
|
|
2778
|
-
sinon.stub(
|
|
3194
|
+
sinon.stub(InternalMediaCoreModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
|
|
2779
3195
|
|
|
2780
3196
|
await meeting.addMedia({
|
|
2781
3197
|
mediaSettings: {},
|
|
@@ -2789,8 +3205,8 @@ describe('plugin-meetings', () => {
|
|
|
2789
3205
|
it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and sends metrics', async () => {
|
|
2790
3206
|
statsAnalyzerStub.emit(
|
|
2791
3207
|
{file: 'test', function: 'test'},
|
|
2792
|
-
|
|
2793
|
-
{
|
|
3208
|
+
StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED,
|
|
3209
|
+
{mediaType: 'audio'}
|
|
2794
3210
|
);
|
|
2795
3211
|
|
|
2796
3212
|
assert.calledWith(
|
|
@@ -2802,7 +3218,7 @@ describe('plugin-meetings', () => {
|
|
|
2802
3218
|
},
|
|
2803
3219
|
EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED,
|
|
2804
3220
|
{
|
|
2805
|
-
|
|
3221
|
+
mediaType: 'audio',
|
|
2806
3222
|
}
|
|
2807
3223
|
);
|
|
2808
3224
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2817,8 +3233,8 @@ describe('plugin-meetings', () => {
|
|
|
2817
3233
|
it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => {
|
|
2818
3234
|
statsAnalyzerStub.emit(
|
|
2819
3235
|
{file: 'test', function: 'test'},
|
|
2820
|
-
|
|
2821
|
-
{
|
|
3236
|
+
StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED,
|
|
3237
|
+
{mediaType: 'video'}
|
|
2822
3238
|
);
|
|
2823
3239
|
|
|
2824
3240
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2833,8 +3249,8 @@ describe('plugin-meetings', () => {
|
|
|
2833
3249
|
it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => {
|
|
2834
3250
|
statsAnalyzerStub.emit(
|
|
2835
3251
|
{file: 'test', function: 'test'},
|
|
2836
|
-
|
|
2837
|
-
{
|
|
3252
|
+
StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
|
|
3253
|
+
{mediaType: 'video'}
|
|
2838
3254
|
);
|
|
2839
3255
|
|
|
2840
3256
|
assert.calledWith(
|
|
@@ -2846,7 +3262,7 @@ describe('plugin-meetings', () => {
|
|
|
2846
3262
|
},
|
|
2847
3263
|
EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
|
|
2848
3264
|
{
|
|
2849
|
-
|
|
3265
|
+
mediaType: 'video',
|
|
2850
3266
|
}
|
|
2851
3267
|
);
|
|
2852
3268
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2861,8 +3277,8 @@ describe('plugin-meetings', () => {
|
|
|
2861
3277
|
it('REMOTE_MEDIA_STOPPED triggers the right metrics', async () => {
|
|
2862
3278
|
statsAnalyzerStub.emit(
|
|
2863
3279
|
{file: 'test', function: 'test'},
|
|
2864
|
-
|
|
2865
|
-
{
|
|
3280
|
+
StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
|
|
3281
|
+
{mediaType: 'audio'}
|
|
2866
3282
|
);
|
|
2867
3283
|
|
|
2868
3284
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2877,8 +3293,8 @@ describe('plugin-meetings', () => {
|
|
|
2877
3293
|
it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics for share', async () => {
|
|
2878
3294
|
statsAnalyzerStub.emit(
|
|
2879
3295
|
{file: 'test', function: 'test'},
|
|
2880
|
-
|
|
2881
|
-
{
|
|
3296
|
+
StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
|
|
3297
|
+
{mediaType: 'share'}
|
|
2882
3298
|
);
|
|
2883
3299
|
|
|
2884
3300
|
assert.calledWith(
|
|
@@ -2890,7 +3306,7 @@ describe('plugin-meetings', () => {
|
|
|
2890
3306
|
},
|
|
2891
3307
|
EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
|
|
2892
3308
|
{
|
|
2893
|
-
|
|
3309
|
+
mediaType: 'share',
|
|
2894
3310
|
}
|
|
2895
3311
|
);
|
|
2896
3312
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2913,8 +3329,8 @@ describe('plugin-meetings', () => {
|
|
|
2913
3329
|
it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
|
|
2914
3330
|
statsAnalyzerStub.emit(
|
|
2915
3331
|
{file: 'test', function: 'test'},
|
|
2916
|
-
|
|
2917
|
-
{
|
|
3332
|
+
StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
|
|
3333
|
+
{mediaType: 'share'}
|
|
2918
3334
|
);
|
|
2919
3335
|
|
|
2920
3336
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2935,19 +3351,18 @@ describe('plugin-meetings', () => {
|
|
|
2935
3351
|
});
|
|
2936
3352
|
|
|
2937
3353
|
it('calls submitMQE correctly', async () => {
|
|
2938
|
-
const fakeData = {intervalMetadata: {bla: 'bla'}};
|
|
3354
|
+
const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
|
|
2939
3355
|
|
|
2940
3356
|
statsAnalyzerStub.emit(
|
|
2941
3357
|
{file: 'test', function: 'test'},
|
|
2942
|
-
|
|
2943
|
-
{data: fakeData
|
|
3358
|
+
StatsAnalyzerEventNames.MEDIA_QUALITY,
|
|
3359
|
+
{data: fakeData}
|
|
2944
3360
|
);
|
|
2945
3361
|
|
|
2946
3362
|
assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
|
|
2947
3363
|
name: 'client.mediaquality.event',
|
|
2948
3364
|
options: {
|
|
2949
3365
|
meetingId: meeting.id,
|
|
2950
|
-
networkType: 'wifi',
|
|
2951
3366
|
},
|
|
2952
3367
|
payload: {
|
|
2953
3368
|
intervals: [fakeData],
|
|
@@ -3004,7 +3419,7 @@ describe('plugin-meetings', () => {
|
|
|
3004
3419
|
it('succeeds even if getDevices() throws', async () => {
|
|
3005
3420
|
meeting.meetingState = 'ACTIVE';
|
|
3006
3421
|
|
|
3007
|
-
sinon.stub(
|
|
3422
|
+
sinon.stub(InternalMediaCoreModule, 'getDevices').rejects(new Error('fake error'));
|
|
3008
3423
|
|
|
3009
3424
|
await meeting.addMedia();
|
|
3010
3425
|
});
|
|
@@ -3021,7 +3436,7 @@ describe('plugin-meetings', () => {
|
|
|
3021
3436
|
clientErrorCode: MISSING_ROAP_ANSWER_CLIENT_CODE,
|
|
3022
3437
|
expectedErrorPayload: {
|
|
3023
3438
|
errorDescription: ERROR_DESCRIPTIONS.MISSING_ROAP_ANSWER,
|
|
3024
|
-
category: '
|
|
3439
|
+
category: 'media',
|
|
3025
3440
|
},
|
|
3026
3441
|
},
|
|
3027
3442
|
{
|
|
@@ -3040,10 +3455,18 @@ describe('plugin-meetings', () => {
|
|
|
3040
3455
|
clientErrorCode: ICE_FAILED_WITH_TURN_TLS_CLIENT_CODE,
|
|
3041
3456
|
expectedErrorPayload: {
|
|
3042
3457
|
errorDescription: ERROR_DESCRIPTIONS.ICE_FAILED_WITH_TURN_TLS,
|
|
3043
|
-
category: '
|
|
3458
|
+
category: 'media',
|
|
3459
|
+
},
|
|
3460
|
+
},
|
|
3461
|
+
{
|
|
3462
|
+
clientErrorCode: ICE_AND_REACHABILITY_FAILED_CLIENT_CODE,
|
|
3463
|
+
unreachable: true,
|
|
3464
|
+
expectedErrorPayload: {
|
|
3465
|
+
errorDescription: ERROR_DESCRIPTIONS.ICE_AND_REACHABILITY_FAILED,
|
|
3466
|
+
category: 'expected',
|
|
3044
3467
|
},
|
|
3045
3468
|
},
|
|
3046
|
-
].forEach(({clientErrorCode, expectedErrorPayload}) => {
|
|
3469
|
+
].forEach(({clientErrorCode, expectedErrorPayload, unreachable}) => {
|
|
3047
3470
|
it(`should handle all ice failures correctly for ${clientErrorCode}`, async () => {
|
|
3048
3471
|
// setting the method to the real implementation
|
|
3049
3472
|
// because newMetrics is mocked completely in the webex-mock
|
|
@@ -3052,14 +3475,18 @@ describe('plugin-meetings', () => {
|
|
|
3052
3475
|
webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
|
|
3053
3476
|
CD.getErrorPayloadForClientErrorCode;
|
|
3054
3477
|
|
|
3478
|
+
webex.meetings.reachability = {
|
|
3479
|
+
isWebexMediaBackendUnreachable: sinon.stub().resolves(unreachable || false),
|
|
3480
|
+
};
|
|
3481
|
+
|
|
3055
3482
|
const generateClientErrorCodeForIceFailureStub = sinon
|
|
3056
3483
|
.stub(CallDiagnosticUtils, 'generateClientErrorCodeForIceFailure')
|
|
3057
3484
|
.returns(clientErrorCode);
|
|
3058
3485
|
|
|
3059
3486
|
meeting.meetingState = 'ACTIVE';
|
|
3060
|
-
meeting.mediaProperties.waitForMediaConnectionConnected.rejects(
|
|
3061
|
-
|
|
3062
|
-
);
|
|
3487
|
+
meeting.mediaProperties.waitForMediaConnectionConnected.rejects({
|
|
3488
|
+
iceConnected: false,
|
|
3489
|
+
});
|
|
3063
3490
|
|
|
3064
3491
|
let errorThrown = false;
|
|
3065
3492
|
|
|
@@ -3073,8 +3500,9 @@ describe('plugin-meetings', () => {
|
|
|
3073
3500
|
|
|
3074
3501
|
assert.calledOnceWithExactly(generateClientErrorCodeForIceFailureStub, {
|
|
3075
3502
|
signalingState: 'unknown',
|
|
3076
|
-
|
|
3503
|
+
iceConnected: false,
|
|
3077
3504
|
turnServerUsed: true,
|
|
3505
|
+
unreachable: unreachable || false,
|
|
3078
3506
|
});
|
|
3079
3507
|
|
|
3080
3508
|
const submitClientEventCalls = webex.internal.newMetrics.submitClientEvent.getCalls();
|
|
@@ -3162,7 +3590,7 @@ describe('plugin-meetings', () => {
|
|
|
3162
3590
|
|
|
3163
3591
|
let clock;
|
|
3164
3592
|
|
|
3165
|
-
beforeEach(() => {
|
|
3593
|
+
beforeEach(async () => {
|
|
3166
3594
|
clock = sinon.useFakeTimers();
|
|
3167
3595
|
|
|
3168
3596
|
sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.unknown);
|
|
@@ -3171,15 +3599,20 @@ describe('plugin-meetings', () => {
|
|
|
3171
3599
|
meeting.config.deviceType = 'web';
|
|
3172
3600
|
meeting.isMultistream = isMultistream;
|
|
3173
3601
|
meeting.meetingState = 'ACTIVE';
|
|
3174
|
-
meeting.mediaId = 'fake media id';
|
|
3175
3602
|
meeting.selfUrl = 'selfUrl';
|
|
3176
3603
|
meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
|
|
3177
|
-
meeting.mediaProperties.getCurrentConnectionInfo = sinon
|
|
3604
|
+
meeting.mediaProperties.getCurrentConnectionInfo = sinon
|
|
3605
|
+
.stub()
|
|
3606
|
+
.resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
|
|
3178
3607
|
meeting.setMercuryListener = sinon.stub();
|
|
3179
3608
|
meeting.locusInfo.onFullLocus = sinon.stub();
|
|
3180
3609
|
meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
|
|
3181
3610
|
meeting.roap.doTurnDiscovery = sinon.stub().resolves({
|
|
3182
|
-
turnServerInfo: {
|
|
3611
|
+
turnServerInfo: {
|
|
3612
|
+
url: 'turns:turn-server-url:443?transport=tcp',
|
|
3613
|
+
username: 'turn user',
|
|
3614
|
+
password: 'turn password',
|
|
3615
|
+
},
|
|
3183
3616
|
turnDiscoverySkippedReason: 'reachability',
|
|
3184
3617
|
});
|
|
3185
3618
|
meeting.deferSDPAnswer = new Defer();
|
|
@@ -3192,7 +3625,18 @@ describe('plugin-meetings', () => {
|
|
|
3192
3625
|
// setup things that are expected to be the same across all the tests and are actually irrelevant for these tests
|
|
3193
3626
|
expectedDebugId = `MC-${meeting.id.substring(0, 4)}`;
|
|
3194
3627
|
expectedMediaConnectionConfig = {
|
|
3195
|
-
iceServers: [
|
|
3628
|
+
iceServers: [
|
|
3629
|
+
{
|
|
3630
|
+
urls: 'turn:turn-server-url:5004?transport=tcp',
|
|
3631
|
+
username: 'turn user',
|
|
3632
|
+
credential: 'turn password',
|
|
3633
|
+
},
|
|
3634
|
+
{
|
|
3635
|
+
urls: 'turns:turn-server-url:443?transport=tcp',
|
|
3636
|
+
username: 'turn user',
|
|
3637
|
+
credential: 'turn password',
|
|
3638
|
+
},
|
|
3639
|
+
],
|
|
3196
3640
|
skipInactiveTransceivers: false,
|
|
3197
3641
|
requireH264: true,
|
|
3198
3642
|
sdpMunging: {
|
|
@@ -3261,16 +3705,28 @@ describe('plugin-meetings', () => {
|
|
|
3261
3705
|
};
|
|
3262
3706
|
|
|
3263
3707
|
roapMediaConnectionConstructorStub = sinon
|
|
3264
|
-
.stub(
|
|
3708
|
+
.stub(InternalMediaCoreModule, 'RoapMediaConnection')
|
|
3265
3709
|
.returns(fakeRoapMediaConnection);
|
|
3266
3710
|
|
|
3267
3711
|
multistreamRoapMediaConnectionConstructorStub = sinon
|
|
3268
|
-
.stub(
|
|
3712
|
+
.stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
|
|
3269
3713
|
.returns(fakeMultistreamRoapMediaConnection);
|
|
3270
3714
|
|
|
3271
3715
|
locusMediaRequestStub = sinon
|
|
3272
3716
|
.stub(WebexPlugin.prototype, 'request')
|
|
3273
3717
|
.resolves({body: {locus: {fullState: {}}}});
|
|
3718
|
+
|
|
3719
|
+
// setup some things and mocks so that the call to join() works
|
|
3720
|
+
// (we need to call join() because it creates the LocusMediaRequest instance
|
|
3721
|
+
// that's being tested in these tests)
|
|
3722
|
+
meeting.webex.meetings.registered = true;
|
|
3723
|
+
meeting.webex.internal.device.config = {};
|
|
3724
|
+
sinon.stub(MeetingUtil, 'joinMeeting').resolves({
|
|
3725
|
+
id: 'fake locus from mocked join request',
|
|
3726
|
+
locusUrl: 'fake locus url',
|
|
3727
|
+
mediaId: 'fake media id',
|
|
3728
|
+
});
|
|
3729
|
+
await meeting.join({enableMultistream: isMultistream});
|
|
3274
3730
|
});
|
|
3275
3731
|
|
|
3276
3732
|
afterEach(() => {
|
|
@@ -3299,13 +3755,14 @@ describe('plugin-meetings', () => {
|
|
|
3299
3755
|
|
|
3300
3756
|
for (let idx = 0; idx < roapMediaConnectionToCheck.on.callCount; idx += 1) {
|
|
3301
3757
|
if (
|
|
3302
|
-
roapMediaConnectionToCheck.on.getCall(idx).args[0] ===
|
|
3758
|
+
roapMediaConnectionToCheck.on.getCall(idx).args[0] ===
|
|
3759
|
+
MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND
|
|
3303
3760
|
) {
|
|
3304
3761
|
return roapMediaConnectionToCheck.on.getCall(idx).args[1];
|
|
3305
3762
|
}
|
|
3306
3763
|
}
|
|
3307
3764
|
assert.fail(
|
|
3308
|
-
'listener for "roap:messageToSend" (
|
|
3765
|
+
'listener for "roap:messageToSend" (MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND) was not registered'
|
|
3309
3766
|
);
|
|
3310
3767
|
};
|
|
3311
3768
|
|
|
@@ -3746,13 +4203,13 @@ describe('plugin-meetings', () => {
|
|
|
3746
4203
|
await meeting.addMedia({
|
|
3747
4204
|
localStreams: {microphone: fakeMicrophoneStream},
|
|
3748
4205
|
audioEnabled: false,
|
|
3749
|
-
videoEnabled: false
|
|
4206
|
+
videoEnabled: false,
|
|
3750
4207
|
});
|
|
3751
4208
|
await simulateRoapOffer();
|
|
3752
4209
|
await simulateRoapOk();
|
|
3753
4210
|
|
|
3754
4211
|
assert.notCalled(handleDeviceLoggingSpy);
|
|
3755
|
-
})
|
|
4212
|
+
});
|
|
3756
4213
|
|
|
3757
4214
|
it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
|
|
3758
4215
|
await meeting.addMedia({audioEnabled: false});
|
|
@@ -5107,7 +5564,7 @@ describe('plugin-meetings', () => {
|
|
|
5107
5564
|
|
|
5108
5565
|
describe('#fetchMeetingInfo', () => {
|
|
5109
5566
|
const FAKE_DESTINATION = 'something@somecompany.com';
|
|
5110
|
-
const FAKE_TYPE =
|
|
5567
|
+
const FAKE_TYPE = DESTINATION_TYPE.SIP_URI;
|
|
5111
5568
|
const FAKE_TIMEOUT_FETCHMEETINGINFO_ID = '123456';
|
|
5112
5569
|
const FAKE_PASSWORD = '123abc';
|
|
5113
5570
|
const FAKE_CAPTCHA_CODE = 'a1b2c3XYZ';
|
|
@@ -5542,7 +5999,7 @@ describe('plugin-meetings', () => {
|
|
|
5542
5999
|
const FAKE_PASSWORD = '123456';
|
|
5543
6000
|
const FAKE_CAPTCHA_CODE = '654321';
|
|
5544
6001
|
const FAKE_DESTINATION = 'something@somecompany.com';
|
|
5545
|
-
const FAKE_TYPE =
|
|
6002
|
+
const FAKE_TYPE = DESTINATION_TYPE.SIP_URI;
|
|
5546
6003
|
const FAKE_INSTALLED_ORG_ID = '123456';
|
|
5547
6004
|
const FAKE_MEETING_INFO_LOOKUP_URL = 'meetingLookupUrl';
|
|
5548
6005
|
|
|
@@ -6187,14 +6644,14 @@ describe('plugin-meetings', () => {
|
|
|
6187
6644
|
beforeEach(() => {
|
|
6188
6645
|
sandbox = sinon.createSandbox();
|
|
6189
6646
|
meeting.statsAnalyzer = {
|
|
6190
|
-
stopAnalyzer: sinon.stub().returns(Promise.resolve())
|
|
6647
|
+
stopAnalyzer: sinon.stub().returns(Promise.resolve()),
|
|
6191
6648
|
};
|
|
6192
6649
|
|
|
6193
6650
|
meeting.reconnectionManager = {
|
|
6194
|
-
cleanUp: sinon.stub()
|
|
6651
|
+
cleanUp: sinon.stub(),
|
|
6195
6652
|
};
|
|
6196
6653
|
|
|
6197
|
-
meeting.cleanupLocalStreams=sinon.stub();
|
|
6654
|
+
meeting.cleanupLocalStreams = sinon.stub();
|
|
6198
6655
|
meeting.closeRemoteStreams = sinon.stub().returns(Promise.resolve());
|
|
6199
6656
|
meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
|
|
6200
6657
|
meeting.unsetRemoteStreams = sinon.stub();
|
|
@@ -6275,7 +6732,6 @@ describe('plugin-meetings', () => {
|
|
|
6275
6732
|
},
|
|
6276
6733
|
'SELF_OBSERVING'
|
|
6277
6734
|
);
|
|
6278
|
-
|
|
6279
6735
|
|
|
6280
6736
|
// Verify that the event handler behaves as expected
|
|
6281
6737
|
expect(meeting.statsAnalyzer.stopAnalyzer.calledOnce).to.be.true;
|
|
@@ -6288,11 +6744,13 @@ describe('plugin-meetings', () => {
|
|
|
6288
6744
|
expect(meeting.unsetPeerConnections.calledOnce).to.be.true;
|
|
6289
6745
|
expect(meeting.reconnectionManager.cleanUp.calledOnce).to.be.true;
|
|
6290
6746
|
expect(meeting.mediaProperties.setMediaDirection.calledOnce).to.be.true;
|
|
6291
|
-
expect(
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6747
|
+
expect(
|
|
6748
|
+
meeting.addMedia.calledOnceWithExactly({
|
|
6749
|
+
audioEnabled: false,
|
|
6750
|
+
videoEnabled: false,
|
|
6751
|
+
shareVideoEnabled: true,
|
|
6752
|
+
})
|
|
6753
|
+
).to.be.true;
|
|
6296
6754
|
await testUtils.flushPromises();
|
|
6297
6755
|
assert.equal(meeting.isMoveToInProgress, false);
|
|
6298
6756
|
});
|
|
@@ -7079,6 +7537,12 @@ describe('plugin-meetings', () => {
|
|
|
7079
7537
|
id: 'stream',
|
|
7080
7538
|
getTracks: () => [{id: 'track', addEventListener: sinon.stub()}],
|
|
7081
7539
|
};
|
|
7540
|
+
const simulateConnectionStateChange = (newState) => {
|
|
7541
|
+
meeting.mediaProperties.webrtcMediaConnection.getConnectionState = sinon
|
|
7542
|
+
.stub()
|
|
7543
|
+
.returns(newState);
|
|
7544
|
+
eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]();
|
|
7545
|
+
};
|
|
7082
7546
|
|
|
7083
7547
|
beforeEach(() => {
|
|
7084
7548
|
eventListeners = {};
|
|
@@ -7088,23 +7552,29 @@ describe('plugin-meetings', () => {
|
|
|
7088
7552
|
on: sinon.stub().callsFake((event, listener) => {
|
|
7089
7553
|
eventListeners[event] = listener;
|
|
7090
7554
|
}),
|
|
7555
|
+
getConnectionState: sinon.stub().returns(ConnectionState.New),
|
|
7091
7556
|
};
|
|
7092
7557
|
MediaUtil.createMediaStream.returns(fakeStream);
|
|
7093
7558
|
});
|
|
7094
7559
|
|
|
7095
7560
|
it('should register for all the correct RoapMediaConnection events', () => {
|
|
7096
7561
|
meeting.setupMediaConnectionListeners();
|
|
7097
|
-
assert.isFunction(eventListeners[
|
|
7098
|
-
assert.isFunction(eventListeners[
|
|
7099
|
-
assert.isFunction(eventListeners[
|
|
7100
|
-
assert.isFunction(eventListeners[
|
|
7101
|
-
assert.isFunction(eventListeners[
|
|
7102
|
-
assert.isFunction(
|
|
7562
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_STARTED]);
|
|
7563
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_DONE]);
|
|
7564
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_FAILURE]);
|
|
7565
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]);
|
|
7566
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]);
|
|
7567
|
+
assert.isFunction(
|
|
7568
|
+
eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]
|
|
7569
|
+
);
|
|
7570
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED]);
|
|
7571
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]);
|
|
7572
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]);
|
|
7103
7573
|
});
|
|
7104
7574
|
|
|
7105
7575
|
it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => {
|
|
7106
7576
|
meeting.setupMediaConnectionListeners();
|
|
7107
|
-
eventListeners[
|
|
7577
|
+
eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
|
|
7108
7578
|
track: 'track',
|
|
7109
7579
|
type: RemoteTrackType.AUDIO,
|
|
7110
7580
|
});
|
|
@@ -7114,7 +7584,7 @@ describe('plugin-meetings', () => {
|
|
|
7114
7584
|
stream: fakeStream,
|
|
7115
7585
|
});
|
|
7116
7586
|
|
|
7117
|
-
eventListeners[
|
|
7587
|
+
eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
|
|
7118
7588
|
track: 'track',
|
|
7119
7589
|
type: RemoteTrackType.VIDEO,
|
|
7120
7590
|
});
|
|
@@ -7124,7 +7594,7 @@ describe('plugin-meetings', () => {
|
|
|
7124
7594
|
stream: fakeStream,
|
|
7125
7595
|
});
|
|
7126
7596
|
|
|
7127
|
-
eventListeners[
|
|
7597
|
+
eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
|
|
7128
7598
|
track: 'track',
|
|
7129
7599
|
type: RemoteTrackType.SCREENSHARE_VIDEO,
|
|
7130
7600
|
});
|
|
@@ -7135,13 +7605,78 @@ describe('plugin-meetings', () => {
|
|
|
7135
7605
|
});
|
|
7136
7606
|
});
|
|
7137
7607
|
|
|
7608
|
+
describe('should react on a ICE_CANDIDATE event', () => {
|
|
7609
|
+
beforeEach(() => {
|
|
7610
|
+
meeting.setupMediaConnectionListeners();
|
|
7611
|
+
});
|
|
7612
|
+
|
|
7613
|
+
it('should collect ice candidates', () => {
|
|
7614
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: 'candidate'});
|
|
7615
|
+
|
|
7616
|
+
assert.equal(meeting.iceCandidatesCount, 1);
|
|
7617
|
+
});
|
|
7618
|
+
|
|
7619
|
+
it('should not collect null ice candidates', () => {
|
|
7620
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: null});
|
|
7621
|
+
|
|
7622
|
+
assert.equal(meeting.iceCandidatesCount, 0);
|
|
7623
|
+
});
|
|
7624
|
+
});
|
|
7625
|
+
|
|
7626
|
+
describe('should react on a ICE_CANDIDATE_ERROR event', () => {
|
|
7627
|
+
beforeEach(() => {
|
|
7628
|
+
meeting.setupMediaConnectionListeners();
|
|
7629
|
+
});
|
|
7630
|
+
|
|
7631
|
+
it('should not collect skipped ice candidates error', () => {
|
|
7632
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
|
|
7633
|
+
error: {
|
|
7634
|
+
errorCode: 600,
|
|
7635
|
+
errorText: 'Address not associated with the desired network interface.',
|
|
7636
|
+
},
|
|
7637
|
+
});
|
|
7638
|
+
|
|
7639
|
+
assert.equal(meeting.iceCandidateErrors.size, 0);
|
|
7640
|
+
});
|
|
7641
|
+
|
|
7642
|
+
it('should collect valid ice candidates error', () => {
|
|
7643
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
|
|
7644
|
+
error: {errorCode: 701, errorText: ''},
|
|
7645
|
+
});
|
|
7646
|
+
|
|
7647
|
+
assert.equal(meeting.iceCandidateErrors.size, 1);
|
|
7648
|
+
assert.equal(meeting.iceCandidateErrors.has('701_'), true);
|
|
7649
|
+
});
|
|
7650
|
+
|
|
7651
|
+
it('should increment counter if same valid ice candidates error collected', () => {
|
|
7652
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
|
|
7653
|
+
error: {errorCode: 701, errorText: ''},
|
|
7654
|
+
});
|
|
7655
|
+
|
|
7656
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
|
|
7657
|
+
error: {errorCode: 701, errorText: 'STUN host lookup received error.'},
|
|
7658
|
+
});
|
|
7659
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
|
|
7660
|
+
error: {errorCode: 701, errorText: 'STUN host lookup received error.'},
|
|
7661
|
+
});
|
|
7662
|
+
|
|
7663
|
+
assert.equal(meeting.iceCandidateErrors.size, 2);
|
|
7664
|
+
assert.equal(meeting.iceCandidateErrors.has('701_'), true);
|
|
7665
|
+
assert.equal(meeting.iceCandidateErrors.get('701_'), 1);
|
|
7666
|
+
assert.equal(
|
|
7667
|
+
meeting.iceCandidateErrors.has('701_stun_host_lookup_received_error'),
|
|
7668
|
+
true
|
|
7669
|
+
);
|
|
7670
|
+
assert.equal(meeting.iceCandidateErrors.get('701_stun_host_lookup_received_error'), 2);
|
|
7671
|
+
});
|
|
7672
|
+
});
|
|
7673
|
+
|
|
7138
7674
|
describe('CONNECTION_STATE_CHANGED event when state = "Connecting"', () => {
|
|
7139
7675
|
it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = true', () => {
|
|
7140
7676
|
meeting.hasMediaConnectionConnectedAtLeastOnce = true;
|
|
7141
7677
|
meeting.setupMediaConnectionListeners();
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
});
|
|
7678
|
+
|
|
7679
|
+
simulateConnectionStateChange(ConnectionState.Connecting);
|
|
7145
7680
|
|
|
7146
7681
|
assert.notCalled(webex.internal.newMetrics.submitClientEvent);
|
|
7147
7682
|
});
|
|
@@ -7149,9 +7684,8 @@ describe('plugin-meetings', () => {
|
|
|
7149
7684
|
it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = false', () => {
|
|
7150
7685
|
meeting.hasMediaConnectionConnectedAtLeastOnce = false;
|
|
7151
7686
|
meeting.setupMediaConnectionListeners();
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
});
|
|
7687
|
+
|
|
7688
|
+
simulateConnectionStateChange(ConnectionState.Connecting);
|
|
7155
7689
|
|
|
7156
7690
|
assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
|
|
7157
7691
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -7177,6 +7711,7 @@ describe('plugin-meetings', () => {
|
|
|
7177
7711
|
on: sinon.stub().callsFake((event, listener) => {
|
|
7178
7712
|
eventListeners[event] = listener;
|
|
7179
7713
|
}),
|
|
7714
|
+
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
|
|
7180
7715
|
};
|
|
7181
7716
|
};
|
|
7182
7717
|
|
|
@@ -7230,9 +7765,7 @@ describe('plugin-meetings', () => {
|
|
|
7230
7765
|
assert.equal(meeting.hasMediaConnectionConnectedAtLeastOnce, false);
|
|
7231
7766
|
|
|
7232
7767
|
// simulate first connection success
|
|
7233
|
-
|
|
7234
|
-
state: 'Connected',
|
|
7235
|
-
});
|
|
7768
|
+
simulateConnectionStateChange(ConnectionState.Connected);
|
|
7236
7769
|
checkExpectedSpies({
|
|
7237
7770
|
icePhase: 'JOIN_MEETING_FINAL',
|
|
7238
7771
|
setNetworkStatusCallParams: [NETWORK_STATUS.CONNECTED],
|
|
@@ -7242,12 +7775,9 @@ describe('plugin-meetings', () => {
|
|
|
7242
7775
|
// now simulate short connection loss, client.ice.end is not sent a second time as hasMediaConnectionConnectedAtLeastOnce = true
|
|
7243
7776
|
resetSpies();
|
|
7244
7777
|
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
eventListeners[Event.CONNECTION_STATE_CHANGED]({
|
|
7249
|
-
state: 'Connected',
|
|
7250
|
-
});
|
|
7778
|
+
simulateConnectionStateChange(ConnectionState.Disconnected);
|
|
7779
|
+
|
|
7780
|
+
simulateConnectionStateChange(ConnectionState.Connected);
|
|
7251
7781
|
|
|
7252
7782
|
checkExpectedSpies({
|
|
7253
7783
|
setNetworkStatusCallParams: [NETWORK_STATUS.DISCONNECTED, NETWORK_STATUS.CONNECTED],
|
|
@@ -7255,12 +7785,9 @@ describe('plugin-meetings', () => {
|
|
|
7255
7785
|
|
|
7256
7786
|
resetSpies();
|
|
7257
7787
|
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
eventListeners[Event.CONNECTION_STATE_CHANGED]({
|
|
7262
|
-
state: 'Connected',
|
|
7263
|
-
});
|
|
7788
|
+
simulateConnectionStateChange(ConnectionState.Disconnected);
|
|
7789
|
+
|
|
7790
|
+
simulateConnectionStateChange(ConnectionState.Connected);
|
|
7264
7791
|
});
|
|
7265
7792
|
});
|
|
7266
7793
|
|
|
@@ -7282,9 +7809,8 @@ describe('plugin-meetings', () => {
|
|
|
7282
7809
|
|
|
7283
7810
|
const mockDisconnectedEvent = () => {
|
|
7284
7811
|
meeting.setupMediaConnectionListeners();
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
});
|
|
7812
|
+
|
|
7813
|
+
simulateConnectionStateChange(ConnectionState.Disconnected);
|
|
7288
7814
|
};
|
|
7289
7815
|
|
|
7290
7816
|
const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
|
|
@@ -7348,9 +7874,8 @@ describe('plugin-meetings', () => {
|
|
|
7348
7874
|
describe('CONNECTION_STATE_CHANGED event when state = "Failed"', () => {
|
|
7349
7875
|
const mockFailedEvent = () => {
|
|
7350
7876
|
meeting.setupMediaConnectionListeners();
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
});
|
|
7877
|
+
|
|
7878
|
+
simulateConnectionStateChange(ConnectionState.Failed);
|
|
7354
7879
|
};
|
|
7355
7880
|
|
|
7356
7881
|
const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
|
|
@@ -7432,7 +7957,7 @@ describe('plugin-meetings', () => {
|
|
|
7432
7957
|
cause: {name: fakeRootCauseName},
|
|
7433
7958
|
});
|
|
7434
7959
|
|
|
7435
|
-
eventListeners[
|
|
7960
|
+
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
|
|
7436
7961
|
|
|
7437
7962
|
checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
|
|
7438
7963
|
checkBehavioralMetricSent(
|
|
@@ -7449,7 +7974,7 @@ describe('plugin-meetings', () => {
|
|
|
7449
7974
|
cause: {name: fakeRootCauseName},
|
|
7450
7975
|
});
|
|
7451
7976
|
|
|
7452
|
-
eventListeners[
|
|
7977
|
+
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
|
|
7453
7978
|
|
|
7454
7979
|
checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
|
|
7455
7980
|
checkBehavioralMetricSent(
|
|
@@ -7466,7 +7991,7 @@ describe('plugin-meetings', () => {
|
|
|
7466
7991
|
cause: {name: fakeRootCauseName},
|
|
7467
7992
|
});
|
|
7468
7993
|
|
|
7469
|
-
eventListeners[
|
|
7994
|
+
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
|
|
7470
7995
|
|
|
7471
7996
|
checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
|
|
7472
7997
|
checkBehavioralMetricSent(
|
|
@@ -7481,7 +8006,7 @@ describe('plugin-meetings', () => {
|
|
|
7481
8006
|
// SdpError is usually without a cause
|
|
7482
8007
|
const fakeError = new Errors.SdpError(fakeErrorMessage, {name: fakeErrorName});
|
|
7483
8008
|
|
|
7484
|
-
eventListeners[
|
|
8009
|
+
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
|
|
7485
8010
|
|
|
7486
8011
|
checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
|
|
7487
8012
|
// expectedMetadataType is the error name in this case
|
|
@@ -7499,7 +8024,7 @@ describe('plugin-meetings', () => {
|
|
|
7499
8024
|
name: fakeErrorName,
|
|
7500
8025
|
});
|
|
7501
8026
|
|
|
7502
|
-
eventListeners[
|
|
8027
|
+
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
|
|
7503
8028
|
|
|
7504
8029
|
checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
|
|
7505
8030
|
// expectedMetadataType is the error name in this case
|
|
@@ -7525,7 +8050,7 @@ describe('plugin-meetings', () => {
|
|
|
7525
8050
|
};
|
|
7526
8051
|
meeting.sdpResponseTimer = '1234';
|
|
7527
8052
|
|
|
7528
|
-
eventListeners[
|
|
8053
|
+
eventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED]();
|
|
7529
8054
|
|
|
7530
8055
|
assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
|
|
7531
8056
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -7553,7 +8078,7 @@ describe('plugin-meetings', () => {
|
|
|
7553
8078
|
it('handles LOCAL_SDP_OFFER_GENERATED correctly', () => {
|
|
7554
8079
|
assert.equal(meeting.deferSDPAnswer, undefined);
|
|
7555
8080
|
|
|
7556
|
-
eventListeners[
|
|
8081
|
+
eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
|
|
7557
8082
|
|
|
7558
8083
|
assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
|
|
7559
8084
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -7565,7 +8090,7 @@ describe('plugin-meetings', () => {
|
|
|
7565
8090
|
});
|
|
7566
8091
|
|
|
7567
8092
|
it('handles LOCAL_SDP_ANSWER_GENERATED correctly', () => {
|
|
7568
|
-
eventListeners[
|
|
8093
|
+
eventListeners[MediaConnectionEventNames.LOCAL_SDP_ANSWER_GENERATED]();
|
|
7569
8094
|
|
|
7570
8095
|
assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
|
|
7571
8096
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -7575,7 +8100,7 @@ describe('plugin-meetings', () => {
|
|
|
7575
8100
|
});
|
|
7576
8101
|
});
|
|
7577
8102
|
|
|
7578
|
-
describe('handles
|
|
8103
|
+
describe('handles MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND correctly', () => {
|
|
7579
8104
|
let sendRoapOKStub;
|
|
7580
8105
|
let sendRoapMediaRequestStub;
|
|
7581
8106
|
let sendRoapAnswerStub;
|
|
@@ -7593,7 +8118,7 @@ describe('plugin-meetings', () => {
|
|
|
7593
8118
|
});
|
|
7594
8119
|
|
|
7595
8120
|
it('handles OK message correctly', () => {
|
|
7596
|
-
eventListeners[
|
|
8121
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7597
8122
|
roapMessage: {messageType: 'OK', seq: 1},
|
|
7598
8123
|
});
|
|
7599
8124
|
|
|
@@ -7608,7 +8133,7 @@ describe('plugin-meetings', () => {
|
|
|
7608
8133
|
it('handles OFFER message correctly (no answer in the http response)', async () => {
|
|
7609
8134
|
sinon.stub(meeting, 'roapMessageReceived');
|
|
7610
8135
|
|
|
7611
|
-
eventListeners[
|
|
8136
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7612
8137
|
roapMessage: {
|
|
7613
8138
|
messageType: 'OFFER',
|
|
7614
8139
|
seq: 1,
|
|
@@ -7634,7 +8159,7 @@ describe('plugin-meetings', () => {
|
|
|
7634
8159
|
sendRoapMediaRequestStub.resolves({roapAnswer: fakeAnswer});
|
|
7635
8160
|
sinon.stub(meeting, 'roapMessageReceived');
|
|
7636
8161
|
|
|
7637
|
-
eventListeners[
|
|
8162
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7638
8163
|
roapMessage: {
|
|
7639
8164
|
messageType: 'OFFER',
|
|
7640
8165
|
seq: 1,
|
|
@@ -7656,14 +8181,20 @@ describe('plugin-meetings', () => {
|
|
|
7656
8181
|
});
|
|
7657
8182
|
|
|
7658
8183
|
it('handles OFFER message correctly when request fails', async () => {
|
|
8184
|
+
const fakeError = new Error('fake error');
|
|
7659
8185
|
const clock = sinon.useFakeTimers();
|
|
7660
8186
|
sinon.spy(clock, 'clearTimeout');
|
|
7661
8187
|
meeting.deferSDPAnswer = {reject: sinon.stub()};
|
|
7662
8188
|
meeting.sdpResponseTimer = '1234';
|
|
7663
|
-
sendRoapMediaRequestStub.rejects();
|
|
8189
|
+
sendRoapMediaRequestStub.rejects(fakeError);
|
|
7664
8190
|
sinon.stub(meeting, 'roapMessageReceived');
|
|
8191
|
+
const getErrorPayloadForClientErrorCodeStub =
|
|
8192
|
+
(webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
|
|
8193
|
+
sinon
|
|
8194
|
+
.stub()
|
|
8195
|
+
.callsFake(({clientErrorCode}) => ({errorCode: clientErrorCode, fatal: true})));
|
|
7665
8196
|
|
|
7666
|
-
eventListeners[
|
|
8197
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7667
8198
|
roapMessage: {
|
|
7668
8199
|
messageType: 'OFFER',
|
|
7669
8200
|
seq: 1,
|
|
@@ -7686,10 +8217,25 @@ describe('plugin-meetings', () => {
|
|
|
7686
8217
|
assert.calledOnce(clock.clearTimeout);
|
|
7687
8218
|
assert.calledWith(clock.clearTimeout, '1234');
|
|
7688
8219
|
assert.equal(meeting.sdpResponseTimer, undefined);
|
|
8220
|
+
|
|
8221
|
+
assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
|
|
8222
|
+
clientErrorCode: 2007,
|
|
8223
|
+
});
|
|
8224
|
+
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
8225
|
+
name: 'client.media-engine.remote-sdp-received',
|
|
8226
|
+
payload: {
|
|
8227
|
+
canProceed: false,
|
|
8228
|
+
errors: [{errorCode: 2007, fatal: true}],
|
|
8229
|
+
},
|
|
8230
|
+
options: {
|
|
8231
|
+
meetingId: meeting.id,
|
|
8232
|
+
rawError: fakeError,
|
|
8233
|
+
},
|
|
8234
|
+
});
|
|
7689
8235
|
});
|
|
7690
8236
|
|
|
7691
8237
|
it('handles ANSWER message correctly', () => {
|
|
7692
|
-
eventListeners[
|
|
8238
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7693
8239
|
roapMessage: {
|
|
7694
8240
|
messageType: 'ANSWER',
|
|
7695
8241
|
seq: 10,
|
|
@@ -7710,7 +8256,7 @@ describe('plugin-meetings', () => {
|
|
|
7710
8256
|
it('sends metrics if fails to send roap ANSWER message', async () => {
|
|
7711
8257
|
sendRoapAnswerStub.rejects(new Error('sending answer failed'));
|
|
7712
8258
|
|
|
7713
|
-
await eventListeners[
|
|
8259
|
+
await eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7714
8260
|
roapMessage: {
|
|
7715
8261
|
messageType: 'ANSWER',
|
|
7716
8262
|
seq: 10,
|
|
@@ -7734,7 +8280,7 @@ describe('plugin-meetings', () => {
|
|
|
7734
8280
|
|
|
7735
8281
|
[ErrorType.CONFLICT, ErrorType.DOUBLECONFLICT].forEach((errorType) =>
|
|
7736
8282
|
it(`handles ERROR message indicating glare condition correctly (errorType=${errorType})`, () => {
|
|
7737
|
-
eventListeners[
|
|
8283
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7738
8284
|
roapMessage: {
|
|
7739
8285
|
messageType: 'ERROR',
|
|
7740
8286
|
seq: 10,
|
|
@@ -7765,7 +8311,7 @@ describe('plugin-meetings', () => {
|
|
|
7765
8311
|
);
|
|
7766
8312
|
|
|
7767
8313
|
it('handles ERROR message indicating other errors correctly', () => {
|
|
7768
|
-
eventListeners[
|
|
8314
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7769
8315
|
roapMessage: {
|
|
7770
8316
|
messageType: 'ERROR',
|
|
7771
8317
|
seq: 10,
|
|
@@ -7793,8 +8339,12 @@ describe('plugin-meetings', () => {
|
|
|
7793
8339
|
});
|
|
7794
8340
|
|
|
7795
8341
|
it('registers for audio and video source count changed', () => {
|
|
7796
|
-
assert.isFunction(
|
|
7797
|
-
|
|
8342
|
+
assert.isFunction(
|
|
8343
|
+
eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED]
|
|
8344
|
+
);
|
|
8345
|
+
assert.isFunction(
|
|
8346
|
+
eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED]
|
|
8347
|
+
);
|
|
7798
8348
|
});
|
|
7799
8349
|
|
|
7800
8350
|
it('forwards the VIDEO_SOURCES_COUNT_CHANGED event as "media:remoteVideoSourceCountChanged"', () => {
|
|
@@ -7804,7 +8354,7 @@ describe('plugin-meetings', () => {
|
|
|
7804
8354
|
|
|
7805
8355
|
sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources');
|
|
7806
8356
|
|
|
7807
|
-
eventListeners[
|
|
8357
|
+
eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
|
|
7808
8358
|
numTotalSources,
|
|
7809
8359
|
numLiveSources,
|
|
7810
8360
|
mediaContent
|
|
@@ -7828,7 +8378,7 @@ describe('plugin-meetings', () => {
|
|
|
7828
8378
|
const numLiveSources = 2;
|
|
7829
8379
|
const mediaContent = 'MAIN';
|
|
7830
8380
|
|
|
7831
|
-
eventListeners[
|
|
8381
|
+
eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED](
|
|
7832
8382
|
numTotalSources,
|
|
7833
8383
|
numLiveSources,
|
|
7834
8384
|
mediaContent
|
|
@@ -7856,7 +8406,7 @@ describe('plugin-meetings', () => {
|
|
|
7856
8406
|
'setNumCurrentSources'
|
|
7857
8407
|
);
|
|
7858
8408
|
|
|
7859
|
-
eventListeners[
|
|
8409
|
+
eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
|
|
7860
8410
|
numTotalSources,
|
|
7861
8411
|
numLiveSources,
|
|
7862
8412
|
'MAIN'
|
|
@@ -7874,7 +8424,7 @@ describe('plugin-meetings', () => {
|
|
|
7874
8424
|
'setNumCurrentSources'
|
|
7875
8425
|
);
|
|
7876
8426
|
|
|
7877
|
-
eventListeners[
|
|
8427
|
+
eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
|
|
7878
8428
|
numTotalSources,
|
|
7879
8429
|
numLiveSources,
|
|
7880
8430
|
'SLIDES'
|
|
@@ -9781,6 +10331,7 @@ describe('plugin-meetings', () => {
|
|
|
9781
10331
|
beforeEach(() => {
|
|
9782
10332
|
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
9783
10333
|
webex.internal.llm.getLocusUrl = sinon.stub();
|
|
10334
|
+
webex.internal.llm.getDatachannelUrl = sinon.stub();
|
|
9784
10335
|
webex.internal.llm.registerAndConnect = sinon
|
|
9785
10336
|
.stub()
|
|
9786
10337
|
.returns(Promise.resolve('something'));
|
|
@@ -9808,6 +10359,7 @@ describe('plugin-meetings', () => {
|
|
|
9808
10359
|
meeting.joinedWith = {state: 'JOINED'};
|
|
9809
10360
|
webex.internal.llm.isConnected.returns(true);
|
|
9810
10361
|
webex.internal.llm.getLocusUrl.returns('a url');
|
|
10362
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
9811
10363
|
|
|
9812
10364
|
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
9813
10365
|
|
|
@@ -9844,6 +10396,7 @@ describe('plugin-meetings', () => {
|
|
|
9844
10396
|
meeting.joinedWith = {state: 'JOINED'};
|
|
9845
10397
|
webex.internal.llm.isConnected.returns(true);
|
|
9846
10398
|
webex.internal.llm.getLocusUrl.returns('a url');
|
|
10399
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
9847
10400
|
|
|
9848
10401
|
meeting.locusInfo = {url: 'a different url', info: {datachannelUrl: 'a datachannel url'}};
|
|
9849
10402
|
|
|
@@ -9869,6 +10422,36 @@ describe('plugin-meetings', () => {
|
|
|
9869
10422
|
);
|
|
9870
10423
|
});
|
|
9871
10424
|
|
|
10425
|
+
it('disconnects if first if the data channel url has changed', async () => {
|
|
10426
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
10427
|
+
webex.internal.llm.isConnected.returns(true);
|
|
10428
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
10429
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
10430
|
+
|
|
10431
|
+
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a different datachannel url'}};
|
|
10432
|
+
|
|
10433
|
+
const result = await meeting.updateLLMConnection();
|
|
10434
|
+
|
|
10435
|
+
assert.calledWith(webex.internal.llm.disconnectLLM);
|
|
10436
|
+
assert.calledWith(
|
|
10437
|
+
webex.internal.llm.registerAndConnect,
|
|
10438
|
+
'a url',
|
|
10439
|
+
'a different datachannel url'
|
|
10440
|
+
);
|
|
10441
|
+
assert.equal(result, 'something');
|
|
10442
|
+
assert.calledWithExactly(
|
|
10443
|
+
meeting.webex.internal.llm.off,
|
|
10444
|
+
'event:relay.event',
|
|
10445
|
+
meeting.processRelayEvent
|
|
10446
|
+
);
|
|
10447
|
+
assert.calledTwice(meeting.webex.internal.llm.off);
|
|
10448
|
+
assert.calledOnceWithExactly(
|
|
10449
|
+
meeting.webex.internal.llm.on,
|
|
10450
|
+
'event:relay.event',
|
|
10451
|
+
meeting.processRelayEvent
|
|
10452
|
+
);
|
|
10453
|
+
});
|
|
10454
|
+
|
|
9872
10455
|
it('disconnects when the state is not JOINED', async () => {
|
|
9873
10456
|
meeting.joinedWith = {state: 'any other state'};
|
|
9874
10457
|
webex.internal.llm.isConnected.returns(true);
|