Skip to content

Commit

Permalink
feat(MeetingsSdkAdapter): move video control in a separate file
Browse files Browse the repository at this point in the history
  • Loading branch information
patricia0817 authored and cipak committed Sep 8, 2021
1 parent 7c1d373 commit 6e5b2a2
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 165 deletions.
110 changes: 30 additions & 80 deletions src/MeetingsSDKAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
import {
catchError,
concatMap,
distinctUntilChanged,
filter,
map,
publishReplay,
Expand All @@ -30,6 +29,7 @@ import SettingsControl from './MeetingsSDKAdapter/controls/SettingsControl';
import SwitchCameraControl from './MeetingsSDKAdapter/controls/SwitchCameraControl';
import SwitchMicrophoneControl from './MeetingsSDKAdapter/controls/SwitchMicrophoneControl';
import SwitchSpeakerControl from './MeetingsSDKAdapter/controls/SwitchSpeakerControl';
import VideoControl from './MeetingsSDKAdapter/controls/VideoControl';
import {chainWith, deepMerge} from './utils';

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

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

this.meetingControls[VIDEO_CONTROL] = {
ID: VIDEO_CONTROL,
action: this.handleLocalVideo.bind(this),
display: this.videoControl.bind(this),
};
this.meetingControls[VIDEO_CONTROL] = new VideoControl(this, VIDEO_CONTROL);

this.meetingControls[SHARE_CONTROL] = {
ID: SHARE_CONTROL,
Expand Down Expand Up @@ -660,91 +656,45 @@ export default class MeetingsSDKAdapter extends MeetingsAdapter {
* @param {string} ID ID of the meeting to mute video
*/
async handleLocalVideo(ID) {
const sdkMeeting = this.fetchMeeting(ID);

try {
const isInSession = !!this.meetings[ID].remoteVideo;
const noVideo = !this.meetings[ID].disabledLocalVideo && !this.meetings[ID].localVideo.stream;
const videoEnabled = !!this.meetings[ID].localVideo.stream;
let state;

if (noVideo) {
state = MeetingControlState.DISABLED;
} else if (videoEnabled) {
// Mute the video only if there is an active meeting
if (isInSession) {
await sdkMeeting.muteVideo();
}
await this.updateMeeting(ID, async (meeting, sdkMeeting) => {
const isInSession = !!meeting.remoteVideo;
const videoEnabled = !!meeting.localVideo.stream;
const videoDisabled = !!meeting.disabledLocalVideo;
let updates;

// Store the current local video stream to avoid an extra request call
this.meetings[ID].disabledLocalVideo = this.meetings[ID].localVideo.stream;
this.meetings[ID].localVideo.stream = null;
state = MeetingControlState.INACTIVE;
} else {
// Unmute the video only if there is an active meeting
if (isInSession) {
await sdkMeeting.unmuteVideo();
}
if (videoEnabled) {
// Mute the video only if there is an active meeting
if (isInSession) {
await sdkMeeting.muteVideo();
}

// Retrieve the stored local video stream
this.meetings[ID].localVideo.stream = this.meetings[ID].disabledLocalVideo;
this.meetings[ID].disabledLocalVideo = null;
state = MeetingControlState.ACTIVE;
}
// Store the current local video stream to avoid an extra request call
updates = {
localVideo: {stream: null},
disabledLocalVideo: meeting.localVideo.stream,
};
} else if (videoDisabled) {
// Unmute the video only if there is an active meeting
if (isInSession) {
await sdkMeeting.unmuteVideo();
}

// Due to SDK limitation around local media updates,
// we need to emit a custom event for video mute updates
sdkMeeting.emit(EVENT_MEDIA_LOCAL_UPDATE, {
control: VIDEO_CONTROL,
state,
// Retrieve the stored local video stream
updates = {
localVideo: {stream: meeting.disabledLocalVideo},
disabledLocalVideo: null,
};
}

return updates;
});
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Unable to update local video settings for meeting "${ID}"`, error);
}
}

/**
* Returns an observable that emits the display data of a mute meeting video control.
*
* @private
* @param {string} ID ID of the meeting to mute video
* @returns {Observable.<MeetingControlDisplay>} Observable stream that emits display data of the video control
*/
videoControl(ID) {
const muted = {
ID: VIDEO_CONTROL,
type: 'TOGGLE',
icon: 'camera-muted_28',
tooltip: 'Start video',
state: MeetingControlState.ACTIVE,
text: 'Start video',
};
const unmuted = {
ID: VIDEO_CONTROL,
type: 'TOGGLE',
icon: 'camera-muted_28',
tooltip: 'Stop video',
state: MeetingControlState.INACTIVE,
text: 'Stop video',
};
const disabled = {
ID: VIDEO_CONTROL,
type: 'TOGGLE',
icon: 'camera-muted_28',
tooltip: 'No camera available',
state: MeetingControlState.DISABLED,
text: 'No camera',
};

return this.getMeeting(ID).pipe(
map(({localVideo: {stream}, disabledLocalVideo}) => (
(stream && unmuted) || (disabledLocalVideo && muted) || disabled
)),
distinctUntilChanged(),
);
}

/**
* Attempts to start/stop screen sharing to the given meeting ID.
* If successful, a sharing start/stop event is dispatched.
Expand Down
179 changes: 94 additions & 85 deletions src/MeetingsSDKAdapter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -601,116 +601,125 @@ describe('Meetings SDK Adapter', () => {
});
});

describe('videoControl()', () => {
test('returns the display data of a meeting control in a proper shape', (done) => {
meetingsSDKAdapter.videoControl(meetingID).subscribe((dataDisplay) => {
expect(dataDisplay).toMatchObject({
ID: 'mute-video',
type: 'TOGGLE',
icon: 'camera-muted_28',
tooltip: 'No camera available',
state: 'disabled',
text: 'No camera',
});
done();
describe('handleLocalVideo()', () => {
describe('video is unmuted', () => {
beforeEach(() => {
meetingsSDKAdapter.meetings[meetingID] = {
...meeting,
localVideo: {
stream: mockSDKMediaStreams.localVideo,
permission: 'ALLOWED',
},
disabledLocalVideo: null,
remoteVideo: {},
};
});
});

test('throws errors if sdk meeting object is not defined', (done) => {
meetingsSDKAdapter.videoControl('inexistent').subscribe(
() => {},
(error) => {
expect(error.message).toBe('Could not find meeting with ID "inexistent"');
done();
},
);
});
});
test('does not call sdk muteVideo() if the meeting is inactive', async () => {
meetingsSDKAdapter.meetings[meetingID].remoteVideo = null;
await meetingsSDKAdapter.handleLocalVideo(meetingID);

describe('handleLocalVideo()', () => {
beforeEach(() => {
meetingsSDKAdapter.meetings[meetingID] = {
...meeting,
localVideo: {
stream: mockSDKMediaStreams.localVideo,
},
remoteVideo: {},
};
});
expect(mockSDKMeeting.muteVideo).not.toHaveBeenCalled();
});

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

expect(mockSDKMeeting.muteVideo).not.toHaveBeenCalled();
});
expect(mockSDKMeeting.muteVideo).toHaveBeenCalled();
});

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

expect(mockSDKMeeting.unmuteVideo).not.toHaveBeenCalled();
});
expect(meetingsSDKAdapter.meetings[meetingID].localVideo.stream).toBeNull();
expect(meetingsSDKAdapter.meetings[meetingID].disabledLocalVideo).toMatchMediaStream(
mockSDKMediaStreams.localVideo,
);
});

test('mutes video if the the video track is enabled', async () => {
await meetingsSDKAdapter.handleLocalVideo(meetingID);
test('emits a meeting updated event', async () => {
await meetingsSDKAdapter.handleLocalVideo(meetingID);

expect(mockSDKMeeting.muteVideo).toHaveBeenCalled();
});
expect(mockSDKMeeting.emit).toHaveBeenCalledTimes(1);
expect(mockSDKMeeting.emit.mock.calls[0][0]).toBe('adapter:meeting:updated');
expect(mockSDKMeeting.emit.mock.calls[0][1]).toMatchObject({
localVideo: {stream: null},
disabledLocalVideo: mockSDKMediaStreams.localVideo,
});
});

test('logs error if the sdk muteVideo() rejects with an error', async () => {
const error = new Error('sdk error');

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

expect(meetingsSDKAdapter.meetings[meetingID].localVideo.stream).toBeNull();
expect(global.console.error).toHaveBeenCalledWith(
'Unable to update local video settings for meeting "meetingID"',
error,
);
});
});

test('emits the custom event after muting the video track', async () => {
await meetingsSDKAdapter.handleLocalVideo(meetingID);
describe('video is muted', () => {
beforeEach(() => {
meetingsSDKAdapter.meetings[meetingID] = {
...meeting,
localVideo: {
stream: null,
permission: undefined,
},
disabledLocalVideo: mockSDKMediaStreams.localVideo,
remoteVideo: {},
};
});

test('does not call sdk unmuteVideo() if the meeting is inactive', async () => {
meetingsSDKAdapter.meetings[meetingID].remoteVideo = null;
await meetingsSDKAdapter.handleLocalVideo(meetingID);

expect(mockSDKMeeting.emit).toHaveBeenCalledWith('adapter:media:local:update', {
control: 'mute-video',
state: 'inactive',
expect(mockSDKMeeting.unmuteVideo).not.toHaveBeenCalled();
});
});

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

expect(mockSDKMeeting.emit).toHaveBeenCalledWith('adapter:media:local:update', {
control: 'mute-video',
state: 'disabled',
expect(mockSDKMeeting.unmuteVideo).toHaveBeenCalled();
});
});

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

expect(meetingsSDKAdapter.meetings[meetingID].localVideo.stream)
.toMatchMediaStream(mockSDKMediaStreams.localVideo);
});
expect(meetingsSDKAdapter.meetings[meetingID].localVideo.stream)
.toMatchMediaStream(mockSDKMediaStreams.localVideo);
expect(meetingsSDKAdapter.meetings[meetingID].disabledLocalVideo).toBeNull();
});

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

expect(mockSDKMeeting.emit).toHaveBeenCalledWith('adapter:media:local:update', {
control: 'mute-video',
state: 'disabled',
expect(mockSDKMeeting.emit).toHaveBeenCalledTimes(1);
expect(mockSDKMeeting.emit.mock.calls[0][0]).toBe('adapter:meeting:updated');
expect(mockSDKMeeting.emit.mock.calls[0][1]).toMatchObject({
localVideo: {
stream: mockSDKMediaStreams.localVideo,
},
});
});
});

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

expect(global.console.error).toHaveBeenCalledWith(
'Unable to update local video settings for meeting "meetingID"',
undefined,
);
mockSDKMeeting.unmuteVideo = jest.fn(() => Promise.reject(error));
global.console.error = jest.fn();
await meetingsSDKAdapter.handleLocalVideo(meetingID);

expect(global.console.error).toHaveBeenCalledWith(
'Unable to update local video settings for meeting "meetingID"',
error,
);
});
});
});

Expand Down
Loading

0 comments on commit 6e5b2a2

Please sign in to comment.