diff --git a/src/MeetingsSDKAdapter.js b/src/MeetingsSDKAdapter.js index fb1c5590..a6ae25ff 100644 --- a/src/MeetingsSDKAdapter.js +++ b/src/MeetingsSDKAdapter.js @@ -227,79 +227,87 @@ export default class MeetingsSDKAdapter extends MeetingsAdapter { getStream(ID, mediaDirection, audioVideo) { logger.debug('MEETING', ID, 'getStream()', ['called with', {ID, mediaDirection, audioVideo}]); - return new Observable(async (subscriber) => { + return new Observable((subscriber) => { let ignored = false; let isAsking; - try { - const sdkMeeting = this.fetchMeeting(ID); - - const ignore = () => { - ignored = true; - subscriber.next({permission: 'IGNORED', stream: null}); - subscriber.complete(); - }; + const ignore = () => { + ignored = true; + subscriber.next({permission: 'IGNORED', stream: null}); + subscriber.complete(); + }; - // wait a bit for the prompt to appear before emitting ASKING - isAsking = true; - setTimeout(() => { - if (isAsking) { - // media access promise was neither fulfilled nor rejected, so the browser prompt is probably showing - subscriber.next({permission: 'ASKING', stream: null, ignore}); + // wait a bit for the prompt to appear before emitting ASKING + isAsking = true; + const askingTimeout = setTimeout(() => { + if (isAsking) { + // media access promise was neither fulfilled nor rejected, so the browser prompt is probably showing + subscriber.next({permission: 'ASKING', stream: null, ignore}); + } + }, 2000); + + (async () => { + try { + const sdkMeeting = this.fetchMeeting(ID); + const [localStream] = await sdkMeeting.getMediaStreams(mediaDirection, audioVideo); + const availableDevices = await navigator.mediaDevices.enumerateDevices(); + const [{label: deviceLabel}] = localStream.getTracks(); + const mediaDevice = availableDevices.find((device) => device.label === deviceLabel); + const deviceId = mediaDevice && mediaDevice.deviceId; + + isAsking = false; + clearTimeout(askingTimeout); + + for (const track of localStream.getTracks()) { + if (track.kind === 'video' && !mediaDirection.sendVideo) { + localStream.removeTrack(track); + } + if (track.kind === 'audio' && !mediaDirection.sendAudio) { + localStream.removeTrack(track); + } } - }, 2000); - const [localStream] = await sdkMeeting.getMediaStreams(mediaDirection, audioVideo); - const availableDevices = await navigator.mediaDevices.enumerateDevices(); - const [{label: deviceLabel}] = localStream.getTracks(); - const mediaDevice = availableDevices.find((device) => device.label === deviceLabel); - const deviceId = mediaDevice && mediaDevice.deviceId; - - isAsking = false; - - for (const track of localStream.getTracks()) { - if (track.kind === 'video' && !mediaDirection.sendVideo) { - localStream.removeTrack(track); + if (!ignored) { + subscriber.next({permission: 'ALLOWED', stream: localStream, deviceId}); + subscriber.complete(); } - if (track.kind === 'audio' && !mediaDirection.sendAudio) { - localStream.removeTrack(track); - } - } - - if (!ignored) { - subscriber.next({permission: 'ALLOWED', stream: localStream, deviceId}); - subscriber.complete(); - } - } catch (error) { - isAsking = false; - - if (!ignored) { - let perm; - const ee = error.error; - - logger.error('MEETING', ID, 'getStream()', 'Unable to retrieve local media stream', ee || error); - - if (ee instanceof DOMException) { - if (ee.name === 'NotAllowedError') { - if (ee.message === 'Permission dismissed') { - perm = 'DISMISSED'; - } else if (ee.message === 'Permission denied by system') { + } catch (error) { + isAsking = false; + clearTimeout(askingTimeout); + + if (!ignored) { + let perm; + const ee = error.error; + + logger.error('MEETING', ID, 'getStream()', 'Unable to retrieve local media stream', ee || error); + + if (ee instanceof DOMException) { + if (ee.name === 'NotAllowedError') { + if (ee.message === 'Permission dismissed') { + perm = 'DISMISSED'; + } else if (ee.message === 'Permission denied by system') { + perm = 'DISABLED'; + } else { + perm = 'DENIED'; + } + } else if (ee.name === 'NotReadableError') { perm = 'DISABLED'; } else { - perm = 'DENIED'; + perm = 'ERROR'; } - } else if (ee.name === 'NotReadableError') { - perm = 'DISABLED'; } else { perm = 'ERROR'; } - } else { - perm = 'ERROR'; + subscriber.next({permission: perm, stream: null}); + subscriber.complete(); } - subscriber.next({permission: perm, stream: null}); - subscriber.complete(); } - } + })(); + + return () => { + isAsking = false; + clearTimeout(askingTimeout); + }; }); } @@ -1061,7 +1069,7 @@ export default class MeetingsSDKAdapter extends MeetingsAdapter { const {stream, permission, deviceId} = await this.getStream( ID, {sendVideo: true}, - {video: {deviceId: cameraID}}, + {video: {deviceId: cameraID === 'default' ? 'default' : {exact: cameraID}}}, ).toPromise(); if (stream) { @@ -1092,6 +1100,11 @@ export default class MeetingsSDKAdapter extends MeetingsAdapter { async switchMicrophone(ID, microphoneID) { logger.debug('MEETING', ID, 'switchMicrophone()', ['called with', {ID, microphoneID}]); logger.info('MEETING', ID, 'SWITCH MICROPHONE', `Switching current microphone to microphone with id "${microphoneID}"`); + + const audioConstraints = { + deviceId: microphoneID === 'default' ? 'default' : {exact: microphoneID}, + }; + await this.updateMeeting(ID, async (meeting) => { let updates; @@ -1099,7 +1112,7 @@ export default class MeetingsSDKAdapter extends MeetingsAdapter { const {stream, permission, deviceId} = await this.getStream( ID, {sendAudio: true}, - {audio: {deviceId: microphoneID}}, + {audio: audioConstraints}, ).toPromise(); if (stream) { diff --git a/src/MeetingsSDKAdapter.test.js b/src/MeetingsSDKAdapter.test.js index a3a315cb..de63d7e4 100644 --- a/src/MeetingsSDKAdapter.test.js +++ b/src/MeetingsSDKAdapter.test.js @@ -942,11 +942,33 @@ describe('Meetings SDK Adapter', () => { meetingsSDKAdapter.getStream = jest.fn(() => ({stream: new MediaStream(), deviceId: '', toPromise: () => Promise.resolve({stream: new MediaStream(), permission: 'ALLOWED', deviceId: 'example-camera-id'})})); await meetingsSDKAdapter.switchCamera(meetingID, 'example-camera-id'); + expect(meetingsSDKAdapter.getStream).toHaveBeenCalledWith( + meetingID, + {sendVideo: true}, + {video: {deviceId: {exact: 'example-camera-id'}}}, + ); expect(mockSDKMeeting.emit).toHaveBeenCalledTimes(1); expect(mockSDKMeeting.emit.mock.calls[0][0]).toBe('adapter:meeting:updated'); expect(mockSDKMeeting.emit.mock.calls[0][1]).toMatchObject({cameraID: 'example-camera-id'}); }); + it('uses plain default deviceId constraint for the default alias', async () => { + meetingsSDKAdapter.getStream = jest.fn(() => ({ + toPromise: () => Promise.resolve({ + stream: new MediaStream(), + permission: 'ALLOWED', + deviceId: 'default', + }), + })); + await meetingsSDKAdapter.switchCamera(meetingID, 'default'); + + expect(meetingsSDKAdapter.getStream).toHaveBeenCalledWith( + meetingID, + {sendVideo: true}, + {video: {deviceId: 'default'}}, + ); + }); + it('returns a rejected promise if meeting does not exist', async () => { await expect(meetingsSDKAdapter.switchCamera('inexistent', 'example-camera-id')) .rejects.toThrow('Could not find meeting with ID "inexistent"'); @@ -968,11 +990,33 @@ describe('Meetings SDK Adapter', () => { meetingsSDKAdapter.getStream = jest.fn(() => ({stream: new MediaStream(), deviceId: '', toPromise: () => Promise.resolve({stream: new MediaStream(), permission: 'ALLOWED', deviceId: 'example-microphone-id'})})); await meetingsSDKAdapter.switchMicrophone(meetingID, 'example-microphone-id'); + expect(meetingsSDKAdapter.getStream).toHaveBeenCalledWith( + meetingID, + {sendAudio: true}, + {audio: {deviceId: {exact: 'example-microphone-id'}}}, + ); expect(mockSDKMeeting.emit).toHaveBeenCalledTimes(1); expect(mockSDKMeeting.emit.mock.calls[0][0]).toBe('adapter:meeting:updated'); expect(mockSDKMeeting.emit.mock.calls[0][1]).toMatchObject({microphoneID: 'example-microphone-id'}); }); + it('uses plain default deviceId constraint for the default alias', async () => { + meetingsSDKAdapter.getStream = jest.fn(() => ({ + toPromise: () => Promise.resolve({ + stream: new MediaStream(), + permission: 'ALLOWED', + deviceId: 'default', + }), + })); + await meetingsSDKAdapter.switchMicrophone(meetingID, 'default'); + + expect(meetingsSDKAdapter.getStream).toHaveBeenCalledWith( + meetingID, + {sendAudio: true}, + {audio: {deviceId: 'default'}}, + ); + }); + it('returns a rejected promise if meeting does not exist', async () => { await expect(meetingsSDKAdapter.switchMicrophone('inexistent', 'example-microphone-id')) .rejects.toThrow('Could not find meeting with ID "inexistent"'); diff --git a/src/MeetingsSDKAdapter/controls/SwitchCameraControl.js b/src/MeetingsSDKAdapter/controls/SwitchCameraControl.js index 6789e38b..9bb8d9d6 100644 --- a/src/MeetingsSDKAdapter/controls/SwitchCameraControl.js +++ b/src/MeetingsSDKAdapter/controls/SwitchCameraControl.js @@ -14,11 +14,16 @@ export default class SwitchCameraControl extends MeetingControl { /** * Calls the action of the switch camera control. * - * @param {string} meetingID Meeting ID - * @param {string} cameraID Id of the camera to switch to + * @param {object} context Meeting control context + * @param {string} context.meetingID Meeting ID + * @param {string} [context.cameraId] Id of the camera to switch to + * @param {string} [deviceId] Id of the camera to switch to (passed by @webex/components) */ - async action({meetingID, cameraId}) { - logger.debug('MEETING', meetingID, 'SwitchCameraControl::action()', ['called with', {meetingID}]); + async action(context, deviceId) { + const {meetingID} = context; + const cameraId = context.cameraId != null ? context.cameraId : deviceId; + + logger.debug('MEETING', meetingID, 'SwitchCameraControl::action()', ['called with', {meetingID, cameraId}]); await this.adapter.switchCamera(meetingID, cameraId); } diff --git a/src/MeetingsSDKAdapter/controls/SwitchCameraControl.test.js b/src/MeetingsSDKAdapter/controls/SwitchCameraControl.test.js index 04cdac99..49d87e82 100644 --- a/src/MeetingsSDKAdapter/controls/SwitchCameraControl.test.js +++ b/src/MeetingsSDKAdapter/controls/SwitchCameraControl.test.js @@ -49,5 +49,12 @@ describe('Switch Camera Control', () => { expect(meetingsSDKAdapter.switchCamera).toHaveBeenCalledTimes(1); expect(meetingsSDKAdapter.switchCamera).toHaveBeenCalledWith(meetingID, 'cameraID'); }); + + it('calls switchCamera() with device ID passed as second argument', async () => { + meetingsSDKAdapter.switchCamera = jest.fn(); + await meetingsSDKAdapter.meetingControls['switch-camera'].action({meetingID}, 'cameraID'); + expect(meetingsSDKAdapter.switchCamera).toHaveBeenCalledTimes(1); + expect(meetingsSDKAdapter.switchCamera).toHaveBeenCalledWith(meetingID, 'cameraID'); + }); }); }); diff --git a/src/MeetingsSDKAdapter/controls/SwitchMicrophoneControl.js b/src/MeetingsSDKAdapter/controls/SwitchMicrophoneControl.js index fb01e4e1..83bc4b69 100644 --- a/src/MeetingsSDKAdapter/controls/SwitchMicrophoneControl.js +++ b/src/MeetingsSDKAdapter/controls/SwitchMicrophoneControl.js @@ -15,11 +15,16 @@ export default class SwitchMicrophoneControl extends MeetingControl { /** * Switches the microphone control. * - * @param {string} meetingID Meeting ID - * @param {string} microphoneID Id of the microphone to switch to + * @param {object} context Meeting control context + * @param {string} context.meetingID Meeting ID + * @param {string} [context.microphoneId] Id of the microphone to switch to + * @param {string} [deviceId] Id of the microphone to switch to (passed by @webex/components) */ - async action({meetingID, microphoneId}) { - logger.debug('MEETING', meetingID, 'SwitchMicrophoneControl::action()', ['called with', {meetingID}]); + async action(context, deviceId) { + const {meetingID} = context; + const microphoneId = context.microphoneId != null ? context.microphoneId : deviceId; + + logger.debug('MEETING', meetingID, 'SwitchMicrophoneControl::action()', ['called with', {meetingID, microphoneId}]); await this.adapter.switchMicrophone(meetingID, microphoneId); } diff --git a/src/MeetingsSDKAdapter/controls/SwitchMicrophoneControl.test.js b/src/MeetingsSDKAdapter/controls/SwitchMicrophoneControl.test.js index b4bc035d..17056e35 100644 --- a/src/MeetingsSDKAdapter/controls/SwitchMicrophoneControl.test.js +++ b/src/MeetingsSDKAdapter/controls/SwitchMicrophoneControl.test.js @@ -64,5 +64,12 @@ describe('Switch Microphone Control', () => { expect(meetingsSDKAdapter.switchMicrophone).toHaveBeenCalledTimes(1); expect(meetingsSDKAdapter.switchMicrophone).toHaveBeenCalledWith(meetingID, 'microphoneID'); }); + + it('calls switchMicrophone() with device ID passed as second argument', async () => { + meetingsSDKAdapter.switchMicrophone = jest.fn(); + await meetingsSDKAdapter.meetingControls['switch-microphone'].action({meetingID}, 'microphoneID'); + expect(meetingsSDKAdapter.switchMicrophone).toHaveBeenCalledTimes(1); + expect(meetingsSDKAdapter.switchMicrophone).toHaveBeenCalledWith(meetingID, 'microphoneID'); + }); }); }); diff --git a/src/MeetingsSDKAdapter/controls/SwitchSpeakerControl.js b/src/MeetingsSDKAdapter/controls/SwitchSpeakerControl.js index 695ef260..7dcc0589 100644 --- a/src/MeetingsSDKAdapter/controls/SwitchSpeakerControl.js +++ b/src/MeetingsSDKAdapter/controls/SwitchSpeakerControl.js @@ -14,11 +14,16 @@ export default class SwitchSpeakerControl extends MeetingControl { /** * Calls the the action of the switch speaker control. * - * @param {string} meetingID Meeting ID - * @param {string} speakerID ID of the speaker device to switch to + * @param {object} context Meeting control context + * @param {string} context.meetingID Meeting ID + * @param {string} [context.speakerId] ID of the speaker device to switch to + * @param {string} [deviceId] ID of the speaker device to switch to (passed by @webex/components) */ - async action({meetingID, speakerId}) { - logger.debug('MEETING', meetingID, 'SwitchSpeakerControl::action()', ['called with', {meetingID}]); + async action(context, deviceId) { + const {meetingID} = context; + const speakerId = context.speakerId != null ? context.speakerId : deviceId; + + logger.debug('MEETING', meetingID, 'SwitchSpeakerControl::action()', ['called with', {meetingID, speakerId}]); await this.adapter.switchSpeaker(meetingID, speakerId); } diff --git a/src/MeetingsSDKAdapter/controls/SwitchSpeakerControl.test.js b/src/MeetingsSDKAdapter/controls/SwitchSpeakerControl.test.js index deb30900..d7175fb5 100644 --- a/src/MeetingsSDKAdapter/controls/SwitchSpeakerControl.test.js +++ b/src/MeetingsSDKAdapter/controls/SwitchSpeakerControl.test.js @@ -67,5 +67,12 @@ describe('Switch Speaker Control', () => { await meetingsSDKAdapter.meetingControls['switch-speaker'].action({meetingID, speakerId: 'speakerID'}); expect(meetingsSDKAdapter.switchSpeaker).toHaveBeenCalledWith(meetingID, 'speakerID'); }); + + it('calls switchSpeaker() with device ID passed as second argument', async () => { + meetingsSDKAdapter.switchSpeaker = jest.fn(); + await meetingsSDKAdapter.meetingControls['switch-speaker'].action({meetingID}, 'speakerID'); + expect(meetingsSDKAdapter.switchSpeaker).toHaveBeenCalledTimes(1); + expect(meetingsSDKAdapter.switchSpeaker).toHaveBeenCalledWith(meetingID, 'speakerID'); + }); }); });