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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) 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 +12 -0
  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 +554 -358
  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 +37 -33
  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 +415 -56
  49. package/dist/reachability/index.js.map +1 -1
  50. package/dist/types/constants.d.ts +11 -3
  51. package/dist/types/media/MediaConnectionAwaiter.d.ts +24 -4
  52. package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
  53. package/dist/types/meeting/index.d.ts +27 -8
  54. package/dist/types/meeting/locusMediaRequest.d.ts +2 -0
  55. package/dist/types/meeting-info/index.d.ts +3 -2
  56. package/dist/types/meeting-info/meeting-info-v2.d.ts +3 -2
  57. package/dist/types/meeting-info/util.d.ts +5 -4
  58. package/dist/types/meeting-info/utilv2.d.ts +3 -2
  59. package/dist/types/meetings/collection.d.ts +3 -2
  60. package/dist/types/meetings/index.d.ts +4 -3
  61. package/dist/types/meetings/meetings.types.d.ts +9 -0
  62. package/dist/types/metrics/constants.d.ts +1 -0
  63. package/dist/types/metrics/index.d.ts +15 -0
  64. package/dist/types/reachability/clusterReachability.d.ts +31 -3
  65. package/dist/types/reachability/index.d.ts +93 -2
  66. package/dist/webinar/index.js +1 -1
  67. package/package.json +23 -23
  68. package/src/breakouts/index.ts +7 -1
  69. package/src/constants.ts +13 -17
  70. package/src/locus-info/selfUtils.ts +0 -5
  71. package/src/media/MediaConnectionAwaiter.ts +89 -14
  72. package/src/media/index.ts +13 -0
  73. package/src/meeting/connectionStateHandler.ts +65 -0
  74. package/src/meeting/index.ts +532 -295
  75. package/src/meeting/locusMediaRequest.ts +5 -0
  76. package/src/meeting/muteState.ts +6 -1
  77. package/src/meeting/util.ts +1 -0
  78. package/src/meeting-info/index.ts +9 -6
  79. package/src/meeting-info/meeting-info-v2.ts +4 -4
  80. package/src/meeting-info/util.ts +23 -28
  81. package/src/meeting-info/utilv2.ts +18 -24
  82. package/src/meetings/collection.ts +3 -3
  83. package/src/meetings/index.ts +39 -40
  84. package/src/meetings/meetings.types.ts +11 -0
  85. package/src/meetings/util.ts +5 -4
  86. package/src/metrics/constants.ts +1 -0
  87. package/src/metrics/index.ts +44 -0
  88. package/src/personal-meeting-room/index.ts +2 -2
  89. package/src/reachability/clusterReachability.ts +86 -25
  90. package/src/reachability/index.ts +316 -27
  91. package/test/unit/spec/breakouts/index.ts +51 -32
  92. package/test/unit/spec/locus-info/selfUtils.js +25 -23
  93. package/test/unit/spec/media/MediaConnectionAwaiter.ts +131 -32
  94. package/test/unit/spec/media/index.ts +42 -27
  95. package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
  96. package/test/unit/spec/meeting/index.js +762 -179
  97. package/test/unit/spec/meeting/locusMediaRequest.ts +7 -0
  98. package/test/unit/spec/meeting/muteState.js +24 -0
  99. package/test/unit/spec/meeting-info/index.js +4 -4
  100. package/test/unit/spec/meeting-info/meetinginfov2.js +24 -28
  101. package/test/unit/spec/meeting-info/request.js +2 -2
  102. package/test/unit/spec/meeting-info/utilv2.js +41 -49
  103. package/test/unit/spec/meetings/index.js +14 -0
  104. package/test/unit/spec/metrics/index.js +126 -0
  105. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -2
  106. package/test/unit/spec/personal-meeting-room/personal-meeting-room.js +2 -2
  107. package/test/unit/spec/reachability/clusterReachability.ts +116 -22
  108. package/test/unit/spec/reachability/index.ts +1153 -84
  109. package/test/unit/spec/rtcMetrics/index.ts +1 -0
  110. package/dist/mediaQualityMetrics/config.js +0 -321
  111. package/dist/mediaQualityMetrics/config.js.map +0 -1
  112. package/dist/networkQualityMonitor/index.js +0 -227
  113. package/dist/networkQualityMonitor/index.js.map +0 -1
  114. package/dist/statsAnalyzer/global.js +0 -44
  115. package/dist/statsAnalyzer/global.js.map +0 -1
  116. package/dist/statsAnalyzer/index.js +0 -1072
  117. package/dist/statsAnalyzer/index.js.map +0 -1
  118. package/dist/statsAnalyzer/mqaUtil.js +0 -368
  119. package/dist/statsAnalyzer/mqaUtil.js.map +0 -1
  120. package/dist/types/mediaQualityMetrics/config.d.ts +0 -247
  121. package/dist/types/networkQualityMonitor/index.d.ts +0 -70
  122. package/dist/types/statsAnalyzer/global.d.ts +0 -36
  123. package/dist/types/statsAnalyzer/index.d.ts +0 -217
  124. package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -48
  125. package/src/mediaQualityMetrics/config.ts +0 -255
  126. package/src/networkQualityMonitor/index.ts +0 -211
  127. package/src/statsAnalyzer/global.ts +0 -37
  128. package/src/statsAnalyzer/index.ts +0 -1318
  129. package/src/statsAnalyzer/mqaUtil.ts +0 -463
  130. package/test/unit/spec/networkQualityMonitor/index.js +0 -99
  131. 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 internalMediaModule from '@webex/internal-media-core';
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, Token, WebexPlugin} from '@webex/webex-core';
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
- _SIP_URI_,
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
- RECONNECTION,
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
- Event,
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: _MEETING_ID_,
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, _MEETING_ID_);
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: _MEETING_ID_,
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: _MEETING_ID_,
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: _MEETING_ID_,
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: _MEETING_ID_,
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
- meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
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 addMedia called
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(meeting.addMedia, mediaOptions, fakeTurnServerInfo);
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, {isRetry: false, prevJoinResponse: undefined});
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 addMedia called
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(meeting.addMedia, mediaOptions, undefined);
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 addMedia called
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(meeting.addMedia, mediaOptions, undefined);
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, {isRetry: false, prevJoinResponse: undefined});
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.stub().onFirstCall().returns(Promise.reject(error)).onSecondCall().returns(Promise.resolve(fakeJoinResult));
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, {isRetry: false, prevJoinResponse: undefined});
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.addMedia = sinon.stub().returns(Promise.resolve(test4));
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 addMedia fails and ignore leave() failure', async () => {
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.addMedia = sinon.stub().rejects(addMediaError);
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 addMedia fails the first time and succeeds the second time and should only call join() once', async () => {
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 leaveError = new Error('leave error');
869
- const leaveStub = sinon.stub(meeting, 'leave').rejects(leaveError);
901
+ const leaveStub = sinon.stub(meeting, 'leave');
870
902
 
871
- meeting.addMedia = sinon
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 not turnOnCaptions if the user is not a host', async () => {
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.notCalled(webex.internal.voicea.turnOnCaptions);
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
- webex.internal.llm.on,
1381
- 'online',
1382
- meeting.handleLLMOnline
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.stub().resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
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
- meeting.setMercuryListener = sinon.stub().returns(true);
1678
- meeting.setupMediaConnectionListeners = sinon.stub();
1679
- meeting.setMercuryListener = sinon.stub();
1680
- meeting.roap.doTurnDiscovery = sinon
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
- meeting.waitForRemoteSDPAnswer = sinon.stub().resolves();
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(new Error('fake error'));
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
- iceConnectionState: 'unknown',
2574
+ iceConnected: false,
2244
2575
  turnServerUsed: false,
2576
+ unreachable: false,
2245
2577
  });
2246
2578
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2247
2579
  signalingState: 'unknown',
2248
- iceConnectionState: 'unknown',
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, [meeting, true, true]);
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
- iceConnectionState: 'unknown',
2788
+ iceConnected: undefined,
2444
2789
  turnServerUsed: false,
2790
+ unreachable: false,
2445
2791
  });
2446
2792
 
2447
2793
  assert.calledOnce(getErrorPayloadForClientErrorCodeStub);
@@ -2547,6 +2893,7 @@ describe('plugin-meetings', () => {
2547
2893
  isMultistream: false,
2548
2894
  retriedWithTurnServer: true,
2549
2895
  isJoinWithMediaRetry: false,
2896
+ iceCandidatesCount: 0,
2550
2897
  },
2551
2898
  ]);
2552
2899
  meeting.roap.doTurnDiscovery;
@@ -2675,6 +3022,10 @@ describe('plugin-meetings', () => {
2675
3022
  someReachabilityMetric2: 'some value2',
2676
3023
  }),
2677
3024
  };
3025
+ meeting.iceCandidatesCount = 3;
3026
+ meeting.iceCandidateErrors.set('701_error', 3);
3027
+ meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
3028
+
2678
3029
  await meeting.addMedia({
2679
3030
  mediaSettings: {},
2680
3031
  });
@@ -2694,6 +3045,9 @@ describe('plugin-meetings', () => {
2694
3045
  isJoinWithMediaRetry: false,
2695
3046
  someReachabilityMetric1: 'some value1',
2696
3047
  someReachabilityMetric2: 'some value2',
3048
+ iceCandidatesCount: 3,
3049
+ '701_error': 3,
3050
+ '701_turn_host_lookup_received_error': 1,
2697
3051
  }
2698
3052
  );
2699
3053
 
@@ -2715,7 +3069,63 @@ describe('plugin-meetings', () => {
2715
3069
  turnDiscoverySkippedReason: undefined,
2716
3070
  });
2717
3071
  meeting.meetingState = 'ACTIVE';
2718
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects(new Error('fake error'));
3072
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
3073
+
3074
+ const forceRtcMetricsSend = sinon.stub().resolves();
3075
+ const closeMediaConnectionStub = sinon.stub();
3076
+ Media.createMediaConnection = sinon.stub().returns({
3077
+ close: closeMediaConnectionStub,
3078
+ forceRtcMetricsSend,
3079
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3080
+ initiateOffer: sinon.stub().resolves({}),
3081
+ on: sinon.stub(),
3082
+ });
3083
+
3084
+ await meeting
3085
+ .addMedia({
3086
+ mediaSettings: {},
3087
+ })
3088
+ .catch((err) => {
3089
+ errorThrown = err;
3090
+ assert.instanceOf(err, AddMediaFailed);
3091
+ });
3092
+
3093
+ // Check that the only metric sent is ADD_MEDIA_FAILURE
3094
+ assert.calledOnceWithExactly(
3095
+ Metrics.sendBehavioralMetric,
3096
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
3097
+ {
3098
+ correlation_id: meeting.correlationId,
3099
+ locus_id: meeting.locusUrl.split('/').pop(),
3100
+ reason: errorThrown.message,
3101
+ stack: errorThrown.stack,
3102
+ code: errorThrown.code,
3103
+ turnDiscoverySkippedReason: undefined,
3104
+ turnServerUsed: true,
3105
+ retriedWithTurnServer: false,
3106
+ isMultistream: false,
3107
+ isJoinWithMediaRetry: false,
3108
+ signalingState: 'unknown',
3109
+ connectionState: 'unknown',
3110
+ iceConnectionState: 'unknown',
3111
+ selectedCandidatePairChanges: 2,
3112
+ numTransports: 1,
3113
+ iceCandidatesCount: 0,
3114
+ }
3115
+ );
3116
+
3117
+ assert.isOk(errorThrown);
3118
+ });
3119
+
3120
+ it('should send ICE_CANDIDATE_ERROR metric if media connection fails and ice candidate errors have been gathered', async () => {
3121
+ let errorThrown = undefined;
3122
+
3123
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3124
+ turnServerInfo: undefined,
3125
+ turnDiscoverySkippedReason: undefined,
3126
+ });
3127
+ meeting.meetingState = 'ACTIVE';
3128
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2719
3129
 
2720
3130
  const forceRtcMetricsSend = sinon.stub().resolves();
2721
3131
  const closeMediaConnectionStub = sinon.stub();
@@ -2727,6 +3137,9 @@ describe('plugin-meetings', () => {
2727
3137
  on: sinon.stub(),
2728
3138
  });
2729
3139
 
3140
+ meeting.iceCandidateErrors.set('701_error', 2);
3141
+ meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
3142
+
2730
3143
  await meeting
2731
3144
  .addMedia({
2732
3145
  mediaSettings: {},
@@ -2756,6 +3169,9 @@ describe('plugin-meetings', () => {
2756
3169
  iceConnectionState: 'unknown',
2757
3170
  selectedCandidatePairChanges: 2,
2758
3171
  numTransports: 1,
3172
+ '701_error': 2,
3173
+ '701_turn_host_lookup_received_error': 1,
3174
+ iceCandidatesCount: 0,
2759
3175
  }
2760
3176
  );
2761
3177
 
@@ -2775,7 +3191,7 @@ describe('plugin-meetings', () => {
2775
3191
 
2776
3192
  statsAnalyzerStub = new EventsScope();
2777
3193
  // mock the StatsAnalyzer constructor
2778
- sinon.stub(StatsAnalyzerModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
3194
+ sinon.stub(InternalMediaCoreModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
2779
3195
 
2780
3196
  await meeting.addMedia({
2781
3197
  mediaSettings: {},
@@ -2789,8 +3205,8 @@ describe('plugin-meetings', () => {
2789
3205
  it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and sends metrics', async () => {
2790
3206
  statsAnalyzerStub.emit(
2791
3207
  {file: 'test', function: 'test'},
2792
- StatsAnalyzerModule.EVENTS.LOCAL_MEDIA_STARTED,
2793
- {type: 'audio'}
3208
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED,
3209
+ {mediaType: 'audio'}
2794
3210
  );
2795
3211
 
2796
3212
  assert.calledWith(
@@ -2802,7 +3218,7 @@ describe('plugin-meetings', () => {
2802
3218
  },
2803
3219
  EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED,
2804
3220
  {
2805
- type: 'audio',
3221
+ mediaType: 'audio',
2806
3222
  }
2807
3223
  );
2808
3224
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2817,8 +3233,8 @@ describe('plugin-meetings', () => {
2817
3233
  it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => {
2818
3234
  statsAnalyzerStub.emit(
2819
3235
  {file: 'test', function: 'test'},
2820
- StatsAnalyzerModule.EVENTS.LOCAL_MEDIA_STOPPED,
2821
- {type: 'video'}
3236
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED,
3237
+ {mediaType: 'video'}
2822
3238
  );
2823
3239
 
2824
3240
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2833,8 +3249,8 @@ describe('plugin-meetings', () => {
2833
3249
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => {
2834
3250
  statsAnalyzerStub.emit(
2835
3251
  {file: 'test', function: 'test'},
2836
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STARTED,
2837
- {type: 'video'}
3252
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3253
+ {mediaType: 'video'}
2838
3254
  );
2839
3255
 
2840
3256
  assert.calledWith(
@@ -2846,7 +3262,7 @@ describe('plugin-meetings', () => {
2846
3262
  },
2847
3263
  EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
2848
3264
  {
2849
- type: 'video',
3265
+ mediaType: 'video',
2850
3266
  }
2851
3267
  );
2852
3268
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2861,8 +3277,8 @@ describe('plugin-meetings', () => {
2861
3277
  it('REMOTE_MEDIA_STOPPED triggers the right metrics', async () => {
2862
3278
  statsAnalyzerStub.emit(
2863
3279
  {file: 'test', function: 'test'},
2864
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STOPPED,
2865
- {type: 'audio'}
3280
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3281
+ {mediaType: 'audio'}
2866
3282
  );
2867
3283
 
2868
3284
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2877,8 +3293,8 @@ describe('plugin-meetings', () => {
2877
3293
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics for share', async () => {
2878
3294
  statsAnalyzerStub.emit(
2879
3295
  {file: 'test', function: 'test'},
2880
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STARTED,
2881
- {type: 'share'}
3296
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3297
+ {mediaType: 'share'}
2882
3298
  );
2883
3299
 
2884
3300
  assert.calledWith(
@@ -2890,7 +3306,7 @@ describe('plugin-meetings', () => {
2890
3306
  },
2891
3307
  EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
2892
3308
  {
2893
- type: 'share',
3309
+ mediaType: 'share',
2894
3310
  }
2895
3311
  );
2896
3312
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2913,8 +3329,8 @@ describe('plugin-meetings', () => {
2913
3329
  it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
2914
3330
  statsAnalyzerStub.emit(
2915
3331
  {file: 'test', function: 'test'},
2916
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STOPPED,
2917
- {type: 'share'}
3332
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3333
+ {mediaType: 'share'}
2918
3334
  );
2919
3335
 
2920
3336
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2935,19 +3351,18 @@ describe('plugin-meetings', () => {
2935
3351
  });
2936
3352
 
2937
3353
  it('calls submitMQE correctly', async () => {
2938
- const fakeData = {intervalMetadata: {bla: 'bla'}};
3354
+ const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
2939
3355
 
2940
3356
  statsAnalyzerStub.emit(
2941
3357
  {file: 'test', function: 'test'},
2942
- StatsAnalyzerModule.EVENTS.MEDIA_QUALITY,
2943
- {data: fakeData, networkType: 'wifi'}
3358
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3359
+ {data: fakeData}
2944
3360
  );
2945
3361
 
2946
3362
  assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
2947
3363
  name: 'client.mediaquality.event',
2948
3364
  options: {
2949
3365
  meetingId: meeting.id,
2950
- networkType: 'wifi',
2951
3366
  },
2952
3367
  payload: {
2953
3368
  intervals: [fakeData],
@@ -3004,7 +3419,7 @@ describe('plugin-meetings', () => {
3004
3419
  it('succeeds even if getDevices() throws', async () => {
3005
3420
  meeting.meetingState = 'ACTIVE';
3006
3421
 
3007
- sinon.stub(internalMediaModule, 'getDevices').rejects(new Error('fake error'));
3422
+ sinon.stub(InternalMediaCoreModule, 'getDevices').rejects(new Error('fake error'));
3008
3423
 
3009
3424
  await meeting.addMedia();
3010
3425
  });
@@ -3021,7 +3436,7 @@ describe('plugin-meetings', () => {
3021
3436
  clientErrorCode: MISSING_ROAP_ANSWER_CLIENT_CODE,
3022
3437
  expectedErrorPayload: {
3023
3438
  errorDescription: ERROR_DESCRIPTIONS.MISSING_ROAP_ANSWER,
3024
- category: 'signaling',
3439
+ category: 'media',
3025
3440
  },
3026
3441
  },
3027
3442
  {
@@ -3040,10 +3455,18 @@ describe('plugin-meetings', () => {
3040
3455
  clientErrorCode: ICE_FAILED_WITH_TURN_TLS_CLIENT_CODE,
3041
3456
  expectedErrorPayload: {
3042
3457
  errorDescription: ERROR_DESCRIPTIONS.ICE_FAILED_WITH_TURN_TLS,
3043
- category: 'network',
3458
+ category: 'media',
3459
+ },
3460
+ },
3461
+ {
3462
+ clientErrorCode: ICE_AND_REACHABILITY_FAILED_CLIENT_CODE,
3463
+ unreachable: true,
3464
+ expectedErrorPayload: {
3465
+ errorDescription: ERROR_DESCRIPTIONS.ICE_AND_REACHABILITY_FAILED,
3466
+ category: 'expected',
3044
3467
  },
3045
3468
  },
3046
- ].forEach(({clientErrorCode, expectedErrorPayload}) => {
3469
+ ].forEach(({clientErrorCode, expectedErrorPayload, unreachable}) => {
3047
3470
  it(`should handle all ice failures correctly for ${clientErrorCode}`, async () => {
3048
3471
  // setting the method to the real implementation
3049
3472
  // because newMetrics is mocked completely in the webex-mock
@@ -3052,14 +3475,18 @@ describe('plugin-meetings', () => {
3052
3475
  webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
3053
3476
  CD.getErrorPayloadForClientErrorCode;
3054
3477
 
3478
+ webex.meetings.reachability = {
3479
+ isWebexMediaBackendUnreachable: sinon.stub().resolves(unreachable || false),
3480
+ };
3481
+
3055
3482
  const generateClientErrorCodeForIceFailureStub = sinon
3056
3483
  .stub(CallDiagnosticUtils, 'generateClientErrorCodeForIceFailure')
3057
3484
  .returns(clientErrorCode);
3058
3485
 
3059
3486
  meeting.meetingState = 'ACTIVE';
3060
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects(
3061
- new Error('fake error')
3062
- );
3487
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({
3488
+ iceConnected: false,
3489
+ });
3063
3490
 
3064
3491
  let errorThrown = false;
3065
3492
 
@@ -3073,8 +3500,9 @@ describe('plugin-meetings', () => {
3073
3500
 
3074
3501
  assert.calledOnceWithExactly(generateClientErrorCodeForIceFailureStub, {
3075
3502
  signalingState: 'unknown',
3076
- iceConnectionState: 'unknown',
3503
+ iceConnected: false,
3077
3504
  turnServerUsed: true,
3505
+ unreachable: unreachable || false,
3078
3506
  });
3079
3507
 
3080
3508
  const submitClientEventCalls = webex.internal.newMetrics.submitClientEvent.getCalls();
@@ -3162,7 +3590,7 @@ describe('plugin-meetings', () => {
3162
3590
 
3163
3591
  let clock;
3164
3592
 
3165
- beforeEach(() => {
3593
+ beforeEach(async () => {
3166
3594
  clock = sinon.useFakeTimers();
3167
3595
 
3168
3596
  sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.unknown);
@@ -3171,15 +3599,20 @@ describe('plugin-meetings', () => {
3171
3599
  meeting.config.deviceType = 'web';
3172
3600
  meeting.isMultistream = isMultistream;
3173
3601
  meeting.meetingState = 'ACTIVE';
3174
- meeting.mediaId = 'fake media id';
3175
3602
  meeting.selfUrl = 'selfUrl';
3176
3603
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
3177
- meeting.mediaProperties.getCurrentConnectionInfo = sinon.stub().resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
3604
+ meeting.mediaProperties.getCurrentConnectionInfo = sinon
3605
+ .stub()
3606
+ .resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
3178
3607
  meeting.setMercuryListener = sinon.stub();
3179
3608
  meeting.locusInfo.onFullLocus = sinon.stub();
3180
3609
  meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
3181
3610
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3182
- turnServerInfo: {url: 'turn-url', username: 'turn user', password: 'turn password'},
3611
+ turnServerInfo: {
3612
+ url: 'turns:turn-server-url:443?transport=tcp',
3613
+ username: 'turn user',
3614
+ password: 'turn password',
3615
+ },
3183
3616
  turnDiscoverySkippedReason: 'reachability',
3184
3617
  });
3185
3618
  meeting.deferSDPAnswer = new Defer();
@@ -3192,7 +3625,18 @@ describe('plugin-meetings', () => {
3192
3625
  // setup things that are expected to be the same across all the tests and are actually irrelevant for these tests
3193
3626
  expectedDebugId = `MC-${meeting.id.substring(0, 4)}`;
3194
3627
  expectedMediaConnectionConfig = {
3195
- iceServers: [{urls: 'turn-url', username: 'turn user', credential: 'turn password'}],
3628
+ iceServers: [
3629
+ {
3630
+ urls: 'turn:turn-server-url:5004?transport=tcp',
3631
+ username: 'turn user',
3632
+ credential: 'turn password',
3633
+ },
3634
+ {
3635
+ urls: 'turns:turn-server-url:443?transport=tcp',
3636
+ username: 'turn user',
3637
+ credential: 'turn password',
3638
+ },
3639
+ ],
3196
3640
  skipInactiveTransceivers: false,
3197
3641
  requireH264: true,
3198
3642
  sdpMunging: {
@@ -3261,16 +3705,28 @@ describe('plugin-meetings', () => {
3261
3705
  };
3262
3706
 
3263
3707
  roapMediaConnectionConstructorStub = sinon
3264
- .stub(internalMediaModule, 'RoapMediaConnection')
3708
+ .stub(InternalMediaCoreModule, 'RoapMediaConnection')
3265
3709
  .returns(fakeRoapMediaConnection);
3266
3710
 
3267
3711
  multistreamRoapMediaConnectionConstructorStub = sinon
3268
- .stub(internalMediaModule, 'MultistreamRoapMediaConnection')
3712
+ .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
3269
3713
  .returns(fakeMultistreamRoapMediaConnection);
3270
3714
 
3271
3715
  locusMediaRequestStub = sinon
3272
3716
  .stub(WebexPlugin.prototype, 'request')
3273
3717
  .resolves({body: {locus: {fullState: {}}}});
3718
+
3719
+ // setup some things and mocks so that the call to join() works
3720
+ // (we need to call join() because it creates the LocusMediaRequest instance
3721
+ // that's being tested in these tests)
3722
+ meeting.webex.meetings.registered = true;
3723
+ meeting.webex.internal.device.config = {};
3724
+ sinon.stub(MeetingUtil, 'joinMeeting').resolves({
3725
+ id: 'fake locus from mocked join request',
3726
+ locusUrl: 'fake locus url',
3727
+ mediaId: 'fake media id',
3728
+ });
3729
+ await meeting.join({enableMultistream: isMultistream});
3274
3730
  });
3275
3731
 
3276
3732
  afterEach(() => {
@@ -3299,13 +3755,14 @@ describe('plugin-meetings', () => {
3299
3755
 
3300
3756
  for (let idx = 0; idx < roapMediaConnectionToCheck.on.callCount; idx += 1) {
3301
3757
  if (
3302
- roapMediaConnectionToCheck.on.getCall(idx).args[0] === Event.ROAP_MESSAGE_TO_SEND
3758
+ roapMediaConnectionToCheck.on.getCall(idx).args[0] ===
3759
+ MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND
3303
3760
  ) {
3304
3761
  return roapMediaConnectionToCheck.on.getCall(idx).args[1];
3305
3762
  }
3306
3763
  }
3307
3764
  assert.fail(
3308
- 'listener for "roap:messageToSend" (Event.ROAP_MESSAGE_TO_SEND) was not registered'
3765
+ 'listener for "roap:messageToSend" (MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND) was not registered'
3309
3766
  );
3310
3767
  };
3311
3768
 
@@ -3746,13 +4203,13 @@ describe('plugin-meetings', () => {
3746
4203
  await meeting.addMedia({
3747
4204
  localStreams: {microphone: fakeMicrophoneStream},
3748
4205
  audioEnabled: false,
3749
- videoEnabled: false
4206
+ videoEnabled: false,
3750
4207
  });
3751
4208
  await simulateRoapOffer();
3752
4209
  await simulateRoapOk();
3753
4210
 
3754
4211
  assert.notCalled(handleDeviceLoggingSpy);
3755
- })
4212
+ });
3756
4213
 
3757
4214
  it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
3758
4215
  await meeting.addMedia({audioEnabled: false});
@@ -5107,7 +5564,7 @@ describe('plugin-meetings', () => {
5107
5564
 
5108
5565
  describe('#fetchMeetingInfo', () => {
5109
5566
  const FAKE_DESTINATION = 'something@somecompany.com';
5110
- const FAKE_TYPE = _SIP_URI_;
5567
+ const FAKE_TYPE = DESTINATION_TYPE.SIP_URI;
5111
5568
  const FAKE_TIMEOUT_FETCHMEETINGINFO_ID = '123456';
5112
5569
  const FAKE_PASSWORD = '123abc';
5113
5570
  const FAKE_CAPTCHA_CODE = 'a1b2c3XYZ';
@@ -5542,7 +5999,7 @@ describe('plugin-meetings', () => {
5542
5999
  const FAKE_PASSWORD = '123456';
5543
6000
  const FAKE_CAPTCHA_CODE = '654321';
5544
6001
  const FAKE_DESTINATION = 'something@somecompany.com';
5545
- const FAKE_TYPE = _SIP_URI_;
6002
+ const FAKE_TYPE = DESTINATION_TYPE.SIP_URI;
5546
6003
  const FAKE_INSTALLED_ORG_ID = '123456';
5547
6004
  const FAKE_MEETING_INFO_LOOKUP_URL = 'meetingLookupUrl';
5548
6005
 
@@ -6187,14 +6644,14 @@ describe('plugin-meetings', () => {
6187
6644
  beforeEach(() => {
6188
6645
  sandbox = sinon.createSandbox();
6189
6646
  meeting.statsAnalyzer = {
6190
- stopAnalyzer: sinon.stub().returns(Promise.resolve())
6647
+ stopAnalyzer: sinon.stub().returns(Promise.resolve()),
6191
6648
  };
6192
6649
 
6193
6650
  meeting.reconnectionManager = {
6194
- cleanUp: sinon.stub()
6651
+ cleanUp: sinon.stub(),
6195
6652
  };
6196
6653
 
6197
- meeting.cleanupLocalStreams=sinon.stub();
6654
+ meeting.cleanupLocalStreams = sinon.stub();
6198
6655
  meeting.closeRemoteStreams = sinon.stub().returns(Promise.resolve());
6199
6656
  meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
6200
6657
  meeting.unsetRemoteStreams = sinon.stub();
@@ -6275,7 +6732,6 @@ describe('plugin-meetings', () => {
6275
6732
  },
6276
6733
  'SELF_OBSERVING'
6277
6734
  );
6278
-
6279
6735
 
6280
6736
  // Verify that the event handler behaves as expected
6281
6737
  expect(meeting.statsAnalyzer.stopAnalyzer.calledOnce).to.be.true;
@@ -6288,11 +6744,13 @@ describe('plugin-meetings', () => {
6288
6744
  expect(meeting.unsetPeerConnections.calledOnce).to.be.true;
6289
6745
  expect(meeting.reconnectionManager.cleanUp.calledOnce).to.be.true;
6290
6746
  expect(meeting.mediaProperties.setMediaDirection.calledOnce).to.be.true;
6291
- expect(meeting.addMedia.calledOnceWithExactly({
6292
- audioEnabled: false,
6293
- videoEnabled: false,
6294
- shareVideoEnabled: true
6295
- })).to.be.true;
6747
+ expect(
6748
+ meeting.addMedia.calledOnceWithExactly({
6749
+ audioEnabled: false,
6750
+ videoEnabled: false,
6751
+ shareVideoEnabled: true,
6752
+ })
6753
+ ).to.be.true;
6296
6754
  await testUtils.flushPromises();
6297
6755
  assert.equal(meeting.isMoveToInProgress, false);
6298
6756
  });
@@ -7079,6 +7537,12 @@ describe('plugin-meetings', () => {
7079
7537
  id: 'stream',
7080
7538
  getTracks: () => [{id: 'track', addEventListener: sinon.stub()}],
7081
7539
  };
7540
+ const simulateConnectionStateChange = (newState) => {
7541
+ meeting.mediaProperties.webrtcMediaConnection.getConnectionState = sinon
7542
+ .stub()
7543
+ .returns(newState);
7544
+ eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]();
7545
+ };
7082
7546
 
7083
7547
  beforeEach(() => {
7084
7548
  eventListeners = {};
@@ -7088,23 +7552,29 @@ describe('plugin-meetings', () => {
7088
7552
  on: sinon.stub().callsFake((event, listener) => {
7089
7553
  eventListeners[event] = listener;
7090
7554
  }),
7555
+ getConnectionState: sinon.stub().returns(ConnectionState.New),
7091
7556
  };
7092
7557
  MediaUtil.createMediaStream.returns(fakeStream);
7093
7558
  });
7094
7559
 
7095
7560
  it('should register for all the correct RoapMediaConnection events', () => {
7096
7561
  meeting.setupMediaConnectionListeners();
7097
- assert.isFunction(eventListeners[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]);
7562
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_STARTED]);
7563
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_DONE]);
7564
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_FAILURE]);
7565
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]);
7566
+ assert.isFunction(eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]);
7567
+ assert.isFunction(
7568
+ eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]
7569
+ );
7570
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED]);
7571
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]);
7572
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]);
7103
7573
  });
7104
7574
 
7105
7575
  it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => {
7106
7576
  meeting.setupMediaConnectionListeners();
7107
- eventListeners[Event.REMOTE_TRACK_ADDED]({
7577
+ eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
7108
7578
  track: 'track',
7109
7579
  type: RemoteTrackType.AUDIO,
7110
7580
  });
@@ -7114,7 +7584,7 @@ describe('plugin-meetings', () => {
7114
7584
  stream: fakeStream,
7115
7585
  });
7116
7586
 
7117
- eventListeners[Event.REMOTE_TRACK_ADDED]({
7587
+ eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
7118
7588
  track: 'track',
7119
7589
  type: RemoteTrackType.VIDEO,
7120
7590
  });
@@ -7124,7 +7594,7 @@ describe('plugin-meetings', () => {
7124
7594
  stream: fakeStream,
7125
7595
  });
7126
7596
 
7127
- eventListeners[Event.REMOTE_TRACK_ADDED]({
7597
+ eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
7128
7598
  track: 'track',
7129
7599
  type: RemoteTrackType.SCREENSHARE_VIDEO,
7130
7600
  });
@@ -7135,13 +7605,78 @@ describe('plugin-meetings', () => {
7135
7605
  });
7136
7606
  });
7137
7607
 
7608
+ describe('should react on a ICE_CANDIDATE event', () => {
7609
+ beforeEach(() => {
7610
+ meeting.setupMediaConnectionListeners();
7611
+ });
7612
+
7613
+ it('should collect ice candidates', () => {
7614
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: 'candidate'});
7615
+
7616
+ assert.equal(meeting.iceCandidatesCount, 1);
7617
+ });
7618
+
7619
+ it('should not collect null ice candidates', () => {
7620
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: null});
7621
+
7622
+ assert.equal(meeting.iceCandidatesCount, 0);
7623
+ });
7624
+ });
7625
+
7626
+ describe('should react on a ICE_CANDIDATE_ERROR event', () => {
7627
+ beforeEach(() => {
7628
+ meeting.setupMediaConnectionListeners();
7629
+ });
7630
+
7631
+ it('should not collect skipped ice candidates error', () => {
7632
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
7633
+ error: {
7634
+ errorCode: 600,
7635
+ errorText: 'Address not associated with the desired network interface.',
7636
+ },
7637
+ });
7638
+
7639
+ assert.equal(meeting.iceCandidateErrors.size, 0);
7640
+ });
7641
+
7642
+ it('should collect valid ice candidates error', () => {
7643
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
7644
+ error: {errorCode: 701, errorText: ''},
7645
+ });
7646
+
7647
+ assert.equal(meeting.iceCandidateErrors.size, 1);
7648
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7649
+ });
7650
+
7651
+ it('should increment counter if same valid ice candidates error collected', () => {
7652
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
7653
+ error: {errorCode: 701, errorText: ''},
7654
+ });
7655
+
7656
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
7657
+ error: {errorCode: 701, errorText: 'STUN host lookup received error.'},
7658
+ });
7659
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({
7660
+ error: {errorCode: 701, errorText: 'STUN host lookup received error.'},
7661
+ });
7662
+
7663
+ assert.equal(meeting.iceCandidateErrors.size, 2);
7664
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7665
+ assert.equal(meeting.iceCandidateErrors.get('701_'), 1);
7666
+ assert.equal(
7667
+ meeting.iceCandidateErrors.has('701_stun_host_lookup_received_error'),
7668
+ true
7669
+ );
7670
+ assert.equal(meeting.iceCandidateErrors.get('701_stun_host_lookup_received_error'), 2);
7671
+ });
7672
+ });
7673
+
7138
7674
  describe('CONNECTION_STATE_CHANGED event when state = "Connecting"', () => {
7139
7675
  it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = true', () => {
7140
7676
  meeting.hasMediaConnectionConnectedAtLeastOnce = true;
7141
7677
  meeting.setupMediaConnectionListeners();
7142
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7143
- state: 'Connecting',
7144
- });
7678
+
7679
+ simulateConnectionStateChange(ConnectionState.Connecting);
7145
7680
 
7146
7681
  assert.notCalled(webex.internal.newMetrics.submitClientEvent);
7147
7682
  });
@@ -7149,9 +7684,8 @@ describe('plugin-meetings', () => {
7149
7684
  it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = false', () => {
7150
7685
  meeting.hasMediaConnectionConnectedAtLeastOnce = false;
7151
7686
  meeting.setupMediaConnectionListeners();
7152
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7153
- state: 'Connecting',
7154
- });
7687
+
7688
+ simulateConnectionStateChange(ConnectionState.Connecting);
7155
7689
 
7156
7690
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7157
7691
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7177,6 +7711,7 @@ describe('plugin-meetings', () => {
7177
7711
  on: sinon.stub().callsFake((event, listener) => {
7178
7712
  eventListeners[event] = listener;
7179
7713
  }),
7714
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
7180
7715
  };
7181
7716
  };
7182
7717
 
@@ -7230,9 +7765,7 @@ describe('plugin-meetings', () => {
7230
7765
  assert.equal(meeting.hasMediaConnectionConnectedAtLeastOnce, false);
7231
7766
 
7232
7767
  // simulate first connection success
7233
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7234
- state: 'Connected',
7235
- });
7768
+ simulateConnectionStateChange(ConnectionState.Connected);
7236
7769
  checkExpectedSpies({
7237
7770
  icePhase: 'JOIN_MEETING_FINAL',
7238
7771
  setNetworkStatusCallParams: [NETWORK_STATUS.CONNECTED],
@@ -7242,12 +7775,9 @@ describe('plugin-meetings', () => {
7242
7775
  // now simulate short connection loss, client.ice.end is not sent a second time as hasMediaConnectionConnectedAtLeastOnce = true
7243
7776
  resetSpies();
7244
7777
 
7245
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7246
- state: 'Disconnected',
7247
- });
7248
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7249
- state: 'Connected',
7250
- });
7778
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7779
+
7780
+ simulateConnectionStateChange(ConnectionState.Connected);
7251
7781
 
7252
7782
  checkExpectedSpies({
7253
7783
  setNetworkStatusCallParams: [NETWORK_STATUS.DISCONNECTED, NETWORK_STATUS.CONNECTED],
@@ -7255,12 +7785,9 @@ describe('plugin-meetings', () => {
7255
7785
 
7256
7786
  resetSpies();
7257
7787
 
7258
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7259
- state: 'Disconnected',
7260
- });
7261
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7262
- state: 'Connected',
7263
- });
7788
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7789
+
7790
+ simulateConnectionStateChange(ConnectionState.Connected);
7264
7791
  });
7265
7792
  });
7266
7793
 
@@ -7282,9 +7809,8 @@ describe('plugin-meetings', () => {
7282
7809
 
7283
7810
  const mockDisconnectedEvent = () => {
7284
7811
  meeting.setupMediaConnectionListeners();
7285
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7286
- state: 'Disconnected',
7287
- });
7812
+
7813
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7288
7814
  };
7289
7815
 
7290
7816
  const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
@@ -7348,9 +7874,8 @@ describe('plugin-meetings', () => {
7348
7874
  describe('CONNECTION_STATE_CHANGED event when state = "Failed"', () => {
7349
7875
  const mockFailedEvent = () => {
7350
7876
  meeting.setupMediaConnectionListeners();
7351
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7352
- state: 'Failed',
7353
- });
7877
+
7878
+ simulateConnectionStateChange(ConnectionState.Failed);
7354
7879
  };
7355
7880
 
7356
7881
  const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
@@ -7432,7 +7957,7 @@ describe('plugin-meetings', () => {
7432
7957
  cause: {name: fakeRootCauseName},
7433
7958
  });
7434
7959
 
7435
- eventListeners[Event.ROAP_FAILURE](fakeError);
7960
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7436
7961
 
7437
7962
  checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
7438
7963
  checkBehavioralMetricSent(
@@ -7449,7 +7974,7 @@ describe('plugin-meetings', () => {
7449
7974
  cause: {name: fakeRootCauseName},
7450
7975
  });
7451
7976
 
7452
- eventListeners[Event.ROAP_FAILURE](fakeError);
7977
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7453
7978
 
7454
7979
  checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
7455
7980
  checkBehavioralMetricSent(
@@ -7466,7 +7991,7 @@ describe('plugin-meetings', () => {
7466
7991
  cause: {name: fakeRootCauseName},
7467
7992
  });
7468
7993
 
7469
- eventListeners[Event.ROAP_FAILURE](fakeError);
7994
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7470
7995
 
7471
7996
  checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
7472
7997
  checkBehavioralMetricSent(
@@ -7481,7 +8006,7 @@ describe('plugin-meetings', () => {
7481
8006
  // SdpError is usually without a cause
7482
8007
  const fakeError = new Errors.SdpError(fakeErrorMessage, {name: fakeErrorName});
7483
8008
 
7484
- eventListeners[Event.ROAP_FAILURE](fakeError);
8009
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7485
8010
 
7486
8011
  checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
7487
8012
  // expectedMetadataType is the error name in this case
@@ -7499,7 +8024,7 @@ describe('plugin-meetings', () => {
7499
8024
  name: fakeErrorName,
7500
8025
  });
7501
8026
 
7502
- eventListeners[Event.ROAP_FAILURE](fakeError);
8027
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7503
8028
 
7504
8029
  checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
7505
8030
  // expectedMetadataType is the error name in this case
@@ -7525,7 +8050,7 @@ describe('plugin-meetings', () => {
7525
8050
  };
7526
8051
  meeting.sdpResponseTimer = '1234';
7527
8052
 
7528
- eventListeners[Event.REMOTE_SDP_ANSWER_PROCESSED]();
8053
+ eventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED]();
7529
8054
 
7530
8055
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7531
8056
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7553,7 +8078,7 @@ describe('plugin-meetings', () => {
7553
8078
  it('handles LOCAL_SDP_OFFER_GENERATED correctly', () => {
7554
8079
  assert.equal(meeting.deferSDPAnswer, undefined);
7555
8080
 
7556
- eventListeners[Event.LOCAL_SDP_OFFER_GENERATED]();
8081
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
7557
8082
 
7558
8083
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7559
8084
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7565,7 +8090,7 @@ describe('plugin-meetings', () => {
7565
8090
  });
7566
8091
 
7567
8092
  it('handles LOCAL_SDP_ANSWER_GENERATED correctly', () => {
7568
- eventListeners[Event.LOCAL_SDP_ANSWER_GENERATED]();
8093
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_ANSWER_GENERATED]();
7569
8094
 
7570
8095
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7571
8096
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7575,7 +8100,7 @@ describe('plugin-meetings', () => {
7575
8100
  });
7576
8101
  });
7577
8102
 
7578
- describe('handles Event.ROAP_MESSAGE_TO_SEND correctly', () => {
8103
+ describe('handles MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND correctly', () => {
7579
8104
  let sendRoapOKStub;
7580
8105
  let sendRoapMediaRequestStub;
7581
8106
  let sendRoapAnswerStub;
@@ -7593,7 +8118,7 @@ describe('plugin-meetings', () => {
7593
8118
  });
7594
8119
 
7595
8120
  it('handles OK message correctly', () => {
7596
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8121
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7597
8122
  roapMessage: {messageType: 'OK', seq: 1},
7598
8123
  });
7599
8124
 
@@ -7608,7 +8133,7 @@ describe('plugin-meetings', () => {
7608
8133
  it('handles OFFER message correctly (no answer in the http response)', async () => {
7609
8134
  sinon.stub(meeting, 'roapMessageReceived');
7610
8135
 
7611
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8136
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7612
8137
  roapMessage: {
7613
8138
  messageType: 'OFFER',
7614
8139
  seq: 1,
@@ -7634,7 +8159,7 @@ describe('plugin-meetings', () => {
7634
8159
  sendRoapMediaRequestStub.resolves({roapAnswer: fakeAnswer});
7635
8160
  sinon.stub(meeting, 'roapMessageReceived');
7636
8161
 
7637
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8162
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7638
8163
  roapMessage: {
7639
8164
  messageType: 'OFFER',
7640
8165
  seq: 1,
@@ -7656,14 +8181,20 @@ describe('plugin-meetings', () => {
7656
8181
  });
7657
8182
 
7658
8183
  it('handles OFFER message correctly when request fails', async () => {
8184
+ const fakeError = new Error('fake error');
7659
8185
  const clock = sinon.useFakeTimers();
7660
8186
  sinon.spy(clock, 'clearTimeout');
7661
8187
  meeting.deferSDPAnswer = {reject: sinon.stub()};
7662
8188
  meeting.sdpResponseTimer = '1234';
7663
- sendRoapMediaRequestStub.rejects();
8189
+ sendRoapMediaRequestStub.rejects(fakeError);
7664
8190
  sinon.stub(meeting, 'roapMessageReceived');
8191
+ const getErrorPayloadForClientErrorCodeStub =
8192
+ (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
8193
+ sinon
8194
+ .stub()
8195
+ .callsFake(({clientErrorCode}) => ({errorCode: clientErrorCode, fatal: true})));
7665
8196
 
7666
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8197
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7667
8198
  roapMessage: {
7668
8199
  messageType: 'OFFER',
7669
8200
  seq: 1,
@@ -7686,10 +8217,25 @@ describe('plugin-meetings', () => {
7686
8217
  assert.calledOnce(clock.clearTimeout);
7687
8218
  assert.calledWith(clock.clearTimeout, '1234');
7688
8219
  assert.equal(meeting.sdpResponseTimer, undefined);
8220
+
8221
+ assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
8222
+ clientErrorCode: 2007,
8223
+ });
8224
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8225
+ name: 'client.media-engine.remote-sdp-received',
8226
+ payload: {
8227
+ canProceed: false,
8228
+ errors: [{errorCode: 2007, fatal: true}],
8229
+ },
8230
+ options: {
8231
+ meetingId: meeting.id,
8232
+ rawError: fakeError,
8233
+ },
8234
+ });
7689
8235
  });
7690
8236
 
7691
8237
  it('handles ANSWER message correctly', () => {
7692
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8238
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7693
8239
  roapMessage: {
7694
8240
  messageType: 'ANSWER',
7695
8241
  seq: 10,
@@ -7710,7 +8256,7 @@ describe('plugin-meetings', () => {
7710
8256
  it('sends metrics if fails to send roap ANSWER message', async () => {
7711
8257
  sendRoapAnswerStub.rejects(new Error('sending answer failed'));
7712
8258
 
7713
- await eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8259
+ await eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7714
8260
  roapMessage: {
7715
8261
  messageType: 'ANSWER',
7716
8262
  seq: 10,
@@ -7734,7 +8280,7 @@ describe('plugin-meetings', () => {
7734
8280
 
7735
8281
  [ErrorType.CONFLICT, ErrorType.DOUBLECONFLICT].forEach((errorType) =>
7736
8282
  it(`handles ERROR message indicating glare condition correctly (errorType=${errorType})`, () => {
7737
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8283
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7738
8284
  roapMessage: {
7739
8285
  messageType: 'ERROR',
7740
8286
  seq: 10,
@@ -7765,7 +8311,7 @@ describe('plugin-meetings', () => {
7765
8311
  );
7766
8312
 
7767
8313
  it('handles ERROR message indicating other errors correctly', () => {
7768
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8314
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7769
8315
  roapMessage: {
7770
8316
  messageType: 'ERROR',
7771
8317
  seq: 10,
@@ -7793,8 +8339,12 @@ describe('plugin-meetings', () => {
7793
8339
  });
7794
8340
 
7795
8341
  it('registers for audio and video source count changed', () => {
7796
- assert.isFunction(eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED]);
7797
- assert.isFunction(eventListeners[Event.AUDIO_SOURCES_COUNT_CHANGED]);
8342
+ assert.isFunction(
8343
+ eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED]
8344
+ );
8345
+ assert.isFunction(
8346
+ eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED]
8347
+ );
7798
8348
  });
7799
8349
 
7800
8350
  it('forwards the VIDEO_SOURCES_COUNT_CHANGED event as "media:remoteVideoSourceCountChanged"', () => {
@@ -7804,7 +8354,7 @@ describe('plugin-meetings', () => {
7804
8354
 
7805
8355
  sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources');
7806
8356
 
7807
- eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](
8357
+ eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
7808
8358
  numTotalSources,
7809
8359
  numLiveSources,
7810
8360
  mediaContent
@@ -7828,7 +8378,7 @@ describe('plugin-meetings', () => {
7828
8378
  const numLiveSources = 2;
7829
8379
  const mediaContent = 'MAIN';
7830
8380
 
7831
- eventListeners[Event.AUDIO_SOURCES_COUNT_CHANGED](
8381
+ eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED](
7832
8382
  numTotalSources,
7833
8383
  numLiveSources,
7834
8384
  mediaContent
@@ -7856,7 +8406,7 @@ describe('plugin-meetings', () => {
7856
8406
  'setNumCurrentSources'
7857
8407
  );
7858
8408
 
7859
- eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](
8409
+ eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
7860
8410
  numTotalSources,
7861
8411
  numLiveSources,
7862
8412
  'MAIN'
@@ -7874,7 +8424,7 @@ describe('plugin-meetings', () => {
7874
8424
  'setNumCurrentSources'
7875
8425
  );
7876
8426
 
7877
- eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](
8427
+ eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
7878
8428
  numTotalSources,
7879
8429
  numLiveSources,
7880
8430
  'SLIDES'
@@ -9781,6 +10331,7 @@ describe('plugin-meetings', () => {
9781
10331
  beforeEach(() => {
9782
10332
  webex.internal.llm.isConnected = sinon.stub().returns(false);
9783
10333
  webex.internal.llm.getLocusUrl = sinon.stub();
10334
+ webex.internal.llm.getDatachannelUrl = sinon.stub();
9784
10335
  webex.internal.llm.registerAndConnect = sinon
9785
10336
  .stub()
9786
10337
  .returns(Promise.resolve('something'));
@@ -9808,6 +10359,7 @@ describe('plugin-meetings', () => {
9808
10359
  meeting.joinedWith = {state: 'JOINED'};
9809
10360
  webex.internal.llm.isConnected.returns(true);
9810
10361
  webex.internal.llm.getLocusUrl.returns('a url');
10362
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9811
10363
 
9812
10364
  meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
9813
10365
 
@@ -9844,6 +10396,7 @@ describe('plugin-meetings', () => {
9844
10396
  meeting.joinedWith = {state: 'JOINED'};
9845
10397
  webex.internal.llm.isConnected.returns(true);
9846
10398
  webex.internal.llm.getLocusUrl.returns('a url');
10399
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9847
10400
 
9848
10401
  meeting.locusInfo = {url: 'a different url', info: {datachannelUrl: 'a datachannel url'}};
9849
10402
 
@@ -9869,6 +10422,36 @@ describe('plugin-meetings', () => {
9869
10422
  );
9870
10423
  });
9871
10424
 
10425
+ it('disconnects if first if the data channel url has changed', async () => {
10426
+ meeting.joinedWith = {state: 'JOINED'};
10427
+ webex.internal.llm.isConnected.returns(true);
10428
+ webex.internal.llm.getLocusUrl.returns('a url');
10429
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
10430
+
10431
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a different datachannel url'}};
10432
+
10433
+ const result = await meeting.updateLLMConnection();
10434
+
10435
+ assert.calledWith(webex.internal.llm.disconnectLLM);
10436
+ assert.calledWith(
10437
+ webex.internal.llm.registerAndConnect,
10438
+ 'a url',
10439
+ 'a different datachannel url'
10440
+ );
10441
+ assert.equal(result, 'something');
10442
+ assert.calledWithExactly(
10443
+ meeting.webex.internal.llm.off,
10444
+ 'event:relay.event',
10445
+ meeting.processRelayEvent
10446
+ );
10447
+ assert.calledTwice(meeting.webex.internal.llm.off);
10448
+ assert.calledOnceWithExactly(
10449
+ meeting.webex.internal.llm.on,
10450
+ 'event:relay.event',
10451
+ meeting.processRelayEvent
10452
+ );
10453
+ });
10454
+
9872
10455
  it('disconnects when the state is not JOINED', async () => {
9873
10456
  meeting.joinedWith = {state: 'any other state'};
9874
10457
  webex.internal.llm.isConnected.returns(true);