feat(devices): Filter MS Teams Audio device

This commit is contained in:
Hristo Terezov
2023-09-21 20:06:55 -05:00
parent 54d052de73
commit c025102511
6 changed files with 143 additions and 33 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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