ref(iframe-api-devices): Use labels instead of IDs

This commit is contained in:
Hristo Terezov 2019-03-27 17:30:56 +00:00
parent ed1d3d3df5
commit 4967488e56
8 changed files with 363 additions and 99 deletions

View File

@ -51,9 +51,9 @@ var domain = "meet.jit.si";
var options = {
...
devices: {
'audioInput': '<device_id>',
'audioOutput': '<device_id>',
'videoInput': '<device_id>'
'audioInput': '<deviceLabel>',
'audioOutput': '<deviceLabel>',
'videoInput': '<deviceLabel>'
}
}
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`:

View File

@ -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'
});
}

View File

@ -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';

View File

@ -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();
}
});
}

View File

@ -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
})));
}

View File

@ -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;
}

View File

@ -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.

View File

@ -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<any>,
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: