From c02510251128a9bed0d327f8b8e31d02305ee737 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Thu, 21 Sep 2023 20:06:55 -0500 Subject: [PATCH] feat(devices): Filter MS Teams Audio device --- conference.js | 27 +++-- modules/devices/mediaDeviceHelper.js | 3 +- react/features/base/devices/actions.web.ts | 16 ++- react/features/base/devices/constants.ts | 8 ++ react/features/base/devices/functions.web.ts | 98 +++++++++++++++++++ react/features/base/devices/middleware.web.ts | 24 +---- 6 files changed, 143 insertions(+), 33 deletions(-) create mode 100644 react/features/base/devices/constants.ts diff --git a/conference.js b/conference.js index 1e50eb14c1..587aaefe81 100644 --- a/conference.js +++ b/conference.js @@ -65,7 +65,11 @@ import { updateDeviceList } from './react/features/base/devices/actions.web'; import { + areDevicesDifferent, + filterIgnoredDevices, + flattenAvailableDevices, getDefaultDeviceId, + logDevices, setAudioOutputDeviceId } from './react/features/base/devices/functions.web'; import { @@ -2241,19 +2245,28 @@ export default { * @returns {Promise} */ async _onDeviceListChanged(devices) { - const oldDevices = APP.store.getState()['features/base/devices'].availableDevices; - const localAudio = getLocalJitsiAudioTrack(APP.store.getState()); - const localVideo = getLocalJitsiVideoTrack(APP.store.getState()); + const state = APP.store.getState(); + const { filteredDevices, ignoredDevices } = filterIgnoredDevices(devices); + const oldDevices = state['features/base/devices'].availableDevices; - APP.store.dispatch(updateDeviceList(devices)); + if (!areDevicesDifferent(flattenAvailableDevices(oldDevices), filteredDevices)) { + return Promise.resolve(); + } + + logDevices(ignoredDevices, 'Ignored devices on device list changed:'); + + const localAudio = getLocalJitsiAudioTrack(state); + const localVideo = getLocalJitsiVideoTrack(state); + + APP.store.dispatch(updateDeviceList(filteredDevices)); // Firefox users can choose their preferred device in the gUM prompt. In that case // we should respect that and not attempt to switch to the preferred device from // our settings. - const newLabelsOnly = mediaDeviceHelper.newDeviceListAddedLabelsOnly(oldDevices, devices); + const newLabelsOnly = mediaDeviceHelper.newDeviceListAddedLabelsOnly(oldDevices, filteredDevices); const newDevices = mediaDeviceHelper.getNewMediaDevicesAfterDeviceListChanged( - devices, + filteredDevices, localVideo, localAudio, newLabelsOnly); @@ -2386,7 +2399,7 @@ export default { return Promise.all(promises) .then(() => { - APP.UI.onAvailableDevicesChanged(devices); + APP.UI.onAvailableDevicesChanged(filteredDevices); }); }, diff --git a/modules/devices/mediaDeviceHelper.js b/modules/devices/mediaDeviceHelper.js index cde05030eb..23f2b759bd 100644 --- a/modules/devices/mediaDeviceHelper.js +++ b/modules/devices/mediaDeviceHelper.js @@ -5,6 +5,7 @@ import { notifyMicError } from '../../react/features/base/devices/actions.web'; import { + flattenAvailableDevices, getAudioOutputDeviceId } from '../../react/features/base/devices/functions.web'; import { updateSettings } from '../../react/features/base/settings/actions'; @@ -186,7 +187,7 @@ export default { * @returns {boolean} */ newDeviceListAddedLabelsOnly(oldDevices, newDevices) { - const oldDevicesFlattend = oldDevices.audioInput.concat(oldDevices.audioOutput).concat(oldDevices.videoInput); + const oldDevicesFlattend = flattenAvailableDevices(oldDevices); if (oldDevicesFlattend.length !== newDevices.length) { return false; diff --git a/react/features/base/devices/actions.web.ts b/react/features/base/devices/actions.web.ts index 9c69d0f93e..3b4fc5aad9 100644 --- a/react/features/base/devices/actions.web.ts +++ b/react/features/base/devices/actions.web.ts @@ -16,9 +16,13 @@ import { } from './actionTypes'; import { areDeviceLabelsInitialized, + areDevicesDifferent, + filterIgnoredDevices, + flattenAvailableDevices, getDeviceIdByLabel, getDeviceLabelById, getDevicesFromURL, + logDevices, setAudioOutputDeviceId } from './functions'; import logger from './logger'; @@ -137,15 +141,21 @@ export function configureInitialDevices() { * @returns {Function} */ export function getAvailableDevices() { - return (dispatch: IStore['dispatch']) => new Promise(resolve => { + return (dispatch: IStore['dispatch'], getState: IStore['getState']) => new Promise(resolve => { const { mediaDevices } = JitsiMeetJS; if (mediaDevices.isDeviceListAvailable() && mediaDevices.isDeviceChangeAvailable()) { mediaDevices.enumerateDevices((devices: MediaDeviceInfo[]) => { - dispatch(updateDeviceList(devices)); + const { filteredDevices, ignoredDevices } = filterIgnoredDevices(devices); + const oldDevices = flattenAvailableDevices(getState()['features/base/devices'].availableDevices); - resolve(devices); + if (areDevicesDifferent(oldDevices, filteredDevices)) { + logDevices(ignoredDevices, 'Ignored devices on device list changed:'); + dispatch(updateDeviceList(filteredDevices)); + } + + resolve(filteredDevices); }); } else { resolve([]); diff --git a/react/features/base/devices/constants.ts b/react/features/base/devices/constants.ts new file mode 100644 index 0000000000..9d04fa5480 --- /dev/null +++ b/react/features/base/devices/constants.ts @@ -0,0 +1,8 @@ +/** + * Prefixes of devices that will be filtered from the device list. + * + * NOTE: Currently we filter only 'Microsoft Teams Audio Device' virtual device. It seems that it 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' ]; diff --git a/react/features/base/devices/functions.web.ts b/react/features/base/devices/functions.web.ts index ab7079a885..34c28436e0 100644 --- a/react/features/base/devices/functions.web.ts +++ b/react/features/base/devices/functions.web.ts @@ -5,6 +5,7 @@ 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'; @@ -176,6 +177,74 @@ export function filterAudioDevices(devices: MediaDeviceInfo[]) { return devices.filter(device => device.kind === 'audioinput'); } +/** + * 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. + */ +export function filterIgnoredDevices(devices: MediaDeviceInfo[] = []) { + 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. +*/ +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. * @@ -240,6 +309,35 @@ 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} + */ +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} + */ +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. diff --git a/react/features/base/devices/middleware.web.ts b/react/features/base/devices/middleware.web.ts index dfe4e8f8a2..dce6c442eb 100644 --- a/react/features/base/devices/middleware.web.ts +++ b/react/features/base/devices/middleware.web.ts @@ -33,11 +33,10 @@ import { import { areDeviceLabelsInitialized, formatDeviceLabel, - groupDevicesByKind, + logDevices, setAudioOutputDeviceId } from './functions'; import logger from './logger'; -import { IDevicesState } from './types'; const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = { microphone: { @@ -62,25 +61,6 @@ const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = { */ let permissionsListener: Function | undefined; -/** - * Logs the current device list. - * - * @param {Object} deviceList - Whatever is returned by {@link groupDevicesByKind}. - * @returns {string} - */ -function logDeviceList(deviceList: IDevicesState['availableDevices']) { - const devicesToStr = (list?: MediaDeviceInfo[]) => - list?.map(device => `\t\t${device.label}[${device.deviceId}]`).join('\n'); - const audioInputs = devicesToStr(deviceList.audioInput); - const audioOutputs = devicesToStr(deviceList.audioOutput); - const videoInputs = devicesToStr(deviceList.videoInput); - - logger.debug('Device list updated:\n' - + `audioInput:\n${audioInputs}\n` - + `audioOutput:\n${audioOutputs}\n` - + `videoInput:\n${videoInputs}`); -} - /** * Implements the middleware of the feature base/devices. * @@ -199,7 +179,7 @@ MiddlewareRegistry.register(store => next => action => { break; } case UPDATE_DEVICE_LIST: - logDeviceList(groupDevicesByKind(action.devices)); + logDevices(action.devices, 'Device list updated'); if (areDeviceLabelsInitialized(store.getState())) { return _processPendingRequests(store, next, action); }