@webex/plugin-meetings 3.0.0-beta.43 → 3.0.0-beta.45

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 (57) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +12 -3
  3. package/dist/breakouts/index.js.map +1 -1
  4. package/dist/constants.js +15 -3
  5. package/dist/constants.js.map +1 -1
  6. package/dist/locus-info/controlsUtils.js +6 -2
  7. package/dist/locus-info/controlsUtils.js.map +1 -1
  8. package/dist/locus-info/index.js +35 -1
  9. package/dist/locus-info/index.js.map +1 -1
  10. package/dist/locus-info/selfUtils.js +28 -0
  11. package/dist/locus-info/selfUtils.js.map +1 -1
  12. package/dist/meeting/in-meeting-actions.js +9 -1
  13. package/dist/meeting/in-meeting-actions.js.map +1 -1
  14. package/dist/meeting/index.js +43 -4
  15. package/dist/meeting/index.js.map +1 -1
  16. package/dist/meeting/muteState.js +45 -20
  17. package/dist/meeting/muteState.js.map +1 -1
  18. package/dist/meeting/util.js +15 -0
  19. package/dist/meeting/util.js.map +1 -1
  20. package/dist/members/index.js +15 -3
  21. package/dist/members/index.js.map +1 -1
  22. package/dist/members/util.js +33 -12
  23. package/dist/members/util.js.map +1 -1
  24. package/dist/multistream/remoteMediaManager.js +112 -55
  25. package/dist/multistream/remoteMediaManager.js.map +1 -1
  26. package/dist/types/constants.d.ts +11 -0
  27. package/dist/types/locus-info/index.d.ts +7 -0
  28. package/dist/types/meeting/in-meeting-actions.d.ts +8 -0
  29. package/dist/types/meeting/index.d.ts +12 -2
  30. package/dist/types/meeting/muteState.d.ts +16 -0
  31. package/dist/types/members/index.d.ts +7 -2
  32. package/dist/types/multistream/remoteMediaManager.d.ts +20 -2
  33. package/package.json +18 -18
  34. package/src/breakouts/index.ts +8 -1
  35. package/src/constants.ts +13 -0
  36. package/src/locus-info/controlsUtils.ts +8 -0
  37. package/src/locus-info/index.ts +42 -1
  38. package/src/locus-info/selfUtils.ts +34 -0
  39. package/src/meeting/in-meeting-actions.ts +16 -0
  40. package/src/meeting/index.ts +53 -4
  41. package/src/meeting/muteState.ts +49 -30
  42. package/src/meeting/util.ts +15 -0
  43. package/src/members/index.ts +12 -4
  44. package/src/members/util.ts +21 -8
  45. package/src/multistream/remoteMediaManager.ts +57 -26
  46. package/test/unit/spec/breakouts/index.ts +2 -2
  47. package/test/unit/spec/locus-info/controlsUtils.js +104 -46
  48. package/test/unit/spec/locus-info/index.js +131 -16
  49. package/test/unit/spec/locus-info/selfConstant.js +9 -5
  50. package/test/unit/spec/locus-info/selfUtils.js +39 -16
  51. package/test/unit/spec/meeting/in-meeting-actions.ts +9 -1
  52. package/test/unit/spec/meeting/index.js +208 -79
  53. package/test/unit/spec/meeting/muteState.js +72 -6
  54. package/test/unit/spec/meeting/utils.js +35 -0
  55. package/test/unit/spec/members/index.js +75 -0
  56. package/test/unit/spec/members/utils.js +112 -0
  57. package/test/unit/spec/multistream/remoteMediaManager.ts +127 -0
@@ -24,6 +24,9 @@ describe('plugin-meetings', () => {
24
24
  },
25
25
  remoteMuted: false,
26
26
  unmuteAllowed: true,
27
+ remoteVideoMuted: false,
28
+ unmuteVideoAllowed: true,
29
+
27
30
  locusInfo: {
28
31
  onFullLocus: sinon.stub(),
29
32
  },
@@ -75,15 +78,16 @@ describe('plugin-meetings', () => {
75
78
  });
76
79
 
77
80
  it('initialises correctly for video', async () => {
78
- // setup fields related to audio remote state
79
- meeting.remoteMuted = true;
80
- meeting.unmuteAllowed = false;
81
- // create a new video MuteState intance
81
+ // setup fields related to video remote state
82
+ meeting.remoteVideoMuted = false;
83
+ meeting.unmuteVideoAllowed = false;
84
+
85
+ // create a new video MuteState instance
82
86
  video = createMuteState(VIDEO, meeting, {sendVideo: true});
83
87
 
84
88
  assert.isFalse(video.isMuted());
85
89
  assert.isFalse(video.state.server.remoteMute);
86
- assert.isTrue(video.state.server.unmuteAllowed);
90
+ assert.isFalse(video.state.server.unmuteAllowed);
87
91
  });
88
92
 
89
93
  it('takes remote mute into account when reporting current state', async () => {
@@ -167,6 +171,30 @@ describe('plugin-meetings', () => {
167
171
  assert.isFalse(audio.isSelf());
168
172
  });
169
173
 
174
+ it('does local video unmute if localVideoUnmuteRequired is received', async () => {
175
+ // first we need to mute
176
+ await video.handleClientRequest(meeting, true);
177
+
178
+ assert.isTrue(video.isMuted());
179
+ assert.isTrue(video.isSelf());
180
+
181
+ MeetingUtil.remoteUpdateAudioVideo.resetHistory();
182
+
183
+ // now simulate server requiring us to locally unmute
184
+ video.handleServerLocalUnmuteRequired(meeting);
185
+ await testUtils.flushPromises();
186
+
187
+ // check that local track was unmuted
188
+ assert.calledWith(meeting.mediaProperties.videoTrack.setMuted, false);
189
+
190
+ // and local unmute was sent to server
191
+ assert.calledOnce(MeetingUtil.remoteUpdateAudioVideo);
192
+ assert.calledWith(MeetingUtil.remoteUpdateAudioVideo, undefined, false, meeting);
193
+
194
+ assert.isFalse(video.isMuted());
195
+ assert.isFalse(video.isSelf());
196
+ });
197
+
170
198
  describe('#isLocallyMuted()', () => {
171
199
  it('does not consider remote mute status for audio', async () => {
172
200
  // simulate being already remote muted
@@ -176,6 +204,15 @@ describe('plugin-meetings', () => {
176
204
 
177
205
  assert.isFalse(audio.isLocallyMuted());
178
206
  });
207
+
208
+ it('does not consider remote mute status for video', async () => {
209
+ // simulate being already remote muted
210
+ meeting.remoteVideoMuted = true;
211
+ // create a new MuteState intance
212
+ video = createMuteState(VIDEO, meeting, {sendVideo: true});
213
+
214
+ assert.isFalse(video.isLocallyMuted());
215
+ });
179
216
  });
180
217
 
181
218
  describe('#handleClientRequest', () => {
@@ -238,12 +275,41 @@ describe('plugin-meetings', () => {
238
275
 
239
276
  // check that remote unmute was sent to server
240
277
  assert.calledOnce(meeting.members.muteMember);
241
- assert.calledWith(meeting.members.muteMember, meeting.members.selfId, false);
278
+ assert.calledWith(meeting.members.muteMember, meeting.members.selfId, false, true);
242
279
 
243
280
  assert.isFalse(audio.isMuted());
244
281
  assert.isFalse(audio.isSelf());
245
282
  });
246
283
 
284
+ it('does video remote unmute when unmuting and remote mute is on', async () => {
285
+ // simulate remote mute
286
+ video.handleServerRemoteMuteUpdate(true, true);
287
+
288
+ // unmute
289
+ await video.handleClientRequest(meeting, false);
290
+
291
+ // check that remote unmute was sent to server
292
+ assert.calledOnce(meeting.members.muteMember);
293
+ assert.calledWith(meeting.members.muteMember, meeting.members.selfId, false, false);
294
+
295
+ assert.isFalse(video.isMuted());
296
+ assert.isFalse(video.isSelf());
297
+ });
298
+
299
+ it('does not video remote unmute when unmuting and remote mute is off', async () => {
300
+ // simulate remote mute
301
+ video.handleServerRemoteMuteUpdate(false, true);
302
+
303
+ // unmute
304
+ await video.handleClientRequest(meeting, false);
305
+
306
+ // check that remote unmute was sent to server
307
+ assert.notCalled(meeting.members.muteMember);
308
+
309
+ assert.isFalse(video.isMuted());
310
+ assert.isFalse(video.isSelf());
311
+ });
312
+
247
313
  it('resolves client request promise once the server is updated', async () => {
248
314
  let clientPromiseResolved = false;
249
315
 
@@ -419,5 +419,40 @@ describe('plugin-meetings', () => {
419
419
  });
420
420
  });
421
421
  });
422
+
423
+ describe('canManageBreakout', () => {
424
+ it('works as expected', () => {
425
+ assert.deepEqual(MeetingUtil.canManageBreakout(['BREAKOUT_MANAGEMENT']), true);
426
+ assert.deepEqual(MeetingUtil.canManageBreakout([]), false);
427
+ });
428
+ });
429
+
430
+ describe('isSuppressBreakoutSupport', () => {
431
+ it('works as expected', () => {
432
+ assert.deepEqual(MeetingUtil.isSuppressBreakoutSupport(['UCF_SUPPRESS_BREAKOUTS_SUPPORT']), true);
433
+ assert.deepEqual(MeetingUtil.isSuppressBreakoutSupport([]), false);
434
+ });
435
+ });
436
+
437
+ describe('canAdmitLobbyToBreakout', () => {
438
+ it('works as expected', () => {
439
+ assert.deepEqual(MeetingUtil.canAdmitLobbyToBreakout(['DISABLE_LOBBY_TO_BREAKOUT']), false);
440
+ assert.deepEqual(MeetingUtil.canAdmitLobbyToBreakout([]), true);
441
+ });
442
+ });
443
+
444
+ describe('canUserAskForHelp', () => {
445
+ it('works as expected', () => {
446
+ assert.deepEqual(MeetingUtil.canUserAskForHelp(['DISABLE_ASK_FOR_HELP']), false);
447
+ assert.deepEqual(MeetingUtil.canUserAskForHelp([]), true);
448
+ });
449
+ });
450
+
451
+ describe('isBreakoutPreassignmentsEnabled', () => {
452
+ it('works as expected', () => {
453
+ assert.deepEqual(MeetingUtil.isBreakoutPreassignmentsEnabled(['DISABLE_BREAKOUT_PREASSIGNMENTS']), false);
454
+ assert.deepEqual(MeetingUtil.isBreakoutPreassignmentsEnabled([]), true);
455
+ });
456
+ });
422
457
  })
423
458
  });
@@ -130,6 +130,81 @@ describe('plugin-meetings', () => {
130
130
  });
131
131
  });
132
132
 
133
+ describe('#admitMembers', () => {
134
+ let members;
135
+ beforeEach(() => {
136
+ members = createMembers({url: url1});
137
+ members.membersRequest.admitMember = sinon.stub().returns(Promise.resolve(true));
138
+ });
139
+ it('should return error if param memberIds is not provided', async () => {
140
+ let error;
141
+ await members.admitMembers().catch((e) => {
142
+ error = e;
143
+ });
144
+ assert.deepEqual(error, new ParameterError('No member ids provided to admit.'));
145
+ });
146
+
147
+ it('should call membersRequest.admitMember as expected', async () => {
148
+ await members.admitMembers(['uuid']);
149
+ const arg1 = members.membersRequest.admitMember.getCall(0).args[0];
150
+ assert.equal(arg1.sessionLocusUrls, undefined);
151
+ assert.equal(arg1.locusUrl.includes('https://example.com/'), true);
152
+ assert.deepEqual(arg1.memberIds, ['uuid']);
153
+
154
+ const sessionLocusUrls = {
155
+ authorizingLocusUrl: 'authorizingLocusUrl',
156
+ mainLocusUrl: 'mainLocusUrl',
157
+ };
158
+ await members.admitMembers(['uuid'], sessionLocusUrls);
159
+ const arg2 = members.membersRequest.admitMember.getCall(1).args[0];
160
+ assert.equal(arg2.sessionLocusUrls, sessionLocusUrls);
161
+ assert.equal(arg1.locusUrl.includes('https://example.com/'), true);
162
+ assert.deepEqual(arg1.memberIds, ['uuid']);
163
+ });
164
+ });
165
+
166
+ describe('#muteMember', () => {
167
+ const testMuteMember = async (mute, isAudio) => {
168
+ sandbox.spy(MembersUtil, 'generateMuteMemberOptions');
169
+
170
+ const locusUrl = 'locus-url';
171
+ const members = createMembers({url: locusUrl});
172
+ const {membersRequest} = members;
173
+ sandbox.spy(membersRequest, 'muteMember');
174
+
175
+ const memberId = 'bob';
176
+
177
+ await members.muteMember(memberId, mute, isAudio);
178
+ assert.calledOnce(MembersUtil.generateMuteMemberOptions);
179
+ assert.calledWith(
180
+ MembersUtil.generateMuteMemberOptions,
181
+ memberId,
182
+ mute,
183
+ members.locusUrl,
184
+ isAudio
185
+ );
186
+
187
+ assert.calledOnce(membersRequest.muteMember);
188
+ assert.calledWith(membersRequest.muteMember, {memberId, muted: mute, locusUrl, isAudio});
189
+ };
190
+
191
+ it('invokes expected functions when muteMember is called for mute=true, isAudio=true', async () => {
192
+ testMuteMember(true, true);
193
+ });
194
+
195
+ it('invokes expected functions when muteMember is called for mute=true, isAudio=false', async () => {
196
+ testMuteMember(true, false);
197
+ });
198
+
199
+ it('invokes expected functions when muteMember is called for mute=false, isAudio=true', async () => {
200
+ testMuteMember(false, true);
201
+ });
202
+
203
+ it('invokes expected functions when muteMember is called for mute=false, isAudio=false', async () => {
204
+ testMuteMember(false, false);
205
+ });
206
+ });
207
+
133
208
  describe('#sendDialPadKey', () => {
134
209
  it('should throw a rejection when calling sendDialPadKey with no tones', async () => {
135
210
  const members = createMembers({url: url1});
@@ -3,6 +3,7 @@ import chai from 'chai';
3
3
  import chaiAsPromised from 'chai-as-promised';
4
4
 
5
5
  import MembersUtil from '@webex/plugin-meetings/src/members/util';
6
+ import {HTTP_VERBS, CONTROLS, PARTICIPANT} from '@webex/plugin-meetings/src/constants';
6
7
 
7
8
  const {assert} = chai;
8
9
 
@@ -38,5 +39,116 @@ describe('plugin-meetings', () => {
38
39
  );
39
40
  });
40
41
  });
42
+ describe('#getAdmitMemberRequestBody', () => {
43
+ it('returns the correct request body', () => {
44
+ const option1 = {memberIds: ['uuid']};
45
+
46
+ assert.deepEqual(MembersUtil.getAdmitMemberRequestBody(option1), {
47
+ admit: {participantIds: ['uuid']},
48
+ });
49
+
50
+ const option2 = {
51
+ memberIds: ['uuid'],
52
+ sessionLocusUrls: {authorizingLocusUrl: 'authorizingLocusUrl'},
53
+ };
54
+
55
+ assert.deepEqual(MembersUtil.getAdmitMemberRequestBody(option2), {
56
+ admit: {participantIds: ['uuid']},
57
+ authorizingLocusUrl: 'authorizingLocusUrl',
58
+ });
59
+ });
60
+ });
61
+ describe('#getAdmitMemberRequestParams', () => {
62
+ it('returns the correct request params', () => {
63
+ const format1 = {memberIds: ['uuid'], locusUrl: 'locusUrl'};
64
+
65
+ assert.deepEqual(MembersUtil.getAdmitMemberRequestParams(format1), {
66
+ method: 'PUT',
67
+ uri: 'locusUrl/controls',
68
+ body: {admit: {participantIds: ['uuid']}},
69
+ });
70
+
71
+ const format2 = {
72
+ memberIds: ['uuid'],
73
+ sessionLocusUrls: {
74
+ authorizingLocusUrl: 'authorizingLocusUrl',
75
+ mainLocusUrl: 'mainLocusUrl',
76
+ },
77
+ locusUrl: 'locusUrl',
78
+ };
79
+
80
+ assert.deepEqual(MembersUtil.getAdmitMemberRequestParams(format2), {
81
+ method: 'PUT',
82
+ uri: 'mainLocusUrl/controls',
83
+ body: {
84
+ admit: {participantIds: ['uuid']},
85
+ authorizingLocusUrl: 'authorizingLocusUrl',
86
+ },
87
+ });
88
+ });
89
+ });
90
+
91
+ describe('#generateMuteMemberOptions', () => {
92
+ const testOptions = (isAudio) => {
93
+ const memberId = 'bob';
94
+ const muteStatus = true;
95
+ const locusUrl = 'urlTest1';
96
+
97
+ assert.deepEqual(
98
+ MembersUtil.generateMuteMemberOptions(memberId, muteStatus, locusUrl, isAudio),
99
+ {
100
+ memberId,
101
+ muted: muteStatus,
102
+ locusUrl,
103
+ isAudio,
104
+ }
105
+ );
106
+ };
107
+
108
+ it('returns the correct options for audio', () => {
109
+ testOptions(true);
110
+ });
111
+
112
+ it('returns the correct options for video', () => {
113
+ testOptions(false);
114
+ });
115
+ });
116
+
117
+ describe('#getMuteMemberRequestParams', () => {
118
+ const testParams = (isAudio) => {
119
+ const memberId = 'bob';
120
+ const muteStatus = true;
121
+ const locusUrl = 'urlTest1';
122
+
123
+ const options = {
124
+ memberId,
125
+ muted: muteStatus,
126
+ locusUrl,
127
+ isAudio,
128
+ };
129
+
130
+ const uri = `${options.locusUrl}/${PARTICIPANT}/${options.memberId}/${CONTROLS}`;
131
+ const property = isAudio ? 'audio' : 'video';
132
+ const body = {
133
+ [property]: {
134
+ muted: options.muted,
135
+ },
136
+ };
137
+
138
+ assert.deepEqual(MembersUtil.getMuteMemberRequestParams(options), {
139
+ method: HTTP_VERBS.PATCH,
140
+ uri,
141
+ body,
142
+ });
143
+ };
144
+
145
+ it('returns the correct params for audio', () => {
146
+ testParams(true);
147
+ });
148
+
149
+ it('returns the correct params for video', () => {
150
+ testParams(false);
151
+ });
152
+ });
41
153
  });
42
154
  });
@@ -699,6 +699,133 @@ describe('RemoteMediaManager', () => {
699
699
  });
700
700
  });
701
701
 
702
+ it('releases slots and reallocates slots when switching to layouts in correct order', async () => {
703
+
704
+ const config = cloneDeep(DefaultTestConfiguration);
705
+ let count = 0;
706
+
707
+ fakeReceiveSlotManager.allocateSlot = sinon.stub().callsFake((mediaType) => {
708
+ switch (mediaType) {
709
+ case MediaType.AudioMain:
710
+ return Promise.resolve(fakeAudioSlot);
711
+ case MediaType.VideoMain:
712
+ return Promise.resolve(new FakeSlot(MediaType.VideoMain, `fake video ${count++}`));
713
+ case MediaType.AudioSlides:
714
+ return Promise.resolve(fakeScreenShareAudioSlot);
715
+ case MediaType.VideoSlides:
716
+ return Promise.resolve(fakeScreenShareVideoSlot);
717
+ }
718
+ throw new Error(`invalid mediaType: ${mediaType}`);
719
+ })
720
+
721
+ remoteMediaManager = new RemoteMediaManager(
722
+ fakeReceiveSlotManager,
723
+ fakeMediaRequestManagers,
724
+ config
725
+ );
726
+
727
+ await remoteMediaManager.start();
728
+
729
+ resetHistory();
730
+
731
+ assert.deepEqual(remoteMediaManager.slots.video.activeSpeaker.map((slot: any) => slot.id), [
732
+ "fake video 0",
733
+ "fake video 1",
734
+ "fake video 2",
735
+ "fake video 3",
736
+ "fake video 4",
737
+ "fake video 5",
738
+ "fake video 6",
739
+ "fake video 7",
740
+ "fake video 8",
741
+ ]);
742
+
743
+ assert.deepEqual(remoteMediaManager.receiveSlotAllocations.activeSpeaker["main"].slots.map((slot: any) => slot.id), [
744
+ "fake video 0",
745
+ "fake video 1",
746
+ "fake video 2",
747
+ "fake video 3",
748
+ "fake video 4",
749
+ "fake video 5",
750
+ "fake video 6",
751
+ "fake video 7",
752
+ "fake video 8",
753
+ ])
754
+
755
+ // switch to "OnePlusFive" layout that requires 3 less video slots (6)
756
+ await remoteMediaManager.setLayout('OnePlusFive');
757
+
758
+ assert.deepEqual(remoteMediaManager.slots.video.unused, []);
759
+
760
+ assert.deepEqual(remoteMediaManager.slots.video.activeSpeaker.map((slot: any) => slot.id), [
761
+ "fake video 0",
762
+ "fake video 1",
763
+ "fake video 2",
764
+ "fake video 3",
765
+ "fake video 4",
766
+ "fake video 5"
767
+ ]);
768
+
769
+ // we're checking that the slots are in the same order as in the previous layout
770
+ // first one goes into main
771
+ assert.deepEqual(remoteMediaManager.receiveSlotAllocations.activeSpeaker["mainBigOne"].slots.map((slot: any) => slot.id), [
772
+ "fake video 0",
773
+ ])
774
+ // and rest go in the pips
775
+ assert.deepEqual(remoteMediaManager.receiveSlotAllocations.activeSpeaker["secondarySetOfSmallPanes"].slots.map((slot: any) => slot.id), [
776
+ "fake video 1",
777
+ "fake video 2",
778
+ "fake video 3",
779
+ "fake video 4",
780
+ "fake video 5"
781
+ ])
782
+
783
+ // verify that 3 main video slots were released
784
+ assert.callCount(fakeReceiveSlotManager.releaseSlot, 3);
785
+ fakeReceiveSlotManager.releaseSlot.getCalls().forEach((call) => {
786
+ const slot = call.args[0];
787
+
788
+ assert.strictEqual(slot.mediaType, MediaType.VideoMain);
789
+ });
790
+
791
+ await remoteMediaManager.setLayout('AllEqual');
792
+
793
+ assert.deepEqual(remoteMediaManager.slots.video.unused, []);
794
+
795
+ // checking that slots are in the same order as in previous layout + 3 new ones
796
+ assert.deepEqual(remoteMediaManager.slots.video.activeSpeaker.map((slot: any) => slot.id), [
797
+ "fake video 0",
798
+ "fake video 1",
799
+ "fake video 2",
800
+ "fake video 3",
801
+ "fake video 4",
802
+ "fake video 5",
803
+ "fake video 10",
804
+ "fake video 11",
805
+ "fake video 12",
806
+ ]);
807
+
808
+ assert.deepEqual(remoteMediaManager.receiveSlotAllocations.activeSpeaker["main"].slots.map((slot: any) => slot.id), [
809
+ "fake video 0",
810
+ "fake video 1",
811
+ "fake video 2",
812
+ "fake video 3",
813
+ "fake video 4",
814
+ "fake video 5",
815
+ "fake video 10",
816
+ "fake video 11",
817
+ "fake video 12"
818
+ ])
819
+
820
+ // verify that 3 main video slots were allocated
821
+ assert.callCount(fakeReceiveSlotManager.allocateSlot, 3);
822
+ fakeReceiveSlotManager.allocateSlot.getCalls().forEach((call) => {
823
+ const mediaType = call.args[0];
824
+
825
+ assert.strictEqual(mediaType, MediaType.VideoMain);
826
+ });
827
+ });
828
+
702
829
  it('stops all current video remoteMedia instances when switching to new layout', async () => {
703
830
  const audioStopStubs = [];
704
831
  const videoStopStubs = [];