@webex/plugin-meetings 3.3.1 → 3.4.0
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 +552 -357
- 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 -7
- 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 +526 -292
- 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 +758 -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/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/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/statsAnalyzer/global.ts +0 -37
- package/src/statsAnalyzer/index.ts +0 -1318
- package/src/statsAnalyzer/mqaUtil.ts +0 -463
- 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,8 @@ describe('plugin-meetings', () => {
|
|
|
2675
3022
|
someReachabilityMetric2: 'some value2',
|
|
2676
3023
|
}),
|
|
2677
3024
|
};
|
|
3025
|
+
meeting.iceCandidatesCount = 3;
|
|
3026
|
+
|
|
2678
3027
|
await meeting.addMedia({
|
|
2679
3028
|
mediaSettings: {},
|
|
2680
3029
|
});
|
|
@@ -2694,6 +3043,7 @@ describe('plugin-meetings', () => {
|
|
|
2694
3043
|
isJoinWithMediaRetry: false,
|
|
2695
3044
|
someReachabilityMetric1: 'some value1',
|
|
2696
3045
|
someReachabilityMetric2: 'some value2',
|
|
3046
|
+
iceCandidatesCount: 3,
|
|
2697
3047
|
}
|
|
2698
3048
|
);
|
|
2699
3049
|
|
|
@@ -2715,7 +3065,63 @@ describe('plugin-meetings', () => {
|
|
|
2715
3065
|
turnDiscoverySkippedReason: undefined,
|
|
2716
3066
|
});
|
|
2717
3067
|
meeting.meetingState = 'ACTIVE';
|
|
2718
|
-
meeting.mediaProperties.waitForMediaConnectionConnected.rejects(
|
|
3068
|
+
meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
|
|
3069
|
+
|
|
3070
|
+
const forceRtcMetricsSend = sinon.stub().resolves();
|
|
3071
|
+
const closeMediaConnectionStub = sinon.stub();
|
|
3072
|
+
Media.createMediaConnection = sinon.stub().returns({
|
|
3073
|
+
close: closeMediaConnectionStub,
|
|
3074
|
+
forceRtcMetricsSend,
|
|
3075
|
+
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
|
|
3076
|
+
initiateOffer: sinon.stub().resolves({}),
|
|
3077
|
+
on: sinon.stub(),
|
|
3078
|
+
});
|
|
3079
|
+
|
|
3080
|
+
await meeting
|
|
3081
|
+
.addMedia({
|
|
3082
|
+
mediaSettings: {},
|
|
3083
|
+
})
|
|
3084
|
+
.catch((err) => {
|
|
3085
|
+
errorThrown = err;
|
|
3086
|
+
assert.instanceOf(err, AddMediaFailed);
|
|
3087
|
+
});
|
|
3088
|
+
|
|
3089
|
+
// Check that the only metric sent is ADD_MEDIA_FAILURE
|
|
3090
|
+
assert.calledOnceWithExactly(
|
|
3091
|
+
Metrics.sendBehavioralMetric,
|
|
3092
|
+
BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
|
|
3093
|
+
{
|
|
3094
|
+
correlation_id: meeting.correlationId,
|
|
3095
|
+
locus_id: meeting.locusUrl.split('/').pop(),
|
|
3096
|
+
reason: errorThrown.message,
|
|
3097
|
+
stack: errorThrown.stack,
|
|
3098
|
+
code: errorThrown.code,
|
|
3099
|
+
turnDiscoverySkippedReason: undefined,
|
|
3100
|
+
turnServerUsed: true,
|
|
3101
|
+
retriedWithTurnServer: false,
|
|
3102
|
+
isMultistream: false,
|
|
3103
|
+
isJoinWithMediaRetry: false,
|
|
3104
|
+
signalingState: 'unknown',
|
|
3105
|
+
connectionState: 'unknown',
|
|
3106
|
+
iceConnectionState: 'unknown',
|
|
3107
|
+
selectedCandidatePairChanges: 2,
|
|
3108
|
+
numTransports: 1,
|
|
3109
|
+
iceCandidatesCount: 0,
|
|
3110
|
+
}
|
|
3111
|
+
);
|
|
3112
|
+
|
|
3113
|
+
assert.isOk(errorThrown);
|
|
3114
|
+
});
|
|
3115
|
+
|
|
3116
|
+
it('should send ICE_CANDIDATE_ERROR metric if media connection fails and ice candidate errors have been gathered', async () => {
|
|
3117
|
+
let errorThrown = undefined;
|
|
3118
|
+
|
|
3119
|
+
meeting.roap.doTurnDiscovery = sinon.stub().returns({
|
|
3120
|
+
turnServerInfo: undefined,
|
|
3121
|
+
turnDiscoverySkippedReason: undefined,
|
|
3122
|
+
});
|
|
3123
|
+
meeting.meetingState = 'ACTIVE';
|
|
3124
|
+
meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
|
|
2719
3125
|
|
|
2720
3126
|
const forceRtcMetricsSend = sinon.stub().resolves();
|
|
2721
3127
|
const closeMediaConnectionStub = sinon.stub();
|
|
@@ -2727,6 +3133,9 @@ describe('plugin-meetings', () => {
|
|
|
2727
3133
|
on: sinon.stub(),
|
|
2728
3134
|
});
|
|
2729
3135
|
|
|
3136
|
+
meeting.iceCandidateErrors.set('701_error', 2);
|
|
3137
|
+
meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
|
|
3138
|
+
|
|
2730
3139
|
await meeting
|
|
2731
3140
|
.addMedia({
|
|
2732
3141
|
mediaSettings: {},
|
|
@@ -2756,6 +3165,9 @@ describe('plugin-meetings', () => {
|
|
|
2756
3165
|
iceConnectionState: 'unknown',
|
|
2757
3166
|
selectedCandidatePairChanges: 2,
|
|
2758
3167
|
numTransports: 1,
|
|
3168
|
+
'701_error': 2,
|
|
3169
|
+
'701_turn_host_lookup_received_error': 1,
|
|
3170
|
+
iceCandidatesCount: 0,
|
|
2759
3171
|
}
|
|
2760
3172
|
);
|
|
2761
3173
|
|
|
@@ -2775,7 +3187,7 @@ describe('plugin-meetings', () => {
|
|
|
2775
3187
|
|
|
2776
3188
|
statsAnalyzerStub = new EventsScope();
|
|
2777
3189
|
// mock the StatsAnalyzer constructor
|
|
2778
|
-
sinon.stub(
|
|
3190
|
+
sinon.stub(InternalMediaCoreModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
|
|
2779
3191
|
|
|
2780
3192
|
await meeting.addMedia({
|
|
2781
3193
|
mediaSettings: {},
|
|
@@ -2789,8 +3201,8 @@ describe('plugin-meetings', () => {
|
|
|
2789
3201
|
it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and sends metrics', async () => {
|
|
2790
3202
|
statsAnalyzerStub.emit(
|
|
2791
3203
|
{file: 'test', function: 'test'},
|
|
2792
|
-
|
|
2793
|
-
{
|
|
3204
|
+
StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED,
|
|
3205
|
+
{mediaType: 'audio'}
|
|
2794
3206
|
);
|
|
2795
3207
|
|
|
2796
3208
|
assert.calledWith(
|
|
@@ -2802,7 +3214,7 @@ describe('plugin-meetings', () => {
|
|
|
2802
3214
|
},
|
|
2803
3215
|
EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED,
|
|
2804
3216
|
{
|
|
2805
|
-
|
|
3217
|
+
mediaType: 'audio',
|
|
2806
3218
|
}
|
|
2807
3219
|
);
|
|
2808
3220
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2817,8 +3229,8 @@ describe('plugin-meetings', () => {
|
|
|
2817
3229
|
it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => {
|
|
2818
3230
|
statsAnalyzerStub.emit(
|
|
2819
3231
|
{file: 'test', function: 'test'},
|
|
2820
|
-
|
|
2821
|
-
{
|
|
3232
|
+
StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED,
|
|
3233
|
+
{mediaType: 'video'}
|
|
2822
3234
|
);
|
|
2823
3235
|
|
|
2824
3236
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2833,8 +3245,8 @@ describe('plugin-meetings', () => {
|
|
|
2833
3245
|
it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => {
|
|
2834
3246
|
statsAnalyzerStub.emit(
|
|
2835
3247
|
{file: 'test', function: 'test'},
|
|
2836
|
-
|
|
2837
|
-
{
|
|
3248
|
+
StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
|
|
3249
|
+
{mediaType: 'video'}
|
|
2838
3250
|
);
|
|
2839
3251
|
|
|
2840
3252
|
assert.calledWith(
|
|
@@ -2846,7 +3258,7 @@ describe('plugin-meetings', () => {
|
|
|
2846
3258
|
},
|
|
2847
3259
|
EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
|
|
2848
3260
|
{
|
|
2849
|
-
|
|
3261
|
+
mediaType: 'video',
|
|
2850
3262
|
}
|
|
2851
3263
|
);
|
|
2852
3264
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2861,8 +3273,8 @@ describe('plugin-meetings', () => {
|
|
|
2861
3273
|
it('REMOTE_MEDIA_STOPPED triggers the right metrics', async () => {
|
|
2862
3274
|
statsAnalyzerStub.emit(
|
|
2863
3275
|
{file: 'test', function: 'test'},
|
|
2864
|
-
|
|
2865
|
-
{
|
|
3276
|
+
StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
|
|
3277
|
+
{mediaType: 'audio'}
|
|
2866
3278
|
);
|
|
2867
3279
|
|
|
2868
3280
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2877,8 +3289,8 @@ describe('plugin-meetings', () => {
|
|
|
2877
3289
|
it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics for share', async () => {
|
|
2878
3290
|
statsAnalyzerStub.emit(
|
|
2879
3291
|
{file: 'test', function: 'test'},
|
|
2880
|
-
|
|
2881
|
-
{
|
|
3292
|
+
StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
|
|
3293
|
+
{mediaType: 'share'}
|
|
2882
3294
|
);
|
|
2883
3295
|
|
|
2884
3296
|
assert.calledWith(
|
|
@@ -2890,7 +3302,7 @@ describe('plugin-meetings', () => {
|
|
|
2890
3302
|
},
|
|
2891
3303
|
EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
|
|
2892
3304
|
{
|
|
2893
|
-
|
|
3305
|
+
mediaType: 'share',
|
|
2894
3306
|
}
|
|
2895
3307
|
);
|
|
2896
3308
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2913,8 +3325,8 @@ describe('plugin-meetings', () => {
|
|
|
2913
3325
|
it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
|
|
2914
3326
|
statsAnalyzerStub.emit(
|
|
2915
3327
|
{file: 'test', function: 'test'},
|
|
2916
|
-
|
|
2917
|
-
{
|
|
3328
|
+
StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
|
|
3329
|
+
{mediaType: 'share'}
|
|
2918
3330
|
);
|
|
2919
3331
|
|
|
2920
3332
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -2935,19 +3347,18 @@ describe('plugin-meetings', () => {
|
|
|
2935
3347
|
});
|
|
2936
3348
|
|
|
2937
3349
|
it('calls submitMQE correctly', async () => {
|
|
2938
|
-
const fakeData = {intervalMetadata: {bla: 'bla'}};
|
|
3350
|
+
const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
|
|
2939
3351
|
|
|
2940
3352
|
statsAnalyzerStub.emit(
|
|
2941
3353
|
{file: 'test', function: 'test'},
|
|
2942
|
-
|
|
2943
|
-
{data: fakeData
|
|
3354
|
+
StatsAnalyzerEventNames.MEDIA_QUALITY,
|
|
3355
|
+
{data: fakeData}
|
|
2944
3356
|
);
|
|
2945
3357
|
|
|
2946
3358
|
assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
|
|
2947
3359
|
name: 'client.mediaquality.event',
|
|
2948
3360
|
options: {
|
|
2949
3361
|
meetingId: meeting.id,
|
|
2950
|
-
networkType: 'wifi',
|
|
2951
3362
|
},
|
|
2952
3363
|
payload: {
|
|
2953
3364
|
intervals: [fakeData],
|
|
@@ -3004,7 +3415,7 @@ describe('plugin-meetings', () => {
|
|
|
3004
3415
|
it('succeeds even if getDevices() throws', async () => {
|
|
3005
3416
|
meeting.meetingState = 'ACTIVE';
|
|
3006
3417
|
|
|
3007
|
-
sinon.stub(
|
|
3418
|
+
sinon.stub(InternalMediaCoreModule, 'getDevices').rejects(new Error('fake error'));
|
|
3008
3419
|
|
|
3009
3420
|
await meeting.addMedia();
|
|
3010
3421
|
});
|
|
@@ -3021,7 +3432,7 @@ describe('plugin-meetings', () => {
|
|
|
3021
3432
|
clientErrorCode: MISSING_ROAP_ANSWER_CLIENT_CODE,
|
|
3022
3433
|
expectedErrorPayload: {
|
|
3023
3434
|
errorDescription: ERROR_DESCRIPTIONS.MISSING_ROAP_ANSWER,
|
|
3024
|
-
category: '
|
|
3435
|
+
category: 'media',
|
|
3025
3436
|
},
|
|
3026
3437
|
},
|
|
3027
3438
|
{
|
|
@@ -3040,10 +3451,18 @@ describe('plugin-meetings', () => {
|
|
|
3040
3451
|
clientErrorCode: ICE_FAILED_WITH_TURN_TLS_CLIENT_CODE,
|
|
3041
3452
|
expectedErrorPayload: {
|
|
3042
3453
|
errorDescription: ERROR_DESCRIPTIONS.ICE_FAILED_WITH_TURN_TLS,
|
|
3043
|
-
category: '
|
|
3454
|
+
category: 'media',
|
|
3455
|
+
},
|
|
3456
|
+
},
|
|
3457
|
+
{
|
|
3458
|
+
clientErrorCode: ICE_AND_REACHABILITY_FAILED_CLIENT_CODE,
|
|
3459
|
+
unreachable: true,
|
|
3460
|
+
expectedErrorPayload: {
|
|
3461
|
+
errorDescription: ERROR_DESCRIPTIONS.ICE_AND_REACHABILITY_FAILED,
|
|
3462
|
+
category: 'expected',
|
|
3044
3463
|
},
|
|
3045
3464
|
},
|
|
3046
|
-
].forEach(({clientErrorCode, expectedErrorPayload}) => {
|
|
3465
|
+
].forEach(({clientErrorCode, expectedErrorPayload, unreachable}) => {
|
|
3047
3466
|
it(`should handle all ice failures correctly for ${clientErrorCode}`, async () => {
|
|
3048
3467
|
// setting the method to the real implementation
|
|
3049
3468
|
// because newMetrics is mocked completely in the webex-mock
|
|
@@ -3052,14 +3471,18 @@ describe('plugin-meetings', () => {
|
|
|
3052
3471
|
webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
|
|
3053
3472
|
CD.getErrorPayloadForClientErrorCode;
|
|
3054
3473
|
|
|
3474
|
+
webex.meetings.reachability = {
|
|
3475
|
+
isWebexMediaBackendUnreachable: sinon.stub().resolves(unreachable || false),
|
|
3476
|
+
};
|
|
3477
|
+
|
|
3055
3478
|
const generateClientErrorCodeForIceFailureStub = sinon
|
|
3056
3479
|
.stub(CallDiagnosticUtils, 'generateClientErrorCodeForIceFailure')
|
|
3057
3480
|
.returns(clientErrorCode);
|
|
3058
3481
|
|
|
3059
3482
|
meeting.meetingState = 'ACTIVE';
|
|
3060
|
-
meeting.mediaProperties.waitForMediaConnectionConnected.rejects(
|
|
3061
|
-
|
|
3062
|
-
);
|
|
3483
|
+
meeting.mediaProperties.waitForMediaConnectionConnected.rejects({
|
|
3484
|
+
iceConnected: false,
|
|
3485
|
+
});
|
|
3063
3486
|
|
|
3064
3487
|
let errorThrown = false;
|
|
3065
3488
|
|
|
@@ -3073,8 +3496,9 @@ describe('plugin-meetings', () => {
|
|
|
3073
3496
|
|
|
3074
3497
|
assert.calledOnceWithExactly(generateClientErrorCodeForIceFailureStub, {
|
|
3075
3498
|
signalingState: 'unknown',
|
|
3076
|
-
|
|
3499
|
+
iceConnected: false,
|
|
3077
3500
|
turnServerUsed: true,
|
|
3501
|
+
unreachable: unreachable || false,
|
|
3078
3502
|
});
|
|
3079
3503
|
|
|
3080
3504
|
const submitClientEventCalls = webex.internal.newMetrics.submitClientEvent.getCalls();
|
|
@@ -3162,7 +3586,7 @@ describe('plugin-meetings', () => {
|
|
|
3162
3586
|
|
|
3163
3587
|
let clock;
|
|
3164
3588
|
|
|
3165
|
-
beforeEach(() => {
|
|
3589
|
+
beforeEach(async () => {
|
|
3166
3590
|
clock = sinon.useFakeTimers();
|
|
3167
3591
|
|
|
3168
3592
|
sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.unknown);
|
|
@@ -3171,15 +3595,20 @@ describe('plugin-meetings', () => {
|
|
|
3171
3595
|
meeting.config.deviceType = 'web';
|
|
3172
3596
|
meeting.isMultistream = isMultistream;
|
|
3173
3597
|
meeting.meetingState = 'ACTIVE';
|
|
3174
|
-
meeting.mediaId = 'fake media id';
|
|
3175
3598
|
meeting.selfUrl = 'selfUrl';
|
|
3176
3599
|
meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
|
|
3177
|
-
meeting.mediaProperties.getCurrentConnectionInfo = sinon
|
|
3600
|
+
meeting.mediaProperties.getCurrentConnectionInfo = sinon
|
|
3601
|
+
.stub()
|
|
3602
|
+
.resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
|
|
3178
3603
|
meeting.setMercuryListener = sinon.stub();
|
|
3179
3604
|
meeting.locusInfo.onFullLocus = sinon.stub();
|
|
3180
3605
|
meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
|
|
3181
3606
|
meeting.roap.doTurnDiscovery = sinon.stub().resolves({
|
|
3182
|
-
turnServerInfo: {
|
|
3607
|
+
turnServerInfo: {
|
|
3608
|
+
url: 'turns:turn-server-url:443?transport=tcp',
|
|
3609
|
+
username: 'turn user',
|
|
3610
|
+
password: 'turn password',
|
|
3611
|
+
},
|
|
3183
3612
|
turnDiscoverySkippedReason: 'reachability',
|
|
3184
3613
|
});
|
|
3185
3614
|
meeting.deferSDPAnswer = new Defer();
|
|
@@ -3192,7 +3621,18 @@ describe('plugin-meetings', () => {
|
|
|
3192
3621
|
// setup things that are expected to be the same across all the tests and are actually irrelevant for these tests
|
|
3193
3622
|
expectedDebugId = `MC-${meeting.id.substring(0, 4)}`;
|
|
3194
3623
|
expectedMediaConnectionConfig = {
|
|
3195
|
-
iceServers: [
|
|
3624
|
+
iceServers: [
|
|
3625
|
+
{
|
|
3626
|
+
urls: 'turn:turn-server-url:5004?transport=tcp',
|
|
3627
|
+
username: 'turn user',
|
|
3628
|
+
credential: 'turn password',
|
|
3629
|
+
},
|
|
3630
|
+
{
|
|
3631
|
+
urls: 'turns:turn-server-url:443?transport=tcp',
|
|
3632
|
+
username: 'turn user',
|
|
3633
|
+
credential: 'turn password',
|
|
3634
|
+
},
|
|
3635
|
+
],
|
|
3196
3636
|
skipInactiveTransceivers: false,
|
|
3197
3637
|
requireH264: true,
|
|
3198
3638
|
sdpMunging: {
|
|
@@ -3261,16 +3701,28 @@ describe('plugin-meetings', () => {
|
|
|
3261
3701
|
};
|
|
3262
3702
|
|
|
3263
3703
|
roapMediaConnectionConstructorStub = sinon
|
|
3264
|
-
.stub(
|
|
3704
|
+
.stub(InternalMediaCoreModule, 'RoapMediaConnection')
|
|
3265
3705
|
.returns(fakeRoapMediaConnection);
|
|
3266
3706
|
|
|
3267
3707
|
multistreamRoapMediaConnectionConstructorStub = sinon
|
|
3268
|
-
.stub(
|
|
3708
|
+
.stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
|
|
3269
3709
|
.returns(fakeMultistreamRoapMediaConnection);
|
|
3270
3710
|
|
|
3271
3711
|
locusMediaRequestStub = sinon
|
|
3272
3712
|
.stub(WebexPlugin.prototype, 'request')
|
|
3273
3713
|
.resolves({body: {locus: {fullState: {}}}});
|
|
3714
|
+
|
|
3715
|
+
// setup some things and mocks so that the call to join() works
|
|
3716
|
+
// (we need to call join() because it creates the LocusMediaRequest instance
|
|
3717
|
+
// that's being tested in these tests)
|
|
3718
|
+
meeting.webex.meetings.registered = true;
|
|
3719
|
+
meeting.webex.internal.device.config = {};
|
|
3720
|
+
sinon.stub(MeetingUtil, 'joinMeeting').resolves({
|
|
3721
|
+
id: 'fake locus from mocked join request',
|
|
3722
|
+
locusUrl: 'fake locus url',
|
|
3723
|
+
mediaId: 'fake media id',
|
|
3724
|
+
});
|
|
3725
|
+
await meeting.join({enableMultistream: isMultistream});
|
|
3274
3726
|
});
|
|
3275
3727
|
|
|
3276
3728
|
afterEach(() => {
|
|
@@ -3299,13 +3751,14 @@ describe('plugin-meetings', () => {
|
|
|
3299
3751
|
|
|
3300
3752
|
for (let idx = 0; idx < roapMediaConnectionToCheck.on.callCount; idx += 1) {
|
|
3301
3753
|
if (
|
|
3302
|
-
roapMediaConnectionToCheck.on.getCall(idx).args[0] ===
|
|
3754
|
+
roapMediaConnectionToCheck.on.getCall(idx).args[0] ===
|
|
3755
|
+
MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND
|
|
3303
3756
|
) {
|
|
3304
3757
|
return roapMediaConnectionToCheck.on.getCall(idx).args[1];
|
|
3305
3758
|
}
|
|
3306
3759
|
}
|
|
3307
3760
|
assert.fail(
|
|
3308
|
-
'listener for "roap:messageToSend" (
|
|
3761
|
+
'listener for "roap:messageToSend" (MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND) was not registered'
|
|
3309
3762
|
);
|
|
3310
3763
|
};
|
|
3311
3764
|
|
|
@@ -3746,13 +4199,13 @@ describe('plugin-meetings', () => {
|
|
|
3746
4199
|
await meeting.addMedia({
|
|
3747
4200
|
localStreams: {microphone: fakeMicrophoneStream},
|
|
3748
4201
|
audioEnabled: false,
|
|
3749
|
-
videoEnabled: false
|
|
4202
|
+
videoEnabled: false,
|
|
3750
4203
|
});
|
|
3751
4204
|
await simulateRoapOffer();
|
|
3752
4205
|
await simulateRoapOk();
|
|
3753
4206
|
|
|
3754
4207
|
assert.notCalled(handleDeviceLoggingSpy);
|
|
3755
|
-
})
|
|
4208
|
+
});
|
|
3756
4209
|
|
|
3757
4210
|
it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
|
|
3758
4211
|
await meeting.addMedia({audioEnabled: false});
|
|
@@ -5107,7 +5560,7 @@ describe('plugin-meetings', () => {
|
|
|
5107
5560
|
|
|
5108
5561
|
describe('#fetchMeetingInfo', () => {
|
|
5109
5562
|
const FAKE_DESTINATION = 'something@somecompany.com';
|
|
5110
|
-
const FAKE_TYPE =
|
|
5563
|
+
const FAKE_TYPE = DESTINATION_TYPE.SIP_URI;
|
|
5111
5564
|
const FAKE_TIMEOUT_FETCHMEETINGINFO_ID = '123456';
|
|
5112
5565
|
const FAKE_PASSWORD = '123abc';
|
|
5113
5566
|
const FAKE_CAPTCHA_CODE = 'a1b2c3XYZ';
|
|
@@ -5542,7 +5995,7 @@ describe('plugin-meetings', () => {
|
|
|
5542
5995
|
const FAKE_PASSWORD = '123456';
|
|
5543
5996
|
const FAKE_CAPTCHA_CODE = '654321';
|
|
5544
5997
|
const FAKE_DESTINATION = 'something@somecompany.com';
|
|
5545
|
-
const FAKE_TYPE =
|
|
5998
|
+
const FAKE_TYPE = DESTINATION_TYPE.SIP_URI;
|
|
5546
5999
|
const FAKE_INSTALLED_ORG_ID = '123456';
|
|
5547
6000
|
const FAKE_MEETING_INFO_LOOKUP_URL = 'meetingLookupUrl';
|
|
5548
6001
|
|
|
@@ -6187,14 +6640,14 @@ describe('plugin-meetings', () => {
|
|
|
6187
6640
|
beforeEach(() => {
|
|
6188
6641
|
sandbox = sinon.createSandbox();
|
|
6189
6642
|
meeting.statsAnalyzer = {
|
|
6190
|
-
stopAnalyzer: sinon.stub().returns(Promise.resolve())
|
|
6643
|
+
stopAnalyzer: sinon.stub().returns(Promise.resolve()),
|
|
6191
6644
|
};
|
|
6192
6645
|
|
|
6193
6646
|
meeting.reconnectionManager = {
|
|
6194
|
-
cleanUp: sinon.stub()
|
|
6647
|
+
cleanUp: sinon.stub(),
|
|
6195
6648
|
};
|
|
6196
6649
|
|
|
6197
|
-
meeting.cleanupLocalStreams=sinon.stub();
|
|
6650
|
+
meeting.cleanupLocalStreams = sinon.stub();
|
|
6198
6651
|
meeting.closeRemoteStreams = sinon.stub().returns(Promise.resolve());
|
|
6199
6652
|
meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
|
|
6200
6653
|
meeting.unsetRemoteStreams = sinon.stub();
|
|
@@ -6275,7 +6728,6 @@ describe('plugin-meetings', () => {
|
|
|
6275
6728
|
},
|
|
6276
6729
|
'SELF_OBSERVING'
|
|
6277
6730
|
);
|
|
6278
|
-
|
|
6279
6731
|
|
|
6280
6732
|
// Verify that the event handler behaves as expected
|
|
6281
6733
|
expect(meeting.statsAnalyzer.stopAnalyzer.calledOnce).to.be.true;
|
|
@@ -6288,11 +6740,13 @@ describe('plugin-meetings', () => {
|
|
|
6288
6740
|
expect(meeting.unsetPeerConnections.calledOnce).to.be.true;
|
|
6289
6741
|
expect(meeting.reconnectionManager.cleanUp.calledOnce).to.be.true;
|
|
6290
6742
|
expect(meeting.mediaProperties.setMediaDirection.calledOnce).to.be.true;
|
|
6291
|
-
expect(
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6743
|
+
expect(
|
|
6744
|
+
meeting.addMedia.calledOnceWithExactly({
|
|
6745
|
+
audioEnabled: false,
|
|
6746
|
+
videoEnabled: false,
|
|
6747
|
+
shareVideoEnabled: true,
|
|
6748
|
+
})
|
|
6749
|
+
).to.be.true;
|
|
6296
6750
|
await testUtils.flushPromises();
|
|
6297
6751
|
assert.equal(meeting.isMoveToInProgress, false);
|
|
6298
6752
|
});
|
|
@@ -7079,6 +7533,12 @@ describe('plugin-meetings', () => {
|
|
|
7079
7533
|
id: 'stream',
|
|
7080
7534
|
getTracks: () => [{id: 'track', addEventListener: sinon.stub()}],
|
|
7081
7535
|
};
|
|
7536
|
+
const simulateConnectionStateChange = (newState) => {
|
|
7537
|
+
meeting.mediaProperties.webrtcMediaConnection.getConnectionState = sinon
|
|
7538
|
+
.stub()
|
|
7539
|
+
.returns(newState);
|
|
7540
|
+
eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]();
|
|
7541
|
+
};
|
|
7082
7542
|
|
|
7083
7543
|
beforeEach(() => {
|
|
7084
7544
|
eventListeners = {};
|
|
@@ -7088,23 +7548,29 @@ describe('plugin-meetings', () => {
|
|
|
7088
7548
|
on: sinon.stub().callsFake((event, listener) => {
|
|
7089
7549
|
eventListeners[event] = listener;
|
|
7090
7550
|
}),
|
|
7551
|
+
getConnectionState: sinon.stub().returns(ConnectionState.New),
|
|
7091
7552
|
};
|
|
7092
7553
|
MediaUtil.createMediaStream.returns(fakeStream);
|
|
7093
7554
|
});
|
|
7094
7555
|
|
|
7095
7556
|
it('should register for all the correct RoapMediaConnection events', () => {
|
|
7096
7557
|
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(
|
|
7558
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_STARTED]);
|
|
7559
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_DONE]);
|
|
7560
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_FAILURE]);
|
|
7561
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]);
|
|
7562
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]);
|
|
7563
|
+
assert.isFunction(
|
|
7564
|
+
eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]
|
|
7565
|
+
);
|
|
7566
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED]);
|
|
7567
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]);
|
|
7568
|
+
assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]);
|
|
7103
7569
|
});
|
|
7104
7570
|
|
|
7105
7571
|
it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => {
|
|
7106
7572
|
meeting.setupMediaConnectionListeners();
|
|
7107
|
-
eventListeners[
|
|
7573
|
+
eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
|
|
7108
7574
|
track: 'track',
|
|
7109
7575
|
type: RemoteTrackType.AUDIO,
|
|
7110
7576
|
});
|
|
@@ -7114,7 +7580,7 @@ describe('plugin-meetings', () => {
|
|
|
7114
7580
|
stream: fakeStream,
|
|
7115
7581
|
});
|
|
7116
7582
|
|
|
7117
|
-
eventListeners[
|
|
7583
|
+
eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
|
|
7118
7584
|
track: 'track',
|
|
7119
7585
|
type: RemoteTrackType.VIDEO,
|
|
7120
7586
|
});
|
|
@@ -7124,7 +7590,7 @@ describe('plugin-meetings', () => {
|
|
|
7124
7590
|
stream: fakeStream,
|
|
7125
7591
|
});
|
|
7126
7592
|
|
|
7127
|
-
eventListeners[
|
|
7593
|
+
eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
|
|
7128
7594
|
track: 'track',
|
|
7129
7595
|
type: RemoteTrackType.SCREENSHARE_VIDEO,
|
|
7130
7596
|
});
|
|
@@ -7135,13 +7601,78 @@ describe('plugin-meetings', () => {
|
|
|
7135
7601
|
});
|
|
7136
7602
|
});
|
|
7137
7603
|
|
|
7604
|
+
describe('should react on a ICE_CANDIDATE event', () => {
|
|
7605
|
+
beforeEach(() => {
|
|
7606
|
+
meeting.setupMediaConnectionListeners();
|
|
7607
|
+
});
|
|
7608
|
+
|
|
7609
|
+
it('should collect ice candidates', () => {
|
|
7610
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: 'candidate'});
|
|
7611
|
+
|
|
7612
|
+
assert.equal(meeting.iceCandidatesCount, 1);
|
|
7613
|
+
});
|
|
7614
|
+
|
|
7615
|
+
it('should not collect null ice candidates', () => {
|
|
7616
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: null});
|
|
7617
|
+
|
|
7618
|
+
assert.equal(meeting.iceCandidatesCount, 0);
|
|
7619
|
+
});
|
|
7620
|
+
});
|
|
7621
|
+
|
|
7622
|
+
describe('should react on a ICE_CANDIDATE_ERROR event', () => {
|
|
7623
|
+
beforeEach(() => {
|
|
7624
|
+
meeting.setupMediaConnectionListeners();
|
|
7625
|
+
});
|
|
7626
|
+
|
|
7627
|
+
it('should not collect skipped ice candidates error', () => {
|
|
7628
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
|
|
7629
|
+
error: {
|
|
7630
|
+
errorCode: 600,
|
|
7631
|
+
errorText: 'Address not associated with the desired network interface.',
|
|
7632
|
+
},
|
|
7633
|
+
});
|
|
7634
|
+
|
|
7635
|
+
assert.equal(meeting.iceCandidateErrors.size, 0);
|
|
7636
|
+
});
|
|
7637
|
+
|
|
7638
|
+
it('should collect valid ice candidates error', () => {
|
|
7639
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
|
|
7640
|
+
error: {errorCode: 701, errorText: ''},
|
|
7641
|
+
});
|
|
7642
|
+
|
|
7643
|
+
assert.equal(meeting.iceCandidateErrors.size, 1);
|
|
7644
|
+
assert.equal(meeting.iceCandidateErrors.has('701_'), true);
|
|
7645
|
+
});
|
|
7646
|
+
|
|
7647
|
+
it('should increment counter if same valid ice candidates error collected', () => {
|
|
7648
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
|
|
7649
|
+
error: {errorCode: 701, errorText: ''},
|
|
7650
|
+
});
|
|
7651
|
+
|
|
7652
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
|
|
7653
|
+
error: {errorCode: 701, errorText: 'STUN host lookup received error.'},
|
|
7654
|
+
});
|
|
7655
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
|
|
7656
|
+
error: {errorCode: 701, errorText: 'STUN host lookup received error.'},
|
|
7657
|
+
});
|
|
7658
|
+
|
|
7659
|
+
assert.equal(meeting.iceCandidateErrors.size, 2);
|
|
7660
|
+
assert.equal(meeting.iceCandidateErrors.has('701_'), true);
|
|
7661
|
+
assert.equal(meeting.iceCandidateErrors.get('701_'), 1);
|
|
7662
|
+
assert.equal(
|
|
7663
|
+
meeting.iceCandidateErrors.has('701_stun_host_lookup_received_error'),
|
|
7664
|
+
true
|
|
7665
|
+
);
|
|
7666
|
+
assert.equal(meeting.iceCandidateErrors.get('701_stun_host_lookup_received_error'), 2);
|
|
7667
|
+
});
|
|
7668
|
+
});
|
|
7669
|
+
|
|
7138
7670
|
describe('CONNECTION_STATE_CHANGED event when state = "Connecting"', () => {
|
|
7139
7671
|
it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = true', () => {
|
|
7140
7672
|
meeting.hasMediaConnectionConnectedAtLeastOnce = true;
|
|
7141
7673
|
meeting.setupMediaConnectionListeners();
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
});
|
|
7674
|
+
|
|
7675
|
+
simulateConnectionStateChange(ConnectionState.Connecting);
|
|
7145
7676
|
|
|
7146
7677
|
assert.notCalled(webex.internal.newMetrics.submitClientEvent);
|
|
7147
7678
|
});
|
|
@@ -7149,9 +7680,8 @@ describe('plugin-meetings', () => {
|
|
|
7149
7680
|
it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = false', () => {
|
|
7150
7681
|
meeting.hasMediaConnectionConnectedAtLeastOnce = false;
|
|
7151
7682
|
meeting.setupMediaConnectionListeners();
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
});
|
|
7683
|
+
|
|
7684
|
+
simulateConnectionStateChange(ConnectionState.Connecting);
|
|
7155
7685
|
|
|
7156
7686
|
assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
|
|
7157
7687
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -7177,6 +7707,7 @@ describe('plugin-meetings', () => {
|
|
|
7177
7707
|
on: sinon.stub().callsFake((event, listener) => {
|
|
7178
7708
|
eventListeners[event] = listener;
|
|
7179
7709
|
}),
|
|
7710
|
+
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
|
|
7180
7711
|
};
|
|
7181
7712
|
};
|
|
7182
7713
|
|
|
@@ -7230,9 +7761,7 @@ describe('plugin-meetings', () => {
|
|
|
7230
7761
|
assert.equal(meeting.hasMediaConnectionConnectedAtLeastOnce, false);
|
|
7231
7762
|
|
|
7232
7763
|
// simulate first connection success
|
|
7233
|
-
|
|
7234
|
-
state: 'Connected',
|
|
7235
|
-
});
|
|
7764
|
+
simulateConnectionStateChange(ConnectionState.Connected);
|
|
7236
7765
|
checkExpectedSpies({
|
|
7237
7766
|
icePhase: 'JOIN_MEETING_FINAL',
|
|
7238
7767
|
setNetworkStatusCallParams: [NETWORK_STATUS.CONNECTED],
|
|
@@ -7242,12 +7771,9 @@ describe('plugin-meetings', () => {
|
|
|
7242
7771
|
// now simulate short connection loss, client.ice.end is not sent a second time as hasMediaConnectionConnectedAtLeastOnce = true
|
|
7243
7772
|
resetSpies();
|
|
7244
7773
|
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
eventListeners[Event.CONNECTION_STATE_CHANGED]({
|
|
7249
|
-
state: 'Connected',
|
|
7250
|
-
});
|
|
7774
|
+
simulateConnectionStateChange(ConnectionState.Disconnected);
|
|
7775
|
+
|
|
7776
|
+
simulateConnectionStateChange(ConnectionState.Connected);
|
|
7251
7777
|
|
|
7252
7778
|
checkExpectedSpies({
|
|
7253
7779
|
setNetworkStatusCallParams: [NETWORK_STATUS.DISCONNECTED, NETWORK_STATUS.CONNECTED],
|
|
@@ -7255,12 +7781,9 @@ describe('plugin-meetings', () => {
|
|
|
7255
7781
|
|
|
7256
7782
|
resetSpies();
|
|
7257
7783
|
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
eventListeners[Event.CONNECTION_STATE_CHANGED]({
|
|
7262
|
-
state: 'Connected',
|
|
7263
|
-
});
|
|
7784
|
+
simulateConnectionStateChange(ConnectionState.Disconnected);
|
|
7785
|
+
|
|
7786
|
+
simulateConnectionStateChange(ConnectionState.Connected);
|
|
7264
7787
|
});
|
|
7265
7788
|
});
|
|
7266
7789
|
|
|
@@ -7282,9 +7805,8 @@ describe('plugin-meetings', () => {
|
|
|
7282
7805
|
|
|
7283
7806
|
const mockDisconnectedEvent = () => {
|
|
7284
7807
|
meeting.setupMediaConnectionListeners();
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
});
|
|
7808
|
+
|
|
7809
|
+
simulateConnectionStateChange(ConnectionState.Disconnected);
|
|
7288
7810
|
};
|
|
7289
7811
|
|
|
7290
7812
|
const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
|
|
@@ -7348,9 +7870,8 @@ describe('plugin-meetings', () => {
|
|
|
7348
7870
|
describe('CONNECTION_STATE_CHANGED event when state = "Failed"', () => {
|
|
7349
7871
|
const mockFailedEvent = () => {
|
|
7350
7872
|
meeting.setupMediaConnectionListeners();
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
});
|
|
7873
|
+
|
|
7874
|
+
simulateConnectionStateChange(ConnectionState.Failed);
|
|
7354
7875
|
};
|
|
7355
7876
|
|
|
7356
7877
|
const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
|
|
@@ -7432,7 +7953,7 @@ describe('plugin-meetings', () => {
|
|
|
7432
7953
|
cause: {name: fakeRootCauseName},
|
|
7433
7954
|
});
|
|
7434
7955
|
|
|
7435
|
-
eventListeners[
|
|
7956
|
+
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
|
|
7436
7957
|
|
|
7437
7958
|
checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
|
|
7438
7959
|
checkBehavioralMetricSent(
|
|
@@ -7449,7 +7970,7 @@ describe('plugin-meetings', () => {
|
|
|
7449
7970
|
cause: {name: fakeRootCauseName},
|
|
7450
7971
|
});
|
|
7451
7972
|
|
|
7452
|
-
eventListeners[
|
|
7973
|
+
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
|
|
7453
7974
|
|
|
7454
7975
|
checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
|
|
7455
7976
|
checkBehavioralMetricSent(
|
|
@@ -7466,7 +7987,7 @@ describe('plugin-meetings', () => {
|
|
|
7466
7987
|
cause: {name: fakeRootCauseName},
|
|
7467
7988
|
});
|
|
7468
7989
|
|
|
7469
|
-
eventListeners[
|
|
7990
|
+
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
|
|
7470
7991
|
|
|
7471
7992
|
checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
|
|
7472
7993
|
checkBehavioralMetricSent(
|
|
@@ -7481,7 +8002,7 @@ describe('plugin-meetings', () => {
|
|
|
7481
8002
|
// SdpError is usually without a cause
|
|
7482
8003
|
const fakeError = new Errors.SdpError(fakeErrorMessage, {name: fakeErrorName});
|
|
7483
8004
|
|
|
7484
|
-
eventListeners[
|
|
8005
|
+
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
|
|
7485
8006
|
|
|
7486
8007
|
checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
|
|
7487
8008
|
// expectedMetadataType is the error name in this case
|
|
@@ -7499,7 +8020,7 @@ describe('plugin-meetings', () => {
|
|
|
7499
8020
|
name: fakeErrorName,
|
|
7500
8021
|
});
|
|
7501
8022
|
|
|
7502
|
-
eventListeners[
|
|
8023
|
+
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
|
|
7503
8024
|
|
|
7504
8025
|
checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
|
|
7505
8026
|
// expectedMetadataType is the error name in this case
|
|
@@ -7525,7 +8046,7 @@ describe('plugin-meetings', () => {
|
|
|
7525
8046
|
};
|
|
7526
8047
|
meeting.sdpResponseTimer = '1234';
|
|
7527
8048
|
|
|
7528
|
-
eventListeners[
|
|
8049
|
+
eventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED]();
|
|
7529
8050
|
|
|
7530
8051
|
assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
|
|
7531
8052
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -7553,7 +8074,7 @@ describe('plugin-meetings', () => {
|
|
|
7553
8074
|
it('handles LOCAL_SDP_OFFER_GENERATED correctly', () => {
|
|
7554
8075
|
assert.equal(meeting.deferSDPAnswer, undefined);
|
|
7555
8076
|
|
|
7556
|
-
eventListeners[
|
|
8077
|
+
eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
|
|
7557
8078
|
|
|
7558
8079
|
assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
|
|
7559
8080
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -7565,7 +8086,7 @@ describe('plugin-meetings', () => {
|
|
|
7565
8086
|
});
|
|
7566
8087
|
|
|
7567
8088
|
it('handles LOCAL_SDP_ANSWER_GENERATED correctly', () => {
|
|
7568
|
-
eventListeners[
|
|
8089
|
+
eventListeners[MediaConnectionEventNames.LOCAL_SDP_ANSWER_GENERATED]();
|
|
7569
8090
|
|
|
7570
8091
|
assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
|
|
7571
8092
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -7575,7 +8096,7 @@ describe('plugin-meetings', () => {
|
|
|
7575
8096
|
});
|
|
7576
8097
|
});
|
|
7577
8098
|
|
|
7578
|
-
describe('handles
|
|
8099
|
+
describe('handles MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND correctly', () => {
|
|
7579
8100
|
let sendRoapOKStub;
|
|
7580
8101
|
let sendRoapMediaRequestStub;
|
|
7581
8102
|
let sendRoapAnswerStub;
|
|
@@ -7593,7 +8114,7 @@ describe('plugin-meetings', () => {
|
|
|
7593
8114
|
});
|
|
7594
8115
|
|
|
7595
8116
|
it('handles OK message correctly', () => {
|
|
7596
|
-
eventListeners[
|
|
8117
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7597
8118
|
roapMessage: {messageType: 'OK', seq: 1},
|
|
7598
8119
|
});
|
|
7599
8120
|
|
|
@@ -7608,7 +8129,7 @@ describe('plugin-meetings', () => {
|
|
|
7608
8129
|
it('handles OFFER message correctly (no answer in the http response)', async () => {
|
|
7609
8130
|
sinon.stub(meeting, 'roapMessageReceived');
|
|
7610
8131
|
|
|
7611
|
-
eventListeners[
|
|
8132
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7612
8133
|
roapMessage: {
|
|
7613
8134
|
messageType: 'OFFER',
|
|
7614
8135
|
seq: 1,
|
|
@@ -7634,7 +8155,7 @@ describe('plugin-meetings', () => {
|
|
|
7634
8155
|
sendRoapMediaRequestStub.resolves({roapAnswer: fakeAnswer});
|
|
7635
8156
|
sinon.stub(meeting, 'roapMessageReceived');
|
|
7636
8157
|
|
|
7637
|
-
eventListeners[
|
|
8158
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7638
8159
|
roapMessage: {
|
|
7639
8160
|
messageType: 'OFFER',
|
|
7640
8161
|
seq: 1,
|
|
@@ -7656,14 +8177,20 @@ describe('plugin-meetings', () => {
|
|
|
7656
8177
|
});
|
|
7657
8178
|
|
|
7658
8179
|
it('handles OFFER message correctly when request fails', async () => {
|
|
8180
|
+
const fakeError = new Error('fake error');
|
|
7659
8181
|
const clock = sinon.useFakeTimers();
|
|
7660
8182
|
sinon.spy(clock, 'clearTimeout');
|
|
7661
8183
|
meeting.deferSDPAnswer = {reject: sinon.stub()};
|
|
7662
8184
|
meeting.sdpResponseTimer = '1234';
|
|
7663
|
-
sendRoapMediaRequestStub.rejects();
|
|
8185
|
+
sendRoapMediaRequestStub.rejects(fakeError);
|
|
7664
8186
|
sinon.stub(meeting, 'roapMessageReceived');
|
|
8187
|
+
const getErrorPayloadForClientErrorCodeStub =
|
|
8188
|
+
(webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
|
|
8189
|
+
sinon
|
|
8190
|
+
.stub()
|
|
8191
|
+
.callsFake(({clientErrorCode}) => ({errorCode: clientErrorCode, fatal: true})));
|
|
7665
8192
|
|
|
7666
|
-
eventListeners[
|
|
8193
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7667
8194
|
roapMessage: {
|
|
7668
8195
|
messageType: 'OFFER',
|
|
7669
8196
|
seq: 1,
|
|
@@ -7686,10 +8213,25 @@ describe('plugin-meetings', () => {
|
|
|
7686
8213
|
assert.calledOnce(clock.clearTimeout);
|
|
7687
8214
|
assert.calledWith(clock.clearTimeout, '1234');
|
|
7688
8215
|
assert.equal(meeting.sdpResponseTimer, undefined);
|
|
8216
|
+
|
|
8217
|
+
assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
|
|
8218
|
+
clientErrorCode: 2007,
|
|
8219
|
+
});
|
|
8220
|
+
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
8221
|
+
name: 'client.media-engine.remote-sdp-received',
|
|
8222
|
+
payload: {
|
|
8223
|
+
canProceed: false,
|
|
8224
|
+
errors: [{errorCode: 2007, fatal: true}],
|
|
8225
|
+
},
|
|
8226
|
+
options: {
|
|
8227
|
+
meetingId: meeting.id,
|
|
8228
|
+
rawError: fakeError,
|
|
8229
|
+
},
|
|
8230
|
+
});
|
|
7689
8231
|
});
|
|
7690
8232
|
|
|
7691
8233
|
it('handles ANSWER message correctly', () => {
|
|
7692
|
-
eventListeners[
|
|
8234
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7693
8235
|
roapMessage: {
|
|
7694
8236
|
messageType: 'ANSWER',
|
|
7695
8237
|
seq: 10,
|
|
@@ -7710,7 +8252,7 @@ describe('plugin-meetings', () => {
|
|
|
7710
8252
|
it('sends metrics if fails to send roap ANSWER message', async () => {
|
|
7711
8253
|
sendRoapAnswerStub.rejects(new Error('sending answer failed'));
|
|
7712
8254
|
|
|
7713
|
-
await eventListeners[
|
|
8255
|
+
await eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7714
8256
|
roapMessage: {
|
|
7715
8257
|
messageType: 'ANSWER',
|
|
7716
8258
|
seq: 10,
|
|
@@ -7734,7 +8276,7 @@ describe('plugin-meetings', () => {
|
|
|
7734
8276
|
|
|
7735
8277
|
[ErrorType.CONFLICT, ErrorType.DOUBLECONFLICT].forEach((errorType) =>
|
|
7736
8278
|
it(`handles ERROR message indicating glare condition correctly (errorType=${errorType})`, () => {
|
|
7737
|
-
eventListeners[
|
|
8279
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7738
8280
|
roapMessage: {
|
|
7739
8281
|
messageType: 'ERROR',
|
|
7740
8282
|
seq: 10,
|
|
@@ -7765,7 +8307,7 @@ describe('plugin-meetings', () => {
|
|
|
7765
8307
|
);
|
|
7766
8308
|
|
|
7767
8309
|
it('handles ERROR message indicating other errors correctly', () => {
|
|
7768
|
-
eventListeners[
|
|
8310
|
+
eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
|
|
7769
8311
|
roapMessage: {
|
|
7770
8312
|
messageType: 'ERROR',
|
|
7771
8313
|
seq: 10,
|
|
@@ -7793,8 +8335,12 @@ describe('plugin-meetings', () => {
|
|
|
7793
8335
|
});
|
|
7794
8336
|
|
|
7795
8337
|
it('registers for audio and video source count changed', () => {
|
|
7796
|
-
assert.isFunction(
|
|
7797
|
-
|
|
8338
|
+
assert.isFunction(
|
|
8339
|
+
eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED]
|
|
8340
|
+
);
|
|
8341
|
+
assert.isFunction(
|
|
8342
|
+
eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED]
|
|
8343
|
+
);
|
|
7798
8344
|
});
|
|
7799
8345
|
|
|
7800
8346
|
it('forwards the VIDEO_SOURCES_COUNT_CHANGED event as "media:remoteVideoSourceCountChanged"', () => {
|
|
@@ -7804,7 +8350,7 @@ describe('plugin-meetings', () => {
|
|
|
7804
8350
|
|
|
7805
8351
|
sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources');
|
|
7806
8352
|
|
|
7807
|
-
eventListeners[
|
|
8353
|
+
eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
|
|
7808
8354
|
numTotalSources,
|
|
7809
8355
|
numLiveSources,
|
|
7810
8356
|
mediaContent
|
|
@@ -7828,7 +8374,7 @@ describe('plugin-meetings', () => {
|
|
|
7828
8374
|
const numLiveSources = 2;
|
|
7829
8375
|
const mediaContent = 'MAIN';
|
|
7830
8376
|
|
|
7831
|
-
eventListeners[
|
|
8377
|
+
eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED](
|
|
7832
8378
|
numTotalSources,
|
|
7833
8379
|
numLiveSources,
|
|
7834
8380
|
mediaContent
|
|
@@ -7856,7 +8402,7 @@ describe('plugin-meetings', () => {
|
|
|
7856
8402
|
'setNumCurrentSources'
|
|
7857
8403
|
);
|
|
7858
8404
|
|
|
7859
|
-
eventListeners[
|
|
8405
|
+
eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
|
|
7860
8406
|
numTotalSources,
|
|
7861
8407
|
numLiveSources,
|
|
7862
8408
|
'MAIN'
|
|
@@ -7874,7 +8420,7 @@ describe('plugin-meetings', () => {
|
|
|
7874
8420
|
'setNumCurrentSources'
|
|
7875
8421
|
);
|
|
7876
8422
|
|
|
7877
|
-
eventListeners[
|
|
8423
|
+
eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
|
|
7878
8424
|
numTotalSources,
|
|
7879
8425
|
numLiveSources,
|
|
7880
8426
|
'SLIDES'
|
|
@@ -9781,6 +10327,7 @@ describe('plugin-meetings', () => {
|
|
|
9781
10327
|
beforeEach(() => {
|
|
9782
10328
|
webex.internal.llm.isConnected = sinon.stub().returns(false);
|
|
9783
10329
|
webex.internal.llm.getLocusUrl = sinon.stub();
|
|
10330
|
+
webex.internal.llm.getDatachannelUrl = sinon.stub();
|
|
9784
10331
|
webex.internal.llm.registerAndConnect = sinon
|
|
9785
10332
|
.stub()
|
|
9786
10333
|
.returns(Promise.resolve('something'));
|
|
@@ -9808,6 +10355,7 @@ describe('plugin-meetings', () => {
|
|
|
9808
10355
|
meeting.joinedWith = {state: 'JOINED'};
|
|
9809
10356
|
webex.internal.llm.isConnected.returns(true);
|
|
9810
10357
|
webex.internal.llm.getLocusUrl.returns('a url');
|
|
10358
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
9811
10359
|
|
|
9812
10360
|
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
|
|
9813
10361
|
|
|
@@ -9844,6 +10392,7 @@ describe('plugin-meetings', () => {
|
|
|
9844
10392
|
meeting.joinedWith = {state: 'JOINED'};
|
|
9845
10393
|
webex.internal.llm.isConnected.returns(true);
|
|
9846
10394
|
webex.internal.llm.getLocusUrl.returns('a url');
|
|
10395
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
9847
10396
|
|
|
9848
10397
|
meeting.locusInfo = {url: 'a different url', info: {datachannelUrl: 'a datachannel url'}};
|
|
9849
10398
|
|
|
@@ -9869,6 +10418,36 @@ describe('plugin-meetings', () => {
|
|
|
9869
10418
|
);
|
|
9870
10419
|
});
|
|
9871
10420
|
|
|
10421
|
+
it('disconnects if first if the data channel url has changed', async () => {
|
|
10422
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
10423
|
+
webex.internal.llm.isConnected.returns(true);
|
|
10424
|
+
webex.internal.llm.getLocusUrl.returns('a url');
|
|
10425
|
+
webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
|
|
10426
|
+
|
|
10427
|
+
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a different datachannel url'}};
|
|
10428
|
+
|
|
10429
|
+
const result = await meeting.updateLLMConnection();
|
|
10430
|
+
|
|
10431
|
+
assert.calledWith(webex.internal.llm.disconnectLLM);
|
|
10432
|
+
assert.calledWith(
|
|
10433
|
+
webex.internal.llm.registerAndConnect,
|
|
10434
|
+
'a url',
|
|
10435
|
+
'a different datachannel url'
|
|
10436
|
+
);
|
|
10437
|
+
assert.equal(result, 'something');
|
|
10438
|
+
assert.calledWithExactly(
|
|
10439
|
+
meeting.webex.internal.llm.off,
|
|
10440
|
+
'event:relay.event',
|
|
10441
|
+
meeting.processRelayEvent
|
|
10442
|
+
);
|
|
10443
|
+
assert.calledTwice(meeting.webex.internal.llm.off);
|
|
10444
|
+
assert.calledOnceWithExactly(
|
|
10445
|
+
meeting.webex.internal.llm.on,
|
|
10446
|
+
'event:relay.event',
|
|
10447
|
+
meeting.processRelayEvent
|
|
10448
|
+
);
|
|
10449
|
+
});
|
|
10450
|
+
|
|
9872
10451
|
it('disconnects when the state is not JOINED', async () => {
|
|
9873
10452
|
meeting.joinedWith = {state: 'any other state'};
|
|
9874
10453
|
webex.internal.llm.isConnected.returns(true);
|