Skip to content

Commit 6e5b2a2

Browse files
patricia0817cipak
authored andcommitted
feat(MeetingsSdkAdapter): move video control in a separate file
1 parent 7c1d373 commit 6e5b2a2

File tree

5 files changed

+233
-165
lines changed

5 files changed

+233
-165
lines changed

src/MeetingsSDKAdapter.js

Lines changed: 30 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
import {
1212
catchError,
1313
concatMap,
14-
distinctUntilChanged,
1514
filter,
1615
map,
1716
publishReplay,
@@ -30,6 +29,7 @@ import SettingsControl from './MeetingsSDKAdapter/controls/SettingsControl';
3029
import SwitchCameraControl from './MeetingsSDKAdapter/controls/SwitchCameraControl';
3130
import SwitchMicrophoneControl from './MeetingsSDKAdapter/controls/SwitchMicrophoneControl';
3231
import SwitchSpeakerControl from './MeetingsSDKAdapter/controls/SwitchSpeakerControl';
32+
import VideoControl from './MeetingsSDKAdapter/controls/VideoControl';
3333
import {chainWith, deepMerge} from './utils';
3434

3535
// TODO: Figure out how to import JS Doc definitions and remove duplication.
@@ -120,11 +120,7 @@ export default class MeetingsSDKAdapter extends MeetingsAdapter {
120120

121121
this.meetingControls[AUDIO_CONTROL] = new AudioControl(this, AUDIO_CONTROL);
122122

123-
this.meetingControls[VIDEO_CONTROL] = {
124-
ID: VIDEO_CONTROL,
125-
action: this.handleLocalVideo.bind(this),
126-
display: this.videoControl.bind(this),
127-
};
123+
this.meetingControls[VIDEO_CONTROL] = new VideoControl(this, VIDEO_CONTROL);
128124

129125
this.meetingControls[SHARE_CONTROL] = {
130126
ID: SHARE_CONTROL,
@@ -660,91 +656,45 @@ export default class MeetingsSDKAdapter extends MeetingsAdapter {
660656
* @param {string} ID ID of the meeting to mute video
661657
*/
662658
async handleLocalVideo(ID) {
663-
const sdkMeeting = this.fetchMeeting(ID);
664-
665659
try {
666-
const isInSession = !!this.meetings[ID].remoteVideo;
667-
const noVideo = !this.meetings[ID].disabledLocalVideo && !this.meetings[ID].localVideo.stream;
668-
const videoEnabled = !!this.meetings[ID].localVideo.stream;
669-
let state;
670-
671-
if (noVideo) {
672-
state = MeetingControlState.DISABLED;
673-
} else if (videoEnabled) {
674-
// Mute the video only if there is an active meeting
675-
if (isInSession) {
676-
await sdkMeeting.muteVideo();
677-
}
660+
await this.updateMeeting(ID, async (meeting, sdkMeeting) => {
661+
const isInSession = !!meeting.remoteVideo;
662+
const videoEnabled = !!meeting.localVideo.stream;
663+
const videoDisabled = !!meeting.disabledLocalVideo;
664+
let updates;
678665

679-
// Store the current local video stream to avoid an extra request call
680-
this.meetings[ID].disabledLocalVideo = this.meetings[ID].localVideo.stream;
681-
this.meetings[ID].localVideo.stream = null;
682-
state = MeetingControlState.INACTIVE;
683-
} else {
684-
// Unmute the video only if there is an active meeting
685-
if (isInSession) {
686-
await sdkMeeting.unmuteVideo();
687-
}
666+
if (videoEnabled) {
667+
// Mute the video only if there is an active meeting
668+
if (isInSession) {
669+
await sdkMeeting.muteVideo();
670+
}
688671

689-
// Retrieve the stored local video stream
690-
this.meetings[ID].localVideo.stream = this.meetings[ID].disabledLocalVideo;
691-
this.meetings[ID].disabledLocalVideo = null;
692-
state = MeetingControlState.ACTIVE;
693-
}
672+
// Store the current local video stream to avoid an extra request call
673+
updates = {
674+
localVideo: {stream: null},
675+
disabledLocalVideo: meeting.localVideo.stream,
676+
};
677+
} else if (videoDisabled) {
678+
// Unmute the video only if there is an active meeting
679+
if (isInSession) {
680+
await sdkMeeting.unmuteVideo();
681+
}
694682

695-
// Due to SDK limitation around local media updates,
696-
// we need to emit a custom event for video mute updates
697-
sdkMeeting.emit(EVENT_MEDIA_LOCAL_UPDATE, {
698-
control: VIDEO_CONTROL,
699-
state,
683+
// Retrieve the stored local video stream
684+
updates = {
685+
localVideo: {stream: meeting.disabledLocalVideo},
686+
disabledLocalVideo: null,
687+
};
688+
}
689+
690+
return updates;
700691
});
701692
} catch (error) {
702693
// eslint-disable-next-line no-console
703694
console.error(`Unable to update local video settings for meeting "${ID}"`, error);
704695
}
705696
}
706697

707-
/**
708-
* Returns an observable that emits the display data of a mute meeting video control.
709-
*
710-
* @private
711-
* @param {string} ID ID of the meeting to mute video
712-
* @returns {Observable.<MeetingControlDisplay>} Observable stream that emits display data of the video control
713-
*/
714-
videoControl(ID) {
715-
const muted = {
716-
ID: VIDEO_CONTROL,
717-
type: 'TOGGLE',
718-
icon: 'camera-muted_28',
719-
tooltip: 'Start video',
720-
state: MeetingControlState.ACTIVE,
721-
text: 'Start video',
722-
};
723-
const unmuted = {
724-
ID: VIDEO_CONTROL,
725-
type: 'TOGGLE',
726-
icon: 'camera-muted_28',
727-
tooltip: 'Stop video',
728-
state: MeetingControlState.INACTIVE,
729-
text: 'Stop video',
730-
};
731-
const disabled = {
732-
ID: VIDEO_CONTROL,
733-
type: 'TOGGLE',
734-
icon: 'camera-muted_28',
735-
tooltip: 'No camera available',
736-
state: MeetingControlState.DISABLED,
737-
text: 'No camera',
738-
};
739-
740-
return this.getMeeting(ID).pipe(
741-
map(({localVideo: {stream}, disabledLocalVideo}) => (
742-
(stream && unmuted) || (disabledLocalVideo && muted) || disabled
743-
)),
744-
distinctUntilChanged(),
745-
);
746-
}
747-
748698
/**
749699
* Attempts to start/stop screen sharing to the given meeting ID.
750700
* If successful, a sharing start/stop event is dispatched.

src/MeetingsSDKAdapter.test.js

Lines changed: 94 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -601,116 +601,125 @@ describe('Meetings SDK Adapter', () => {
601601
});
602602
});
603603

604-
describe('videoControl()', () => {
605-
test('returns the display data of a meeting control in a proper shape', (done) => {
606-
meetingsSDKAdapter.videoControl(meetingID).subscribe((dataDisplay) => {
607-
expect(dataDisplay).toMatchObject({
608-
ID: 'mute-video',
609-
type: 'TOGGLE',
610-
icon: 'camera-muted_28',
611-
tooltip: 'No camera available',
612-
state: 'disabled',
613-
text: 'No camera',
614-
});
615-
done();
604+
describe('handleLocalVideo()', () => {
605+
describe('video is unmuted', () => {
606+
beforeEach(() => {
607+
meetingsSDKAdapter.meetings[meetingID] = {
608+
...meeting,
609+
localVideo: {
610+
stream: mockSDKMediaStreams.localVideo,
611+
permission: 'ALLOWED',
612+
},
613+
disabledLocalVideo: null,
614+
remoteVideo: {},
615+
};
616616
});
617-
});
618617

619-
test('throws errors if sdk meeting object is not defined', (done) => {
620-
meetingsSDKAdapter.videoControl('inexistent').subscribe(
621-
() => {},
622-
(error) => {
623-
expect(error.message).toBe('Could not find meeting with ID "inexistent"');
624-
done();
625-
},
626-
);
627-
});
628-
});
618+
test('does not call sdk muteVideo() if the meeting is inactive', async () => {
619+
meetingsSDKAdapter.meetings[meetingID].remoteVideo = null;
620+
await meetingsSDKAdapter.handleLocalVideo(meetingID);
629621

630-
describe('handleLocalVideo()', () => {
631-
beforeEach(() => {
632-
meetingsSDKAdapter.meetings[meetingID] = {
633-
...meeting,
634-
localVideo: {
635-
stream: mockSDKMediaStreams.localVideo,
636-
},
637-
remoteVideo: {},
638-
};
639-
});
622+
expect(mockSDKMeeting.muteVideo).not.toHaveBeenCalled();
623+
});
640624

641-
test('skips muting video if there is an inactive meeting', async () => {
642-
meetingsSDKAdapter.meetings[meetingID].remoteVideo = null;
643-
await meetingsSDKAdapter.handleLocalVideo(meetingID);
625+
test('calls sdk muteVideo() if the meeting is active', async () => {
626+
await meetingsSDKAdapter.handleLocalVideo(meetingID);
644627

645-
expect(mockSDKMeeting.muteVideo).not.toHaveBeenCalled();
646-
});
628+
expect(mockSDKMeeting.muteVideo).toHaveBeenCalled();
629+
});
647630

648-
test('skips unmuting video if there is an inactive meeting', async () => {
649-
meetingsSDKAdapter.meetings[meetingID].remoteVideo = null;
650-
await meetingsSDKAdapter.handleLocalVideo(meetingID);
631+
test('updates the meeting object to have video muted', async () => {
632+
await meetingsSDKAdapter.handleLocalVideo(meetingID);
651633

652-
expect(mockSDKMeeting.unmuteVideo).not.toHaveBeenCalled();
653-
});
634+
expect(meetingsSDKAdapter.meetings[meetingID].localVideo.stream).toBeNull();
635+
expect(meetingsSDKAdapter.meetings[meetingID].disabledLocalVideo).toMatchMediaStream(
636+
mockSDKMediaStreams.localVideo,
637+
);
638+
});
654639

655-
test('mutes video if the the video track is enabled', async () => {
656-
await meetingsSDKAdapter.handleLocalVideo(meetingID);
640+
test('emits a meeting updated event', async () => {
641+
await meetingsSDKAdapter.handleLocalVideo(meetingID);
657642

658-
expect(mockSDKMeeting.muteVideo).toHaveBeenCalled();
659-
});
643+
expect(mockSDKMeeting.emit).toHaveBeenCalledTimes(1);
644+
expect(mockSDKMeeting.emit.mock.calls[0][0]).toBe('adapter:meeting:updated');
645+
expect(mockSDKMeeting.emit.mock.calls[0][1]).toMatchObject({
646+
localVideo: {stream: null},
647+
disabledLocalVideo: mockSDKMediaStreams.localVideo,
648+
});
649+
});
650+
651+
test('logs error if the sdk muteVideo() rejects with an error', async () => {
652+
const error = new Error('sdk error');
660653

661-
test('localVideo property should be null once the video track is muted', async () => {
662-
await meetingsSDKAdapter.handleLocalVideo(meetingID);
654+
mockSDKMeeting.muteVideo = jest.fn(() => Promise.reject(error));
655+
global.console.error = jest.fn();
656+
await meetingsSDKAdapter.handleLocalVideo(meetingID);
663657

664-
expect(meetingsSDKAdapter.meetings[meetingID].localVideo.stream).toBeNull();
658+
expect(global.console.error).toHaveBeenCalledWith(
659+
'Unable to update local video settings for meeting "meetingID"',
660+
error,
661+
);
662+
});
665663
});
666664

667-
test('emits the custom event after muting the video track', async () => {
668-
await meetingsSDKAdapter.handleLocalVideo(meetingID);
665+
describe('video is muted', () => {
666+
beforeEach(() => {
667+
meetingsSDKAdapter.meetings[meetingID] = {
668+
...meeting,
669+
localVideo: {
670+
stream: null,
671+
permission: undefined,
672+
},
673+
disabledLocalVideo: mockSDKMediaStreams.localVideo,
674+
remoteVideo: {},
675+
};
676+
});
677+
678+
test('does not call sdk unmuteVideo() if the meeting is inactive', async () => {
679+
meetingsSDKAdapter.meetings[meetingID].remoteVideo = null;
680+
await meetingsSDKAdapter.handleLocalVideo(meetingID);
669681

670-
expect(mockSDKMeeting.emit).toHaveBeenCalledWith('adapter:media:local:update', {
671-
control: 'mute-video',
672-
state: 'inactive',
682+
expect(mockSDKMeeting.unmuteVideo).not.toHaveBeenCalled();
673683
});
674-
});
675684

676-
test('unmutes video if the video track is disabled', async () => {
677-
meetingsSDKAdapter.meetings[meetingID].localVideo.stream = null;
678-
await meetingsSDKAdapter.handleLocalVideo(meetingID);
685+
test('calls sdk unmuteVideo() if the meeting is active', async () => {
686+
await meetingsSDKAdapter.handleLocalVideo(meetingID);
679687

680-
expect(mockSDKMeeting.emit).toHaveBeenCalledWith('adapter:media:local:update', {
681-
control: 'mute-video',
682-
state: 'disabled',
688+
expect(mockSDKMeeting.unmuteVideo).toHaveBeenCalled();
683689
});
684-
});
685690

686-
test('localVideo property should be defined once the video track is unmuted', async () => {
687-
meetingsSDKAdapter.meetings[meetingID].localVideo.stream = null;
688-
meetingsSDKAdapter.meetings[meetingID].disabledLocalVideo = mockSDKMediaStreams.localVideo;
689-
await meetingsSDKAdapter.handleLocalVideo(meetingID);
691+
test('updates the meeting object to have video unmuted', async () => {
692+
await meetingsSDKAdapter.handleLocalVideo(meetingID);
690693

691-
expect(meetingsSDKAdapter.meetings[meetingID].localVideo.stream)
692-
.toMatchMediaStream(mockSDKMediaStreams.localVideo);
693-
});
694+
expect(meetingsSDKAdapter.meetings[meetingID].localVideo.stream)
695+
.toMatchMediaStream(mockSDKMediaStreams.localVideo);
696+
expect(meetingsSDKAdapter.meetings[meetingID].disabledLocalVideo).toBeNull();
697+
});
694698

695-
test('emits the custom event after unmuting the video track', async () => {
696-
meetingsSDKAdapter.meetings[meetingID].localVideo.stream = null;
697-
await meetingsSDKAdapter.handleLocalVideo(meetingID);
699+
test('emits a meeting updated event', async () => {
700+
await meetingsSDKAdapter.handleLocalVideo(meetingID);
698701

699-
expect(mockSDKMeeting.emit).toHaveBeenCalledWith('adapter:media:local:update', {
700-
control: 'mute-video',
701-
state: 'disabled',
702+
expect(mockSDKMeeting.emit).toHaveBeenCalledTimes(1);
703+
expect(mockSDKMeeting.emit.mock.calls[0][0]).toBe('adapter:meeting:updated');
704+
expect(mockSDKMeeting.emit.mock.calls[0][1]).toMatchObject({
705+
localVideo: {
706+
stream: mockSDKMediaStreams.localVideo,
707+
},
708+
});
702709
});
703-
});
704710

705-
test('throws error if video control is not handled properly', async () => {
706-
mockSDKMeeting.muteVideo = jest.fn(() => Promise.reject());
707-
global.console.error = jest.fn();
708-
await meetingsSDKAdapter.handleLocalVideo(meetingID);
711+
test('logs error if the sdk unmuteVideo() rejects with an error', async () => {
712+
const error = new Error('sdk error');
709713

710-
expect(global.console.error).toHaveBeenCalledWith(
711-
'Unable to update local video settings for meeting "meetingID"',
712-
undefined,
713-
);
714+
mockSDKMeeting.unmuteVideo = jest.fn(() => Promise.reject(error));
715+
global.console.error = jest.fn();
716+
await meetingsSDKAdapter.handleLocalVideo(meetingID);
717+
718+
expect(global.console.error).toHaveBeenCalledWith(
719+
'Unable to update local video settings for meeting "meetingID"',
720+
error,
721+
);
722+
});
714723
});
715724
});
716725

0 commit comments

Comments
 (0)