// @flow import JitsiMeetJS from '../lib-jitsi-meet'; import { updateSettings } from '../settings'; import { parseURLParams } from '../util'; import logger from './logger'; declare var APP: Object; 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: Object) { // 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' ]) { if ((state['features/base/devices'].availableDevices[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. * * @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: Object, kind: string) { const kindToSearch = webrtcKindToJitsiKindTranslator[kind] || kind; const defaultDevice = (state['features/base/devices'].availableDevices[kindToSearch] || []) .find(d => d.deviceId === 'default'); // Find the device with a matching group id. const matchingDevice = (state['features/base/devices'].availableDevices[kindToSearch] || []) .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: Object, label: string, kind: string) { const kindToSearch = webrtcKindToJitsiKindTranslator[kind] || kind; const device = (state['features/base/devices'].availableDevices[kindToSearch] || []) .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: Object, id: string, kind: string) { const kindToSearch = webrtcKindToJitsiKindTranslator[kind] || kind; const device = (state['features/base/devices'].availableDevices[kindToSearch] || []) .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: 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; } /** * Converts an array of media devices into an object organized by device kind. * * @param {Array} 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. */ export function groupDevicesByKind(devices: Object[]): Object { return { audioInput: devices.filter(device => device.kind === 'audioinput'), audioOutput: devices.filter(device => device.kind === 'audiooutput'), videoInput: devices.filter(device => device.kind === 'videoinput') }; } /** * Filters audio devices from a list of MediaDeviceInfo objects. * * @param {Array} devices - Unfiltered media devices. * @private * @returns {Array} Filtered audio devices. */ export function filterAudioDevices(devices: Object[]): Object { return devices.filter(device => device.kind === 'audioinput'); } /** * 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: Object) { 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: Object) { 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: Object) { return state['features/base/devices'].availableDevices.videoInput.map(({ deviceId }) => deviceId); } /** * 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: Object, type: string) { if (state['features/base/devices'] === undefined) { return true; } return state['features/base/devices'].availableDevices[type].length > 0; } /** * 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: string = 'default', dispatch: Function, userSelection: boolean = false, newLabel: ?string): Promise<*> { 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(() => { const newSettings = { 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)); }); }