@webex/plugin-meetings 3.3.1-next.4 → 3.3.1-next.41

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 (115) 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/media/MediaConnectionAwaiter.js +70 -15
  9. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  10. package/dist/meeting/connectionStateHandler.js +67 -0
  11. package/dist/meeting/connectionStateHandler.js.map +1 -0
  12. package/dist/meeting/index.js +552 -357
  13. package/dist/meeting/index.js.map +1 -1
  14. package/dist/meeting/locusMediaRequest.js +7 -0
  15. package/dist/meeting/locusMediaRequest.js.map +1 -1
  16. package/dist/meeting/util.js +1 -0
  17. package/dist/meeting/util.js.map +1 -1
  18. package/dist/meeting-info/index.js +4 -4
  19. package/dist/meeting-info/index.js.map +1 -1
  20. package/dist/meeting-info/meeting-info-v2.js +2 -2
  21. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  22. package/dist/meeting-info/util.js +17 -17
  23. package/dist/meeting-info/util.js.map +1 -1
  24. package/dist/meeting-info/utilv2.js +16 -16
  25. package/dist/meeting-info/utilv2.js.map +1 -1
  26. package/dist/meetings/collection.js +1 -1
  27. package/dist/meetings/collection.js.map +1 -1
  28. package/dist/meetings/index.js +37 -33
  29. package/dist/meetings/index.js.map +1 -1
  30. package/dist/meetings/meetings.types.js +8 -0
  31. package/dist/meetings/meetings.types.js.map +1 -1
  32. package/dist/meetings/util.js +3 -2
  33. package/dist/meetings/util.js.map +1 -1
  34. package/dist/metrics/constants.js +2 -1
  35. package/dist/metrics/constants.js.map +1 -1
  36. package/dist/metrics/index.js +57 -0
  37. package/dist/metrics/index.js.map +1 -1
  38. package/dist/personal-meeting-room/index.js +1 -1
  39. package/dist/personal-meeting-room/index.js.map +1 -1
  40. package/dist/reachability/clusterReachability.js +108 -53
  41. package/dist/reachability/clusterReachability.js.map +1 -1
  42. package/dist/reachability/index.js +415 -56
  43. package/dist/reachability/index.js.map +1 -1
  44. package/dist/types/constants.d.ts +11 -3
  45. package/dist/types/media/MediaConnectionAwaiter.d.ts +24 -4
  46. package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
  47. package/dist/types/meeting/index.d.ts +27 -7
  48. package/dist/types/meeting/locusMediaRequest.d.ts +2 -0
  49. package/dist/types/meeting-info/index.d.ts +3 -2
  50. package/dist/types/meeting-info/meeting-info-v2.d.ts +3 -2
  51. package/dist/types/meeting-info/util.d.ts +5 -4
  52. package/dist/types/meeting-info/utilv2.d.ts +3 -2
  53. package/dist/types/meetings/collection.d.ts +3 -2
  54. package/dist/types/meetings/index.d.ts +4 -3
  55. package/dist/types/meetings/meetings.types.d.ts +9 -0
  56. package/dist/types/metrics/constants.d.ts +1 -0
  57. package/dist/types/metrics/index.d.ts +15 -0
  58. package/dist/types/reachability/clusterReachability.d.ts +31 -3
  59. package/dist/types/reachability/index.d.ts +93 -2
  60. package/dist/webinar/index.js +1 -1
  61. package/package.json +23 -23
  62. package/src/breakouts/index.ts +7 -1
  63. package/src/constants.ts +13 -17
  64. package/src/media/MediaConnectionAwaiter.ts +89 -14
  65. package/src/meeting/connectionStateHandler.ts +65 -0
  66. package/src/meeting/index.ts +526 -292
  67. package/src/meeting/locusMediaRequest.ts +5 -0
  68. package/src/meeting/util.ts +1 -0
  69. package/src/meeting-info/index.ts +9 -6
  70. package/src/meeting-info/meeting-info-v2.ts +4 -4
  71. package/src/meeting-info/util.ts +23 -28
  72. package/src/meeting-info/utilv2.ts +18 -24
  73. package/src/meetings/collection.ts +3 -3
  74. package/src/meetings/index.ts +39 -40
  75. package/src/meetings/meetings.types.ts +11 -0
  76. package/src/meetings/util.ts +5 -4
  77. package/src/metrics/constants.ts +1 -0
  78. package/src/metrics/index.ts +44 -0
  79. package/src/personal-meeting-room/index.ts +2 -2
  80. package/src/reachability/clusterReachability.ts +86 -25
  81. package/src/reachability/index.ts +316 -27
  82. package/test/unit/spec/breakouts/index.ts +51 -32
  83. package/test/unit/spec/media/MediaConnectionAwaiter.ts +131 -32
  84. package/test/unit/spec/media/index.ts +8 -9
  85. package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
  86. package/test/unit/spec/meeting/index.js +643 -140
  87. package/test/unit/spec/meeting/locusMediaRequest.ts +7 -0
  88. package/test/unit/spec/meeting-info/index.js +4 -4
  89. package/test/unit/spec/meeting-info/meetinginfov2.js +24 -28
  90. package/test/unit/spec/meeting-info/request.js +2 -2
  91. package/test/unit/spec/meeting-info/utilv2.js +41 -49
  92. package/test/unit/spec/meetings/index.js +14 -0
  93. package/test/unit/spec/metrics/index.js +126 -0
  94. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -2
  95. package/test/unit/spec/personal-meeting-room/personal-meeting-room.js +2 -2
  96. package/test/unit/spec/reachability/clusterReachability.ts +116 -22
  97. package/test/unit/spec/reachability/index.ts +1153 -84
  98. package/test/unit/spec/rtcMetrics/index.ts +1 -0
  99. package/dist/mediaQualityMetrics/config.js +0 -321
  100. package/dist/mediaQualityMetrics/config.js.map +0 -1
  101. package/dist/statsAnalyzer/global.js +0 -44
  102. package/dist/statsAnalyzer/global.js.map +0 -1
  103. package/dist/statsAnalyzer/index.js +0 -1072
  104. package/dist/statsAnalyzer/index.js.map +0 -1
  105. package/dist/statsAnalyzer/mqaUtil.js +0 -368
  106. package/dist/statsAnalyzer/mqaUtil.js.map +0 -1
  107. package/dist/types/mediaQualityMetrics/config.d.ts +0 -247
  108. package/dist/types/statsAnalyzer/global.d.ts +0 -36
  109. package/dist/types/statsAnalyzer/index.d.ts +0 -217
  110. package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -48
  111. package/src/mediaQualityMetrics/config.ts +0 -255
  112. package/src/statsAnalyzer/global.ts +0 -37
  113. package/src/statsAnalyzer/index.ts +0 -1318
  114. package/src/statsAnalyzer/mqaUtil.ts +0 -463
  115. 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,11 @@ 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.stub(meeting, 'addMediaInternal').returns(Promise.resolve(test4));
629
629
 
630
630
  webex.meetings.reachability.getReachabilityResults.resolves(fakeReachabilityResults);
631
631
 
@@ -644,7 +644,7 @@ describe('plugin-meetings', () => {
644
644
  mediaOptions,
645
645
  });
646
646
 
647
- // check that TURN discovery is done with join and addMedia called
647
+ // check that TURN discovery is done with join and addMediaInternal() called
648
648
  assert.calledOnceWithExactly(meeting.join, {
649
649
  ...joinOptions,
650
650
  roapMessage: fakeRoapMessage,
@@ -656,7 +656,7 @@ describe('plugin-meetings', () => {
656
656
  meeting,
657
657
  fakeJoinResult
658
658
  );
659
- assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, fakeTurnServerInfo);
659
+ assert.calledOnceWithExactly(meeting.addMediaInternal, sinon.match.any, fakeTurnServerInfo, false, mediaOptions);
660
660
 
661
661
  assert.deepEqual(result, {join: fakeJoinResult, media: test4});
662
662
 
@@ -672,7 +672,7 @@ describe('plugin-meetings', () => {
672
672
  mediaOptions,
673
673
  });
674
674
 
675
- // check that TURN discovery is done with join and addMedia called
675
+ // check that TURN discovery is done with join and addMediaInternal() called
676
676
  assert.calledOnceWithExactly(meeting.join, {
677
677
  ...joinOptions,
678
678
  roapMessage: undefined,
@@ -681,7 +681,7 @@ describe('plugin-meetings', () => {
681
681
  assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
682
682
  assert.notCalled(handleTurnDiscoveryHttpResponseStub);
683
683
  assert.notCalled(abortTurnDiscoveryStub);
684
- assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, undefined);
684
+ assert.calledOnceWithExactly(meeting.addMediaInternal, sinon.match.any, undefined, false, mediaOptions);
685
685
 
686
686
  assert.deepEqual(result, {join: fakeJoinResult, media: test4});
687
687
  assert.equal(meeting.turnServerUsed, false);
@@ -698,7 +698,7 @@ describe('plugin-meetings', () => {
698
698
  mediaOptions,
699
699
  });
700
700
 
701
- // check that TURN discovery is done with join and addMedia called
701
+ // check that TURN discovery is done with join and addMediaInternal() called
702
702
  assert.calledOnceWithExactly(meeting.join, {
703
703
  ...joinOptions,
704
704
  roapMessage: fakeRoapMessage,
@@ -711,7 +711,7 @@ describe('plugin-meetings', () => {
711
711
  fakeJoinResult
712
712
  );
713
713
  assert.calledOnceWithExactly(abortTurnDiscoveryStub);
714
- assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, undefined);
714
+ assert.calledOnceWithExactly(meeting.addMediaInternal, sinon.match.any, undefined, false, mediaOptions);
715
715
 
716
716
  assert.deepEqual(result, {join: fakeJoinResult, media: test4});
717
717
  });
@@ -800,19 +800,19 @@ describe('plugin-meetings', () => {
800
800
 
801
801
  it('should fail if called with allowMediaInLobby:false', async () => {
802
802
  meeting.join = sinon.stub().returns(Promise.resolve(test1));
803
- meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
803
+ meeting.addMediaInternal = sinon.stub().returns(Promise.resolve(test4));
804
804
 
805
805
  await assert.isRejected(
806
806
  meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: false}})
807
807
  );
808
808
  });
809
809
 
810
- it('should call leave() if addMedia fails and ignore leave() failure', async () => {
810
+ it('should call leave() if addMediaInternal() fails and ignore leave() failure', async () => {
811
811
  const leaveError = new Error('leave error');
812
812
  const addMediaError = new Error('fake addMedia error');
813
813
 
814
814
  const leaveStub = sinon.stub(meeting, 'leave').rejects(leaveError);
815
- meeting.addMedia = sinon.stub().rejects(addMediaError);
815
+ meeting.addMediaInternal = sinon.stub().rejects(addMediaError);
816
816
 
817
817
  await assert.isRejected(
818
818
  meeting.joinWithMedia({
@@ -863,12 +863,11 @@ describe('plugin-meetings', () => {
863
863
  );
864
864
  });
865
865
 
866
- it('should not call leave() if addMedia fails the first time and succeeds the second time and should only call join() once', async () => {
866
+ it('should not call leave() if addMediaInternal() fails the first time and succeeds the second time and should only call join() once', async () => {
867
867
  const addMediaError = new Error('fake addMedia error');
868
- const leaveError = new Error('leave error');
869
- const leaveStub = sinon.stub(meeting, 'leave').rejects(leaveError);
868
+ const leaveStub = sinon.stub(meeting, 'leave');
870
869
 
871
- meeting.addMedia = sinon
870
+ meeting.addMediaInternal = sinon
872
871
  .stub()
873
872
  .onFirstCall()
874
873
  .rejects(addMediaError)
@@ -902,6 +901,200 @@ describe('plugin-meetings', () => {
902
901
  }
903
902
  );
904
903
  });
904
+
905
+ it('should send the right CA events when media connection fails', async () => {
906
+ const fakeClientError = {id: 'error'};
907
+
908
+ const fakeMediaConnection = {
909
+ close: sinon.stub(),
910
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
911
+ initiateOffer: sinon.stub().resolves({}),
912
+ on: sinon.stub(),
913
+ forceRtcMetricsSend: sinon.stub().resolves(),
914
+ };
915
+
916
+ // setup the stubs so that media connection always fails on waitForMediaConnectionConnected()
917
+ addMediaInternalStub.restore();
918
+ meeting.join.returns(
919
+ Promise.resolve({id: 'join result', roapMessage: 'fake TURN discovery response'})
920
+ );
921
+
922
+ sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
923
+ sinon.stub(meeting, 'waitForRemoteSDPAnswer').resolves();
924
+ sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
925
+ sinon
926
+ .stub(meeting.mediaProperties, 'waitForMediaConnectionConnected')
927
+ .rejects(new Error('fake error'));
928
+
929
+ webex.meetings.reachability.isWebexMediaBackendUnreachable = sinon.stub().resolves(false);
930
+ webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode = sinon
931
+ .stub()
932
+ .returns(fakeClientError);
933
+
934
+ // call joinWithMedia() - it should fail
935
+ await assert.isRejected(
936
+ meeting.joinWithMedia({
937
+ joinOptions,
938
+ mediaOptions,
939
+ })
940
+ );
941
+
942
+ // check the right CA events have been sent:
943
+ // calls at index 0 and 2 to submitClientEvent are for "client.media.capabilities" which we don't care about in this test
944
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent.getCall(1), {
945
+ name: 'client.ice.end',
946
+ payload: {
947
+ canProceed: false,
948
+ icePhase: 'JOIN_MEETING_RETRY',
949
+ errors: [fakeClientError],
950
+ },
951
+ options: {
952
+ meetingId: meeting.id,
953
+ },
954
+ });
955
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent.getCall(3), {
956
+ name: 'client.ice.end',
957
+ payload: {
958
+ canProceed: false,
959
+ icePhase: 'JOIN_MEETING_FINAL',
960
+ errors: [fakeClientError],
961
+ },
962
+ options: {
963
+ meetingId: meeting.id,
964
+ },
965
+ });
966
+ });
967
+
968
+ it('should force TURN discovery on the 2nd attempt, if addMediaInternal() fails the first time', async () => {
969
+ const addMediaError = new Error('fake addMedia error');
970
+
971
+ const fakeMediaConnection = {
972
+ close: sinon.stub(),
973
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
974
+ initiateOffer: sinon.stub().resolves({}),
975
+ on: sinon.stub(),
976
+ };
977
+
978
+ /* Setup the stubs so that the first call to addMediaInternal() fails
979
+ and the 2nd call calls the real implementation - so that we can check that
980
+ addMediaInternal() eventually calls meeting.roap.doTurnDiscovery() with isForced=true.
981
+ As a result we need to also stub a few other methods like createMediaConnection() and waitForRemoteSDPAnswer() */
982
+ sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
983
+ sinon.stub(meeting, 'waitForRemoteSDPAnswer').resolves();
984
+
985
+ addMediaInternalStub.onFirstCall().rejects(addMediaError);
986
+ addMediaInternalStub.onSecondCall().callsFake((...args) => {
987
+ return addMediaInternalStub.wrappedMethod.bind(meeting)(...args);
988
+ });
989
+
990
+ sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
991
+
992
+ const result = await meeting.joinWithMedia({
993
+ joinOptions,
994
+ mediaOptions,
995
+ });
996
+
997
+ assert.deepEqual(result, {join: fakeJoinResult, media: undefined});
998
+
999
+ assert.calledOnce(meeting.join);
1000
+
1001
+ // first addMediaInternal() call without forcing TURN
1002
+ assert.calledWith(
1003
+ meeting.addMediaInternal.firstCall,
1004
+ sinon.match.any,
1005
+ fakeTurnServerInfo,
1006
+ false,
1007
+ mediaOptions
1008
+ );
1009
+
1010
+ // second addMediaInternal() call with forcing TURN
1011
+ assert.calledWith(
1012
+ meeting.addMediaInternal.secondCall,
1013
+ sinon.match.any,
1014
+ undefined,
1015
+ true,
1016
+ mediaOptions
1017
+ );
1018
+
1019
+ // now check that TURN is actually forced by addMediaInternal(),
1020
+ // we're not checking the isReconnecting param value, because it depends on the full sequence of things
1021
+ // being done correctly (like SDP offer creation) and some of these are stubbed in this test
1022
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, sinon.match.any, true);
1023
+ });
1024
+
1025
+ it('should return the right icePhase in icePhaseCallback on 1st attempt and retry', async () => {
1026
+ const addMediaError = new Error('fake addMedia error');
1027
+
1028
+ const icePhaseCallbacks = [];
1029
+ const addMediaInternalResults = [];
1030
+
1031
+ meeting.addMediaInternal = sinon
1032
+ .stub()
1033
+ .callsFake((icePhaseCallback, _turnServerInfo, _forceTurnDiscovery) => {
1034
+ const defer = new Defer();
1035
+
1036
+ icePhaseCallbacks.push(icePhaseCallback);
1037
+ addMediaInternalResults.push(defer);
1038
+ return defer.promise;
1039
+ });
1040
+
1041
+ const result = meeting.joinWithMedia({
1042
+ joinOptions,
1043
+ mediaOptions,
1044
+ });
1045
+
1046
+ await testUtils.flushPromises();
1047
+
1048
+ // check the callback works correctly on the 1st attempt
1049
+ assert.equal(icePhaseCallbacks.length, 1);
1050
+ assert.equal(icePhaseCallbacks[0](), 'JOIN_MEETING_RETRY');
1051
+
1052
+ // now trigger the failure, so that joinWithMedia() does a retry
1053
+ addMediaInternalResults[0].reject(addMediaError);
1054
+
1055
+ await testUtils.flushPromises();
1056
+
1057
+ // check the callback works correctly on the 2nd attempt
1058
+ assert.equal(icePhaseCallbacks.length, 2);
1059
+ assert.equal(icePhaseCallbacks[1](), 'JOIN_MEETING_FINAL');
1060
+
1061
+ // trigger 2nd failure
1062
+ addMediaInternalResults[1].reject(addMediaError);
1063
+
1064
+ await assert.isRejected(result);
1065
+ });
1066
+
1067
+ it('should not attempt a retry if we fail to create the offer on first atttempt', async () => {
1068
+ const addMediaError = new Error('fake addMedia error');
1069
+ addMediaError.name = 'SdpOfferCreationError';
1070
+
1071
+ meeting.addMediaInternal.rejects(addMediaError)
1072
+
1073
+ await assert.isRejected(meeting.joinWithMedia({
1074
+ joinOptions,
1075
+ mediaOptions,
1076
+ }), addMediaError);
1077
+
1078
+ // check that only 1 attempt was done
1079
+ assert.calledOnce(meeting.join);
1080
+ assert.calledOnce(meeting.addMediaInternal);
1081
+ assert.calledOnce(Metrics.sendBehavioralMetric);
1082
+ assert.calledWith(
1083
+ Metrics.sendBehavioralMetric.firstCall,
1084
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
1085
+ {
1086
+ correlation_id: meeting.correlationId,
1087
+ locus_id: meeting.locusUrl.split('/').pop(),
1088
+ reason: addMediaError.message,
1089
+ stack: addMediaError.stack,
1090
+ leaveErrorReason: undefined,
1091
+ isRetry: false,
1092
+ },
1093
+ {
1094
+ type: addMediaError.name,
1095
+ }
1096
+ );
1097
+ });
905
1098
  });
906
1099
 
907
1100
  describe('#isTranscriptionSupported', () => {
@@ -946,19 +1139,18 @@ describe('plugin-meetings', () => {
946
1139
  assert.calledTwice(webex.internal.voicea.turnOnCaptions);
947
1140
  });
948
1141
 
949
- it('should listen to events and not turnOnCaptions if the user is not a host', async () => {
1142
+ it('should listen to events and turnOnCaptions for all users', async () => {
950
1143
  meeting.joinedWith = {
951
1144
  state: 'JOINED',
952
1145
  };
953
1146
  meeting.areVoiceaEventsSetup = false;
954
- meeting.roles = ['COHOST'];
955
1147
 
956
1148
  await meeting.startTranscription();
957
1149
 
958
1150
  assert.equal(webex.internal.voicea.on.callCount, 4);
959
1151
  assert.equal(meeting.areVoiceaEventsSetup, true);
960
1152
  assert.equal(webex.internal.voicea.listenToEvents.callCount, 1);
961
- assert.notCalled(webex.internal.voicea.turnOnCaptions);
1153
+ assert.calledOnce(webex.internal.voicea.turnOnCaptions);
962
1154
  });
963
1155
 
964
1156
  it("should throw error if request doesn't work", async () => {
@@ -1075,6 +1267,7 @@ describe('plugin-meetings', () => {
1075
1267
  webex.internal.voicea.on = sinon.stub();
1076
1268
  webex.internal.voicea.off = sinon.stub();
1077
1269
  webex.internal.voicea.setSpokenLanguage = sinon.stub();
1270
+ meeting.roles = ['MODERATOR'];
1078
1271
  });
1079
1272
 
1080
1273
  afterEach(() => {
@@ -1091,6 +1284,16 @@ describe('plugin-meetings', () => {
1091
1284
  });
1092
1285
  });
1093
1286
 
1287
+ it('should reject if current user is not a host', (done) => {
1288
+ meeting.isTranscriptionSupported.returns(true);
1289
+ meeting.roles = ['COHOST'];
1290
+
1291
+ meeting.setSpokenLanguage('fr').catch((error) => {
1292
+ assert.equal(error.message, 'Only host can set spoken language');
1293
+ done();
1294
+ });
1295
+ });
1296
+
1094
1297
  it('should resolve with the language code on successful language update', (done) => {
1095
1298
  meeting.isTranscriptionSupported.returns(true);
1096
1299
  const languageCode = 'fr';
@@ -1383,6 +1586,39 @@ describe('plugin-meetings', () => {
1383
1586
  );
1384
1587
  });
1385
1588
 
1589
+ [true, false].forEach((enableMultistream) => {
1590
+ it(`should instantiate LocusMediaRequest with correct parameters (enableMultistream=${enableMultistream})`, async () => {
1591
+ meeting.config.deviceType = 'web';
1592
+ meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
1593
+
1594
+ const mockLocusMediaRequestCtor = sinon
1595
+ .stub(LocusMediaRequestModule, 'LocusMediaRequest')
1596
+ .returns({
1597
+ id: 'fake LocusMediaRequest instance',
1598
+ });
1599
+
1600
+ await meeting.join({enableMultistream});
1601
+
1602
+ assert.calledOnceWithExactly(
1603
+ mockLocusMediaRequestCtor,
1604
+ {
1605
+ correlationId: meeting.correlationId,
1606
+ meetingId: meeting.id,
1607
+ device: {
1608
+ url: meeting.deviceUrl,
1609
+ deviceType: meeting.config.deviceType,
1610
+ countryCode: 'UK',
1611
+ regionCode: 'EU',
1612
+ },
1613
+ preferTranscoding: !enableMultistream,
1614
+ },
1615
+ {
1616
+ parent: meeting.webex,
1617
+ }
1618
+ );
1619
+ });
1620
+ });
1621
+
1386
1622
  it('should take trigger from meeting joinTrigger if available', () => {
1387
1623
  meeting.updateCallStateForMetrics({joinTrigger: 'fake-join-trigger'});
1388
1624
  const join = meeting.join();
@@ -1661,7 +1897,7 @@ describe('plugin-meetings', () => {
1661
1897
 
1662
1898
  let fakeMediaConnection;
1663
1899
 
1664
- beforeEach(() => {
1900
+ beforeEach(async () => {
1665
1901
  fakeMediaConnection = {
1666
1902
  close: sinon.stub(),
1667
1903
  getConnectionState: sinon.stub().returns(ConnectionState.Connected),
@@ -1674,13 +1910,18 @@ describe('plugin-meetings', () => {
1674
1910
  meeting.audio = muteStateStub;
1675
1911
  meeting.video = muteStateStub;
1676
1912
  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()
1913
+ sinon.stub(meeting, 'setupMediaConnectionListeners');
1914
+ sinon.stub(meeting, 'setMercuryListener');
1915
+ sinon
1916
+ .stub(meeting.roap, 'doTurnDiscovery')
1682
1917
  .resolves({turnServerInfo: {}, turnDiscoverySkippedReason: undefined});
1683
- meeting.waitForRemoteSDPAnswer = sinon.stub().resolves();
1918
+ sinon.stub(meeting, 'waitForRemoteSDPAnswer').resolves();
1919
+
1920
+ // normally the first Roap message we send is creating confluence, so mock LocusMediaRequest.isConfluenceCreated()
1921
+ // to return false the first time it's called and true the 2nd time, to simulate how it would happen for real
1922
+ meeting.locusMediaRequest = {
1923
+ isConfluenceCreated: sinon.stub().onFirstCall().returns(false).onSecondCall().returns(true)
1924
+ };
1684
1925
  });
1685
1926
 
1686
1927
  it('should have #addMedia', () => {
@@ -1778,6 +2019,7 @@ describe('plugin-meetings', () => {
1778
2019
  someReachabilityMetric2: 'some value2',
1779
2020
  selectedCandidatePairChanges: 2,
1780
2021
  numTransports: 1,
2022
+ iceCandidatesCount: 0,
1781
2023
  }
1782
2024
  );
1783
2025
  });
@@ -1885,6 +2127,7 @@ describe('plugin-meetings', () => {
1885
2127
  someReachabilityMetric2: 'some value2',
1886
2128
  selectedCandidatePairChanges: 2,
1887
2129
  numTransports: 1,
2130
+ iceCandidatesCount: 0,
1888
2131
  }
1889
2132
  );
1890
2133
  });
@@ -2028,6 +2271,61 @@ describe('plugin-meetings', () => {
2028
2271
  }
2029
2272
  });
2030
2273
 
2274
+ it('sends correct CA event when times out waiting for SDP answer', async () => {
2275
+ const eventListeners = {};
2276
+ const clock = sinon.useFakeTimers();
2277
+
2278
+ // these 2 are stubbed, we need the real versions:
2279
+ meeting.waitForRemoteSDPAnswer.restore();
2280
+ meeting.setupMediaConnectionListeners.restore();
2281
+
2282
+ meeting.meetingState = 'ACTIVE';
2283
+
2284
+ // setup a mock media connection that will trigger an offer when initiateOffer() is called
2285
+ Media.createMediaConnection = sinon.stub().returns({
2286
+ initiateOffer: sinon.stub().callsFake(() => {
2287
+ // simulate offer being generated
2288
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
2289
+
2290
+ return Promise.resolve();
2291
+ }),
2292
+ close: sinon.stub(),
2293
+ on: (event, listener) => {
2294
+ eventListeners[event] = listener;
2295
+ },
2296
+ forceRtcMetricsSend: sinon.stub().resolves(),
2297
+ });
2298
+
2299
+ const getErrorPayloadForClientErrorCodeStub =
2300
+ (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
2301
+ sinon
2302
+ .stub()
2303
+ .callsFake(({clientErrorCode}) => ({errorCode: clientErrorCode, fatal: true})));
2304
+
2305
+ const result = meeting.addMedia();
2306
+ await testUtils.flushPromises();
2307
+
2308
+ // simulate timeout waiting for the SDP answer that never comes
2309
+ await clock.tickAsync(ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT);
2310
+
2311
+ await assert.isRejected(result);
2312
+
2313
+ assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
2314
+ clientErrorCode: 2007,
2315
+ });
2316
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2317
+ name: 'client.media-engine.remote-sdp-received',
2318
+ payload: {
2319
+ canProceed: false,
2320
+ errors: [{errorCode: 2007, fatal: true}],
2321
+ },
2322
+ options: {
2323
+ meetingId: meeting.id,
2324
+ rawError: sinon.match.instanceOf(Error),
2325
+ },
2326
+ });
2327
+ });
2328
+
2031
2329
  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
2330
  meeting.meetingState = 'ACTIVE';
2033
2331
  // setup the mock to cause addMedia() to fail
@@ -2187,8 +2485,13 @@ describe('plugin-meetings', () => {
2187
2485
  it('should reject if waitForMediaConnectionConnected() rejects after turn server retry', async () => {
2188
2486
  const FAKE_ERROR = {fatal: true};
2189
2487
  const getErrorPayloadForClientErrorCodeStub =
2488
+
2190
2489
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
2191
2490
  sinon.stub().returns(FAKE_ERROR));
2491
+ webex.meetings.reachability = {
2492
+ isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2493
+ getReachabilityMetrics: sinon.stub().resolves(),
2494
+ };
2192
2495
  const MOCK_CLIENT_ERROR_CODE = 2004;
2193
2496
  const generateClientErrorCodeForIceFailureStub = sinon
2194
2497
  .stub(CallDiagnosticUtils, 'generateClientErrorCodeForIceFailure')
@@ -2216,7 +2519,7 @@ describe('plugin-meetings', () => {
2216
2519
  turnDiscoverySkippedReason: undefined,
2217
2520
  });
2218
2521
  meeting.meetingState = 'ACTIVE';
2219
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects(new Error('fake error'));
2522
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2220
2523
 
2221
2524
  const forceRtcMetricsSend = sinon.stub().resolves();
2222
2525
  const closeMediaConnectionStub = sinon.stub();
@@ -2240,13 +2543,15 @@ describe('plugin-meetings', () => {
2240
2543
  assert.calledTwice(generateClientErrorCodeForIceFailureStub);
2241
2544
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2242
2545
  signalingState: 'unknown',
2243
- iceConnectionState: 'unknown',
2546
+ iceConnected: false,
2244
2547
  turnServerUsed: false,
2548
+ unreachable: false,
2245
2549
  });
2246
2550
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2247
2551
  signalingState: 'unknown',
2248
- iceConnectionState: 'unknown',
2552
+ iceConnected: false,
2249
2553
  turnServerUsed: true,
2554
+ unreachable: false,
2250
2555
  });
2251
2556
 
2252
2557
  assert.calledTwice(getErrorPayloadForClientErrorCodeStub);
@@ -2364,6 +2669,7 @@ describe('plugin-meetings', () => {
2364
2669
  iceConnectionState: 'unknown',
2365
2670
  selectedCandidatePairChanges: 2,
2366
2671
  numTransports: 1,
2672
+ iceCandidatesCount: 0,
2367
2673
  },
2368
2674
  ]);
2369
2675
 
@@ -2371,7 +2677,7 @@ describe('plugin-meetings', () => {
2371
2677
  const doTurnDiscoveryCalls = meeting.roap.doTurnDiscovery.getCalls();
2372
2678
  assert.equal(doTurnDiscoveryCalls.length, 2);
2373
2679
  assert.deepEqual(doTurnDiscoveryCalls[0].args, [meeting, false, false]);
2374
- assert.deepEqual(doTurnDiscoveryCalls[1].args, [meeting, true, true]);
2680
+ assert.deepEqual(doTurnDiscoveryCalls[1].args.slice(1), [true, true]);
2375
2681
 
2376
2682
  // Some clean up steps happens twice
2377
2683
  assert.calledTwice(forceRtcMetricsSend);
@@ -2383,6 +2689,10 @@ describe('plugin-meetings', () => {
2383
2689
 
2384
2690
  it('should resolve if waitForMediaConnectionConnected() rejects the first time but resolves the second time', async () => {
2385
2691
  const FAKE_ERROR = {fatal: true};
2692
+ webex.meetings.reachability = {
2693
+ isWebexMediaBackendUnreachable: sinon.stub().onCall(0).rejects().onCall(1).resolves(true).onCall(2).resolves(false),
2694
+ getReachabilityMetrics: sinon.stub().resolves({}),
2695
+ }
2386
2696
  const getErrorPayloadForClientErrorCodeStub =
2387
2697
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
2388
2698
  sinon.stub().returns(FAKE_ERROR));
@@ -2440,8 +2750,9 @@ describe('plugin-meetings', () => {
2440
2750
  assert.calledOnce(generateClientErrorCodeForIceFailureStub);
2441
2751
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2442
2752
  signalingState: 'unknown',
2443
- iceConnectionState: 'unknown',
2753
+ iceConnected: undefined,
2444
2754
  turnServerUsed: false,
2755
+ unreachable: false,
2445
2756
  });
2446
2757
 
2447
2758
  assert.calledOnce(getErrorPayloadForClientErrorCodeStub);
@@ -2547,6 +2858,7 @@ describe('plugin-meetings', () => {
2547
2858
  isMultistream: false,
2548
2859
  retriedWithTurnServer: true,
2549
2860
  isJoinWithMediaRetry: false,
2861
+ iceCandidatesCount: 0,
2550
2862
  },
2551
2863
  ]);
2552
2864
  meeting.roap.doTurnDiscovery;
@@ -2675,6 +2987,8 @@ describe('plugin-meetings', () => {
2675
2987
  someReachabilityMetric2: 'some value2',
2676
2988
  }),
2677
2989
  };
2990
+ meeting.iceCandidatesCount = 3;
2991
+
2678
2992
  await meeting.addMedia({
2679
2993
  mediaSettings: {},
2680
2994
  });
@@ -2694,6 +3008,7 @@ describe('plugin-meetings', () => {
2694
3008
  isJoinWithMediaRetry: false,
2695
3009
  someReachabilityMetric1: 'some value1',
2696
3010
  someReachabilityMetric2: 'some value2',
3011
+ iceCandidatesCount: 3,
2697
3012
  }
2698
3013
  );
2699
3014
 
@@ -2715,7 +3030,63 @@ describe('plugin-meetings', () => {
2715
3030
  turnDiscoverySkippedReason: undefined,
2716
3031
  });
2717
3032
  meeting.meetingState = 'ACTIVE';
2718
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects(new Error('fake error'));
3033
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
3034
+
3035
+ const forceRtcMetricsSend = sinon.stub().resolves();
3036
+ const closeMediaConnectionStub = sinon.stub();
3037
+ Media.createMediaConnection = sinon.stub().returns({
3038
+ close: closeMediaConnectionStub,
3039
+ forceRtcMetricsSend,
3040
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
3041
+ initiateOffer: sinon.stub().resolves({}),
3042
+ on: sinon.stub(),
3043
+ });
3044
+
3045
+ await meeting
3046
+ .addMedia({
3047
+ mediaSettings: {},
3048
+ })
3049
+ .catch((err) => {
3050
+ errorThrown = err;
3051
+ assert.instanceOf(err, AddMediaFailed);
3052
+ });
3053
+
3054
+ // Check that the only metric sent is ADD_MEDIA_FAILURE
3055
+ assert.calledOnceWithExactly(
3056
+ Metrics.sendBehavioralMetric,
3057
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
3058
+ {
3059
+ correlation_id: meeting.correlationId,
3060
+ locus_id: meeting.locusUrl.split('/').pop(),
3061
+ reason: errorThrown.message,
3062
+ stack: errorThrown.stack,
3063
+ code: errorThrown.code,
3064
+ turnDiscoverySkippedReason: undefined,
3065
+ turnServerUsed: true,
3066
+ retriedWithTurnServer: false,
3067
+ isMultistream: false,
3068
+ isJoinWithMediaRetry: false,
3069
+ signalingState: 'unknown',
3070
+ connectionState: 'unknown',
3071
+ iceConnectionState: 'unknown',
3072
+ selectedCandidatePairChanges: 2,
3073
+ numTransports: 1,
3074
+ iceCandidatesCount: 0,
3075
+ }
3076
+ );
3077
+
3078
+ assert.isOk(errorThrown);
3079
+ });
3080
+
3081
+ it('should send ICE_CANDIDATE_ERROR metric if media connection fails and ice candidate errors have been gathered', async () => {
3082
+ let errorThrown = undefined;
3083
+
3084
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
3085
+ turnServerInfo: undefined,
3086
+ turnDiscoverySkippedReason: undefined,
3087
+ });
3088
+ meeting.meetingState = 'ACTIVE';
3089
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2719
3090
 
2720
3091
  const forceRtcMetricsSend = sinon.stub().resolves();
2721
3092
  const closeMediaConnectionStub = sinon.stub();
@@ -2727,6 +3098,9 @@ describe('plugin-meetings', () => {
2727
3098
  on: sinon.stub(),
2728
3099
  });
2729
3100
 
3101
+ meeting.iceCandidateErrors.set('701_error', 2);
3102
+ meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
3103
+
2730
3104
  await meeting
2731
3105
  .addMedia({
2732
3106
  mediaSettings: {},
@@ -2756,6 +3130,9 @@ describe('plugin-meetings', () => {
2756
3130
  iceConnectionState: 'unknown',
2757
3131
  selectedCandidatePairChanges: 2,
2758
3132
  numTransports: 1,
3133
+ '701_error': 2,
3134
+ '701_turn_host_lookup_received_error': 1,
3135
+ iceCandidatesCount: 0,
2759
3136
  }
2760
3137
  );
2761
3138
 
@@ -2775,7 +3152,7 @@ describe('plugin-meetings', () => {
2775
3152
 
2776
3153
  statsAnalyzerStub = new EventsScope();
2777
3154
  // mock the StatsAnalyzer constructor
2778
- sinon.stub(StatsAnalyzerModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
3155
+ sinon.stub(InternalMediaCoreModule, 'StatsAnalyzer').returns(statsAnalyzerStub);
2779
3156
 
2780
3157
  await meeting.addMedia({
2781
3158
  mediaSettings: {},
@@ -2789,8 +3166,8 @@ describe('plugin-meetings', () => {
2789
3166
  it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and sends metrics', async () => {
2790
3167
  statsAnalyzerStub.emit(
2791
3168
  {file: 'test', function: 'test'},
2792
- StatsAnalyzerModule.EVENTS.LOCAL_MEDIA_STARTED,
2793
- {type: 'audio'}
3169
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED,
3170
+ {mediaType: 'audio'}
2794
3171
  );
2795
3172
 
2796
3173
  assert.calledWith(
@@ -2802,7 +3179,7 @@ describe('plugin-meetings', () => {
2802
3179
  },
2803
3180
  EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED,
2804
3181
  {
2805
- type: 'audio',
3182
+ mediaType: 'audio',
2806
3183
  }
2807
3184
  );
2808
3185
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2817,8 +3194,8 @@ describe('plugin-meetings', () => {
2817
3194
  it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => {
2818
3195
  statsAnalyzerStub.emit(
2819
3196
  {file: 'test', function: 'test'},
2820
- StatsAnalyzerModule.EVENTS.LOCAL_MEDIA_STOPPED,
2821
- {type: 'video'}
3197
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED,
3198
+ {mediaType: 'video'}
2822
3199
  );
2823
3200
 
2824
3201
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2833,8 +3210,8 @@ describe('plugin-meetings', () => {
2833
3210
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => {
2834
3211
  statsAnalyzerStub.emit(
2835
3212
  {file: 'test', function: 'test'},
2836
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STARTED,
2837
- {type: 'video'}
3213
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3214
+ {mediaType: 'video'}
2838
3215
  );
2839
3216
 
2840
3217
  assert.calledWith(
@@ -2846,7 +3223,7 @@ describe('plugin-meetings', () => {
2846
3223
  },
2847
3224
  EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
2848
3225
  {
2849
- type: 'video',
3226
+ mediaType: 'video',
2850
3227
  }
2851
3228
  );
2852
3229
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2861,8 +3238,8 @@ describe('plugin-meetings', () => {
2861
3238
  it('REMOTE_MEDIA_STOPPED triggers the right metrics', async () => {
2862
3239
  statsAnalyzerStub.emit(
2863
3240
  {file: 'test', function: 'test'},
2864
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STOPPED,
2865
- {type: 'audio'}
3241
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3242
+ {mediaType: 'audio'}
2866
3243
  );
2867
3244
 
2868
3245
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2877,8 +3254,8 @@ describe('plugin-meetings', () => {
2877
3254
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics for share', async () => {
2878
3255
  statsAnalyzerStub.emit(
2879
3256
  {file: 'test', function: 'test'},
2880
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STARTED,
2881
- {type: 'share'}
3257
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3258
+ {mediaType: 'share'}
2882
3259
  );
2883
3260
 
2884
3261
  assert.calledWith(
@@ -2890,7 +3267,7 @@ describe('plugin-meetings', () => {
2890
3267
  },
2891
3268
  EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
2892
3269
  {
2893
- type: 'share',
3270
+ mediaType: 'share',
2894
3271
  }
2895
3272
  );
2896
3273
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2913,8 +3290,8 @@ describe('plugin-meetings', () => {
2913
3290
  it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
2914
3291
  statsAnalyzerStub.emit(
2915
3292
  {file: 'test', function: 'test'},
2916
- StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STOPPED,
2917
- {type: 'share'}
3293
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3294
+ {mediaType: 'share'}
2918
3295
  );
2919
3296
 
2920
3297
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -2935,19 +3312,18 @@ describe('plugin-meetings', () => {
2935
3312
  });
2936
3313
 
2937
3314
  it('calls submitMQE correctly', async () => {
2938
- const fakeData = {intervalMetadata: {bla: 'bla'}};
3315
+ const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
2939
3316
 
2940
3317
  statsAnalyzerStub.emit(
2941
3318
  {file: 'test', function: 'test'},
2942
- StatsAnalyzerModule.EVENTS.MEDIA_QUALITY,
2943
- {data: fakeData, networkType: 'wifi'}
3319
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3320
+ {data: fakeData}
2944
3321
  );
2945
3322
 
2946
3323
  assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
2947
3324
  name: 'client.mediaquality.event',
2948
3325
  options: {
2949
3326
  meetingId: meeting.id,
2950
- networkType: 'wifi',
2951
3327
  },
2952
3328
  payload: {
2953
3329
  intervals: [fakeData],
@@ -3004,7 +3380,7 @@ describe('plugin-meetings', () => {
3004
3380
  it('succeeds even if getDevices() throws', async () => {
3005
3381
  meeting.meetingState = 'ACTIVE';
3006
3382
 
3007
- sinon.stub(internalMediaModule, 'getDevices').rejects(new Error('fake error'));
3383
+ sinon.stub(InternalMediaCoreModule, 'getDevices').rejects(new Error('fake error'));
3008
3384
 
3009
3385
  await meeting.addMedia();
3010
3386
  });
@@ -3021,7 +3397,7 @@ describe('plugin-meetings', () => {
3021
3397
  clientErrorCode: MISSING_ROAP_ANSWER_CLIENT_CODE,
3022
3398
  expectedErrorPayload: {
3023
3399
  errorDescription: ERROR_DESCRIPTIONS.MISSING_ROAP_ANSWER,
3024
- category: 'signaling',
3400
+ category: 'media',
3025
3401
  },
3026
3402
  },
3027
3403
  {
@@ -3040,10 +3416,18 @@ describe('plugin-meetings', () => {
3040
3416
  clientErrorCode: ICE_FAILED_WITH_TURN_TLS_CLIENT_CODE,
3041
3417
  expectedErrorPayload: {
3042
3418
  errorDescription: ERROR_DESCRIPTIONS.ICE_FAILED_WITH_TURN_TLS,
3043
- category: 'network',
3419
+ category: 'media',
3420
+ },
3421
+ },
3422
+ {
3423
+ clientErrorCode: ICE_AND_REACHABILITY_FAILED_CLIENT_CODE,
3424
+ unreachable: true,
3425
+ expectedErrorPayload: {
3426
+ errorDescription: ERROR_DESCRIPTIONS.ICE_AND_REACHABILITY_FAILED,
3427
+ category: 'expected',
3044
3428
  },
3045
3429
  },
3046
- ].forEach(({clientErrorCode, expectedErrorPayload}) => {
3430
+ ].forEach(({clientErrorCode, expectedErrorPayload, unreachable}) => {
3047
3431
  it(`should handle all ice failures correctly for ${clientErrorCode}`, async () => {
3048
3432
  // setting the method to the real implementation
3049
3433
  // because newMetrics is mocked completely in the webex-mock
@@ -3052,13 +3436,17 @@ describe('plugin-meetings', () => {
3052
3436
  webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
3053
3437
  CD.getErrorPayloadForClientErrorCode;
3054
3438
 
3439
+ webex.meetings.reachability = {
3440
+ isWebexMediaBackendUnreachable: sinon.stub().resolves(unreachable || false),
3441
+ };
3442
+
3055
3443
  const generateClientErrorCodeForIceFailureStub = sinon
3056
3444
  .stub(CallDiagnosticUtils, 'generateClientErrorCodeForIceFailure')
3057
3445
  .returns(clientErrorCode);
3058
3446
 
3059
3447
  meeting.meetingState = 'ACTIVE';
3060
3448
  meeting.mediaProperties.waitForMediaConnectionConnected.rejects(
3061
- new Error('fake error')
3449
+ {iceConnected: false}
3062
3450
  );
3063
3451
 
3064
3452
  let errorThrown = false;
@@ -3073,8 +3461,9 @@ describe('plugin-meetings', () => {
3073
3461
 
3074
3462
  assert.calledOnceWithExactly(generateClientErrorCodeForIceFailureStub, {
3075
3463
  signalingState: 'unknown',
3076
- iceConnectionState: 'unknown',
3464
+ iceConnected: false,
3077
3465
  turnServerUsed: true,
3466
+ unreachable: unreachable || false,
3078
3467
  });
3079
3468
 
3080
3469
  const submitClientEventCalls = webex.internal.newMetrics.submitClientEvent.getCalls();
@@ -3162,7 +3551,7 @@ describe('plugin-meetings', () => {
3162
3551
 
3163
3552
  let clock;
3164
3553
 
3165
- beforeEach(() => {
3554
+ beforeEach(async () => {
3166
3555
  clock = sinon.useFakeTimers();
3167
3556
 
3168
3557
  sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.unknown);
@@ -3171,7 +3560,6 @@ describe('plugin-meetings', () => {
3171
3560
  meeting.config.deviceType = 'web';
3172
3561
  meeting.isMultistream = isMultistream;
3173
3562
  meeting.meetingState = 'ACTIVE';
3174
- meeting.mediaId = 'fake media id';
3175
3563
  meeting.selfUrl = 'selfUrl';
3176
3564
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
3177
3565
  meeting.mediaProperties.getCurrentConnectionInfo = sinon.stub().resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
@@ -3261,16 +3649,30 @@ describe('plugin-meetings', () => {
3261
3649
  };
3262
3650
 
3263
3651
  roapMediaConnectionConstructorStub = sinon
3264
- .stub(internalMediaModule, 'RoapMediaConnection')
3652
+ .stub(InternalMediaCoreModule, 'RoapMediaConnection')
3265
3653
  .returns(fakeRoapMediaConnection);
3266
3654
 
3267
3655
  multistreamRoapMediaConnectionConstructorStub = sinon
3268
- .stub(internalMediaModule, 'MultistreamRoapMediaConnection')
3656
+ .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
3269
3657
  .returns(fakeMultistreamRoapMediaConnection);
3270
3658
 
3271
3659
  locusMediaRequestStub = sinon
3272
3660
  .stub(WebexPlugin.prototype, 'request')
3273
3661
  .resolves({body: {locus: {fullState: {}}}});
3662
+
3663
+ // setup some things and mocks so that the call to join() works
3664
+ // (we need to call join() because it creates the LocusMediaRequest instance
3665
+ // that's being tested in these tests)
3666
+ meeting.webex.meetings.registered = true;
3667
+ meeting.webex.internal.device.config = {};
3668
+ sinon
3669
+ .stub(MeetingUtil, 'joinMeeting')
3670
+ .resolves({
3671
+ id: 'fake locus from mocked join request',
3672
+ locusUrl: 'fake locus url',
3673
+ mediaId: 'fake media id',
3674
+ });
3675
+ await meeting.join({enableMultistream: isMultistream});
3274
3676
  });
3275
3677
 
3276
3678
  afterEach(() => {
@@ -3299,13 +3701,13 @@ describe('plugin-meetings', () => {
3299
3701
 
3300
3702
  for (let idx = 0; idx < roapMediaConnectionToCheck.on.callCount; idx += 1) {
3301
3703
  if (
3302
- roapMediaConnectionToCheck.on.getCall(idx).args[0] === Event.ROAP_MESSAGE_TO_SEND
3704
+ roapMediaConnectionToCheck.on.getCall(idx).args[0] === MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND
3303
3705
  ) {
3304
3706
  return roapMediaConnectionToCheck.on.getCall(idx).args[1];
3305
3707
  }
3306
3708
  }
3307
3709
  assert.fail(
3308
- 'listener for "roap:messageToSend" (Event.ROAP_MESSAGE_TO_SEND) was not registered'
3710
+ 'listener for "roap:messageToSend" (MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND) was not registered'
3309
3711
  );
3310
3712
  };
3311
3713
 
@@ -5107,7 +5509,7 @@ describe('plugin-meetings', () => {
5107
5509
 
5108
5510
  describe('#fetchMeetingInfo', () => {
5109
5511
  const FAKE_DESTINATION = 'something@somecompany.com';
5110
- const FAKE_TYPE = _SIP_URI_;
5512
+ const FAKE_TYPE = DESTINATION_TYPE.SIP_URI;
5111
5513
  const FAKE_TIMEOUT_FETCHMEETINGINFO_ID = '123456';
5112
5514
  const FAKE_PASSWORD = '123abc';
5113
5515
  const FAKE_CAPTCHA_CODE = 'a1b2c3XYZ';
@@ -5542,7 +5944,7 @@ describe('plugin-meetings', () => {
5542
5944
  const FAKE_PASSWORD = '123456';
5543
5945
  const FAKE_CAPTCHA_CODE = '654321';
5544
5946
  const FAKE_DESTINATION = 'something@somecompany.com';
5545
- const FAKE_TYPE = _SIP_URI_;
5947
+ const FAKE_TYPE = DESTINATION_TYPE.SIP_URI;
5546
5948
  const FAKE_INSTALLED_ORG_ID = '123456';
5547
5949
  const FAKE_MEETING_INFO_LOOKUP_URL = 'meetingLookupUrl';
5548
5950
 
@@ -6275,7 +6677,7 @@ describe('plugin-meetings', () => {
6275
6677
  },
6276
6678
  'SELF_OBSERVING'
6277
6679
  );
6278
-
6680
+
6279
6681
 
6280
6682
  // Verify that the event handler behaves as expected
6281
6683
  expect(meeting.statsAnalyzer.stopAnalyzer.calledOnce).to.be.true;
@@ -7079,6 +7481,10 @@ describe('plugin-meetings', () => {
7079
7481
  id: 'stream',
7080
7482
  getTracks: () => [{id: 'track', addEventListener: sinon.stub()}],
7081
7483
  };
7484
+ const simulateConnectionStateChange = (newState) => {
7485
+ meeting.mediaProperties.webrtcMediaConnection.getConnectionState = sinon.stub().returns(newState);
7486
+ eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]();
7487
+ }
7082
7488
 
7083
7489
  beforeEach(() => {
7084
7490
  eventListeners = {};
@@ -7088,23 +7494,27 @@ describe('plugin-meetings', () => {
7088
7494
  on: sinon.stub().callsFake((event, listener) => {
7089
7495
  eventListeners[event] = listener;
7090
7496
  }),
7497
+ getConnectionState: sinon.stub().returns(ConnectionState.New),
7091
7498
  };
7092
7499
  MediaUtil.createMediaStream.returns(fakeStream);
7093
7500
  });
7094
7501
 
7095
7502
  it('should register for all the correct RoapMediaConnection events', () => {
7096
7503
  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]);
7504
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_STARTED]);
7505
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_DONE]);
7506
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_FAILURE]);
7507
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]);
7508
+ assert.isFunction(eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]);
7509
+ assert.isFunction(eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]);
7510
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED]);
7511
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]);
7512
+ assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]);
7103
7513
  });
7104
7514
 
7105
7515
  it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => {
7106
7516
  meeting.setupMediaConnectionListeners();
7107
- eventListeners[Event.REMOTE_TRACK_ADDED]({
7517
+ eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
7108
7518
  track: 'track',
7109
7519
  type: RemoteTrackType.AUDIO,
7110
7520
  });
@@ -7114,7 +7524,7 @@ describe('plugin-meetings', () => {
7114
7524
  stream: fakeStream,
7115
7525
  });
7116
7526
 
7117
- eventListeners[Event.REMOTE_TRACK_ADDED]({
7527
+ eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
7118
7528
  track: 'track',
7119
7529
  type: RemoteTrackType.VIDEO,
7120
7530
  });
@@ -7124,7 +7534,7 @@ describe('plugin-meetings', () => {
7124
7534
  stream: fakeStream,
7125
7535
  });
7126
7536
 
7127
- eventListeners[Event.REMOTE_TRACK_ADDED]({
7537
+ eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]({
7128
7538
  track: 'track',
7129
7539
  type: RemoteTrackType.SCREENSHARE_VIDEO,
7130
7540
  });
@@ -7135,13 +7545,62 @@ describe('plugin-meetings', () => {
7135
7545
  });
7136
7546
  });
7137
7547
 
7548
+ describe('should react on a ICE_CANDIDATE event', () => {
7549
+ beforeEach(() => {
7550
+ meeting.setupMediaConnectionListeners();
7551
+ });
7552
+
7553
+ it('should collect ice candidates', () => {
7554
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: 'candidate'});
7555
+
7556
+ assert.equal(meeting.iceCandidatesCount, 1);
7557
+ });
7558
+
7559
+ it('should not collect null ice candidates', () => {
7560
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: null});
7561
+
7562
+ assert.equal(meeting.iceCandidatesCount, 0);
7563
+ });
7564
+ });
7565
+
7566
+ describe('should react on a ICE_CANDIDATE_ERROR event', () => {
7567
+ beforeEach(() => {
7568
+ meeting.setupMediaConnectionListeners();
7569
+ });
7570
+
7571
+ it('should not collect skipped ice candidates error', () => {
7572
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 600, errorText: 'Address not associated with the desired network interface.' }});
7573
+
7574
+ assert.equal(meeting.iceCandidateErrors.size, 0);
7575
+ });
7576
+
7577
+ it('should collect valid ice candidates error', () => {
7578
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }});
7579
+
7580
+ assert.equal(meeting.iceCandidateErrors.size, 1);
7581
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7582
+ });
7583
+
7584
+ it('should increment counter if same valid ice candidates error collected', () => {
7585
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }});
7586
+
7587
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }});
7588
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }});
7589
+
7590
+ assert.equal(meeting.iceCandidateErrors.size, 2);
7591
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7592
+ assert.equal(meeting.iceCandidateErrors.get('701_'), 1);
7593
+ assert.equal(meeting.iceCandidateErrors.has('701_stun_host_lookup_received_error'), true);
7594
+ assert.equal(meeting.iceCandidateErrors.get('701_stun_host_lookup_received_error'), 2);
7595
+ });
7596
+ });
7597
+
7138
7598
  describe('CONNECTION_STATE_CHANGED event when state = "Connecting"', () => {
7139
7599
  it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = true', () => {
7140
7600
  meeting.hasMediaConnectionConnectedAtLeastOnce = true;
7141
7601
  meeting.setupMediaConnectionListeners();
7142
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7143
- state: 'Connecting',
7144
- });
7602
+
7603
+ simulateConnectionStateChange(ConnectionState.Connecting);
7145
7604
 
7146
7605
  assert.notCalled(webex.internal.newMetrics.submitClientEvent);
7147
7606
  });
@@ -7149,9 +7608,8 @@ describe('plugin-meetings', () => {
7149
7608
  it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = false', () => {
7150
7609
  meeting.hasMediaConnectionConnectedAtLeastOnce = false;
7151
7610
  meeting.setupMediaConnectionListeners();
7152
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7153
- state: 'Connecting',
7154
- });
7611
+
7612
+ simulateConnectionStateChange(ConnectionState.Connecting);
7155
7613
 
7156
7614
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7157
7615
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7177,6 +7635,7 @@ describe('plugin-meetings', () => {
7177
7635
  on: sinon.stub().callsFake((event, listener) => {
7178
7636
  eventListeners[event] = listener;
7179
7637
  }),
7638
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
7180
7639
  };
7181
7640
  };
7182
7641
 
@@ -7230,9 +7689,7 @@ describe('plugin-meetings', () => {
7230
7689
  assert.equal(meeting.hasMediaConnectionConnectedAtLeastOnce, false);
7231
7690
 
7232
7691
  // simulate first connection success
7233
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7234
- state: 'Connected',
7235
- });
7692
+ simulateConnectionStateChange(ConnectionState.Connected);
7236
7693
  checkExpectedSpies({
7237
7694
  icePhase: 'JOIN_MEETING_FINAL',
7238
7695
  setNetworkStatusCallParams: [NETWORK_STATUS.CONNECTED],
@@ -7242,12 +7699,9 @@ describe('plugin-meetings', () => {
7242
7699
  // now simulate short connection loss, client.ice.end is not sent a second time as hasMediaConnectionConnectedAtLeastOnce = true
7243
7700
  resetSpies();
7244
7701
 
7245
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7246
- state: 'Disconnected',
7247
- });
7248
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7249
- state: 'Connected',
7250
- });
7702
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7703
+
7704
+ simulateConnectionStateChange(ConnectionState.Connected);
7251
7705
 
7252
7706
  checkExpectedSpies({
7253
7707
  setNetworkStatusCallParams: [NETWORK_STATUS.DISCONNECTED, NETWORK_STATUS.CONNECTED],
@@ -7255,12 +7709,9 @@ describe('plugin-meetings', () => {
7255
7709
 
7256
7710
  resetSpies();
7257
7711
 
7258
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7259
- state: 'Disconnected',
7260
- });
7261
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7262
- state: 'Connected',
7263
- });
7712
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7713
+
7714
+ simulateConnectionStateChange(ConnectionState.Connected);
7264
7715
  });
7265
7716
  });
7266
7717
 
@@ -7282,9 +7733,8 @@ describe('plugin-meetings', () => {
7282
7733
 
7283
7734
  const mockDisconnectedEvent = () => {
7284
7735
  meeting.setupMediaConnectionListeners();
7285
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7286
- state: 'Disconnected',
7287
- });
7736
+
7737
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7288
7738
  };
7289
7739
 
7290
7740
  const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
@@ -7348,9 +7798,8 @@ describe('plugin-meetings', () => {
7348
7798
  describe('CONNECTION_STATE_CHANGED event when state = "Failed"', () => {
7349
7799
  const mockFailedEvent = () => {
7350
7800
  meeting.setupMediaConnectionListeners();
7351
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7352
- state: 'Failed',
7353
- });
7801
+
7802
+ simulateConnectionStateChange(ConnectionState.Failed);
7354
7803
  };
7355
7804
 
7356
7805
  const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
@@ -7432,7 +7881,7 @@ describe('plugin-meetings', () => {
7432
7881
  cause: {name: fakeRootCauseName},
7433
7882
  });
7434
7883
 
7435
- eventListeners[Event.ROAP_FAILURE](fakeError);
7884
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7436
7885
 
7437
7886
  checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
7438
7887
  checkBehavioralMetricSent(
@@ -7449,7 +7898,7 @@ describe('plugin-meetings', () => {
7449
7898
  cause: {name: fakeRootCauseName},
7450
7899
  });
7451
7900
 
7452
- eventListeners[Event.ROAP_FAILURE](fakeError);
7901
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7453
7902
 
7454
7903
  checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
7455
7904
  checkBehavioralMetricSent(
@@ -7466,7 +7915,7 @@ describe('plugin-meetings', () => {
7466
7915
  cause: {name: fakeRootCauseName},
7467
7916
  });
7468
7917
 
7469
- eventListeners[Event.ROAP_FAILURE](fakeError);
7918
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7470
7919
 
7471
7920
  checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
7472
7921
  checkBehavioralMetricSent(
@@ -7481,7 +7930,7 @@ describe('plugin-meetings', () => {
7481
7930
  // SdpError is usually without a cause
7482
7931
  const fakeError = new Errors.SdpError(fakeErrorMessage, {name: fakeErrorName});
7483
7932
 
7484
- eventListeners[Event.ROAP_FAILURE](fakeError);
7933
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7485
7934
 
7486
7935
  checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
7487
7936
  // expectedMetadataType is the error name in this case
@@ -7499,7 +7948,7 @@ describe('plugin-meetings', () => {
7499
7948
  name: fakeErrorName,
7500
7949
  });
7501
7950
 
7502
- eventListeners[Event.ROAP_FAILURE](fakeError);
7951
+ eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
7503
7952
 
7504
7953
  checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
7505
7954
  // expectedMetadataType is the error name in this case
@@ -7525,7 +7974,7 @@ describe('plugin-meetings', () => {
7525
7974
  };
7526
7975
  meeting.sdpResponseTimer = '1234';
7527
7976
 
7528
- eventListeners[Event.REMOTE_SDP_ANSWER_PROCESSED]();
7977
+ eventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED]();
7529
7978
 
7530
7979
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7531
7980
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7553,7 +8002,7 @@ describe('plugin-meetings', () => {
7553
8002
  it('handles LOCAL_SDP_OFFER_GENERATED correctly', () => {
7554
8003
  assert.equal(meeting.deferSDPAnswer, undefined);
7555
8004
 
7556
- eventListeners[Event.LOCAL_SDP_OFFER_GENERATED]();
8005
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
7557
8006
 
7558
8007
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7559
8008
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7565,7 +8014,7 @@ describe('plugin-meetings', () => {
7565
8014
  });
7566
8015
 
7567
8016
  it('handles LOCAL_SDP_ANSWER_GENERATED correctly', () => {
7568
- eventListeners[Event.LOCAL_SDP_ANSWER_GENERATED]();
8017
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_ANSWER_GENERATED]();
7569
8018
 
7570
8019
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7571
8020
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7575,7 +8024,7 @@ describe('plugin-meetings', () => {
7575
8024
  });
7576
8025
  });
7577
8026
 
7578
- describe('handles Event.ROAP_MESSAGE_TO_SEND correctly', () => {
8027
+ describe('handles MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND correctly', () => {
7579
8028
  let sendRoapOKStub;
7580
8029
  let sendRoapMediaRequestStub;
7581
8030
  let sendRoapAnswerStub;
@@ -7593,7 +8042,7 @@ describe('plugin-meetings', () => {
7593
8042
  });
7594
8043
 
7595
8044
  it('handles OK message correctly', () => {
7596
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8045
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7597
8046
  roapMessage: {messageType: 'OK', seq: 1},
7598
8047
  });
7599
8048
 
@@ -7608,7 +8057,7 @@ describe('plugin-meetings', () => {
7608
8057
  it('handles OFFER message correctly (no answer in the http response)', async () => {
7609
8058
  sinon.stub(meeting, 'roapMessageReceived');
7610
8059
 
7611
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8060
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7612
8061
  roapMessage: {
7613
8062
  messageType: 'OFFER',
7614
8063
  seq: 1,
@@ -7634,7 +8083,7 @@ describe('plugin-meetings', () => {
7634
8083
  sendRoapMediaRequestStub.resolves({roapAnswer: fakeAnswer});
7635
8084
  sinon.stub(meeting, 'roapMessageReceived');
7636
8085
 
7637
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8086
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7638
8087
  roapMessage: {
7639
8088
  messageType: 'OFFER',
7640
8089
  seq: 1,
@@ -7656,14 +8105,20 @@ describe('plugin-meetings', () => {
7656
8105
  });
7657
8106
 
7658
8107
  it('handles OFFER message correctly when request fails', async () => {
8108
+ const fakeError = new Error('fake error');
7659
8109
  const clock = sinon.useFakeTimers();
7660
8110
  sinon.spy(clock, 'clearTimeout');
7661
8111
  meeting.deferSDPAnswer = {reject: sinon.stub()};
7662
8112
  meeting.sdpResponseTimer = '1234';
7663
- sendRoapMediaRequestStub.rejects();
8113
+ sendRoapMediaRequestStub.rejects(fakeError);
7664
8114
  sinon.stub(meeting, 'roapMessageReceived');
8115
+ const getErrorPayloadForClientErrorCodeStub =
8116
+ (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
8117
+ sinon
8118
+ .stub()
8119
+ .callsFake(({clientErrorCode}) => ({errorCode: clientErrorCode, fatal: true})));
7665
8120
 
7666
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8121
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7667
8122
  roapMessage: {
7668
8123
  messageType: 'OFFER',
7669
8124
  seq: 1,
@@ -7686,10 +8141,25 @@ describe('plugin-meetings', () => {
7686
8141
  assert.calledOnce(clock.clearTimeout);
7687
8142
  assert.calledWith(clock.clearTimeout, '1234');
7688
8143
  assert.equal(meeting.sdpResponseTimer, undefined);
8144
+
8145
+ assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
8146
+ clientErrorCode: 2007,
8147
+ });
8148
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8149
+ name: 'client.media-engine.remote-sdp-received',
8150
+ payload: {
8151
+ canProceed: false,
8152
+ errors: [{errorCode: 2007, fatal: true}],
8153
+ },
8154
+ options: {
8155
+ meetingId: meeting.id,
8156
+ rawError: fakeError,
8157
+ },
8158
+ });
7689
8159
  });
7690
8160
 
7691
8161
  it('handles ANSWER message correctly', () => {
7692
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8162
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7693
8163
  roapMessage: {
7694
8164
  messageType: 'ANSWER',
7695
8165
  seq: 10,
@@ -7710,7 +8180,7 @@ describe('plugin-meetings', () => {
7710
8180
  it('sends metrics if fails to send roap ANSWER message', async () => {
7711
8181
  sendRoapAnswerStub.rejects(new Error('sending answer failed'));
7712
8182
 
7713
- await eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8183
+ await eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7714
8184
  roapMessage: {
7715
8185
  messageType: 'ANSWER',
7716
8186
  seq: 10,
@@ -7734,7 +8204,7 @@ describe('plugin-meetings', () => {
7734
8204
 
7735
8205
  [ErrorType.CONFLICT, ErrorType.DOUBLECONFLICT].forEach((errorType) =>
7736
8206
  it(`handles ERROR message indicating glare condition correctly (errorType=${errorType})`, () => {
7737
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8207
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7738
8208
  roapMessage: {
7739
8209
  messageType: 'ERROR',
7740
8210
  seq: 10,
@@ -7765,7 +8235,7 @@ describe('plugin-meetings', () => {
7765
8235
  );
7766
8236
 
7767
8237
  it('handles ERROR message indicating other errors correctly', () => {
7768
- eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
8238
+ eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]({
7769
8239
  roapMessage: {
7770
8240
  messageType: 'ERROR',
7771
8241
  seq: 10,
@@ -7793,8 +8263,8 @@ describe('plugin-meetings', () => {
7793
8263
  });
7794
8264
 
7795
8265
  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]);
8266
+ assert.isFunction(eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED]);
8267
+ assert.isFunction(eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED]);
7798
8268
  });
7799
8269
 
7800
8270
  it('forwards the VIDEO_SOURCES_COUNT_CHANGED event as "media:remoteVideoSourceCountChanged"', () => {
@@ -7804,7 +8274,7 @@ describe('plugin-meetings', () => {
7804
8274
 
7805
8275
  sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources');
7806
8276
 
7807
- eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](
8277
+ eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
7808
8278
  numTotalSources,
7809
8279
  numLiveSources,
7810
8280
  mediaContent
@@ -7828,7 +8298,7 @@ describe('plugin-meetings', () => {
7828
8298
  const numLiveSources = 2;
7829
8299
  const mediaContent = 'MAIN';
7830
8300
 
7831
- eventListeners[Event.AUDIO_SOURCES_COUNT_CHANGED](
8301
+ eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED](
7832
8302
  numTotalSources,
7833
8303
  numLiveSources,
7834
8304
  mediaContent
@@ -7856,7 +8326,7 @@ describe('plugin-meetings', () => {
7856
8326
  'setNumCurrentSources'
7857
8327
  );
7858
8328
 
7859
- eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](
8329
+ eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
7860
8330
  numTotalSources,
7861
8331
  numLiveSources,
7862
8332
  'MAIN'
@@ -7874,7 +8344,7 @@ describe('plugin-meetings', () => {
7874
8344
  'setNumCurrentSources'
7875
8345
  );
7876
8346
 
7877
- eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](
8347
+ eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED](
7878
8348
  numTotalSources,
7879
8349
  numLiveSources,
7880
8350
  'SLIDES'
@@ -9781,6 +10251,7 @@ describe('plugin-meetings', () => {
9781
10251
  beforeEach(() => {
9782
10252
  webex.internal.llm.isConnected = sinon.stub().returns(false);
9783
10253
  webex.internal.llm.getLocusUrl = sinon.stub();
10254
+ webex.internal.llm.getDatachannelUrl = sinon.stub();
9784
10255
  webex.internal.llm.registerAndConnect = sinon
9785
10256
  .stub()
9786
10257
  .returns(Promise.resolve('something'));
@@ -9808,6 +10279,7 @@ describe('plugin-meetings', () => {
9808
10279
  meeting.joinedWith = {state: 'JOINED'};
9809
10280
  webex.internal.llm.isConnected.returns(true);
9810
10281
  webex.internal.llm.getLocusUrl.returns('a url');
10282
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9811
10283
 
9812
10284
  meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
9813
10285
 
@@ -9844,6 +10316,7 @@ describe('plugin-meetings', () => {
9844
10316
  meeting.joinedWith = {state: 'JOINED'};
9845
10317
  webex.internal.llm.isConnected.returns(true);
9846
10318
  webex.internal.llm.getLocusUrl.returns('a url');
10319
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9847
10320
 
9848
10321
  meeting.locusInfo = {url: 'a different url', info: {datachannelUrl: 'a datachannel url'}};
9849
10322
 
@@ -9869,6 +10342,36 @@ describe('plugin-meetings', () => {
9869
10342
  );
9870
10343
  });
9871
10344
 
10345
+ it('disconnects if first if the data channel url has changed', async () => {
10346
+ meeting.joinedWith = {state: 'JOINED'};
10347
+ webex.internal.llm.isConnected.returns(true);
10348
+ webex.internal.llm.getLocusUrl.returns('a url');
10349
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
10350
+
10351
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a different datachannel url'}};
10352
+
10353
+ const result = await meeting.updateLLMConnection();
10354
+
10355
+ assert.calledWith(webex.internal.llm.disconnectLLM);
10356
+ assert.calledWith(
10357
+ webex.internal.llm.registerAndConnect,
10358
+ 'a url',
10359
+ 'a different datachannel url'
10360
+ );
10361
+ assert.equal(result, 'something');
10362
+ assert.calledWithExactly(
10363
+ meeting.webex.internal.llm.off,
10364
+ 'event:relay.event',
10365
+ meeting.processRelayEvent
10366
+ );
10367
+ assert.calledTwice(meeting.webex.internal.llm.off);
10368
+ assert.calledOnceWithExactly(
10369
+ meeting.webex.internal.llm.on,
10370
+ 'event:relay.event',
10371
+ meeting.processRelayEvent
10372
+ );
10373
+ });
10374
+
9872
10375
  it('disconnects when the state is not JOINED', async () => {
9873
10376
  meeting.joinedWith = {state: 'any other state'};
9874
10377
  webex.internal.llm.isConnected.returns(true);