This commit is contained in:
96
react/features/base/devices/actionTypes.ts
Normal file
96
react/features/base/devices/actionTypes.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* The type of Redux action which signals that an error occurred while obtaining
|
||||
* a camera.
|
||||
*
|
||||
* {
|
||||
* type: NOTIFY_CAMERA_ERROR,
|
||||
* error: Object
|
||||
* }
|
||||
*/
|
||||
export const NOTIFY_CAMERA_ERROR = 'NOTIFY_CAMERA_ERROR';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that an error occurred while obtaining
|
||||
* a microphone.
|
||||
*
|
||||
* {
|
||||
* type: NOTIFY_MIC_ERROR,
|
||||
* error: Object
|
||||
* }
|
||||
*/
|
||||
export const NOTIFY_MIC_ERROR = 'NOTIFY_MIC_ERROR';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the currently used audio
|
||||
* input device should be changed.
|
||||
*
|
||||
* {
|
||||
* type: SET_AUDIO_INPUT_DEVICE,
|
||||
* deviceId: string,
|
||||
* }
|
||||
*/
|
||||
export const SET_AUDIO_INPUT_DEVICE = 'SET_AUDIO_INPUT_DEVICE';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the currently used video
|
||||
* input device should be changed.
|
||||
*
|
||||
* {
|
||||
* type: SET_VIDEO_INPUT_DEVICE,
|
||||
* deviceId: string,
|
||||
* }
|
||||
*/
|
||||
export const SET_VIDEO_INPUT_DEVICE = 'SET_VIDEO_INPUT_DEVICE';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the list of known available
|
||||
* audio and video sources has changed.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_DEVICE_LIST,
|
||||
* devices: Array<MediaDeviceInfo>,
|
||||
* }
|
||||
*/
|
||||
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
|
||||
* }
|
||||
*/
|
||||
export const REMOVE_PENDING_DEVICE_REQUESTS = 'REMOVE_PENDING_DEVICE_REQUESTS';
|
||||
|
||||
/**
|
||||
* The type of Redux action which will check passed old and passed new devices
|
||||
* and if needed will show notifications asking the user whether to use those.
|
||||
*
|
||||
* {
|
||||
* type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE
|
||||
* newDevices: Array<MediaDeviceInfo>
|
||||
* oldDevices: Array<MediaDeviceInfo>
|
||||
* }
|
||||
*/
|
||||
export const CHECK_AND_NOTIFY_FOR_NEW_DEVICE = 'CHECK_AND_NOTIFY_FOR_NEW_DEVICE';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the device permissions have changed.
|
||||
*
|
||||
* {
|
||||
* type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE
|
||||
* permissions: Object
|
||||
* }
|
||||
*/
|
||||
export const DEVICE_PERMISSIONS_CHANGED = 'DEVICE_PERMISSIONS_CHANGED';
|
||||
356
react/features/base/devices/actions.web.ts
Normal file
356
react/features/base/devices/actions.web.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import { updateSettings } from '../settings/actions';
|
||||
import { getUserSelectedOutputDeviceId } from '../settings/functions.web';
|
||||
|
||||
import {
|
||||
ADD_PENDING_DEVICE_REQUEST,
|
||||
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
|
||||
DEVICE_PERMISSIONS_CHANGED,
|
||||
NOTIFY_CAMERA_ERROR,
|
||||
NOTIFY_MIC_ERROR,
|
||||
REMOVE_PENDING_DEVICE_REQUESTS,
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
import {
|
||||
areDeviceLabelsInitialized,
|
||||
areDevicesDifferent,
|
||||
filterIgnoredDevices,
|
||||
flattenAvailableDevices,
|
||||
getDeviceIdByLabel,
|
||||
getDeviceLabelById,
|
||||
getDevicesFromURL,
|
||||
logDevices,
|
||||
setAudioOutputDeviceId
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Maps the WebRTC string for device type to the keys used to store configure,
|
||||
* within redux, which devices should be used by default.
|
||||
*/
|
||||
const DEVICE_TYPE_TO_SETTINGS_KEYS = {
|
||||
audioInput: {
|
||||
currentDeviceId: 'micDeviceId',
|
||||
userSelectedDeviceId: 'userSelectedMicDeviceId',
|
||||
userSelectedDeviceLabel: 'userSelectedMicDeviceLabel'
|
||||
},
|
||||
audioOutput: {
|
||||
currentDeviceId: 'audioOutputDeviceId',
|
||||
userSelectedDeviceId: 'userSelectedAudioOutputDeviceId',
|
||||
userSelectedDeviceLabel: 'userSelectedAudioOutputDeviceLabel'
|
||||
},
|
||||
videoInput: {
|
||||
currentDeviceId: 'cameraDeviceId',
|
||||
userSelectedDeviceId: 'userSelectedCameraDeviceId',
|
||||
userSelectedDeviceLabel: 'userSelectedCameraDeviceLabel'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: Object) {
|
||||
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: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const deviceLabels = getDevicesFromURL(getState());
|
||||
let updateSettingsPromise;
|
||||
|
||||
logger.debug(`(TIME) configureInitialDevices: deviceLabels=${
|
||||
Boolean(deviceLabels)}, performance.now=${window.performance.now()}`);
|
||||
|
||||
if (deviceLabels) {
|
||||
updateSettingsPromise = 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 as keyof typeof deviceLabels]
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
responseCallback() {}
|
||||
}));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newSettings: any = {};
|
||||
|
||||
Object.keys(deviceLabels).forEach(key => {
|
||||
const label = deviceLabels[key as keyof typeof deviceLabels];
|
||||
|
||||
// @ts-ignore
|
||||
const deviceId = getDeviceIdByLabel(state, label, key);
|
||||
|
||||
if (deviceId) {
|
||||
const settingsTranslationMap = DEVICE_TYPE_TO_SETTINGS_KEYS[
|
||||
key as keyof typeof DEVICE_TYPE_TO_SETTINGS_KEYS];
|
||||
|
||||
newSettings[settingsTranslationMap.currentDeviceId] = deviceId;
|
||||
newSettings[settingsTranslationMap.userSelectedDeviceId] = deviceId;
|
||||
newSettings[settingsTranslationMap.userSelectedDeviceLabel] = label;
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(updateSettings(newSettings));
|
||||
});
|
||||
} else {
|
||||
updateSettingsPromise = Promise.resolve();
|
||||
}
|
||||
|
||||
return updateSettingsPromise
|
||||
.then(() => {
|
||||
const userSelectedAudioOutputDeviceId = getUserSelectedOutputDeviceId(getState());
|
||||
|
||||
logger.debug(`(TIME) configureInitialDevices -> setAudioOutputDeviceId: performance.now=${
|
||||
window.performance.now()}`);
|
||||
|
||||
return setAudioOutputDeviceId(userSelectedAudioOutputDeviceId, dispatch)
|
||||
.catch(ex => logger.warn(`Failed to set audio output device.
|
||||
Default audio output device will be used instead ${ex}`));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries for connected A/V input and output devices and updates the redux
|
||||
* state of known devices.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function getAvailableDevices() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => new Promise(resolve => {
|
||||
const { mediaDevices } = JitsiMeetJS;
|
||||
|
||||
if (mediaDevices.isDeviceChangeAvailable()) {
|
||||
mediaDevices.enumerateDevices((devices: MediaDeviceInfo[]) => {
|
||||
const { filteredDevices, ignoredDevices } = filterIgnoredDevices(devices);
|
||||
const oldDevices = flattenAvailableDevices(getState()['features/base/devices'].availableDevices);
|
||||
|
||||
if (areDevicesDifferent(oldDevices, filteredDevices)) {
|
||||
logDevices(ignoredDevices, 'Ignored devices on device list changed:');
|
||||
dispatch(updateDeviceList(filteredDevices));
|
||||
}
|
||||
|
||||
resolve(filteredDevices);
|
||||
});
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that an error occurred while trying to obtain a track from a camera.
|
||||
*
|
||||
* @param {Object} error - The device error, as provided by lib-jitsi-meet.
|
||||
* @param {string} error.name - The constant for the type of the error.
|
||||
* @param {string} error.message - Optional additional information about the
|
||||
* error.
|
||||
* @returns {{
|
||||
* type: NOTIFY_CAMERA_ERROR,
|
||||
* error: Object
|
||||
* }}
|
||||
*/
|
||||
export function notifyCameraError(error: Error) {
|
||||
return {
|
||||
type: NOTIFY_CAMERA_ERROR,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that an error occurred while trying to obtain a track from a mic.
|
||||
*
|
||||
* @param {Object} error - The device error, as provided by lib-jitsi-meet.
|
||||
* @param {Object} error.name - The constant for the type of the error.
|
||||
* @param {string} error.message - Optional additional information about the
|
||||
* error.
|
||||
* @returns {{
|
||||
* type: NOTIFY_MIC_ERROR,
|
||||
* error: Object
|
||||
* }}
|
||||
*/
|
||||
export function notifyMicError(error: Error) {
|
||||
return {
|
||||
type: NOTIFY_MIC_ERROR,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new audio input device.
|
||||
* @returns {{
|
||||
* type: SET_AUDIO_INPUT_DEVICE,
|
||||
* deviceId: string
|
||||
* }}
|
||||
*/
|
||||
export function setAudioInputDevice(deviceId: string) {
|
||||
return {
|
||||
type: SET_AUDIO_INPUT_DEVICE,
|
||||
deviceId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio input device id and updates the settings
|
||||
* so they are persisted across sessions.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new audio input device.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setAudioInputDeviceAndUpdateSettings(deviceId: string) {
|
||||
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const deviceLabel = getDeviceLabelById(getState(), deviceId, 'audioInput');
|
||||
|
||||
dispatch(setAudioInputDevice(deviceId));
|
||||
dispatch(updateSettings({
|
||||
userSelectedMicDeviceId: deviceId,
|
||||
userSelectedMicDeviceLabel: deviceLabel
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the output device id.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new output device.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setAudioOutputDevice(deviceId: string) {
|
||||
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const deviceLabel = getDeviceLabelById(getState(), deviceId, 'audioOutput');
|
||||
|
||||
return setAudioOutputDeviceId(deviceId, dispatch, true, deviceLabel);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the currently used video input device.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new video input device.
|
||||
* @returns {{
|
||||
* type: SET_VIDEO_INPUT_DEVICE,
|
||||
* deviceId: string
|
||||
* }}
|
||||
*/
|
||||
export function setVideoInputDevice(deviceId: string) {
|
||||
return {
|
||||
type: SET_VIDEO_INPUT_DEVICE,
|
||||
deviceId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the video input device id and updates the settings
|
||||
* so they are persisted across sessions.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new video input device.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setVideoInputDeviceAndUpdateSettings(deviceId: string) {
|
||||
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const deviceLabel = getDeviceLabelById(getState(), deviceId, 'videoInput');
|
||||
|
||||
dispatch(setVideoInputDevice(deviceId));
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: deviceId,
|
||||
userSelectedCameraDeviceLabel: deviceLabel
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the list of known audio and video devices.
|
||||
*
|
||||
* @param {Array<MediaDeviceInfo>} devices - All known available audio input,
|
||||
* audio output, and video input devices.
|
||||
* @returns {{
|
||||
* type: UPDATE_DEVICE_LIST,
|
||||
* devices: Array<MediaDeviceInfo>
|
||||
* }}
|
||||
*/
|
||||
export function updateDeviceList(devices: MediaDeviceInfo[]) {
|
||||
return {
|
||||
type: UPDATE_DEVICE_LIST,
|
||||
devices
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to check new and old devices for newly added devices and notify.
|
||||
*
|
||||
* @param {Array<MediaDeviceInfo>} newDevices - Array of the new devices.
|
||||
* @param {Array<MediaDeviceInfo>} oldDevices - Array of the old devices.
|
||||
* @returns {{
|
||||
* type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
|
||||
* newDevices: Array<MediaDeviceInfo>,
|
||||
* oldDevices: Array<MediaDeviceInfo>
|
||||
* }}
|
||||
*/
|
||||
export function checkAndNotifyForNewDevice(newDevices: MediaDeviceInfo[], oldDevices: MediaDeviceInfo[]) {
|
||||
return {
|
||||
type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
|
||||
newDevices,
|
||||
oldDevices
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the device permissions have changed.
|
||||
*
|
||||
* @param {Object} permissions - Object with the permissions.
|
||||
* @returns {{
|
||||
* type: DEVICE_PERMISSIONS_CHANGED,
|
||||
* permissions: Object
|
||||
* }}
|
||||
*/
|
||||
export function devicePermissionsChanged(permissions: Object) {
|
||||
return {
|
||||
type: DEVICE_PERMISSIONS_CHANGED,
|
||||
permissions
|
||||
};
|
||||
}
|
||||
11
react/features/base/devices/constants.ts
Normal file
11
react/features/base/devices/constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Prefixes of devices that will be filtered from the device list.
|
||||
*
|
||||
* NOTE: It seems that the filtered devices can't be set
|
||||
* as default device on the OS level and this use case is not handled in the code. If we add more device prefixes that
|
||||
* can be default devices we should make sure to handle the default device use case.
|
||||
*/
|
||||
export const DEVICE_LABEL_PREFIXES_TO_IGNORE = [
|
||||
'Microsoft Teams Audio Device',
|
||||
'ZoomAudioDevice'
|
||||
];
|
||||
19
react/features/base/devices/functions.any.ts
Normal file
19
react/features/base/devices/functions.any.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
|
||||
/**
|
||||
* Returns true if there are devices of a specific type or on native platform.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @param {string} type - The type of device: VideoOutput | audioOutput | audioInput.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasAvailableDevices(state: IReduxState, type: string) {
|
||||
if (state['features/base/devices'] === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const availableDevices = state['features/base/devices'].availableDevices;
|
||||
|
||||
return Number(availableDevices[type as keyof typeof availableDevices]?.length) > 0;
|
||||
}
|
||||
1
react/features/base/devices/functions.native.ts
Normal file
1
react/features/base/devices/functions.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './functions.any';
|
||||
383
react/features/base/devices/functions.web.ts
Normal file
383
react/features/base/devices/functions.web.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
/* eslint-disable require-jsdoc */
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import { updateSettings } from '../settings/actions';
|
||||
import { ISettingsState } from '../settings/reducer';
|
||||
import { setNewAudioOutputDevice } from '../sounds/functions.web';
|
||||
import { parseURLParams } from '../util/parseURLParams';
|
||||
|
||||
import { DEVICE_LABEL_PREFIXES_TO_IGNORE } from './constants';
|
||||
import logger from './logger';
|
||||
import { IDevicesState } from './types';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
const webrtcKindToJitsiKindTranslator = {
|
||||
audioinput: 'audioInput',
|
||||
audiooutput: 'audioOutput',
|
||||
videoinput: 'videoInput'
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: IReduxState) {
|
||||
// TODO: Replace with something that doesn't use APP when the conference.js logic is reactified.
|
||||
if (APP.conference._localTracksInitialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const type of [ 'audioInput', 'audioOutput', 'videoInput' ]) {
|
||||
const availableDevices = state['features/base/devices'].availableDevices;
|
||||
|
||||
if ((availableDevices[type as keyof typeof availableDevices] || []).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.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getAudioOutputDeviceId() {
|
||||
return JitsiMeetJS.mediaDevices.getAudioOutputDevice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the real device id of the default device of the given type.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {*} kind - The type of the device. One of "audioInput",
|
||||
* "audioOutput", and "videoInput". Also supported is all lowercase versions
|
||||
* of the preceding types.
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export function getDefaultDeviceId(state: IReduxState, kind: string) {
|
||||
const kindToSearch = webrtcKindToJitsiKindTranslator[kind as keyof typeof webrtcKindToJitsiKindTranslator] || kind;
|
||||
const availableDevices = state['features/base/devices'].availableDevices;
|
||||
const defaultDevice = (availableDevices[kindToSearch as keyof typeof availableDevices] || [])
|
||||
.find(d => d.deviceId === 'default');
|
||||
|
||||
// Find the device with a matching group id.
|
||||
const matchingDevice = (availableDevices[kindToSearch as keyof typeof availableDevices] || [])
|
||||
.find(d => d.deviceId !== 'default' && d.groupId === defaultDevice?.groupId);
|
||||
|
||||
if (matchingDevice) {
|
||||
return matchingDevice.deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a device with a label that matches the passed label and returns its id.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {string} label - The label.
|
||||
* @param {string} kind - The type of the device. One of "audioInput",
|
||||
* "audioOutput", and "videoInput". Also supported is all lowercase versions
|
||||
* of the preceding types.
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export function getDeviceIdByLabel(state: IReduxState, label: string, kind: string) {
|
||||
const kindToSearch = webrtcKindToJitsiKindTranslator[kind as keyof typeof webrtcKindToJitsiKindTranslator] || kind;
|
||||
|
||||
const availableDevices = state['features/base/devices'].availableDevices;
|
||||
const device
|
||||
= (availableDevices[kindToSearch as keyof typeof availableDevices] || [])
|
||||
.find(d => d.label === label);
|
||||
|
||||
if (device) {
|
||||
return device.deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a device with a label that matches the passed id and returns its label.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {string} id - The device id.
|
||||
* @param {string} kind - The type of the device. One of "audioInput",
|
||||
* "audioOutput", and "videoInput". Also supported is all lowercase versions
|
||||
* of the preceding types.
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export function getDeviceLabelById(state: IReduxState, id: string, kind: string) {
|
||||
const kindToSearch = webrtcKindToJitsiKindTranslator[kind as keyof typeof webrtcKindToJitsiKindTranslator] || kind;
|
||||
|
||||
const availableDevices = state['features/base/devices'].availableDevices;
|
||||
const device
|
||||
= (availableDevices[kindToSearch as keyof typeof availableDevices] || [])
|
||||
.find(d => d.deviceId === id);
|
||||
|
||||
if (device) {
|
||||
return device.label;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the devices set in the URL.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
export function getDevicesFromURL(state: IReduxState) {
|
||||
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: IDevicesState['availableDevices'] = {};
|
||||
|
||||
audioOutput && (devices.audioOutput = audioOutput);
|
||||
videoInput && (devices.videoInput = videoInput);
|
||||
audioInput && (devices.audioInput = audioInput);
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of media devices into an object organized by device kind.
|
||||
*
|
||||
* @param {Array<MediaDeviceInfo>} devices - Available media devices.
|
||||
* @private
|
||||
* @returns {Object} An object with the media devices split by type. The keys
|
||||
* are device type and the values are arrays with devices matching the device
|
||||
* type.
|
||||
*/
|
||||
// @ts-ignore
|
||||
export function groupDevicesByKind(devices: MediaDeviceInfo[]): IDevicesState['availableDevices'] {
|
||||
return {
|
||||
audioInput: devices.filter(device => device.kind === 'audioinput'),
|
||||
audioOutput: devices.filter(device => device.kind === 'audiooutput'),
|
||||
videoInput: devices.filter(device => device.kind === 'videoinput')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the devices that start with one of the prefixes from DEVICE_LABEL_PREFIXES_TO_IGNORE.
|
||||
*
|
||||
* @param {MediaDeviceInfo[]} devices - The devices to be filtered.
|
||||
* @returns {MediaDeviceInfo[]} - The filtered devices.
|
||||
*/
|
||||
// @ts-ignore
|
||||
export function filterIgnoredDevices(devices: MediaDeviceInfo[] = []) {
|
||||
|
||||
// @ts-ignore
|
||||
const ignoredDevices: MediaDeviceInfo[] = [];
|
||||
const filteredDevices = devices.filter(device => {
|
||||
if (!device.label) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DEVICE_LABEL_PREFIXES_TO_IGNORE.find(prefix => device.label?.startsWith(prefix))) {
|
||||
ignoredDevices.push(device);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
filteredDevices,
|
||||
ignoredDevices
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the passed device arrays are different.
|
||||
*
|
||||
* @param {MediaDeviceInfo[]} devices1 - Array with devices to be compared.
|
||||
* @param {MediaDeviceInfo[]} devices2 - Array with devices to be compared.
|
||||
* @returns {boolean} - True if the device arrays are different and false otherwise.
|
||||
*/
|
||||
// @ts-ignore
|
||||
export function areDevicesDifferent(devices1: MediaDeviceInfo[] = [], devices2: MediaDeviceInfo[] = []) {
|
||||
if (devices1.length !== devices2.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < devices1.length; i++) {
|
||||
const device1 = devices1[i];
|
||||
const found = devices2.find(({ deviceId, groupId, kind, label }) =>
|
||||
device1.deviceId === deviceId
|
||||
&& device1.groupId === groupId
|
||||
&& device1.kind === kind
|
||||
&& device1.label === label
|
||||
);
|
||||
|
||||
if (!found) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the availableDevices from redux.
|
||||
*
|
||||
* @param {IDevicesState.availableDevices} devices - The available devices from redux.
|
||||
* @returns {MediaDeviceInfo[]} - The flattened array of devices.
|
||||
*/
|
||||
export function flattenAvailableDevices(
|
||||
{ audioInput = [], audioOutput = [], videoInput = [] }: IDevicesState['availableDevices']) {
|
||||
return audioInput.concat(audioOutput).concat(videoInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* We want to strip any device details that are not very user friendly, like usb ids put in brackets at the end.
|
||||
*
|
||||
* @param {string} label - Device label to format.
|
||||
*
|
||||
* @returns {string} - Formatted string.
|
||||
*/
|
||||
export function formatDeviceLabel(label: string) {
|
||||
|
||||
let formattedLabel = label;
|
||||
|
||||
// Remove braked description at the end as it contains non user friendly strings i.e.
|
||||
// Microsoft® LifeCam HD-3000 (045e:0779:31dg:d1231)
|
||||
const ix = formattedLabel.lastIndexOf('(');
|
||||
|
||||
if (ix !== -1) {
|
||||
formattedLabel = formattedLabel.substr(0, ix);
|
||||
}
|
||||
|
||||
return formattedLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of objects containing all the microphone device ids and labels.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function getAudioInputDeviceData(state: IReduxState) {
|
||||
return state['features/base/devices'].availableDevices.audioInput?.map(
|
||||
({ deviceId, label }) => {
|
||||
return {
|
||||
deviceId,
|
||||
label
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of objectes containing all the output device ids and labels.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function getAudioOutputDeviceData(state: IReduxState) {
|
||||
return state['features/base/devices'].availableDevices.audioOutput?.map(
|
||||
({ deviceId, label }) => {
|
||||
return {
|
||||
deviceId,
|
||||
label
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all the camera device ids.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getVideoDeviceIds(state: IReduxState) {
|
||||
return state['features/base/devices'].availableDevices.videoInput?.map(({ deviceId }) => deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of device info objects into string.
|
||||
*
|
||||
* @param {MediaDeviceInfo[]} devices - The devices.
|
||||
* @returns {string}
|
||||
*/
|
||||
// @ts-ignore
|
||||
function devicesToStr(devices?: MediaDeviceInfo[]) {
|
||||
return devices?.map(device => `\t\t${device.label}[${device.deviceId}]`).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an array of devices.
|
||||
*
|
||||
* @param {MediaDeviceInfo[]} devices - The array of devices.
|
||||
* @param {string} title - The title that will be printed in the log.
|
||||
* @returns {void}
|
||||
*/
|
||||
// @ts-ignore
|
||||
export function logDevices(devices: MediaDeviceInfo[], title = '') {
|
||||
const deviceList = groupDevicesByKind(devices);
|
||||
const audioInputs = devicesToStr(deviceList.audioInput);
|
||||
const audioOutputs = devicesToStr(deviceList.audioOutput);
|
||||
const videoInputs = devicesToStr(deviceList.videoInput);
|
||||
|
||||
logger.debug(`${title}:\n`
|
||||
+ `audioInput:\n${audioInputs}\n`
|
||||
+ `audioOutput:\n${audioOutputs}\n`
|
||||
+ `videoInput:\n${videoInputs}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set device id of the audio output device which is currently in use.
|
||||
* Empty string stands for default device.
|
||||
*
|
||||
* @param {string} newId - New audio output device id.
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @param {boolean} userSelection - Whether this is a user selection update.
|
||||
* @param {?string} newLabel - New audio output device label to store.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function setAudioOutputDeviceId(
|
||||
newId = 'default',
|
||||
dispatch: IStore['dispatch'],
|
||||
userSelection = false,
|
||||
newLabel?: string): Promise<any> {
|
||||
|
||||
logger.debug(`setAudioOutputDevice: ${String(newLabel)}[${newId}]`);
|
||||
|
||||
if (!JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
|
||||
logger.warn('Adjusting audio output is not supported');
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId)
|
||||
.then(() => {
|
||||
dispatch(setNewAudioOutputDevice(newId));
|
||||
const newSettings: Partial<ISettingsState> = {
|
||||
audioOutputDeviceId: newId,
|
||||
userSelectedAudioOutputDeviceId: undefined,
|
||||
userSelectedAudioOutputDeviceLabel: undefined
|
||||
};
|
||||
|
||||
if (userSelection) {
|
||||
newSettings.userSelectedAudioOutputDeviceId = newId;
|
||||
newSettings.userSelectedAudioOutputDeviceLabel = newLabel;
|
||||
} else {
|
||||
// a flow workaround, I needed to add 'userSelectedAudioOutputDeviceId: undefined'
|
||||
delete newSettings.userSelectedAudioOutputDeviceId;
|
||||
delete newSettings.userSelectedAudioOutputDeviceLabel;
|
||||
}
|
||||
|
||||
return dispatch(updateSettings(newSettings));
|
||||
});
|
||||
}
|
||||
3
react/features/base/devices/logger.ts
Normal file
3
react/features/base/devices/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/devices');
|
||||
349
react/features/base/devices/middleware.web.ts
Normal file
349
react/features/base/devices/middleware.web.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { processExternalDeviceRequest } from '../../device-selection/functions';
|
||||
import { showNotification, showWarningNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app/actionTypes';
|
||||
import { isMobileBrowser } from '../environment/utils';
|
||||
import JitsiMeetJS, { JitsiMediaDevicesEvents, JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../media/constants';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { updateSettings } from '../settings/actions';
|
||||
import { getLocalTrack } from '../tracks/functions';
|
||||
|
||||
import {
|
||||
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
|
||||
NOTIFY_CAMERA_ERROR,
|
||||
NOTIFY_MIC_ERROR,
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
import {
|
||||
devicePermissionsChanged,
|
||||
removePendingDeviceRequests,
|
||||
setAudioInputDevice,
|
||||
setVideoInputDevice
|
||||
} from './actions';
|
||||
import {
|
||||
areDeviceLabelsInitialized,
|
||||
formatDeviceLabel,
|
||||
logDevices,
|
||||
setAudioOutputDeviceId
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
|
||||
microphone: {
|
||||
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.micConstraintFailedError',
|
||||
[JitsiTrackErrors.GENERAL]: 'dialog.micUnknownError',
|
||||
[JitsiTrackErrors.NOT_FOUND]: 'dialog.micNotFoundError',
|
||||
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError',
|
||||
[JitsiTrackErrors.TIMEOUT]: 'dialog.micTimeoutError'
|
||||
},
|
||||
camera: {
|
||||
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.cameraConstraintFailedError',
|
||||
[JitsiTrackErrors.GENERAL]: 'dialog.cameraUnknownError',
|
||||
[JitsiTrackErrors.NOT_FOUND]: 'dialog.cameraNotFoundError',
|
||||
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.cameraPermissionDeniedError',
|
||||
[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError',
|
||||
[JitsiTrackErrors.TIMEOUT]: 'dialog.cameraTimeoutError'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A listener for device permissions changed reported from lib-jitsi-meet.
|
||||
*/
|
||||
let permissionsListener: Function | undefined;
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature base/devices.
|
||||
*
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT: {
|
||||
const _permissionsListener = (permissions: Object) => {
|
||||
store.dispatch(devicePermissionsChanged(permissions));
|
||||
};
|
||||
const { mediaDevices } = JitsiMeetJS;
|
||||
|
||||
permissionsListener = _permissionsListener;
|
||||
mediaDevices.addEventListener(JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
|
||||
Promise.all([
|
||||
mediaDevices.isDevicePermissionGranted('audio'),
|
||||
mediaDevices.isDevicePermissionGranted('video')
|
||||
])
|
||||
.then(results => {
|
||||
_permissionsListener({
|
||||
audio: results[0],
|
||||
video: results[1]
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
break;
|
||||
}
|
||||
case APP_WILL_UNMOUNT:
|
||||
if (typeof permissionsListener === 'function') {
|
||||
JitsiMeetJS.mediaDevices.removeEventListener(
|
||||
JitsiMediaDevicesEvents.PERMISSIONS_CHANGED, permissionsListener);
|
||||
permissionsListener = undefined;
|
||||
}
|
||||
break;
|
||||
case NOTIFY_CAMERA_ERROR: {
|
||||
if (!action.error) {
|
||||
break;
|
||||
}
|
||||
|
||||
const { message, name } = action.error;
|
||||
|
||||
const cameraJitsiTrackErrorMsg
|
||||
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
|
||||
const cameraErrorMsg = cameraJitsiTrackErrorMsg
|
||||
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.camera[JitsiTrackErrors.GENERAL];
|
||||
const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
|
||||
const titleKey = name === JitsiTrackErrors.PERMISSION_DENIED
|
||||
? 'deviceError.cameraPermission' : 'deviceError.cameraError';
|
||||
|
||||
store.dispatch(showWarningNotification({
|
||||
description: additionalCameraErrorMsg,
|
||||
descriptionKey: cameraErrorMsg,
|
||||
titleKey
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(setDeviceStatusWarning(titleKey));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case NOTIFY_MIC_ERROR: {
|
||||
if (!action.error) {
|
||||
break;
|
||||
}
|
||||
|
||||
const { message, name } = action.error;
|
||||
|
||||
const micJitsiTrackErrorMsg
|
||||
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name];
|
||||
const micErrorMsg = micJitsiTrackErrorMsg
|
||||
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.microphone[JitsiTrackErrors.GENERAL];
|
||||
const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message;
|
||||
const titleKey = name === JitsiTrackErrors.PERMISSION_DENIED
|
||||
? 'deviceError.microphonePermission'
|
||||
: 'deviceError.microphoneError';
|
||||
|
||||
store.dispatch(showWarningNotification({
|
||||
description: additionalMicErrorMsg,
|
||||
descriptionKey: micErrorMsg,
|
||||
titleKey
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(setDeviceStatusWarning(titleKey));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case SET_AUDIO_INPUT_DEVICE:
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(replaceAudioTrackById(action.deviceId));
|
||||
} else {
|
||||
APP.conference.onAudioDeviceChanged(action.deviceId);
|
||||
}
|
||||
break;
|
||||
case SET_VIDEO_INPUT_DEVICE: {
|
||||
const localTrack = getLocalTrack(store.getState()['features/base/tracks'], MEDIA_TYPE.VIDEO);
|
||||
|
||||
// on mobile devices the video stream has to be stopped before replacing it
|
||||
if (isMobileBrowser() && localTrack && !localTrack.muted) {
|
||||
localTrack.jitsiTrack.stopStream();
|
||||
}
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(replaceVideoTrackById(action.deviceId));
|
||||
} else {
|
||||
APP.conference.onVideoDeviceChanged(action.deviceId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UPDATE_DEVICE_LIST:
|
||||
logDevices(action.devices, 'Device list updated');
|
||||
if (areDeviceLabelsInitialized(store.getState())) {
|
||||
return _processPendingRequests(store, next, action);
|
||||
}
|
||||
break;
|
||||
case CHECK_AND_NOTIFY_FOR_NEW_DEVICE:
|
||||
_checkAndNotifyForNewDevice(store, action.newDevices, action.oldDevices);
|
||||
break;
|
||||
}
|
||||
|
||||
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 _processPendingRequests({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
const state = getState();
|
||||
const { pendingRequests } = state['features/base/devices'];
|
||||
|
||||
if (!pendingRequests || pendingRequests.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
pendingRequests.forEach((request: any) => {
|
||||
processExternalDeviceRequest(
|
||||
dispatch,
|
||||
getState,
|
||||
request,
|
||||
request.responseCallback);
|
||||
});
|
||||
dispatch(removePendingDeviceRequests());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a new device by comparing new and old array of devices and dispatches
|
||||
* notification with the new device. For new devices with same groupId only one
|
||||
* notification will be shown, this is so to avoid showing multiple notifications
|
||||
* for audio input and audio output devices.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {MediaDeviceInfo[]} newDevices - The array of new devices we received.
|
||||
* @param {MediaDeviceInfo[]} oldDevices - The array of the old devices we have.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _checkAndNotifyForNewDevice(store: IStore, newDevices: MediaDeviceInfo[], oldDevices: MediaDeviceInfo[]) {
|
||||
const { dispatch } = store;
|
||||
|
||||
// let's intersect both newDevices and oldDevices and handle thew newly
|
||||
// added devices
|
||||
const onlyNewDevices = newDevices.filter(
|
||||
nDevice => !oldDevices.find(
|
||||
device => device.deviceId === nDevice.deviceId));
|
||||
|
||||
// we group devices by groupID which normally is the grouping by physical device
|
||||
// plugging in headset we provide normally two device, one input and one output
|
||||
// and we want to show only one notification for this physical audio device
|
||||
const devicesGroupBy: {
|
||||
[key: string]: MediaDeviceInfo[];
|
||||
} = onlyNewDevices.reduce((accumulated: any, value) => {
|
||||
accumulated[value.groupId] = accumulated[value.groupId] || [];
|
||||
accumulated[value.groupId].push(value);
|
||||
|
||||
return accumulated;
|
||||
}, {});
|
||||
|
||||
Object.values(devicesGroupBy).forEach(devicesArray => {
|
||||
|
||||
if (devicesArray.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// let's get the first device as a reference, we will use it for
|
||||
// label and type
|
||||
const newDevice = devicesArray[0];
|
||||
|
||||
// we want to strip any device details that are not very
|
||||
// user friendly, like usb ids put in brackets at the end
|
||||
const description = formatDeviceLabel(newDevice.label);
|
||||
|
||||
let titleKey;
|
||||
|
||||
switch (newDevice.kind) {
|
||||
case 'videoinput': {
|
||||
titleKey = 'notify.newDeviceCameraTitle';
|
||||
break;
|
||||
}
|
||||
case 'audioinput' :
|
||||
case 'audiooutput': {
|
||||
titleKey = 'notify.newDeviceAudioTitle';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isPrejoinPageVisible(store.getState())) {
|
||||
dispatch(showNotification({
|
||||
description,
|
||||
titleKey,
|
||||
customActionNameKey: [ 'notify.newDeviceAction' ],
|
||||
customActionHandler: [ _useDevice.bind(undefined, store, devicesArray) ]
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a device to be currently used, selected by the user.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Array<MediaDeviceInfo|InputDeviceInfo>} devices - The devices to save.
|
||||
* @returns {boolean} - Returns true in order notifications to be dismissed.
|
||||
* @private
|
||||
*/
|
||||
function _useDevice({ dispatch }: IStore, devices: MediaDeviceInfo[]) {
|
||||
devices.forEach(device => {
|
||||
switch (device.kind) {
|
||||
case 'videoinput': {
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: device.deviceId,
|
||||
userSelectedCameraDeviceLabel: device.label
|
||||
}));
|
||||
|
||||
dispatch(setVideoInputDevice(device.deviceId));
|
||||
break;
|
||||
}
|
||||
case 'audioinput': {
|
||||
dispatch(updateSettings({
|
||||
userSelectedMicDeviceId: device.deviceId,
|
||||
userSelectedMicDeviceLabel: device.label
|
||||
}));
|
||||
|
||||
dispatch(setAudioInputDevice(device.deviceId));
|
||||
break;
|
||||
}
|
||||
case 'audiooutput': {
|
||||
setAudioOutputDeviceId(
|
||||
device.deviceId,
|
||||
dispatch,
|
||||
true,
|
||||
device.label)
|
||||
.then(() => logger.log('changed audio output device'))
|
||||
.catch(err => {
|
||||
logger.warn(
|
||||
'Failed to change audio output device.',
|
||||
'Default or previously set audio output device will',
|
||||
' be used instead.',
|
||||
err);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
88
react/features/base/devices/reducer.web.ts
Normal file
88
react/features/base/devices/reducer.web.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
ADD_PENDING_DEVICE_REQUEST,
|
||||
DEVICE_PERMISSIONS_CHANGED,
|
||||
REMOVE_PENDING_DEVICE_REQUESTS,
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
UPDATE_DEVICE_LIST
|
||||
} from './actionTypes';
|
||||
import { groupDevicesByKind } from './functions.web';
|
||||
import logger from './logger';
|
||||
import { IDevicesState } from './types';
|
||||
|
||||
|
||||
const DEFAULT_STATE: IDevicesState = {
|
||||
availableDevices: {
|
||||
audioInput: [],
|
||||
audioOutput: [],
|
||||
videoInput: []
|
||||
},
|
||||
pendingRequests: [],
|
||||
permissions: {
|
||||
audio: false,
|
||||
video: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for actions which changes the state of known and used devices.
|
||||
*
|
||||
* @param {IDevicesState} state - The Redux state of the feature features/base/devices.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @param {Array<MediaDeviceInfo>} action.devices - All available audio and
|
||||
* video devices.
|
||||
* @returns {Object}
|
||||
*/
|
||||
ReducerRegistry.register<IDevicesState>(
|
||||
'features/base/devices',
|
||||
(state = DEFAULT_STATE, action): IDevicesState => {
|
||||
switch (action.type) {
|
||||
case UPDATE_DEVICE_LIST: {
|
||||
const deviceList = groupDevicesByKind(action.devices);
|
||||
|
||||
return {
|
||||
...state,
|
||||
availableDevices: 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.
|
||||
case SET_AUDIO_INPUT_DEVICE: {
|
||||
logger.debug(`set audio input device: ${action.deviceId}`);
|
||||
|
||||
return state;
|
||||
}
|
||||
case SET_VIDEO_INPUT_DEVICE: {
|
||||
logger.debug(`set video input device: ${action.deviceId}`);
|
||||
|
||||
return state;
|
||||
}
|
||||
case DEVICE_PERMISSIONS_CHANGED: {
|
||||
return {
|
||||
...state,
|
||||
permissions: action.permissions
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
17
react/features/base/devices/types.ts
Normal file
17
react/features/base/devices/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
export interface IDevicesState {
|
||||
availableDevices: {
|
||||
// @ts-ignore
|
||||
audioInput?: MediaDeviceInfo[];
|
||||
// @ts-ignore
|
||||
audioOutput?: MediaDeviceInfo[];
|
||||
// @ts-ignore
|
||||
videoInput?: MediaDeviceInfo[];
|
||||
};
|
||||
pendingRequests: any[];
|
||||
permissions: {
|
||||
audio: boolean;
|
||||
video: boolean;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user