Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 73 additions & 60 deletions src/MeetingsSDKAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
});
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1092,14 +1100,19 @@ 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;

this.stopStream(meeting.settings.preview.audio);
const {stream, permission, deviceId} = await this.getStream(
ID,
{sendAudio: true},
{audio: {deviceId: microphoneID}},
{audio: audioConstraints},
).toPromise();

if (stream) {
Expand Down
44 changes: 44 additions & 0 deletions src/MeetingsSDKAdapter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"');
Expand All @@ -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"');
Expand Down
13 changes: 9 additions & 4 deletions src/MeetingsSDKAdapter/controls/SwitchCameraControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
7 changes: 7 additions & 0 deletions src/MeetingsSDKAdapter/controls/SwitchCameraControl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
13 changes: 9 additions & 4 deletions src/MeetingsSDKAdapter/controls/SwitchMicrophoneControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
13 changes: 9 additions & 4 deletions src/MeetingsSDKAdapter/controls/SwitchSpeakerControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not 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);
}
Expand Down
7 changes: 7 additions & 0 deletions src/MeetingsSDKAdapter/controls/SwitchSpeakerControl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});