@webex/plugin-meetings 3.3.1 → 3.4.0-next.10

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.
Files changed (138) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +7 -2
  3. package/dist/breakouts/index.js.map +1 -1
  4. package/dist/constants.js +11 -4
  5. package/dist/constants.js.map +1 -1
  6. package/dist/interpretation/index.js +1 -1
  7. package/dist/interpretation/siLanguage.js +1 -1
  8. package/dist/locus-info/selfUtils.js +0 -5
  9. package/dist/locus-info/selfUtils.js.map +1 -1
  10. package/dist/media/MediaConnectionAwaiter.js +70 -15
  11. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  12. package/dist/media/index.js +18 -9
  13. package/dist/media/index.js.map +1 -1
  14. package/dist/meeting/connectionStateHandler.js +67 -0
  15. package/dist/meeting/connectionStateHandler.js.map +1 -0
  16. package/dist/meeting/index.js +576 -374
  17. package/dist/meeting/index.js.map +1 -1
  18. package/dist/meeting/locusMediaRequest.js +7 -0
  19. package/dist/meeting/locusMediaRequest.js.map +1 -1
  20. package/dist/meeting/muteState.js +6 -1
  21. package/dist/meeting/muteState.js.map +1 -1
  22. package/dist/meeting/util.js +1 -0
  23. package/dist/meeting/util.js.map +1 -1
  24. package/dist/meeting-info/index.js +4 -4
  25. package/dist/meeting-info/index.js.map +1 -1
  26. package/dist/meeting-info/meeting-info-v2.js +2 -2
  27. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  28. package/dist/meeting-info/util.js +17 -17
  29. package/dist/meeting-info/util.js.map +1 -1
  30. package/dist/meeting-info/utilv2.js +16 -16
  31. package/dist/meeting-info/utilv2.js.map +1 -1
  32. package/dist/meetings/collection.js +1 -1
  33. package/dist/meetings/collection.js.map +1 -1
  34. package/dist/meetings/index.js +41 -35
  35. package/dist/meetings/index.js.map +1 -1
  36. package/dist/meetings/meetings.types.js +8 -0
  37. package/dist/meetings/meetings.types.js.map +1 -1
  38. package/dist/meetings/util.js +3 -2
  39. package/dist/meetings/util.js.map +1 -1
  40. package/dist/metrics/constants.js +2 -1
  41. package/dist/metrics/constants.js.map +1 -1
  42. package/dist/metrics/index.js +57 -0
  43. package/dist/metrics/index.js.map +1 -1
  44. package/dist/personal-meeting-room/index.js +1 -1
  45. package/dist/personal-meeting-room/index.js.map +1 -1
  46. package/dist/reachability/clusterReachability.js +108 -53
  47. package/dist/reachability/clusterReachability.js.map +1 -1
  48. package/dist/reachability/index.js +546 -115
  49. package/dist/reachability/index.js.map +1 -1
  50. package/dist/reconnection-manager/index.js +1 -1
  51. package/dist/reconnection-manager/index.js.map +1 -1
  52. package/dist/rtcMetrics/index.js +26 -6
  53. package/dist/rtcMetrics/index.js.map +1 -1
  54. package/dist/types/constants.d.ts +11 -3
  55. package/dist/types/media/MediaConnectionAwaiter.d.ts +24 -4
  56. package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
  57. package/dist/types/meeting/index.d.ts +28 -8
  58. package/dist/types/meeting/locusMediaRequest.d.ts +2 -0
  59. package/dist/types/meeting-info/index.d.ts +3 -2
  60. package/dist/types/meeting-info/meeting-info-v2.d.ts +3 -2
  61. package/dist/types/meeting-info/util.d.ts +5 -4
  62. package/dist/types/meeting-info/utilv2.d.ts +3 -2
  63. package/dist/types/meetings/collection.d.ts +3 -2
  64. package/dist/types/meetings/index.d.ts +6 -4
  65. package/dist/types/meetings/meetings.types.d.ts +9 -0
  66. package/dist/types/metrics/constants.d.ts +1 -0
  67. package/dist/types/metrics/index.d.ts +15 -0
  68. package/dist/types/reachability/clusterReachability.d.ts +31 -3
  69. package/dist/types/reachability/index.d.ts +107 -4
  70. package/dist/types/rtcMetrics/index.d.ts +11 -1
  71. package/dist/webinar/index.js +1 -1
  72. package/package.json +23 -23
  73. package/src/breakouts/index.ts +7 -1
  74. package/src/constants.ts +13 -17
  75. package/src/locus-info/selfUtils.ts +0 -5
  76. package/src/media/MediaConnectionAwaiter.ts +89 -14
  77. package/src/media/index.ts +18 -9
  78. package/src/meeting/connectionStateHandler.ts +65 -0
  79. package/src/meeting/index.ts +541 -298
  80. package/src/meeting/locusMediaRequest.ts +5 -0
  81. package/src/meeting/muteState.ts +6 -1
  82. package/src/meeting/util.ts +1 -0
  83. package/src/meeting-info/index.ts +9 -6
  84. package/src/meeting-info/meeting-info-v2.ts +4 -4
  85. package/src/meeting-info/util.ts +23 -28
  86. package/src/meeting-info/utilv2.ts +18 -24
  87. package/src/meetings/collection.ts +3 -3
  88. package/src/meetings/index.ts +43 -43
  89. package/src/meetings/meetings.types.ts +11 -0
  90. package/src/meetings/util.ts +5 -4
  91. package/src/metrics/constants.ts +1 -0
  92. package/src/metrics/index.ts +44 -0
  93. package/src/personal-meeting-room/index.ts +2 -2
  94. package/src/reachability/clusterReachability.ts +86 -25
  95. package/src/reachability/index.ts +364 -30
  96. package/src/reconnection-manager/index.ts +1 -1
  97. package/src/rtcMetrics/index.ts +25 -5
  98. package/test/unit/spec/breakouts/index.ts +51 -32
  99. package/test/unit/spec/locus-info/selfUtils.js +25 -23
  100. package/test/unit/spec/media/MediaConnectionAwaiter.ts +131 -32
  101. package/test/unit/spec/media/index.ts +75 -34
  102. package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
  103. package/test/unit/spec/meeting/index.js +807 -185
  104. package/test/unit/spec/meeting/locusMediaRequest.ts +7 -0
  105. package/test/unit/spec/meeting/muteState.js +24 -0
  106. package/test/unit/spec/meeting-info/index.js +4 -4
  107. package/test/unit/spec/meeting-info/meetinginfov2.js +24 -28
  108. package/test/unit/spec/meeting-info/request.js +2 -2
  109. package/test/unit/spec/meeting-info/utilv2.js +41 -49
  110. package/test/unit/spec/meetings/index.js +44 -3
  111. package/test/unit/spec/metrics/index.js +126 -0
  112. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -2
  113. package/test/unit/spec/personal-meeting-room/personal-meeting-room.js +2 -2
  114. package/test/unit/spec/reachability/clusterReachability.ts +116 -22
  115. package/test/unit/spec/reachability/index.ts +1398 -131
  116. package/test/unit/spec/rtcMetrics/index.ts +32 -0
  117. package/dist/mediaQualityMetrics/config.js +0 -321
  118. package/dist/mediaQualityMetrics/config.js.map +0 -1
  119. package/dist/networkQualityMonitor/index.js +0 -227
  120. package/dist/networkQualityMonitor/index.js.map +0 -1
  121. package/dist/statsAnalyzer/global.js +0 -44
  122. package/dist/statsAnalyzer/global.js.map +0 -1
  123. package/dist/statsAnalyzer/index.js +0 -1072
  124. package/dist/statsAnalyzer/index.js.map +0 -1
  125. package/dist/statsAnalyzer/mqaUtil.js +0 -368
  126. package/dist/statsAnalyzer/mqaUtil.js.map +0 -1
  127. package/dist/types/mediaQualityMetrics/config.d.ts +0 -247
  128. package/dist/types/networkQualityMonitor/index.d.ts +0 -70
  129. package/dist/types/statsAnalyzer/global.d.ts +0 -36
  130. package/dist/types/statsAnalyzer/index.d.ts +0 -217
  131. package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -48
  132. package/src/mediaQualityMetrics/config.ts +0 -255
  133. package/src/networkQualityMonitor/index.ts +0 -211
  134. package/src/statsAnalyzer/global.ts +0 -37
  135. package/src/statsAnalyzer/index.ts +0 -1318
  136. package/src/statsAnalyzer/mqaUtil.ts +0 -463
  137. package/test/unit/spec/networkQualityMonitor/index.js +0 -99
  138. package/test/unit/spec/stats-analyzer/index.js +0 -1819
@@ -4,11 +4,13 @@
4
4
  import 'jsdom-global/register';
5
5
  import {cloneDeep, forEach, isEqual, isUndefined} from 'lodash';
6
6
  import sinon from 'sinon';
7
- import * as internalMediaModule from '@webex/internal-media-core';
7
+ import * as InternalMediaCoreModule from '@webex/internal-media-core';
8
+ import * as RtcMetricsModule from '@webex/plugin-meetings/src/rtcMetrics';
9
+ import * as RemoteMediaManagerModule from '@webex/plugin-meetings/src/multistream/remoteMediaManager';
8
10
  import StateMachine from 'javascript-state-machine';
9
11
  import uuid from 'uuid';
10
12
  import {assert, expect} from '@webex/test-helper-chai';
11
- import {Credentials, Token, WebexPlugin} from '@webex/webex-core';
13
+ import {Credentials, WebexPlugin} from '@webex/webex-core';
12
14
  import Support from '@webex/internal-plugin-support';
13
15
  import MockWebex from '@webex/test-helper-mock-webex';
14
16
  import StaticConfig from '@webex/plugin-meetings/src/common/config';
@@ -21,31 +23,28 @@ import {
21
23
  PASSWORD_STATUS,
22
24
  EVENTS,
23
25
  EVENT_TRIGGERS,
24
- _SIP_URI_,
25
- _MEETING_ID_,
26
+ DESTINATION_TYPE,
26
27
  MEETING_REMOVED_REASON,
27
28
  LOCUSINFO,
28
29
  ICE_AND_DTLS_CONNECTION_TIMEOUT,
29
30
  DISPLAY_HINTS,
30
31
  SELF_POLICY,
31
32
  IP_VERSION,
32
- ERROR_DICTIONARY,
33
33
  NETWORK_STATUS,
34
34
  ONLINE,
35
35
  OFFLINE,
36
- RECONNECTION,
36
+ ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
37
37
  } from '@webex/plugin-meetings/src/constants';
38
- import * as InternalMediaCoreModule from '@webex/internal-media-core';
39
38
  import {
40
39
  ConnectionState,
41
- Event,
40
+ MediaConnectionEventNames,
41
+ StatsAnalyzerEventNames,
42
42
  Errors,
43
43
  ErrorType,
44
44
  RemoteTrackType,
45
45
  MediaType,
46
46
  } from '@webex/internal-media-core';
47
47
  import {LocalStreamEventNames} from '@webex/media-helpers';
48
- import * as StatsAnalyzerModule from '@webex/plugin-meetings/src/statsAnalyzer';
49
48
  import EventsScope from '@webex/plugin-meetings/src/common/events/events-scope';
50
49
  import Meetings, {CONSTANTS} from '@webex/plugin-meetings';
51
50
  import Meeting from '@webex/plugin-meetings/src/meeting';
@@ -72,6 +71,7 @@ import {MediaRequestManager} from '@webex/plugin-meetings/src/multistream/mediaR
72
71
  import * as ReceiveSlotManagerModule from '@webex/plugin-meetings/src/multistream/receiveSlotManager';
73
72
  import * as SendSlotManagerModule from '@webex/plugin-meetings/src/multistream/sendSlotManager';
74
73
  import {CallDiagnosticUtils} from '@webex/internal-plugin-metrics';
74
+ import * as LocusMediaRequestModule from '@webex/plugin-meetings/src/meeting/locusMediaRequest';
75
75
 
76
76
  import CallDiagnosticLatencies from '@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics-latencies';
77
77
  import LLM from '@webex/internal-plugin-llm';
@@ -102,6 +102,7 @@ import {
102
102
  import {
103
103
  DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
104
104
  ICE_FAILED_WITHOUT_TURN_TLS_CLIENT_CODE,
105
+ ICE_AND_REACHABILITY_FAILED_CLIENT_CODE,
105
106
  ICE_FAILED_WITH_TURN_TLS_CLIENT_CODE,
106
107
  ICE_FAILURE_CLIENT_CODE,
107
108
  MISSING_ROAP_ANSWER_CLIENT_CODE,
@@ -279,7 +280,7 @@ describe('plugin-meetings', () => {
279
280
  deviceUrl: uuid3,
280
281
  locus: {url: url1},
281
282
  destination: testDestination,
282
- destinationType: _MEETING_ID_,
283
+ destinationType: DESTINATION_TYPE.MEETING_ID,
283
284
  correlationId,
284
285
  selfId: uuid1,
285
286
  },
@@ -347,7 +348,7 @@ describe('plugin-meetings', () => {
347
348
  assert.equal(meeting.requiredCaptcha, null);
348
349
  assert.equal(meeting.meetingInfoFailureReason, undefined);
349
350
  assert.equal(meeting.destination, testDestination);
350
- assert.equal(meeting.destinationType, _MEETING_ID_);
351
+ assert.equal(meeting.destinationType, DESTINATION_TYPE.MEETING_ID);
351
352
  assert.instanceOf(meeting.breakouts, Breakouts);
352
353
  assert.instanceOf(meeting.simultaneousInterpretation, SimultaneousInterpretation);
353
354
  assert.instanceOf(meeting.webinar, Webinar);
@@ -367,7 +368,7 @@ describe('plugin-meetings', () => {
367
368
  deviceUrl: uuid3,
368
369
  locus: {url: url1},
369
370
  destination: testDestination,
370
- destinationType: _MEETING_ID_,
371
+ destinationType: DESTINATION_TYPE.MEETING_ID,
371
372
  },
372
373
  {
373
374
  parent: webex,
@@ -385,7 +386,7 @@ describe('plugin-meetings', () => {
385
386
  deviceUrl: uuid3,
386
387
  locus: {url: url1},
387
388
  destination: testDestination,
388
- destinationType: _MEETING_ID_,
389
+ destinationType: DESTINATION_TYPE.MEETING_ID,
389
390
  callStateForMetrics: {
390
391
  correlationId: uuid4,
391
392
  joinTrigger: 'fake-join-trigger',
@@ -426,7 +427,7 @@ describe('plugin-meetings', () => {
426
427
  deviceUrl: uuid3,
427
428
  locus: {url: url1},
428
429
  destination: testDestination,
429
- destinationType: _MEETING_ID_,
430
+ destinationType: DESTINATION_TYPE.MEETING_ID,
430
431
  },
431
432
  {
432
433
  parent: webex,
@@ -502,7 +503,7 @@ describe('plugin-meetings', () => {
502
503
  deviceUrl: uuid3,
503
504
  locus: {url: url1},
504
505
  destination: testDestination,
505
- destinationType: _MEETING_ID_,
506
+ destinationType: DESTINATION_TYPE.MEETING_ID,
506
507
  },
507
508
  {
508
509
  parent: webex,
@@ -622,10 +623,13 @@ describe('plugin-meetings', () => {
622
623
  let generateTurnDiscoveryRequestMessageStub;
623
624
  let handleTurnDiscoveryHttpResponseStub;
624
625
  let abortTurnDiscoveryStub;
626
+ let addMediaInternalStub;
625
627
 
626
628
  beforeEach(() => {
627
629
  meeting.join = sinon.stub().returns(Promise.resolve(fakeJoinResult));
628
- meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
630
+ addMediaInternalStub = sinon
631
+ .stub(meeting, 'addMediaInternal')
632
+ .returns(Promise.resolve(test4));
629
633
 
630
634
  webex.meetings.reachability.getReachabilityResults.resolves(fakeReachabilityResults);
631
635
 
@@ -644,7 +648,7 @@ describe('plugin-meetings', () => {
644
648
  mediaOptions,
645
649
  });
646
650
 
647
- // check that TURN discovery is done with join and addMedia called
651
+ // check that TURN discovery is done with join and addMediaInternal() called
648
652
  assert.calledOnceWithExactly(meeting.join, {
649
653
  ...joinOptions,
650
654
  roapMessage: fakeRoapMessage,
@@ -656,12 +660,21 @@ describe('plugin-meetings', () => {
656
660
  meeting,
657
661
  fakeJoinResult
658
662
  );
659
- assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, fakeTurnServerInfo);
663
+ assert.calledOnceWithExactly(
664
+ meeting.addMediaInternal,
665
+ sinon.match.any,
666
+ fakeTurnServerInfo,
667
+ false,
668
+ mediaOptions
669
+ );
660
670
 
661
671
  assert.deepEqual(result, {join: fakeJoinResult, media: test4});
662
672
 
663
673
  // resets joinWithMediaRetryInfo
664
- assert.deepEqual(meeting.joinWithMediaRetryInfo, {isRetry: false, prevJoinResponse: undefined});
674
+ assert.deepEqual(meeting.joinWithMediaRetryInfo, {
675
+ isRetry: false,
676
+ prevJoinResponse: undefined,
677
+ });
665
678
  });
666
679
 
667
680
  it("should not call handleTurnDiscoveryHttpResponse if we don't send a TURN discovery request with join", async () => {
@@ -672,7 +685,7 @@ describe('plugin-meetings', () => {
672
685
  mediaOptions,
673
686
  });
674
687
 
675
- // check that TURN discovery is done with join and addMedia called
688
+ // check that TURN discovery is done with join and addMediaInternal() called
676
689
  assert.calledOnceWithExactly(meeting.join, {
677
690
  ...joinOptions,
678
691
  roapMessage: undefined,
@@ -681,7 +694,13 @@ describe('plugin-meetings', () => {
681
694
  assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
682
695
  assert.notCalled(handleTurnDiscoveryHttpResponseStub);
683
696
  assert.notCalled(abortTurnDiscoveryStub);
684
- assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, undefined);
697
+ assert.calledOnceWithExactly(
698
+ meeting.addMediaInternal,
699
+ sinon.match.any,
700
+ undefined,
701
+ false,
702
+ mediaOptions
703
+ );
685
704
 
686
705
  assert.deepEqual(result, {join: fakeJoinResult, media: test4});
687
706
  assert.equal(meeting.turnServerUsed, false);
@@ -698,7 +717,7 @@ describe('plugin-meetings', () => {
698
717
  mediaOptions,
699
718
  });
700
719
 
701
- // check that TURN discovery is done with join and addMedia called
720
+ // check that TURN discovery is done with join and addMediaInternal() called
702
721
  assert.calledOnceWithExactly(meeting.join, {
703
722
  ...joinOptions,
704
723
  roapMessage: fakeRoapMessage,
@@ -711,7 +730,13 @@ describe('plugin-meetings', () => {
711
730
  fakeJoinResult
712
731
  );
713
732
  assert.calledOnceWithExactly(abortTurnDiscoveryStub);
714
- assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, undefined);
733
+ assert.calledOnceWithExactly(
734
+ meeting.addMediaInternal,
735
+ sinon.match.any,
736
+ undefined,
737
+ false,
738
+ mediaOptions
739
+ );
715
740
 
716
741
  assert.deepEqual(result, {join: fakeJoinResult, media: test4});
717
742
  });
@@ -758,12 +783,20 @@ describe('plugin-meetings', () => {
758
783
  );
759
784
 
760
785
  // resets joinWithMediaRetryInfo
761
- assert.deepEqual(meeting.joinWithMediaRetryInfo, {isRetry: false, prevJoinResponse: undefined});
786
+ assert.deepEqual(meeting.joinWithMediaRetryInfo, {
787
+ isRetry: false,
788
+ prevJoinResponse: undefined,
789
+ });
762
790
  });
763
791
 
764
792
  it('should resolve if join() fails the first time but succeeds the second time', async () => {
765
793
  const error = new Error('fake');
766
- meeting.join = sinon.stub().onFirstCall().returns(Promise.reject(error)).onSecondCall().returns(Promise.resolve(fakeJoinResult));
794
+ meeting.join = sinon
795
+ .stub()
796
+ .onFirstCall()
797
+ .returns(Promise.reject(error))
798
+ .onSecondCall()
799
+ .returns(Promise.resolve(fakeJoinResult));
767
800
  const leaveStub = sinon.stub(meeting, 'leave').resolves();
768
801
 
769
802
  const result = await meeting.joinWithMedia({
@@ -795,24 +828,27 @@ describe('plugin-meetings', () => {
795
828
  assert.deepEqual(result, {join: fakeJoinResult, media: test4});
796
829
 
797
830
  // resets joinWithMediaRetryInfo
798
- assert.deepEqual(meeting.joinWithMediaRetryInfo, {isRetry: false, prevJoinResponse: undefined});
831
+ assert.deepEqual(meeting.joinWithMediaRetryInfo, {
832
+ isRetry: false,
833
+ prevJoinResponse: undefined,
834
+ });
799
835
  });
800
836
 
801
837
  it('should fail if called with allowMediaInLobby:false', async () => {
802
838
  meeting.join = sinon.stub().returns(Promise.resolve(test1));
803
- meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
839
+ meeting.addMediaInternal = sinon.stub().returns(Promise.resolve(test4));
804
840
 
805
841
  await assert.isRejected(
806
842
  meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: false}})
807
843
  );
808
844
  });
809
845
 
810
- it('should call leave() if addMedia fails and ignore leave() failure', async () => {
846
+ it('should call leave() if addMediaInternal() fails and ignore leave() failure', async () => {
811
847
  const leaveError = new Error('leave error');
812
848
  const addMediaError = new Error('fake addMedia error');
813
849
 
814
850
  const leaveStub = sinon.stub(meeting, 'leave').rejects(leaveError);
815
- meeting.addMedia = sinon.stub().rejects(addMediaError);
851
+ meeting.addMediaInternal = sinon.stub().rejects(addMediaError);
816
852
 
817
853
  await assert.isRejected(
818
854
  meeting.joinWithMedia({
@@ -828,7 +864,6 @@ describe('plugin-meetings', () => {
828
864
  reason: 'joinWithMedia failure',
829
865
  });
830
866
 
831
-
832
867
  // Behavioral metric is sent on both calls of joinWithMedia
833
868
  assert.calledTwice(Metrics.sendBehavioralMetric);
834
869
  assert.calledWith(
@@ -863,12 +898,11 @@ describe('plugin-meetings', () => {
863
898
  );
864
899
  });
865
900
 
866
- it('should not call leave() if addMedia fails the first time and succeeds the second time and should only call join() once', async () => {
901
+ it('should not call leave() if addMediaInternal() fails the first time and succeeds the second time and should only call join() once', async () => {
867
902
  const addMediaError = new Error('fake addMedia error');
868
- const leaveError = new Error('leave error');
869
- const leaveStub = sinon.stub(meeting, 'leave').rejects(leaveError);
903
+ const leaveStub = sinon.stub(meeting, 'leave');
870
904
 
871
- meeting.addMedia = sinon
905
+ meeting.addMediaInternal = sinon
872
906
  .stub()
873
907
  .onFirstCall()
874
908
  .rejects(addMediaError)
@@ -902,6 +936,203 @@ describe('plugin-meetings', () => {
902
936
  }
903
937
  );
904
938
  });
939
+
940
+ it('should send the right CA events when media connection fails', async () => {
941
+ const fakeClientError = {id: 'error'};
942
+
943
+ const fakeMediaConnection = {
944
+ close: sinon.stub(),
945
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
946
+ initiateOffer: sinon.stub().resolves({}),
947
+ on: sinon.stub(),
948
+ forceRtcMetricsSend: sinon.stub().resolves(),
949
+ };
950
+
951
+ // setup the stubs so that media connection always fails on waitForMediaConnectionConnected()
952
+ addMediaInternalStub.restore();
953
+ meeting.join.returns(
954
+ Promise.resolve({id: 'join result', roapMessage: 'fake TURN discovery response'})
955
+ );
956
+
957
+ sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
958
+ sinon.stub(meeting, 'waitForRemoteSDPAnswer').resolves();
959
+ sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
960
+ sinon
961
+ .stub(meeting.mediaProperties, 'waitForMediaConnectionConnected')
962
+ .rejects(new Error('fake error'));
963
+
964
+ webex.meetings.reachability.isWebexMediaBackendUnreachable = sinon.stub().resolves(false);
965
+ webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode = sinon
966
+ .stub()
967
+ .returns(fakeClientError);
968
+
969
+ // call joinWithMedia() - it should fail
970
+ await assert.isRejected(
971
+ meeting.joinWithMedia({
972
+ joinOptions,
973
+ mediaOptions,
974
+ })
975
+ );
976
+
977
+ // check the right CA events have been sent:
978
+ // calls at index 0 and 2 to submitClientEvent are for "client.media.capabilities" which we don't care about in this test
979
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent.getCall(1), {
980
+ name: 'client.ice.end',
981
+ payload: {
982
+ canProceed: false,
983
+ icePhase: 'JOIN_MEETING_RETRY',
984
+ errors: [fakeClientError],
985
+ },
986
+ options: {
987
+ meetingId: meeting.id,
988
+ },
989
+ });
990
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent.getCall(3), {
991
+ name: 'client.ice.end',
992
+ payload: {
993
+ canProceed: false,
994
+ icePhase: 'JOIN_MEETING_FINAL',
995
+ errors: [fakeClientError],
996
+ },
997
+ options: {
998
+ meetingId: meeting.id,
999
+ },
1000
+ });
1001
+ });
1002
+
1003
+ it('should force TURN discovery on the 2nd attempt, if addMediaInternal() fails the first time', async () => {
1004
+ const addMediaError = new Error('fake addMedia error');
1005
+
1006
+ const fakeMediaConnection = {
1007
+ close: sinon.stub(),
1008
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
1009
+ initiateOffer: sinon.stub().resolves({}),
1010
+ on: sinon.stub(),
1011
+ };
1012
+
1013
+ /* Setup the stubs so that the first call to addMediaInternal() fails
1014
+ and the 2nd call calls the real implementation - so that we can check that
1015
+ addMediaInternal() eventually calls meeting.roap.doTurnDiscovery() with isForced=true.
1016
+ As a result we need to also stub a few other methods like createMediaConnection() and waitForRemoteSDPAnswer() */
1017
+ sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
1018
+ sinon.stub(meeting, 'waitForRemoteSDPAnswer').resolves();
1019
+
1020
+ addMediaInternalStub.onFirstCall().rejects(addMediaError);
1021
+ addMediaInternalStub.onSecondCall().callsFake((...args) => {
1022
+ return addMediaInternalStub.wrappedMethod.bind(meeting)(...args);
1023
+ });
1024
+
1025
+ sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
1026
+
1027
+ const result = await meeting.joinWithMedia({
1028
+ joinOptions,
1029
+ mediaOptions,
1030
+ });
1031
+
1032
+ assert.deepEqual(result, {join: fakeJoinResult, media: undefined});
1033
+
1034
+ assert.calledOnce(meeting.join);
1035
+
1036
+ // first addMediaInternal() call without forcing TURN
1037
+ assert.calledWith(
1038
+ meeting.addMediaInternal.firstCall,
1039
+ sinon.match.any,
1040
+ fakeTurnServerInfo,
1041
+ false,
1042
+ mediaOptions
1043
+ );
1044
+
1045
+ // second addMediaInternal() call with forcing TURN
1046
+ assert.calledWith(
1047
+ meeting.addMediaInternal.secondCall,
1048
+ sinon.match.any,
1049
+ undefined,
1050
+ true,
1051
+ mediaOptions
1052
+ );
1053
+
1054
+ // now check that TURN is actually forced by addMediaInternal(),
1055
+ // we're not checking the isReconnecting param value, because it depends on the full sequence of things
1056
+ // being done correctly (like SDP offer creation) and some of these are stubbed in this test
1057
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, sinon.match.any, true);
1058
+ });
1059
+
1060
+ it('should return the right icePhase in icePhaseCallback on 1st attempt and retry', async () => {
1061
+ const addMediaError = new Error('fake addMedia error');
1062
+
1063
+ const icePhaseCallbacks = [];
1064
+ const addMediaInternalResults = [];
1065
+
1066
+ meeting.addMediaInternal = sinon
1067
+ .stub()
1068
+ .callsFake((icePhaseCallback, _turnServerInfo, _forceTurnDiscovery) => {
1069
+ const defer = new Defer();
1070
+
1071
+ icePhaseCallbacks.push(icePhaseCallback);
1072
+ addMediaInternalResults.push(defer);
1073
+ return defer.promise;
1074
+ });
1075
+
1076
+ const result = meeting.joinWithMedia({
1077
+ joinOptions,
1078
+ mediaOptions,
1079
+ });
1080
+
1081
+ await testUtils.flushPromises();
1082
+
1083
+ // check the callback works correctly on the 1st attempt
1084
+ assert.equal(icePhaseCallbacks.length, 1);
1085
+ assert.equal(icePhaseCallbacks[0](), 'JOIN_MEETING_RETRY');
1086
+
1087
+ // now trigger the failure, so that joinWithMedia() does a retry
1088
+ addMediaInternalResults[0].reject(addMediaError);
1089
+
1090
+ await testUtils.flushPromises();
1091
+
1092
+ // check the callback works correctly on the 2nd attempt
1093
+ assert.equal(icePhaseCallbacks.length, 2);
1094
+ assert.equal(icePhaseCallbacks[1](), 'JOIN_MEETING_FINAL');
1095
+
1096
+ // trigger 2nd failure
1097
+ addMediaInternalResults[1].reject(addMediaError);
1098
+
1099
+ await assert.isRejected(result);
1100
+ });
1101
+
1102
+ it('should not attempt a retry if we fail to create the offer on first atttempt', async () => {
1103
+ const addMediaError = new Error('fake addMedia error');
1104
+ addMediaError.name = 'SdpOfferCreationError';
1105
+
1106
+ meeting.addMediaInternal.rejects(addMediaError);
1107
+
1108
+ await assert.isRejected(
1109
+ meeting.joinWithMedia({
1110
+ joinOptions,
1111
+ mediaOptions,
1112
+ }),
1113
+ addMediaError
1114
+ );
1115
+
1116
+ // check that only 1 attempt was done
1117
+ assert.calledOnce(meeting.join);
1118
+ assert.calledOnce(meeting.addMediaInternal);
1119
+ assert.calledOnce(Metrics.sendBehavioralMetric);
1120
+ assert.calledWith(
1121
+ Metrics.sendBehavioralMetric.firstCall,
1122
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
1123
+ {
1124
+ correlation_id: meeting.correlationId,
1125
+ locus_id: meeting.locusUrl.split('/').pop(),
1126
+ reason: addMediaError.message,
1127
+ stack: addMediaError.stack,
1128
+ leaveErrorReason: undefined,
1129
+ isRetry: false,
1130
+ },
1131
+ {
1132
+ type: addMediaError.name,
1133
+ }
1134
+ );
1135
+ });
905
1136
  });
906
1137
 
907
1138
  describe('#isTranscriptionSupported', () => {
@@ -946,19 +1177,18 @@ describe('plugin-meetings', () => {
946
1177
  assert.calledTwice(webex.internal.voicea.turnOnCaptions);
947
1178
  });
948
1179
 
949
- it('should listen to events and not turnOnCaptions if the user is not a host', async () => {
1180
+ it('should listen to events and turnOnCaptions for all users', async () => {
950
1181
  meeting.joinedWith = {
951
1182
  state: 'JOINED',
952
1183
  };
953
1184
  meeting.areVoiceaEventsSetup = false;
954
- meeting.roles = ['COHOST'];
955
1185
 
956
1186
  await meeting.startTranscription();
957
1187
 
958
1188
  assert.equal(webex.internal.voicea.on.callCount, 4);
959
1189
  assert.equal(meeting.areVoiceaEventsSetup, true);
960
1190
  assert.equal(webex.internal.voicea.listenToEvents.callCount, 1);
961
- assert.notCalled(webex.internal.voicea.turnOnCaptions);
1191
+ assert.calledOnce(webex.internal.voicea.turnOnCaptions);
962
1192
  });
963
1193
 
964
1194
  it("should throw error if request doesn't work", async () => {
@@ -1075,6 +1305,7 @@ describe('plugin-meetings', () => {
1075
1305
  webex.internal.voicea.on = sinon.stub();
1076
1306
  webex.internal.voicea.off = sinon.stub();
1077
1307
  webex.internal.voicea.setSpokenLanguage = sinon.stub();
1308
+ meeting.roles = ['MODERATOR'];
1078
1309
  });
1079
1310
 
1080
1311
  afterEach(() => {
@@ -1091,6 +1322,16 @@ describe('plugin-meetings', () => {
1091
1322
  });
1092
1323
  });
1093
1324
 
1325
+ it('should reject if current user is not a host', (done) => {
1326
+ meeting.isTranscriptionSupported.returns(true);
1327
+ meeting.roles = ['COHOST'];
1328
+
1329
+ meeting.setSpokenLanguage('fr').catch((error) => {
1330
+ assert.equal(error.message, 'Only host can set spoken language');
1331
+ done();
1332
+ });
1333
+ });
1334
+
1094
1335
  it('should resolve with the language code on successful language update', (done) => {
1095
1336
  meeting.isTranscriptionSupported.returns(true);
1096
1337
  const languageCode = 'fr';
@@ -1136,10 +1377,7 @@ describe('plugin-meetings', () => {
1136
1377
 
1137
1378
  it('should trigger meeting:caption-received event', () => {
1138
1379
  meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({});
1139
- assert.calledWith(
1140
- meeting.trigger,
1141
- EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED
1142
- );
1380
+ assert.calledWith(meeting.trigger, EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED);
1143
1381
  });
1144
1382
 
1145
1383
  it('should trigger meeting:receiveTranscription:started event', () => {
@@ -1152,10 +1390,7 @@ describe('plugin-meetings', () => {
1152
1390
 
1153
1391
  it('should trigger meeting:caption-received event', () => {
1154
1392
  meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({});
1155
- assert.calledWith(
1156
- meeting.trigger,
1157
- EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED
1158
- );
1393
+ assert.calledWith(meeting.trigger, EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED);
1159
1394
  });
1160
1395
  });
1161
1396
 
@@ -1310,11 +1545,7 @@ describe('plugin-meetings', () => {
1310
1545
 
1311
1546
  it('turns off llm online, emits transcription connected events', () => {
1312
1547
  meeting.handleLLMOnline();
1313
- assert.calledOnceWithExactly(
1314
- webex.internal.llm.off,
1315
- 'online',
1316
- meeting.handleLLMOnline
1317
- );
1548
+ assert.calledOnceWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline);
1318
1549
  assert.calledWith(
1319
1550
  TriggerProxy.trigger,
1320
1551
  sinon.match.instanceOf(Meeting),
@@ -1376,11 +1607,40 @@ describe('plugin-meetings', () => {
1376
1607
  assert.calledOnce(MeetingUtil.joinMeeting);
1377
1608
  assert.calledOnce(meeting.setLocus);
1378
1609
  assert.equal(result, joinMeetingResult);
1379
- assert.calledWith(
1380
- webex.internal.llm.on,
1381
- 'online',
1382
- meeting.handleLLMOnline
1383
- );
1610
+ assert.calledWith(webex.internal.llm.on, 'online', meeting.handleLLMOnline);
1611
+ });
1612
+
1613
+ [true, false].forEach((enableMultistream) => {
1614
+ it(`should instantiate LocusMediaRequest with correct parameters (enableMultistream=${enableMultistream})`, async () => {
1615
+ meeting.config.deviceType = 'web';
1616
+ meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
1617
+
1618
+ const mockLocusMediaRequestCtor = sinon
1619
+ .stub(LocusMediaRequestModule, 'LocusMediaRequest')
1620
+ .returns({
1621
+ id: 'fake LocusMediaRequest instance',
1622
+ });
1623
+
1624
+ await meeting.join({enableMultistream});
1625
+
1626
+ assert.calledOnceWithExactly(
1627
+ mockLocusMediaRequestCtor,
1628
+ {
1629
+ correlationId: meeting.correlationId,
1630
+ meetingId: meeting.id,
1631
+ device: {
1632
+ url: meeting.deviceUrl,
1633
+ deviceType: meeting.config.deviceType,
1634
+ countryCode: 'UK',
1635
+ regionCode: 'EU',
1636
+ },
1637
+ preferTranscoding: !enableMultistream,
1638
+ },
1639
+ {
1640
+ parent: meeting.webex,
1641
+ }
1642
+ );
1643
+ });
1384
1644
  });
1385
1645
 
1386
1646
  it('should take trigger from meeting joinTrigger if available', () => {
@@ -1661,7 +1921,7 @@ describe('plugin-meetings', () => {
1661
1921
 
1662
1922
  let fakeMediaConnection;
1663
1923
 
1664
- beforeEach(() => {
1924
+ beforeEach(async () => {
1665
1925
  fakeMediaConnection = {
1666
1926
  close: sinon.stub(),
1667
1927
  getConnectionState: sinon.stub().returns(ConnectionState.Connected),
@@ -1670,17 +1930,29 @@ describe('plugin-meetings', () => {
1670
1930
  };
1671
1931
  meeting.mediaProperties.setMediaDirection = sinon.stub().returns(true);
1672
1932
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
1673
- meeting.mediaProperties.getCurrentConnectionInfo = sinon.stub().resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
1933
+ meeting.mediaProperties.getCurrentConnectionInfo = sinon
1934
+ .stub()
1935
+ .resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
1674
1936
  meeting.audio = muteStateStub;
1675
1937
  meeting.video = muteStateStub;
1676
1938
  sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
1677
- meeting.setMercuryListener = sinon.stub().returns(true);
1678
- meeting.setupMediaConnectionListeners = sinon.stub();
1679
- meeting.setMercuryListener = sinon.stub();
1680
- meeting.roap.doTurnDiscovery = sinon
1681
- .stub()
1939
+ sinon.stub(meeting, 'setupMediaConnectionListeners');
1940
+ sinon.stub(meeting, 'setMercuryListener');
1941
+ sinon
1942
+ .stub(meeting.roap, 'doTurnDiscovery')
1682
1943
  .resolves({turnServerInfo: {}, turnDiscoverySkippedReason: undefined});
1683
- meeting.waitForRemoteSDPAnswer = sinon.stub().resolves();
1944
+ sinon.stub(meeting, 'waitForRemoteSDPAnswer').resolves();
1945
+
1946
+ // normally the first Roap message we send is creating confluence, so mock LocusMediaRequest.isConfluenceCreated()
1947
+ // to return false the first time it's called and true the 2nd time, to simulate how it would happen for real
1948
+ meeting.locusMediaRequest = {
1949
+ isConfluenceCreated: sinon
1950
+ .stub()
1951
+ .onFirstCall()
1952
+ .returns(false)
1953
+ .onSecondCall()
1954
+ .returns(true),
1955
+ };
1684
1956
  });
1685
1957
 
1686
1958
  it('should have #addMedia', () => {
@@ -1778,6 +2050,7 @@ describe('plugin-meetings', () => {
1778
2050
  someReachabilityMetric2: 'some value2',
1779
2051
  selectedCandidatePairChanges: 2,
1780
2052
  numTransports: 1,
2053
+ iceCandidatesCount: 0,
1781
2054
  }
1782
2055
  );
1783
2056
  });
@@ -1885,6 +2158,7 @@ describe('plugin-meetings', () => {
1885
2158
  someReachabilityMetric2: 'some value2',
1886
2159
  selectedCandidatePairChanges: 2,
1887
2160
  numTransports: 1,
2161
+ iceCandidatesCount: 0,
1888
2162
  }
1889
2163
  );
1890
2164
  });
@@ -2028,6 +2302,61 @@ describe('plugin-meetings', () => {
2028
2302
  }
2029
2303
  });
2030
2304
 
2305
+ it('sends correct CA event when times out waiting for SDP answer', async () => {
2306
+ const eventListeners = {};
2307
+ const clock = sinon.useFakeTimers();
2308
+
2309
+ // these 2 are stubbed, we need the real versions:
2310
+ meeting.waitForRemoteSDPAnswer.restore();
2311
+ meeting.setupMediaConnectionListeners.restore();
2312
+
2313
+ meeting.meetingState = 'ACTIVE';
2314
+
2315
+ // setup a mock media connection that will trigger an offer when initiateOffer() is called
2316
+ Media.createMediaConnection = sinon.stub().returns({
2317
+ initiateOffer: sinon.stub().callsFake(() => {
2318
+ // simulate offer being generated
2319
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
2320
+
2321
+ return Promise.resolve();
2322
+ }),
2323
+ close: sinon.stub(),
2324
+ on: (event, listener) => {
2325
+ eventListeners[event] = listener;
2326
+ },
2327
+ forceRtcMetricsSend: sinon.stub().resolves(),
2328
+ });
2329
+
2330
+ const getErrorPayloadForClientErrorCodeStub =
2331
+ (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
2332
+ sinon
2333
+ .stub()
2334
+ .callsFake(({clientErrorCode}) => ({errorCode: clientErrorCode, fatal: true})));
2335
+
2336
+ const result = meeting.addMedia();
2337
+ await testUtils.flushPromises();
2338
+
2339
+ // simulate timeout waiting for the SDP answer that never comes
2340
+ await clock.tickAsync(ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT);
2341
+
2342
+ await assert.isRejected(result);
2343
+
2344
+ assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
2345
+ clientErrorCode: 2007,
2346
+ });
2347
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2348
+ name: 'client.media-engine.remote-sdp-received',
2349
+ payload: {
2350
+ canProceed: false,
2351
+ errors: [{errorCode: 2007, fatal: true}],
2352
+ },
2353
+ options: {
2354
+ meetingId: meeting.id,
2355
+ rawError: sinon.match.instanceOf(Error),
2356
+ },
2357
+ });
2358
+ });
2359
+
2031
2360
  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
2361
  meeting.meetingState = 'ACTIVE';
2033
2362
  // setup the mock to cause addMedia() to fail
@@ -2078,9 +2407,7 @@ describe('plugin-meetings', () => {
2078
2407
  Media.createMediaConnection,
2079
2408
  false,
2080
2409
  meeting.getMediaConnectionDebugId(),
2081
- webex,
2082
2410
  meeting.id,
2083
- meeting.correlationId,
2084
2411
  sinon.match({turnServerInfo: undefined})
2085
2412
  );
2086
2413
  assert.calledOnce(meeting.setMercuryListener);
@@ -2122,6 +2449,44 @@ describe('plugin-meetings', () => {
2122
2449
  checkWorking({allowMediaInLobby: true});
2123
2450
  });
2124
2451
 
2452
+ it('should create rtcMetrics and pass them to Media.createMediaConnection()', async () => {
2453
+ const fakeRtcMetrics = {id: 'fake rtc metrics object'};
2454
+ const rtcMetricsCtor = sinon.stub(RtcMetricsModule, 'default').returns(fakeRtcMetrics);
2455
+
2456
+ // setup the minimum mocks required for multistream connection
2457
+ fakeMediaConnection.createSendSlot = sinon.stub().returns({
2458
+ publishStream: sinon.stub(),
2459
+ unpublishStream: sinon.stub(),
2460
+ setNamedMediaGroups: sinon.stub(),
2461
+ });
2462
+ sinon.stub(RemoteMediaManagerModule, 'RemoteMediaManager').returns({
2463
+ start: sinon.stub().resolves(),
2464
+ on: sinon.stub(),
2465
+ logAllReceiveSlots: sinon.stub(),
2466
+ });
2467
+
2468
+ meeting.meetingState = 'ACTIVE';
2469
+ meeting.isMultistream = true;
2470
+
2471
+ await meeting.addMedia({
2472
+ mediaSettings: {},
2473
+ });
2474
+
2475
+ assert.calledOnceWithExactly(rtcMetricsCtor, webex, meeting.id, meeting.correlationId);
2476
+
2477
+ // check that rtcMetrics was passed to Media.createMediaConnection
2478
+ assert.calledOnce(Media.createMediaConnection);
2479
+ assert.calledWith(
2480
+ Media.createMediaConnection,
2481
+ true,
2482
+ meeting.getMediaConnectionDebugId(),
2483
+ meeting.id,
2484
+ sinon.match({
2485
+ rtcMetrics: fakeRtcMetrics,
2486
+ })
2487
+ );
2488
+ });
2489
+
2125
2490
  it('should pass the turn server info to the peer connection', async () => {
2126
2491
  const FAKE_TURN_URL = 'turns:webex.com:3478';
2127
2492
  const FAKE_TURN_USER = 'some-turn-username';
@@ -2151,9 +2516,7 @@ describe('plugin-meetings', () => {
2151
2516
  Media.createMediaConnection,
2152
2517
  false,
2153
2518
  meeting.getMediaConnectionDebugId(),
2154
- webex,
2155
2519
  meeting.id,
2156
- meeting.correlationId,
2157
2520
  sinon.match({
2158
2521
  turnServerInfo: {
2159
2522
  url: FAKE_TURN_URL,
@@ -2189,6 +2552,10 @@ describe('plugin-meetings', () => {
2189
2552
  const getErrorPayloadForClientErrorCodeStub =
2190
2553
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
2191
2554
  sinon.stub().returns(FAKE_ERROR));
2555
+ webex.meetings.reachability = {
2556
+ isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2557
+ getReachabilityMetrics: sinon.stub().resolves(),
2558
+ };
2192
2559
  const MOCK_CLIENT_ERROR_CODE = 2004;
2193
2560
  const generateClientErrorCodeForIceFailureStub = sinon
2194
2561
  .stub(CallDiagnosticUtils, 'generateClientErrorCodeForIceFailure')
@@ -2216,7 +2583,7 @@ describe('plugin-meetings', () => {
2216
2583
  turnDiscoverySkippedReason: undefined,
2217
2584
  });
2218
2585
  meeting.meetingState = 'ACTIVE';
2219
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects(new Error('fake error'));
2586
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2220
2587
 
2221
2588
  const forceRtcMetricsSend = sinon.stub().resolves();
2222
2589
  const closeMediaConnectionStub = sinon.stub();
@@ -2240,13 +2607,15 @@ describe('plugin-meetings', () => {
2240
2607
  assert.calledTwice(generateClientErrorCodeForIceFailureStub);
2241
2608
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2242
2609
  signalingState: 'unknown',
2243
- iceConnectionState: 'unknown',
2610
+ iceConnected: false,
2244
2611
  turnServerUsed: false,
2612
+ unreachable: false,
2245
2613
  });
2246
2614
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2247
2615
  signalingState: 'unknown',
2248
- iceConnectionState: 'unknown',
2616
+ iceConnected: false,
2249
2617
  turnServerUsed: true,
2618
+ unreachable: false,
2250
2619
  });
2251
2620
 
2252
2621
  assert.calledTwice(getErrorPayloadForClientErrorCodeStub);
@@ -2364,6 +2733,7 @@ describe('plugin-meetings', () => {
2364
2733
  iceConnectionState: 'unknown',
2365
2734
  selectedCandidatePairChanges: 2,
2366
2735
  numTransports: 1,
2736
+ iceCandidatesCount: 0,
2367
2737
  },
2368
2738
  ]);
2369
2739
 
@@ -2371,7 +2741,7 @@ describe('plugin-meetings', () => {
2371
2741
  const doTurnDiscoveryCalls = meeting.roap.doTurnDiscovery.getCalls();
2372
2742
  assert.equal(doTurnDiscoveryCalls.length, 2);
2373
2743
  assert.deepEqual(doTurnDiscoveryCalls[0].args, [meeting, false, false]);
2374
- assert.deepEqual(doTurnDiscoveryCalls[1].args, [meeting, true, true]);
2744
+ assert.deepEqual(doTurnDiscoveryCalls[1].args.slice(1), [true, true]);
2375
2745
 
2376
2746
  // Some clean up steps happens twice
2377
2747
  assert.calledTwice(forceRtcMetricsSend);
@@ -2383,6 +2753,17 @@ describe('plugin-meetings', () => {
2383
2753
 
2384
2754
  it('should resolve if waitForMediaConnectionConnected() rejects the first time but resolves the second time', async () => {
2385
2755
  const FAKE_ERROR = {fatal: true};
2756
+ webex.meetings.reachability = {
2757
+ isWebexMediaBackendUnreachable: sinon
2758
+ .stub()
2759
+ .onCall(0)
2760
+ .rejects()
2761
+ .onCall(1)
2762
+ .resolves(true)
2763
+ .onCall(2)
2764
+ .resolves(false),
2765
+ getReachabilityMetrics: sinon.stub().resolves({}),
2766
+ };
2386
2767
  const getErrorPayloadForClientErrorCodeStub =
2387
2768
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
2388
2769
  sinon.stub().returns(FAKE_ERROR));
@@ -2440,8 +2821,9 @@ describe('plugin-meetings', () => {
2440
2821
  assert.calledOnce(generateClientErrorCodeForIceFailureStub);
2441
2822
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2442
2823
  signalingState: 'unknown',
2443
- iceConnectionState: 'unknown',
2824
+ iceConnected: undefined,
2444
2825
  turnServerUsed: false,
2826
+ unreachable: false,
2445
2827
  });
2446
2828
 
2447
2829
  assert.calledOnce(getErrorPayloadForClientErrorCodeStub);
@@ -2547,6 +2929,7 @@ describe('plugin-meetings', () => {
2547
2929
  isMultistream: false,
2548
2930
  retriedWithTurnServer: true,
2549
2931
  isJoinWithMediaRetry: false,
2932
+ iceCandidatesCount: 0,
2550
2933
  },
2551
2934
  ]);
2552
2935
  meeting.roap.doTurnDiscovery;
@@ -2675,6 +3058,10 @@ describe('plugin-meetings', () => {
2675
3058
  someReachabilityMetric2: 'some value2',
2676
3059
  }),
2677
3060
  };
3061
+ meeting.iceCandidatesCount = 3;
3062
+ meeting.iceCandidateErrors.set('701_error', 3);
3063
+ meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
3064
+
2678
3065
  await meeting.addMedia({
2679
3066
  mediaSettings: {},
2680
3067
  });
@@ -2694,6 +3081,9 @@ describe('plugin-meetings', () => {
2694
3081
  isJoinWithMediaRetry: false,
2695
3082
  someReachabilityMetric1: 'some value1',
2696
3083
  someReachabilityMetric2: 'some value2',
3084
+ iceCandidatesCount: 3,
3085
+ '701_error': 3,
3086
+ '701_turn_host_lookup_received_error': 1,
2697
3087
  }
2698
3088
  );
2699
3089
 
@@ -2715,7 +3105,63 @@ describe('plugin-meetings', () => {
2715
3105
  turnDiscoverySkippedReason: undefined,
2716
3106
  });
2717
3107
  meeting.meetingState = 'ACTIVE';
2718
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects(new Error('fake error'));
3108
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
3109
+
3110
+ const forceRtcMetricsSend = sinon.stub().resolves();
3111
+ const closeMediaConnectionStub = sinon.stub();
3112
+ Media.createMediaConnection = sinon.stub().returns({
3113
+ close: closeMediaConnectionStub,
3114
+ forceRtcMetricsSend,
3115
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3116
+ initiateOffer: sinon.stub().resolves({}),
3117
+ on: sinon.stub(),
3118
+ });
3119
+
3120
+ await meeting
3121
+ .addMedia({
3122
+ mediaSettings: {},
3123
+ })
3124
+ .catch((err) => {
3125
+ errorThrown = err;
3126
+ assert.instanceOf(err, AddMediaFailed);
3127
+ });
3128
+
3129
+ // Check that the only metric sent is ADD_MEDIA_FAILURE
3130
+ assert.calledOnceWithExactly(
3131
+ Metrics.sendBehavioralMetric,
3132
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
3133
+ {
3134
+ correlation_id: meeting.correlationId,
3135
+ locus_id: meeting.locusUrl.split('/').pop(),
3136
+ reason: errorThrown.message,
3137
+ stack: errorThrown.stack,
3138
+ code: errorThrown.code,
3139
+ turnDiscoverySkippedReason: undefined,
3140
+ turnServerUsed: true,
3141
+ retriedWithTurnServer: false,
3142
+ isMultistream: false,
3143
+ isJoinWithMediaRetry: false,
3144
+ signalingState: 'unknown',
3145
+ connectionState: 'unknown',
3146
+ iceConnectionState: 'unknown',
3147
+ selectedCandidatePairChanges: 2,
3148
+ numTransports: 1,
3149
+ iceCandidatesCount: 0,
3150
+ }
3151
+ );
3152
+
3153
+ assert.isOk(errorThrown);
3154
+ });
3155
+
3156
+ it('should send ICE_CANDIDATE_ERROR metric if media connection fails and ice candidate errors have been gathered', async () => {
3157
+ let errorThrown = undefined;
3158
+
3159
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3160
+ turnServerInfo: undefined,
3161
+ turnDiscoverySkippedReason: undefined,
3162
+ });
3163
+ meeting.meetingState = 'ACTIVE';
3164
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2719
3165
 
2720
3166
  const forceRtcMetricsSend = sinon.stub().resolves();
2721
3167
  const closeMediaConnectionStub = sinon.stub();
@@ -2727,6 +3173,9 @@ describe('plugin-meetings', () => {
2727
3173
  on: sinon.stub(),
2728
3174
  });
2729
3175
 
3176
+ meeting.iceCandidateErrors.set('701_error', 2);
3177
+ meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
3178
+
2730
3179
  await meeting
2731
3180
  .addMedia({
2732
3181
  mediaSettings: {},
@@ -2756,6 +3205,9 @@ describe('plugin-meetings', () => {
2756
3205
  iceConnectionState: 'unknown',
2757
3206
  selectedCandidatePairChanges: 2,
2758
3207
  numTransports: 1,
3208
+ '701_error': 2,
3209
+ '701_turn_host_lookup_received_error': 1,
3210
+ iceCandidatesCount: 0,
2759
3211
  }
2760
3212
  );
2761
3213
 
@@ -2775,7 +3227,7 @@ describe('plugin-meetings', () => {
2775
3227
 
2776
3228
  statsAnalyzerStub = new EventsScope();
2777
3229
  // mock the StatsAnalyzer constructor
2778
- sinon.stub(StatsAnalyzerModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
3230
+ sinon.stub(InternalMediaCoreModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
2779
3231
 
2780
3232
  await meeting.addMedia({
2781
3233
  mediaSettings: {},
@@ -2789,8 +3241,8 @@ describe('plugin-meetings', () => {
2789
3241
  it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and sends metrics', async () => {
2790
3242
  statsAnalyzerStub.emit(
2791
3243
  {file: 'test', function: 'test'},
2792
- StatsAnalyzerModule.EVENTS.LOCAL_MEDIA_STARTED,
2793
- {type: 'audio'}
3244
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED,
3245
+ {mediaType: 'audio'}
2794
3246
  );
2795
3247
 
2796
3248
  assert.calledWith(
@@ -2802,7 +3254,7 @@ describe('plugin-meetings', () => {
2802
3254
  },
2803
3255
  EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED,
2804
3256
  {
2805
- type: 'audio',
3257
+ mediaType: 'audio',
2806
3258
  }
2807
3259
  );
2808
3260
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2817,8 +3269,8 @@ describe('plugin-meetings', () => {
2817
3269
  it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => {
2818
3270
  statsAnalyzerStub.emit(
2819
3271
  {file: 'test', function: 'test'},
2820
- StatsAnalyzerModule.EVENTS.LOCAL_MEDIA_STOPPED,
2821
- {type: 'video'}
3272
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED,
3273
+ {mediaType: 'video'}
2822
3274
  );
2823
3275
 
2824
3276
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2833,8 +3285,8 @@ describe('plugin-meetings', () => {
2833
3285
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => {
2834
3286
  statsAnalyzerStub.emit(
2835
3287
  {file: 'test', function: 'test'},
2836
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STARTED,
2837
- {type: 'video'}
3288
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3289
+ {mediaType: 'video'}
2838
3290
  );
2839
3291
 
2840
3292
  assert.calledWith(
@@ -2846,7 +3298,7 @@ describe('plugin-meetings', () => {
2846
3298
  },
2847
3299
  EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
2848
3300
  {
2849
- type: 'video',
3301
+ mediaType: 'video',
2850
3302
  }
2851
3303
  );
2852
3304
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2861,8 +3313,8 @@ describe('plugin-meetings', () => {
2861
3313
  it('REMOTE_MEDIA_STOPPED triggers the right metrics', async () => {
2862
3314
  statsAnalyzerStub.emit(
2863
3315
  {file: 'test', function: 'test'},
2864
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STOPPED,
2865
- {type: 'audio'}
3316
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3317
+ {mediaType: 'audio'}
2866
3318
  );
2867
3319
 
2868
3320
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2877,8 +3329,8 @@ describe('plugin-meetings', () => {
2877
3329
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics for share', async () => {
2878
3330
  statsAnalyzerStub.emit(
2879
3331
  {file: 'test', function: 'test'},
2880
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STARTED,
2881
- {type: 'share'}
3332
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3333
+ {mediaType: 'share'}
2882
3334
  );
2883
3335
 
2884
3336
  assert.calledWith(
@@ -2890,7 +3342,7 @@ describe('plugin-meetings', () => {
2890
3342
  },
2891
3343
  EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
2892
3344
  {
2893
- type: 'share',
3345
+ mediaType: 'share',
2894
3346
  }
2895
3347
  );
2896
3348
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2913,8 +3365,8 @@ describe('plugin-meetings', () => {
2913
3365
  it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
2914
3366
  statsAnalyzerStub.emit(
2915
3367
  {file: 'test', function: 'test'},
2916
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STOPPED,
2917
- {type: 'share'}
3368
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3369
+ {mediaType: 'share'}
2918
3370
  );
2919
3371
 
2920
3372
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2935,19 +3387,18 @@ describe('plugin-meetings', () => {
2935
3387
  });
2936
3388
 
2937
3389
  it('calls submitMQE correctly', async () => {
2938
- const fakeData = {intervalMetadata: {bla: 'bla'}};
3390
+ const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
2939
3391
 
2940
3392
  statsAnalyzerStub.emit(
2941
3393
  {file: 'test', function: 'test'},
2942
- StatsAnalyzerModule.EVENTS.MEDIA_QUALITY,
2943
- {data: fakeData, networkType: 'wifi'}
3394
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3395
+ {data: fakeData}
2944
3396
  );
2945
3397
 
2946
3398
  assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
2947
3399
  name: 'client.mediaquality.event',
2948
3400
  options: {
2949
3401
  meetingId: meeting.id,
2950
- networkType: 'wifi',
2951
3402
  },
2952
3403
  payload: {
2953
3404
  intervals: [fakeData],
@@ -2986,9 +3437,7 @@ describe('plugin-meetings', () => {
2986
3437
  Media.createMediaConnection,
2987
3438
  false,
2988
3439
  meeting.getMediaConnectionDebugId(),
2989
- webex,
2990
3440
  meeting.id,
2991
- meeting.correlationId,
2992
3441
  sinon.match({
2993
3442
  turnServerInfo: {
2994
3443
  url: FAKE_TURN_URL,
@@ -3004,7 +3453,7 @@ describe('plugin-meetings', () => {
3004
3453
  it('succeeds even if getDevices() throws', async () => {
3005
3454
  meeting.meetingState = 'ACTIVE';
3006
3455
 
3007
- sinon.stub(internalMediaModule, 'getDevices').rejects(new Error('fake error'));
3456
+ sinon.stub(InternalMediaCoreModule, 'getDevices').rejects(new Error('fake error'));
3008
3457
 
3009
3458
  await meeting.addMedia();
3010
3459
  });
@@ -3021,7 +3470,7 @@ describe('plugin-meetings', () => {
3021
3470
  clientErrorCode: MISSING_ROAP_ANSWER_CLIENT_CODE,
3022
3471
  expectedErrorPayload: {
3023
3472
  errorDescription: ERROR_DESCRIPTIONS.MISSING_ROAP_ANSWER,
3024
- category: 'signaling',
3473
+ category: 'media',
3025
3474
  },
3026
3475
  },
3027
3476
  {
@@ -3040,10 +3489,18 @@ describe('plugin-meetings', () => {
3040
3489
  clientErrorCode: ICE_FAILED_WITH_TURN_TLS_CLIENT_CODE,
3041
3490
  expectedErrorPayload: {
3042
3491
  errorDescription: ERROR_DESCRIPTIONS.ICE_FAILED_WITH_TURN_TLS,
3043
- category: 'network',
3492
+ category: 'media',
3493
+ },
3494
+ },
3495
+ {
3496
+ clientErrorCode: ICE_AND_REACHABILITY_FAILED_CLIENT_CODE,
3497
+ unreachable: true,
3498
+ expectedErrorPayload: {
3499
+ errorDescription: ERROR_DESCRIPTIONS.ICE_AND_REACHABILITY_FAILED,
3500
+ category: 'expected',
3044
3501
  },
3045
3502
  },
3046
- ].forEach(({clientErrorCode, expectedErrorPayload}) => {
3503
+ ].forEach(({clientErrorCode, expectedErrorPayload, unreachable}) => {
3047
3504
  it(`should handle all ice failures correctly for ${clientErrorCode}`, async () => {
3048
3505
  // setting the method to the real implementation
3049
3506
  // because newMetrics is mocked completely in the webex-mock
@@ -3052,14 +3509,18 @@ describe('plugin-meetings', () => {
3052
3509
  webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
3053
3510
  CD.getErrorPayloadForClientErrorCode;
3054
3511
 
3512
+ webex.meetings.reachability = {
3513
+ isWebexMediaBackendUnreachable: sinon.stub().resolves(unreachable || false),
3514
+ };
3515
+
3055
3516
  const generateClientErrorCodeForIceFailureStub = sinon
3056
3517
  .stub(CallDiagnosticUtils, 'generateClientErrorCodeForIceFailure')
3057
3518
  .returns(clientErrorCode);
3058
3519
 
3059
3520
  meeting.meetingState = 'ACTIVE';
3060
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects(
3061
- new Error('fake error')
3062
- );
3521
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({
3522
+ iceConnected: false,
3523
+ });
3063
3524
 
3064
3525
  let errorThrown = false;
3065
3526
 
@@ -3073,8 +3534,9 @@ describe('plugin-meetings', () => {
3073
3534
 
3074
3535
  assert.calledOnceWithExactly(generateClientErrorCodeForIceFailureStub, {
3075
3536
  signalingState: 'unknown',
3076
- iceConnectionState: 'unknown',
3537
+ iceConnected: false,
3077
3538
  turnServerUsed: true,
3539
+ unreachable: unreachable || false,
3078
3540
  });
3079
3541
 
3080
3542
  const submitClientEventCalls = webex.internal.newMetrics.submitClientEvent.getCalls();
@@ -3162,7 +3624,7 @@ describe('plugin-meetings', () => {
3162
3624
 
3163
3625
  let clock;
3164
3626
 
3165
- beforeEach(() => {
3627
+ beforeEach(async () => {
3166
3628
  clock = sinon.useFakeTimers();
3167
3629
 
3168
3630
  sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.unknown);
@@ -3171,15 +3633,20 @@ describe('plugin-meetings', () => {
3171
3633
  meeting.config.deviceType = 'web';
3172
3634
  meeting.isMultistream = isMultistream;
3173
3635
  meeting.meetingState = 'ACTIVE';
3174
- meeting.mediaId = 'fake media id';
3175
3636
  meeting.selfUrl = 'selfUrl';
3176
3637
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
3177
- meeting.mediaProperties.getCurrentConnectionInfo = sinon.stub().resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
3638
+ meeting.mediaProperties.getCurrentConnectionInfo = sinon
3639
+ .stub()
3640
+ .resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
3178
3641
  meeting.setMercuryListener = sinon.stub();
3179
3642
  meeting.locusInfo.onFullLocus = sinon.stub();
3180
3643
  meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
3181
3644
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3182
- turnServerInfo: {url: 'turn-url', username: 'turn user', password: 'turn password'},
3645
+ turnServerInfo: {
3646
+ url: 'turns:turn-server-url:443?transport=tcp',
3647
+ username: 'turn user',
3648
+ password: 'turn password',
3649
+ },
3183
3650
  turnDiscoverySkippedReason: 'reachability',
3184
3651
  });
3185
3652
  meeting.deferSDPAnswer = new Defer();
@@ -3192,7 +3659,18 @@ describe('plugin-meetings', () => {
3192
3659
  // setup things that are expected to be the same across all the tests and are actually irrelevant for these tests
3193
3660
  expectedDebugId = `MC-${meeting.id.substring(0, 4)}`;
3194
3661
  expectedMediaConnectionConfig = {
3195
- iceServers: [{urls: 'turn-url', username: 'turn user', credential: 'turn password'}],
3662
+ iceServers: [
3663
+ {
3664
+ urls: 'turn:turn-server-url:5004?transport=tcp',
3665
+ username: 'turn user',
3666
+ credential: 'turn password',
3667
+ },
3668
+ {
3669
+ urls: 'turns:turn-server-url:443?transport=tcp',
3670
+ username: 'turn user',
3671
+ credential: 'turn password',
3672
+ },
3673
+ ],
3196
3674
  skipInactiveTransceivers: false,
3197
3675
  requireH264: true,
3198
3676
  sdpMunging: {
@@ -3261,16 +3739,28 @@ describe('plugin-meetings', () => {
3261
3739
  };
3262
3740
 
3263
3741
  roapMediaConnectionConstructorStub = sinon
3264
- .stub(internalMediaModule, 'RoapMediaConnection')
3742
+ .stub(InternalMediaCoreModule, 'RoapMediaConnection')
3265
3743
  .returns(fakeRoapMediaConnection);
3266
3744
 
3267
3745
  multistreamRoapMediaConnectionConstructorStub = sinon
3268
- .stub(internalMediaModule, 'MultistreamRoapMediaConnection')
3746
+ .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
3269
3747
  .returns(fakeMultistreamRoapMediaConnection);
3270
3748
 
3271
3749
  locusMediaRequestStub = sinon
3272
3750
  .stub(WebexPlugin.prototype, 'request')
3273
3751
  .resolves({body: {locus: {fullState: {}}}});
3752
+
3753
+ // setup some things and mocks so that the call to join() works
3754
+ // (we need to call join() because it creates the LocusMediaRequest instance
3755
+ // that's being tested in these tests)
3756
+ meeting.webex.meetings.registered = true;
3757
+ meeting.webex.internal.device.config = {};
3758
+ sinon.stub(MeetingUtil, 'joinMeeting').resolves({
3759
+ id: 'fake locus from mocked join request',
3760
+ locusUrl: 'fake locus url',
3761
+ mediaId: 'fake media id',
3762
+ });
3763
+ await meeting.join({enableMultistream: isMultistream});
3274
3764
  });
3275
3765
 
3276
3766
  afterEach(() => {
@@ -3299,13 +3789,14 @@ describe('plugin-meetings', () => {
3299
3789
 
3300
3790
  for (let idx = 0; idx < roapMediaConnectionToCheck.on.callCount; idx += 1) {
3301
3791
  if (
3302
- roapMediaConnectionToCheck.on.getCall(idx).args[0] === Event.ROAP_MESSAGE_TO_SEND
3792
+ roapMediaConnectionToCheck.on.getCall(idx).args[0] ===
3793
+ MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND
3303
3794
  ) {
3304
3795
  return roapMediaConnectionToCheck.on.getCall(idx).args[1];
3305
3796
  }
3306
3797
  }
3307
3798
  assert.fail(
3308
- 'listener for "roap:messageToSend" (Event.ROAP_MESSAGE_TO_SEND) was not registered'
3799
+ 'listener for "roap:messageToSend" (MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND) was not registered'
3309
3800
  );
3310
3801
  };
3311
3802
 
@@ -3746,13 +4237,13 @@ describe('plugin-meetings', () => {
3746
4237
  await meeting.addMedia({
3747
4238
  localStreams: {microphone: fakeMicrophoneStream},
3748
4239
  audioEnabled: false,
3749
- videoEnabled: false
4240
+ videoEnabled: false,
3750
4241
  });
3751
4242
  await simulateRoapOffer();
3752
4243
  await simulateRoapOk();
3753
4244
 
3754
4245
  assert.notCalled(handleDeviceLoggingSpy);
3755
- })
4246
+ });
3756
4247
 
3757
4248
  it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
3758
4249
  await meeting.addMedia({audioEnabled: false});
@@ -5107,7 +5598,7 @@ describe('plugin-meetings', () => {
5107
5598
 
5108
5599
  describe('#fetchMeetingInfo', () => {
5109
5600
  const FAKE_DESTINATION = 'something@somecompany.com';
5110
- const FAKE_TYPE = _SIP_URI_;
5601
+ const FAKE_TYPE = DESTINATION_TYPE.SIP_URI;
5111
5602
  const FAKE_TIMEOUT_FETCHMEETINGINFO_ID = '123456';
5112
5603
  const FAKE_PASSWORD = '123abc';
5113
5604
  const FAKE_CAPTCHA_CODE = 'a1b2c3XYZ';
@@ -5542,7 +6033,7 @@ describe('plugin-meetings', () => {
5542
6033
  const FAKE_PASSWORD = '123456';
5543
6034
  const FAKE_CAPTCHA_CODE = '654321';
5544
6035
  const FAKE_DESTINATION = 'something@somecompany.com';
5545
- const FAKE_TYPE = _SIP_URI_;
6036
+ const FAKE_TYPE = DESTINATION_TYPE.SIP_URI;
5546
6037
  const FAKE_INSTALLED_ORG_ID = '123456';
5547
6038
  const FAKE_MEETING_INFO_LOOKUP_URL = 'meetingLookupUrl';
5548
6039
 
@@ -6187,14 +6678,14 @@ describe('plugin-meetings', () => {
6187
6678
  beforeEach(() => {
6188
6679
  sandbox = sinon.createSandbox();
6189
6680
  meeting.statsAnalyzer = {
6190
- stopAnalyzer: sinon.stub().returns(Promise.resolve())
6681
+ stopAnalyzer: sinon.stub().returns(Promise.resolve()),
6191
6682
  };
6192
6683
 
6193
6684
  meeting.reconnectionManager = {
6194
- cleanUp: sinon.stub()
6685
+ cleanUp: sinon.stub(),
6195
6686
  };
6196
6687
 
6197
- meeting.cleanupLocalStreams=sinon.stub();
6688
+ meeting.cleanupLocalStreams = sinon.stub();
6198
6689
  meeting.closeRemoteStreams = sinon.stub().returns(Promise.resolve());
6199
6690
  meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
6200
6691
  meeting.unsetRemoteStreams = sinon.stub();
@@ -6275,7 +6766,6 @@ describe('plugin-meetings', () => {
6275
6766
  },
6276
6767
  'SELF_OBSERVING'
6277
6768
  );
6278
-
6279
6769
 
6280
6770
  // Verify that the event handler behaves as expected
6281
6771
  expect(meeting.statsAnalyzer.stopAnalyzer.calledOnce).to.be.true;
@@ -6288,11 +6778,13 @@ describe('plugin-meetings', () => {
6288
6778
  expect(meeting.unsetPeerConnections.calledOnce).to.be.true;
6289
6779
  expect(meeting.reconnectionManager.cleanUp.calledOnce).to.be.true;
6290
6780
  expect(meeting.mediaProperties.setMediaDirection.calledOnce).to.be.true;
6291
- expect(meeting.addMedia.calledOnceWithExactly({
6292
- audioEnabled: false,
6293
- videoEnabled: false,
6294
- shareVideoEnabled: true
6295
- })).to.be.true;
6781
+ expect(
6782
+ meeting.addMedia.calledOnceWithExactly({
6783
+ audioEnabled: false,
6784
+ videoEnabled: false,
6785
+ shareVideoEnabled: true,
6786
+ })
6787
+ ).to.be.true;
6296
6788
  await testUtils.flushPromises();
6297
6789
  assert.equal(meeting.isMoveToInProgress, false);
6298
6790
  });
@@ -7079,6 +7571,12 @@ describe('plugin-meetings', () => {
7079
7571
  id: 'stream',
7080
7572
  getTracks: () => [{id: 'track', addEventListener: sinon.stub()}],
7081
7573
  };
7574
+ const simulateConnectionStateChange = (newState) => {
7575
+ meeting.mediaProperties.webrtcMediaConnection.getConnectionState = sinon
7576
+ .stub()
7577
+ .returns(newState);
7578
+ eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]();
7579
+ };
7082
7580
 
7083
7581
  beforeEach(() => {
7084
7582
  eventListeners = {};
@@ -7088,23 +7586,29 @@ describe('plugin-meetings', () => {
7088
7586
  on: sinon.stub().callsFake((event, listener) => {
7089
7587
  eventListeners[event] = listener;
7090
7588
  }),
7589
+ getConnectionState: sinon.stub().returns(ConnectionState.New),
7091
7590
  };
7092
7591
  MediaUtil.createMediaStream.returns(fakeStream);
7093
7592
  });
7094
7593
 
7095
7594
  it('should register for all the correct RoapMediaConnection events', () => {
7096
7595
  meeting.setupMediaConnectionListeners();
7097
- assert.isFunction(eventListeners[Event.ROAP_STARTED]);
7098
- assert.isFunction(eventListeners[Event.ROAP_DONE]);
7099
- assert.isFunction(eventListeners[Event.ROAP_FAILURE]);
7100
- assert.isFunction(eventListeners[Event.ROAP_MESSAGE_TO_SEND]);
7101
- assert.isFunction(eventListeners[Event.REMOTE_TRACK_ADDED]);
7102
- assert.isFunction(eventListeners[Event.CONNECTION_STATE_CHANGED]);
7596
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_STARTED]);
7597
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_DONE]);
7598
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_FAILURE]);
7599
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]);
7600
+ assert.isFunction(eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]);
7601
+ assert.isFunction(
7602
+ eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]
7603
+ );
7604
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED]);
7605
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]);
7606
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]);
7103
7607
  });
7104
7608
 
7105
7609
  it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => {
7106
7610
  meeting.setupMediaConnectionListeners();
7107
- eventListeners[Event.REMOTE_TRACK_ADDED]({
7611
+ eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
7108
7612
  track: 'track',
7109
7613
  type: RemoteTrackType.AUDIO,
7110
7614
  });
@@ -7114,7 +7618,7 @@ describe('plugin-meetings', () => {
7114
7618
  stream: fakeStream,
7115
7619
  });
7116
7620
 
7117
- eventListeners[Event.REMOTE_TRACK_ADDED]({
7621
+ eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
7118
7622
  track: 'track',
7119
7623
  type: RemoteTrackType.VIDEO,
7120
7624
  });
@@ -7124,7 +7628,7 @@ describe('plugin-meetings', () => {
7124
7628
  stream: fakeStream,
7125
7629
  });
7126
7630
 
7127
- eventListeners[Event.REMOTE_TRACK_ADDED]({
7631
+ eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
7128
7632
  track: 'track',
7129
7633
  type: RemoteTrackType.SCREENSHARE_VIDEO,
7130
7634
  });
@@ -7135,13 +7639,78 @@ describe('plugin-meetings', () => {
7135
7639
  });
7136
7640
  });
7137
7641
 
7642
+ describe('should react on a ICE_CANDIDATE event', () => {
7643
+ beforeEach(() => {
7644
+ meeting.setupMediaConnectionListeners();
7645
+ });
7646
+
7647
+ it('should collect ice candidates', () => {
7648
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: 'candidate'});
7649
+
7650
+ assert.equal(meeting.iceCandidatesCount, 1);
7651
+ });
7652
+
7653
+ it('should not collect null ice candidates', () => {
7654
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: null});
7655
+
7656
+ assert.equal(meeting.iceCandidatesCount, 0);
7657
+ });
7658
+ });
7659
+
7660
+ describe('should react on a ICE_CANDIDATE_ERROR event', () => {
7661
+ beforeEach(() => {
7662
+ meeting.setupMediaConnectionListeners();
7663
+ });
7664
+
7665
+ it('should not collect skipped ice candidates error', () => {
7666
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
7667
+ error: {
7668
+ errorCode: 600,
7669
+ errorText: 'Address not associated with the desired network interface.',
7670
+ },
7671
+ });
7672
+
7673
+ assert.equal(meeting.iceCandidateErrors.size, 0);
7674
+ });
7675
+
7676
+ it('should collect valid ice candidates error', () => {
7677
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
7678
+ error: {errorCode: 701, errorText: ''},
7679
+ });
7680
+
7681
+ assert.equal(meeting.iceCandidateErrors.size, 1);
7682
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7683
+ });
7684
+
7685
+ it('should increment counter if same valid ice candidates error collected', () => {
7686
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
7687
+ error: {errorCode: 701, errorText: ''},
7688
+ });
7689
+
7690
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
7691
+ error: {errorCode: 701, errorText: 'STUN host lookup received error.'},
7692
+ });
7693
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
7694
+ error: {errorCode: 701, errorText: 'STUN host lookup received error.'},
7695
+ });
7696
+
7697
+ assert.equal(meeting.iceCandidateErrors.size, 2);
7698
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7699
+ assert.equal(meeting.iceCandidateErrors.get('701_'), 1);
7700
+ assert.equal(
7701
+ meeting.iceCandidateErrors.has('701_stun_host_lookup_received_error'),
7702
+ true
7703
+ );
7704
+ assert.equal(meeting.iceCandidateErrors.get('701_stun_host_lookup_received_error'), 2);
7705
+ });
7706
+ });
7707
+
7138
7708
  describe('CONNECTION_STATE_CHANGED event when state = "Connecting"', () => {
7139
7709
  it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = true', () => {
7140
7710
  meeting.hasMediaConnectionConnectedAtLeastOnce = true;
7141
7711
  meeting.setupMediaConnectionListeners();
7142
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7143
- state: 'Connecting',
7144
- });
7712
+
7713
+ simulateConnectionStateChange(ConnectionState.Connecting);
7145
7714
 
7146
7715
  assert.notCalled(webex.internal.newMetrics.submitClientEvent);
7147
7716
  });
@@ -7149,9 +7718,8 @@ describe('plugin-meetings', () => {
7149
7718
  it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = false', () => {
7150
7719
  meeting.hasMediaConnectionConnectedAtLeastOnce = false;
7151
7720
  meeting.setupMediaConnectionListeners();
7152
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7153
- state: 'Connecting',
7154
- });
7721
+
7722
+ simulateConnectionStateChange(ConnectionState.Connecting);
7155
7723
 
7156
7724
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7157
7725
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7177,6 +7745,7 @@ describe('plugin-meetings', () => {
7177
7745
  on: sinon.stub().callsFake((event, listener) => {
7178
7746
  eventListeners[event] = listener;
7179
7747
  }),
7748
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
7180
7749
  };
7181
7750
  };
7182
7751
 
@@ -7230,9 +7799,7 @@ describe('plugin-meetings', () => {
7230
7799
  assert.equal(meeting.hasMediaConnectionConnectedAtLeastOnce, false);
7231
7800
 
7232
7801
  // simulate first connection success
7233
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7234
- state: 'Connected',
7235
- });
7802
+ simulateConnectionStateChange(ConnectionState.Connected);
7236
7803
  checkExpectedSpies({
7237
7804
  icePhase: 'JOIN_MEETING_FINAL',
7238
7805
  setNetworkStatusCallParams: [NETWORK_STATUS.CONNECTED],
@@ -7242,12 +7809,9 @@ describe('plugin-meetings', () => {
7242
7809
  // now simulate short connection loss, client.ice.end is not sent a second time as hasMediaConnectionConnectedAtLeastOnce = true
7243
7810
  resetSpies();
7244
7811
 
7245
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7246
- state: 'Disconnected',
7247
- });
7248
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7249
- state: 'Connected',
7250
- });
7812
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7813
+
7814
+ simulateConnectionStateChange(ConnectionState.Connected);
7251
7815
 
7252
7816
  checkExpectedSpies({
7253
7817
  setNetworkStatusCallParams: [NETWORK_STATUS.DISCONNECTED, NETWORK_STATUS.CONNECTED],
@@ -7255,12 +7819,9 @@ describe('plugin-meetings', () => {
7255
7819
 
7256
7820
  resetSpies();
7257
7821
 
7258
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7259
- state: 'Disconnected',
7260
- });
7261
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7262
- state: 'Connected',
7263
- });
7822
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7823
+
7824
+ simulateConnectionStateChange(ConnectionState.Connected);
7264
7825
  });
7265
7826
  });
7266
7827
 
@@ -7282,9 +7843,8 @@ describe('plugin-meetings', () => {
7282
7843
 
7283
7844
  const mockDisconnectedEvent = () => {
7284
7845
  meeting.setupMediaConnectionListeners();
7285
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7286
- state: 'Disconnected',
7287
- });
7846
+
7847
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7288
7848
  };
7289
7849
 
7290
7850
  const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
@@ -7348,9 +7908,8 @@ describe('plugin-meetings', () => {
7348
7908
  describe('CONNECTION_STATE_CHANGED event when state = "Failed"', () => {
7349
7909
  const mockFailedEvent = () => {
7350
7910
  meeting.setupMediaConnectionListeners();
7351
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7352
- state: 'Failed',
7353
- });
7911
+
7912
+ simulateConnectionStateChange(ConnectionState.Failed);
7354
7913
  };
7355
7914
 
7356
7915
  const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
@@ -7432,7 +7991,7 @@ describe('plugin-meetings', () => {
7432
7991
  cause: {name: fakeRootCauseName},
7433
7992
  });
7434
7993
 
7435
- eventListeners[Event.ROAP_FAILURE](fakeError);
7994
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7436
7995
 
7437
7996
  checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
7438
7997
  checkBehavioralMetricSent(
@@ -7449,7 +8008,7 @@ describe('plugin-meetings', () => {
7449
8008
  cause: {name: fakeRootCauseName},
7450
8009
  });
7451
8010
 
7452
- eventListeners[Event.ROAP_FAILURE](fakeError);
8011
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7453
8012
 
7454
8013
  checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
7455
8014
  checkBehavioralMetricSent(
@@ -7466,7 +8025,7 @@ describe('plugin-meetings', () => {
7466
8025
  cause: {name: fakeRootCauseName},
7467
8026
  });
7468
8027
 
7469
- eventListeners[Event.ROAP_FAILURE](fakeError);
8028
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7470
8029
 
7471
8030
  checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
7472
8031
  checkBehavioralMetricSent(
@@ -7481,7 +8040,7 @@ describe('plugin-meetings', () => {
7481
8040
  // SdpError is usually without a cause
7482
8041
  const fakeError = new Errors.SdpError(fakeErrorMessage, {name: fakeErrorName});
7483
8042
 
7484
- eventListeners[Event.ROAP_FAILURE](fakeError);
8043
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7485
8044
 
7486
8045
  checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
7487
8046
  // expectedMetadataType is the error name in this case
@@ -7499,7 +8058,7 @@ describe('plugin-meetings', () => {
7499
8058
  name: fakeErrorName,
7500
8059
  });
7501
8060
 
7502
- eventListeners[Event.ROAP_FAILURE](fakeError);
8061
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7503
8062
 
7504
8063
  checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
7505
8064
  // expectedMetadataType is the error name in this case
@@ -7525,7 +8084,7 @@ describe('plugin-meetings', () => {
7525
8084
  };
7526
8085
  meeting.sdpResponseTimer = '1234';
7527
8086
 
7528
- eventListeners[Event.REMOTE_SDP_ANSWER_PROCESSED]();
8087
+ eventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED]();
7529
8088
 
7530
8089
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7531
8090
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7553,7 +8112,7 @@ describe('plugin-meetings', () => {
7553
8112
  it('handles LOCAL_SDP_OFFER_GENERATED correctly', () => {
7554
8113
  assert.equal(meeting.deferSDPAnswer, undefined);
7555
8114
 
7556
- eventListeners[Event.LOCAL_SDP_OFFER_GENERATED]();
8115
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
7557
8116
 
7558
8117
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7559
8118
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7565,7 +8124,7 @@ describe('plugin-meetings', () => {
7565
8124
  });
7566
8125
 
7567
8126
  it('handles LOCAL_SDP_ANSWER_GENERATED correctly', () => {
7568
- eventListeners[Event.LOCAL_SDP_ANSWER_GENERATED]();
8127
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_ANSWER_GENERATED]();
7569
8128
 
7570
8129
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7571
8130
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7575,7 +8134,7 @@ describe('plugin-meetings', () => {
7575
8134
  });
7576
8135
  });
7577
8136
 
7578
- describe('handles Event.ROAP_MESSAGE_TO_SEND correctly', () => {
8137
+ describe('handles MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND correctly', () => {
7579
8138
  let sendRoapOKStub;
7580
8139
  let sendRoapMediaRequestStub;
7581
8140
  let sendRoapAnswerStub;
@@ -7593,7 +8152,7 @@ describe('plugin-meetings', () => {
7593
8152
  });
7594
8153
 
7595
8154
  it('handles OK message correctly', () => {
7596
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8155
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7597
8156
  roapMessage: {messageType: 'OK', seq: 1},
7598
8157
  });
7599
8158
 
@@ -7608,7 +8167,7 @@ describe('plugin-meetings', () => {
7608
8167
  it('handles OFFER message correctly (no answer in the http response)', async () => {
7609
8168
  sinon.stub(meeting, 'roapMessageReceived');
7610
8169
 
7611
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8170
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7612
8171
  roapMessage: {
7613
8172
  messageType: 'OFFER',
7614
8173
  seq: 1,
@@ -7634,7 +8193,7 @@ describe('plugin-meetings', () => {
7634
8193
  sendRoapMediaRequestStub.resolves({roapAnswer: fakeAnswer});
7635
8194
  sinon.stub(meeting, 'roapMessageReceived');
7636
8195
 
7637
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8196
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7638
8197
  roapMessage: {
7639
8198
  messageType: 'OFFER',
7640
8199
  seq: 1,
@@ -7656,14 +8215,20 @@ describe('plugin-meetings', () => {
7656
8215
  });
7657
8216
 
7658
8217
  it('handles OFFER message correctly when request fails', async () => {
8218
+ const fakeError = new Error('fake error');
7659
8219
  const clock = sinon.useFakeTimers();
7660
8220
  sinon.spy(clock, 'clearTimeout');
7661
8221
  meeting.deferSDPAnswer = {reject: sinon.stub()};
7662
8222
  meeting.sdpResponseTimer = '1234';
7663
- sendRoapMediaRequestStub.rejects();
8223
+ sendRoapMediaRequestStub.rejects(fakeError);
7664
8224
  sinon.stub(meeting, 'roapMessageReceived');
8225
+ const getErrorPayloadForClientErrorCodeStub =
8226
+ (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
8227
+ sinon
8228
+ .stub()
8229
+ .callsFake(({clientErrorCode}) => ({errorCode: clientErrorCode, fatal: true})));
7665
8230
 
7666
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8231
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7667
8232
  roapMessage: {
7668
8233
  messageType: 'OFFER',
7669
8234
  seq: 1,
@@ -7686,10 +8251,25 @@ describe('plugin-meetings', () => {
7686
8251
  assert.calledOnce(clock.clearTimeout);
7687
8252
  assert.calledWith(clock.clearTimeout, '1234');
7688
8253
  assert.equal(meeting.sdpResponseTimer, undefined);
8254
+
8255
+ assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
8256
+ clientErrorCode: 2007,
8257
+ });
8258
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8259
+ name: 'client.media-engine.remote-sdp-received',
8260
+ payload: {
8261
+ canProceed: false,
8262
+ errors: [{errorCode: 2007, fatal: true}],
8263
+ },
8264
+ options: {
8265
+ meetingId: meeting.id,
8266
+ rawError: fakeError,
8267
+ },
8268
+ });
7689
8269
  });
7690
8270
 
7691
8271
  it('handles ANSWER message correctly', () => {
7692
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8272
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7693
8273
  roapMessage: {
7694
8274
  messageType: 'ANSWER',
7695
8275
  seq: 10,
@@ -7710,7 +8290,7 @@ describe('plugin-meetings', () => {
7710
8290
  it('sends metrics if fails to send roap ANSWER message', async () => {
7711
8291
  sendRoapAnswerStub.rejects(new Error('sending answer failed'));
7712
8292
 
7713
- await eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8293
+ await eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7714
8294
  roapMessage: {
7715
8295
  messageType: 'ANSWER',
7716
8296
  seq: 10,
@@ -7734,7 +8314,7 @@ describe('plugin-meetings', () => {
7734
8314
 
7735
8315
  [ErrorType.CONFLICT, ErrorType.DOUBLECONFLICT].forEach((errorType) =>
7736
8316
  it(`handles ERROR message indicating glare condition correctly (errorType=${errorType})`, () => {
7737
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8317
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7738
8318
  roapMessage: {
7739
8319
  messageType: 'ERROR',
7740
8320
  seq: 10,
@@ -7765,7 +8345,7 @@ describe('plugin-meetings', () => {
7765
8345
  );
7766
8346
 
7767
8347
  it('handles ERROR message indicating other errors correctly', () => {
7768
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8348
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7769
8349
  roapMessage: {
7770
8350
  messageType: 'ERROR',
7771
8351
  seq: 10,
@@ -7793,8 +8373,12 @@ describe('plugin-meetings', () => {
7793
8373
  });
7794
8374
 
7795
8375
  it('registers for audio and video source count changed', () => {
7796
- assert.isFunction(eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED]);
7797
- assert.isFunction(eventListeners[Event.AUDIO_SOURCES_COUNT_CHANGED]);
8376
+ assert.isFunction(
8377
+ eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED]
8378
+ );
8379
+ assert.isFunction(
8380
+ eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED]
8381
+ );
7798
8382
  });
7799
8383
 
7800
8384
  it('forwards the VIDEO_SOURCES_COUNT_CHANGED event as "media:remoteVideoSourceCountChanged"', () => {
@@ -7804,7 +8388,7 @@ describe('plugin-meetings', () => {
7804
8388
 
7805
8389
  sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources');
7806
8390
 
7807
- eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](
8391
+ eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
7808
8392
  numTotalSources,
7809
8393
  numLiveSources,
7810
8394
  mediaContent
@@ -7828,7 +8412,7 @@ describe('plugin-meetings', () => {
7828
8412
  const numLiveSources = 2;
7829
8413
  const mediaContent = 'MAIN';
7830
8414
 
7831
- eventListeners[Event.AUDIO_SOURCES_COUNT_CHANGED](
8415
+ eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED](
7832
8416
  numTotalSources,
7833
8417
  numLiveSources,
7834
8418
  mediaContent
@@ -7856,7 +8440,7 @@ describe('plugin-meetings', () => {
7856
8440
  'setNumCurrentSources'
7857
8441
  );
7858
8442
 
7859
- eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](
8443
+ eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
7860
8444
  numTotalSources,
7861
8445
  numLiveSources,
7862
8446
  'MAIN'
@@ -7874,7 +8458,7 @@ describe('plugin-meetings', () => {
7874
8458
  'setNumCurrentSources'
7875
8459
  );
7876
8460
 
7877
- eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](
8461
+ eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
7878
8462
  numTotalSources,
7879
8463
  numLiveSources,
7880
8464
  'SLIDES'
@@ -7904,6 +8488,9 @@ describe('plugin-meetings', () => {
7904
8488
  it('listens to the self admitted guest event', (done) => {
7905
8489
  meeting.stopKeepAlive = sinon.stub();
7906
8490
  meeting.updateLLMConnection = sinon.stub();
8491
+ meeting.rtcMetrics = {
8492
+ sendNextMetrics: sinon.stub(),
8493
+ };
7907
8494
  meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
7908
8495
  assert.calledOnceWithExactly(meeting.stopKeepAlive);
7909
8496
  assert.calledThrice(TriggerProxy.trigger);
@@ -7915,6 +8502,8 @@ describe('plugin-meetings', () => {
7915
8502
  {payload: test1}
7916
8503
  );
7917
8504
  assert.calledOnce(meeting.updateLLMConnection);
8505
+ assert.calledOnceWithExactly(meeting.rtcMetrics.sendNextMetrics);
8506
+
7918
8507
  done();
7919
8508
  });
7920
8509
 
@@ -9781,6 +10370,7 @@ describe('plugin-meetings', () => {
9781
10370
  beforeEach(() => {
9782
10371
  webex.internal.llm.isConnected = sinon.stub().returns(false);
9783
10372
  webex.internal.llm.getLocusUrl = sinon.stub();
10373
+ webex.internal.llm.getDatachannelUrl = sinon.stub();
9784
10374
  webex.internal.llm.registerAndConnect = sinon
9785
10375
  .stub()
9786
10376
  .returns(Promise.resolve('something'));
@@ -9808,6 +10398,7 @@ describe('plugin-meetings', () => {
9808
10398
  meeting.joinedWith = {state: 'JOINED'};
9809
10399
  webex.internal.llm.isConnected.returns(true);
9810
10400
  webex.internal.llm.getLocusUrl.returns('a url');
10401
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9811
10402
 
9812
10403
  meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
9813
10404
 
@@ -9844,6 +10435,7 @@ describe('plugin-meetings', () => {
9844
10435
  meeting.joinedWith = {state: 'JOINED'};
9845
10436
  webex.internal.llm.isConnected.returns(true);
9846
10437
  webex.internal.llm.getLocusUrl.returns('a url');
10438
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9847
10439
 
9848
10440
  meeting.locusInfo = {url: 'a different url', info: {datachannelUrl: 'a datachannel url'}};
9849
10441
 
@@ -9869,6 +10461,36 @@ describe('plugin-meetings', () => {
9869
10461
  );
9870
10462
  });
9871
10463
 
10464
+ it('disconnects if first if the data channel url has changed', async () => {
10465
+ meeting.joinedWith = {state: 'JOINED'};
10466
+ webex.internal.llm.isConnected.returns(true);
10467
+ webex.internal.llm.getLocusUrl.returns('a url');
10468
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
10469
+
10470
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a different datachannel url'}};
10471
+
10472
+ const result = await meeting.updateLLMConnection();
10473
+
10474
+ assert.calledWith(webex.internal.llm.disconnectLLM);
10475
+ assert.calledWith(
10476
+ webex.internal.llm.registerAndConnect,
10477
+ 'a url',
10478
+ 'a different datachannel url'
10479
+ );
10480
+ assert.equal(result, 'something');
10481
+ assert.calledWithExactly(
10482
+ meeting.webex.internal.llm.off,
10483
+ 'event:relay.event',
10484
+ meeting.processRelayEvent
10485
+ );
10486
+ assert.calledTwice(meeting.webex.internal.llm.off);
10487
+ assert.calledOnceWithExactly(
10488
+ meeting.webex.internal.llm.on,
10489
+ 'event:relay.event',
10490
+ meeting.processRelayEvent
10491
+ );
10492
+ });
10493
+
9872
10494
  it('disconnects when the state is not JOINED', async () => {
9873
10495
  meeting.joinedWith = {state: 'any other state'};
9874
10496
  webex.internal.llm.isConnected.returns(true);