diff --git a/doc/api.md b/doc/api.md index c5db75fd8..0dbb6f0ad 100644 --- a/doc/api.md +++ b/doc/api.md @@ -51,9 +51,9 @@ var domain = "meet.jit.si"; var options = { ... devices: { - 'audioInput': '', - 'audioOutput': '', - 'videoInput': '' + 'audioInput': '', + 'audioOutput': '', + 'videoInput': '' } } var api = new JitsiMeetExternalAPI(domain, options); @@ -109,14 +109,14 @@ api.getAvailableDevices().then(function(devices) { ... }); ``` -* **getCurrentDevices** - Retrieve a list with the devices that are currently sected. +* **getCurrentDevices** - Retrieve a list with the devices that are currently selected. ```javascript api.getCurrentDevices().then(function(devices) { // devices = { - // 'audioInput': 'deviceID', - // 'audioOutput': 'deviceID', - // 'videoInput': 'deviceID' + // 'audioInput': 'deviceLabel', + // 'audioOutput': 'deviceLabel', + // 'videoInput': 'deviceLabel' // } ... }); @@ -143,20 +143,20 @@ api.isMultipleAudioInputSupported().then(function(isMultipleAudioInputSupported) ... }); ``` -* **setAudioInputDevice** - Sets the audio input device to the one with the id that is passed. +* **setAudioInputDevice** - Sets the audio input device to the one with the label that is passed. ```javascript -api.setAudioInputDevice(deviceId); +api.setAudioInputDevice(deviceLabel); ``` -* **setAudioOutputDevice** - Sets the audio output device to the one with the id that is passed. +* **setAudioOutputDevice** - Sets the audio output device to the one with the label that is passed. ```javascript -api.setAudioOutputDevice(deviceId); +api.setAudioOutputDevice(deviceLabel); ``` -* **setVideoInputDevice** - Sets the video input device to the one with the id that is passed. +* **setVideoInputDevice** - Sets the video input device to the one with the label that is passed. ```javascript -api.setVideoInputDevice(deviceId); +api.setVideoInputDevice(deviceLabel); ``` You can control the embedded Jitsi Meet conference using the `JitsiMeetExternalAPI` object by using `executeCommand`: diff --git a/modules/API/external/functions.js b/modules/API/external/functions.js index e1fe5bae7..df4453488 100644 --- a/modules/API/external/functions.js +++ b/modules/API/external/functions.js @@ -105,12 +105,12 @@ export function isMultipleAudioInputSupported(transport: Object) { * * @param {Transport} transport - The @code{Transport} instance responsible for * the external communication. - * @param {string} id - The id of the new device. + * @param {string} label - The label of the new device. * @returns {Promise} */ -export function setAudioInputDevice(transport: Object, id: string) { +export function setAudioInputDevice(transport: Object, label: string) { return _setDevice(transport, { - id, + label, kind: 'audioinput' }); } @@ -120,12 +120,12 @@ export function setAudioInputDevice(transport: Object, id: string) { * * @param {Transport} transport - The @code{Transport} instance responsible for * the external communication. - * @param {string} id - The id of the new device. + * @param {string} label - The label of the new device. * @returns {Promise} */ -export function setAudioOutputDevice(transport: Object, id: string) { +export function setAudioOutputDevice(transport: Object, label: string) { return _setDevice(transport, { - id, + label, kind: 'audiooutput' }); } @@ -151,12 +151,12 @@ function _setDevice(transport: Object, device) { * * @param {Transport} transport - The @code{Transport} instance responsible for * the external communication. - * @param {string} id - The id of the new device. + * @param {string} label - The label of the new device. * @returns {Promise} */ -export function setVideoInputDevice(transport: Object, id: string) { +export function setVideoInputDevice(transport: Object, label: string) { return _setDevice(transport, { - id, + label, kind: 'videoinput' }); } diff --git a/react/features/base/devices/actionTypes.js b/react/features/base/devices/actionTypes.js index 5b5e40693..753d5e00c 100644 --- a/react/features/base/devices/actionTypes.js +++ b/react/features/base/devices/actionTypes.js @@ -30,3 +30,24 @@ export const SET_VIDEO_INPUT_DEVICE = 'SET_VIDEO_INPUT_DEVICE'; * } */ export const UPDATE_DEVICE_LIST = 'UPDATE_DEVICE_LIST'; + +/** + * The type of Redux action which will add a pending device requests that will + * be executed later when it is possible (when the conference is joined). + * + * { + * type: ADD_PENDING_DEVICE_REQUEST, + * request: Object + * } + */ +export const ADD_PENDING_DEVICE_REQUEST = 'ADD_PENDING_DEVICE_REQUEST'; + +/** + * The type of Redux action which will remove all pending device requests. + * + * { + * type: REMOVE_PENDING_DEVICE_REQUESTS, + * request: Object + * } + */ +export const REMOVE_PENDING_DEVICE_REQUESTS = 'REMOVE_PENDING_DEVICE_REQUESTS'; diff --git a/react/features/base/devices/actions.js b/react/features/base/devices/actions.js index c1b0cb7e7..6fb9b0062 100644 --- a/react/features/base/devices/actions.js +++ b/react/features/base/devices/actions.js @@ -2,11 +2,92 @@ import JitsiMeetJS from '../lib-jitsi-meet'; import { updateSettings } from '../settings'; import { + ADD_PENDING_DEVICE_REQUEST, + REMOVE_PENDING_DEVICE_REQUESTS, SET_AUDIO_INPUT_DEVICE, SET_VIDEO_INPUT_DEVICE, UPDATE_DEVICE_LIST } from './actionTypes'; -import { getDevicesFromURL } from './functions'; +import { + areDeviceLabelsInitialized, + getDeviceIdByLabel, + getDevicesFromURL +} from './functions'; + +/** + * Adds a pending device request. + * + * @param {Object} request - The request to be added. + * @returns {{ + * type: ADD_PENDING_DEVICE_REQUEST, + * request: Object + * }} + */ +export function addPendingDeviceRequest(request) { + return { + type: ADD_PENDING_DEVICE_REQUEST, + request + }; +} + +/** + * Configures the initial A/V devices before the conference has started. + * + * @returns {Function} + */ +export function configureInitialDevices() { + return (dispatch, getState) => new Promise(resolve => { + const deviceLabels = getDevicesFromURL(getState()); + + if (deviceLabels) { + dispatch(getAvailableDevices()).then(() => { + const state = getState(); + + if (!areDeviceLabelsInitialized(state)) { + // The labels are not available if the A/V permissions are + // not yet granted. + + Object.keys(deviceLabels).forEach(key => { + dispatch(addPendingDeviceRequest({ + type: 'devices', + name: 'setDevice', + device: { + kind: key.toLowerCase(), + label: deviceLabels[key] + }, + // eslint-disable-next-line no-empty-function + responseCallback() {} + })); + }); + resolve(); + + return; + } + const newSettings = {}; + const devicesKeysToSettingsKeys = { + audioInput: 'micDeviceId', + audioOutput: 'audioOutputDeviceId', + videoInput: 'cameraDeviceId' + }; + + Object.keys(deviceLabels).forEach(key => { + const label = deviceLabels[key]; + const deviceId = getDeviceIdByLabel(state, label); + + if (deviceId) { + newSettings[devicesKeysToSettingsKeys[key]] = deviceId; + } + }); + + dispatch(updateSettings(newSettings)); + resolve(); + }); + + } else { + resolve(); + } + }); +} /** * Queries for connected A/V input and output devices and updates the redux @@ -31,6 +112,20 @@ export function getAvailableDevices() { }); } + +/** + * Remove all pending device requests. + * + * @returns {{ + * type: REMOVE_PENDING_DEVICE_REQUESTS + * }} + */ +export function removePendingDeviceRequests() { + return { + type: REMOVE_PENDING_DEVICE_REQUESTS + }; +} + /** * Signals to update the currently used audio input device. * @@ -80,21 +175,3 @@ export function updateDeviceList(devices) { }; } -/** - * Configures the initial A/V devices before the conference has started. - * - * @returns {Function} - */ -export function configureInitialDevices() { - return (dispatch, getState) => new Promise(resolve => { - const devices = getDevicesFromURL(getState()); - - if (devices) { - dispatch(updateSettings({ - ...devices - })); - resolve(); - } - }); -} - diff --git a/react/features/base/devices/functions.js b/react/features/base/devices/functions.js index bad9fdcd8..d5d07dade 100644 --- a/react/features/base/devices/functions.js +++ b/react/features/base/devices/functions.js @@ -4,6 +4,30 @@ import { parseURLParams } from '../config'; import JitsiMeetJS from '../lib-jitsi-meet'; import { updateSettings } from '../settings'; +declare var APP: Object; + +/** + * Detects the use case when the labels are not available if the A/V permissions + * are not yet granted. + * + * @param {Object} state - The redux state. + * @returns {boolean} - True if the labels are already initialized and false + * otherwise. + */ +export function areDeviceLabelsInitialized(state: Object) { + if (APP.conference._localTracksInitialized) { + return true; + } + + for (const type of [ 'audioInput', 'audioOutput', 'videoInput' ]) { + if (state['features/base/devices'][type].find(d => Boolean(d.label))) { + return true; + } + } + + return false; +} + /** * Get device id of the audio output device which is currently in use. * Empty string stands for default device. @@ -15,21 +39,50 @@ export function getAudioOutputDeviceId() { } /** - * Set device id of the audio output device which is currently in use. - * Empty string stands for default device. + * Finds a device with a label that matches the passed label and returns its id. * - * @param {string} newId - New audio output device id. - * @param {Function} dispatch - The Redux dispatch function. - * @returns {Promise} + * @param {Object} state - The redux state. + * @param {string} label - The label. + * @returns {string|undefined} */ -export function setAudioOutputDeviceId( - newId: string = 'default', - dispatch: Function): Promise<*> { - return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId) - .then(() => - dispatch(updateSettings({ - audioOutputDeviceId: newId - }))); +export function getDeviceIdByLabel(state: Object, label: string) { + const types = [ 'audioInput', 'audioOutput', 'videoInput' ]; + + for (const type of types) { + const device + = state['features/base/devices'][type].find(d => d.label === label); + + if (device) { + return device.deviceId; + } + } +} + +/** + * Returns the devices set in the URL. + * + * @param {Object} state - The redux state. + * @returns {Object|undefined} + */ +export function getDevicesFromURL(state: Object) { + const urlParams + = parseURLParams(state['features/base/connection'].locationURL); + + const audioOutput = urlParams['devices.audioOutput']; + const videoInput = urlParams['devices.videoInput']; + const audioInput = urlParams['devices.audioInput']; + + if (!audioOutput && !videoInput && !audioInput) { + return undefined; + } + + const devices = {}; + + audioOutput && (devices.audioOutput = audioOutput); + videoInput && (devices.videoInput = videoInput); + audioInput && (devices.audioInput = audioInput); + + return devices; } /** @@ -50,28 +103,19 @@ export function groupDevicesByKind(devices: Object[]): Object { } /** - * Returns the devices set in the URL. + * Set device id of the audio output device which is currently in use. + * Empty string stands for default device. * - * @param {Object} state - The redux state. - * @returns {Object|undefined} + * @param {string} newId - New audio output device id. + * @param {Function} dispatch - The Redux dispatch function. + * @returns {Promise} */ -export function getDevicesFromURL(state: Object) { - const urlParams - = parseURLParams(state['features/base/connection'].locationURL); - - const audioOutputDeviceId = urlParams['devices.audioOutput']; - const cameraDeviceId = urlParams['devices.videoInput']; - const micDeviceId = urlParams['devices.audioInput']; - - if (!audioOutputDeviceId && !cameraDeviceId && !micDeviceId) { - return undefined; - } - - const devices = {}; - - audioOutputDeviceId && (devices.audioOutputDeviceId = audioOutputDeviceId); - cameraDeviceId && (devices.cameraDeviceId = cameraDeviceId); - micDeviceId && (devices.micDeviceId = micDeviceId); - - return devices; +export function setAudioOutputDeviceId( + newId: string = 'default', + dispatch: Function): Promise<*> { + return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId) + .then(() => + dispatch(updateSettings({ + audioOutputDeviceId: newId + }))); } diff --git a/react/features/base/devices/middleware.js b/react/features/base/devices/middleware.js index 3fedd0f4f..f6562e17f 100644 --- a/react/features/base/devices/middleware.js +++ b/react/features/base/devices/middleware.js @@ -1,9 +1,11 @@ /* global APP */ +import { CONFERENCE_JOINED } from '../conference'; +import { processRequest } from '../../device-selection'; +import { MiddlewareRegistry } from '../redux'; import UIEvents from '../../../../service/UI/UIEvents'; -import { MiddlewareRegistry } from '../redux'; - +import { removePendingDeviceRequests } from './actions'; import { SET_AUDIO_INPUT_DEVICE, SET_VIDEO_INPUT_DEVICE @@ -18,6 +20,8 @@ import { // eslint-disable-next-line no-unused-vars MiddlewareRegistry.register(store => next => action => { switch (action.type) { + case CONFERENCE_JOINED: + return _conferenceJoined(store, next, action); case SET_AUDIO_INPUT_DEVICE: APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId); break; @@ -28,3 +32,34 @@ MiddlewareRegistry.register(store => next => action => { return next(action); }); + + +/** + * Does extra sync up on properties that may need to be updated after the + * conference was joined. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is + * being dispatched in the specified {@code store}. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _conferenceJoined({ dispatch, getState }, next, action) { + const result = next(action); + const state = getState(); + const { pendingRequests } = state['features/base/devices']; + + pendingRequests.forEach(request => { + processRequest( + dispatch, + getState, + request, + request.responseCallback); + }); + dispatch(removePendingDeviceRequests()); + + return result; +} diff --git a/react/features/base/devices/reducer.js b/react/features/base/devices/reducer.js index 38f744ad6..6ebbc9d4c 100644 --- a/react/features/base/devices/reducer.js +++ b/react/features/base/devices/reducer.js @@ -1,4 +1,6 @@ import { + ADD_PENDING_DEVICE_REQUEST, + REMOVE_PENDING_DEVICE_REQUESTS, SET_AUDIO_INPUT_DEVICE, SET_VIDEO_INPUT_DEVICE, UPDATE_DEVICE_LIST @@ -10,7 +12,8 @@ import { ReducerRegistry } from '../redux'; const DEFAULT_STATE = { audioInput: [], audioOutput: [], - videoInput: [] + videoInput: [], + pendingRequests: [] }; /** @@ -31,10 +34,26 @@ ReducerRegistry.register( const deviceList = groupDevicesByKind(action.devices); return { + pendingRequests: state.pendingRequests, ...deviceList }; } + case ADD_PENDING_DEVICE_REQUEST: + return { + ...state, + pendingRequests: [ + ...state.pendingRequests, + action.request + ] + }; + + case REMOVE_PENDING_DEVICE_REQUESTS: + return { + ...state, + pendingRequests: [ ] + }; + // TODO: Changing of current audio and video device id is currently // handled outside of react/redux. Fall through to default logic for // now. diff --git a/react/features/device-selection/functions.js b/react/features/device-selection/functions.js index ba938af94..e5404d4d1 100644 --- a/react/features/device-selection/functions.js +++ b/react/features/device-selection/functions.js @@ -1,7 +1,13 @@ // @flow + +import type { Dispatch } from 'redux'; + import { + addPendingDeviceRequest, + areDeviceLabelsInitialized, getAudioOutputDeviceId, getAvailableDevices, + getDeviceIdByLabel, groupDevicesByKind, setAudioInputDevice, setAudioOutputDeviceId, @@ -49,13 +55,15 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) { * @returns {boolean} */ export function processRequest( // eslint-disable-line max-params - dispatch: Dispatch<*>, + dispatch: Dispatch, getState: Function, request: Object, responseCallback: Function) { if (request.type === 'devices') { const state = getState(); const settings = state['features/base/settings']; + const { conference } = state['features/base/conference']; + let result = true; switch (request.name) { case 'isDeviceListAvailable': @@ -70,35 +78,95 @@ export function processRequest( // eslint-disable-line max-params responseCallback(JitsiMeetJS.isMultipleAudioInputSupported()); break; case 'getCurrentDevices': - responseCallback({ - audioInput: settings.micDeviceId, - audioOutput: getAudioOutputDeviceId(), - videoInput: settings.cameraDeviceId + dispatch(getAvailableDevices()).then(devices => { + if (areDeviceLabelsInitialized(state)) { + let audioInput, audioOutput, videoInput; + const audioOutputDeviceId = getAudioOutputDeviceId(); + const { cameraDeviceId, micDeviceId } = settings; + + devices.forEach(({ deviceId, label }) => { + switch (deviceId) { + case micDeviceId: + audioInput = label; + break; + case audioOutputDeviceId: + audioOutput = label; + break; + case cameraDeviceId: + videoInput = label; + break; + } + }); + + responseCallback({ + audioInput, + audioOutput, + videoInput + }); + } else { + // The labels are not available if the A/V permissions are + // not yet granted. + dispatch(addPendingDeviceRequest({ + type: 'devices', + name: 'getCurrentDevices', + responseCallback + })); + } }); + break; case 'getAvailableDevices': - dispatch(getAvailableDevices()).then( - devices => responseCallback(groupDevicesByKind(devices))); + dispatch(getAvailableDevices()).then(devices => { + if (areDeviceLabelsInitialized(state)) { + responseCallback(groupDevicesByKind(devices)); + } else { + // The labels are not available if the A/V permissions are + // not yet granted. + dispatch(addPendingDeviceRequest({ + type: 'devices', + name: 'getAvailableDevices', + responseCallback + })); + } + }); break; case 'setDevice': { const { device } = request; - switch (device.kind) { - case 'audioinput': - dispatch(setAudioInputDevice(device.id)); - break; - case 'audiooutput': - setAudioOutputDeviceId(device.id, dispatch); - break; - case 'videoinput': - dispatch(setVideoInputDevice(device.id)); - break; - default: - responseCallback(false); + if (!conference) { + dispatch(addPendingDeviceRequest({ + type: 'devices', + name: 'setDevice', + device, + responseCallback + })); + + return true; } - responseCallback(true); + const deviceId = getDeviceIdByLabel(state, device.label); + + if (deviceId) { + switch (device.kind) { + case 'audioinput': { + dispatch(setAudioInputDevice(deviceId)); + break; + } + case 'audiooutput': + setAudioOutputDeviceId(deviceId, dispatch); + break; + case 'videoinput': + dispatch(setVideoInputDevice(deviceId)); + break; + default: + result = false; + } + } else { + result = false; + } + + responseCallback(result); break; } default: