@webex/plugin-meetings 3.0.0-beta.146 → 3.0.0-beta.148

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 (60) hide show
  1. package/dist/annotation/index.js +0 -2
  2. package/dist/annotation/index.js.map +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/common/errors/webex-errors.js +3 -2
  6. package/dist/common/errors/webex-errors.js.map +1 -1
  7. package/dist/config.js +1 -7
  8. package/dist/config.js.map +1 -1
  9. package/dist/constants.js +7 -15
  10. package/dist/constants.js.map +1 -1
  11. package/dist/index.js +6 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/media/index.js +5 -56
  14. package/dist/media/index.js.map +1 -1
  15. package/dist/media/properties.js +15 -93
  16. package/dist/media/properties.js.map +1 -1
  17. package/dist/meeting/index.js +1112 -1873
  18. package/dist/meeting/index.js.map +1 -1
  19. package/dist/meeting/muteState.js +88 -184
  20. package/dist/meeting/muteState.js.map +1 -1
  21. package/dist/meeting/util.js +1 -23
  22. package/dist/meeting/util.js.map +1 -1
  23. package/dist/meetings/index.js +1 -2
  24. package/dist/meetings/index.js.map +1 -1
  25. package/dist/reconnection-manager/index.js +153 -134
  26. package/dist/reconnection-manager/index.js.map +1 -1
  27. package/dist/roap/index.js +8 -7
  28. package/dist/roap/index.js.map +1 -1
  29. package/dist/types/common/errors/webex-errors.d.ts +1 -1
  30. package/dist/types/config.d.ts +0 -6
  31. package/dist/types/constants.d.ts +1 -18
  32. package/dist/types/index.d.ts +1 -1
  33. package/dist/types/media/properties.d.ts +16 -38
  34. package/dist/types/meeting/index.d.ts +97 -353
  35. package/dist/types/meeting/muteState.d.ts +36 -38
  36. package/dist/types/meeting/util.d.ts +2 -4
  37. package/package.json +19 -19
  38. package/src/annotation/index.ts +0 -2
  39. package/src/common/errors/webex-errors.ts +6 -2
  40. package/src/config.ts +0 -6
  41. package/src/constants.ts +1 -14
  42. package/src/index.ts +1 -0
  43. package/src/media/index.ts +10 -53
  44. package/src/media/properties.ts +32 -92
  45. package/src/meeting/index.ts +544 -1567
  46. package/src/meeting/muteState.ts +87 -178
  47. package/src/meeting/util.ts +3 -24
  48. package/src/meetings/index.ts +0 -1
  49. package/src/reconnection-manager/index.ts +4 -9
  50. package/src/roap/index.ts +13 -14
  51. package/test/integration/spec/converged-space-meetings.js +59 -3
  52. package/test/integration/spec/journey.js +330 -256
  53. package/test/integration/spec/space-meeting.js +75 -3
  54. package/test/unit/spec/annotation/index.ts +4 -4
  55. package/test/unit/spec/meeting/index.js +811 -1367
  56. package/test/unit/spec/meeting/muteState.js +238 -394
  57. package/test/unit/spec/meeting/utils.js +2 -9
  58. package/test/unit/spec/multistream/receiveSlot.ts +1 -1
  59. package/test/unit/spec/roap/index.ts +2 -2
  60. package/test/utils/integrationTestUtils.js +5 -23
@@ -4,12 +4,14 @@
4
4
  import 'jsdom-global/register';
5
5
  import {cloneDeep, forEach, isEqual} from 'lodash';
6
6
  import sinon from 'sinon';
7
+ import * as internalMediaModule from '@webex/internal-media-core';
7
8
  import StateMachine from 'javascript-state-machine';
8
9
  import uuid from 'uuid';
9
10
  import {assert} from '@webex/test-helper-chai';
10
- import {Credentials, Token} from '@webex/webex-core';
11
+ import {Credentials, Token, WebexPlugin} from '@webex/webex-core';
11
12
  import Support from '@webex/internal-plugin-support';
12
13
  import MockWebex from '@webex/test-helper-mock-webex';
14
+ import StaticConfig from '@webex/plugin-meetings/src/common/config';
13
15
  import {
14
16
  FLOOR_ACTION,
15
17
  SHARE_STATUS,
@@ -30,10 +32,12 @@ import {
30
32
  Event,
31
33
  Errors,
32
34
  ErrorType,
33
- LocalTrackEvents,
34
35
  RemoteTrackType,
35
36
  MediaType,
36
37
  } from '@webex/internal-media-core';
38
+ import {
39
+ LocalTrackEvents,
40
+ } from '@webex/media-helpers';
37
41
  import * as StatsAnalyzerModule from '@webex/plugin-meetings/src/statsAnalyzer';
38
42
  import * as MuteStateModule from '@webex/plugin-meetings/src/meeting/muteState';
39
43
  import EventsScope from '@webex/plugin-meetings/src/common/events/events-scope';
@@ -42,6 +46,7 @@ import Meeting from '@webex/plugin-meetings/src/meeting';
42
46
  import Members from '@webex/plugin-meetings/src/members';
43
47
  import * as MembersImport from '@webex/plugin-meetings/src/members';
44
48
  import Roap from '@webex/plugin-meetings/src/roap';
49
+ import RoapRequest from '@webex/plugin-meetings/src/roap/request';
45
50
  import MeetingRequest from '@webex/plugin-meetings/src/meeting/request';
46
51
  import * as MeetingRequestImport from '@webex/plugin-meetings/src/meeting/request';
47
52
  import LocusInfo from '@webex/plugin-meetings/src/locus-info';
@@ -151,6 +156,15 @@ describe('plugin-meetings', () => {
151
156
  },
152
157
  });
153
158
 
159
+ Object.defineProperty(global.window.navigator, 'permissions', {
160
+ writable: true,
161
+ value: {
162
+ query: sinon.stub().callsFake(async (arg) => {
163
+ return {state: 'granted', name: arg.name};
164
+ }),
165
+ },
166
+ });
167
+
154
168
  Object.defineProperty(global.window, 'MediaStream', {
155
169
  writable: true,
156
170
  value: MediaStream,
@@ -196,6 +210,7 @@ describe('plugin-meetings', () => {
196
210
  metrics: {},
197
211
  stats: {},
198
212
  experimental: {enableUnifiedMeetings: true},
213
+ degradationPreferences: { maxMacroblocksLimit: 8192 },
199
214
  },
200
215
  metrics: {
201
216
  type: ['behavioral'],
@@ -469,246 +484,7 @@ describe('plugin-meetings', () => {
469
484
  assert.instanceOf(members, Members);
470
485
  });
471
486
  });
472
- describe('#isAudioMuted', () => {
473
- it('should have #isAudioMuted', () => {
474
- assert.exists(meeting.invite);
475
- });
476
- it('should get the audio muted status and return as a boolean', () => {
477
- const muted = meeting.isAudioMuted();
478
-
479
- assert.isNotOk(muted);
480
- });
481
- });
482
- describe('#isAudioSelf', () => {
483
- it('should have #isAudioSelf', () => {
484
- assert.exists(meeting.invite);
485
- });
486
- it('should get the audio self status and return as a boolean', () => {
487
- const self = meeting.isAudioSelf();
488
-
489
- assert.isNotOk(self);
490
- });
491
- });
492
- describe('#isVideoMuted', () => {
493
- it('should have #isVideoMuted', () => {
494
- assert.exists(meeting.isVideoMuted);
495
- });
496
- it('should get the video muted status and return as a boolean', () => {
497
- const muted = meeting.isVideoMuted();
498
-
499
- assert.isNotOk(muted);
500
- });
501
- });
502
- describe('#isVideoSelf', () => {
503
- it('should have #isVideoSelf', () => {
504
- assert.exists(meeting.invite);
505
- });
506
- it('should get the video self status and return as a boolean', () => {
507
- const self = meeting.isVideoSelf();
508
-
509
- assert.isNotOk(self);
510
- });
511
- });
512
- describe('#muteAudio', () => {
513
- it('should have #muteAudio', () => {
514
- assert.exists(meeting.muteAudio);
515
- });
516
- describe('before audio is defined', () => {
517
- it('should reject and return a promise', async () => {
518
- await meeting.muteAudio().catch((err) => {
519
- assert.instanceOf(err, UserNotJoinedError);
520
- });
521
- });
522
-
523
- it('should reject and return a promise', async () => {
524
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
525
- await meeting.muteAudio().catch((err) => {
526
- assert.instanceOf(err, NoMediaEstablishedYetError);
527
- });
528
- });
529
-
530
- it('should reject and return a promise', async () => {
531
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
532
- meeting.mediaId = 'mediaId';
533
- await meeting.muteAudio().catch((err) => {
534
- assert.instanceOf(err, ParameterError);
535
- });
536
- });
537
- });
538
- describe('after audio is defined', () => {
539
- let handleClientRequest;
540
-
541
- beforeEach(() => {
542
- handleClientRequest = sinon.stub().returns(Promise.resolve());
543
- meeting.audio = {handleClientRequest};
544
- });
545
-
546
- it('should return a promise resolution', async () => {
547
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
548
- meeting.mediaId = 'mediaId';
549
-
550
- const audio = meeting.muteAudio();
551
-
552
- assert.exists(audio.then);
553
- await audio;
554
- assert.calledOnce(handleClientRequest);
555
- assert.calledWith(handleClientRequest, meeting, true);
556
- });
557
- });
558
- });
559
- describe('#unmuteAudio', () => {
560
- it('should have #unmuteAudio', () => {
561
- assert.exists(meeting.unmuteAudio);
562
- });
563
- describe('before audio is defined', () => {
564
- it('should reject when user not joined', async () => {
565
- await meeting.unmuteAudio().catch((err) => {
566
- assert.instanceOf(err, UserNotJoinedError);
567
- });
568
- });
569
-
570
- it('should reject when no media is established yet ', async () => {
571
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
572
- await meeting.unmuteAudio().catch((err) => {
573
- assert.instanceOf(err, NoMediaEstablishedYetError);
574
- });
575
- });
576
-
577
- it('should reject when audio is not there or established', async () => {
578
- meeting.mediaId = 'mediaId';
579
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
580
- await meeting.unmuteAudio().catch((err) => {
581
- assert.instanceOf(err, ParameterError);
582
- });
583
- });
584
- });
585
- describe('after audio is defined', () => {
586
- let handleClientRequest;
587
-
588
- beforeEach(() => {
589
- handleClientRequest = sinon.stub().returns(Promise.resolve());
590
- meeting.mediaId = 'mediaId';
591
- meeting.audio = {handleClientRequest};
592
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
593
- });
594
-
595
- it('should return a promise resolution', async () => {
596
- meeting.audio = {handleClientRequest};
597
-
598
- const audio = meeting.unmuteAudio();
599
-
600
- assert.exists(audio.then);
601
- await audio;
602
- assert.calledOnce(handleClientRequest);
603
- assert.calledWith(handleClientRequest, meeting, false);
604
- });
605
- });
606
- });
607
- describe('BNR', () => {
608
- const fakeMediaTrack = () => ({
609
- id: Date.now().toString(),
610
- stop: () => {},
611
- readyState: 'live',
612
- enabled: true,
613
- getSettings: () => ({
614
- sampleRate: 48000,
615
- }),
616
- });
617
-
618
- beforeEach(() => {
619
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve());
620
- sinon.replace(meeting, 'addMedia', () => {
621
- sinon.stub(meeting.mediaProperties, 'audioTrack').value(fakeMediaTrack());
622
- sinon.stub(meeting.mediaProperties, 'mediaDirection').value({
623
- receiveAudio: true,
624
- });
625
- });
626
- });
627
- });
628
- describe('#muteVideo', () => {
629
- it('should have #muteVideo', () => {
630
- assert.exists(meeting.muteVideo);
631
- });
632
- describe('before video is defined', () => {
633
- it('should reject when user not joined', async () => {
634
- await meeting.muteVideo().catch((err) => {
635
- assert.instanceOf(err, UserNotJoinedError);
636
- });
637
- });
638
-
639
- it('should reject when no media is established', async () => {
640
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
641
- await meeting.muteVideo().catch((err) => {
642
- assert.instanceOf(err, NoMediaEstablishedYetError);
643
- });
644
- });
645
-
646
- it('should reject when no video added or established', async () => {
647
- meeting.mediaId = 'mediaId';
648
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
649
- await meeting.muteVideo().catch((err) => {
650
- assert.instanceOf(err, ParameterError);
651
- });
652
- });
653
- });
654
- describe('after video is defined', () => {
655
- it('should return a promise resolution', async () => {
656
- const handleClientRequest = sinon.stub().returns(Promise.resolve());
657
-
658
- meeting.mediaId = 'mediaId';
659
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
660
- meeting.video = {handleClientRequest};
661
- const video = meeting.muteVideo();
662
-
663
- assert.exists(video.then);
664
- await video;
665
- assert.calledOnce(handleClientRequest);
666
- assert.calledWith(handleClientRequest, meeting, true);
667
- });
668
- });
669
- });
670
- describe('#unmuteVideo', () => {
671
- it('should have #unmuteVideo', () => {
672
- assert.exists(meeting.unmuteVideo);
673
- });
674
- describe('before video is defined', () => {
675
- it('should reject no user joined', async () => {
676
- await meeting.unmuteVideo().catch((err) => {
677
- assert.instanceOf(err, Error);
678
- });
679
- });
680
487
 
681
- it('should reject no media established', async () => {
682
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
683
- await meeting.unmuteVideo().catch((err) => {
684
- assert.instanceOf(err, Error);
685
- });
686
- });
687
-
688
- it('should reject when no video added or established', async () => {
689
- meeting.mediaId = 'mediaId';
690
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
691
- await meeting.unmuteVideo().catch((err) => {
692
- assert.instanceOf(err, Error);
693
- });
694
- });
695
- });
696
- describe('after video is defined', () => {
697
- it('should return a promise resolution', async () => {
698
- const handleClientRequest = sinon.stub().returns(Promise.resolve());
699
-
700
- meeting.mediaId = 'mediaId';
701
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
702
- meeting.video = {handleClientRequest};
703
- const video = meeting.unmuteVideo();
704
-
705
- assert.exists(video.then);
706
- await video;
707
- assert.calledOnce(handleClientRequest);
708
- assert.calledWith(handleClientRequest, meeting, false);
709
- });
710
- });
711
- });
712
488
  describe('#joinWithMedia', () => {
713
489
  it('should have #joinWithMedia', () => {
714
490
  assert.exists(meeting.joinWithMedia);
@@ -716,125 +492,21 @@ describe('plugin-meetings', () => {
716
492
  describe('resolution', () => {
717
493
  it('should success and return a promise', async () => {
718
494
  meeting.join = sinon.stub().returns(Promise.resolve(test1));
719
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([test2, test3]));
720
495
  meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
721
- await meeting.joinWithMedia({});
496
+ const result = await meeting.joinWithMedia({});
722
497
  assert.calledOnce(meeting.join);
723
- assert.calledOnce(meeting.getMediaStreams);
498
+ assert.calledOnce(meeting.addMedia);
499
+ assert.deepEqual(result, {join: test1, media: test4});
724
500
  });
725
501
  });
726
502
  describe('rejection', () => {
727
503
  it('should error out and return a promise', async () => {
728
504
  meeting.join = sinon.stub().returns(Promise.reject());
729
- meeting.getMediaStreams = sinon.stub().returns(true);
730
505
  assert.isRejected(meeting.joinWithMedia({}));
731
506
  });
732
507
  });
733
508
  });
734
- describe('#getMediaStreams', () => {
735
- beforeEach(() => {
736
- sinon
737
- .stub(Media, 'getSupportedDevice')
738
- .callsFake((options) =>
739
- Promise.resolve({sendAudio: options.sendAudio, sendVideo: options.sendVideo})
740
- );
741
- sinon.stub(Media, 'getUserMedia').returns(Promise.resolve(['stream1', 'stream2']));
742
- });
743
- afterEach(() => {
744
- sinon.restore();
745
- });
746
- it('should have #getMediaStreams', () => {
747
- assert.exists(meeting.getMediaStreams);
748
- });
749
- it('should proxy Media getUserMedia, and return a promise', async () => {
750
- await meeting.getMediaStreams({sendAudio: true, sendVideo: true});
751
-
752
- assert.calledOnce(Media.getUserMedia);
753
- });
754
-
755
- it('uses the preferred video device if set', async () => {
756
- const videoDevice = 'video1';
757
- const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
758
- const audioVideoSettings = {};
759
-
760
- sinon.stub(meeting.mediaProperties, 'videoDeviceId').value(videoDevice);
761
- sinon.stub(meeting.mediaProperties, 'localQualityLevel').value('480p');
762
- await meeting.getMediaStreams(mediaDirection, audioVideoSettings);
763
-
764
- assert.calledWith(
765
- Media.getUserMedia,
766
- {
767
- ...mediaDirection,
768
- isSharing: false,
769
- },
770
- {
771
- video: {
772
- width: {max: 640, ideal: 640},
773
- height: {max: 480, ideal: 480},
774
- deviceId: videoDevice,
775
- },
776
- }
777
- );
778
- });
779
- it('will set a new preferred video input device if passed in', async () => {
780
- // if audioVideo settings parameter specifies a new video device it
781
- // will store that device as the preferred video device.
782
- // Which is the case with meeting.updateVideo()
783
- const oldVideoDevice = 'video1';
784
- const newVideoDevice = 'video2';
785
- const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
786
- const audioVideoSettings = {video: {deviceId: newVideoDevice}};
787
-
788
- sinon.stub(meeting.mediaProperties, 'videoDeviceId').value(oldVideoDevice);
789
- sinon.stub(meeting.mediaProperties, 'setVideoDeviceId');
790
509
 
791
- await meeting.getMediaStreams(mediaDirection, audioVideoSettings);
792
-
793
- assert.calledWith(meeting.mediaProperties.setVideoDeviceId, newVideoDevice);
794
- });
795
-
796
- it('uses the passed custom video resolution', async () => {
797
- const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
798
- const customAudioVideoSettings = {
799
- video: {
800
- width: {
801
- max: 400,
802
- ideal: 400,
803
- },
804
- height: {
805
- max: 200,
806
- ideal: 200,
807
- },
808
- frameRate: {
809
- ideal: 15,
810
- max: 30,
811
- },
812
- facingMode: {
813
- ideal: 'user',
814
- },
815
- },
816
- };
817
-
818
- sinon.stub(meeting.mediaProperties, 'localQualityLevel').value('200p');
819
- await meeting.getMediaStreams(mediaDirection, customAudioVideoSettings);
820
-
821
- assert.calledWith(
822
- Media.getUserMedia,
823
- {
824
- ...mediaDirection,
825
- isSharing: false,
826
- },
827
- customAudioVideoSettings
828
- );
829
- });
830
- it('should not access camera if sendVideo is false ', async () => {
831
- await meeting.getMediaStreams({sendAudio: true, sendVideo: false});
832
-
833
- assert.calledOnce(Media.getUserMedia);
834
-
835
- assert.equal(Media.getUserMedia.args[0][0].sendVideo, false);
836
- });
837
- });
838
510
  describe('#isTranscriptionSupported', () => {
839
511
  it('should return false if the feature is not supported for the meeting', () => {
840
512
  meeting.locusInfo.controls = {transcribe: {transcribing: false}};
@@ -1159,7 +831,7 @@ describe('plugin-meetings', () => {
1159
831
  meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
1160
832
  meeting.audio = muteStateStub;
1161
833
  meeting.video = muteStateStub;
1162
- Media.createMediaConnection = sinon.stub().returns(fakeMediaConnection);
834
+ sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
1163
835
  meeting.setMercuryListener = sinon.stub().returns(true);
1164
836
  meeting.setupMediaConnectionListeners = sinon.stub();
1165
837
  meeting.setMercuryListener = sinon.stub();
@@ -1240,6 +912,7 @@ describe('plugin-meetings', () => {
1240
912
  locus_id: meeting.locusUrl.split('/').pop(),
1241
913
  reason: err.message,
1242
914
  stack: err.stack,
915
+ code: err.code,
1243
916
  turnDiscoverySkippedReason: 'config',
1244
917
  turnServerUsed: false,
1245
918
  isMultistream: false,
@@ -1703,649 +1376,810 @@ describe('plugin-meetings', () => {
1703
1376
  assert.calledOnce(fakeMediaConnection.initiateOffer);
1704
1377
  });
1705
1378
  });
1706
- describe('#acknowledge', () => {
1707
- it('should have #acknowledge', () => {
1708
- assert.exists(meeting.acknowledge);
1709
- });
1710
- beforeEach(() => {
1711
- meeting.meetingRequest.acknowledgeMeeting = sinon.stub().returns(Promise.resolve());
1712
- });
1713
- it('should acknowledge incoming and return a promise', async () => {
1714
- const ack = meeting.acknowledge('INCOMING', false);
1715
1379
 
1716
- assert.exists(ack.then);
1717
- await ack;
1718
- assert.calledOnce(meeting.meetingRequest.acknowledgeMeeting);
1719
- });
1720
- it('should acknowledge a non incoming and return a promise', async () => {
1721
- const ack = meeting.acknowledge(test1, false);
1380
+ /* This set of tests are like semi-integration tests, they use real MuteState, Media, LocusMediaRequest and Roap classes.
1381
+ They mock the @webex/internal-media-core and sending of /media http requests to Locus.
1382
+ Their main purpose is to test that we send the right http requests to Locus and make right calls
1383
+ to @webex/internal-media-core when addMedia, updateMedia, publishTracks, unpublishTracks are called
1384
+ in various combinations.
1385
+ */
1386
+ [true,false].forEach((isMultistream) =>
1387
+ describe(`addMedia/updateMedia semi-integration tests (${isMultistream ? 'multistream' : 'transcoded'})`, () => {
1388
+ const webrtcAudioTrack = {
1389
+ id: 'underlying audio track',
1390
+ getSettings: sinon.stub().returns({deviceId: 'fake device id for audio track'}),
1391
+ };
1722
1392
 
1723
- assert.exists(ack.then);
1724
- await ack;
1725
- assert.notCalled(meeting.meetingRequest.acknowledgeMeeting);
1726
- });
1727
- });
1728
- describe('#decline', () => {
1729
- it('should have #decline', () => {
1730
- assert.exists(meeting.decline);
1731
- });
1732
- beforeEach(() => {
1733
- meeting.meetingRequest.declineMeeting = sinon.stub().returns(Promise.resolve());
1734
- meeting.meetingFiniteStateMachine.ring();
1735
- });
1736
- it('should decline the meeting and trigger meeting destroy for 1:1', async () => {
1737
- await meeting.decline();
1738
- assert.calledOnce(meeting.meetingRequest.declineMeeting);
1739
- });
1740
- });
1741
- describe('#leave', () => {
1742
- let sandbox;
1393
+ let fakeMicrophoneTrack;
1394
+ let fakeRoapMediaConnection;
1395
+ let fakeMultistreamRoapMediaConnection;
1396
+ let roapMediaConnectionConstructorStub;
1397
+ let multistreamRoapMediaConnectionConstructorStub;
1398
+ let locusMediaRequestStub; // stub for /media requests to Locus
1743
1399
 
1744
- it('should have #leave', () => {
1745
- assert.exists(meeting.leave);
1746
- });
1400
+ const roapOfferMessage = {messageType: 'OFFER', sdp: 'sdp', seq: '1', tieBreaker: '123'};
1747
1401
 
1748
- it('should reject if meeting is already inactive', async () => {
1749
- await meeting.leave().catch((err) => {
1750
- assert.instanceOf(err, MeetingNotActiveError);
1751
- });
1752
- });
1402
+ let expectedMediaConnectionConfig;
1403
+ let expectedDebugId;
1753
1404
 
1754
- it('should reject if meeting is already left', async () => {
1755
- meeting.meetingState = 'ACTIVE';
1756
- await meeting.leave().catch((err) => {
1757
- assert.instanceOf(err, UserNotJoinedError);
1758
- });
1759
- });
1405
+ let clock;
1760
1406
 
1761
1407
  beforeEach(() => {
1762
- sandbox = sinon.createSandbox();
1763
- meeting.meetingFiniteStateMachine.ring();
1764
- meeting.meetingFiniteStateMachine.join();
1765
- meeting.meetingRequest.leaveMeeting = sinon
1766
- .stub()
1767
- .returns(Promise.resolve({body: 'test'}));
1768
- meeting.locusInfo.onFullLocus = sinon.stub().returns(true);
1769
- // the 3 need to be promises because we do closeLocalStream.then(closeLocalShare.then) etc in the src code
1770
- meeting.closeLocalStream = sinon.stub().returns(Promise.resolve());
1771
- meeting.closeLocalShare = sinon.stub().returns(Promise.resolve());
1772
- meeting.closeRemoteStream = sinon.stub().returns(Promise.resolve());
1773
- sandbox.stub(meeting, 'closeRemoteTracks').returns(Promise.resolve());
1774
- meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
1775
- meeting.unsetLocalVideoTrack = sinon.stub().returns(true);
1776
- meeting.unsetLocalShareTrack = sinon.stub().returns(true);
1777
- meeting.unsetRemoteTracks = sinon.stub();
1778
- meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
1779
- meeting.unsetRemoteStream = sinon.stub().returns(true);
1780
- meeting.unsetPeerConnections = sinon.stub().returns(true);
1781
- meeting.logger.error = sinon.stub().returns(true);
1782
- meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
1408
+ clock = sinon.useFakeTimers();
1783
1409
 
1784
- // A meeting needs to be joined to leave
1410
+ meeting.deviceUrl = 'deviceUrl';
1411
+ meeting.config.deviceType = 'web';
1412
+ meeting.isMultistream = isMultistream;
1785
1413
  meeting.meetingState = 'ACTIVE';
1786
- meeting.state = 'JOINED';
1414
+ meeting.mediaId = 'fake media id';
1415
+ meeting.selfUrl = 'selfUrl';
1416
+ meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
1417
+ meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
1418
+ meeting.setMercuryListener = sinon.stub();
1419
+ meeting.locusInfo.onFullLocus = sinon.stub();
1420
+ meeting.webex.meetings.reachability = {
1421
+ isAnyClusterReachable: sinon.stub().resolves(true),
1422
+ };
1423
+ meeting.roap.doTurnDiscovery = sinon
1424
+ .stub()
1425
+ .resolves({turnServerInfo: {}, turnDiscoverySkippedReason: 'reachability'});
1426
+
1427
+ StaticConfig.set({bandwidth: {audio: 1234, video: 5678, startBitrate: 9876}});
1428
+
1429
+ Metrics.postEvent = sinon.stub();
1430
+
1431
+ // setup things that are expected to be the same across all the tests and are actually irrelevant for these tests
1432
+ expectedDebugId = `MC-${meeting.id.substring(0, 4)}`;
1433
+ expectedMediaConnectionConfig = {
1434
+ iceServers: [ { urls: undefined, username: '', credential: '' } ],
1435
+ skipInactiveTransceivers: false,
1436
+ requireH264: true,
1437
+ sdpMunging: {
1438
+ convertPort9to0: false,
1439
+ addContentSlides: true,
1440
+ bandwidthLimits: {
1441
+ audio: StaticConfig.meetings.bandwidth.audio,
1442
+ video: StaticConfig.meetings.bandwidth.video,
1443
+ },
1444
+ startBitrate: StaticConfig.meetings.bandwidth.startBitrate,
1445
+ periodicKeyframes: 20,
1446
+ disableExtmap: !meeting.config.enableExtmap,
1447
+ disableRtx: !meeting.config.enableRtx,
1448
+ },
1449
+ };
1450
+
1451
+ // setup stubs
1452
+ fakeMicrophoneTrack = {
1453
+ id: 'fake mic',
1454
+ on: sinon.stub(),
1455
+ off: sinon.stub(),
1456
+ setUnmuteAllowed: sinon.stub(),
1457
+ setMuted: sinon.stub(),
1458
+ setPublished: sinon.stub(),
1459
+ muted: false,
1460
+ underlyingTrack: webrtcAudioTrack
1461
+ };
1462
+
1463
+ fakeRoapMediaConnection = {
1464
+ id: 'roap media connection',
1465
+ close: sinon.stub(),
1466
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
1467
+ initiateOffer: sinon.stub().resolves({}),
1468
+ update: sinon.stub().resolves({}),
1469
+ on: sinon.stub(),
1470
+ };
1471
+
1472
+ fakeMultistreamRoapMediaConnection = {
1473
+ id: 'multistream roap media connection',
1474
+ close: sinon.stub(),
1475
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
1476
+ initiateOffer: sinon.stub().resolves({}),
1477
+ publishTrack: sinon.stub().resolves({}),
1478
+ unpublishTrack: sinon.stub().resolves({}),
1479
+ on: sinon.stub(),
1480
+ requestMedia: sinon.stub(),
1481
+ createReceiveSlot: sinon.stub().resolves({on: sinon.stub()}),
1482
+ enableMultistreamAudio: sinon.stub(),
1483
+ };
1484
+
1485
+ roapMediaConnectionConstructorStub = sinon
1486
+ .stub(internalMediaModule, 'RoapMediaConnection')
1487
+ .returns(fakeRoapMediaConnection);
1488
+
1489
+ multistreamRoapMediaConnectionConstructorStub = sinon
1490
+ .stub(internalMediaModule, 'MultistreamRoapMediaConnection')
1491
+ .returns(fakeMultistreamRoapMediaConnection);
1492
+
1493
+ locusMediaRequestStub = sinon.stub(WebexPlugin.prototype, 'request').resolves({body: {locus: { fullState: {}}}});
1787
1494
  });
1495
+
1788
1496
  afterEach(() => {
1789
- sandbox.restore();
1790
- sandbox = null;
1497
+ clock.restore();
1791
1498
  });
1792
- it('should leave the meeting and return promise', async () => {
1793
- const leave = meeting.leave();
1794
1499
 
1795
- assert.exists(leave.then);
1796
- await leave;
1797
- assert.calledOnce(meeting.meetingRequest.leaveMeeting);
1798
- assert.calledOnce(meeting.closeLocalStream);
1799
- assert.calledOnce(meeting.closeLocalShare);
1800
- assert.calledOnce(meeting.closeRemoteTracks);
1801
- assert.calledOnce(meeting.closePeerConnections);
1802
- assert.calledOnce(meeting.unsetLocalVideoTrack);
1803
- assert.calledOnce(meeting.unsetLocalShareTrack);
1804
- assert.calledOnce(meeting.unsetRemoteTracks);
1805
- assert.calledOnce(meeting.unsetPeerConnections);
1806
- });
1807
- describe('after audio/video is defined', () => {
1808
- let handleClientRequest;
1500
+ // helper function that waits until all promises are resolved and any queued up /media requests to Locus are sent out
1501
+ const stableState = async () => {
1502
+ await testUtils.flushPromises();
1503
+ clock.tick(1); // needed because LocusMediaRequest uses Lodash.defer()
1504
+ }
1505
+
1506
+ const resetHistory = () => {
1507
+ locusMediaRequestStub.resetHistory();
1508
+ fakeRoapMediaConnection.update.resetHistory();
1509
+ fakeMultistreamRoapMediaConnection.publishTrack.resetHistory();
1510
+ fakeMultistreamRoapMediaConnection.unpublishTrack.resetHistory();
1511
+ };
1809
1512
 
1810
- beforeEach(() => {
1811
- handleClientRequest = sinon.stub().returns(Promise.resolve(true));
1513
+ const getRoapListener = () => {
1514
+ const roapMediaConnectionToCheck = isMultistream ? fakeMultistreamRoapMediaConnection : fakeRoapMediaConnection;
1812
1515
 
1813
- meeting.audio = {handleClientRequest};
1814
- meeting.video = {handleClientRequest};
1815
- });
1516
+ for(let idx = 0; idx < roapMediaConnectionToCheck.on.callCount; idx+= 1) {
1517
+ if (roapMediaConnectionToCheck.on.getCall(idx).args[0] === Event.ROAP_MESSAGE_TO_SEND) {
1518
+ return roapMediaConnectionToCheck.on.getCall(idx).args[1];
1519
+ }
1520
+ }
1521
+ assert.fail('listener for "roap:messageToSend" (Event.ROAP_MESSAGE_TO_SEND) was not registered')
1522
+ }
1816
1523
 
1817
- it('should delete audio and video state machines when leaving the meeting', async () => {
1818
- const leave = meeting.leave();
1524
+ // simulates a Roap offer being generated by the RoapMediaConnection
1525
+ const simulateRoapOffer = async () => {
1526
+ const roapListener = getRoapListener();
1819
1527
 
1820
- assert.exists(leave.then);
1821
- await leave;
1528
+ await roapListener({roapMessage: roapOfferMessage});
1529
+ await stableState();
1530
+ }
1822
1531
 
1823
- assert.isNull(meeting.audio);
1824
- assert.isNull(meeting.video);
1532
+ const checkSdpOfferSent = ({audioMuted, videoMuted}) => {
1533
+ const {sdp, seq, tieBreaker} = roapOfferMessage;
1534
+
1535
+ assert.calledWith(locusMediaRequestStub,
1536
+ {
1537
+ method: 'PUT',
1538
+ uri: `${meeting.selfUrl}/media`,
1539
+ body: {
1540
+ device: { url: meeting.deviceUrl, deviceType: meeting.config.deviceType },
1541
+ correlationId: meeting.correlationId,
1542
+ localMedias: [
1543
+ {
1544
+ localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted},"roapMessage":{"messageType":"OFFER","sdps":["${sdp}"],"version":"2","seq":"${seq}","tieBreaker":"${tieBreaker}"}}`,
1545
+ mediaId: 'fake media id'
1546
+ }
1547
+ ],
1548
+ clientMediaPreferences: {
1549
+ preferTranscoding: !meeting.isMultistream,
1550
+ joinCookie: undefined
1551
+ }
1552
+ },
1553
+ });
1554
+ };
1555
+
1556
+ const checkLocalMuteSentToLocus = ({audioMuted, videoMuted}) => {
1557
+ assert.calledWith(locusMediaRequestStub, {
1558
+ method: 'PUT',
1559
+ uri: `${meeting.selfUrl}/media`,
1560
+ body: {
1561
+ device: { url: meeting.deviceUrl, deviceType: meeting.config.deviceType },
1562
+ correlationId: meeting.correlationId,
1563
+ localMedias: [
1564
+ {
1565
+ localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted}}`,
1566
+ mediaId: 'fake media id'
1567
+ }
1568
+ ],
1569
+ clientMediaPreferences: {
1570
+ preferTranscoding: !meeting.isMultistream,
1571
+ },
1572
+ respOnlySdp: true,
1573
+ usingResource: null,
1574
+ },
1575
+ });
1576
+ };
1577
+
1578
+ const checkMediaConnectionCreated = ({mediaConnectionConfig, localTracks, direction, remoteQualityLevel, expectedDebugId}) => {
1579
+ if (isMultistream) {
1580
+ const {iceServers} = mediaConnectionConfig;
1581
+
1582
+ assert.calledOnceWithExactly(multistreamRoapMediaConnectionConstructorStub, {
1583
+ iceServers,
1584
+ enableMainAudio: direction.audio !== 'inactive',
1585
+ enableMainVideo: true
1586
+ }, expectedDebugId);
1587
+
1588
+ Object.values(localTracks).forEach((track) => {
1589
+ if (track) {
1590
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.publishTrack, track);
1591
+ }
1592
+ })
1593
+ } else {
1594
+ assert.calledOnceWithExactly(roapMediaConnectionConstructorStub, mediaConnectionConfig,
1595
+ {
1596
+ localTracks: {
1597
+ audio: localTracks.audio?.underlyingTrack,
1598
+ video: localTracks.video?.underlyingTrack,
1599
+ screenShareVideo: localTracks.screenShareVideo?.underlyingTrack,
1600
+ },
1601
+ direction,
1602
+ remoteQualityLevel,
1603
+ },
1604
+ expectedDebugId);
1605
+ }
1606
+ }
1607
+
1608
+ it('addMedia() works correctly when media is enabled without tracks to publish', async () => {
1609
+ await meeting.addMedia();
1610
+ await simulateRoapOffer();
1611
+
1612
+ // check RoapMediaConnection was created correctly
1613
+ checkMediaConnectionCreated({
1614
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1615
+ localTracks: {
1616
+ audio: undefined,
1617
+ video: undefined,
1618
+ screenShareVideo: undefined,
1619
+ },
1620
+ direction: {
1621
+ audio: 'sendrecv',
1622
+ video: 'sendrecv',
1623
+ screenShareVideo: 'recvonly',
1624
+ },
1625
+ remoteQualityLevel: 'HIGH',
1626
+ expectedDebugId,
1825
1627
  });
1628
+
1629
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1630
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1631
+
1632
+ // and that it was the only /media request that was sent
1633
+ assert.calledOnce(locusMediaRequestStub);
1826
1634
  });
1827
- it('should leave the meeting without leaving resource', async () => {
1828
- const leave = meeting.leave({resourceId: null});
1829
1635
 
1830
- assert.exists(leave.then);
1831
- await leave;
1832
- assert.calledWith(meeting.meetingRequest.leaveMeeting, {
1833
- locusUrl: meeting.locusUrl,
1834
- correlationId: meeting.correlationId,
1835
- selfId: meeting.selfId,
1836
- resourceId: null,
1837
- deviceUrl: meeting.deviceUrl,
1636
+ it('addMedia() works correctly when media is enabled with tracks to publish', async () => {
1637
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}});
1638
+ await simulateRoapOffer();
1639
+
1640
+ // check RoapMediaConnection was created correctly
1641
+ checkMediaConnectionCreated({
1642
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1643
+ localTracks: {
1644
+ audio: fakeMicrophoneTrack,
1645
+ video: undefined,
1646
+ screenShareVideo: undefined,
1647
+ },
1648
+ direction: {
1649
+ audio: 'sendrecv',
1650
+ video: 'sendrecv',
1651
+ screenShareVideo: 'recvonly',
1652
+ },
1653
+ remoteQualityLevel: 'HIGH',
1654
+ expectedDebugId
1838
1655
  });
1656
+
1657
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1658
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
1659
+
1660
+ // and no other local mute requests were sent to Locus
1661
+ assert.calledOnce(locusMediaRequestStub);
1839
1662
  });
1840
- it('should leave the meeting on the resource', async () => {
1841
- const leave = meeting.leave();
1842
1663
 
1843
- assert.exists(leave.then);
1844
- await leave;
1845
- assert.calledWith(meeting.meetingRequest.leaveMeeting, {
1846
- locusUrl: meeting.locusUrl,
1847
- correlationId: meeting.correlationId,
1848
- selfId: meeting.selfId,
1849
- resourceId: meeting.resourceId,
1850
- deviceUrl: meeting.deviceUrl
1664
+ it('addMedia() works correctly when media is enabled with tracks to publish and track is muted', async () => {
1665
+ fakeMicrophoneTrack.muted = true;
1666
+
1667
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}});
1668
+ await simulateRoapOffer();
1669
+
1670
+ // check RoapMediaConnection was created correctly
1671
+ checkMediaConnectionCreated({
1672
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1673
+ localTracks: {
1674
+ audio: fakeMicrophoneTrack,
1675
+ video: undefined,
1676
+ screenShareVideo: undefined,
1677
+ },
1678
+ direction: {
1679
+ audio: 'sendrecv',
1680
+ video: 'sendrecv',
1681
+ screenShareVideo: 'recvonly',
1682
+ },
1683
+ remoteQualityLevel: 'HIGH',
1684
+ expectedDebugId,
1851
1685
  });
1686
+
1687
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1688
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1689
+
1690
+ // and no other local mute requests were sent to Locus
1691
+ assert.calledOnce(locusMediaRequestStub);
1852
1692
  });
1853
- it('should leave the meeting on the resource with reason', async () => {
1854
- const leave = meeting.leave({resourceId: meeting.resourceId, reason: MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST});
1855
1693
 
1856
- assert.exists(leave.then);
1857
- await leave;
1858
- assert.calledWith(meeting.meetingRequest.leaveMeeting, {
1859
- locusUrl: meeting.locusUrl,
1860
- correlationId: meeting.correlationId,
1861
- selfId: meeting.selfId,
1862
- resourceId: meeting.resourceId,
1863
- deviceUrl: meeting.deviceUrl,
1864
- reason: MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST
1694
+ it('addMedia() works correctly when media is disabled with tracks to publish', async () => {
1695
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}, audioEnabled: false});
1696
+ await simulateRoapOffer();
1697
+
1698
+ // check RoapMediaConnection was created correctly
1699
+ checkMediaConnectionCreated({
1700
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1701
+ localTracks: {
1702
+ audio: fakeMicrophoneTrack,
1703
+ video: undefined,
1704
+ screenShareVideo: undefined,
1705
+ },
1706
+ direction: {
1707
+ audio: 'inactive',
1708
+ video: 'sendrecv',
1709
+ screenShareVideo: 'recvonly',
1710
+ },
1711
+ remoteQualityLevel: 'HIGH',
1712
+ expectedDebugId
1865
1713
  });
1866
- });
1867
- });
1868
- describe('#requestScreenShareFloor', () => {
1869
- it('should have #requestScreenShareFloor', () => {
1870
- assert.exists(meeting.requestScreenShareFloor);
1871
- });
1872
- beforeEach(() => {
1873
- meeting.locusInfo.mediaShares = [{name: 'content', url: url1}];
1874
- meeting.locusInfo.self = {url: url1};
1875
- meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
1876
- });
1877
- it('should send the share', async () => {
1878
- const share = meeting.requestScreenShareFloor();
1879
1714
 
1880
- assert.exists(share.then);
1881
- await share;
1882
- assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
1715
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1716
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1717
+
1718
+ // and no other local mute requests were sent to Locus
1719
+ assert.calledOnce(locusMediaRequestStub);
1883
1720
  });
1884
- });
1885
1721
 
1886
- describe('#shareScreen', () => {
1887
- let _mediaDirection;
1722
+ it('addMedia() works correctly when media is disabled with no tracks to publish', async () => {
1723
+ await meeting.addMedia({audioEnabled: false});
1724
+ await simulateRoapOffer();
1888
1725
 
1889
- beforeEach(() => {
1890
- _mediaDirection = meeting.mediaProperties.mediaDirection || {};
1891
- sinon
1892
- .stub(meeting.mediaProperties, 'mediaDirection')
1893
- .value({sendAudio: true, sendVideo: true, sendShare: false});
1894
- });
1726
+ // check RoapMediaConnection was created correctly
1727
+ checkMediaConnectionCreated({
1728
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1729
+ localTracks: {
1730
+ audio: undefined,
1731
+ video: undefined,
1732
+ screenShareVideo: undefined,
1733
+ },
1734
+ direction: {
1735
+ audio: 'inactive',
1736
+ video: 'sendrecv',
1737
+ screenShareVideo: 'recvonly',
1738
+ },
1739
+ remoteQualityLevel: 'HIGH',
1740
+ expectedDebugId
1741
+ });
1895
1742
 
1896
- afterEach(() => {
1897
- meeting.mediaProperties.mediaDirection = _mediaDirection;
1898
- });
1743
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1744
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1899
1745
 
1900
- it('should have #shareScreen', () => {
1901
- assert.exists(meeting.shareScreen);
1746
+ // and no other local mute requests were sent to Locus
1747
+ assert.calledOnce(locusMediaRequestStub);
1902
1748
  });
1903
1749
 
1904
- describe('basic functionality', () => {
1905
- beforeEach(() => {
1906
- sinon.stub(Media, 'getDisplayMedia').returns(Promise.resolve());
1907
- sinon.stub(meeting, 'updateShare').returns(Promise.resolve());
1908
- });
1750
+ describe('publishTracks()/unpublishTracks() calls', () => {
1751
+ [
1752
+ {mediaEnabled: true, expected: {direction: 'sendrecv', localMuteSentValue: false}},
1753
+ {mediaEnabled: false, expected: {direction: 'inactive', localMuteSentValue: undefined}}
1754
+ ]
1755
+ .forEach(({mediaEnabled, expected}) => {
1756
+ it(`first publishTracks() call while media is ${mediaEnabled ? 'enabled' : 'disabled'}`, async () => {
1757
+ await meeting.addMedia({audioEnabled: mediaEnabled});
1758
+ await simulateRoapOffer();
1759
+
1760
+ resetHistory();
1761
+
1762
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack});
1763
+ await stableState();
1764
+
1765
+ if (expected.localMuteSentValue !== undefined) {
1766
+ // check local mute was sent and it was the only /media request
1767
+ checkLocalMuteSentToLocus({audioMuted: expected.localMuteSentValue, videoMuted: true});
1768
+ assert.calledOnce(locusMediaRequestStub);
1769
+ } else {
1770
+ assert.notCalled(locusMediaRequestStub);
1771
+ }
1772
+ if (isMultistream) {
1773
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.publishTrack, fakeMicrophoneTrack);
1774
+ } else {
1775
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1776
+ localTracks: { audio: webrtcAudioTrack, video: null, screenShareVideo: null },
1777
+ direction: {
1778
+ audio: expected.direction,
1779
+ video: 'sendrecv',
1780
+ screenShareVideo: 'recvonly',
1781
+ },
1782
+ remoteQualityLevel: 'HIGH'
1783
+ });
1784
+ }
1785
+ });
1909
1786
 
1910
- afterEach(() => {
1911
- Media.getDisplayMedia.restore();
1912
- meeting.updateShare.restore();
1913
- });
1787
+ it(`second publishTracks() call while media is ${mediaEnabled ? 'enabled' : 'disabled'}`, async () => {
1788
+ await meeting.addMedia({audioEnabled: mediaEnabled});
1789
+ await simulateRoapOffer();
1790
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack});
1791
+ await stableState();
1792
+
1793
+ resetHistory();
1794
+
1795
+ const webrtcAudioTrack2 = {id: 'underlying audio track 2'};
1796
+ const fakeMicrophoneTrack2 = {
1797
+ id: 'fake mic 2',
1798
+ on: sinon.stub(),
1799
+ off: sinon.stub(),
1800
+ setUnmuteAllowed: sinon.stub(),
1801
+ setMuted: sinon.stub(),
1802
+ setPublished: sinon.stub(),
1803
+ muted: false,
1804
+ underlyingTrack: webrtcAudioTrack2
1805
+ };
1806
+
1807
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack2});
1808
+ await stableState();
1809
+
1810
+ // only the roap media connection should be updated
1811
+ if (isMultistream) {
1812
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.publishTrack, fakeMicrophoneTrack2);
1813
+ } else {
1814
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1815
+ localTracks: { audio: webrtcAudioTrack2, video: null, screenShareVideo: null },
1816
+ direction: {
1817
+ audio: expected.direction,
1818
+ video: 'sendrecv',
1819
+ screenShareVideo: 'recvonly',
1820
+ },
1821
+ remoteQualityLevel: 'HIGH'
1822
+ });
1823
+ }
1914
1824
 
1915
- it('should call get display media', async () => {
1916
- await meeting.shareScreen();
1825
+ // and no other roap messages or local mute requests were sent
1826
+ assert.notCalled(locusMediaRequestStub);
1827
+ });
1917
1828
 
1918
- assert.calledOnce(Media.getDisplayMedia);
1919
- });
1829
+ it(`unpublishTracks() call while media is ${mediaEnabled ? 'enabled' : 'disabled'}`, async () => {
1830
+ await meeting.addMedia({audioEnabled: mediaEnabled});
1831
+ await simulateRoapOffer();
1832
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack});
1833
+ await stableState();
1920
1834
 
1921
- it('should call updateShare', async () => {
1922
- await meeting.shareScreen();
1835
+ resetHistory();
1923
1836
 
1924
- assert.calledOnce(meeting.updateShare);
1925
- });
1837
+ await meeting.unpublishTracks([fakeMicrophoneTrack]);
1838
+ await stableState();
1926
1839
 
1927
- it('properly assigns default values', async () => {
1928
- await meeting.shareScreen({sharePreferences: {highFrameRate: true}});
1840
+ // the roap media connection should be updated
1841
+ if (isMultistream) {
1842
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.unpublishTrack, fakeMicrophoneTrack);
1843
+ } else {
1844
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1845
+ localTracks: { audio: null, video: null, screenShareVideo: null },
1846
+ direction: {
1847
+ audio: expected.direction,
1848
+ video: 'sendrecv',
1849
+ screenShareVideo: 'recvonly',
1850
+ },
1851
+ remoteQualityLevel: 'HIGH'
1852
+ });
1853
+ }
1929
1854
 
1930
- assert.calledWith(Media.getDisplayMedia, {
1931
- sendShare: true,
1932
- sendAudio: false,
1933
- sharePreferences: {highFrameRate: true},
1855
+ if (expected.localMuteSentValue !== undefined) {
1856
+ // and local mute sent to Locus
1857
+ checkLocalMuteSentToLocus({audioMuted: !expected.localMuteSentValue /* negation, because we're un-publishing */, videoMuted: true});
1858
+ assert.calledOnce(locusMediaRequestStub);
1859
+ } else {
1860
+ assert.notCalled(locusMediaRequestStub);
1861
+ }
1862
+ });
1934
1863
  });
1935
- });
1936
1864
  });
1937
1865
 
1938
- describe('stops share immediately', () => {
1939
- let sandbox;
1866
+ describe('updateMedia()', () => {
1940
1867
 
1941
- beforeEach(() => {
1942
- sandbox = sinon.createSandbox();
1943
- });
1868
+ const addMedia = async (enableMedia, track) => {
1869
+ await meeting.addMedia({audioEnabled: enableMedia, localTracks: {microphone: track}});
1870
+ await simulateRoapOffer();
1944
1871
 
1945
- afterEach(() => {
1946
- sandbox.restore();
1947
- sandbox = null;
1872
+ resetHistory();
1873
+ }
1874
+
1875
+ const checkAudioEnabled = (expectedTrack, expectedDirection) => {
1876
+ if (isMultistream) {
1877
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.enableMultistreamAudio, expectedDirection !== 'inactive');
1878
+ } else {
1879
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1880
+ localTracks: { audio: expectedTrack, video: null, screenShareVideo: null },
1881
+ direction: {
1882
+ audio: expectedDirection,
1883
+ video: 'sendrecv',
1884
+ screenShareVideo: 'recvonly',
1885
+ },
1886
+ remoteQualityLevel: 'HIGH'
1887
+ });
1888
+ }
1889
+ }
1890
+
1891
+ it('updateMedia() disables media when nothing is published', async () => {
1892
+ await addMedia(true);
1893
+
1894
+ await meeting.updateMedia({audioEnabled: false});
1895
+
1896
+ // the roap media connection should be updated
1897
+ checkAudioEnabled(null, 'inactive');
1898
+
1899
+ // and that would trigger a new offer so we simulate it happening
1900
+ await simulateRoapOffer();
1901
+
1902
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1903
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1904
+
1905
+ // and no other local mute requests were sent to Locus
1906
+ assert.calledOnce(locusMediaRequestStub);
1948
1907
  });
1949
1908
 
1950
- it('Can bypass canUpdateMedia() check', () => {
1951
- const sendShare = true;
1952
- const receiveShare = false;
1953
- const stream = 'stream';
1909
+ it('updateMedia() enables media when nothing is published', async () => {
1910
+ await addMedia(false);
1954
1911
 
1955
- sandbox.stub(MeetingUtil, 'getTrack').returns({videoTrack: true});
1956
- MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve(true));
1957
- sandbox.stub(meeting, 'canUpdateMedia').returns(true);
1958
- sandbox.stub(meeting, 'setLocalShareTrack');
1912
+ await meeting.updateMedia({audioEnabled: true});
1959
1913
 
1960
- meeting.updateShare({
1961
- sendShare,
1962
- receiveShare,
1963
- stream,
1964
- skipSignalingCheck: true,
1965
- });
1914
+ // the roap media connection should be updated
1915
+ checkAudioEnabled(null, 'sendrecv');
1916
+
1917
+ // and that would trigger a new offer so we simulate it happening
1918
+ await simulateRoapOffer();
1966
1919
 
1967
- assert.notCalled(meeting.canUpdateMedia);
1920
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1921
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1922
+
1923
+ // and no other local mute requests were sent to Locus
1924
+ assert.calledOnce(locusMediaRequestStub);
1968
1925
  });
1969
1926
 
1970
- it('skips canUpdateMedia() check on contentTracks.onended', () => {
1971
- const {mediaProperties} = meeting;
1972
- const fakeTrack = {
1973
- getSettings: sinon.stub().returns({}),
1974
- };
1927
+ it('updateMedia() disables media when track is published', async () => {
1928
+ await addMedia(true, fakeMicrophoneTrack);
1975
1929
 
1976
- const listeners = {};
1930
+ await meeting.updateMedia({audioEnabled: false});
1931
+ await stableState();
1977
1932
 
1978
- const fakeLocalDisplayTrack = {
1979
- on: sinon.stub().callsFake((event, listener) => {
1980
- listeners[event] = listener;
1981
- }),
1982
- };
1983
- sinon.stub(InternalMediaCoreModule, 'LocalDisplayTrack').returns(fakeLocalDisplayTrack);
1933
+ // the roap media connection should be updated
1934
+ checkAudioEnabled(webrtcAudioTrack, 'inactive');
1935
+
1936
+ checkLocalMuteSentToLocus({audioMuted: true, videoMuted: true});
1984
1937
 
1985
- sandbox.stub(mediaProperties, 'setLocalShareTrack');
1986
- sandbox.stub(mediaProperties, 'setMediaSettings');
1987
- sandbox.stub(meeting, 'stopShare').resolves(true);
1988
- meeting.setLocalShareTrack(fakeTrack);
1938
+ locusMediaRequestStub.resetHistory();
1989
1939
 
1990
- assert.calledOnce(fakeLocalDisplayTrack.on);
1991
- assert.calledWith(fakeLocalDisplayTrack.on, LocalTrackEvents.Ended, sinon.match.any);
1992
- assert.isNotNull(listeners[LocalTrackEvents.Ended]);
1940
+ // and that would trigger a new offer so we simulate it happening
1941
+ await simulateRoapOffer();
1993
1942
 
1994
- listeners[LocalTrackEvents.Ended]();
1943
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1944
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1995
1945
 
1996
- assert.calledWith(meeting.stopShare, {skipSignalingCheck: true});
1946
+ // and no other local mute requests were sent to Locus
1947
+ assert.calledOnce(locusMediaRequestStub);
1997
1948
  });
1998
1949
 
1999
- it('stopShare accepts and passes along optional parameters', () => {
2000
- const args = {
2001
- abc: 123,
2002
- receiveShare: false,
2003
- sendShare: false,
2004
- };
1950
+ it('updateMedia() enables media when track is published', async () => {
1951
+ await addMedia(false, fakeMicrophoneTrack);
1952
+
1953
+ await meeting.updateMedia({audioEnabled: true});
1954
+ await stableState();
1955
+
1956
+ // the roap media connection should be updated
1957
+ checkAudioEnabled(webrtcAudioTrack, 'sendrecv');
1958
+
1959
+ checkLocalMuteSentToLocus({audioMuted: false, videoMuted: true});
2005
1960
 
2006
- sandbox.stub(meeting, 'updateShare').returns(Promise.resolve());
2007
- sandbox.stub(meeting.mediaProperties, 'mediaDirection').value(false);
1961
+ locusMediaRequestStub.resetHistory();
2008
1962
 
2009
- meeting.stopShare(args);
1963
+ // and that would trigger a new offer so we simulate it happening
1964
+ await simulateRoapOffer();
2010
1965
 
2011
- assert.calledWith(meeting.updateShare, args);
1966
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1967
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
1968
+
1969
+ // and no other local mute requests were sent to Locus
1970
+ assert.calledOnce(locusMediaRequestStub);
2012
1971
  });
2013
1972
  });
2014
1973
 
2015
- describe('out-of-sync sharing', () => {
2016
- let sandbox;
1974
+ [
1975
+ {mute: true, title: 'muting a track before confluence is created'},
1976
+ {mute: false, title: 'unmuting a track before confluence is created'}
1977
+ ].forEach(({mute, title}) =>
1978
+ it(title, async () => {
1979
+ // initialize the microphone mute state to opposite of what we do in the test
1980
+ fakeMicrophoneTrack.muted = !mute;
2017
1981
 
2018
- beforeEach(() => {
2019
- sandbox = sinon.createSandbox();
2020
- });
1982
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}});
1983
+ await stableState();
2021
1984
 
2022
- afterEach(() => {
2023
- sandbox.restore();
2024
- sandbox = null;
2025
- });
1985
+ resetHistory();
2026
1986
 
2027
- it('handleShareTrackEnded triggers an event', () => {
2028
- const {EVENT_TYPES} = CONSTANTS;
1987
+ assert.equal(fakeMicrophoneTrack.on.getCall(0).args[0], LocalTrackEvents.Muted);
1988
+ const mutedListener = fakeMicrophoneTrack.on.getCall(0).args[1];
1989
+ // simulate track being muted
1990
+ mutedListener({trackState: {muted: mute}});
2029
1991
 
2030
- sandbox.stub(meeting, 'stopShare').resolves(true);
1992
+ await stableState();
2031
1993
 
2032
- meeting.handleShareTrackEnded();
1994
+ // nothing should happen
1995
+ assert.notCalled(locusMediaRequestStub);
1996
+ assert.notCalled(fakeRoapMediaConnection.update);
2033
1997
 
2034
- assert.calledWith(
2035
- TriggerProxy.trigger,
2036
- sinon.match.instanceOf(Meeting),
2037
- {
2038
- file: 'meeting/index',
2039
- function: 'handleShareTrackEnded',
2040
- },
2041
- EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
2042
- {
2043
- type: EVENT_TYPES.LOCAL_SHARE,
2044
- }
2045
- );
2046
- });
2047
- });
2048
- });
1998
+ // now simulate roap offer
1999
+ await simulateRoapOffer();
2049
2000
 
2050
- describe('#shareScreen resolutions', () => {
2051
- let _getDisplayMedia = null;
2052
- const config = DefaultSDKConfig.meetings;
2053
- const {resolution} = config;
2054
- const shareOptions = {
2055
- sendShare: true,
2056
- sendAudio: false,
2057
- };
2058
- const fireFoxOptions = {
2059
- audio: false,
2060
- video: {
2061
- audio: shareOptions.sendAudio,
2062
- video: shareOptions.sendShare,
2063
- },
2064
- };
2001
+ // it should be sent with the right mute status
2002
+ checkSdpOfferSent({audioMuted: mute, videoMuted: true});
2065
2003
 
2066
- const MediaStream = {
2067
- getVideoTracks: () => [
2068
- {
2069
- applyConstraints: () => {},
2070
- },
2071
- ],
2072
- };
2004
+ // nothing else should happen
2005
+ assert.calledOnce(locusMediaRequestStub);
2006
+ assert.notCalled(fakeRoapMediaConnection.update);
2007
+ })
2008
+ );
2009
+ }));
2073
2010
 
2074
- const MediaConstraint = {
2075
- cursor: 'always',
2076
- aspectRatio: config.aspectRatio,
2077
- frameRate: config.screenFrameRate,
2078
- width: null,
2079
- height: null,
2080
- };
2011
+ describe('#acknowledge', () => {
2012
+ it('should have #acknowledge', () => {
2013
+ assert.exists(meeting.acknowledge);
2014
+ });
2015
+ beforeEach(() => {
2016
+ meeting.meetingRequest.acknowledgeMeeting = sinon.stub().returns(Promise.resolve());
2017
+ });
2018
+ it('should acknowledge incoming and return a promise', async () => {
2019
+ const ack = meeting.acknowledge('INCOMING', false);
2081
2020
 
2082
- const browserConditionalValue = (value) => {
2083
- const key = getBrowserName().toLowerCase();
2084
- const defaultKey = 'default';
2021
+ assert.exists(ack.then);
2022
+ await ack;
2023
+ assert.calledOnce(meeting.meetingRequest.acknowledgeMeeting);
2024
+ });
2025
+ it('should acknowledge a non incoming and return a promise', async () => {
2026
+ const ack = meeting.acknowledge(test1, false);
2085
2027
 
2086
- return value[key] || value[defaultKey];
2087
- };
2028
+ assert.exists(ack.then);
2029
+ await ack;
2030
+ assert.notCalled(meeting.meetingRequest.acknowledgeMeeting);
2031
+ });
2032
+ });
2033
+ describe('#decline', () => {
2034
+ it('should have #decline', () => {
2035
+ assert.exists(meeting.decline);
2036
+ });
2037
+ beforeEach(() => {
2038
+ meeting.meetingRequest.declineMeeting = sinon.stub().returns(Promise.resolve());
2039
+ meeting.meetingFiniteStateMachine.ring();
2040
+ });
2041
+ it('should decline the meeting and trigger meeting destroy for 1:1', async () => {
2042
+ await meeting.decline();
2043
+ assert.calledOnce(meeting.meetingRequest.declineMeeting);
2044
+ });
2045
+ });
2046
+ describe('#leave', () => {
2047
+ let sandbox;
2088
2048
 
2089
- before(() => {
2090
- meeting.updateShare = sinon.stub().returns(Promise.resolve());
2049
+ it('should have #leave', () => {
2050
+ assert.exists(meeting.leave);
2051
+ });
2091
2052
 
2092
- if (!global.navigator) {
2093
- global.navigator = {
2094
- mediaDevices: {
2095
- getDisplayMedia: null,
2096
- },
2097
- };
2098
- }
2099
- _getDisplayMedia = global.navigator.mediaDevices.getDisplayMedia;
2100
- Object.defineProperty(global.navigator.mediaDevices, 'getDisplayMedia', {
2101
- value: sinon.stub().returns(Promise.resolve(MediaStream)),
2102
- writable: true,
2053
+ it('should reject if meeting is already inactive', async () => {
2054
+ await meeting.leave().catch((err) => {
2055
+ assert.instanceOf(err, MeetingNotActiveError);
2103
2056
  });
2104
2057
  });
2105
2058
 
2106
- after(() => {
2107
- // clean up for browser
2108
- Object.defineProperty(global.navigator.mediaDevices, 'getDisplayMedia', {
2109
- value: _getDisplayMedia,
2110
- writable: true,
2059
+ it('should reject if meeting is already left', async () => {
2060
+ meeting.meetingState = 'ACTIVE';
2061
+ await meeting.leave().catch((err) => {
2062
+ assert.instanceOf(err, UserNotJoinedError);
2111
2063
  });
2112
2064
  });
2113
2065
 
2114
- // eslint-disable-next-line max-len
2115
- it('will use shareConstraints if defined in provided options', () => {
2116
- const SHARE_WIDTH = 640;
2117
- const SHARE_HEIGHT = 480;
2118
- const shareConstraints = {
2119
- highFrameRate: 2,
2120
- maxWidth: SHARE_WIDTH,
2121
- maxHeight: SHARE_HEIGHT,
2122
- idealWidth: SHARE_WIDTH,
2123
- idealHeight: SHARE_HEIGHT,
2124
- };
2125
-
2126
- // If sharePreferences.shareConstraints is defined it ignores
2127
- // default SDK config settings
2128
- getDisplayMedia(
2129
- {
2130
- ...shareOptions,
2131
- sharePreferences: {shareConstraints},
2132
- },
2133
- config
2134
- );
2066
+ beforeEach(() => {
2067
+ sandbox = sinon.createSandbox();
2068
+ meeting.meetingFiniteStateMachine.ring();
2069
+ meeting.meetingFiniteStateMachine.join();
2070
+ meeting.meetingRequest.leaveMeeting = sinon
2071
+ .stub()
2072
+ .returns(Promise.resolve({body: 'test'}));
2073
+ meeting.locusInfo.onFullLocus = sinon.stub().returns(true);
2074
+ meeting.cleanupLocalTracks = sinon.stub().returns(Promise.resolve());
2075
+ meeting.closeRemoteStream = sinon.stub().returns(Promise.resolve());
2076
+ sandbox.stub(meeting, 'closeRemoteTracks').returns(Promise.resolve());
2077
+ meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
2078
+ meeting.unsetRemoteTracks = sinon.stub();
2079
+ meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2080
+ meeting.unsetPeerConnections = sinon.stub().returns(true);
2081
+ meeting.logger.error = sinon.stub().returns(true);
2082
+ meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
2135
2083
 
2136
- // eslint-disable-next-line no-undef
2137
- assert.calledWith(
2138
- navigator.mediaDevices.getDisplayMedia,
2139
- browserConditionalValue({
2140
- default: {
2141
- video: {...shareConstraints},
2142
- },
2143
- // Firefox is being handled differently
2144
- firefox: fireFoxOptions,
2145
- })
2146
- );
2084
+ // A meeting needs to be joined to leave
2085
+ meeting.meetingState = 'ACTIVE';
2086
+ meeting.state = 'JOINED';
2147
2087
  });
2088
+ afterEach(() => {
2089
+ sandbox.restore();
2090
+ sandbox = null;
2091
+ });
2092
+ it('should leave the meeting and return promise', async () => {
2093
+ const leave = meeting.leave();
2148
2094
 
2149
- // eslint-disable-next-line max-len
2150
- it('will use default resolution if shareConstraints is undefined and highFrameRate is defined', () => {
2151
- // If highFrameRate is defined it ignores default SDK config settings
2152
- getDisplayMedia(
2153
- {
2154
- ...shareOptions,
2155
- sharePreferences: {
2156
- highFrameRate: true,
2157
- },
2158
- },
2159
- config
2160
- );
2161
-
2162
- // eslint-disable-next-line no-undef
2163
- assert.calledWith(
2164
- navigator.mediaDevices.getDisplayMedia,
2165
- browserConditionalValue({
2166
- default: {
2167
- video: {
2168
- ...MediaConstraint,
2169
- frameRate: config.videoShareFrameRate,
2170
- width: resolution.idealWidth,
2171
- height: resolution.idealHeight,
2172
- maxWidth: resolution.maxWidth,
2173
- maxHeight: resolution.maxHeight,
2174
- idealWidth: resolution.idealWidth,
2175
- idealHeight: resolution.idealHeight,
2176
- },
2177
- },
2178
- firefox: fireFoxOptions,
2179
- })
2180
- );
2095
+ assert.exists(leave.then);
2096
+ await leave;
2097
+ assert.calledOnce(meeting.meetingRequest.leaveMeeting);
2098
+ assert.calledOnce(meeting.cleanupLocalTracks);
2099
+ assert.calledOnce(meeting.closeRemoteTracks);
2100
+ assert.calledOnce(meeting.closePeerConnections);
2101
+ assert.calledOnce(meeting.unsetRemoteTracks);
2102
+ assert.calledOnce(meeting.unsetPeerConnections);
2181
2103
  });
2104
+ describe('after audio/video is defined', () => {
2105
+ let handleClientRequest;
2182
2106
 
2183
- // eslint-disable-next-line max-len
2184
- it('will use default screenResolution if shareConstraints, highFrameRate, and SDK defaults is undefined', () => {
2185
- getDisplayMedia(shareOptions);
2186
- const {screenResolution} = config;
2107
+ beforeEach(() => {
2108
+ handleClientRequest = sinon.stub().returns(Promise.resolve(true));
2187
2109
 
2188
- // eslint-disable-next-line no-undef
2189
- assert.calledWith(
2190
- navigator.mediaDevices.getDisplayMedia,
2191
- browserConditionalValue({
2192
- default: {
2193
- video: {
2194
- ...MediaConstraint,
2195
- width: screenResolution.idealWidth,
2196
- height: screenResolution.idealHeight,
2197
- },
2198
- },
2199
- firefox: fireFoxOptions,
2200
- })
2201
- );
2202
- });
2110
+ meeting.audio = {handleClientRequest};
2111
+ meeting.video = {handleClientRequest};
2112
+ });
2203
2113
 
2204
- // Test screenResolution
2205
- // eslint-disable-next-line max-len
2206
- it('will use SDK config screenResolution if set, with shareConstraints and highFrameRate being undefined', () => {
2207
- const SHARE_WIDTH = 800;
2208
- const SHARE_HEIGHT = 600;
2209
- const customConfig = {
2210
- screenResolution: {
2211
- maxWidth: SHARE_WIDTH,
2212
- maxHeight: SHARE_HEIGHT,
2213
- idealWidth: SHARE_WIDTH,
2214
- idealHeight: SHARE_HEIGHT,
2215
- },
2216
- };
2114
+ it('should delete audio and video state machines when leaving the meeting', async () => {
2115
+ const leave = meeting.leave();
2217
2116
 
2218
- getDisplayMedia(shareOptions, customConfig);
2117
+ assert.exists(leave.then);
2118
+ await leave;
2219
2119
 
2220
- // eslint-disable-next-line no-undef
2221
- assert.calledWith(
2222
- navigator.mediaDevices.getDisplayMedia,
2223
- browserConditionalValue({
2224
- default: {
2225
- video: {
2226
- ...MediaConstraint,
2227
- width: SHARE_WIDTH,
2228
- height: SHARE_HEIGHT,
2229
- maxWidth: SHARE_WIDTH,
2230
- maxHeight: SHARE_HEIGHT,
2231
- idealWidth: SHARE_WIDTH,
2232
- idealHeight: SHARE_HEIGHT,
2233
- },
2234
- },
2235
- firefox: fireFoxOptions,
2236
- })
2237
- );
2120
+ assert.isNull(meeting.audio);
2121
+ assert.isNull(meeting.video);
2122
+ });
2238
2123
  });
2124
+ it('should leave the meeting without leaving resource', async () => {
2125
+ const leave = meeting.leave({resourceId: null});
2239
2126
 
2240
- // Test screenFrameRate
2241
- it('will use SDK config screenFrameRate if set, with shareConstraints and highFrameRate being undefined', () => {
2242
- const SHARE_WIDTH = 800;
2243
- const SHARE_HEIGHT = 600;
2244
- const customConfig = {
2245
- screenFrameRate: 999,
2246
- screenResolution: {
2247
- maxWidth: SHARE_WIDTH,
2248
- maxHeight: SHARE_HEIGHT,
2249
- idealWidth: SHARE_WIDTH,
2250
- idealHeight: SHARE_HEIGHT,
2251
- },
2252
- };
2127
+ assert.exists(leave.then);
2128
+ await leave;
2129
+ assert.calledWith(meeting.meetingRequest.leaveMeeting, {
2130
+ locusUrl: meeting.locusUrl,
2131
+ correlationId: meeting.correlationId,
2132
+ selfId: meeting.selfId,
2133
+ resourceId: null,
2134
+ deviceUrl: meeting.deviceUrl,
2135
+ });
2136
+ });
2137
+ it('should leave the meeting on the resource', async () => {
2138
+ const leave = meeting.leave();
2253
2139
 
2254
- getDisplayMedia(shareOptions, customConfig);
2140
+ assert.exists(leave.then);
2141
+ await leave;
2142
+ assert.calledWith(meeting.meetingRequest.leaveMeeting, {
2143
+ locusUrl: meeting.locusUrl,
2144
+ correlationId: meeting.correlationId,
2145
+ selfId: meeting.selfId,
2146
+ resourceId: meeting.resourceId,
2147
+ deviceUrl: meeting.deviceUrl
2148
+ });
2149
+ });
2150
+ it('should leave the meeting on the resource with reason', async () => {
2151
+ const leave = meeting.leave({resourceId: meeting.resourceId, reason: MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST});
2255
2152
 
2256
- // eslint-disable-next-line no-undef
2257
- assert.calledWith(
2258
- navigator.mediaDevices.getDisplayMedia,
2259
- browserConditionalValue({
2260
- default: {
2261
- video: {
2262
- ...MediaConstraint,
2263
- frameRate: customConfig.screenFrameRate,
2264
- width: SHARE_WIDTH,
2265
- height: SHARE_HEIGHT,
2266
- maxWidth: SHARE_WIDTH,
2267
- maxHeight: SHARE_HEIGHT,
2268
- idealWidth: SHARE_WIDTH,
2269
- idealHeight: SHARE_HEIGHT,
2270
- },
2271
- },
2272
- firefox: fireFoxOptions,
2273
- })
2274
- );
2153
+ assert.exists(leave.then);
2154
+ await leave;
2155
+ assert.calledWith(meeting.meetingRequest.leaveMeeting, {
2156
+ locusUrl: meeting.locusUrl,
2157
+ correlationId: meeting.correlationId,
2158
+ selfId: meeting.selfId,
2159
+ resourceId: meeting.resourceId,
2160
+ deviceUrl: meeting.deviceUrl,
2161
+ reason: MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST
2162
+ });
2275
2163
  });
2276
2164
  });
2277
-
2278
- describe('#stopShare', () => {
2279
- it('should have #stopShare', () => {
2280
- assert.exists(meeting.stopShare);
2165
+ describe('#requestScreenShareFloor', () => {
2166
+ it('should have #requestScreenShareFloor', () => {
2167
+ assert.exists(meeting.requestScreenShareFloor);
2281
2168
  });
2282
2169
  beforeEach(() => {
2283
- meeting.mediaProperties.mediaDirection = {receiveShare: true};
2284
- meeting.updateShare = sinon.stub().returns(Promise.resolve());
2170
+ meeting.locusInfo.mediaShares = [{name: 'content', url: url1}];
2171
+ meeting.locusInfo.self = {url: url1};
2172
+ meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
2173
+ meeting.mediaProperties.shareTrack = {}
2174
+ meeting.mediaProperties.mediaDirection.sendShare = true;
2175
+ meeting.state = 'JOINED';
2285
2176
  });
2286
- it('should call updateShare', async () => {
2287
- const share = meeting.stopShare();
2177
+ it('should send the share', async () => {
2178
+ const share = meeting.requestScreenShareFloor();
2288
2179
 
2289
2180
  assert.exists(share.then);
2290
2181
  await share;
2291
- assert.calledOnce(meeting.updateShare);
2292
- });
2293
- });
2294
-
2295
- describe('#updateAudio', () => {
2296
- const FAKE_AUDIO_TRACK = {
2297
- id: 'fake audio track',
2298
- getSettings: sinon.stub().returns({}),
2299
- };
2300
-
2301
- describe('when canUpdateMedia is true', () => {
2302
- beforeEach(() => {
2303
- meeting.canUpdateMedia = sinon.stub().returns(true);
2304
- });
2305
- describe('when options are valid', () => {
2306
- beforeEach(() => {
2307
- MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
2308
- meeting.mediaProperties.mediaDirection = {
2309
- sendAudio: false,
2310
- sendVideo: true,
2311
- sendShare: false,
2312
- receiveAudio: false,
2313
- receiveVideo: true,
2314
- receiveShare: true,
2315
- };
2316
- meeting.mediaProperties.webrtcMediaConnection = {
2317
- update: sinon.stub(),
2318
- };
2319
- sinon.stub(MeetingUtil, 'getTrack').returns({audioTrack: FAKE_AUDIO_TRACK});
2320
- });
2321
- it('calls this.mediaProperties.webrtcMediaConnection.update', () =>
2322
- meeting
2323
- .updateAudio({
2324
- sendAudio: true,
2325
- receiveAudio: true,
2326
- stream: {id: 'fake stream'},
2327
- })
2328
- .then(() => {
2329
- assert.calledOnce(
2330
- meeting.mediaProperties.webrtcMediaConnection.update
2331
- );
2332
- assert.calledWith(
2333
- meeting.mediaProperties.webrtcMediaConnection.update,
2334
- {
2335
- localTracks: {audio: FAKE_AUDIO_TRACK},
2336
- direction: {
2337
- audio: 'sendrecv',
2338
- video: 'sendrecv',
2339
- screenShareVideo: 'recvonly',
2340
- },
2341
- remoteQualityLevel: 'HIGH',
2342
- }
2343
- );
2344
- }));
2345
- });
2346
- afterEach(() => {
2347
- sinon.restore();
2348
- });
2182
+ assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
2349
2183
  });
2350
2184
  });
2351
2185
 
@@ -2393,38 +2227,25 @@ describe('plugin-meetings', () => {
2393
2227
 
2394
2228
  describe('#updateMedia', () => {
2395
2229
  let sandbox;
2396
- const mockLocalStream = {id: 'mock local stream'};
2397
- const mockLocalShare = {id: 'mock local share stream'};
2398
- const FAKE_TRACKS = {
2399
- audio: {
2400
- id: 'fake audio track',
2401
- getSettings: sinon.stub().returns({}),
2402
- },
2403
- video: {
2404
- id: 'fake video track',
2405
- getSettings: sinon.stub().returns({}),
2406
- },
2407
- screenshareVideo: {
2408
- id: 'fake share track',
2409
- getSettings: sinon.stub().returns({}),
2410
- on: sinon.stub(),
2411
- },
2412
- };
2413
2230
 
2231
+ const createFakeLocalTrack = () => ({
2232
+ underlyingTrack: {id: 'fake underlying track'}
2233
+ });
2414
2234
  beforeEach(() => {
2415
2235
  sandbox = sinon.createSandbox();
2416
- meeting.mediaProperties.mediaDirection = {sendShare: true};
2417
- // setup the stub to return the right tracks
2418
- sandbox.stub(MeetingUtil, 'getTrack').callsFake((stream) => {
2419
- if (stream === mockLocalStream) {
2420
- return {audioTrack: FAKE_TRACKS.audio, videoTrack: FAKE_TRACKS.video};
2421
- }
2422
- if (stream === mockLocalShare) {
2423
- return {audioTrack: null, videoTrack: FAKE_TRACKS.screenshareVideo};
2424
- }
2425
-
2426
- return {audioTrack: null, videoTrack: null};
2427
- });
2236
+ meeting.audio = { enable: sinon.stub()};
2237
+ meeting.video = { enable: sinon.stub()};
2238
+ meeting.mediaProperties.audioTrack = createFakeLocalTrack();
2239
+ meeting.mediaProperties.videoTrack = createFakeLocalTrack();
2240
+ meeting.mediaProperties.shareTrack = createFakeLocalTrack();
2241
+ meeting.mediaProperties.mediaDirection = {
2242
+ sendAudio: true,
2243
+ sendVideo: true,
2244
+ sendShare: true,
2245
+ receiveAudio: true,
2246
+ receiveVideo: true,
2247
+ receiveShare: true,
2248
+ }
2428
2249
  });
2429
2250
 
2430
2251
  afterEach(() => {
@@ -2434,52 +2255,28 @@ describe('plugin-meetings', () => {
2434
2255
 
2435
2256
  forEach(
2436
2257
  [
2437
- {receiveAudio: true, sendAudio: true, enableMultistreamAudio: true},
2438
- {receiveAudio: true, sendAudio: false, enableMultistreamAudio: true},
2439
- {receiveAudio: false, sendAudio: true, enableMultistreamAudio: true},
2440
- {receiveAudio: false, sendAudio: false, enableMultistreamAudio: false},
2258
+ {audioEnabled: true, enableMultistreamAudio: true},
2259
+ {audioEnabled: false, enableMultistreamAudio: false},
2441
2260
  ],
2442
- ({receiveAudio, sendAudio, enableMultistreamAudio}) => {
2443
- it(`should call enableMultistreamAudio with ${enableMultistreamAudio} if it is a multistream connection and receiveAudio: ${receiveAudio} sendAudio: ${sendAudio}`, async () => {
2444
- const mediaSettings = {
2445
- sendAudio,
2446
- receiveAudio,
2447
- sendVideo: true,
2448
- receiveVideo: true,
2449
- sendShare: true,
2450
- receiveShare: true,
2451
- isSharing: true,
2452
- };
2453
-
2261
+ ({audioEnabled, enableMultistreamAudio}) => {
2262
+ it(`should call enableMultistreamAudio with ${enableMultistreamAudio} if it is a multistream connection and audioEnabled: ${audioEnabled}`, async () => {
2454
2263
  meeting.mediaProperties.webrtcMediaConnection = {
2455
- enableMultistreamAudio: sinon.stub().resolves('some value'),
2264
+ enableMultistreamAudio: sinon.stub().resolves({}),
2456
2265
  };
2457
2266
  meeting.isMultistream = true;
2458
2267
 
2459
- const result = await meeting.updateMedia({
2460
- mediaSettings,
2461
- });
2268
+ await meeting.updateMedia({audioEnabled});
2462
2269
 
2463
2270
  assert.calledOnceWithExactly(
2464
2271
  meeting.mediaProperties.webrtcMediaConnection.enableMultistreamAudio,
2465
2272
  enableMultistreamAudio
2466
2273
  );
2467
- assert.equal(result, 'some value');
2274
+ assert.calledOnceWithExactly(meeting.audio.enable, meeting, enableMultistreamAudio);
2468
2275
  });
2469
2276
  }
2470
2277
  );
2471
2278
 
2472
2279
  it('should use a queue if currently busy', async () => {
2473
- const mediaSettings = {
2474
- sendAudio: true,
2475
- receiveAudio: true,
2476
- sendVideo: true,
2477
- receiveVideo: true,
2478
- sendShare: true,
2479
- receiveShare: true,
2480
- isSharing: true,
2481
- };
2482
-
2483
2280
  sandbox.stub(meeting, 'canUpdateMedia').returns(false);
2484
2281
  meeting.mediaProperties.webrtcMediaConnection = {
2485
2282
  update: sinon.stub().resolves({}),
@@ -2488,11 +2285,7 @@ describe('plugin-meetings', () => {
2488
2285
  let myPromiseResolved = false;
2489
2286
 
2490
2287
  meeting
2491
- .updateMedia({
2492
- localStream: mockLocalStream,
2493
- localShare: mockLocalShare,
2494
- mediaSettings,
2495
- })
2288
+ .updateMedia({audioEnabled: false, videoEnabled: false})
2496
2289
  .then(() => {
2497
2290
  myPromiseResolved = true;
2498
2291
  });
@@ -2513,13 +2306,13 @@ describe('plugin-meetings', () => {
2513
2306
  meeting.mediaProperties.webrtcMediaConnection.update,
2514
2307
  {
2515
2308
  localTracks: {
2516
- audio: FAKE_TRACKS.audio,
2517
- video: FAKE_TRACKS.video,
2518
- screenShareVideo: FAKE_TRACKS.screenshareVideo,
2309
+ audio: meeting.mediaProperties.audioTrack.underlyingTrack,
2310
+ video: meeting.mediaProperties.videoTrack.underlyingTrack,
2311
+ screenShareVideo: meeting.mediaProperties.shareTrack.underlyingTrack,
2519
2312
  },
2520
2313
  direction: {
2521
- audio: 'sendrecv',
2522
- video: 'sendrecv',
2314
+ audio: 'inactive',
2315
+ video: 'inactive',
2523
2316
  screenShareVideo: 'sendrecv',
2524
2317
  },
2525
2318
  remoteQualityLevel: 'HIGH',
@@ -2527,195 +2320,6 @@ describe('plugin-meetings', () => {
2527
2320
  );
2528
2321
  assert.isTrue(myPromiseResolved);
2529
2322
  });
2530
-
2531
- it('should request floor only after roap transaction is completed', async () => {
2532
- const eventListeners = {};
2533
-
2534
- meeting.webex.meetings.reachability = {
2535
- isAnyClusterReachable: sandbox.stub().resolves(true),
2536
- };
2537
-
2538
- const fakeMediaConnection = {
2539
- close: sinon.stub(),
2540
- getConnectionState: sinon.stub().returns(ConnectionState.Connected),
2541
- initiateOffer: sinon.stub().resolves({}),
2542
-
2543
- // mock the on() method and store all the listeners
2544
- on: sinon.stub().callsFake((event, listener) => {
2545
- eventListeners[event] = listener;
2546
- }),
2547
-
2548
- update: sinon.stub().callsFake(() => {
2549
- // trigger ROAP_STARTED before update() resolves
2550
- if (eventListeners[Event.ROAP_STARTED]) {
2551
- eventListeners[Event.ROAP_STARTED]();
2552
- } else {
2553
- throw new Error('ROAP_STARTED listener not registered');
2554
- }
2555
- return Promise.resolve();
2556
- }),
2557
- };
2558
-
2559
- meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
2560
- meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
2561
- Media.createMediaConnection = sinon.stub().returns(fakeMediaConnection);
2562
-
2563
- const requestScreenShareFloorStub = sandbox
2564
- .stub(meeting, 'requestScreenShareFloor')
2565
- .resolves({});
2566
-
2567
- let myPromiseResolved = false;
2568
-
2569
- meeting.meetingState = 'ACTIVE';
2570
- await meeting.addMedia({
2571
- mediaSettings: {},
2572
- });
2573
-
2574
- meeting
2575
- .updateMedia({
2576
- localShare: mockLocalShare,
2577
- mediaSettings: {
2578
- sendShare: true,
2579
- },
2580
- })
2581
- .then(() => {
2582
- myPromiseResolved = true;
2583
- });
2584
-
2585
- await testUtils.flushPromises();
2586
-
2587
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.update);
2588
- assert.isFalse(myPromiseResolved);
2589
-
2590
- // verify that requestScreenShareFloorStub was not called yet
2591
- assert.notCalled(requestScreenShareFloorStub);
2592
-
2593
- eventListeners[Event.ROAP_DONE]();
2594
- await testUtils.flushPromises();
2595
-
2596
- // now it should have been called
2597
- assert.calledOnce(requestScreenShareFloorStub);
2598
- });
2599
- });
2600
-
2601
- describe('#updateShare', () => {
2602
- const mockLocalShare = {id: 'mock local share stream'};
2603
- let eventListeners;
2604
- let fakeMediaConnection;
2605
- let requestScreenShareFloorStub;
2606
-
2607
- const FAKE_TRACKS = {
2608
- screenshareVideo: {
2609
- id: 'fake share track',
2610
- getSettings: sinon.stub().returns({}),
2611
- on: sinon.stub(),
2612
- },
2613
- };
2614
-
2615
- beforeEach(async () => {
2616
- eventListeners = {};
2617
-
2618
- sinon.stub(MeetingUtil, 'getTrack').callsFake((stream) => {
2619
- if (stream === mockLocalShare) {
2620
- return {audioTrack: null, videoTrack: FAKE_TRACKS.screenshareVideo};
2621
- }
2622
-
2623
- return {audioTrack: null, videoTrack: null};
2624
- });
2625
-
2626
- meeting.webex.meetings.reachability = {
2627
- isAnyClusterReachable: sinon.stub().resolves(true),
2628
- };
2629
-
2630
- fakeMediaConnection = {
2631
- close: sinon.stub(),
2632
- getConnectionState: sinon.stub().returns(ConnectionState.Connected),
2633
- initiateOffer: sinon.stub().resolves({}),
2634
-
2635
- // mock the on() method and store all the listeners
2636
- on: sinon.stub().callsFake((event, listener) => {
2637
- eventListeners[event] = listener;
2638
- }),
2639
-
2640
- update: sinon.stub().callsFake(() => {
2641
- // trigger ROAP_STARTED before update() resolves
2642
- if (eventListeners[Event.ROAP_STARTED]) {
2643
- eventListeners[Event.ROAP_STARTED]();
2644
- } else {
2645
- throw new Error('ROAP_STARTED listener not registered');
2646
- }
2647
- return Promise.resolve();
2648
- }),
2649
- };
2650
-
2651
- meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
2652
- meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
2653
- Media.createMediaConnection = sinon.stub().returns(fakeMediaConnection);
2654
-
2655
- requestScreenShareFloorStub = sinon.stub(meeting, 'requestScreenShareFloor').resolves({});
2656
-
2657
- meeting.meetingState = 'ACTIVE';
2658
- await meeting.addMedia({
2659
- mediaSettings: {},
2660
- });
2661
- });
2662
-
2663
- afterEach(() => {
2664
- sinon.restore();
2665
- });
2666
-
2667
- it('when starting share, it should request floor only after roap transaction is completed', async () => {
2668
- let myPromiseResolved = false;
2669
-
2670
- meeting
2671
- .updateShare({
2672
- sendShare: true,
2673
- receiveShare: true,
2674
- stream: mockLocalShare,
2675
- })
2676
- .then(() => {
2677
- myPromiseResolved = true;
2678
- });
2679
-
2680
- await testUtils.flushPromises();
2681
-
2682
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.update);
2683
- assert.isFalse(myPromiseResolved);
2684
-
2685
- // verify that requestScreenShareFloorStub was not called yet
2686
- assert.notCalled(requestScreenShareFloorStub);
2687
-
2688
- eventListeners[Event.ROAP_DONE]();
2689
- await testUtils.flushPromises();
2690
-
2691
- // now it should have been called
2692
- assert.calledOnce(requestScreenShareFloorStub);
2693
- });
2694
-
2695
- it('when changing screen share stream and no roap transaction happening, it requests floor immediately', async () => {
2696
- let myPromiseResolved = false;
2697
-
2698
- // simulate a case when no roap transaction is triggered by update
2699
- meeting.mediaProperties.webrtcMediaConnection.update = sinon
2700
- .stub()
2701
- .resolves({});
2702
-
2703
- meeting
2704
- .updateShare({
2705
- sendShare: true,
2706
- receiveShare: true,
2707
- stream: mockLocalShare,
2708
- })
2709
- .then(() => {
2710
- myPromiseResolved = true;
2711
- });
2712
-
2713
- await testUtils.flushPromises();
2714
-
2715
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.update);
2716
- assert.calledOnce(requestScreenShareFloorStub);
2717
- assert.isTrue(myPromiseResolved);
2718
- });
2719
2323
  });
2720
2324
 
2721
2325
  describe('#changeVideoLayout', () => {
@@ -2730,8 +2334,6 @@ describe('plugin-meetings', () => {
2730
2334
  sendShare: false,
2731
2335
  receiveVideo: true,
2732
2336
  };
2733
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([]));
2734
- meeting.updateVideo = sinon.stub().returns(Promise.resolve());
2735
2337
  meeting.mediaProperties.mediaDirection = mediaDirection;
2736
2338
  meeting.mediaProperties.remoteVideoTrack = sinon
2737
2339
  .stub()
@@ -2968,76 +2570,12 @@ describe('plugin-meetings', () => {
2968
2570
  });
2969
2571
  });
2970
2572
 
2971
- describe('#setLocalVideoQuality', () => {
2972
- let mediaDirection;
2973
-
2974
- const fakeTrack = {getSettings: () => ({height: 720})};
2975
- const USER_AGENT_CHROME_MAC =
2976
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
2977
- 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36';
2978
-
2979
- beforeEach(() => {
2980
- mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
2981
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([]));
2982
- meeting.mediaProperties.mediaDirection = mediaDirection;
2983
- meeting.canUpdateMedia = sinon.stub().returns(true);
2984
- MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
2985
- meeting.updateVideo = sinon.stub().resolves();
2986
- sinon.stub(MeetingUtil, 'getTrack').returns({videoTrack: fakeTrack});
2987
- });
2988
-
2989
- it('should have #setLocalVideoQuality', () => {
2990
- assert.exists(meeting.setLocalVideoQuality);
2991
- });
2992
-
2993
- it('should call getMediaStreams with the proper level', () =>
2994
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2995
- delete mediaDirection.receiveVideo;
2996
- assert.calledWith(
2997
- meeting.getMediaStreams,
2998
- mediaDirection,
2999
- CONSTANTS.VIDEO_RESOLUTIONS[CONSTANTS.QUALITY_LEVELS.LOW]
3000
- );
3001
- }));
3002
-
3003
- it('when browser is chrome then it should stop previous video track', () => {
3004
- meeting.mediaProperties.videoTrack = fakeTrack;
3005
- assert.equal(BrowserDetection(USER_AGENT_CHROME_MAC).getBrowserName(), 'Chrome');
3006
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
3007
- assert.calledWith(Media.stopTracks, fakeTrack);
3008
- });
3009
- });
3010
-
3011
- it('should set mediaProperty with the proper level', () =>
3012
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
3013
- assert.equal(meeting.mediaProperties.localQualityLevel, CONSTANTS.QUALITY_LEVELS.LOW);
3014
- }));
3015
-
3016
- it('when device does not support 1080p then it should set localQualityLevel with highest possible resolution', () => {
3017
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS['1080p']).then(() => {
3018
- assert.equal(
3019
- meeting.mediaProperties.localQualityLevel,
3020
- CONSTANTS.QUALITY_LEVELS['720p']
3021
- );
3022
- });
3023
- });
3024
-
3025
- it('should error if set to a invalid level', () => {
3026
- assert.isRejected(meeting.setLocalVideoQuality('invalid'));
3027
- });
3028
-
3029
- it('should error if sendVideo is set to false', () => {
3030
- meeting.mediaProperties.mediaDirection = {sendVideo: false};
3031
- assert.isRejected(meeting.setLocalVideoQuality('LOW'));
3032
- });
3033
- });
3034
-
3035
2573
  describe('#setRemoteQualityLevel', () => {
3036
2574
  let mediaDirection;
3037
2575
 
3038
2576
  beforeEach(() => {
3039
2577
  mediaDirection = {receiveAudio: true, receiveVideo: true, receiveShare: false};
3040
- meeting.updateMedia = sinon.stub().returns(Promise.resolve());
2578
+ meeting.updateTranscodedMediaConnection = sinon.stub().returns(Promise.resolve());
3041
2579
  meeting.mediaProperties.mediaDirection = mediaDirection;
3042
2580
  });
3043
2581
 
@@ -3050,9 +2588,9 @@ describe('plugin-meetings', () => {
3050
2588
  assert.equal(meeting.mediaProperties.remoteQualityLevel, CONSTANTS.QUALITY_LEVELS.LOW);
3051
2589
  }));
3052
2590
 
3053
- it('should call updateMedia', () =>
2591
+ it('should call Meeting.updateTranscodedMediaConnection()', () =>
3054
2592
  meeting.setRemoteQualityLevel(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
3055
- assert.calledOnce(meeting.updateMedia);
2593
+ assert.calledOnce(meeting.updateTranscodedMediaConnection);
3056
2594
  }));
3057
2595
 
3058
2596
  it('should error if set to a invalid level', () => {
@@ -3726,16 +3264,12 @@ describe('plugin-meetings', () => {
3726
3264
  .stub()
3727
3265
  .returns(Promise.resolve({body: 'test'}));
3728
3266
  meeting.locusInfo.onFullLocus = sinon.stub().returns(true);
3729
- meeting.closeLocalStream = sinon.stub().returns(Promise.resolve());
3730
- meeting.closeLocalShare = sinon.stub().returns(Promise.resolve());
3267
+ meeting.cleanupLocalTracks = sinon.stub().returns(Promise.resolve());
3731
3268
  meeting.closeRemoteStream = sinon.stub().returns(Promise.resolve());
3732
3269
  sandbox.stub(meeting, 'closeRemoteTracks').returns(Promise.resolve());
3733
3270
  meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
3734
- meeting.unsetLocalVideoTrack = sinon.stub().returns(true);
3735
- meeting.unsetLocalShareTrack = sinon.stub().returns(true);
3736
3271
  meeting.unsetRemoteTracks = sinon.stub();
3737
3272
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
3738
- meeting.unsetRemoteStream = sinon.stub().returns(true);
3739
3273
  meeting.unsetPeerConnections = sinon.stub().returns(true);
3740
3274
  meeting.logger.error = sinon.stub().returns(true);
3741
3275
  meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
@@ -3754,12 +3288,9 @@ describe('plugin-meetings', () => {
3754
3288
  assert.exists(endMeetingForAll.then);
3755
3289
  await endMeetingForAll;
3756
3290
  assert.calledOnce(meeting?.meetingRequest?.endMeetingForAll);
3757
- assert.calledOnce(meeting?.closeLocalStream);
3758
- assert.calledOnce(meeting?.closeLocalShare);
3291
+ assert.calledOnce(meeting?.cleanupLocalTracks);
3759
3292
  assert.calledOnce(meeting?.closeRemoteTracks);
3760
3293
  assert.calledOnce(meeting?.closePeerConnections);
3761
- assert.calledOnce(meeting?.unsetLocalVideoTrack);
3762
- assert.calledOnce(meeting?.unsetLocalShareTrack);
3763
3294
  assert.calledOnce(meeting?.unsetRemoteTracks);
3764
3295
  assert.calledOnce(meeting?.unsetPeerConnections);
3765
3296
  });
@@ -3770,11 +3301,9 @@ describe('plugin-meetings', () => {
3770
3301
 
3771
3302
  beforeEach(() => {
3772
3303
  sandbox = sinon.createSandbox();
3773
- sandbox.stub(meeting, 'closeLocalStream');
3774
- sandbox.stub(meeting, 'closeLocalShare');
3304
+ sandbox.stub(meeting, 'cleanupLocalTracks');
3775
3305
 
3776
3306
  sandbox.stub(meeting.mediaProperties, 'setMediaDirection');
3777
- sandbox.stub(meeting.mediaProperties, 'unsetMediaTracks');
3778
3307
 
3779
3308
  sandbox.stub(meeting.reconnectionManager, 'reconnectMedia').returns(Promise.resolve());
3780
3309
  sandbox
@@ -3847,14 +3376,12 @@ describe('plugin-meetings', () => {
3847
3376
 
3848
3377
  // beacuse we are calling callback so we need to wait
3849
3378
 
3850
- assert.called(meeting.closeLocalStream);
3851
- assert.called(meeting.closeLocalShare);
3379
+ assert.called(meeting.cleanupLocalTracks);
3852
3380
 
3853
3381
  // give queued Promise callbacks a chance to run
3854
3382
  await Promise.resolve();
3855
3383
 
3856
3384
  assert.called(meeting.mediaProperties.setMediaDirection);
3857
- assert.called(meeting.mediaProperties.unsetMediaTracks);
3858
3385
 
3859
3386
  assert.calledWith(meeting.reconnectionManager.reconnectMedia, {
3860
3387
  mediaDirection: {
@@ -4003,8 +3530,8 @@ describe('plugin-meetings', () => {
4003
3530
  meeting.requestScreenShareFloor = sinon.stub().resolves({});
4004
3531
  meeting.releaseScreenShareFloor = sinon.stub().resolves({});
4005
3532
  meeting.mediaProperties.mediaDirection = {
4006
- sendAudio: false,
4007
- sendVideo: false,
3533
+ sendAudio: 'fake value', // using non-boolean here so that we can check that these values are untouched in tests
3534
+ sendVideo: 'fake value',
4008
3535
  sendShare: false,
4009
3536
  };
4010
3537
  meeting.isMultistream = true;
@@ -4012,6 +3539,8 @@ describe('plugin-meetings', () => {
4012
3539
  publishTrack: sinon.stub().resolves({}),
4013
3540
  unpublishTrack: sinon.stub().resolves({}),
4014
3541
  };
3542
+ meeting.audio = { handleLocalTrackChange: sinon.stub()};
3543
+ meeting.video = { handleLocalTrackChange: sinon.stub()};
4015
3544
 
4016
3545
  const createFakeLocalTrack = (originalTrack) => ({
4017
3546
  on: sinon.stub(),
@@ -4052,33 +3581,25 @@ describe('plugin-meetings', () => {
4052
3581
  });
4053
3582
 
4054
3583
  const checkAudioPublished = (track) => {
4055
- assert.calledWith(
4056
- createMuteStateStub,
4057
- 'audio',
4058
- meeting,
4059
- meeting.mediaProperties.mediaDirection
4060
- );
3584
+ assert.calledOnceWithExactly(meeting.audio.handleLocalTrackChange, meeting);
4061
3585
  assert.calledWith(
4062
3586
  meeting.mediaProperties.webrtcMediaConnection.publishTrack,
4063
3587
  track
4064
3588
  );
4065
3589
  assert.equal(meeting.mediaProperties.audioTrack, track);
4066
- assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, true);
3590
+ // check that sendAudio hasn't been touched
3591
+ assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, 'fake value');
4067
3592
  };
4068
3593
 
4069
3594
  const checkVideoPublished = (track) => {
4070
- assert.calledWith(
4071
- createMuteStateStub,
4072
- 'video',
4073
- meeting,
4074
- meeting.mediaProperties.mediaDirection
4075
- );
3595
+ assert.calledOnceWithExactly(meeting.video.handleLocalTrackChange, meeting);
4076
3596
  assert.calledWith(
4077
3597
  meeting.mediaProperties.webrtcMediaConnection.publishTrack,
4078
3598
  track
4079
3599
  );
4080
3600
  assert.equal(meeting.mediaProperties.videoTrack, track);
4081
- assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, true);
3601
+ // check that sendVideo hasn't been touched
3602
+ assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, 'fake value');
4082
3603
  };
4083
3604
 
4084
3605
  const checkScreenShareVideoPublished = (track) => {
@@ -4099,18 +3620,16 @@ describe('plugin-meetings', () => {
4099
3620
  checkScreenShareVideoPublished(videoShareTrack);
4100
3621
  });
4101
3622
 
4102
- it('creates MuteState instance and publishes the track for main audio', async () => {
3623
+ it('updates MuteState instance and publishes the track for main audio', async () => {
4103
3624
  await meeting.publishTracks({microphone: audioTrack});
4104
3625
 
4105
- assert.calledOnce(createMuteStateStub);
4106
3626
  assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
4107
3627
  checkAudioPublished(audioTrack);
4108
3628
  });
4109
3629
 
4110
- it('creates MuteState instance and publishes the track for main video', async () => {
3630
+ it('updates MuteState instance and publishes the track for main video', async () => {
4111
3631
  await meeting.publishTracks({camera: videoTrack});
4112
3632
 
4113
- assert.calledOnce(createMuteStateStub);
4114
3633
  assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
4115
3634
  checkVideoPublished(videoTrack);
4116
3635
  });
@@ -4124,7 +3643,6 @@ describe('plugin-meetings', () => {
4124
3643
  },
4125
3644
  });
4126
3645
 
4127
- assert.calledTwice(createMuteStateStub);
4128
3646
  assert.calledThrice(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
4129
3647
  checkAudioPublished(audioTrack);
4130
3648
  checkVideoPublished(videoTrack);
@@ -4156,7 +3674,7 @@ describe('plugin-meetings', () => {
4156
3674
  );
4157
3675
 
4158
3676
  assert.equal(meeting.mediaProperties.audioTrack, null);
4159
- assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, false);
3677
+ assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, 'fake value');
4160
3678
  };
4161
3679
 
4162
3680
  const checkVideoUnpublished = () => {
@@ -4166,7 +3684,7 @@ describe('plugin-meetings', () => {
4166
3684
  );
4167
3685
 
4168
3686
  assert.equal(meeting.mediaProperties.videoTrack, null);
4169
- assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, false);
3687
+ assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, 'fake value');
4170
3688
  };
4171
3689
 
4172
3690
  const checkScreenShareVideoUnpublished = () => {
@@ -4398,82 +3916,6 @@ describe('plugin-meetings', () => {
4398
3916
  );
4399
3917
  });
4400
3918
  });
4401
- describe('#closeLocalShare', () => {
4402
- it('should stop the stream, and trigger a media:stopped event when the local share stream stops', async () => {
4403
- await meeting.closeLocalShare();
4404
- assert.calledTwice(TriggerProxy.trigger);
4405
-
4406
- assert.equal(TriggerProxy.trigger.getCall(1).args[2], 'media:stopped');
4407
- assert.deepEqual(TriggerProxy.trigger.getCall(1).args[3], {type: 'localShare'});
4408
- });
4409
- });
4410
- describe('#closeLocalStream', () => {
4411
- it('should stop the stream, and trigger a media:stopped event when the local stream stops', async () => {
4412
- await meeting.closeLocalStream();
4413
- assert.calledTwice(TriggerProxy.trigger);
4414
- assert.calledWith(
4415
- TriggerProxy.trigger,
4416
- sinon.match.instanceOf(Meeting),
4417
- {file: 'meeting/index', function: 'closeLocalStream'},
4418
- 'media:stopped',
4419
- {type: 'local'}
4420
- );
4421
- });
4422
- });
4423
- describe('#setLocalTracks', () => {
4424
- it('stores the current video device as the preferred video device', () => {
4425
- const videoDevice = 'video1';
4426
- const fakeTrack = {getSettings: () => ({deviceId: videoDevice})};
4427
- const fakeStream = 'stream1';
4428
-
4429
- sandbox.stub(MeetingUtil, 'getTrack').returns({audioTrack: null, videoTrack: fakeTrack});
4430
- sandbox.stub(meeting.mediaProperties, 'setMediaSettings');
4431
- sandbox.stub(meeting.mediaProperties, 'setVideoDeviceId');
4432
-
4433
- meeting.setLocalTracks(fakeStream);
4434
-
4435
- assert.calledWith(meeting.mediaProperties.setVideoDeviceId, videoDevice);
4436
- });
4437
- });
4438
- describe('#setLocalShareTrack', () => {
4439
- it('should trigger a media:ready event with local share stream', () => {
4440
- const track = {
4441
- getSettings: sinon.stub().returns({
4442
- aspectRatio: '1.7',
4443
- frameRate: 30,
4444
- height: 1980,
4445
- width: 1080,
4446
- displaySurface: true,
4447
- cursor: true,
4448
- }),
4449
- };
4450
-
4451
- const listeners = {};
4452
- const fakeLocalDisplayTrack = {
4453
- on: sinon.stub().callsFake((event, listener) => {
4454
- listeners[event] = listener;
4455
- }),
4456
- };
4457
- sinon.stub(InternalMediaCoreModule, 'LocalDisplayTrack').returns(fakeLocalDisplayTrack);
4458
-
4459
- meeting.mediaProperties.setLocalShareTrack = sinon.stub().returns(true);
4460
- meeting.stopShare = sinon.stub().resolves(true);
4461
- meeting.mediaProperties.mediaDirection = {};
4462
- meeting.setLocalShareTrack(track);
4463
- assert.calledTwice(TriggerProxy.trigger);
4464
- assert.calledWith(
4465
- TriggerProxy.trigger,
4466
- sinon.match.instanceOf(Meeting),
4467
- {file: 'meeting/index', function: 'setLocalShareTrack'},
4468
- 'media:ready'
4469
- );
4470
- assert.calledOnce(meeting.mediaProperties.setLocalShareTrack);
4471
- assert.equal(meeting.mediaProperties.localStream, undefined);
4472
- assert.isNotNull(listeners[LocalTrackEvents.Ended]);
4473
- listeners[LocalTrackEvents.Ended]();
4474
- assert.calledOnce(meeting.stopShare);
4475
- });
4476
- });
4477
3919
  describe('#setupMediaConnectionListeners', () => {
4478
3920
  let eventListeners;
4479
3921
 
@@ -5381,20 +4823,6 @@ describe('plugin-meetings', () => {
5381
4823
  assert.calledOnce(meeting.mediaProperties.unsetRemoteTracks);
5382
4824
  });
5383
4825
  });
5384
- describe('#unsetLocalVideoTrack', () => {
5385
- it('should unset the local stream and return null', () => {
5386
- meeting.mediaProperties.unsetLocalVideoTrack = sinon.stub().returns(true);
5387
- meeting.unsetLocalVideoTrack();
5388
- assert.calledOnce(meeting.mediaProperties.unsetLocalVideoTrack);
5389
- });
5390
- });
5391
- describe('#unsetLocalShareTrack', () => {
5392
- it('should unset the local share stream and return null', () => {
5393
- meeting.mediaProperties.unsetLocalShareTrack = sinon.stub().returns(true);
5394
- meeting.unsetLocalShareTrack();
5395
- assert.calledOnce(meeting.mediaProperties.unsetLocalShareTrack);
5396
- });
5397
- });
5398
4826
  // TODO: remove
5399
4827
  describe('#setMercuryListener', () => {
5400
4828
  it('should listen to mercury events', () => {
@@ -6061,14 +5489,53 @@ describe('plugin-meetings', () => {
6061
5489
  });
6062
5490
  });
6063
5491
  describe('share scenarios', () => {
5492
+
5493
+ describe('triggerAnnotationInfoEvent', () => {
5494
+ it('check triggerAnnotationInfoEvent event', () => {
5495
+
5496
+ TriggerProxy.trigger.reset();
5497
+ const annotationInfo = {version: '1', policy: 'Approval'};
5498
+ meeting.triggerAnnotationInfoEvent({annotation:annotationInfo},{});
5499
+
5500
+ assert.calledWith(
5501
+ TriggerProxy.trigger,
5502
+ meeting,
5503
+ {
5504
+ file: 'meeting/index',
5505
+ function: 'triggerAnnotationInfoEvent',
5506
+ },
5507
+ 'meeting:updateAnnotationInfo',
5508
+ annotationInfo
5509
+ );
5510
+
5511
+ TriggerProxy.trigger.reset();
5512
+ meeting.triggerAnnotationInfoEvent({annotation:annotationInfo},{annotation:annotationInfo});
5513
+ assert.notCalled(TriggerProxy.trigger);
5514
+
5515
+ TriggerProxy.trigger.reset();
5516
+ const annotationInfoUpdated = {version: '1', policy: 'AnnotationNotAllowed'};
5517
+ meeting.triggerAnnotationInfoEvent({annotation:annotationInfoUpdated},{annotation:annotationInfo});
5518
+ assert.calledWith(
5519
+ TriggerProxy.trigger,
5520
+ meeting,
5521
+ {
5522
+ file: 'meeting/index',
5523
+ function: 'triggerAnnotationInfoEvent',
5524
+ },
5525
+ 'meeting:updateAnnotationInfo',
5526
+ annotationInfoUpdated
5527
+ );
5528
+
5529
+ TriggerProxy.trigger.reset();
5530
+ meeting.triggerAnnotationInfoEvent(null,{annotation:annotationInfoUpdated});
5531
+ assert.notCalled(TriggerProxy.trigger);
5532
+
5533
+ });
5534
+ });
5535
+
6064
5536
  describe('setUpLocusMediaSharesListener', () => {
6065
5537
  beforeEach(() => {
6066
5538
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
6067
- sinon.stub(meeting, 'updateShare').returns(Promise.resolve());
6068
- });
6069
-
6070
- afterEach(() => {
6071
- meeting.updateShare.restore();
6072
5539
  });
6073
5540
 
6074
5541
  const USER_IDS = {
@@ -6756,29 +6223,6 @@ describe('plugin-meetings', () => {
6756
6223
  payloadTestHelper([data1, data2, data3]);
6757
6224
  });
6758
6225
  });
6759
-
6760
- describe('annotation policy', () => {
6761
-
6762
- it('Scenario #1: blank annotation', () => {
6763
- const data1 = generateData(blankPayload, true, true, USER_IDS.ME);
6764
- const data2 = generateData(data1.payload, false, true, USER_IDS.ME);
6765
- const data3 = generateData(data2.payload, true, true, USER_IDS.ME);
6766
- const data4 = generateData(data3.payload, false, true, USER_IDS.ME);
6767
-
6768
- payloadTestHelper([data1, data2, data3, data4]);
6769
- });
6770
-
6771
- it('Scenario #2: annotation', () => {
6772
- const annotationInfo = {version: '1', policy: 'Approval'};
6773
- const data1 = generateData(blankPayload, true, true, USER_IDS.ME, annotationInfo);
6774
- const data2 = generateData(data1.payload, false, true, USER_IDS.ME);
6775
- const data3 = generateData(data2.payload, true, true, USER_IDS.ME);
6776
- const data4 = generateData(data3.payload, false, true, USER_IDS.ME);
6777
-
6778
- payloadTestHelper([data1, data2, data3, data4]);
6779
- });
6780
- });
6781
-
6782
6226
  describe('Desktop A --> Desktop B', () => {
6783
6227
  it('Scenario #1: you share desktop A and then share desktop B', () => {
6784
6228
  const data1 = generateData(blankPayload, true, true, USER_IDS.ME);