@webex/plugin-meetings 3.0.0-next.2 → 3.0.0-next.21

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 (94) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.d.ts +3 -9
  4. package/dist/constants.js +4 -9
  5. package/dist/constants.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +6 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/interpretation/index.js +3 -3
  10. package/dist/interpretation/index.js.map +1 -1
  11. package/dist/interpretation/siLanguage.js +1 -1
  12. package/dist/locus-info/mediaSharesUtils.js +15 -1
  13. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  14. package/dist/media/index.js +4 -1
  15. package/dist/media/index.js.map +1 -1
  16. package/dist/mediaQualityMetrics/config.d.ts +9 -1
  17. package/dist/mediaQualityMetrics/config.js +10 -1
  18. package/dist/mediaQualityMetrics/config.js.map +1 -1
  19. package/dist/meeting/index.d.ts +18 -7
  20. package/dist/meeting/index.js +745 -567
  21. package/dist/meeting/index.js.map +1 -1
  22. package/dist/meeting/muteState.d.ts +2 -8
  23. package/dist/meeting/muteState.js +37 -25
  24. package/dist/meeting/muteState.js.map +1 -1
  25. package/dist/meeting/request.d.ts +3 -0
  26. package/dist/meeting/request.js +32 -23
  27. package/dist/meeting/request.js.map +1 -1
  28. package/dist/meeting/util.js +1 -0
  29. package/dist/meeting/util.js.map +1 -1
  30. package/dist/multistream/mediaRequestManager.d.ts +1 -2
  31. package/dist/multistream/mediaRequestManager.js.map +1 -1
  32. package/dist/multistream/remoteMediaGroup.d.ts +1 -1
  33. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  34. package/dist/multistream/remoteMediaManager.d.ts +1 -2
  35. package/dist/multistream/remoteMediaManager.js.map +1 -1
  36. package/dist/multistream/sendSlotManager.d.ts +1 -2
  37. package/dist/multistream/sendSlotManager.js.map +1 -1
  38. package/dist/reachability/request.js +12 -10
  39. package/dist/reachability/request.js.map +1 -1
  40. package/dist/reconnection-manager/index.js +2 -1
  41. package/dist/reconnection-manager/index.js.map +1 -1
  42. package/dist/roap/index.d.ts +10 -2
  43. package/dist/roap/index.js +15 -0
  44. package/dist/roap/index.js.map +1 -1
  45. package/dist/roap/request.js +3 -3
  46. package/dist/roap/request.js.map +1 -1
  47. package/dist/roap/turnDiscovery.d.ts +64 -17
  48. package/dist/roap/turnDiscovery.js +307 -126
  49. package/dist/roap/turnDiscovery.js.map +1 -1
  50. package/dist/statsAnalyzer/index.js +61 -41
  51. package/dist/statsAnalyzer/index.js.map +1 -1
  52. package/dist/webinar/index.js +1 -1
  53. package/package.json +22 -22
  54. package/src/constants.ts +3 -9
  55. package/src/index.ts +1 -0
  56. package/src/interpretation/index.ts +2 -2
  57. package/src/locus-info/mediaSharesUtils.ts +16 -0
  58. package/src/media/index.ts +3 -1
  59. package/src/mediaQualityMetrics/config.ts +11 -1
  60. package/src/meeting/index.ts +264 -90
  61. package/src/meeting/muteState.ts +34 -20
  62. package/src/meeting/request.ts +18 -2
  63. package/src/meeting/util.ts +1 -0
  64. package/src/multistream/mediaRequestManager.ts +1 -1
  65. package/src/multistream/remoteMediaGroup.ts +1 -1
  66. package/src/multistream/remoteMediaManager.ts +1 -2
  67. package/src/multistream/sendSlotManager.ts +1 -2
  68. package/src/reachability/request.ts +15 -11
  69. package/src/reconnection-manager/index.ts +1 -1
  70. package/src/roap/index.ts +25 -3
  71. package/src/roap/request.ts +3 -3
  72. package/src/roap/turnDiscovery.ts +244 -78
  73. package/src/statsAnalyzer/index.ts +72 -43
  74. package/test/integration/spec/journey.js +14 -14
  75. package/test/integration/spec/space-meeting.js +1 -1
  76. package/test/unit/spec/interpretation/index.ts +4 -1
  77. package/test/unit/spec/locus-info/mediaSharesUtils.ts +9 -0
  78. package/test/unit/spec/media/index.ts +89 -78
  79. package/test/unit/spec/meeting/index.js +611 -125
  80. package/test/unit/spec/meeting/muteState.js +219 -67
  81. package/test/unit/spec/meeting/request.js +21 -0
  82. package/test/unit/spec/meeting/utils.js +6 -1
  83. package/test/unit/spec/meetings/index.js +27 -20
  84. package/test/unit/spec/multistream/remoteMediaGroup.ts +0 -1
  85. package/test/unit/spec/multistream/remoteMediaManager.ts +0 -1
  86. package/test/unit/spec/reachability/request.js +15 -7
  87. package/test/unit/spec/reconnection-manager/index.js +28 -0
  88. package/test/unit/spec/roap/index.ts +61 -6
  89. package/test/unit/spec/roap/turnDiscovery.ts +298 -16
  90. package/test/unit/spec/stats-analyzer/index.js +183 -8
  91. package/dist/member/member.types.d.ts +0 -11
  92. package/dist/member/member.types.js +0 -17
  93. package/dist/member/member.types.js.map +0 -1
  94. package/src/member/member.types.ts +0 -13
@@ -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,
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,
631
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,47 +977,37 @@ 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', () => {
@@ -1134,7 +1251,7 @@ describe('plugin-meetings', () => {
1134
1251
  file: 'meeting/index',
1135
1252
  function: 'join',
1136
1253
  },
1137
- EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED,
1254
+ EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED
1138
1255
  );
1139
1256
  });
1140
1257
 
@@ -1412,7 +1529,6 @@ describe('plugin-meetings', () => {
1412
1529
  describe('#addMedia', () => {
1413
1530
  const muteStateStub = {
1414
1531
  handleClientRequest: sinon.stub().returns(Promise.resolve(true)),
1415
- applyClientStateLocally: sinon.stub().returns(Promise.resolve(true)),
1416
1532
  };
1417
1533
 
1418
1534
  let fakeMediaConnection;
@@ -2506,6 +2622,7 @@ describe('plugin-meetings', () => {
2506
2622
 
2507
2623
  beforeEach(async () => {
2508
2624
  meeting.meetingState = 'ACTIVE';
2625
+ meeting.remoteShareInstanceId = '1234';
2509
2626
  prevConfigValue = meeting.config.stats.enableStatsAnalyzer;
2510
2627
 
2511
2628
  meeting.config.stats.enableStatsAnalyzer = true;
@@ -2611,6 +2728,66 @@ describe('plugin-meetings', () => {
2611
2728
  });
2612
2729
  });
2613
2730
 
2731
+ it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics for share', async () => {
2732
+ statsAnalyzerStub.emit(
2733
+ {file: 'test', function: 'test'},
2734
+ StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STARTED,
2735
+ {type: 'share'}
2736
+ );
2737
+
2738
+ assert.calledWith(
2739
+ TriggerProxy.trigger,
2740
+ sinon.match.instanceOf(Meeting),
2741
+ {
2742
+ file: 'meeting/index',
2743
+ function: 'addMedia',
2744
+ },
2745
+ EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
2746
+ {
2747
+ type: 'share',
2748
+ }
2749
+ );
2750
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2751
+ name: 'client.media.rx.start',
2752
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2753
+ options: {
2754
+ meetingId: meeting.id,
2755
+ },
2756
+ });
2757
+
2758
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2759
+ name: 'client.media.render.start',
2760
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2761
+ options: {
2762
+ meetingId: meeting.id,
2763
+ },
2764
+ });
2765
+ });
2766
+
2767
+ it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
2768
+ statsAnalyzerStub.emit(
2769
+ {file: 'test', function: 'test'},
2770
+ StatsAnalyzerModule.EVENTS.REMOTE_MEDIA_STOPPED,
2771
+ {type: 'share'}
2772
+ );
2773
+
2774
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2775
+ name: 'client.media.rx.stop',
2776
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2777
+ options: {
2778
+ meetingId: meeting.id,
2779
+ },
2780
+ });
2781
+
2782
+ assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
2783
+ name: 'client.media.render.stop',
2784
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
2785
+ options: {
2786
+ meetingId: meeting.id,
2787
+ },
2788
+ });
2789
+ });
2790
+
2614
2791
  it('calls submitMQE correctly', async () => {
2615
2792
  const fakeData = {intervalMetadata: {bla: 'bla'}};
2616
2793
 
@@ -2855,9 +3032,10 @@ describe('plugin-meetings', () => {
2855
3032
  meeting.setMercuryListener = sinon.stub();
2856
3033
  meeting.locusInfo.onFullLocus = sinon.stub();
2857
3034
  meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
2858
- meeting.roap.doTurnDiscovery = sinon
2859
- .stub()
2860
- .resolves({turnServerInfo: {}, turnDiscoverySkippedReason: 'reachability'});
3035
+ meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3036
+ turnServerInfo: {url: 'turn-url', username: 'turn user', password: 'turn password'},
3037
+ turnDiscoverySkippedReason: 'reachability',
3038
+ });
2861
3039
  meeting.deferSDPAnswer = new Defer();
2862
3040
  meeting.deferSDPAnswer.resolve();
2863
3041
  meeting.webex.meetings.meetingCollection = new MeetingCollection();
@@ -2868,7 +3046,7 @@ describe('plugin-meetings', () => {
2868
3046
  // setup things that are expected to be the same across all the tests and are actually irrelevant for these tests
2869
3047
  expectedDebugId = `MC-${meeting.id.substring(0, 4)}`;
2870
3048
  expectedMediaConnectionConfig = {
2871
- iceServers: [{urls: undefined, username: '', credential: ''}],
3049
+ iceServers: [{urls: 'turn-url', username: 'turn user', credential: 'turn password'}],
2872
3050
  skipInactiveTransceivers: false,
2873
3051
  requireH264: true,
2874
3052
  sdpMunging: {
@@ -2892,9 +3070,13 @@ describe('plugin-meetings', () => {
2892
3070
  getSettings: sinon.stub().returns({
2893
3071
  deviceId: 'some device id',
2894
3072
  }),
2895
- muted: false,
3073
+ userMuted: false,
3074
+ systemMuted: false,
3075
+ get muted() {
3076
+ return this.userMuted || this.systemMuted;
3077
+ },
2896
3078
  setUnmuteAllowed: sinon.stub(),
2897
- setMuted: sinon.stub(),
3079
+ setUserMuted: sinon.stub(),
2898
3080
  setServerMuted: sinon.stub(),
2899
3081
  outputStream: {
2900
3082
  getTracks: () => {
@@ -3129,28 +3311,52 @@ describe('plugin-meetings', () => {
3129
3311
  if (stream !== undefined) {
3130
3312
  switch (type) {
3131
3313
  case 'audio':
3132
- assert.calledOnceWithExactly(
3133
- meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream,
3134
- stream
3135
- );
3314
+ if (stream?.readyState === 'ended') {
3315
+ assert.notCalled(
3316
+ meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream
3317
+ );
3318
+ } else {
3319
+ assert.calledOnceWithExactly(
3320
+ meeting.sendSlotManager.getSlot(MediaType.AudioMain).publishStream,
3321
+ stream
3322
+ );
3323
+ }
3136
3324
  break;
3137
3325
  case 'video':
3138
- assert.calledOnceWithExactly(
3139
- meeting.sendSlotManager.getSlot(MediaType.VideoMain).publishStream,
3140
- stream
3141
- );
3326
+ if (stream?.readyState === 'ended') {
3327
+ assert.notCalled(
3328
+ meeting.sendSlotManager.getSlot(MediaType.VideoMain).publishStream
3329
+ );
3330
+ } else {
3331
+ assert.calledOnceWithExactly(
3332
+ meeting.sendSlotManager.getSlot(MediaType.VideoMain).publishStream,
3333
+ stream
3334
+ );
3335
+ }
3142
3336
  break;
3143
3337
  case 'screenShareAudio':
3144
- assert.calledOnceWithExactly(
3145
- meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
3146
- stream
3147
- );
3338
+ if (stream?.readyState === 'ended') {
3339
+ assert.notCalled(
3340
+ meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream
3341
+ );
3342
+ } else {
3343
+ assert.calledOnceWithExactly(
3344
+ meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
3345
+ stream
3346
+ );
3347
+ }
3148
3348
  break;
3149
3349
  case 'screenShareVideo':
3150
- assert.calledOnceWithExactly(
3151
- meeting.sendSlotManager.getSlot(MediaType.VideoSlides).publishStream,
3152
- stream
3153
- );
3350
+ if (stream?.readyState === 'ended') {
3351
+ assert.notCalled(
3352
+ meeting.sendSlotManager.getSlot(MediaType.VideoSlides).publishStream
3353
+ );
3354
+ } else {
3355
+ assert.calledOnceWithExactly(
3356
+ meeting.sendSlotManager.getSlot(MediaType.VideoSlides).publishStream,
3357
+ stream
3358
+ );
3359
+ }
3154
3360
  break;
3155
3361
  }
3156
3362
  }
@@ -3178,7 +3384,7 @@ describe('plugin-meetings', () => {
3178
3384
  }
3179
3385
  };
3180
3386
 
3181
- it('addMedia() works correctly when media is enabled without tracks to publish', async () => {
3387
+ it('addMedia() works correctly when media is enabled without streams to publish', async () => {
3182
3388
  await meeting.addMedia();
3183
3389
  await simulateRoapOffer();
3184
3390
  await simulateRoapOk();
@@ -3212,6 +3418,7 @@ describe('plugin-meetings', () => {
3212
3418
  });
3213
3419
 
3214
3420
  it('addMedia() works correctly when media is enabled with streams to publish', async () => {
3421
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3215
3422
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3216
3423
  await simulateRoapOffer();
3217
3424
  await simulateRoapOk();
@@ -3242,10 +3449,82 @@ describe('plugin-meetings', () => {
3242
3449
 
3243
3450
  // and that these were the only /media requests that were sent
3244
3451
  assert.calledTwice(locusMediaRequestStub);
3452
+
3453
+ assert.calledOnce(handleDeviceLoggingSpy);
3454
+ });
3455
+
3456
+ it('addMedia() works correctly when media is enabled with streams to publish and stream is user muted', async () => {
3457
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3458
+ fakeMicrophoneStream.userMuted = true;
3459
+
3460
+ await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3461
+ await simulateRoapOffer();
3462
+ await simulateRoapOk();
3463
+
3464
+ // check RoapMediaConnection was created correctly
3465
+ checkMediaConnectionCreated({
3466
+ mediaConnectionConfig: expectedMediaConnectionConfig,
3467
+ localStreams: {
3468
+ audio: fakeMicrophoneStream,
3469
+ video: undefined,
3470
+ screenShareVideo: undefined,
3471
+ screenShareAudio: undefined,
3472
+ },
3473
+ direction: {
3474
+ audio: 'sendrecv',
3475
+ video: 'sendrecv',
3476
+ screenShare: 'recvonly',
3477
+ },
3478
+ remoteQualityLevel: 'HIGH',
3479
+ expectedDebugId,
3480
+ meetingId: meeting.id,
3481
+ });
3482
+ // and SDP offer was sent with the right audioMuted/videoMuted values
3483
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
3484
+ // check OK was sent with the right audioMuted/videoMuted values
3485
+ checkOkSent({audioMuted: true, videoMuted: true});
3486
+
3487
+ // and that these were the only /media requests that were sent
3488
+ assert.calledTwice(locusMediaRequestStub);
3489
+ assert.calledOnce(handleDeviceLoggingSpy);
3490
+ });
3491
+
3492
+ it('addMedia() works correctly when media is enabled with tracks to publish and track is ended', async () => {
3493
+ fakeMicrophoneStream.readyState = 'ended';
3494
+
3495
+ await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3496
+ await simulateRoapOffer();
3497
+ await simulateRoapOk();
3498
+
3499
+ // check RoapMediaConnection was created correctly
3500
+ checkMediaConnectionCreated({
3501
+ mediaConnectionConfig: expectedMediaConnectionConfig,
3502
+ localStreams: {
3503
+ audio: undefined,
3504
+ video: undefined,
3505
+ screenShareVideo: undefined,
3506
+ screenShareAudio: undefined,
3507
+ },
3508
+ direction: {
3509
+ audio: 'sendrecv',
3510
+ video: 'sendrecv',
3511
+ screenShare: 'recvonly',
3512
+ },
3513
+ remoteQualityLevel: 'HIGH',
3514
+ expectedDebugId,
3515
+ meetingId: meeting.id,
3516
+ });
3517
+ // and SDP offer was sent with the right audioMuted/videoMuted values
3518
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
3519
+ // check OK was sent with the right audioMuted/videoMuted values
3520
+ checkOkSent({audioMuted: true, videoMuted: true});
3521
+
3522
+ // and that these were the only /media requests that were sent
3523
+ assert.calledTwice(locusMediaRequestStub);
3245
3524
  });
3246
3525
 
3247
- it('addMedia() works correctly when media is enabled with tracks to publish and track is muted', async () => {
3248
- fakeMicrophoneStream.muted = true;
3526
+ it('addMedia() works correctly when media is enabled with streams to publish and stream is system muted', async () => {
3527
+ fakeMicrophoneStream.systemMuted = true;
3249
3528
 
3250
3529
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3251
3530
  await simulateRoapOffer();
@@ -3278,7 +3557,8 @@ describe('plugin-meetings', () => {
3278
3557
  assert.calledTwice(locusMediaRequestStub);
3279
3558
  });
3280
3559
 
3281
- it('addMedia() works correctly when media is disabled with tracks to publish', async () => {
3560
+ it('addMedia() works correctly when media is disabled with streams to publish', async () => {
3561
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3282
3562
  await meeting.addMedia({
3283
3563
  localStreams: {microphone: fakeMicrophoneStream},
3284
3564
  audioEnabled: false,
@@ -3312,9 +3592,23 @@ describe('plugin-meetings', () => {
3312
3592
 
3313
3593
  // and that these were the only /media requests that were sent
3314
3594
  assert.calledTwice(locusMediaRequestStub);
3595
+ assert.calledOnce(handleDeviceLoggingSpy);
3315
3596
  });
3316
3597
 
3317
- it('addMedia() works correctly when media is disabled with no tracks to publish', async () => {
3598
+ it('handleDeviceLogging not called when media is disabled', async () => {
3599
+ const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
3600
+ await meeting.addMedia({
3601
+ localStreams: {microphone: fakeMicrophoneStream},
3602
+ audioEnabled: false,
3603
+ videoEnabled: false
3604
+ });
3605
+ await simulateRoapOffer();
3606
+ await simulateRoapOk();
3607
+
3608
+ assert.notCalled(handleDeviceLoggingSpy);
3609
+ })
3610
+
3611
+ it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
3318
3612
  await meeting.addMedia({audioEnabled: false});
3319
3613
  await simulateRoapOffer();
3320
3614
  await simulateRoapOk();
@@ -3347,7 +3641,7 @@ describe('plugin-meetings', () => {
3347
3641
  assert.calledTwice(locusMediaRequestStub);
3348
3642
  });
3349
3643
 
3350
- it('addMedia() works correctly when video is disabled with no tracks to publish', async () => {
3644
+ it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
3351
3645
  await meeting.addMedia({videoEnabled: false});
3352
3646
  await simulateRoapOffer();
3353
3647
  await simulateRoapOk();
@@ -3380,7 +3674,7 @@ describe('plugin-meetings', () => {
3380
3674
  assert.calledTwice(locusMediaRequestStub);
3381
3675
  });
3382
3676
 
3383
- it('addMedia() works correctly when screen share is disabled with no tracks to publish', async () => {
3677
+ it('addMedia() works correctly when screen share is disabled with no streams to publish', async () => {
3384
3678
  await meeting.addMedia({shareAudioEnabled: false, shareVideoEnabled: false});
3385
3679
  await simulateRoapOffer();
3386
3680
  await simulateRoapOk();
@@ -3481,9 +3775,13 @@ describe('plugin-meetings', () => {
3481
3775
  const fakeMicrophoneStream2 = {
3482
3776
  on: sinon.stub(),
3483
3777
  off: sinon.stub(),
3484
- muted: false,
3778
+ userMuted: false,
3779
+ systemMuted: false,
3780
+ get muted() {
3781
+ return this.userMuted || this.systemMuted;
3782
+ },
3485
3783
  setUnmuteAllowed: sinon.stub(),
3486
- setMuted: sinon.stub(),
3784
+ setUserMuted: sinon.stub(),
3487
3785
  outputStream: {
3488
3786
  getTracks: () => {
3489
3787
  return [
@@ -3720,12 +4018,12 @@ describe('plugin-meetings', () => {
3720
4018
  });
3721
4019
 
3722
4020
  [
3723
- {mute: true, title: 'muting a track before confluence is created'},
3724
- {mute: false, title: 'unmuting a track before confluence is created'},
4021
+ {mute: true, title: 'user muting a track before confluence is created'},
4022
+ {mute: false, title: 'user unmuting a track before confluence is created'},
3725
4023
  ].forEach(({mute, title}) =>
3726
4024
  it(title, async () => {
3727
4025
  // initialize the microphone mute state to opposite of what we do in the test
3728
- fakeMicrophoneStream.muted = !mute;
4026
+ fakeMicrophoneStream.userMuted = !mute;
3729
4027
 
3730
4028
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
3731
4029
  await stableState();
@@ -3734,10 +4032,54 @@ describe('plugin-meetings', () => {
3734
4032
 
3735
4033
  assert.equal(
3736
4034
  fakeMicrophoneStream.on.getCall(0).args[0],
3737
- StreamEventNames.MuteStateChange
4035
+ LocalStreamEventNames.UserMuteStateChange
3738
4036
  );
3739
4037
  const mutedListener = fakeMicrophoneStream.on.getCall(0).args[1];
3740
4038
  // simulate track being muted
4039
+ fakeMicrophoneStream.userMuted = mute;
4040
+ mutedListener(mute);
4041
+
4042
+ await stableState();
4043
+
4044
+ // nothing should happen
4045
+ assert.notCalled(locusMediaRequestStub);
4046
+ assert.notCalled(fakeRoapMediaConnection.update);
4047
+
4048
+ // now simulate roap offer and ok
4049
+ await simulateRoapOffer();
4050
+ await simulateRoapOk();
4051
+
4052
+ // it should be sent with the right mute status
4053
+ checkSdpOfferSent({audioMuted: mute, videoMuted: true});
4054
+ // check OK was sent with the right audioMuted/videoMuted values
4055
+ checkOkSent({audioMuted: mute, videoMuted: true});
4056
+
4057
+ // nothing else should happen
4058
+ assert.calledTwice(locusMediaRequestStub);
4059
+ assert.notCalled(fakeRoapMediaConnection.update);
4060
+ })
4061
+ );
4062
+
4063
+ [
4064
+ {mute: true, title: 'system muting a track before confluence is created'},
4065
+ {mute: false, title: 'system unmuting a track before confluence is created'},
4066
+ ].forEach(({mute, title}) =>
4067
+ it(title, async () => {
4068
+ // initialize the microphone mute state to opposite of what we do in the test
4069
+ fakeMicrophoneStream.systemMuted = !mute;
4070
+
4071
+ await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
4072
+ await stableState();
4073
+
4074
+ resetHistory();
4075
+
4076
+ assert.equal(
4077
+ fakeMicrophoneStream.on.getCall(0).args[0],
4078
+ LocalStreamEventNames.UserMuteStateChange
4079
+ );
4080
+ const mutedListener = fakeMicrophoneStream.on.getCall(0).args[1];
4081
+ // simulate track being muted
4082
+ fakeMicrophoneStream.systemMuted = mute;
3741
4083
  mutedListener(mute);
3742
4084
 
3743
4085
  await stableState();
@@ -6138,6 +6480,65 @@ describe('plugin-meetings', () => {
6138
6480
  checkScreenShareVideoPublished(videoShareStream);
6139
6481
  checkScreenShareAudioPublished(audioShareStream);
6140
6482
  });
6483
+
6484
+ [
6485
+ {
6486
+ endedStream: 'microphone',
6487
+ streams: {
6488
+ microphone: {
6489
+ readyState: 'ended',
6490
+ },
6491
+ camera: undefined,
6492
+ screenShare: {
6493
+ audio: undefined,
6494
+ video: undefined,
6495
+ },
6496
+ },
6497
+ },
6498
+ {
6499
+ endedStream: 'camera',
6500
+ streams: {
6501
+ microphone: undefined,
6502
+ camera: {
6503
+ readyState: 'ended',
6504
+ },
6505
+ screenShare: {
6506
+ audio: undefined,
6507
+ video: undefined,
6508
+ },
6509
+ },
6510
+ },
6511
+ {
6512
+ endedStream: 'screenShare audio',
6513
+ streams: {
6514
+ microphone: undefined,
6515
+ camera: undefined,
6516
+ screenShare: {
6517
+ audio: {
6518
+ readyState: 'ended',
6519
+ },
6520
+ video: undefined,
6521
+ },
6522
+ },
6523
+ },
6524
+ {
6525
+ endedStream: 'screenShare video',
6526
+ streams: {
6527
+ microphone: undefined,
6528
+ camera: undefined,
6529
+ screenShare: {
6530
+ audio: undefined,
6531
+ video: {
6532
+ readyState: 'ended',
6533
+ },
6534
+ },
6535
+ },
6536
+ },
6537
+ ].forEach(({endedStream, streams}) => {
6538
+ it(`throws error if readyState of ${endedStream} is ended`, async () => {
6539
+ assert.isRejected(meeting.publishStreams(streams));
6540
+ });
6541
+ });
6141
6542
  });
6142
6543
 
6143
6544
  describe('unpublishStreams', () => {
@@ -6272,11 +6673,11 @@ describe('plugin-meetings', () => {
6272
6673
  meeting.sendSlotManager.setNamedMediaGroups = sinon.stub().returns(undefined);
6273
6674
  });
6274
6675
  it('should throw error if not audio type', () => {
6275
- expect(() => meeting.setSendNamedMediaGroup(MediaType.VideoMain, 20)).to.throw(`cannot set send named media group which media type is ${MediaType.VideoMain}`)
6276
-
6676
+ expect(() => meeting.setSendNamedMediaGroup(MediaType.VideoMain, 20)).to.throw(
6677
+ `cannot set send named media group which media type is ${MediaType.VideoMain}`
6678
+ );
6277
6679
  });
6278
6680
  it('fails if there is no media connection', () => {
6279
-
6280
6681
  meeting.mediaProperties.webrtcMediaConnection = undefined;
6281
6682
  meeting.setSendNamedMediaGroup('AUDIO-MAIN', 20);
6282
6683
  assert.notCalled(meeting.sendSlotManager.setNamedMediaGroups);
@@ -6285,8 +6686,10 @@ describe('plugin-meetings', () => {
6285
6686
  it('success if there is media connection', () => {
6286
6687
  meeting.isMultistream = true;
6287
6688
  meeting.mediaProperties.webrtcMediaConnection = true;
6288
- meeting.setSendNamedMediaGroup("AUDIO-MAIN", 20);
6289
- assert.calledOnceWithExactly(meeting.sendSlotManager.setNamedMediaGroups, "AUDIO-MAIN", [{type: 1, value: 20}]);
6689
+ meeting.setSendNamedMediaGroup('AUDIO-MAIN', 20);
6690
+ assert.calledOnceWithExactly(meeting.sendSlotManager.setNamedMediaGroups, 'AUDIO-MAIN', [
6691
+ {type: 1, value: 20},
6692
+ ]);
6290
6693
  });
6291
6694
  });
6292
6695
 
@@ -7360,6 +7763,7 @@ describe('plugin-meetings', () => {
7360
7763
  });
7361
7764
  it('listens to the self admitted guest event', (done) => {
7362
7765
  meeting.stopKeepAlive = sinon.stub();
7766
+ meeting.updateLLMConnection = sinon.stub();
7363
7767
  meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
7364
7768
  assert.calledOnceWithExactly(meeting.stopKeepAlive);
7365
7769
  assert.calledThrice(TriggerProxy.trigger);
@@ -7370,6 +7774,7 @@ describe('plugin-meetings', () => {
7370
7774
  'meeting:self:guestAdmitted',
7371
7775
  {payload: test1}
7372
7776
  );
7777
+ assert.calledOnce(meeting.updateLLMConnection);
7373
7778
  done();
7374
7779
  });
7375
7780
 
@@ -8291,6 +8696,34 @@ describe('plugin-meetings', () => {
8291
8696
 
8292
8697
  checkParseMeetingInfo(expectedInfoToParse);
8293
8698
  });
8699
+
8700
+ it('should parse meeting info, set values, and return null when permissionToken is not present', () => {
8701
+ meeting.config.experimental = {enableMediaNegotiatedEvent: true};
8702
+ meeting.config.experimental.enableUnifiedMeetings = true;
8703
+ const FAKE_STRING_DESTINATION = 'sipUrl';
8704
+ const FAKE_MEETING_INFO = {
8705
+ conversationUrl: uuid1,
8706
+ locusUrl: url1,
8707
+ meetingJoinUrl: url2,
8708
+ meetingNumber: '12345',
8709
+ sipMeetingUri: test1,
8710
+ sipUrl: test1,
8711
+ owner: test2,
8712
+ };
8713
+
8714
+ meeting.parseMeetingInfo(FAKE_MEETING_INFO, FAKE_STRING_DESTINATION);
8715
+ const expectedInfoToParse = {
8716
+ conversationUrl: uuid1,
8717
+ locusUrl: url1,
8718
+ sipUri: test1,
8719
+ meetingNumber: '12345',
8720
+ meetingJoinUrl: url2,
8721
+ owner: test2,
8722
+ };
8723
+
8724
+ checkParseMeetingInfo(expectedInfoToParse);
8725
+ });
8726
+
8294
8727
  it('should parse interpretation info correctly', () => {
8295
8728
  const parseInterpretationInfo = sinon.spy(MeetingUtil, 'parseInterpretationInfo');
8296
8729
  const mockToggleOnData = {
@@ -9427,9 +9860,16 @@ describe('plugin-meetings', () => {
9427
9860
  it('check triggerAnnotationInfoEvent event', () => {
9428
9861
  TriggerProxy.trigger.reset();
9429
9862
  const annotationInfo = {version: '1', policy: 'Approval'};
9430
- const expectAnnotationInfo = {annotationInfo, meetingId: meeting.id};
9863
+ const expectAnnotationInfo = {
9864
+ annotationInfo,
9865
+ meetingId: meeting.id,
9866
+ resourceType: 'FILE',
9867
+ };
9431
9868
  meeting.webex.meetings = {};
9432
- meeting.triggerAnnotationInfoEvent({annotation: annotationInfo}, {});
9869
+ meeting.triggerAnnotationInfoEvent(
9870
+ {annotation: annotationInfo, resourceType: 'FILE'},
9871
+ {}
9872
+ );
9433
9873
  assert.calledWith(
9434
9874
  TriggerProxy.trigger,
9435
9875
  {},
@@ -9443,8 +9883,8 @@ describe('plugin-meetings', () => {
9443
9883
 
9444
9884
  TriggerProxy.trigger.reset();
9445
9885
  meeting.triggerAnnotationInfoEvent(
9446
- {annotation: annotationInfo},
9447
- {annotation: annotationInfo}
9886
+ {annotation: annotationInfo, resourceType: 'FILE'},
9887
+ {annotation: annotationInfo, resourceType: 'FILE'}
9448
9888
  );
9449
9889
  assert.notCalled(TriggerProxy.trigger);
9450
9890
 
@@ -9453,10 +9893,11 @@ describe('plugin-meetings', () => {
9453
9893
  const expectAnnotationInfoUpdated = {
9454
9894
  annotationInfo: annotationInfoUpdate,
9455
9895
  meetingId: meeting.id,
9896
+ resourceType: 'FILE',
9456
9897
  };
9457
9898
  meeting.triggerAnnotationInfoEvent(
9458
- {annotation: annotationInfoUpdate},
9459
- {annotation: annotationInfo}
9899
+ {annotation: annotationInfoUpdate, resourceType: 'FILE'},
9900
+ {annotation: annotationInfo, resourceType: 'FILE'}
9460
9901
  );
9461
9902
  assert.calledWith(
9462
9903
  TriggerProxy.trigger,
@@ -9470,7 +9911,10 @@ describe('plugin-meetings', () => {
9470
9911
  );
9471
9912
 
9472
9913
  TriggerProxy.trigger.reset();
9473
- meeting.triggerAnnotationInfoEvent(null, {annotation: annotationInfoUpdate});
9914
+ meeting.triggerAnnotationInfoEvent(null, {
9915
+ annotation: annotationInfoUpdate,
9916
+ resourceType: 'FILE',
9917
+ });
9474
9918
  assert.notCalled(TriggerProxy.trigger);
9475
9919
  });
9476
9920
  });
@@ -9494,6 +9938,11 @@ describe('plugin-meetings', () => {
9494
9938
  'https://board-a.wbx2.com/board/api/v1/channels/977a7330-54f4-11eb-b1ef-91f5eefc7bf3',
9495
9939
  };
9496
9940
 
9941
+ const SHARE_TYPE = {
9942
+ FILE: 'FILE',
9943
+ DESKTOP: 'DESKTOP',
9944
+ };
9945
+
9497
9946
  const DEVICE_URL = {
9498
9947
  LOCAL_WEB: 'my-web-url',
9499
9948
  LOCAL_MAC: 'my-mac-url',
@@ -9505,11 +9954,14 @@ describe('plugin-meetings', () => {
9505
9954
  beneficiaryId = null,
9506
9955
  disposition = null,
9507
9956
  deviceUrlSharing = null,
9508
- annotation = undefined
9957
+ annotation = undefined,
9958
+ resourceType = undefined
9509
9959
  ) => ({
9510
9960
  beneficiaryId,
9511
9961
  disposition,
9512
9962
  deviceUrlSharing,
9963
+ annotation,
9964
+ resourceType,
9513
9965
  });
9514
9966
  const generateWhiteboard = (
9515
9967
  beneficiaryId = null,
@@ -9528,7 +9980,8 @@ describe('plugin-meetings', () => {
9528
9980
  annotation,
9529
9981
  url,
9530
9982
  shareInstanceId,
9531
- deviceUrlSharing
9983
+ deviceUrlSharing,
9984
+ resourceType
9532
9985
  ) => {
9533
9986
  const newPayload = cloneDeep(payload);
9534
9987
 
@@ -9562,7 +10015,8 @@ describe('plugin-meetings', () => {
9562
10015
  beneficiaryId,
9563
10016
  FLOOR_ACTION.GRANTED,
9564
10017
  deviceUrlSharing,
9565
- annotation
10018
+ annotation,
10019
+ resourceType
9566
10020
  );
9567
10021
 
9568
10022
  if (isEqual(newPayload.current, newPayload.previous)) {
@@ -9623,6 +10077,7 @@ describe('plugin-meetings', () => {
9623
10077
  url,
9624
10078
  shareInstanceId,
9625
10079
  annotationInfo: undefined,
10080
+ resourceType: undefined,
9626
10081
  },
9627
10082
  });
9628
10083
  }
@@ -10464,7 +10919,8 @@ describe('plugin-meetings', () => {
10464
10919
  undefined,
10465
10920
  undefined,
10466
10921
  undefined,
10467
- DEVICE_URL.REMOTE_A
10922
+ DEVICE_URL.REMOTE_A,
10923
+ undefined
10468
10924
  );
10469
10925
  const data2 = generateData(
10470
10926
  data1.payload,
@@ -10477,9 +10933,39 @@ describe('plugin-meetings', () => {
10477
10933
  undefined,
10478
10934
  undefined,
10479
10935
  undefined,
10480
- DEVICE_URL.REMOTE_B
10936
+ DEVICE_URL.REMOTE_B,
10937
+ undefined
10938
+ );
10939
+ const data3 = generateData(data2.payload, false, true, USER_IDS.REMOTE_B, undefined);
10940
+
10941
+ payloadTestHelper([data1, data2, data3]);
10942
+ });
10943
+ });
10944
+
10945
+ describe('File Share --> Desktop Share', () => {
10946
+ it('Scenario #1: remote person A shares file then share desktop', () => {
10947
+ const data1 = generateData(
10948
+ blankPayload,
10949
+ true,
10950
+ true,
10951
+ USER_IDS.ME,
10952
+ undefined,
10953
+ false,
10954
+ undefined,
10955
+ undefined,
10956
+ undefined,
10957
+ undefined,
10958
+ DEVICE_URL.LOCAL_WEB,
10959
+ SHARE_TYPE.FILE
10960
+ );
10961
+ const data2 = generateData(
10962
+ data1.payload,
10963
+ true,
10964
+ false,
10965
+ USER_IDS.ME,
10966
+ SHARE_TYPE.DESKTOP
10481
10967
  );
10482
- const data3 = generateData(data2.payload, false, true, USER_IDS.REMOTE_B);
10968
+ const data3 = generateData(data2.payload, true, true, USER_IDS.ME);
10483
10969
 
10484
10970
  payloadTestHelper([data1, data2, data3]);
10485
10971
  });