Skip to content

Commit

Permalink
feat(MeetingSdkAdapter): implement video control handler
Browse files Browse the repository at this point in the history
  • Loading branch information
akoushke committed Dec 13, 2019
1 parent 6cf9949 commit 155fc6b
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 31 deletions.
68 changes: 44 additions & 24 deletions src/MeetingsSDKAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const EVENT_MEDIA_READY = 'media:ready';
const EVENT_MEDIA_LOCAL_UPDATE = 'adapter:media:local:update';
const JOIN_CONTROL = 'join-meeting';
const AUDIO_CONTROL = 'audio';
const MUTE_VIDEO_CONTROL = 'mute-video';
const VIDEO_CONTROL = 'video';
const MEDIA_TYPE_LOCAL = 'local';
const MEDIA_TYPE_REMOTE_AUDIO = 'remoteAudio';
const MEDIA_TYPE_REMOTE_VIDEO = 'remoteVideo';
Expand Down Expand Up @@ -37,10 +37,10 @@ export default class MeetingsSDKAdapter extends MeetingsAdapter {
display: this.audioControl.bind(this),
};

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

Expand Down Expand Up @@ -286,17 +286,30 @@ export default class MeetingsSDKAdapter extends MeetingsAdapter {
* @param {string} ID ID of the meeting to mute video
* @memberof MeetingsSDKAdapter
*/
async muteVideoMeeting(ID) {
async handleLocalVideo(ID) {
const sdkMeeting = this.fetchMeeting(ID);

try {
await sdkMeeting.muteVideo();
let videoEnabled = this.meetings[ID].localVideo.getVideoTracks()[0].enabled;

// Due to SDK limitation around promises, we need to emit a custom event for video mute action
sdkMeeting.emit(EVENT_MEDIA_LOCAL_UPDATE, {control: MUTE_VIDEO_CONTROL});
if (videoEnabled) {
await sdkMeeting.muteVideo();
} else {
await sdkMeeting.unmuteVideo();
}

// re-assign the variable after the mute/unmute actions
videoEnabled = this.meetings[ID].localVideo.getVideoTracks()[0].enabled;

// 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: videoEnabled,
});
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Unable to mute video for meeting "${ID}"`, error);
console.error(`Unable to update local video settings for meeting "${ID}"`, error);
}
}

Expand All @@ -307,32 +320,39 @@ export default class MeetingsSDKAdapter extends MeetingsAdapter {
* @returns {Observable.<MeetingControlDisplay>}
* @memberof MeetingJSONAdapter
*/
muteVideoControl(ID) {
videoControl(ID) {
const sdkMeeting = this.fetchMeeting(ID);
const muted = {
ID: MUTE_VIDEO_CONTROL,
icon: 'camera',
tooltip: 'Mute',
ID: VIDEO_CONTROL,
icon: 'camera-muted',
tooltip: 'Start video',
state: MeetingControlState.ACTIVE,
text: null,
};
const unmuted = {
ID: VIDEO_CONTROL,
icon: 'camera',
tooltip: 'Stop video',
state: MeetingControlState.INACTIVE,
text: null,
};

const getDisplayData$ = Observable.create((observer) => {
if (sdkMeeting) {
observer.next(muted);
observer.next(unmuted);
} else {
observer.error(new Error(`Could not find meeting with ID "${ID}" to mute video on`));
observer.error(new Error(`Could not find meeting with ID "${ID}" to add video control`));
}

observer.complete();
});

const muteEvent$ = fromEvent(sdkMeeting, EVENT_MEDIA_LOCAL_UPDATE).pipe(
filter((event) => event.control === MUTE_VIDEO_CONTROL),
map(() => ({...muted, state: MeetingControlState.INACTIVE}))
const localMediaUpdateEvent$ = fromEvent(sdkMeeting, EVENT_MEDIA_LOCAL_UPDATE).pipe(
filter((event) => event.control === VIDEO_CONTROL),
map(({state}) => (state ? unmuted : muted))
);

return concat(getDisplayData$, muteEvent$);
return concat(getDisplayData$, localMediaUpdateEvent$);
}

/**
Expand All @@ -356,19 +376,19 @@ export default class MeetingsSDKAdapter extends MeetingsAdapter {
});

// Listen to attach mediaStream source objects to the existing meeting
const meetingWithReadyEvent$ = fromEvent(sdkMeeting, EVENT_MEDIA_READY).pipe(
const meetingWithMediaReadyEvent$ = fromEvent(sdkMeeting, EVENT_MEDIA_READY).pipe(
filter((event) => MEDIA_EVENT_TYPES.includes(event.type)),
map((event) => this.attachMedia(ID, event)),
map(() => this.meetings[ID])
);

// Listen to mute event to return the meeting object
const meetingWithLocalMuteEvents$ = fromEvent(sdkMeeting, EVENT_MEDIA_LOCAL_UPDATE).pipe(
// Listen to update event to return the meeting object
const meetingWithLocalUpdateEvents$ = fromEvent(sdkMeeting, EVENT_MEDIA_LOCAL_UPDATE).pipe(
map(() => this.meetings[ID])
);

// Merge all event observables to update the existing meeting object simultaneously
const meetingsWithEvents$ = merge(meetingWithReadyEvent$, meetingWithLocalMuteEvents$);
const meetingsWithEvents$ = merge(meetingWithMediaReadyEvent$, meetingWithLocalUpdateEvents$);

const getMeetingWithEvents$ = getMeeting$.pipe(
meetingsWithEvents$,
Expand Down
65 changes: 58 additions & 7 deletions src/MeetingsSDKAdapter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,15 @@ describe('Meetings SDK Adapter', () => {
);
});
});
describe('muteVideoControl()', () => {

describe('videoControl()', () => {
test('returns the display data of a meeting control in a proper shape', (done) => {
meetingSDKAdapter.muteVideoControl(meetingID).subscribe((dataDisplay) => {
meetingSDKAdapter.videoControl(meetingID).subscribe((dataDisplay) => {
expect(dataDisplay).toMatchObject({
ID: 'mute-video',
ID: 'video',
icon: 'camera',
tooltip: 'Mute',
state: 'active',
tooltip: 'Stop video',
state: 'inactive',
text: null,
});
done();
Expand All @@ -233,16 +234,66 @@ describe('Meetings SDK Adapter', () => {
test('throws errors if sdk meeting object is not defined', (done) => {
meetingSDKAdapter.fetchMeeting = jest.fn();

meetingSDKAdapter.muteVideoControl(meetingID).subscribe(
meetingSDKAdapter.videoControl(meetingID).subscribe(
() => {},
(error) => {
expect(error.message).toBe('Could not find meeting with ID "meetingID" to mute video on');
expect(error.message).toBe('Could not find meeting with ID "meetingID" to add video control');
done();
}
);
});
});

describe('handleLocalVideo()', () => {
beforeEach(() => {
meetingSDKAdapter.meetings[meetingID] = {
...meeting,
localVideo: {
getVideoTracks: jest.fn(() => [{enabled: true}]),
},
};
});

test('mutes video if the the video track is enabled', () => {
meetingSDKAdapter.handleLocalVideo(meetingID);
expect(mockSDKMeeting.muteVideo).toHaveBeenCalled();
});

test('emits the custom event after muting the video track', () => {
meetingSDKAdapter.handleLocalVideo(meetingID);
expect(mockSDKMeeting.emit).toHaveBeenCalledWith('adapter:media:local:update', {
control: 'video',
state: true,
});
});

test('unmutes video if the the video track is disabled', () => {
meetingSDKAdapter.meetings[meetingID].localVideo.getVideoTracks = jest.fn(() => [{enabled: false}]);
meetingSDKAdapter.handleLocalVideo(meetingID);
expect(mockSDKMeeting.unmuteVideo).toHaveBeenCalled();
});

test('emits the custom event after unmuting the video track', () => {
meetingSDKAdapter.meetings[meetingID].localVideo.getVideoTracks = jest.fn(() => [{enabled: false}]);
meetingSDKAdapter.handleLocalVideo(meetingID);
expect(mockSDKMeeting.emit).toHaveBeenCalledWith('adapter:media:local:update', {
control: 'video',
state: false,
});
});

test('throws error if video control is not handled properly', async () => {
mockSDKMeeting.muteVideo = jest.fn(() => Promise.reject());
global.console.error = jest.fn();
await meetingSDKAdapter.handleLocalVideo(meetingID);

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

describe('getMeeting()', () => {
test('returns a meeting in a proper shape', (done) => {
meetingSDKAdapter
Expand Down
2 changes: 2 additions & 0 deletions src/__mocks__/sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ export const mockSDKMeeting = {
emit: jest.fn(() => Promise.resolve()),
getMediaStreams: jest.fn(() => Promise.resolve(['localStream', 'localShare'])),
muteAudio: jest.fn(() => Promise.resolve()),
muteVideo: jest.fn(() => Promise.resolve()),
register: jest.fn(() => Promise.resolve()),
syncMeetings: jest.fn(() => Promise.resolve()),
unregister: jest.fn(() => Promise.resolve()),
join: jest.fn(() => Promise.resolve()),
unmuteAudio: jest.fn(() => Promise.resolve()),
unmuteVideo: jest.fn(() => Promise.resolve()),
};

/**
Expand Down

0 comments on commit 155fc6b

Please sign in to comment.