From 427f49367bdb0636b65ebe3cd66b9c65cab4074c Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Thu, 21 Mar 2019 12:33:40 +0000 Subject: [PATCH] feat(iframe-api): Device handling. --- doc/api.md | 78 +++++++++ modules/API/API.js | 9 + modules/API/external/external_api.js | 90 ++++++++++ modules/API/external/functions.js | 162 ++++++++++++++++++ react/features/device-selection/actions.js | 75 +------- react/features/device-selection/functions.js | 76 +++++++- .../components/web/DeviceSelectionPopup.js | 88 ++-------- 7 files changed, 434 insertions(+), 144 deletions(-) create mode 100644 modules/API/external/functions.js diff --git a/doc/api.md b/doc/api.md index ff0ff7752..765733238 100644 --- a/doc/api.md +++ b/doc/api.md @@ -65,6 +65,84 @@ var api = new JitsiMeetExternalAPI(domain, options); ### Controlling the embedded Jitsi Meet Conference +You can control the available devices with the following methods of `JitsiMeetExternalAPI` instance: +* **getAvailableDevices** - Retrieve a list of available devices. + +```javascript +api.getAvailableDevices().then(function(devices) { + // devices = { + // 'audioInput': [{ + // deviceId: "ID" + // groupId: "grpID" + // kind: "audioinput" + // label: "Label" + // },....], + // 'audioOutput': [{ + // deviceId: "ID" + // groupId: "grpID" + // kind: "audioOutput" + // label: "Label" + // },....], + // 'videoInput': [{ + // deviceId: "ID" + // groupId: "grpID" + // kind: "videoInput" + // label: "Label" + // },....] + // } + ... +}); +``` +* **getCurrentDevices** - Retrieve a list with the devices that are currently sected. + +```javascript +api.getCurrentDevices().then(function(devices) { + // devices = { + // 'audioInput': 'deviceID', + // 'audioOutput': 'deviceID', + // 'videoInput': 'deviceID' + // } + ... +}); +``` +* **isDeviceChangeAvailable** - Resolves with true if the device change is available and with false if not. + +```javascript +// The accepted deviceType values are - 'output', 'input' or undefined. +api.isDeviceChangeAvailable(deviceType).then(function(isDeviceChangeAvailable) { + ... +}); +``` +* **isDeviceListAvailable** - Resolves with true if the device list is available and with false if not. + +```javascript +api.isDeviceListAvailable().then(function(isDeviceListAvailable) { + ... +}); +``` +* **isMultipleAudioInputSupported** - Resolves with true if the device list is available and with false if not. + +```javascript +api.isMultipleAudioInputSupported().then(function(isMultipleAudioInputSupported) { + ... +}); +``` +* **setAudioInputDevice** - Sets the audio input device to the one with the id that is passed. + +```javascript +api.setAudioInputDevice(deviceId); +``` +* **setAudioOutputDevice** - Sets the audio output device to the one with the id that is passed. + +```javascript +api.setAudioOutputDevice(deviceId); +``` +* **setVideoInputDevice** - Sets the video input device to the one with the id that is passed. + +```javascript +api.setVideoInputDevice(deviceId); +``` + You can control the embedded Jitsi Meet conference using the `JitsiMeetExternalAPI` object by using `executeCommand`: ```javascript diff --git a/modules/API/API.js b/modules/API/API.js index 4463ea357..0904df19e 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -11,6 +11,9 @@ import { invite } from '../../react/features/invite'; import { getJitsiMeetTransport } from '../transport'; import { API_ID } from './constants'; +import { + processRequest +} from '../../react/features/device-selection/functions'; const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -117,6 +120,12 @@ function initCommands() { return false; }); transport.on('request', (request, callback) => { + const { dispatch, getState } = APP.store; + + if (processRequest(dispatch, getState, request, callback)) { + return true; + } + const { name } = request; switch (name) { diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js index fead06129..fe9d3cd66 100644 --- a/modules/API/external/external_api.js +++ b/modules/API/external/external_api.js @@ -7,6 +7,16 @@ import { } from '../../transport'; import electronPopupsConfig from './electronPopupsConfig.json'; +import { + getAvailableDevices, + getCurrentDevices, + isDeviceChangeAvailable, + isDeviceListAvailable, + isMultipleAudioInputSupported, + setAudioInputDevice, + setAudioOutputDevice, + setVideoInputDevice +} from './functions'; const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -593,6 +603,24 @@ export default class JitsiMeetExternalAPI extends EventEmitter { } } + /** + * Returns Promise that resolves with result an list of available devices. + * + * @returns {Promise} + */ + getAvailableDevices() { + return getAvailableDevices(this._transport); + } + + /** + * Returns Promise that resolves with current selected devices. + * + * @returns {Promise} + */ + getCurrentDevices() { + return getCurrentDevices(this._transport); + } + /** * Check if the audio is available. * @@ -605,6 +633,38 @@ export default class JitsiMeetExternalAPI extends EventEmitter { }); } + /** + * Returns Promise that resolves with true if the device change is available + * and with false if not. + * + * @param {string} [deviceType] - Values - 'output', 'input' or undefined. + * Default - 'input'. + * @returns {Promise} + */ + isDeviceChangeAvailable(deviceType) { + return isDeviceChangeAvailable(this._transport, deviceType); + } + + /** + * Returns Promise that resolves with true if the device list is available + * and with false if not. + * + * @returns {Promise} + */ + isDeviceListAvailable() { + return isDeviceListAvailable(this._transport); + } + + /** + * Returns Promise that resolves with true if the device list is available + * and with false if not. + * + * @returns {Promise} + */ + isMultipleAudioInputSupported() { + return isMultipleAudioInputSupported(this._transport); + } + /** * Invite people to the call. * @@ -771,6 +831,36 @@ export default class JitsiMeetExternalAPI extends EventEmitter { }); } + /** + * Sets the audio input device to the one with the id that is passed. + * + * @param {string} deviceId - The id of the new device. + * @returns {Promise} + */ + setAudioInputDevice(deviceId) { + return setAudioInputDevice(this._transport, deviceId); + } + + /** + * Sets the audio output device to the one with the id that is passed. + * + * @param {string} deviceId - The id of the new device. + * @returns {Promise} + */ + setAudioOutputDevice(deviceId) { + return setAudioOutputDevice(this._transport, deviceId); + } + + /** + * Sets the video input device to the one with the id that is passed. + * + * @param {string} deviceId - The id of the new device. + * @returns {Promise} + */ + setVideoInputDevice(deviceId) { + return setVideoInputDevice(this._transport, deviceId); + } + /** * Returns the configuration for electron for the windows that are open * from Jitsi Meet. diff --git a/modules/API/external/functions.js b/modules/API/external/functions.js new file mode 100644 index 000000000..e1fe5bae7 --- /dev/null +++ b/modules/API/external/functions.js @@ -0,0 +1,162 @@ +// @flow + +import Logger from 'jitsi-meet-logger'; + +const logger = Logger.getLogger(__filename); + +/** + * Returns Promise that resolves with result an list of available devices. + * + * @param {Transport} transport - The @code{Transport} instance responsible for + * the external communication. + * @returns {Promise} + */ +export function getAvailableDevices(transport: Object) { + return transport.sendRequest({ + type: 'devices', + name: 'getAvailableDevices' + }).catch(e => { + logger.error(e); + + return {}; + }); +} + +/** + * Returns Promise that resolves with current selected devices. + * + * @param {Transport} transport - The @code{Transport} instance responsible for + * the external communication. + * @returns {Promise} + */ +export function getCurrentDevices(transport: Object) { + return transport.sendRequest({ + type: 'devices', + name: 'getCurrentDevices' + }).catch(e => { + logger.error(e); + + return {}; + }); +} + +/** + * Returns Promise that resolves with true if the device change is available + * and with false if not. + * + * @param {Transport} transport - The @code{Transport} instance responsible for + * the external communication. + * @param {string} [deviceType] - Values - 'output', 'input' or undefined. + * Default - 'input'. + * @returns {Promise} + */ +export function isDeviceChangeAvailable(transport: Object, deviceType: string) { + return transport.sendRequest({ + deviceType, + type: 'devices', + name: 'isDeviceChangeAvailable' + }).catch(e => { + logger.error(e); + + return false; + }); +} + +/** + * Returns Promise that resolves with true if the device list is available + * and with false if not. + * + * @param {Transport} transport - The @code{Transport} instance responsible for + * the external communication. + * @returns {Promise} + */ +export function isDeviceListAvailable(transport: Object) { + return transport.sendRequest({ + type: 'devices', + name: 'isDeviceListAvailable' + }).catch(e => { + logger.error(e); + + return false; + }); +} + +/** + * Returns Promise that resolves with true if the device list is available + * and with false if not. + * + * @param {Transport} transport - The @code{Transport} instance responsible for + * the external communication. + * @returns {Promise} + */ +export function isMultipleAudioInputSupported(transport: Object) { + return transport.sendRequest({ + type: 'devices', + name: 'isMultipleAudioInputSupported' + }).catch(e => { + logger.error(e); + + return false; + }); +} + +/** + * Sets the audio input device to the one with the id that is passed. + * + * @param {Transport} transport - The @code{Transport} instance responsible for + * the external communication. + * @param {string} id - The id of the new device. + * @returns {Promise} + */ +export function setAudioInputDevice(transport: Object, id: string) { + return _setDevice(transport, { + id, + kind: 'audioinput' + }); +} + +/** + * Sets the audio output device to the one with the id that is passed. + * + * @param {Transport} transport - The @code{Transport} instance responsible for + * the external communication. + * @param {string} id - The id of the new device. + * @returns {Promise} + */ +export function setAudioOutputDevice(transport: Object, id: string) { + return _setDevice(transport, { + id, + kind: 'audiooutput' + }); +} + +/** + * Sets the currently used device to the one that is passed. + * + * @param {Transport} transport - The @code{Transport} instance responsible for + * the external communication. + * @param {Object} device - The new device to be used. + * @returns {Promise} + */ +function _setDevice(transport: Object, device) { + return transport.sendRequest({ + type: 'devices', + name: 'setDevice', + device + }); +} + +/** + * Sets the video input device to the one with the id that is passed. + * + * @param {Transport} transport - The @code{Transport} instance responsible for + * the external communication. + * @param {string} id - The id of the new device. + * @returns {Promise} + */ +export function setVideoInputDevice(transport: Object, id: string) { + return _setDevice(transport, { + id, + kind: 'videoinput' + }); +} diff --git a/react/features/device-selection/actions.js b/react/features/device-selection/actions.js index 4631210a0..f87138fdb 100644 --- a/react/features/device-selection/actions.js +++ b/react/features/device-selection/actions.js @@ -6,17 +6,15 @@ import { import { createDeviceChangedEvent, sendAnalytics } from '../analytics'; import { - getAudioOutputDeviceId, setAudioInputDevice, setAudioOutputDeviceId, setVideoInputDevice } from '../base/devices'; import { i18next } from '../base/i18n'; -import JitsiMeetJS from '../base/lib-jitsi-meet'; import { updateSettings } from '../base/settings'; import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes'; -import { getDeviceSelectionDialogProps } from './functions'; +import { getDeviceSelectionDialogProps, processRequest } from './functions'; const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -60,7 +58,7 @@ export function openDeviceSelectionPopup() { }); transport.on('request', - _processRequest.bind(undefined, dispatch, getState)); + processRequest.bind(undefined, dispatch, getState)); transport.on('event', event => { if (event.type === 'devices-dialog' && event.name === 'close') { popup.close(); @@ -80,75 +78,6 @@ export function openDeviceSelectionPopup() { }; } -/** - * Processes device requests from external applications. - * - * @param {Dispatch} dispatch - The redux {@code dispatch} function. - * @param {Function} getState - The redux function that gets/retrieves the redux - * state. - * @param {Object} request - The request to be processed. - * @param {Function} responseCallback - The callback that will send the - * response. - * @returns {boolean} - */ -function _processRequest(dispatch, getState, request, responseCallback) { // eslint-disable-line max-len, max-params - if (request.type === 'devices') { - const state = getState(); - const settings = state['features/base/settings']; - - switch (request.name) { - case 'isDeviceListAvailable': - responseCallback(JitsiMeetJS.mediaDevices.isDeviceListAvailable()); - break; - case 'isDeviceChangeAvailable': - responseCallback( - JitsiMeetJS.mediaDevices.isDeviceChangeAvailable( - request.deviceType)); - break; - case 'isMultipleAudioInputSupported': - responseCallback(JitsiMeetJS.isMultipleAudioInputSupported()); - break; - case 'getCurrentDevices': - responseCallback({ - audioInput: settings.micDeviceId, - audioOutput: getAudioOutputDeviceId(), - videoInput: settings.cameraDeviceId - }); - break; - case 'getAvailableDevices': - responseCallback(getState()['features/base/devices']); - 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(true); - break; - } - default: - - return false; - } - - return true; - } - - return false; -} - /** * Sets information about device selection popup in the store. * diff --git a/react/features/device-selection/functions.js b/react/features/device-selection/functions.js index 0c2ce25e7..f9215fd16 100644 --- a/react/features/device-selection/functions.js +++ b/react/features/device-selection/functions.js @@ -1,5 +1,10 @@ // @flow -import { getAudioOutputDeviceId } from '../base/devices'; +import { + getAudioOutputDeviceId, + setAudioInputDevice, + setAudioOutputDeviceId, + setVideoInputDevice +} from '../base/devices'; import JitsiMeetJS from '../base/lib-jitsi-meet'; import { toState } from '../base/redux'; @@ -29,3 +34,72 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) { selectedVideoInputId: settings.cameraDeviceId }; } + +/** + * Processes device requests from external applications. + * + * @param {Dispatch} dispatch - The redux {@code dispatch} function. + * @param {Function} getState - The redux function that gets/retrieves the redux + * state. + * @param {Object} request - The request to be processed. + * @param {Function} responseCallback - The callback that will send the + * response. + * @returns {boolean} + */ +export function processRequest(dispatch: Dispatch<*>, getState: Function, request: Object, responseCallback: Function) { // eslint-disable-line max-len, max-params + if (request.type === 'devices') { + const state = getState(); + const settings = state['features/base/settings']; + + switch (request.name) { + case 'isDeviceListAvailable': + responseCallback(JitsiMeetJS.mediaDevices.isDeviceListAvailable()); + break; + case 'isDeviceChangeAvailable': + responseCallback( + JitsiMeetJS.mediaDevices.isDeviceChangeAvailable( + request.deviceType)); + break; + case 'isMultipleAudioInputSupported': + responseCallback(JitsiMeetJS.isMultipleAudioInputSupported()); + break; + case 'getCurrentDevices': + responseCallback({ + audioInput: settings.micDeviceId, + audioOutput: getAudioOutputDeviceId(), + videoInput: settings.cameraDeviceId + }); + break; + case 'getAvailableDevices': + responseCallback(getState()['features/base/devices']); + 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(true); + break; + } + default: + + return false; + } + + return true; + } + + return false; +} diff --git a/react/features/settings/components/web/DeviceSelectionPopup.js b/react/features/settings/components/web/DeviceSelectionPopup.js index 1e5869343..bfff2e1d5 100644 --- a/react/features/settings/components/web/DeviceSelectionPopup.js +++ b/react/features/settings/components/web/DeviceSelectionPopup.js @@ -1,7 +1,6 @@ /* global JitsiMeetJS */ import { AtlasKitThemeProvider } from '@atlaskit/theme'; -import Logger from 'jitsi-meet-logger'; import React from 'react'; import ReactDOM from 'react-dom'; import { I18nextProvider } from 'react-i18next'; @@ -10,13 +9,21 @@ import { PostMessageTransportBackend, Transport } from '../../../../../modules/transport'; +import { + getAvailableDevices, + getCurrentDevices, + isDeviceChangeAvailable, + isDeviceListAvailable, + isMultipleAudioInputSupported, + setAudioInputDevice, + setAudioOutputDevice, + setVideoInputDevice +} from '../../../../../modules/API/external'; import { parseURLParams } from '../../../base/config'; import { DialogWithTabs } from '../../../base/dialog'; import { DeviceSelection } from '../../../device-selection'; -const logger = Logger.getLogger(__filename); - /** * Implements a class that renders the React components for the device selection * popup page and handles the communication between the components and Jitsi @@ -102,14 +109,7 @@ export default class DeviceSelectionPopup { * @returns {Promise} */ _getAvailableDevices() { - return this._transport.sendRequest({ - type: 'devices', - name: 'getAvailableDevices' - }).catch(e => { - logger.error(e); - - return {}; - }); + return getAvailableDevices(this._transport); } /** @@ -118,14 +118,7 @@ export default class DeviceSelectionPopup { * @returns {Promise} */ _getCurrentDevices() { - return this._transport.sendRequest({ - type: 'devices', - name: 'getCurrentDevices' - }).catch(e => { - logger.error(e); - - return {}; - }); + return getCurrentDevices(this._transport); } /** @@ -170,15 +163,7 @@ export default class DeviceSelectionPopup { * @returns {Promise} */ _isDeviceChangeAvailable(deviceType) { - return this._transport.sendRequest({ - deviceType, - type: 'devices', - name: 'isDeviceChangeAvailable' - }).catch(e => { - logger.error(e); - - return false; - }); + return isDeviceChangeAvailable(this._transport, deviceType); } /** @@ -188,14 +173,7 @@ export default class DeviceSelectionPopup { * @returns {Promise} */ _isDeviceListAvailable() { - return this._transport.sendRequest({ - type: 'devices', - name: 'isDeviceListAvailable' - }).catch(e => { - logger.error(e); - - return false; - }); + return isDeviceListAvailable(this._transport); } /** @@ -205,14 +183,7 @@ export default class DeviceSelectionPopup { * @returns {Promise} */ _isMultipleAudioInputSupported() { - return this._transport.sendRequest({ - type: 'devices', - name: 'isMultipleAudioInputSupported' - }).catch(e => { - logger.error(e); - - return false; - }); + return isMultipleAudioInputSupported(this._transport); } /** @@ -281,10 +252,7 @@ export default class DeviceSelectionPopup { * @returns {Promise} */ _setAudioInputDevice(id) { - return this._setDevice({ - id, - kind: 'audioinput' - }); + return setAudioInputDevice(this._transport, id); } /** @@ -294,24 +262,7 @@ export default class DeviceSelectionPopup { * @returns {Promise} */ _setAudioOutputDevice(id) { - return this._setDevice({ - id, - kind: 'audiooutput' - }); - } - - /** - * Sets the currently used device to the one that is passed. - * - * @param {Object} device - The new device to be used. - * @returns {Promise} - */ - _setDevice(device) { - return this._transport.sendRequest({ - type: 'devices', - name: 'setDevice', - device - }); + return setAudioOutputDevice(this._transport, id); } /** @@ -321,10 +272,7 @@ export default class DeviceSelectionPopup { * @returns {Promise} */ _setVideoInputDevice(id) { - return this._setDevice({ - id, - kind: 'videoinput' - }); + return setVideoInputDevice(this._transport, id); } /**