@webex/plugin-meetings 3.0.0 → 3.1.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 (138) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.js +2 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/constants.d.ts +5 -4
  7. package/dist/constants.js +8 -4
  8. package/dist/constants.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js +6 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/interpretation/index.js +16 -2
  13. package/dist/interpretation/index.js.map +1 -1
  14. package/dist/interpretation/siLanguage.js +1 -1
  15. package/dist/locus-info/mediaSharesUtils.js +15 -1
  16. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  17. package/dist/locus-info/selfUtils.js +5 -0
  18. package/dist/locus-info/selfUtils.js.map +1 -1
  19. package/dist/media/MediaConnectionAwaiter.d.ts +61 -0
  20. package/dist/media/MediaConnectionAwaiter.js +163 -0
  21. package/dist/media/MediaConnectionAwaiter.js.map +1 -0
  22. package/dist/media/index.js +4 -1
  23. package/dist/media/index.js.map +1 -1
  24. package/dist/media/properties.js +4 -24
  25. package/dist/media/properties.js.map +1 -1
  26. package/dist/meeting/index.d.ts +26 -7
  27. package/dist/meeting/index.js +893 -677
  28. package/dist/meeting/index.js.map +1 -1
  29. package/dist/meeting/muteState.d.ts +2 -8
  30. package/dist/meeting/muteState.js +37 -25
  31. package/dist/meeting/muteState.js.map +1 -1
  32. package/dist/meeting/request.d.ts +3 -0
  33. package/dist/meeting/request.js +32 -23
  34. package/dist/meeting/request.js.map +1 -1
  35. package/dist/meeting/util.js +1 -0
  36. package/dist/meeting/util.js.map +1 -1
  37. package/dist/meeting-info/utilv2.js +4 -1
  38. package/dist/meeting-info/utilv2.js.map +1 -1
  39. package/dist/meetings/index.d.ts +8 -0
  40. package/dist/meetings/index.js +20 -0
  41. package/dist/meetings/index.js.map +1 -1
  42. package/dist/multistream/mediaRequestManager.d.ts +2 -1
  43. package/dist/multistream/mediaRequestManager.js +1 -1
  44. package/dist/multistream/mediaRequestManager.js.map +1 -1
  45. package/dist/multistream/remoteMediaGroup.d.ts +2 -0
  46. package/dist/multistream/remoteMediaGroup.js +16 -2
  47. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  48. package/dist/multistream/remoteMediaManager.d.ts +15 -0
  49. package/dist/multistream/remoteMediaManager.js +179 -65
  50. package/dist/multistream/remoteMediaManager.js.map +1 -1
  51. package/dist/multistream/sendSlotManager.d.ts +9 -1
  52. package/dist/multistream/sendSlotManager.js +22 -0
  53. package/dist/multistream/sendSlotManager.js.map +1 -1
  54. package/dist/reachability/clusterReachability.d.ts +1 -0
  55. package/dist/reachability/clusterReachability.js +29 -15
  56. package/dist/reachability/clusterReachability.js.map +1 -1
  57. package/dist/reachability/index.d.ts +4 -0
  58. package/dist/reachability/index.js +18 -2
  59. package/dist/reachability/index.js.map +1 -1
  60. package/dist/reachability/request.js +12 -10
  61. package/dist/reachability/request.js.map +1 -1
  62. package/dist/reachability/util.d.ts +7 -0
  63. package/dist/reachability/util.js +19 -0
  64. package/dist/reachability/util.js.map +1 -1
  65. package/dist/reconnection-manager/index.js +2 -1
  66. package/dist/reconnection-manager/index.js.map +1 -1
  67. package/dist/roap/index.d.ts +10 -2
  68. package/dist/roap/index.js +15 -0
  69. package/dist/roap/index.js.map +1 -1
  70. package/dist/roap/request.js +3 -3
  71. package/dist/roap/request.js.map +1 -1
  72. package/dist/roap/turnDiscovery.d.ts +64 -17
  73. package/dist/roap/turnDiscovery.js +307 -126
  74. package/dist/roap/turnDiscovery.js.map +1 -1
  75. package/dist/statsAnalyzer/index.js +53 -30
  76. package/dist/statsAnalyzer/index.js.map +1 -1
  77. package/dist/webinar/index.js +1 -1
  78. package/package.json +22 -22
  79. package/src/config.ts +1 -0
  80. package/src/constants.ts +7 -3
  81. package/src/index.ts +1 -0
  82. package/src/interpretation/index.ts +18 -1
  83. package/src/locus-info/mediaSharesUtils.ts +16 -0
  84. package/src/locus-info/selfUtils.ts +5 -0
  85. package/src/media/MediaConnectionAwaiter.ts +174 -0
  86. package/src/media/index.ts +3 -1
  87. package/src/media/properties.ts +6 -31
  88. package/src/meeting/index.ts +321 -106
  89. package/src/meeting/muteState.ts +34 -20
  90. package/src/meeting/request.ts +18 -2
  91. package/src/meeting/util.ts +1 -0
  92. package/src/meeting-info/utilv2.ts +2 -1
  93. package/src/meetings/index.ts +18 -0
  94. package/src/multistream/mediaRequestManager.ts +4 -1
  95. package/src/multistream/remoteMediaGroup.ts +19 -0
  96. package/src/multistream/remoteMediaManager.ts +101 -16
  97. package/src/multistream/sendSlotManager.ts +28 -0
  98. package/src/reachability/clusterReachability.ts +20 -5
  99. package/src/reachability/index.ts +24 -1
  100. package/src/reachability/request.ts +15 -11
  101. package/src/reachability/util.ts +21 -0
  102. package/src/reconnection-manager/index.ts +1 -1
  103. package/src/roap/index.ts +25 -3
  104. package/src/roap/request.ts +3 -3
  105. package/src/roap/turnDiscovery.ts +244 -78
  106. package/src/statsAnalyzer/index.ts +63 -27
  107. package/test/integration/spec/journey.js +14 -14
  108. package/test/integration/spec/space-meeting.js +1 -1
  109. package/test/unit/spec/interpretation/index.ts +39 -3
  110. package/test/unit/spec/locus-info/index.js +28 -19
  111. package/test/unit/spec/locus-info/mediaSharesUtils.ts +9 -0
  112. package/test/unit/spec/locus-info/selfUtils.js +42 -12
  113. package/test/unit/spec/media/MediaConnectionAwaiter.ts +344 -0
  114. package/test/unit/spec/media/index.ts +89 -78
  115. package/test/unit/spec/media/properties.ts +16 -70
  116. package/test/unit/spec/meeting/index.js +638 -139
  117. package/test/unit/spec/meeting/muteState.js +219 -67
  118. package/test/unit/spec/meeting/request.js +21 -0
  119. package/test/unit/spec/meeting/utils.js +6 -1
  120. package/test/unit/spec/meeting-info/utilv2.js +6 -0
  121. package/test/unit/spec/meetings/index.js +40 -20
  122. package/test/unit/spec/multistream/mediaRequestManager.ts +20 -2
  123. package/test/unit/spec/multistream/remoteMediaGroup.ts +79 -1
  124. package/test/unit/spec/multistream/remoteMediaManager.ts +199 -1
  125. package/test/unit/spec/multistream/sendSlotManager.ts +50 -18
  126. package/test/unit/spec/reachability/clusterReachability.ts +86 -22
  127. package/test/unit/spec/reachability/index.ts +197 -60
  128. package/test/unit/spec/reachability/request.js +15 -7
  129. package/test/unit/spec/reachability/util.ts +32 -2
  130. package/test/unit/spec/reconnection-manager/index.js +28 -0
  131. package/test/unit/spec/roap/index.ts +61 -6
  132. package/test/unit/spec/roap/turnDiscovery.ts +298 -16
  133. package/test/unit/spec/stats-analyzer/index.js +179 -0
  134. package/dist/member/member.types.d.ts +0 -11
  135. package/dist/member/member.types.js +0 -17
  136. package/dist/member/member.types.js.map +0 -1
  137. package/src/member/member.types.ts +0 -13
  138. /package/test/unit/spec/locus-info/{lib/selfConstant.js → selfConstant.js} +0 -0
@@ -7,7 +7,7 @@ import sinon from 'sinon';
7
7
  import * as internalMediaModule from '@webex/internal-media-core';
8
8
  import StateMachine from 'javascript-state-machine';
9
9
  import uuid from 'uuid';
10
- import {assert} from '@webex/test-helper-chai';
10
+ import {assert, expect} from '@webex/test-helper-chai';
11
11
  import {Credentials, Token, WebexPlugin} from '@webex/webex-core';
12
12
  import Support from '@webex/internal-plugin-support';
13
13
  import MockWebex from '@webex/test-helper-mock-webex';
@@ -43,7 +43,7 @@ import {
43
43
  RemoteTrackType,
44
44
  MediaType,
45
45
  } from '@webex/internal-media-core';
46
- import {StreamEventNames} from '@webex/media-helpers';
46
+ import {LocalStreamEventNames} from '@webex/media-helpers';
47
47
  import * as StatsAnalyzerModule from '@webex/plugin-meetings/src/statsAnalyzer';
48
48
  import EventsScope from '@webex/plugin-meetings/src/common/events/events-scope';
49
49
  import Meetings, {CONSTANTS} from '@webex/plugin-meetings';
@@ -99,7 +99,6 @@ import {
99
99
  MeetingInfoV2PolicyError,
100
100
  } from '../../../../src/meeting-info/meeting-info-v2';
101
101
  import {
102
- CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD,
103
102
  DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
104
103
  ICE_FAILED_WITHOUT_TURN_TLS_CLIENT_CODE,
105
104
  ICE_FAILED_WITH_TURN_TLS_CLIENT_CODE,
@@ -110,9 +109,7 @@ import CallDiagnosticMetrics from '@webex/internal-plugin-metrics/src/call-diagn
110
109
  import {ERROR_DESCRIPTIONS} from '@webex/internal-plugin-metrics/src/call-diagnostic/config';
111
110
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
112
111
 
113
- import {
114
- EVENT_TRIGGERS as VOICEAEVENTS,
115
- } from '@webex/internal-plugin-voicea';
112
+ import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
116
113
 
117
114
  describe('plugin-meetings', () => {
118
115
  const logger = {
@@ -613,36 +610,177 @@ describe('plugin-meetings', () => {
613
610
  assert.exists(meeting.joinWithMedia);
614
611
  });
615
612
 
616
- describe('resolution', () => {
617
- it('should success and return a promise', async () => {
618
- meeting.join = sinon.stub().returns(Promise.resolve(test1));
619
- meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
613
+ const fakeRoapMessage = {id: 'fake TURN discovery message'};
614
+ const fakeReachabilityResults = {id: 'fake reachability'};
615
+ const fakeTurnServerInfo = {id: 'fake turn info'};
616
+ const fakeJoinResult = {id: 'join result'};
620
617
 
621
- const joinOptions = {correlationId: '12345'};
622
- const mediaOptions = {audioEnabled: test1, allowMediaInLobby: true};
618
+ const joinOptions = {correlationId: '12345'};
619
+ const mediaOptions = {audioEnabled: true, allowMediaInLobby: true};
623
620
 
624
- const result = await meeting.joinWithMedia({
625
- joinOptions,
626
- mediaOptions,
627
- });
628
- assert.calledOnceWithExactly(meeting.join, joinOptions);
629
- assert.calledOnceWithExactly(meeting.addMedia, mediaOptions);
630
- assert.deepEqual(result, {join: test1, media: test4});
621
+ let generateTurnDiscoveryRequestMessageStub;
622
+ let handleTurnDiscoveryHttpResponseStub;
623
+ let abortTurnDiscoveryStub;
624
+
625
+ beforeEach(() => {
626
+ meeting.join = sinon.stub().returns(Promise.resolve(fakeJoinResult));
627
+ meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
628
+
629
+ webex.meetings.reachability.getReachabilityResults.resolves(fakeReachabilityResults);
630
+
631
+ generateTurnDiscoveryRequestMessageStub = sinon
632
+ .stub(meeting.roap, 'generateTurnDiscoveryRequestMessage')
633
+ .resolves({roapMessage: fakeRoapMessage});
634
+ handleTurnDiscoveryHttpResponseStub = sinon
635
+ .stub(meeting.roap, 'handleTurnDiscoveryHttpResponse')
636
+ .resolves({turnServerInfo: fakeTurnServerInfo, turnDiscoverySkippedReason: undefined});
637
+ abortTurnDiscoveryStub = sinon.stub(meeting.roap, 'abortTurnDiscovery');
638
+ });
639
+
640
+ it('should work as expected', async () => {
641
+ const result = await meeting.joinWithMedia({
642
+ joinOptions,
643
+ mediaOptions,
644
+ });
645
+
646
+ // check that TURN discovery is done with join and addMedia called
647
+ assert.calledOnceWithExactly(meeting.join, {
648
+ ...joinOptions,
649
+ roapMessage: fakeRoapMessage,
650
+ reachability: fakeReachabilityResults,
651
+ });
652
+ assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
653
+ assert.calledOnceWithExactly(
654
+ handleTurnDiscoveryHttpResponseStub,
655
+ meeting,
656
+ fakeJoinResult
657
+ );
658
+ assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, fakeTurnServerInfo);
659
+
660
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4});
661
+ });
662
+
663
+ it("should not call handleTurnDiscoveryHttpResponse if we don't send a TURN discovery request with join", async () => {
664
+ generateTurnDiscoveryRequestMessageStub.resolves({roapMessage: undefined});
665
+
666
+ const result = await meeting.joinWithMedia({
667
+ joinOptions,
668
+ mediaOptions,
631
669
  });
670
+
671
+ // check that TURN discovery is done with join and addMedia called
672
+ assert.calledOnceWithExactly(meeting.join, {
673
+ ...joinOptions,
674
+ roapMessage: undefined,
675
+ reachability: fakeReachabilityResults,
676
+ });
677
+ assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
678
+ assert.notCalled(handleTurnDiscoveryHttpResponseStub);
679
+ assert.notCalled(abortTurnDiscoveryStub);
680
+ assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, undefined);
681
+
682
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4});
683
+ assert.equal(meeting.turnServerUsed, false);
632
684
  });
633
685
 
634
- describe('rejection', () => {
635
- it('should error out and return a promise', async () => {
636
- meeting.join = sinon.stub().returns(Promise.reject());
637
- assert.isRejected(meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: true}}));
686
+ it('should call abortTurnDiscovery() if we do not get a TURN server info', async () => {
687
+ handleTurnDiscoveryHttpResponseStub.resolves({
688
+ turnServerInfo: undefined,
689
+ turnDiscoverySkippedReason: 'missing http response',
638
690
  });
639
691
 
640
- it('should fail if called with allowMediaInLobby:false', async () => {
641
- meeting.join = sinon.stub().returns(Promise.resolve(test1));
642
- meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
692
+ const result = await meeting.joinWithMedia({
693
+ joinOptions,
694
+ mediaOptions,
695
+ });
643
696
 
644
- assert.isRejected(meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: false}}));
697
+ // check that TURN discovery is done with join and addMedia called
698
+ assert.calledOnceWithExactly(meeting.join, {
699
+ ...joinOptions,
700
+ roapMessage: fakeRoapMessage,
701
+ reachability: fakeReachabilityResults,
645
702
  });
703
+ assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
704
+ assert.calledOnceWithExactly(
705
+ handleTurnDiscoveryHttpResponseStub,
706
+ meeting,
707
+ fakeJoinResult
708
+ );
709
+ assert.calledOnceWithExactly(abortTurnDiscoveryStub);
710
+ assert.calledOnceWithExactly(meeting.addMedia, mediaOptions, undefined);
711
+
712
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4});
713
+ });
714
+
715
+ it('should reject if join() fails', async () => {
716
+ const error = new Error('fake');
717
+ meeting.join = sinon.stub().returns(Promise.reject(error));
718
+ meeting.locusUrl = null; // when join fails, we end up with null locusUrl
719
+
720
+ await assert.isRejected(meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: true}}));
721
+
722
+ assert.calledOnceWithExactly(abortTurnDiscoveryStub);
723
+
724
+ assert.calledWith(
725
+ Metrics.sendBehavioralMetric,
726
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
727
+ {
728
+ correlation_id: meeting.correlationId,
729
+ locus_id: undefined,
730
+ reason: error.message,
731
+ stack: error.stack,
732
+ leaveErrorReason: undefined,
733
+ },
734
+ {
735
+ type: error.name,
736
+ }
737
+ );
738
+ });
739
+
740
+ it('should fail if called with allowMediaInLobby:false', async () => {
741
+ meeting.join = sinon.stub().returns(Promise.resolve(test1));
742
+ meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
743
+
744
+ await assert.isRejected(
745
+ meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: false}})
746
+ );
747
+ });
748
+
749
+ it('should call leave() if addMedia fails and ignore leave() failure', async () => {
750
+ const leaveError = new Error('leave error');
751
+ const addMediaError = new Error('fake addMedia error');
752
+
753
+ const leaveStub = sinon.stub(meeting, 'leave').rejects(leaveError);
754
+ meeting.addMedia = sinon.stub().rejects(addMediaError);
755
+
756
+ await assert.isRejected(
757
+ meeting.joinWithMedia({
758
+ joinOptions: {resourceId: 'some resource'},
759
+ mediaOptions: {allowMediaInLobby: true},
760
+ }),
761
+ addMediaError
762
+ );
763
+
764
+ assert.calledOnce(leaveStub);
765
+ assert.calledOnceWithExactly(leaveStub, {
766
+ resourceId: 'some resource',
767
+ reason: 'joinWithMedia failure',
768
+ });
769
+
770
+ assert.calledWith(
771
+ Metrics.sendBehavioralMetric,
772
+ BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
773
+ {
774
+ correlation_id: meeting.correlationId,
775
+ locus_id: meeting.locusUrl.split('/').pop(),
776
+ reason: addMediaError.message,
777
+ stack: addMediaError.stack,
778
+ leaveErrorReason: leaveError.message,
779
+ },
780
+ {
781
+ type: addMediaError.name,
782
+ }
783
+ );
646
784
  });
647
785
  });
648
786
 
@@ -669,7 +807,7 @@ describe('plugin-meetings', () => {
669
807
 
670
808
  it('should subscribe to events for the first time and avoid subscribing for future transcription starts', async () => {
671
809
  meeting.joinedWith = {
672
- state: 'JOINED'
810
+ state: 'JOINED',
673
811
  };
674
812
  meeting.areVoiceaEventsSetup = false;
675
813
  meeting.roles = ['MODERATOR'];
@@ -679,27 +817,19 @@ describe('plugin-meetings', () => {
679
817
  assert.equal(webex.internal.voicea.on.callCount, 4);
680
818
  assert.equal(meeting.areVoiceaEventsSetup, true);
681
819
  assert.equal(webex.internal.voicea.listenToEvents.callCount, 1);
682
- assert.calledWith(
683
- webex.internal.voicea.toggleTranscribing,
684
- true,
685
- );
820
+ assert.calledWith(webex.internal.voicea.toggleTranscribing, true);
686
821
 
687
822
  await meeting.startTranscription();
688
823
  assert.equal(webex.internal.voicea.on.callCount, 4);
689
824
  assert.equal(meeting.areVoiceaEventsSetup, true);
690
825
  assert.equal(webex.internal.voicea.listenToEvents.callCount, 1);
691
- assert.calledTwice(
692
- webex.internal.voicea.toggleTranscribing,
693
- );
694
- assert.calledWith(
695
- webex.internal.voicea.toggleTranscribing,
696
- true,
697
- );
826
+ assert.calledTwice(webex.internal.voicea.toggleTranscribing);
827
+ assert.calledWith(webex.internal.voicea.toggleTranscribing, true);
698
828
  });
699
829
 
700
830
  it('should listen to events and not toggleTranscribing if the user is not a host', async () => {
701
831
  meeting.joinedWith = {
702
- state: 'JOINED'
832
+ state: 'JOINED',
703
833
  };
704
834
  meeting.areVoiceaEventsSetup = false;
705
835
  meeting.roles = ['COHOST'];
@@ -709,9 +839,7 @@ describe('plugin-meetings', () => {
709
839
  assert.equal(webex.internal.voicea.on.callCount, 4);
710
840
  assert.equal(meeting.areVoiceaEventsSetup, true);
711
841
  assert.equal(webex.internal.voicea.listenToEvents.callCount, 1);
712
- assert.notCalled(
713
- webex.internal.voicea.toggleTranscribing
714
- );
842
+ assert.notCalled(webex.internal.voicea.toggleTranscribing);
715
843
  });
716
844
 
717
845
  it("should throw error if request doesn't work", async () => {
@@ -752,7 +880,7 @@ describe('plugin-meetings', () => {
752
880
  describe('#setCaptionLanguage', () => {
753
881
  beforeEach(() => {
754
882
  meeting.isTranscriptionSupported = sinon.stub();
755
- meeting.transcription = { languageOptions: {} };
883
+ meeting.transcription = {languageOptions: {}};
756
884
  webex.internal.voicea.on = sinon.stub();
757
885
  webex.internal.voicea.off = sinon.stub();
758
886
  webex.internal.voicea.setCaptionLanguage = sinon.stub();
@@ -778,23 +906,23 @@ describe('plugin-meetings', () => {
778
906
  const languageCode = 'fr';
779
907
 
780
908
  meeting.setCaptionLanguage(languageCode).then((resolvedLanguageCode) => {
781
- assert.calledWith(
782
- webex.internal.voicea.requestLanguage,
909
+ assert.calledWith(webex.internal.voicea.requestLanguage, languageCode);
910
+ assert.equal(resolvedLanguageCode, languageCode);
911
+ assert.equal(
912
+ meeting.transcription.languageOptions.currentCaptionLanguage,
783
913
  languageCode
784
914
  );
785
- assert.equal(resolvedLanguageCode, languageCode);
786
- assert.equal(meeting.transcription.languageOptions.currentCaptionLanguage, languageCode);
787
915
  done();
788
916
  });
789
917
 
790
918
  assert.calledOnceWithMatch(
791
919
  webex.internal.voicea.on,
792
- VOICEAEVENTS.CAPTION_LANGUAGE_UPDATE,
920
+ VOICEAEVENTS.CAPTION_LANGUAGE_UPDATE
793
921
  );
794
922
 
795
923
  // Trigger the event
796
924
  const voiceaListenerLangugeUpdate = webex.internal.voicea.on.getCall(0).args[1];
797
- voiceaListenerLangugeUpdate({ statusCode: 200, languageCode });
925
+ voiceaListenerLangugeUpdate({statusCode: 200, languageCode});
798
926
  });
799
927
 
800
928
  it('should reject if the statusCode in payload is not 200', (done) => {
@@ -802,8 +930,8 @@ describe('plugin-meetings', () => {
802
930
  const languageCode = 'fr';
803
931
  const rejectPayload = {
804
932
  statusCode: 400,
805
- message: 'some error message'
806
- }
933
+ message: 'some error message',
934
+ };
807
935
 
808
936
  meeting.setCaptionLanguage(languageCode).catch((payload) => {
809
937
  assert.equal(payload, rejectPayload);
@@ -812,20 +940,19 @@ describe('plugin-meetings', () => {
812
940
 
813
941
  assert.calledOnceWithMatch(
814
942
  webex.internal.voicea.on,
815
- VOICEAEVENTS.CAPTION_LANGUAGE_UPDATE,
943
+ VOICEAEVENTS.CAPTION_LANGUAGE_UPDATE
816
944
  );
817
945
 
818
946
  // Trigger the event
819
947
  const voiceaListenerLangugeUpdate = webex.internal.voicea.on.getCall(0).args[1];
820
948
  voiceaListenerLangugeUpdate(rejectPayload);
821
949
  });
822
-
823
950
  });
824
951
 
825
952
  describe('#setSpokenLanguage', () => {
826
953
  beforeEach(() => {
827
954
  meeting.isTranscriptionSupported = sinon.stub();
828
- meeting.transcription = { languageOptions: {} };
955
+ meeting.transcription = {languageOptions: {}};
829
956
  webex.internal.voicea.on = sinon.stub();
830
957
  webex.internal.voicea.off = sinon.stub();
831
958
  webex.internal.voicea.setSpokenLanguage = sinon.stub();
@@ -850,59 +977,48 @@ describe('plugin-meetings', () => {
850
977
  const languageCode = 'fr';
851
978
 
852
979
  meeting.setSpokenLanguage(languageCode).then((resolvedLanguageCode) => {
853
- assert.calledWith(
854
- webex.internal.voicea.setSpokenLanguage,
855
- languageCode
856
- );
980
+ assert.calledWith(webex.internal.voicea.setSpokenLanguage, languageCode);
857
981
  assert.equal(resolvedLanguageCode, languageCode);
858
982
  assert.equal(meeting.transcription.languageOptions.currentSpokenLanguage, languageCode);
859
983
  done();
860
984
  });
861
985
 
862
- assert.calledOnceWithMatch(
863
- webex.internal.voicea.on,
864
- VOICEAEVENTS.SPOKEN_LANGUAGE_UPDATE,
865
- );
986
+ assert.calledOnceWithMatch(webex.internal.voicea.on, VOICEAEVENTS.SPOKEN_LANGUAGE_UPDATE);
866
987
 
867
988
  // Trigger the event
868
989
  const voiceaListenerLangugeUpdate = webex.internal.voicea.on.getCall(0).args[1];
869
- voiceaListenerLangugeUpdate({ languageCode });
990
+ voiceaListenerLangugeUpdate({languageCode});
870
991
  });
871
992
 
872
993
  it('should reject if the language code does not exist in payload', (done) => {
873
994
  meeting.isTranscriptionSupported.returns(true);
874
995
  const languageCode = 'fr';
875
996
  const rejectPayload = {
876
- 'message': 'some error message'
877
- }
997
+ message: 'some error message',
998
+ };
878
999
 
879
1000
  meeting.setSpokenLanguage(languageCode).catch((payload) => {
880
1001
  assert.equal(payload, rejectPayload);
881
1002
  done();
882
1003
  });
883
1004
 
884
- assert.calledOnceWithMatch(
885
- webex.internal.voicea.on,
886
- VOICEAEVENTS.SPOKEN_LANGUAGE_UPDATE,
887
- );
1005
+ assert.calledOnceWithMatch(webex.internal.voicea.on, VOICEAEVENTS.SPOKEN_LANGUAGE_UPDATE);
888
1006
 
889
1007
  // Trigger the event
890
1008
  const voiceaListenerLangugeUpdate = webex.internal.voicea.on.getCall(0).args[1];
891
1009
  voiceaListenerLangugeUpdate(rejectPayload);
892
1010
  });
893
-
894
1011
  });
895
1012
 
896
1013
  describe('transcription events', () => {
1014
+ beforeEach(() => {
1015
+ meeting.trigger = sinon.stub();
1016
+ });
1017
+
897
1018
  it('should trigger meeting:caption-received event', () => {
898
1019
  meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({});
899
1020
  assert.calledWith(
900
- TriggerProxy.trigger,
901
- sinon.match.instanceOf(Meeting),
902
- {
903
- file: 'meeting/index',
904
- function: 'setUpVoiceaListeners',
905
- },
1021
+ meeting.trigger,
906
1022
  EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED
907
1023
  );
908
1024
  });
@@ -910,12 +1026,7 @@ describe('plugin-meetings', () => {
910
1026
  it('should trigger meeting:receiveTranscription:started event', () => {
911
1027
  meeting.voiceaListenerCallbacks[VOICEAEVENTS.VOICEA_ANNOUNCEMENT]({});
912
1028
  assert.calledWith(
913
- TriggerProxy.trigger,
914
- sinon.match.instanceOf(Meeting),
915
- {
916
- file: 'meeting/index',
917
- function: 'setUpVoiceaListeners',
918
- },
1029
+ meeting.trigger,
919
1030
  EVENT_TRIGGERS.MEETING_STARTED_RECEIVING_TRANSCRIPTION
920
1031
  );
921
1032
  });
@@ -923,12 +1034,7 @@ describe('plugin-meetings', () => {
923
1034
  it('should trigger meeting:caption-received event', () => {
924
1035
  meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({});
925
1036
  assert.calledWith(
926
- TriggerProxy.trigger,
927
- sinon.match.instanceOf(Meeting),
928
- {
929
- file: 'meeting/index',
930
- function: 'setUpVoiceaListeners',
931
- },
1037
+ meeting.trigger,
932
1038
  EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED
933
1039
  );
934
1040
  });
@@ -1134,7 +1240,7 @@ describe('plugin-meetings', () => {
1134
1240
  file: 'meeting/index',
1135
1241
  function: 'join',
1136
1242
  },
1137
- EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED,
1243
+ EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED
1138
1244
  );
1139
1245
  });
1140
1246
 
@@ -1412,7 +1518,6 @@ describe('plugin-meetings', () => {
1412
1518
  describe('#addMedia', () => {
1413
1519
  const muteStateStub = {
1414
1520
  handleClientRequest: sinon.stub().returns(Promise.resolve(true)),
1415
- applyClientStateLocally: sinon.stub().returns(Promise.resolve(true)),
1416
1521
  };
1417
1522
 
1418
1523
  let fakeMediaConnection;
@@ -2506,6 +2611,7 @@ describe('plugin-meetings', () => {
2506
2611
 
2507
2612
  beforeEach(async () => {
2508
2613
  meeting.meetingState = 'ACTIVE';
2614
+ meeting.remoteShareInstanceId = '1234';
2509
2615
  prevConfigValue = meeting.config.stats.enableStatsAnalyzer;
2510
2616
 
2511
2617
  meeting.config.stats.enableStatsAnalyzer = true;
@@ -2611,6 +2717,66 @@ describe('plugin-meetings', () => {
2611
2717
  });
2612
2718
  });
2613
2719
 
2720
+ it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics for share', async () => {
2721
+ statsAnalyzerStub.emit(
2722
+ {file: 'test', function: 'test'},
2723
+ StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STARTED,
2724
+ {type: 'share'}
2725
+ );
2726
+
2727
+ assert.calledWith(
2728
+ TriggerProxy.trigger,
2729
+ sinon.match.instanceOf(Meeting),
2730
+ {
2731
+ file: 'meeting/index',
2732
+ function: 'addMedia',
2733
+ },
2734
+ EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
2735
+ {
2736
+ type: 'share',
2737
+ }
2738
+ );
2739
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2740
+ name: 'client.media.rx.start',
2741
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2742
+ options: {
2743
+ meetingId: meeting.id,
2744
+ },
2745
+ });
2746
+
2747
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2748
+ name: 'client.media.render.start',
2749
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2750
+ options: {
2751
+ meetingId: meeting.id,
2752
+ },
2753
+ });
2754
+ });
2755
+
2756
+ it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
2757
+ statsAnalyzerStub.emit(
2758
+ {file: 'test', function: 'test'},
2759
+ StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STOPPED,
2760
+ {type: 'share'}
2761
+ );
2762
+
2763
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2764
+ name: 'client.media.rx.stop',
2765
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2766
+ options: {
2767
+ meetingId: meeting.id,
2768
+ },
2769
+ });
2770
+
2771
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2772
+ name: 'client.media.render.stop',
2773
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2774
+ options: {
2775
+ meetingId: meeting.id,
2776
+ },
2777
+ });
2778
+ });
2779
+
2614
2780
  it('calls submitMQE correctly', async () => {
2615
2781
  const fakeData = {intervalMetadata: {bla: 'bla'}};
2616
2782
 
@@ -2855,9 +3021,10 @@ describe('plugin-meetings', () => {
2855
3021
  meeting.setMercuryListener = sinon.stub();
2856
3022
  meeting.locusInfo.onFullLocus = sinon.stub();
2857
3023
  meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
2858
- meeting.roap.doTurnDiscovery = sinon
2859
- .stub()
2860
- .resolves({turnServerInfo: {}, turnDiscoverySkippedReason: 'reachability'});
3024
+ meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3025
+ turnServerInfo: {url: 'turn-url', username: 'turn user', password: 'turn password'},
3026
+ turnDiscoverySkippedReason: 'reachability',
3027
+ });
2861
3028
  meeting.deferSDPAnswer = new Defer();
2862
3029
  meeting.deferSDPAnswer.resolve();
2863
3030
  meeting.webex.meetings.meetingCollection = new MeetingCollection();
@@ -2868,7 +3035,7 @@ describe('plugin-meetings', () => {
2868
3035
  // setup things that are expected to be the same across all the tests and are actually irrelevant for these tests
2869
3036
  expectedDebugId = `MC-${meeting.id.substring(0, 4)}`;
2870
3037
  expectedMediaConnectionConfig = {
2871
- iceServers: [{urls: undefined, username: '', credential: ''}],
3038
+ iceServers: [{urls: 'turn-url', username: 'turn user', credential: 'turn password'}],
2872
3039
  skipInactiveTransceivers: false,
2873
3040
  requireH264: true,
2874
3041
  sdpMunging: {
@@ -2892,9 +3059,13 @@ describe('plugin-meetings', () => {
2892
3059
  getSettings: sinon.stub().returns({
2893
3060
  deviceId: 'some device id',
2894
3061
  }),
2895
- muted: false,
3062
+ userMuted: false,
3063
+ systemMuted: false,
3064
+ get muted() {
3065
+ return this.userMuted || this.systemMuted;
3066
+ },
2896
3067
  setUnmuteAllowed: sinon.stub(),
2897
- setMuted: sinon.stub(),
3068
+ setUserMuted: sinon.stub(),
2898
3069
  setServerMuted: sinon.stub(),
2899
3070
  outputStream: {
2900
3071
  getTracks: () => {
@@ -2927,6 +3098,7 @@ describe('plugin-meetings', () => {
2927
3098
  createSendSlot: sinon.stub().returns({
2928
3099
  publishStream: sinon.stub(),
2929
3100
  unpublishStream: sinon.stub(),
3101
+ setNamedMediaGroups: sinon.stub(),
2930
3102
  }),
2931
3103
  enableMultistreamAudio: sinon.stub(),
2932
3104
  };
@@ -3128,28 +3300,52 @@ describe('plugin-meetings', () => {
3128
3300
  if (stream !== undefined) {
3129
3301
  switch (type) {
3130
3302
  case 'audio':
3131
- assert.calledOnceWithExactly(
3132
- meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream,
3133
- stream
3134
- );
3303
+ if (stream?.readyState === 'ended') {
3304
+ assert.notCalled(
3305
+ meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream
3306
+ );
3307
+ } else {
3308
+ assert.calledOnceWithExactly(
3309
+ meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream,
3310
+ stream
3311
+ );
3312
+ }
3135
3313
  break;
3136
3314
  case 'video':
3137
- assert.calledOnceWithExactly(
3138
- meeting.sendSlotManager.getSlot(MediaType.VideoMain).publishStream,
3139
- stream
3140
- );
3315
+ if (stream?.readyState === 'ended') {
3316
+ assert.notCalled(
3317
+ meeting.sendSlotManager.getSlot(MediaType.VideoMain).publishStream
3318
+ );
3319
+ } else {
3320
+ assert.calledOnceWithExactly(
3321
+ meeting.sendSlotManager.getSlot(MediaType.VideoMain).publishStream,
3322
+ stream
3323
+ );
3324
+ }
3141
3325
  break;
3142
3326
  case 'screenShareAudio':
3143
- assert.calledOnceWithExactly(
3144
- meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
3145
- stream
3146
- );
3327
+ if (stream?.readyState === 'ended') {
3328
+ assert.notCalled(
3329
+ meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream
3330
+ );
3331
+ } else {
3332
+ assert.calledOnceWithExactly(
3333
+ meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
3334
+ stream
3335
+ );
3336
+ }
3147
3337
  break;
3148
3338
  case 'screenShareVideo':
3149
- assert.calledOnceWithExactly(
3150
- meeting.sendSlotManager.getSlot(MediaType.VideoSlides).publishStream,
3151
- stream
3152
- );
3339
+ if (stream?.readyState === 'ended') {
3340
+ assert.notCalled(
3341
+ meeting.sendSlotManager.getSlot(MediaType.VideoSlides).publishStream
3342
+ );
3343
+ } else {
3344
+ assert.calledOnceWithExactly(
3345
+ meeting.sendSlotManager.getSlot(MediaType.VideoSlides).publishStream,
3346
+ stream
3347
+ );
3348
+ }
3153
3349
  break;
3154
3350
  }
3155
3351
  }
@@ -3177,7 +3373,7 @@ describe('plugin-meetings', () => {
3177
3373
  }
3178
3374
  };
3179
3375
 
3180
- it('addMedia() works correctly when media is enabled without tracks to publish', async () => {
3376
+ it('addMedia() works correctly when media is enabled without streams to publish', async () => {
3181
3377
  await meeting.addMedia();
3182
3378
  await simulateRoapOffer();
3183
3379
  await simulateRoapOk();
@@ -3211,6 +3407,7 @@ describe('plugin-meetings', () => {
3211
3407
  });
3212
3408
 
3213
3409
  it('addMedia() works correctly when media is enabled with streams to publish', async () => {
3410
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3214
3411
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3215
3412
  await simulateRoapOffer();
3216
3413
  await simulateRoapOk();
@@ -3241,10 +3438,82 @@ describe('plugin-meetings', () => {
3241
3438
 
3242
3439
  // and that these were the only /media requests that were sent
3243
3440
  assert.calledTwice(locusMediaRequestStub);
3441
+
3442
+ assert.calledOnce(handleDeviceLoggingSpy);
3443
+ });
3444
+
3445
+ it('addMedia() works correctly when media is enabled with streams to publish and stream is user muted', async () => {
3446
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3447
+ fakeMicrophoneStream.userMuted = true;
3448
+
3449
+ await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3450
+ await simulateRoapOffer();
3451
+ await simulateRoapOk();
3452
+
3453
+ // check RoapMediaConnection was created correctly
3454
+ checkMediaConnectionCreated({
3455
+ mediaConnectionConfig: expectedMediaConnectionConfig,
3456
+ localStreams: {
3457
+ audio: fakeMicrophoneStream,
3458
+ video: undefined,
3459
+ screenShareVideo: undefined,
3460
+ screenShareAudio: undefined,
3461
+ },
3462
+ direction: {
3463
+ audio: 'sendrecv',
3464
+ video: 'sendrecv',
3465
+ screenShare: 'recvonly',
3466
+ },
3467
+ remoteQualityLevel: 'HIGH',
3468
+ expectedDebugId,
3469
+ meetingId: meeting.id,
3470
+ });
3471
+ // and SDP offer was sent with the right audioMuted/videoMuted values
3472
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
3473
+ // check OK was sent with the right audioMuted/videoMuted values
3474
+ checkOkSent({audioMuted: true, videoMuted: true});
3475
+
3476
+ // and that these were the only /media requests that were sent
3477
+ assert.calledTwice(locusMediaRequestStub);
3478
+ assert.calledOnce(handleDeviceLoggingSpy);
3244
3479
  });
3245
3480
 
3246
- it('addMedia() works correctly when media is enabled with tracks to publish and track is muted', async () => {
3247
- fakeMicrophoneStream.muted = true;
3481
+ it('addMedia() works correctly when media is enabled with tracks to publish and track is ended', async () => {
3482
+ fakeMicrophoneStream.readyState = 'ended';
3483
+
3484
+ await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3485
+ await simulateRoapOffer();
3486
+ await simulateRoapOk();
3487
+
3488
+ // check RoapMediaConnection was created correctly
3489
+ checkMediaConnectionCreated({
3490
+ mediaConnectionConfig: expectedMediaConnectionConfig,
3491
+ localStreams: {
3492
+ audio: undefined,
3493
+ video: undefined,
3494
+ screenShareVideo: undefined,
3495
+ screenShareAudio: undefined,
3496
+ },
3497
+ direction: {
3498
+ audio: 'sendrecv',
3499
+ video: 'sendrecv',
3500
+ screenShare: 'recvonly',
3501
+ },
3502
+ remoteQualityLevel: 'HIGH',
3503
+ expectedDebugId,
3504
+ meetingId: meeting.id,
3505
+ });
3506
+ // and SDP offer was sent with the right audioMuted/videoMuted values
3507
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
3508
+ // check OK was sent with the right audioMuted/videoMuted values
3509
+ checkOkSent({audioMuted: true, videoMuted: true});
3510
+
3511
+ // and that these were the only /media requests that were sent
3512
+ assert.calledTwice(locusMediaRequestStub);
3513
+ });
3514
+
3515
+ it('addMedia() works correctly when media is enabled with streams to publish and stream is system muted', async () => {
3516
+ fakeMicrophoneStream.systemMuted = true;
3248
3517
 
3249
3518
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3250
3519
  await simulateRoapOffer();
@@ -3277,7 +3546,8 @@ describe('plugin-meetings', () => {
3277
3546
  assert.calledTwice(locusMediaRequestStub);
3278
3547
  });
3279
3548
 
3280
- it('addMedia() works correctly when media is disabled with tracks to publish', async () => {
3549
+ it('addMedia() works correctly when media is disabled with streams to publish', async () => {
3550
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3281
3551
  await meeting.addMedia({
3282
3552
  localStreams: {microphone: fakeMicrophoneStream},
3283
3553
  audioEnabled: false,
@@ -3311,9 +3581,23 @@ describe('plugin-meetings', () => {
3311
3581
 
3312
3582
  // and that these were the only /media requests that were sent
3313
3583
  assert.calledTwice(locusMediaRequestStub);
3584
+ assert.calledOnce(handleDeviceLoggingSpy);
3314
3585
  });
3315
3586
 
3316
- it('addMedia() works correctly when media is disabled with no tracks to publish', async () => {
3587
+ it('handleDeviceLogging not called when media is disabled', async () => {
3588
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3589
+ await meeting.addMedia({
3590
+ localStreams: {microphone: fakeMicrophoneStream},
3591
+ audioEnabled: false,
3592
+ videoEnabled: false
3593
+ });
3594
+ await simulateRoapOffer();
3595
+ await simulateRoapOk();
3596
+
3597
+ assert.notCalled(handleDeviceLoggingSpy);
3598
+ })
3599
+
3600
+ it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
3317
3601
  await meeting.addMedia({audioEnabled: false});
3318
3602
  await simulateRoapOffer();
3319
3603
  await simulateRoapOk();
@@ -3346,7 +3630,7 @@ describe('plugin-meetings', () => {
3346
3630
  assert.calledTwice(locusMediaRequestStub);
3347
3631
  });
3348
3632
 
3349
- it('addMedia() works correctly when video is disabled with no tracks to publish', async () => {
3633
+ it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
3350
3634
  await meeting.addMedia({videoEnabled: false});
3351
3635
  await simulateRoapOffer();
3352
3636
  await simulateRoapOk();
@@ -3379,7 +3663,7 @@ describe('plugin-meetings', () => {
3379
3663
  assert.calledTwice(locusMediaRequestStub);
3380
3664
  });
3381
3665
 
3382
- it('addMedia() works correctly when screen share is disabled with no tracks to publish', async () => {
3666
+ it('addMedia() works correctly when screen share is disabled with no streams to publish', async () => {
3383
3667
  await meeting.addMedia({shareAudioEnabled: false, shareVideoEnabled: false});
3384
3668
  await simulateRoapOffer();
3385
3669
  await simulateRoapOk();
@@ -3480,9 +3764,13 @@ describe('plugin-meetings', () => {
3480
3764
  const fakeMicrophoneStream2 = {
3481
3765
  on: sinon.stub(),
3482
3766
  off: sinon.stub(),
3483
- muted: false,
3767
+ userMuted: false,
3768
+ systemMuted: false,
3769
+ get muted() {
3770
+ return this.userMuted || this.systemMuted;
3771
+ },
3484
3772
  setUnmuteAllowed: sinon.stub(),
3485
- setMuted: sinon.stub(),
3773
+ setUserMuted: sinon.stub(),
3486
3774
  outputStream: {
3487
3775
  getTracks: () => {
3488
3776
  return [
@@ -3719,12 +4007,55 @@ describe('plugin-meetings', () => {
3719
4007
  });
3720
4008
 
3721
4009
  [
3722
- {mute: true, title: 'muting a track before confluence is created'},
3723
- {mute: false, title: 'unmuting a track before confluence is created'},
4010
+ {mute: true, title: 'user muting a track before confluence is created'},
4011
+ {mute: false, title: 'user unmuting a track before confluence is created'},
4012
+ ].forEach(({mute, title}) =>
4013
+ it(title, async () => {
4014
+ // initialize the microphone mute state to opposite of what we do in the test
4015
+ fakeMicrophoneStream.userMuted = !mute;
4016
+
4017
+ await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
4018
+ await stableState();
4019
+
4020
+ resetHistory();
4021
+
4022
+ assert.equal(
4023
+ fakeMicrophoneStream.on.getCall(0).args[0],
4024
+ LocalStreamEventNames.UserMuteStateChange
4025
+ );
4026
+ const mutedListener = fakeMicrophoneStream.on.getCall(0).args[1];
4027
+ // simulate track being muted
4028
+ fakeMicrophoneStream.userMuted = mute;
4029
+ mutedListener(mute);
4030
+
4031
+ await stableState();
4032
+
4033
+ // nothing should happen
4034
+ assert.notCalled(locusMediaRequestStub);
4035
+ assert.notCalled(fakeRoapMediaConnection.update);
4036
+
4037
+ // now simulate roap offer and ok
4038
+ await simulateRoapOffer();
4039
+ await simulateRoapOk();
4040
+
4041
+ // it should be sent with the right mute status
4042
+ checkSdpOfferSent({audioMuted: mute, videoMuted: true});
4043
+ // check OK was sent with the right audioMuted/videoMuted values
4044
+ checkOkSent({audioMuted: mute, videoMuted: true});
4045
+
4046
+ // nothing else should happen
4047
+ assert.calledTwice(locusMediaRequestStub);
4048
+ assert.notCalled(fakeRoapMediaConnection.update);
4049
+ })
4050
+ );
4051
+
4052
+ [
4053
+ {mute: true, title: 'system muting a track before confluence is created'},
4054
+ {mute: false, title: 'system unmuting a track before confluence is created'},
3724
4055
  ].forEach(({mute, title}) =>
3725
4056
  it(title, async () => {
3726
4057
  // initialize the microphone mute state to opposite of what we do in the test
3727
- fakeMicrophoneStream.muted = !mute;
4058
+ fakeMicrophoneStream.systemMuted = !mute;
3728
4059
 
3729
4060
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3730
4061
  await stableState();
@@ -3733,10 +4064,11 @@ describe('plugin-meetings', () => {
3733
4064
 
3734
4065
  assert.equal(
3735
4066
  fakeMicrophoneStream.on.getCall(0).args[0],
3736
- StreamEventNames.MuteStateChange
4067
+ LocalStreamEventNames.UserMuteStateChange
3737
4068
  );
3738
4069
  const mutedListener = fakeMicrophoneStream.on.getCall(0).args[1];
3739
4070
  // simulate track being muted
4071
+ fakeMicrophoneStream.systemMuted = mute;
3740
4072
  mutedListener(mute);
3741
4073
 
3742
4074
  await stableState();
@@ -6137,6 +6469,65 @@ describe('plugin-meetings', () => {
6137
6469
  checkScreenShareVideoPublished(videoShareStream);
6138
6470
  checkScreenShareAudioPublished(audioShareStream);
6139
6471
  });
6472
+
6473
+ [
6474
+ {
6475
+ endedStream: 'microphone',
6476
+ streams: {
6477
+ microphone: {
6478
+ readyState: 'ended',
6479
+ },
6480
+ camera: undefined,
6481
+ screenShare: {
6482
+ audio: undefined,
6483
+ video: undefined,
6484
+ },
6485
+ },
6486
+ },
6487
+ {
6488
+ endedStream: 'camera',
6489
+ streams: {
6490
+ microphone: undefined,
6491
+ camera: {
6492
+ readyState: 'ended',
6493
+ },
6494
+ screenShare: {
6495
+ audio: undefined,
6496
+ video: undefined,
6497
+ },
6498
+ },
6499
+ },
6500
+ {
6501
+ endedStream: 'screenShare audio',
6502
+ streams: {
6503
+ microphone: undefined,
6504
+ camera: undefined,
6505
+ screenShare: {
6506
+ audio: {
6507
+ readyState: 'ended',
6508
+ },
6509
+ video: undefined,
6510
+ },
6511
+ },
6512
+ },
6513
+ {
6514
+ endedStream: 'screenShare video',
6515
+ streams: {
6516
+ microphone: undefined,
6517
+ camera: undefined,
6518
+ screenShare: {
6519
+ audio: undefined,
6520
+ video: {
6521
+ readyState: 'ended',
6522
+ },
6523
+ },
6524
+ },
6525
+ },
6526
+ ].forEach(({endedStream, streams}) => {
6527
+ it(`throws error if readyState of ${endedStream} is ended`, async () => {
6528
+ assert.isRejected(meeting.publishStreams(streams));
6529
+ });
6530
+ });
6140
6531
  });
6141
6532
 
6142
6533
  describe('unpublishStreams', () => {
@@ -6266,6 +6657,31 @@ describe('plugin-meetings', () => {
6266
6657
  });
6267
6658
  });
6268
6659
 
6660
+ describe('#setSendNamedMediaGroup', () => {
6661
+ beforeEach(() => {
6662
+ meeting.sendSlotManager.setNamedMediaGroups = sinon.stub().returns(undefined);
6663
+ });
6664
+ it('should throw error if not audio type', () => {
6665
+ expect(() => meeting.setSendNamedMediaGroup(MediaType.VideoMain, 20)).to.throw(
6666
+ `cannot set send named media group which media type is ${MediaType.VideoMain}`
6667
+ );
6668
+ });
6669
+ it('fails if there is no media connection', () => {
6670
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
6671
+ meeting.setSendNamedMediaGroup('AUDIO-MAIN', 20);
6672
+ assert.notCalled(meeting.sendSlotManager.setNamedMediaGroups);
6673
+ });
6674
+
6675
+ it('success if there is media connection', () => {
6676
+ meeting.isMultistream = true;
6677
+ meeting.mediaProperties.webrtcMediaConnection = true;
6678
+ meeting.setSendNamedMediaGroup('AUDIO-MAIN', 20);
6679
+ assert.calledOnceWithExactly(meeting.sendSlotManager.setNamedMediaGroups, 'AUDIO-MAIN', [
6680
+ {type: 1, value: 20},
6681
+ ]);
6682
+ });
6683
+ });
6684
+
6269
6685
  describe('#enableMusicMode', () => {
6270
6686
  beforeEach(() => {
6271
6687
  meeting.isMultistream = true;
@@ -7336,6 +7752,7 @@ describe('plugin-meetings', () => {
7336
7752
  });
7337
7753
  it('listens to the self admitted guest event', (done) => {
7338
7754
  meeting.stopKeepAlive = sinon.stub();
7755
+ meeting.updateLLMConnection = sinon.stub();
7339
7756
  meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
7340
7757
  assert.calledOnceWithExactly(meeting.stopKeepAlive);
7341
7758
  assert.calledThrice(TriggerProxy.trigger);
@@ -7346,6 +7763,7 @@ describe('plugin-meetings', () => {
7346
7763
  'meeting:self:guestAdmitted',
7347
7764
  {payload: test1}
7348
7765
  );
7766
+ assert.calledOnce(meeting.updateLLMConnection);
7349
7767
  done();
7350
7768
  });
7351
7769
 
@@ -8267,6 +8685,34 @@ describe('plugin-meetings', () => {
8267
8685
 
8268
8686
  checkParseMeetingInfo(expectedInfoToParse);
8269
8687
  });
8688
+
8689
+ it('should parse meeting info, set values, and return null when permissionToken is not present', () => {
8690
+ meeting.config.experimental = {enableMediaNegotiatedEvent: true};
8691
+ meeting.config.experimental.enableUnifiedMeetings = true;
8692
+ const FAKE_STRING_DESTINATION = 'sipUrl';
8693
+ const FAKE_MEETING_INFO = {
8694
+ conversationUrl: uuid1,
8695
+ locusUrl: url1,
8696
+ meetingJoinUrl: url2,
8697
+ meetingNumber: '12345',
8698
+ sipMeetingUri: test1,
8699
+ sipUrl: test1,
8700
+ owner: test2,
8701
+ };
8702
+
8703
+ meeting.parseMeetingInfo(FAKE_MEETING_INFO, FAKE_STRING_DESTINATION);
8704
+ const expectedInfoToParse = {
8705
+ conversationUrl: uuid1,
8706
+ locusUrl: url1,
8707
+ sipUri: test1,
8708
+ meetingNumber: '12345',
8709
+ meetingJoinUrl: url2,
8710
+ owner: test2,
8711
+ };
8712
+
8713
+ checkParseMeetingInfo(expectedInfoToParse);
8714
+ });
8715
+
8270
8716
  it('should parse interpretation info correctly', () => {
8271
8717
  const parseInterpretationInfo = sinon.spy(MeetingUtil, 'parseInterpretationInfo');
8272
8718
  const mockToggleOnData = {
@@ -9403,9 +9849,16 @@ describe('plugin-meetings', () => {
9403
9849
  it('check triggerAnnotationInfoEvent event', () => {
9404
9850
  TriggerProxy.trigger.reset();
9405
9851
  const annotationInfo = {version: '1', policy: 'Approval'};
9406
- const expectAnnotationInfo = {annotationInfo, meetingId: meeting.id};
9852
+ const expectAnnotationInfo = {
9853
+ annotationInfo,
9854
+ meetingId: meeting.id,
9855
+ resourceType: 'FILE',
9856
+ };
9407
9857
  meeting.webex.meetings = {};
9408
- meeting.triggerAnnotationInfoEvent({annotation: annotationInfo}, {});
9858
+ meeting.triggerAnnotationInfoEvent(
9859
+ {annotation: annotationInfo, resourceType: 'FILE'},
9860
+ {}
9861
+ );
9409
9862
  assert.calledWith(
9410
9863
  TriggerProxy.trigger,
9411
9864
  {},
@@ -9419,8 +9872,8 @@ describe('plugin-meetings', () => {
9419
9872
 
9420
9873
  TriggerProxy.trigger.reset();
9421
9874
  meeting.triggerAnnotationInfoEvent(
9422
- {annotation: annotationInfo},
9423
- {annotation: annotationInfo}
9875
+ {annotation: annotationInfo, resourceType: 'FILE'},
9876
+ {annotation: annotationInfo, resourceType: 'FILE'}
9424
9877
  );
9425
9878
  assert.notCalled(TriggerProxy.trigger);
9426
9879
 
@@ -9429,10 +9882,11 @@ describe('plugin-meetings', () => {
9429
9882
  const expectAnnotationInfoUpdated = {
9430
9883
  annotationInfo: annotationInfoUpdate,
9431
9884
  meetingId: meeting.id,
9885
+ resourceType: 'FILE',
9432
9886
  };
9433
9887
  meeting.triggerAnnotationInfoEvent(
9434
- {annotation: annotationInfoUpdate},
9435
- {annotation: annotationInfo}
9888
+ {annotation: annotationInfoUpdate, resourceType: 'FILE'},
9889
+ {annotation: annotationInfo, resourceType: 'FILE'}
9436
9890
  );
9437
9891
  assert.calledWith(
9438
9892
  TriggerProxy.trigger,
@@ -9446,7 +9900,10 @@ describe('plugin-meetings', () => {
9446
9900
  );
9447
9901
 
9448
9902
  TriggerProxy.trigger.reset();
9449
- meeting.triggerAnnotationInfoEvent(null, {annotation: annotationInfoUpdate});
9903
+ meeting.triggerAnnotationInfoEvent(null, {
9904
+ annotation: annotationInfoUpdate,
9905
+ resourceType: 'FILE',
9906
+ });
9450
9907
  assert.notCalled(TriggerProxy.trigger);
9451
9908
  });
9452
9909
  });
@@ -9470,6 +9927,11 @@ describe('plugin-meetings', () => {
9470
9927
  'https://board-a.wbx2.com/board/api/v1/channels/977a7330-54f4-11eb-b1ef-91f5eefc7bf3',
9471
9928
  };
9472
9929
 
9930
+ const SHARE_TYPE = {
9931
+ FILE: 'FILE',
9932
+ DESKTOP: 'DESKTOP',
9933
+ };
9934
+
9473
9935
  const DEVICE_URL = {
9474
9936
  LOCAL_WEB: 'my-web-url',
9475
9937
  LOCAL_MAC: 'my-mac-url',
@@ -9481,11 +9943,14 @@ describe('plugin-meetings', () => {
9481
9943
  beneficiaryId = null,
9482
9944
  disposition = null,
9483
9945
  deviceUrlSharing = null,
9484
- annotation = undefined
9946
+ annotation = undefined,
9947
+ resourceType = undefined
9485
9948
  ) => ({
9486
9949
  beneficiaryId,
9487
9950
  disposition,
9488
9951
  deviceUrlSharing,
9952
+ annotation,
9953
+ resourceType,
9489
9954
  });
9490
9955
  const generateWhiteboard = (
9491
9956
  beneficiaryId = null,
@@ -9504,7 +9969,8 @@ describe('plugin-meetings', () => {
9504
9969
  annotation,
9505
9970
  url,
9506
9971
  shareInstanceId,
9507
- deviceUrlSharing
9972
+ deviceUrlSharing,
9973
+ resourceType
9508
9974
  ) => {
9509
9975
  const newPayload = cloneDeep(payload);
9510
9976
 
@@ -9538,7 +10004,8 @@ describe('plugin-meetings', () => {
9538
10004
  beneficiaryId,
9539
10005
  FLOOR_ACTION.GRANTED,
9540
10006
  deviceUrlSharing,
9541
- annotation
10007
+ annotation,
10008
+ resourceType
9542
10009
  );
9543
10010
 
9544
10011
  if (isEqual(newPayload.current, newPayload.previous)) {
@@ -9599,6 +10066,7 @@ describe('plugin-meetings', () => {
9599
10066
  url,
9600
10067
  shareInstanceId,
9601
10068
  annotationInfo: undefined,
10069
+ resourceType: undefined,
9602
10070
  },
9603
10071
  });
9604
10072
  }
@@ -10440,7 +10908,8 @@ describe('plugin-meetings', () => {
10440
10908
  undefined,
10441
10909
  undefined,
10442
10910
  undefined,
10443
- DEVICE_URL.REMOTE_A
10911
+ DEVICE_URL.REMOTE_A,
10912
+ undefined
10444
10913
  );
10445
10914
  const data2 = generateData(
10446
10915
  data1.payload,
@@ -10453,9 +10922,39 @@ describe('plugin-meetings', () => {
10453
10922
  undefined,
10454
10923
  undefined,
10455
10924
  undefined,
10456
- DEVICE_URL.REMOTE_B
10925
+ DEVICE_URL.REMOTE_B,
10926
+ undefined
10927
+ );
10928
+ const data3 = generateData(data2.payload, false, true, USER_IDS.REMOTE_B, undefined);
10929
+
10930
+ payloadTestHelper([data1, data2, data3]);
10931
+ });
10932
+ });
10933
+
10934
+ describe('File Share --> Desktop Share', () => {
10935
+ it('Scenario #1: remote person A shares file then share desktop', () => {
10936
+ const data1 = generateData(
10937
+ blankPayload,
10938
+ true,
10939
+ true,
10940
+ USER_IDS.ME,
10941
+ undefined,
10942
+ false,
10943
+ undefined,
10944
+ undefined,
10945
+ undefined,
10946
+ undefined,
10947
+ DEVICE_URL.LOCAL_WEB,
10948
+ SHARE_TYPE.FILE
10949
+ );
10950
+ const data2 = generateData(
10951
+ data1.payload,
10952
+ true,
10953
+ false,
10954
+ USER_IDS.ME,
10955
+ SHARE_TYPE.DESKTOP
10457
10956
  );
10458
- const data3 = generateData(data2.payload, false, true, USER_IDS.REMOTE_B);
10957
+ const data3 = generateData(data2.payload, true, true, USER_IDS.ME);
10459
10958
 
10460
10959
  payloadTestHelper([data1, data2, data3]);
10461
10960
  });