@webex/plugin-meetings 3.0.0-beta.145 → 3.0.0-beta.147

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 (68) hide show
  1. package/dist/annotation/annotation.types.js.map +1 -1
  2. package/dist/annotation/constants.js +6 -5
  3. package/dist/annotation/constants.js.map +1 -1
  4. package/dist/breakouts/breakout.js +1 -1
  5. package/dist/breakouts/index.js +1 -1
  6. package/dist/common/errors/webex-errors.js +3 -2
  7. package/dist/common/errors/webex-errors.js.map +1 -1
  8. package/dist/config.js +1 -7
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +7 -15
  11. package/dist/constants.js.map +1 -1
  12. package/dist/index.js +6 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/media/index.js +5 -56
  15. package/dist/media/index.js.map +1 -1
  16. package/dist/media/properties.js +15 -93
  17. package/dist/media/properties.js.map +1 -1
  18. package/dist/meeting/index.js +1106 -1876
  19. package/dist/meeting/index.js.map +1 -1
  20. package/dist/meeting/muteState.js +88 -184
  21. package/dist/meeting/muteState.js.map +1 -1
  22. package/dist/meeting/request.js +2 -2
  23. package/dist/meeting/request.js.map +1 -1
  24. package/dist/meeting/util.js +1 -23
  25. package/dist/meeting/util.js.map +1 -1
  26. package/dist/meetings/index.js +1 -2
  27. package/dist/meetings/index.js.map +1 -1
  28. package/dist/reconnection-manager/index.js +153 -134
  29. package/dist/reconnection-manager/index.js.map +1 -1
  30. package/dist/roap/index.js +8 -7
  31. package/dist/roap/index.js.map +1 -1
  32. package/dist/types/annotation/annotation.types.d.ts +9 -1
  33. package/dist/types/annotation/constants.d.ts +5 -5
  34. package/dist/types/common/errors/webex-errors.d.ts +1 -1
  35. package/dist/types/config.d.ts +0 -6
  36. package/dist/types/constants.d.ts +1 -18
  37. package/dist/types/index.d.ts +1 -1
  38. package/dist/types/media/properties.d.ts +16 -38
  39. package/dist/types/meeting/index.d.ts +92 -352
  40. package/dist/types/meeting/muteState.d.ts +36 -38
  41. package/dist/types/meeting/request.d.ts +2 -1
  42. package/dist/types/meeting/util.d.ts +2 -4
  43. package/package.json +19 -19
  44. package/src/annotation/annotation.types.ts +10 -1
  45. package/src/annotation/constants.ts +5 -5
  46. package/src/common/errors/webex-errors.ts +6 -2
  47. package/src/config.ts +0 -6
  48. package/src/constants.ts +1 -14
  49. package/src/index.ts +1 -0
  50. package/src/media/index.ts +10 -53
  51. package/src/media/properties.ts +32 -92
  52. package/src/meeting/index.ts +532 -1564
  53. package/src/meeting/muteState.ts +87 -178
  54. package/src/meeting/request.ts +4 -3
  55. package/src/meeting/util.ts +3 -24
  56. package/src/meetings/index.ts +0 -1
  57. package/src/reconnection-manager/index.ts +4 -9
  58. package/src/roap/index.ts +13 -14
  59. package/test/integration/spec/converged-space-meetings.js +59 -3
  60. package/test/integration/spec/journey.js +330 -256
  61. package/test/integration/spec/space-meeting.js +75 -3
  62. package/test/unit/spec/meeting/index.js +776 -1344
  63. package/test/unit/spec/meeting/muteState.js +238 -394
  64. package/test/unit/spec/meeting/request.js +4 -4
  65. package/test/unit/spec/meeting/utils.js +2 -9
  66. package/test/unit/spec/multistream/receiveSlot.ts +1 -1
  67. package/test/unit/spec/roap/index.ts +2 -2
  68. 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';
@@ -86,6 +91,7 @@ import {
86
91
  MeetingInfoV2PasswordError,
87
92
  MeetingInfoV2PolicyError,
88
93
  } from '../../../../src/meeting-info/meeting-info-v2';
94
+ import {ANNOTATION_POLICY} from "../../../../src/annotation/constants";
89
95
 
90
96
  const {getBrowserName, getOSVersion} = BrowserDetection();
91
97
 
@@ -150,6 +156,15 @@ describe('plugin-meetings', () => {
150
156
  },
151
157
  });
152
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
+
153
168
  Object.defineProperty(global.window, 'MediaStream', {
154
169
  writable: true,
155
170
  value: MediaStream,
@@ -195,6 +210,7 @@ describe('plugin-meetings', () => {
195
210
  metrics: {},
196
211
  stats: {},
197
212
  experimental: {enableUnifiedMeetings: true},
213
+ degradationPreferences: { maxMacroblocksLimit: 8192 },
198
214
  },
199
215
  metrics: {
200
216
  type: ['behavioral'],
@@ -468,246 +484,7 @@ describe('plugin-meetings', () => {
468
484
  assert.instanceOf(members, Members);
469
485
  });
470
486
  });
471
- describe('#isAudioMuted', () => {
472
- it('should have #isAudioMuted', () => {
473
- assert.exists(meeting.invite);
474
- });
475
- it('should get the audio muted status and return as a boolean', () => {
476
- const muted = meeting.isAudioMuted();
477
-
478
- assert.isNotOk(muted);
479
- });
480
- });
481
- describe('#isAudioSelf', () => {
482
- it('should have #isAudioSelf', () => {
483
- assert.exists(meeting.invite);
484
- });
485
- it('should get the audio self status and return as a boolean', () => {
486
- const self = meeting.isAudioSelf();
487
-
488
- assert.isNotOk(self);
489
- });
490
- });
491
- describe('#isVideoMuted', () => {
492
- it('should have #isVideoMuted', () => {
493
- assert.exists(meeting.isVideoMuted);
494
- });
495
- it('should get the video muted status and return as a boolean', () => {
496
- const muted = meeting.isVideoMuted();
497
-
498
- assert.isNotOk(muted);
499
- });
500
- });
501
- describe('#isVideoSelf', () => {
502
- it('should have #isVideoSelf', () => {
503
- assert.exists(meeting.invite);
504
- });
505
- it('should get the video self status and return as a boolean', () => {
506
- const self = meeting.isVideoSelf();
507
-
508
- assert.isNotOk(self);
509
- });
510
- });
511
- describe('#muteAudio', () => {
512
- it('should have #muteAudio', () => {
513
- assert.exists(meeting.muteAudio);
514
- });
515
- describe('before audio is defined', () => {
516
- it('should reject and return a promise', async () => {
517
- await meeting.muteAudio().catch((err) => {
518
- assert.instanceOf(err, UserNotJoinedError);
519
- });
520
- });
521
-
522
- it('should reject and return a promise', async () => {
523
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
524
- await meeting.muteAudio().catch((err) => {
525
- assert.instanceOf(err, NoMediaEstablishedYetError);
526
- });
527
- });
528
-
529
- it('should reject and return a promise', async () => {
530
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
531
- meeting.mediaId = 'mediaId';
532
- await meeting.muteAudio().catch((err) => {
533
- assert.instanceOf(err, ParameterError);
534
- });
535
- });
536
- });
537
- describe('after audio is defined', () => {
538
- let handleClientRequest;
539
-
540
- beforeEach(() => {
541
- handleClientRequest = sinon.stub().returns(Promise.resolve());
542
- meeting.audio = {handleClientRequest};
543
- });
544
-
545
- it('should return a promise resolution', async () => {
546
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
547
- meeting.mediaId = 'mediaId';
548
-
549
- const audio = meeting.muteAudio();
550
-
551
- assert.exists(audio.then);
552
- await audio;
553
- assert.calledOnce(handleClientRequest);
554
- assert.calledWith(handleClientRequest, meeting, true);
555
- });
556
- });
557
- });
558
- describe('#unmuteAudio', () => {
559
- it('should have #unmuteAudio', () => {
560
- assert.exists(meeting.unmuteAudio);
561
- });
562
- describe('before audio is defined', () => {
563
- it('should reject when user not joined', async () => {
564
- await meeting.unmuteAudio().catch((err) => {
565
- assert.instanceOf(err, UserNotJoinedError);
566
- });
567
- });
568
-
569
- it('should reject when no media is established yet ', async () => {
570
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
571
- await meeting.unmuteAudio().catch((err) => {
572
- assert.instanceOf(err, NoMediaEstablishedYetError);
573
- });
574
- });
575
-
576
- it('should reject when audio is not there or established', async () => {
577
- meeting.mediaId = 'mediaId';
578
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
579
- await meeting.unmuteAudio().catch((err) => {
580
- assert.instanceOf(err, ParameterError);
581
- });
582
- });
583
- });
584
- describe('after audio is defined', () => {
585
- let handleClientRequest;
586
-
587
- beforeEach(() => {
588
- handleClientRequest = sinon.stub().returns(Promise.resolve());
589
- meeting.mediaId = 'mediaId';
590
- meeting.audio = {handleClientRequest};
591
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
592
- });
593
-
594
- it('should return a promise resolution', async () => {
595
- meeting.audio = {handleClientRequest};
596
-
597
- const audio = meeting.unmuteAudio();
598
-
599
- assert.exists(audio.then);
600
- await audio;
601
- assert.calledOnce(handleClientRequest);
602
- assert.calledWith(handleClientRequest, meeting, false);
603
- });
604
- });
605
- });
606
- describe('BNR', () => {
607
- const fakeMediaTrack = () => ({
608
- id: Date.now().toString(),
609
- stop: () => {},
610
- readyState: 'live',
611
- enabled: true,
612
- getSettings: () => ({
613
- sampleRate: 48000,
614
- }),
615
- });
616
-
617
- beforeEach(() => {
618
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve());
619
- sinon.replace(meeting, 'addMedia', () => {
620
- sinon.stub(meeting.mediaProperties, 'audioTrack').value(fakeMediaTrack());
621
- sinon.stub(meeting.mediaProperties, 'mediaDirection').value({
622
- receiveAudio: true,
623
- });
624
- });
625
- });
626
- });
627
- describe('#muteVideo', () => {
628
- it('should have #muteVideo', () => {
629
- assert.exists(meeting.muteVideo);
630
- });
631
- describe('before video is defined', () => {
632
- it('should reject when user not joined', async () => {
633
- await meeting.muteVideo().catch((err) => {
634
- assert.instanceOf(err, UserNotJoinedError);
635
- });
636
- });
637
-
638
- it('should reject when no media is established', async () => {
639
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
640
- await meeting.muteVideo().catch((err) => {
641
- assert.instanceOf(err, NoMediaEstablishedYetError);
642
- });
643
- });
644
-
645
- it('should reject when no video added or established', async () => {
646
- meeting.mediaId = 'mediaId';
647
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
648
- await meeting.muteVideo().catch((err) => {
649
- assert.instanceOf(err, ParameterError);
650
- });
651
- });
652
- });
653
- describe('after video is defined', () => {
654
- it('should return a promise resolution', async () => {
655
- const handleClientRequest = sinon.stub().returns(Promise.resolve());
656
-
657
- meeting.mediaId = 'mediaId';
658
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
659
- meeting.video = {handleClientRequest};
660
- const video = meeting.muteVideo();
661
-
662
- assert.exists(video.then);
663
- await video;
664
- assert.calledOnce(handleClientRequest);
665
- assert.calledWith(handleClientRequest, meeting, true);
666
- });
667
- });
668
- });
669
- describe('#unmuteVideo', () => {
670
- it('should have #unmuteVideo', () => {
671
- assert.exists(meeting.unmuteVideo);
672
- });
673
- describe('before video is defined', () => {
674
- it('should reject no user joined', async () => {
675
- await meeting.unmuteVideo().catch((err) => {
676
- assert.instanceOf(err, Error);
677
- });
678
- });
679
487
 
680
- it('should reject no media established', async () => {
681
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
682
- await meeting.unmuteVideo().catch((err) => {
683
- assert.instanceOf(err, Error);
684
- });
685
- });
686
-
687
- it('should reject when no video added or established', async () => {
688
- meeting.mediaId = 'mediaId';
689
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
690
- await meeting.unmuteVideo().catch((err) => {
691
- assert.instanceOf(err, Error);
692
- });
693
- });
694
- });
695
- describe('after video is defined', () => {
696
- it('should return a promise resolution', async () => {
697
- const handleClientRequest = sinon.stub().returns(Promise.resolve());
698
-
699
- meeting.mediaId = 'mediaId';
700
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
701
- meeting.video = {handleClientRequest};
702
- const video = meeting.unmuteVideo();
703
-
704
- assert.exists(video.then);
705
- await video;
706
- assert.calledOnce(handleClientRequest);
707
- assert.calledWith(handleClientRequest, meeting, false);
708
- });
709
- });
710
- });
711
488
  describe('#joinWithMedia', () => {
712
489
  it('should have #joinWithMedia', () => {
713
490
  assert.exists(meeting.joinWithMedia);
@@ -715,125 +492,21 @@ describe('plugin-meetings', () => {
715
492
  describe('resolution', () => {
716
493
  it('should success and return a promise', async () => {
717
494
  meeting.join = sinon.stub().returns(Promise.resolve(test1));
718
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([test2, test3]));
719
495
  meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
720
- await meeting.joinWithMedia({});
496
+ const result = await meeting.joinWithMedia({});
721
497
  assert.calledOnce(meeting.join);
722
- assert.calledOnce(meeting.getMediaStreams);
498
+ assert.calledOnce(meeting.addMedia);
499
+ assert.deepEqual(result, {join: test1, media: test4});
723
500
  });
724
501
  });
725
502
  describe('rejection', () => {
726
503
  it('should error out and return a promise', async () => {
727
504
  meeting.join = sinon.stub().returns(Promise.reject());
728
- meeting.getMediaStreams = sinon.stub().returns(true);
729
505
  assert.isRejected(meeting.joinWithMedia({}));
730
506
  });
731
507
  });
732
508
  });
733
- describe('#getMediaStreams', () => {
734
- beforeEach(() => {
735
- sinon
736
- .stub(Media, 'getSupportedDevice')
737
- .callsFake((options) =>
738
- Promise.resolve({sendAudio: options.sendAudio, sendVideo: options.sendVideo})
739
- );
740
- sinon.stub(Media, 'getUserMedia').returns(Promise.resolve(['stream1', 'stream2']));
741
- });
742
- afterEach(() => {
743
- sinon.restore();
744
- });
745
- it('should have #getMediaStreams', () => {
746
- assert.exists(meeting.getMediaStreams);
747
- });
748
- it('should proxy Media getUserMedia, and return a promise', async () => {
749
- await meeting.getMediaStreams({sendAudio: true, sendVideo: true});
750
-
751
- assert.calledOnce(Media.getUserMedia);
752
- });
753
-
754
- it('uses the preferred video device if set', async () => {
755
- const videoDevice = 'video1';
756
- const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
757
- const audioVideoSettings = {};
758
-
759
- sinon.stub(meeting.mediaProperties, 'videoDeviceId').value(videoDevice);
760
- sinon.stub(meeting.mediaProperties, 'localQualityLevel').value('480p');
761
- await meeting.getMediaStreams(mediaDirection, audioVideoSettings);
762
-
763
- assert.calledWith(
764
- Media.getUserMedia,
765
- {
766
- ...mediaDirection,
767
- isSharing: false,
768
- },
769
- {
770
- video: {
771
- width: {max: 640, ideal: 640},
772
- height: {max: 480, ideal: 480},
773
- deviceId: videoDevice,
774
- },
775
- }
776
- );
777
- });
778
- it('will set a new preferred video input device if passed in', async () => {
779
- // if audioVideo settings parameter specifies a new video device it
780
- // will store that device as the preferred video device.
781
- // Which is the case with meeting.updateVideo()
782
- const oldVideoDevice = 'video1';
783
- const newVideoDevice = 'video2';
784
- const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
785
- const audioVideoSettings = {video: {deviceId: newVideoDevice}};
786
-
787
- sinon.stub(meeting.mediaProperties, 'videoDeviceId').value(oldVideoDevice);
788
- sinon.stub(meeting.mediaProperties, 'setVideoDeviceId');
789
509
 
790
- await meeting.getMediaStreams(mediaDirection, audioVideoSettings);
791
-
792
- assert.calledWith(meeting.mediaProperties.setVideoDeviceId, newVideoDevice);
793
- });
794
-
795
- it('uses the passed custom video resolution', async () => {
796
- const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
797
- const customAudioVideoSettings = {
798
- video: {
799
- width: {
800
- max: 400,
801
- ideal: 400,
802
- },
803
- height: {
804
- max: 200,
805
- ideal: 200,
806
- },
807
- frameRate: {
808
- ideal: 15,
809
- max: 30,
810
- },
811
- facingMode: {
812
- ideal: 'user',
813
- },
814
- },
815
- };
816
-
817
- sinon.stub(meeting.mediaProperties, 'localQualityLevel').value('200p');
818
- await meeting.getMediaStreams(mediaDirection, customAudioVideoSettings);
819
-
820
- assert.calledWith(
821
- Media.getUserMedia,
822
- {
823
- ...mediaDirection,
824
- isSharing: false,
825
- },
826
- customAudioVideoSettings
827
- );
828
- });
829
- it('should not access camera if sendVideo is false ', async () => {
830
- await meeting.getMediaStreams({sendAudio: true, sendVideo: false});
831
-
832
- assert.calledOnce(Media.getUserMedia);
833
-
834
- assert.equal(Media.getUserMedia.args[0][0].sendVideo, false);
835
- });
836
- });
837
510
  describe('#isTranscriptionSupported', () => {
838
511
  it('should return false if the feature is not supported for the meeting', () => {
839
512
  meeting.locusInfo.controls = {transcribe: {transcribing: false}};
@@ -1158,7 +831,7 @@ describe('plugin-meetings', () => {
1158
831
  meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
1159
832
  meeting.audio = muteStateStub;
1160
833
  meeting.video = muteStateStub;
1161
- Media.createMediaConnection = sinon.stub().returns(fakeMediaConnection);
834
+ sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
1162
835
  meeting.setMercuryListener = sinon.stub().returns(true);
1163
836
  meeting.setupMediaConnectionListeners = sinon.stub();
1164
837
  meeting.setMercuryListener = sinon.stub();
@@ -1239,6 +912,7 @@ describe('plugin-meetings', () => {
1239
912
  locus_id: meeting.locusUrl.split('/').pop(),
1240
913
  reason: err.message,
1241
914
  stack: err.stack,
915
+ code: err.code,
1242
916
  turnDiscoverySkippedReason: 'config',
1243
917
  turnServerUsed: false,
1244
918
  isMultistream: false,
@@ -1702,649 +1376,810 @@ describe('plugin-meetings', () => {
1702
1376
  assert.calledOnce(fakeMediaConnection.initiateOffer);
1703
1377
  });
1704
1378
  });
1705
- describe('#acknowledge', () => {
1706
- it('should have #acknowledge', () => {
1707
- assert.exists(meeting.acknowledge);
1708
- });
1709
- beforeEach(() => {
1710
- meeting.meetingRequest.acknowledgeMeeting = sinon.stub().returns(Promise.resolve());
1711
- });
1712
- it('should acknowledge incoming and return a promise', async () => {
1713
- const ack = meeting.acknowledge('INCOMING', false);
1714
1379
 
1715
- assert.exists(ack.then);
1716
- await ack;
1717
- assert.calledOnce(meeting.meetingRequest.acknowledgeMeeting);
1718
- });
1719
- it('should acknowledge a non incoming and return a promise', async () => {
1720
- 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
+ };
1721
1392
 
1722
- assert.exists(ack.then);
1723
- await ack;
1724
- assert.notCalled(meeting.meetingRequest.acknowledgeMeeting);
1725
- });
1726
- });
1727
- describe('#decline', () => {
1728
- it('should have #decline', () => {
1729
- assert.exists(meeting.decline);
1730
- });
1731
- beforeEach(() => {
1732
- meeting.meetingRequest.declineMeeting = sinon.stub().returns(Promise.resolve());
1733
- meeting.meetingFiniteStateMachine.ring();
1734
- });
1735
- it('should decline the meeting and trigger meeting destroy for 1:1', async () => {
1736
- await meeting.decline();
1737
- assert.calledOnce(meeting.meetingRequest.declineMeeting);
1738
- });
1739
- });
1740
- describe('#leave', () => {
1741
- 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
1742
1399
 
1743
- it('should have #leave', () => {
1744
- assert.exists(meeting.leave);
1745
- });
1400
+ const roapOfferMessage = {messageType: 'OFFER', sdp: 'sdp', seq: '1', tieBreaker: '123'};
1746
1401
 
1747
- it('should reject if meeting is already inactive', async () => {
1748
- await meeting.leave().catch((err) => {
1749
- assert.instanceOf(err, MeetingNotActiveError);
1750
- });
1751
- });
1402
+ let expectedMediaConnectionConfig;
1403
+ let expectedDebugId;
1752
1404
 
1753
- it('should reject if meeting is already left', async () => {
1754
- meeting.meetingState = 'ACTIVE';
1755
- await meeting.leave().catch((err) => {
1756
- assert.instanceOf(err, UserNotJoinedError);
1757
- });
1758
- });
1405
+ let clock;
1759
1406
 
1760
1407
  beforeEach(() => {
1761
- sandbox = sinon.createSandbox();
1762
- meeting.meetingFiniteStateMachine.ring();
1763
- meeting.meetingFiniteStateMachine.join();
1764
- meeting.meetingRequest.leaveMeeting = sinon
1765
- .stub()
1766
- .returns(Promise.resolve({body: 'test'}));
1767
- meeting.locusInfo.onFullLocus = sinon.stub().returns(true);
1768
- // the 3 need to be promises because we do closeLocalStream.then(closeLocalShare.then) etc in the src code
1769
- meeting.closeLocalStream = sinon.stub().returns(Promise.resolve());
1770
- meeting.closeLocalShare = sinon.stub().returns(Promise.resolve());
1771
- meeting.closeRemoteStream = sinon.stub().returns(Promise.resolve());
1772
- sandbox.stub(meeting, 'closeRemoteTracks').returns(Promise.resolve());
1773
- meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
1774
- meeting.unsetLocalVideoTrack = sinon.stub().returns(true);
1775
- meeting.unsetLocalShareTrack = sinon.stub().returns(true);
1776
- meeting.unsetRemoteTracks = sinon.stub();
1777
- meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
1778
- meeting.unsetRemoteStream = sinon.stub().returns(true);
1779
- meeting.unsetPeerConnections = sinon.stub().returns(true);
1780
- meeting.logger.error = sinon.stub().returns(true);
1781
- meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
1408
+ clock = sinon.useFakeTimers();
1782
1409
 
1783
- // A meeting needs to be joined to leave
1410
+ meeting.deviceUrl = 'deviceUrl';
1411
+ meeting.config.deviceType = 'web';
1412
+ meeting.isMultistream = isMultistream;
1784
1413
  meeting.meetingState = 'ACTIVE';
1785
- 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: {}}}});
1786
1494
  });
1495
+
1787
1496
  afterEach(() => {
1788
- sandbox.restore();
1789
- sandbox = null;
1497
+ clock.restore();
1790
1498
  });
1791
- it('should leave the meeting and return promise', async () => {
1792
- const leave = meeting.leave();
1793
1499
 
1794
- assert.exists(leave.then);
1795
- await leave;
1796
- assert.calledOnce(meeting.meetingRequest.leaveMeeting);
1797
- assert.calledOnce(meeting.closeLocalStream);
1798
- assert.calledOnce(meeting.closeLocalShare);
1799
- assert.calledOnce(meeting.closeRemoteTracks);
1800
- assert.calledOnce(meeting.closePeerConnections);
1801
- assert.calledOnce(meeting.unsetLocalVideoTrack);
1802
- assert.calledOnce(meeting.unsetLocalShareTrack);
1803
- assert.calledOnce(meeting.unsetRemoteTracks);
1804
- assert.calledOnce(meeting.unsetPeerConnections);
1805
- });
1806
- describe('after audio/video is defined', () => {
1807
- 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
+ };
1808
1512
 
1809
- beforeEach(() => {
1810
- handleClientRequest = sinon.stub().returns(Promise.resolve(true));
1513
+ const getRoapListener = () => {
1514
+ const roapMediaConnectionToCheck = isMultistream ? fakeMultistreamRoapMediaConnection : fakeRoapMediaConnection;
1811
1515
 
1812
- meeting.audio = {handleClientRequest};
1813
- meeting.video = {handleClientRequest};
1814
- });
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
+ }
1815
1523
 
1816
- it('should delete audio and video state machines when leaving the meeting', async () => {
1817
- const leave = meeting.leave();
1524
+ // simulates a Roap offer being generated by the RoapMediaConnection
1525
+ const simulateRoapOffer = async () => {
1526
+ const roapListener = getRoapListener();
1818
1527
 
1819
- assert.exists(leave.then);
1820
- await leave;
1528
+ await roapListener({roapMessage: roapOfferMessage});
1529
+ await stableState();
1530
+ }
1821
1531
 
1822
- assert.isNull(meeting.audio);
1823
- 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,
1824
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);
1825
1634
  });
1826
- it('should leave the meeting without leaving resource', async () => {
1827
- const leave = meeting.leave({resourceId: null});
1828
1635
 
1829
- assert.exists(leave.then);
1830
- await leave;
1831
- assert.calledWith(meeting.meetingRequest.leaveMeeting, {
1832
- locusUrl: meeting.locusUrl,
1833
- correlationId: meeting.correlationId,
1834
- selfId: meeting.selfId,
1835
- resourceId: null,
1836
- 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
1837
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);
1838
1662
  });
1839
- it('should leave the meeting on the resource', async () => {
1840
- const leave = meeting.leave();
1841
1663
 
1842
- assert.exists(leave.then);
1843
- await leave;
1844
- assert.calledWith(meeting.meetingRequest.leaveMeeting, {
1845
- locusUrl: meeting.locusUrl,
1846
- correlationId: meeting.correlationId,
1847
- selfId: meeting.selfId,
1848
- resourceId: meeting.resourceId,
1849
- 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,
1850
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);
1851
1692
  });
1852
- it('should leave the meeting on the resource with reason', async () => {
1853
- const leave = meeting.leave({resourceId: meeting.resourceId, reason: MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST});
1854
1693
 
1855
- assert.exists(leave.then);
1856
- await leave;
1857
- assert.calledWith(meeting.meetingRequest.leaveMeeting, {
1858
- locusUrl: meeting.locusUrl,
1859
- correlationId: meeting.correlationId,
1860
- selfId: meeting.selfId,
1861
- resourceId: meeting.resourceId,
1862
- deviceUrl: meeting.deviceUrl,
1863
- 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
1864
1713
  });
1865
- });
1866
- });
1867
- describe('#requestScreenShareFloor', () => {
1868
- it('should have #requestScreenShareFloor', () => {
1869
- assert.exists(meeting.requestScreenShareFloor);
1870
- });
1871
- beforeEach(() => {
1872
- meeting.locusInfo.mediaShares = [{name: 'content', url: url1}];
1873
- meeting.locusInfo.self = {url: url1};
1874
- meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
1875
- });
1876
- it('should send the share', async () => {
1877
- const share = meeting.requestScreenShareFloor();
1878
1714
 
1879
- assert.exists(share.then);
1880
- await share;
1881
- 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);
1882
1720
  });
1883
- });
1884
1721
 
1885
- describe('#shareScreen', () => {
1886
- 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();
1887
1725
 
1888
- beforeEach(() => {
1889
- _mediaDirection = meeting.mediaProperties.mediaDirection || {};
1890
- sinon
1891
- .stub(meeting.mediaProperties, 'mediaDirection')
1892
- .value({sendAudio: true, sendVideo: true, sendShare: false});
1893
- });
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
+ });
1894
1742
 
1895
- afterEach(() => {
1896
- meeting.mediaProperties.mediaDirection = _mediaDirection;
1897
- });
1743
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1744
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1898
1745
 
1899
- it('should have #shareScreen', () => {
1900
- assert.exists(meeting.shareScreen);
1746
+ // and no other local mute requests were sent to Locus
1747
+ assert.calledOnce(locusMediaRequestStub);
1901
1748
  });
1902
1749
 
1903
- describe('basic functionality', () => {
1904
- beforeEach(() => {
1905
- sinon.stub(Media, 'getDisplayMedia').returns(Promise.resolve());
1906
- sinon.stub(meeting, 'updateShare').returns(Promise.resolve());
1907
- });
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
+ });
1908
1786
 
1909
- afterEach(() => {
1910
- Media.getDisplayMedia.restore();
1911
- meeting.updateShare.restore();
1912
- });
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
+ }
1913
1824
 
1914
- it('should call get display media', async () => {
1915
- await meeting.shareScreen();
1825
+ // and no other roap messages or local mute requests were sent
1826
+ assert.notCalled(locusMediaRequestStub);
1827
+ });
1916
1828
 
1917
- assert.calledOnce(Media.getDisplayMedia);
1918
- });
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();
1919
1834
 
1920
- it('should call updateShare', async () => {
1921
- await meeting.shareScreen();
1835
+ resetHistory();
1922
1836
 
1923
- assert.calledOnce(meeting.updateShare);
1924
- });
1837
+ await meeting.unpublishTracks([fakeMicrophoneTrack]);
1838
+ await stableState();
1925
1839
 
1926
- it('properly assigns default values', async () => {
1927
- 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
+ }
1928
1854
 
1929
- assert.calledWith(Media.getDisplayMedia, {
1930
- sendShare: true,
1931
- sendAudio: false,
1932
- 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
+ });
1933
1863
  });
1934
- });
1935
1864
  });
1936
1865
 
1937
- describe('stops share immediately', () => {
1938
- let sandbox;
1866
+ describe('updateMedia()', () => {
1939
1867
 
1940
- beforeEach(() => {
1941
- sandbox = sinon.createSandbox();
1942
- });
1868
+ const addMedia = async (enableMedia, track) => {
1869
+ await meeting.addMedia({audioEnabled: enableMedia, localTracks: {microphone: track}});
1870
+ await simulateRoapOffer();
1943
1871
 
1944
- afterEach(() => {
1945
- sandbox.restore();
1946
- 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);
1947
1907
  });
1948
1908
 
1949
- it('Can bypass canUpdateMedia() check', () => {
1950
- const sendShare = true;
1951
- const receiveShare = false;
1952
- const stream = 'stream';
1909
+ it('updateMedia() enables media when nothing is published', async () => {
1910
+ await addMedia(false);
1953
1911
 
1954
- sandbox.stub(MeetingUtil, 'getTrack').returns({videoTrack: true});
1955
- MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve(true));
1956
- sandbox.stub(meeting, 'canUpdateMedia').returns(true);
1957
- sandbox.stub(meeting, 'setLocalShareTrack');
1912
+ await meeting.updateMedia({audioEnabled: true});
1958
1913
 
1959
- meeting.updateShare({
1960
- sendShare,
1961
- receiveShare,
1962
- stream,
1963
- skipSignalingCheck: true,
1964
- });
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();
1965
1919
 
1966
- 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);
1967
1925
  });
1968
1926
 
1969
- it('skips canUpdateMedia() check on contentTracks.onended', () => {
1970
- const {mediaProperties} = meeting;
1971
- const fakeTrack = {
1972
- getSettings: sinon.stub().returns({}),
1973
- };
1927
+ it('updateMedia() disables media when track is published', async () => {
1928
+ await addMedia(true, fakeMicrophoneTrack);
1974
1929
 
1975
- const listeners = {};
1930
+ await meeting.updateMedia({audioEnabled: false});
1931
+ await stableState();
1976
1932
 
1977
- const fakeLocalDisplayTrack = {
1978
- on: sinon.stub().callsFake((event, listener) => {
1979
- listeners[event] = listener;
1980
- }),
1981
- };
1982
- sinon.stub(InternalMediaCoreModule, 'LocalDisplayTrack').returns(fakeLocalDisplayTrack);
1933
+ // the roap media connection should be updated
1934
+ checkAudioEnabled(webrtcAudioTrack, 'inactive');
1983
1935
 
1984
- sandbox.stub(mediaProperties, 'setLocalShareTrack');
1985
- sandbox.stub(mediaProperties, 'setMediaSettings');
1986
- sandbox.stub(meeting, 'stopShare').resolves(true);
1987
- meeting.setLocalShareTrack(fakeTrack);
1936
+ checkLocalMuteSentToLocus({audioMuted: true, videoMuted: true});
1988
1937
 
1989
- assert.calledOnce(fakeLocalDisplayTrack.on);
1990
- assert.calledWith(fakeLocalDisplayTrack.on, LocalTrackEvents.Ended, sinon.match.any);
1991
- assert.isNotNull(listeners[LocalTrackEvents.Ended]);
1938
+ locusMediaRequestStub.resetHistory();
1992
1939
 
1993
- listeners[LocalTrackEvents.Ended]();
1940
+ // and that would trigger a new offer so we simulate it happening
1941
+ await simulateRoapOffer();
1994
1942
 
1995
- assert.calledWith(meeting.stopShare, {skipSignalingCheck: true});
1943
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1944
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1945
+
1946
+ // and no other local mute requests were sent to Locus
1947
+ assert.calledOnce(locusMediaRequestStub);
1996
1948
  });
1997
1949
 
1998
- it('stopShare accepts and passes along optional parameters', () => {
1999
- const args = {
2000
- abc: 123,
2001
- receiveShare: false,
2002
- sendShare: false,
2003
- };
1950
+ it('updateMedia() enables media when track is published', async () => {
1951
+ await addMedia(false, fakeMicrophoneTrack);
2004
1952
 
2005
- sandbox.stub(meeting, 'updateShare').returns(Promise.resolve());
2006
- sandbox.stub(meeting.mediaProperties, 'mediaDirection').value(false);
1953
+ await meeting.updateMedia({audioEnabled: true});
1954
+ await stableState();
2007
1955
 
2008
- meeting.stopShare(args);
1956
+ // the roap media connection should be updated
1957
+ checkAudioEnabled(webrtcAudioTrack, 'sendrecv');
2009
1958
 
2010
- assert.calledWith(meeting.updateShare, args);
1959
+ checkLocalMuteSentToLocus({audioMuted: false, videoMuted: true});
1960
+
1961
+ locusMediaRequestStub.resetHistory();
1962
+
1963
+ // and that would trigger a new offer so we simulate it happening
1964
+ await simulateRoapOffer();
1965
+
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);
2011
1971
  });
2012
1972
  });
2013
1973
 
2014
- describe('out-of-sync sharing', () => {
2015
- 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;
2016
1981
 
2017
- beforeEach(() => {
2018
- sandbox = sinon.createSandbox();
2019
- });
1982
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}});
1983
+ await stableState();
2020
1984
 
2021
- afterEach(() => {
2022
- sandbox.restore();
2023
- sandbox = null;
2024
- });
1985
+ resetHistory();
2025
1986
 
2026
- it('handleShareTrackEnded triggers an event', () => {
2027
- 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}});
2028
1991
 
2029
- sandbox.stub(meeting, 'stopShare').resolves(true);
1992
+ await stableState();
2030
1993
 
2031
- meeting.handleShareTrackEnded();
1994
+ // nothing should happen
1995
+ assert.notCalled(locusMediaRequestStub);
1996
+ assert.notCalled(fakeRoapMediaConnection.update);
2032
1997
 
2033
- assert.calledWith(
2034
- TriggerProxy.trigger,
2035
- sinon.match.instanceOf(Meeting),
2036
- {
2037
- file: 'meeting/index',
2038
- function: 'handleShareTrackEnded',
2039
- },
2040
- EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
2041
- {
2042
- type: EVENT_TYPES.LOCAL_SHARE,
2043
- }
2044
- );
2045
- });
2046
- });
2047
- });
1998
+ // now simulate roap offer
1999
+ await simulateRoapOffer();
2048
2000
 
2049
- describe('#shareScreen resolutions', () => {
2050
- let _getDisplayMedia = null;
2051
- const config = DefaultSDKConfig.meetings;
2052
- const {resolution} = config;
2053
- const shareOptions = {
2054
- sendShare: true,
2055
- sendAudio: false,
2056
- };
2057
- const fireFoxOptions = {
2058
- audio: false,
2059
- video: {
2060
- audio: shareOptions.sendAudio,
2061
- video: shareOptions.sendShare,
2062
- },
2063
- };
2001
+ // it should be sent with the right mute status
2002
+ checkSdpOfferSent({audioMuted: mute, videoMuted: true});
2064
2003
 
2065
- const MediaStream = {
2066
- getVideoTracks: () => [
2067
- {
2068
- applyConstraints: () => {},
2069
- },
2070
- ],
2071
- };
2004
+ // nothing else should happen
2005
+ assert.calledOnce(locusMediaRequestStub);
2006
+ assert.notCalled(fakeRoapMediaConnection.update);
2007
+ })
2008
+ );
2009
+ }));
2072
2010
 
2073
- const MediaConstraint = {
2074
- cursor: 'always',
2075
- aspectRatio: config.aspectRatio,
2076
- frameRate: config.screenFrameRate,
2077
- width: null,
2078
- height: null,
2079
- };
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);
2080
2020
 
2081
- const browserConditionalValue = (value) => {
2082
- const key = getBrowserName().toLowerCase();
2083
- 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);
2084
2027
 
2085
- return value[key] || value[defaultKey];
2086
- };
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;
2087
2048
 
2088
- before(() => {
2089
- meeting.updateShare = sinon.stub().returns(Promise.resolve());
2049
+ it('should have #leave', () => {
2050
+ assert.exists(meeting.leave);
2051
+ });
2090
2052
 
2091
- if (!global.navigator) {
2092
- global.navigator = {
2093
- mediaDevices: {
2094
- getDisplayMedia: null,
2095
- },
2096
- };
2097
- }
2098
- _getDisplayMedia = global.navigator.mediaDevices.getDisplayMedia;
2099
- Object.defineProperty(global.navigator.mediaDevices, 'getDisplayMedia', {
2100
- value: sinon.stub().returns(Promise.resolve(MediaStream)),
2101
- writable: true,
2053
+ it('should reject if meeting is already inactive', async () => {
2054
+ await meeting.leave().catch((err) => {
2055
+ assert.instanceOf(err, MeetingNotActiveError);
2102
2056
  });
2103
2057
  });
2104
2058
 
2105
- after(() => {
2106
- // clean up for browser
2107
- Object.defineProperty(global.navigator.mediaDevices, 'getDisplayMedia', {
2108
- value: _getDisplayMedia,
2109
- 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);
2110
2063
  });
2111
2064
  });
2112
2065
 
2113
- // eslint-disable-next-line max-len
2114
- it('will use shareConstraints if defined in provided options', () => {
2115
- const SHARE_WIDTH = 640;
2116
- const SHARE_HEIGHT = 480;
2117
- const shareConstraints = {
2118
- highFrameRate: 2,
2119
- maxWidth: SHARE_WIDTH,
2120
- maxHeight: SHARE_HEIGHT,
2121
- idealWidth: SHARE_WIDTH,
2122
- idealHeight: SHARE_HEIGHT,
2123
- };
2124
-
2125
- // If sharePreferences.shareConstraints is defined it ignores
2126
- // default SDK config settings
2127
- getDisplayMedia(
2128
- {
2129
- ...shareOptions,
2130
- sharePreferences: {shareConstraints},
2131
- },
2132
- config
2133
- );
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());
2134
2083
 
2135
- // eslint-disable-next-line no-undef
2136
- assert.calledWith(
2137
- navigator.mediaDevices.getDisplayMedia,
2138
- browserConditionalValue({
2139
- default: {
2140
- video: {...shareConstraints},
2141
- },
2142
- // Firefox is being handled differently
2143
- firefox: fireFoxOptions,
2144
- })
2145
- );
2084
+ // A meeting needs to be joined to leave
2085
+ meeting.meetingState = 'ACTIVE';
2086
+ meeting.state = 'JOINED';
2146
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();
2147
2094
 
2148
- // eslint-disable-next-line max-len
2149
- it('will use default resolution if shareConstraints is undefined and highFrameRate is defined', () => {
2150
- // If highFrameRate is defined it ignores default SDK config settings
2151
- getDisplayMedia(
2152
- {
2153
- ...shareOptions,
2154
- sharePreferences: {
2155
- highFrameRate: true,
2156
- },
2157
- },
2158
- config
2159
- );
2160
-
2161
- // eslint-disable-next-line no-undef
2162
- assert.calledWith(
2163
- navigator.mediaDevices.getDisplayMedia,
2164
- browserConditionalValue({
2165
- default: {
2166
- video: {
2167
- ...MediaConstraint,
2168
- frameRate: config.videoShareFrameRate,
2169
- width: resolution.idealWidth,
2170
- height: resolution.idealHeight,
2171
- maxWidth: resolution.maxWidth,
2172
- maxHeight: resolution.maxHeight,
2173
- idealWidth: resolution.idealWidth,
2174
- idealHeight: resolution.idealHeight,
2175
- },
2176
- },
2177
- firefox: fireFoxOptions,
2178
- })
2179
- );
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);
2180
2103
  });
2104
+ describe('after audio/video is defined', () => {
2105
+ let handleClientRequest;
2181
2106
 
2182
- // eslint-disable-next-line max-len
2183
- it('will use default screenResolution if shareConstraints, highFrameRate, and SDK defaults is undefined', () => {
2184
- getDisplayMedia(shareOptions);
2185
- const {screenResolution} = config;
2107
+ beforeEach(() => {
2108
+ handleClientRequest = sinon.stub().returns(Promise.resolve(true));
2186
2109
 
2187
- // eslint-disable-next-line no-undef
2188
- assert.calledWith(
2189
- navigator.mediaDevices.getDisplayMedia,
2190
- browserConditionalValue({
2191
- default: {
2192
- video: {
2193
- ...MediaConstraint,
2194
- width: screenResolution.idealWidth,
2195
- height: screenResolution.idealHeight,
2196
- },
2197
- },
2198
- firefox: fireFoxOptions,
2199
- })
2200
- );
2201
- });
2110
+ meeting.audio = {handleClientRequest};
2111
+ meeting.video = {handleClientRequest};
2112
+ });
2202
2113
 
2203
- // Test screenResolution
2204
- // eslint-disable-next-line max-len
2205
- it('will use SDK config screenResolution if set, with shareConstraints and highFrameRate being undefined', () => {
2206
- const SHARE_WIDTH = 800;
2207
- const SHARE_HEIGHT = 600;
2208
- const customConfig = {
2209
- screenResolution: {
2210
- maxWidth: SHARE_WIDTH,
2211
- maxHeight: SHARE_HEIGHT,
2212
- idealWidth: SHARE_WIDTH,
2213
- idealHeight: SHARE_HEIGHT,
2214
- },
2215
- };
2114
+ it('should delete audio and video state machines when leaving the meeting', async () => {
2115
+ const leave = meeting.leave();
2216
2116
 
2217
- getDisplayMedia(shareOptions, customConfig);
2117
+ assert.exists(leave.then);
2118
+ await leave;
2218
2119
 
2219
- // eslint-disable-next-line no-undef
2220
- assert.calledWith(
2221
- navigator.mediaDevices.getDisplayMedia,
2222
- browserConditionalValue({
2223
- default: {
2224
- video: {
2225
- ...MediaConstraint,
2226
- width: SHARE_WIDTH,
2227
- height: SHARE_HEIGHT,
2228
- maxWidth: SHARE_WIDTH,
2229
- maxHeight: SHARE_HEIGHT,
2230
- idealWidth: SHARE_WIDTH,
2231
- idealHeight: SHARE_HEIGHT,
2232
- },
2233
- },
2234
- firefox: fireFoxOptions,
2235
- })
2236
- );
2120
+ assert.isNull(meeting.audio);
2121
+ assert.isNull(meeting.video);
2122
+ });
2237
2123
  });
2124
+ it('should leave the meeting without leaving resource', async () => {
2125
+ const leave = meeting.leave({resourceId: null});
2238
2126
 
2239
- // Test screenFrameRate
2240
- it('will use SDK config screenFrameRate if set, with shareConstraints and highFrameRate being undefined', () => {
2241
- const SHARE_WIDTH = 800;
2242
- const SHARE_HEIGHT = 600;
2243
- const customConfig = {
2244
- screenFrameRate: 999,
2245
- screenResolution: {
2246
- maxWidth: SHARE_WIDTH,
2247
- maxHeight: SHARE_HEIGHT,
2248
- idealWidth: SHARE_WIDTH,
2249
- idealHeight: SHARE_HEIGHT,
2250
- },
2251
- };
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();
2252
2139
 
2253
- 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});
2254
2152
 
2255
- // eslint-disable-next-line no-undef
2256
- assert.calledWith(
2257
- navigator.mediaDevices.getDisplayMedia,
2258
- browserConditionalValue({
2259
- default: {
2260
- video: {
2261
- ...MediaConstraint,
2262
- frameRate: customConfig.screenFrameRate,
2263
- width: SHARE_WIDTH,
2264
- height: SHARE_HEIGHT,
2265
- maxWidth: SHARE_WIDTH,
2266
- maxHeight: SHARE_HEIGHT,
2267
- idealWidth: SHARE_WIDTH,
2268
- idealHeight: SHARE_HEIGHT,
2269
- },
2270
- },
2271
- firefox: fireFoxOptions,
2272
- })
2273
- );
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
+ });
2274
2163
  });
2275
2164
  });
2276
-
2277
- describe('#stopShare', () => {
2278
- it('should have #stopShare', () => {
2279
- assert.exists(meeting.stopShare);
2165
+ describe('#requestScreenShareFloor', () => {
2166
+ it('should have #requestScreenShareFloor', () => {
2167
+ assert.exists(meeting.requestScreenShareFloor);
2280
2168
  });
2281
2169
  beforeEach(() => {
2282
- meeting.mediaProperties.mediaDirection = {receiveShare: true};
2283
- 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';
2284
2176
  });
2285
- it('should call updateShare', async () => {
2286
- const share = meeting.stopShare();
2177
+ it('should send the share', async () => {
2178
+ const share = meeting.requestScreenShareFloor();
2287
2179
 
2288
2180
  assert.exists(share.then);
2289
2181
  await share;
2290
- assert.calledOnce(meeting.updateShare);
2291
- });
2292
- });
2293
-
2294
- describe('#updateAudio', () => {
2295
- const FAKE_AUDIO_TRACK = {
2296
- id: 'fake audio track',
2297
- getSettings: sinon.stub().returns({}),
2298
- };
2299
-
2300
- describe('when canUpdateMedia is true', () => {
2301
- beforeEach(() => {
2302
- meeting.canUpdateMedia = sinon.stub().returns(true);
2303
- });
2304
- describe('when options are valid', () => {
2305
- beforeEach(() => {
2306
- MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
2307
- meeting.mediaProperties.mediaDirection = {
2308
- sendAudio: false,
2309
- sendVideo: true,
2310
- sendShare: false,
2311
- receiveAudio: false,
2312
- receiveVideo: true,
2313
- receiveShare: true,
2314
- };
2315
- meeting.mediaProperties.webrtcMediaConnection = {
2316
- update: sinon.stub(),
2317
- };
2318
- sinon.stub(MeetingUtil, 'getTrack').returns({audioTrack: FAKE_AUDIO_TRACK});
2319
- });
2320
- it('calls this.mediaProperties.webrtcMediaConnection.update', () =>
2321
- meeting
2322
- .updateAudio({
2323
- sendAudio: true,
2324
- receiveAudio: true,
2325
- stream: {id: 'fake stream'},
2326
- })
2327
- .then(() => {
2328
- assert.calledOnce(
2329
- meeting.mediaProperties.webrtcMediaConnection.update
2330
- );
2331
- assert.calledWith(
2332
- meeting.mediaProperties.webrtcMediaConnection.update,
2333
- {
2334
- localTracks: {audio: FAKE_AUDIO_TRACK},
2335
- direction: {
2336
- audio: 'sendrecv',
2337
- video: 'sendrecv',
2338
- screenShareVideo: 'recvonly',
2339
- },
2340
- remoteQualityLevel: 'HIGH',
2341
- }
2342
- );
2343
- }));
2344
- });
2345
- afterEach(() => {
2346
- sinon.restore();
2347
- });
2182
+ assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
2348
2183
  });
2349
2184
  });
2350
2185
 
@@ -2392,38 +2227,25 @@ describe('plugin-meetings', () => {
2392
2227
 
2393
2228
  describe('#updateMedia', () => {
2394
2229
  let sandbox;
2395
- const mockLocalStream = {id: 'mock local stream'};
2396
- const mockLocalShare = {id: 'mock local share stream'};
2397
- const FAKE_TRACKS = {
2398
- audio: {
2399
- id: 'fake audio track',
2400
- getSettings: sinon.stub().returns({}),
2401
- },
2402
- video: {
2403
- id: 'fake video track',
2404
- getSettings: sinon.stub().returns({}),
2405
- },
2406
- screenshareVideo: {
2407
- id: 'fake share track',
2408
- getSettings: sinon.stub().returns({}),
2409
- on: sinon.stub(),
2410
- },
2411
- };
2412
2230
 
2231
+ const createFakeLocalTrack = () => ({
2232
+ underlyingTrack: {id: 'fake underlying track'}
2233
+ });
2413
2234
  beforeEach(() => {
2414
2235
  sandbox = sinon.createSandbox();
2415
- meeting.mediaProperties.mediaDirection = {sendShare: true};
2416
- // setup the stub to return the right tracks
2417
- sandbox.stub(MeetingUtil, 'getTrack').callsFake((stream) => {
2418
- if (stream === mockLocalStream) {
2419
- return {audioTrack: FAKE_TRACKS.audio, videoTrack: FAKE_TRACKS.video};
2420
- }
2421
- if (stream === mockLocalShare) {
2422
- return {audioTrack: null, videoTrack: FAKE_TRACKS.screenshareVideo};
2423
- }
2424
-
2425
- return {audioTrack: null, videoTrack: null};
2426
- });
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
+ }
2427
2249
  });
2428
2250
 
2429
2251
  afterEach(() => {
@@ -2433,52 +2255,28 @@ describe('plugin-meetings', () => {
2433
2255
 
2434
2256
  forEach(
2435
2257
  [
2436
- {receiveAudio: true, sendAudio: true, enableMultistreamAudio: true},
2437
- {receiveAudio: true, sendAudio: false, enableMultistreamAudio: true},
2438
- {receiveAudio: false, sendAudio: true, enableMultistreamAudio: true},
2439
- {receiveAudio: false, sendAudio: false, enableMultistreamAudio: false},
2258
+ {audioEnabled: true, enableMultistreamAudio: true},
2259
+ {audioEnabled: false, enableMultistreamAudio: false},
2440
2260
  ],
2441
- ({receiveAudio, sendAudio, enableMultistreamAudio}) => {
2442
- it(`should call enableMultistreamAudio with ${enableMultistreamAudio} if it is a multistream connection and receiveAudio: ${receiveAudio} sendAudio: ${sendAudio}`, async () => {
2443
- const mediaSettings = {
2444
- sendAudio,
2445
- receiveAudio,
2446
- sendVideo: true,
2447
- receiveVideo: true,
2448
- sendShare: true,
2449
- receiveShare: true,
2450
- isSharing: true,
2451
- };
2452
-
2261
+ ({audioEnabled, enableMultistreamAudio}) => {
2262
+ it(`should call enableMultistreamAudio with ${enableMultistreamAudio} if it is a multistream connection and audioEnabled: ${audioEnabled}`, async () => {
2453
2263
  meeting.mediaProperties.webrtcMediaConnection = {
2454
- enableMultistreamAudio: sinon.stub().resolves('some value'),
2264
+ enableMultistreamAudio: sinon.stub().resolves({}),
2455
2265
  };
2456
2266
  meeting.isMultistream = true;
2457
2267
 
2458
- const result = await meeting.updateMedia({
2459
- mediaSettings,
2460
- });
2268
+ await meeting.updateMedia({audioEnabled});
2461
2269
 
2462
2270
  assert.calledOnceWithExactly(
2463
2271
  meeting.mediaProperties.webrtcMediaConnection.enableMultistreamAudio,
2464
2272
  enableMultistreamAudio
2465
2273
  );
2466
- assert.equal(result, 'some value');
2274
+ assert.calledOnceWithExactly(meeting.audio.enable, meeting, enableMultistreamAudio);
2467
2275
  });
2468
2276
  }
2469
2277
  );
2470
2278
 
2471
2279
  it('should use a queue if currently busy', async () => {
2472
- const mediaSettings = {
2473
- sendAudio: true,
2474
- receiveAudio: true,
2475
- sendVideo: true,
2476
- receiveVideo: true,
2477
- sendShare: true,
2478
- receiveShare: true,
2479
- isSharing: true,
2480
- };
2481
-
2482
2280
  sandbox.stub(meeting, 'canUpdateMedia').returns(false);
2483
2281
  meeting.mediaProperties.webrtcMediaConnection = {
2484
2282
  update: sinon.stub().resolves({}),
@@ -2487,11 +2285,7 @@ describe('plugin-meetings', () => {
2487
2285
  let myPromiseResolved = false;
2488
2286
 
2489
2287
  meeting
2490
- .updateMedia({
2491
- localStream: mockLocalStream,
2492
- localShare: mockLocalShare,
2493
- mediaSettings,
2494
- })
2288
+ .updateMedia({audioEnabled: false, videoEnabled: false})
2495
2289
  .then(() => {
2496
2290
  myPromiseResolved = true;
2497
2291
  });
@@ -2512,13 +2306,13 @@ describe('plugin-meetings', () => {
2512
2306
  meeting.mediaProperties.webrtcMediaConnection.update,
2513
2307
  {
2514
2308
  localTracks: {
2515
- audio: FAKE_TRACKS.audio,
2516
- video: FAKE_TRACKS.video,
2517
- screenShareVideo: FAKE_TRACKS.screenshareVideo,
2309
+ audio: meeting.mediaProperties.audioTrack.underlyingTrack,
2310
+ video: meeting.mediaProperties.videoTrack.underlyingTrack,
2311
+ screenShareVideo: meeting.mediaProperties.shareTrack.underlyingTrack,
2518
2312
  },
2519
2313
  direction: {
2520
- audio: 'sendrecv',
2521
- video: 'sendrecv',
2314
+ audio: 'inactive',
2315
+ video: 'inactive',
2522
2316
  screenShareVideo: 'sendrecv',
2523
2317
  },
2524
2318
  remoteQualityLevel: 'HIGH',
@@ -2526,195 +2320,6 @@ describe('plugin-meetings', () => {
2526
2320
  );
2527
2321
  assert.isTrue(myPromiseResolved);
2528
2322
  });
2529
-
2530
- it('should request floor only after roap transaction is completed', async () => {
2531
- const eventListeners = {};
2532
-
2533
- meeting.webex.meetings.reachability = {
2534
- isAnyClusterReachable: sandbox.stub().resolves(true),
2535
- };
2536
-
2537
- const fakeMediaConnection = {
2538
- close: sinon.stub(),
2539
- getConnectionState: sinon.stub().returns(ConnectionState.Connected),
2540
- initiateOffer: sinon.stub().resolves({}),
2541
-
2542
- // mock the on() method and store all the listeners
2543
- on: sinon.stub().callsFake((event, listener) => {
2544
- eventListeners[event] = listener;
2545
- }),
2546
-
2547
- update: sinon.stub().callsFake(() => {
2548
- // trigger ROAP_STARTED before update() resolves
2549
- if (eventListeners[Event.ROAP_STARTED]) {
2550
- eventListeners[Event.ROAP_STARTED]();
2551
- } else {
2552
- throw new Error('ROAP_STARTED listener not registered');
2553
- }
2554
- return Promise.resolve();
2555
- }),
2556
- };
2557
-
2558
- meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
2559
- meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
2560
- Media.createMediaConnection = sinon.stub().returns(fakeMediaConnection);
2561
-
2562
- const requestScreenShareFloorStub = sandbox
2563
- .stub(meeting, 'requestScreenShareFloor')
2564
- .resolves({});
2565
-
2566
- let myPromiseResolved = false;
2567
-
2568
- meeting.meetingState = 'ACTIVE';
2569
- await meeting.addMedia({
2570
- mediaSettings: {},
2571
- });
2572
-
2573
- meeting
2574
- .updateMedia({
2575
- localShare: mockLocalShare,
2576
- mediaSettings: {
2577
- sendShare: true,
2578
- },
2579
- })
2580
- .then(() => {
2581
- myPromiseResolved = true;
2582
- });
2583
-
2584
- await testUtils.flushPromises();
2585
-
2586
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.update);
2587
- assert.isFalse(myPromiseResolved);
2588
-
2589
- // verify that requestScreenShareFloorStub was not called yet
2590
- assert.notCalled(requestScreenShareFloorStub);
2591
-
2592
- eventListeners[Event.ROAP_DONE]();
2593
- await testUtils.flushPromises();
2594
-
2595
- // now it should have been called
2596
- assert.calledOnce(requestScreenShareFloorStub);
2597
- });
2598
- });
2599
-
2600
- describe('#updateShare', () => {
2601
- const mockLocalShare = {id: 'mock local share stream'};
2602
- let eventListeners;
2603
- let fakeMediaConnection;
2604
- let requestScreenShareFloorStub;
2605
-
2606
- const FAKE_TRACKS = {
2607
- screenshareVideo: {
2608
- id: 'fake share track',
2609
- getSettings: sinon.stub().returns({}),
2610
- on: sinon.stub(),
2611
- },
2612
- };
2613
-
2614
- beforeEach(async () => {
2615
- eventListeners = {};
2616
-
2617
- sinon.stub(MeetingUtil, 'getTrack').callsFake((stream) => {
2618
- if (stream === mockLocalShare) {
2619
- return {audioTrack: null, videoTrack: FAKE_TRACKS.screenshareVideo};
2620
- }
2621
-
2622
- return {audioTrack: null, videoTrack: null};
2623
- });
2624
-
2625
- meeting.webex.meetings.reachability = {
2626
- isAnyClusterReachable: sinon.stub().resolves(true),
2627
- };
2628
-
2629
- fakeMediaConnection = {
2630
- close: sinon.stub(),
2631
- getConnectionState: sinon.stub().returns(ConnectionState.Connected),
2632
- initiateOffer: sinon.stub().resolves({}),
2633
-
2634
- // mock the on() method and store all the listeners
2635
- on: sinon.stub().callsFake((event, listener) => {
2636
- eventListeners[event] = listener;
2637
- }),
2638
-
2639
- update: sinon.stub().callsFake(() => {
2640
- // trigger ROAP_STARTED before update() resolves
2641
- if (eventListeners[Event.ROAP_STARTED]) {
2642
- eventListeners[Event.ROAP_STARTED]();
2643
- } else {
2644
- throw new Error('ROAP_STARTED listener not registered');
2645
- }
2646
- return Promise.resolve();
2647
- }),
2648
- };
2649
-
2650
- meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
2651
- meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
2652
- Media.createMediaConnection = sinon.stub().returns(fakeMediaConnection);
2653
-
2654
- requestScreenShareFloorStub = sinon.stub(meeting, 'requestScreenShareFloor').resolves({});
2655
-
2656
- meeting.meetingState = 'ACTIVE';
2657
- await meeting.addMedia({
2658
- mediaSettings: {},
2659
- });
2660
- });
2661
-
2662
- afterEach(() => {
2663
- sinon.restore();
2664
- });
2665
-
2666
- it('when starting share, it should request floor only after roap transaction is completed', async () => {
2667
- let myPromiseResolved = false;
2668
-
2669
- meeting
2670
- .updateShare({
2671
- sendShare: true,
2672
- receiveShare: true,
2673
- stream: mockLocalShare,
2674
- })
2675
- .then(() => {
2676
- myPromiseResolved = true;
2677
- });
2678
-
2679
- await testUtils.flushPromises();
2680
-
2681
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.update);
2682
- assert.isFalse(myPromiseResolved);
2683
-
2684
- // verify that requestScreenShareFloorStub was not called yet
2685
- assert.notCalled(requestScreenShareFloorStub);
2686
-
2687
- eventListeners[Event.ROAP_DONE]();
2688
- await testUtils.flushPromises();
2689
-
2690
- // now it should have been called
2691
- assert.calledOnce(requestScreenShareFloorStub);
2692
- });
2693
-
2694
- it('when changing screen share stream and no roap transaction happening, it requests floor immediately', async () => {
2695
- let myPromiseResolved = false;
2696
-
2697
- // simulate a case when no roap transaction is triggered by update
2698
- meeting.mediaProperties.webrtcMediaConnection.update = sinon
2699
- .stub()
2700
- .resolves({});
2701
-
2702
- meeting
2703
- .updateShare({
2704
- sendShare: true,
2705
- receiveShare: true,
2706
- stream: mockLocalShare,
2707
- })
2708
- .then(() => {
2709
- myPromiseResolved = true;
2710
- });
2711
-
2712
- await testUtils.flushPromises();
2713
-
2714
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.update);
2715
- assert.calledOnce(requestScreenShareFloorStub);
2716
- assert.isTrue(myPromiseResolved);
2717
- });
2718
2323
  });
2719
2324
 
2720
2325
  describe('#changeVideoLayout', () => {
@@ -2729,8 +2334,6 @@ describe('plugin-meetings', () => {
2729
2334
  sendShare: false,
2730
2335
  receiveVideo: true,
2731
2336
  };
2732
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([]));
2733
- meeting.updateVideo = sinon.stub().returns(Promise.resolve());
2734
2337
  meeting.mediaProperties.mediaDirection = mediaDirection;
2735
2338
  meeting.mediaProperties.remoteVideoTrack = sinon
2736
2339
  .stub()
@@ -2967,76 +2570,12 @@ describe('plugin-meetings', () => {
2967
2570
  });
2968
2571
  });
2969
2572
 
2970
- describe('#setLocalVideoQuality', () => {
2971
- let mediaDirection;
2972
-
2973
- const fakeTrack = {getSettings: () => ({height: 720})};
2974
- const USER_AGENT_CHROME_MAC =
2975
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
2976
- 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36';
2977
-
2978
- beforeEach(() => {
2979
- mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
2980
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([]));
2981
- meeting.mediaProperties.mediaDirection = mediaDirection;
2982
- meeting.canUpdateMedia = sinon.stub().returns(true);
2983
- MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
2984
- meeting.updateVideo = sinon.stub().resolves();
2985
- sinon.stub(MeetingUtil, 'getTrack').returns({videoTrack: fakeTrack});
2986
- });
2987
-
2988
- it('should have #setLocalVideoQuality', () => {
2989
- assert.exists(meeting.setLocalVideoQuality);
2990
- });
2991
-
2992
- it('should call getMediaStreams with the proper level', () =>
2993
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2994
- delete mediaDirection.receiveVideo;
2995
- assert.calledWith(
2996
- meeting.getMediaStreams,
2997
- mediaDirection,
2998
- CONSTANTS.VIDEO_RESOLUTIONS[CONSTANTS.QUALITY_LEVELS.LOW]
2999
- );
3000
- }));
3001
-
3002
- it('when browser is chrome then it should stop previous video track', () => {
3003
- meeting.mediaProperties.videoTrack = fakeTrack;
3004
- assert.equal(BrowserDetection(USER_AGENT_CHROME_MAC).getBrowserName(), 'Chrome');
3005
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
3006
- assert.calledWith(Media.stopTracks, fakeTrack);
3007
- });
3008
- });
3009
-
3010
- it('should set mediaProperty with the proper level', () =>
3011
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
3012
- assert.equal(meeting.mediaProperties.localQualityLevel, CONSTANTS.QUALITY_LEVELS.LOW);
3013
- }));
3014
-
3015
- it('when device does not support 1080p then it should set localQualityLevel with highest possible resolution', () => {
3016
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS['1080p']).then(() => {
3017
- assert.equal(
3018
- meeting.mediaProperties.localQualityLevel,
3019
- CONSTANTS.QUALITY_LEVELS['720p']
3020
- );
3021
- });
3022
- });
3023
-
3024
- it('should error if set to a invalid level', () => {
3025
- assert.isRejected(meeting.setLocalVideoQuality('invalid'));
3026
- });
3027
-
3028
- it('should error if sendVideo is set to false', () => {
3029
- meeting.mediaProperties.mediaDirection = {sendVideo: false};
3030
- assert.isRejected(meeting.setLocalVideoQuality('LOW'));
3031
- });
3032
- });
3033
-
3034
2573
  describe('#setRemoteQualityLevel', () => {
3035
2574
  let mediaDirection;
3036
2575
 
3037
2576
  beforeEach(() => {
3038
2577
  mediaDirection = {receiveAudio: true, receiveVideo: true, receiveShare: false};
3039
- meeting.updateMedia = sinon.stub().returns(Promise.resolve());
2578
+ meeting.updateTranscodedMediaConnection = sinon.stub().returns(Promise.resolve());
3040
2579
  meeting.mediaProperties.mediaDirection = mediaDirection;
3041
2580
  });
3042
2581
 
@@ -3049,9 +2588,9 @@ describe('plugin-meetings', () => {
3049
2588
  assert.equal(meeting.mediaProperties.remoteQualityLevel, CONSTANTS.QUALITY_LEVELS.LOW);
3050
2589
  }));
3051
2590
 
3052
- it('should call updateMedia', () =>
2591
+ it('should call Meeting.updateTranscodedMediaConnection()', () =>
3053
2592
  meeting.setRemoteQualityLevel(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
3054
- assert.calledOnce(meeting.updateMedia);
2593
+ assert.calledOnce(meeting.updateTranscodedMediaConnection);
3055
2594
  }));
3056
2595
 
3057
2596
  it('should error if set to a invalid level', () => {
@@ -3725,16 +3264,12 @@ describe('plugin-meetings', () => {
3725
3264
  .stub()
3726
3265
  .returns(Promise.resolve({body: 'test'}));
3727
3266
  meeting.locusInfo.onFullLocus = sinon.stub().returns(true);
3728
- meeting.closeLocalStream = sinon.stub().returns(Promise.resolve());
3729
- meeting.closeLocalShare = sinon.stub().returns(Promise.resolve());
3267
+ meeting.cleanupLocalTracks = sinon.stub().returns(Promise.resolve());
3730
3268
  meeting.closeRemoteStream = sinon.stub().returns(Promise.resolve());
3731
3269
  sandbox.stub(meeting, 'closeRemoteTracks').returns(Promise.resolve());
3732
3270
  meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
3733
- meeting.unsetLocalVideoTrack = sinon.stub().returns(true);
3734
- meeting.unsetLocalShareTrack = sinon.stub().returns(true);
3735
3271
  meeting.unsetRemoteTracks = sinon.stub();
3736
3272
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
3737
- meeting.unsetRemoteStream = sinon.stub().returns(true);
3738
3273
  meeting.unsetPeerConnections = sinon.stub().returns(true);
3739
3274
  meeting.logger.error = sinon.stub().returns(true);
3740
3275
  meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
@@ -3753,12 +3288,9 @@ describe('plugin-meetings', () => {
3753
3288
  assert.exists(endMeetingForAll.then);
3754
3289
  await endMeetingForAll;
3755
3290
  assert.calledOnce(meeting?.meetingRequest?.endMeetingForAll);
3756
- assert.calledOnce(meeting?.closeLocalStream);
3757
- assert.calledOnce(meeting?.closeLocalShare);
3291
+ assert.calledOnce(meeting?.cleanupLocalTracks);
3758
3292
  assert.calledOnce(meeting?.closeRemoteTracks);
3759
3293
  assert.calledOnce(meeting?.closePeerConnections);
3760
- assert.calledOnce(meeting?.unsetLocalVideoTrack);
3761
- assert.calledOnce(meeting?.unsetLocalShareTrack);
3762
3294
  assert.calledOnce(meeting?.unsetRemoteTracks);
3763
3295
  assert.calledOnce(meeting?.unsetPeerConnections);
3764
3296
  });
@@ -3769,11 +3301,9 @@ describe('plugin-meetings', () => {
3769
3301
 
3770
3302
  beforeEach(() => {
3771
3303
  sandbox = sinon.createSandbox();
3772
- sandbox.stub(meeting, 'closeLocalStream');
3773
- sandbox.stub(meeting, 'closeLocalShare');
3304
+ sandbox.stub(meeting, 'cleanupLocalTracks');
3774
3305
 
3775
3306
  sandbox.stub(meeting.mediaProperties, 'setMediaDirection');
3776
- sandbox.stub(meeting.mediaProperties, 'unsetMediaTracks');
3777
3307
 
3778
3308
  sandbox.stub(meeting.reconnectionManager, 'reconnectMedia').returns(Promise.resolve());
3779
3309
  sandbox
@@ -3846,14 +3376,12 @@ describe('plugin-meetings', () => {
3846
3376
 
3847
3377
  // beacuse we are calling callback so we need to wait
3848
3378
 
3849
- assert.called(meeting.closeLocalStream);
3850
- assert.called(meeting.closeLocalShare);
3379
+ assert.called(meeting.cleanupLocalTracks);
3851
3380
 
3852
3381
  // give queued Promise callbacks a chance to run
3853
3382
  await Promise.resolve();
3854
3383
 
3855
3384
  assert.called(meeting.mediaProperties.setMediaDirection);
3856
- assert.called(meeting.mediaProperties.unsetMediaTracks);
3857
3385
 
3858
3386
  assert.calledWith(meeting.reconnectionManager.reconnectMedia, {
3859
3387
  mediaDirection: {
@@ -4002,8 +3530,8 @@ describe('plugin-meetings', () => {
4002
3530
  meeting.requestScreenShareFloor = sinon.stub().resolves({});
4003
3531
  meeting.releaseScreenShareFloor = sinon.stub().resolves({});
4004
3532
  meeting.mediaProperties.mediaDirection = {
4005
- sendAudio: false,
4006
- 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',
4007
3535
  sendShare: false,
4008
3536
  };
4009
3537
  meeting.isMultistream = true;
@@ -4011,6 +3539,8 @@ describe('plugin-meetings', () => {
4011
3539
  publishTrack: sinon.stub().resolves({}),
4012
3540
  unpublishTrack: sinon.stub().resolves({}),
4013
3541
  };
3542
+ meeting.audio = { handleLocalTrackChange: sinon.stub()};
3543
+ meeting.video = { handleLocalTrackChange: sinon.stub()};
4014
3544
 
4015
3545
  const createFakeLocalTrack = (originalTrack) => ({
4016
3546
  on: sinon.stub(),
@@ -4051,33 +3581,25 @@ describe('plugin-meetings', () => {
4051
3581
  });
4052
3582
 
4053
3583
  const checkAudioPublished = (track) => {
4054
- assert.calledWith(
4055
- createMuteStateStub,
4056
- 'audio',
4057
- meeting,
4058
- meeting.mediaProperties.mediaDirection
4059
- );
3584
+ assert.calledOnceWithExactly(meeting.audio.handleLocalTrackChange, meeting);
4060
3585
  assert.calledWith(
4061
3586
  meeting.mediaProperties.webrtcMediaConnection.publishTrack,
4062
3587
  track
4063
3588
  );
4064
3589
  assert.equal(meeting.mediaProperties.audioTrack, track);
4065
- assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, true);
3590
+ // check that sendAudio hasn't been touched
3591
+ assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, 'fake value');
4066
3592
  };
4067
3593
 
4068
3594
  const checkVideoPublished = (track) => {
4069
- assert.calledWith(
4070
- createMuteStateStub,
4071
- 'video',
4072
- meeting,
4073
- meeting.mediaProperties.mediaDirection
4074
- );
3595
+ assert.calledOnceWithExactly(meeting.video.handleLocalTrackChange, meeting);
4075
3596
  assert.calledWith(
4076
3597
  meeting.mediaProperties.webrtcMediaConnection.publishTrack,
4077
3598
  track
4078
3599
  );
4079
3600
  assert.equal(meeting.mediaProperties.videoTrack, track);
4080
- assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, true);
3601
+ // check that sendVideo hasn't been touched
3602
+ assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, 'fake value');
4081
3603
  };
4082
3604
 
4083
3605
  const checkScreenShareVideoPublished = (track) => {
@@ -4098,18 +3620,16 @@ describe('plugin-meetings', () => {
4098
3620
  checkScreenShareVideoPublished(videoShareTrack);
4099
3621
  });
4100
3622
 
4101
- 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 () => {
4102
3624
  await meeting.publishTracks({microphone: audioTrack});
4103
3625
 
4104
- assert.calledOnce(createMuteStateStub);
4105
3626
  assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
4106
3627
  checkAudioPublished(audioTrack);
4107
3628
  });
4108
3629
 
4109
- 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 () => {
4110
3631
  await meeting.publishTracks({camera: videoTrack});
4111
3632
 
4112
- assert.calledOnce(createMuteStateStub);
4113
3633
  assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
4114
3634
  checkVideoPublished(videoTrack);
4115
3635
  });
@@ -4123,13 +3643,20 @@ describe('plugin-meetings', () => {
4123
3643
  },
4124
3644
  });
4125
3645
 
4126
- assert.calledTwice(createMuteStateStub);
4127
3646
  assert.calledThrice(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
4128
3647
  checkAudioPublished(audioTrack);
4129
3648
  checkVideoPublished(videoTrack);
4130
3649
  checkScreenShareVideoPublished(videoShareTrack);
4131
3650
  });
4132
3651
  });
3652
+ it('creates instance and publishes with annotation info', async () => {
3653
+ const annotationInfo = {
3654
+ version: '1',
3655
+ policy: ANNOTATION_POLICY.APPROVAL,
3656
+ };
3657
+ await meeting.publishTracks({annotationInfo});
3658
+ assert.equal(meeting.annotationInfo, annotationInfo);
3659
+ });
4133
3660
 
4134
3661
  describe('unpublishTracks', () => {
4135
3662
  beforeEach(async () => {
@@ -4147,7 +3674,7 @@ describe('plugin-meetings', () => {
4147
3674
  );
4148
3675
 
4149
3676
  assert.equal(meeting.mediaProperties.audioTrack, null);
4150
- assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, false);
3677
+ assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, 'fake value');
4151
3678
  };
4152
3679
 
4153
3680
  const checkVideoUnpublished = () => {
@@ -4157,7 +3684,7 @@ describe('plugin-meetings', () => {
4157
3684
  );
4158
3685
 
4159
3686
  assert.equal(meeting.mediaProperties.videoTrack, null);
4160
- assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, false);
3687
+ assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, 'fake value');
4161
3688
  };
4162
3689
 
4163
3690
  const checkScreenShareVideoUnpublished = () => {
@@ -4389,82 +3916,6 @@ describe('plugin-meetings', () => {
4389
3916
  );
4390
3917
  });
4391
3918
  });
4392
- describe('#closeLocalShare', () => {
4393
- it('should stop the stream, and trigger a media:stopped event when the local share stream stops', async () => {
4394
- await meeting.closeLocalShare();
4395
- assert.calledTwice(TriggerProxy.trigger);
4396
-
4397
- assert.equal(TriggerProxy.trigger.getCall(1).args[2], 'media:stopped');
4398
- assert.deepEqual(TriggerProxy.trigger.getCall(1).args[3], {type: 'localShare'});
4399
- });
4400
- });
4401
- describe('#closeLocalStream', () => {
4402
- it('should stop the stream, and trigger a media:stopped event when the local stream stops', async () => {
4403
- await meeting.closeLocalStream();
4404
- assert.calledTwice(TriggerProxy.trigger);
4405
- assert.calledWith(
4406
- TriggerProxy.trigger,
4407
- sinon.match.instanceOf(Meeting),
4408
- {file: 'meeting/index', function: 'closeLocalStream'},
4409
- 'media:stopped',
4410
- {type: 'local'}
4411
- );
4412
- });
4413
- });
4414
- describe('#setLocalTracks', () => {
4415
- it('stores the current video device as the preferred video device', () => {
4416
- const videoDevice = 'video1';
4417
- const fakeTrack = {getSettings: () => ({deviceId: videoDevice})};
4418
- const fakeStream = 'stream1';
4419
-
4420
- sandbox.stub(MeetingUtil, 'getTrack').returns({audioTrack: null, videoTrack: fakeTrack});
4421
- sandbox.stub(meeting.mediaProperties, 'setMediaSettings');
4422
- sandbox.stub(meeting.mediaProperties, 'setVideoDeviceId');
4423
-
4424
- meeting.setLocalTracks(fakeStream);
4425
-
4426
- assert.calledWith(meeting.mediaProperties.setVideoDeviceId, videoDevice);
4427
- });
4428
- });
4429
- describe('#setLocalShareTrack', () => {
4430
- it('should trigger a media:ready event with local share stream', () => {
4431
- const track = {
4432
- getSettings: sinon.stub().returns({
4433
- aspectRatio: '1.7',
4434
- frameRate: 30,
4435
- height: 1980,
4436
- width: 1080,
4437
- displaySurface: true,
4438
- cursor: true,
4439
- }),
4440
- };
4441
-
4442
- const listeners = {};
4443
- const fakeLocalDisplayTrack = {
4444
- on: sinon.stub().callsFake((event, listener) => {
4445
- listeners[event] = listener;
4446
- }),
4447
- };
4448
- sinon.stub(InternalMediaCoreModule, 'LocalDisplayTrack').returns(fakeLocalDisplayTrack);
4449
-
4450
- meeting.mediaProperties.setLocalShareTrack = sinon.stub().returns(true);
4451
- meeting.stopShare = sinon.stub().resolves(true);
4452
- meeting.mediaProperties.mediaDirection = {};
4453
- meeting.setLocalShareTrack(track);
4454
- assert.calledTwice(TriggerProxy.trigger);
4455
- assert.calledWith(
4456
- TriggerProxy.trigger,
4457
- sinon.match.instanceOf(Meeting),
4458
- {file: 'meeting/index', function: 'setLocalShareTrack'},
4459
- 'media:ready'
4460
- );
4461
- assert.calledOnce(meeting.mediaProperties.setLocalShareTrack);
4462
- assert.equal(meeting.mediaProperties.localStream, undefined);
4463
- assert.isNotNull(listeners[LocalTrackEvents.Ended]);
4464
- listeners[LocalTrackEvents.Ended]();
4465
- assert.calledOnce(meeting.stopShare);
4466
- });
4467
- });
4468
3919
  describe('#setupMediaConnectionListeners', () => {
4469
3920
  let eventListeners;
4470
3921
 
@@ -5372,20 +4823,6 @@ describe('plugin-meetings', () => {
5372
4823
  assert.calledOnce(meeting.mediaProperties.unsetRemoteTracks);
5373
4824
  });
5374
4825
  });
5375
- describe('#unsetLocalVideoTrack', () => {
5376
- it('should unset the local stream and return null', () => {
5377
- meeting.mediaProperties.unsetLocalVideoTrack = sinon.stub().returns(true);
5378
- meeting.unsetLocalVideoTrack();
5379
- assert.calledOnce(meeting.mediaProperties.unsetLocalVideoTrack);
5380
- });
5381
- });
5382
- describe('#unsetLocalShareTrack', () => {
5383
- it('should unset the local share stream and return null', () => {
5384
- meeting.mediaProperties.unsetLocalShareTrack = sinon.stub().returns(true);
5385
- meeting.unsetLocalShareTrack();
5386
- assert.calledOnce(meeting.mediaProperties.unsetLocalShareTrack);
5387
- });
5388
- });
5389
4826
  // TODO: remove
5390
4827
  describe('#setMercuryListener', () => {
5391
4828
  it('should listen to mercury events', () => {
@@ -6055,11 +5492,6 @@ describe('plugin-meetings', () => {
6055
5492
  describe('setUpLocusMediaSharesListener', () => {
6056
5493
  beforeEach(() => {
6057
5494
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
6058
- sinon.stub(meeting, 'updateShare').returns(Promise.resolve());
6059
- });
6060
-
6061
- afterEach(() => {
6062
- meeting.updateShare.restore();
6063
5495
  });
6064
5496
 
6065
5497
  const USER_IDS = {