@webex/plugin-meetings 3.3.1 → 3.4.0

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