Fall back to using label for preferred devices (#4171)

* Skips setting undefined device id to sink in audio preview.

* Fallbacks to use labels for user selected devices.

* Fixes comment.
This commit is contained in:
Дамян Минков 2019-05-07 09:53:01 +01:00 committed by GitHub
parent 3f4a71c26d
commit c040b3a7dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 261 additions and 36 deletions

View File

@ -718,13 +718,21 @@ export default {
this.roomName = options.roomName;
return (
this.createInitialLocalTracksAndConnect(
// Initialize the device list first. This way, when creating tracks
// based on preferred devices, loose label matching can be done in
// cases where the exact ID match is no longer available, such as
// when the camera device has switched USB ports.
this._initDeviceList()
.catch(error => logger.warn(
'initial device list initialization failed', error))
.then(() => this.createInitialLocalTracksAndConnect(
options.roomName, {
startAudioOnly: config.startAudioOnly,
startScreenSharing: config.startScreenSharing,
startWithAudioMuted: config.startWithAudioMuted,
startWithVideoMuted: config.startWithVideoMuted
})
}))
.then(([ tracks, con ]) => {
tracks.forEach(track => {
if ((track.isAudioTrack() && this.isLocalAudioMuted())
@ -769,7 +777,10 @@ export default {
this.setVideoMuteStatus(true);
}
this._initDeviceList();
// Initialize device list a second time to ensure device labels
// get populated in case of an initial gUM acceptance; otherwise
// they may remain as empty strings.
this._initDeviceList(true);
if (config.iAmRecorder) {
this.recorder = new Recorder();
@ -2277,20 +2288,23 @@ export default {
},
/**
* Inits list of current devices and event listener for device change.
* Updates the list of current devices.
* @param {boolean} setDeviceListChangeHandler - Whether to add the deviceList change handlers.
* @private
* @returns {Promise}
*/
_initDeviceList() {
_initDeviceList(setDeviceListChangeHandler = false) {
const { mediaDevices } = JitsiMeetJS;
if (mediaDevices.isDeviceListAvailable()
&& mediaDevices.isDeviceChangeAvailable()) {
this.deviceChangeListener = devices =>
window.setTimeout(() => this._onDeviceListChanged(devices), 0);
mediaDevices.addEventListener(
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
this.deviceChangeListener);
if (setDeviceListChangeHandler) {
this.deviceChangeListener = devices =>
window.setTimeout(() => this._onDeviceListChanged(devices), 0);
mediaDevices.addEventListener(
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
this.deviceChangeListener);
}
const { dispatch } = APP.store;

View File

@ -1,6 +1,11 @@
/* global APP, JitsiMeetJS */
import { getAudioOutputDeviceId } from '../../react/features/base/devices';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId,
getUserSelectedOutputDeviceId
} from '../../react/features/base/settings';
/**
* Determines if currently selected audio output device should be changed after
@ -26,8 +31,7 @@ function getNewAudioOutputDevice(newDevices) {
return 'default';
}
const settings = APP.store.getState()['features/base/settings'];
const preferredAudioOutputDeviceId = settings.userSelectedAudioOutputDeviceId;
const preferredAudioOutputDeviceId = getUserSelectedOutputDeviceId(APP.store.getState());
// if the preferred one is not the selected and is available in the new devices
// we want to use it as it was just added
@ -49,8 +53,7 @@ function getNewAudioOutputDevice(newDevices) {
function getNewAudioInputDevice(newDevices, localAudio) {
const availableAudioInputDevices = newDevices.filter(
d => d.kind === 'audioinput');
const settings = APP.store.getState()['features/base/settings'];
const selectedAudioInputDeviceId = settings.userSelectedMicDeviceId;
const selectedAudioInputDeviceId = getUserSelectedMicDeviceId(APP.store.getState());
const selectedAudioInputDevice = availableAudioInputDevices.find(
d => d.deviceId === selectedAudioInputDeviceId);
@ -88,8 +91,7 @@ function getNewAudioInputDevice(newDevices, localAudio) {
function getNewVideoInputDevice(newDevices, localVideo) {
const availableVideoInputDevices = newDevices.filter(
d => d.kind === 'videoinput');
const settings = APP.store.getState()['features/base/settings'];
const selectedVideoInputDeviceId = settings.userSelectedCameraDeviceId;
const selectedVideoInputDeviceId = getUserSelectedCameraDeviceId(APP.store.getState());
const selectedVideoInputDevice = availableVideoInputDevices.find(
d => d.deviceId === selectedVideoInputDeviceId);

View File

@ -1,5 +1,8 @@
import JitsiMeetJS from '../lib-jitsi-meet';
import { updateSettings } from '../settings';
import {
getUserSelectedOutputDeviceId,
updateSettings
} from '../settings';
import {
ADD_PENDING_DEVICE_REQUEST,
@ -91,8 +94,7 @@ export function configureInitialDevices() {
return updateSettingsPromise
.then(() => {
const { userSelectedAudioOutputDeviceId }
= getState()['features/base/settings'];
const userSelectedAudioOutputDeviceId = getUserSelectedOutputDeviceId(getState());
return setAudioOutputDeviceId(userSelectedAudioOutputDeviceId, dispatch)
.catch(ex => logger.warn(`Failed to set audio output device.

View File

@ -67,6 +67,34 @@ export function getDeviceIdByLabel(state: Object, label: string, kind: string) {
}
}
/**
* 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 webrtcKindToJitsiKindTranslator = {
audioinput: 'audioInput',
audiooutput: 'audioOutput',
videoinput: 'videoInput'
};
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.
*
@ -118,24 +146,29 @@ export function groupDevicesByKind(devices: Object[]): Object {
* @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): Promise<*> {
userSelection: boolean = false,
newLabel: ?string): Promise<*> {
return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId)
.then(() => {
const newSettings = {
audioOutputDeviceId: newId,
userSelectedAudioOutputDeviceId: undefined
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));

View File

@ -147,7 +147,8 @@ function _useDevice({ dispatch }, device) {
switch (device.kind) {
case 'videoinput': {
dispatch(updateSettings({
userSelectedCameraDeviceId: device.deviceId
userSelectedCameraDeviceId: device.deviceId,
userSelectedCameraDeviceLabel: device.label
}));
dispatch(setVideoInputDevice(device.deviceId));
@ -155,7 +156,8 @@ function _useDevice({ dispatch }, device) {
}
case 'audioinput': {
dispatch(updateSettings({
userSelectedMicDeviceId: device.deviceId
userSelectedMicDeviceId: device.deviceId,
userSelectedMicDeviceLabel: device.label
}));
dispatch(setAudioInputDevice(device.deviceId));
@ -165,7 +167,8 @@ function _useDevice({ dispatch }, device) {
setAudioOutputDeviceId(
device.deviceId,
dispatch,
true)
true,
device.label)
.then(() => logger.log('changed audio output device'))
.catch(err => {
logger.warn(

View File

@ -2,6 +2,8 @@
import { Component } from 'react';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Describes audio element interface used in the base/media feature for audio
* playback.
@ -10,7 +12,7 @@ export type AudioElement = {
currentTime: number,
pause: () => void,
play: () => void,
setSinkId?: string => void,
setSinkId?: string => Function,
stop: () => void
};
@ -113,7 +115,8 @@ export default class AbstractAudio extends Component<Props> {
setSinkId(sinkId: string): void {
this._audioElementImpl
&& typeof this._audioElementImpl.setSinkId === 'function'
&& this._audioElementImpl.setSinkId(sinkId);
&& this._audioElementImpl.setSinkId(sinkId)
.catch(error => logger.error('Error setting sink', error));
}
/**

View File

@ -97,3 +97,152 @@ export function getServerURL(stateful: Object | Function) {
return state['features/base/settings'].serverURL || DEFAULT_SERVER_URL;
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred cameraDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedCameraDeviceId(stateful: Object | Function) {
const state = toState(stateful);
const {
userSelectedCameraDeviceId,
userSelectedCameraDeviceLabel
} = state['features/base/settings'];
const { videoInput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: videoInput,
// Operating systems may append " #{number}" somewhere in the label so
// find and strip that bit.
matchRegex: /\s#\d*(?!.*\s#\d*)/,
userSelectedDeviceId: userSelectedCameraDeviceId,
userSelectedDeviceLabel: userSelectedCameraDeviceLabel,
replacement: ''
});
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred micDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedMicDeviceId(stateful: Object | Function) {
const state = toState(stateful);
const {
userSelectedMicDeviceId,
userSelectedMicDeviceLabel
} = state['features/base/settings'];
const { audioInput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: audioInput,
// Operating systems may append " ({number}-" somewhere in the label so
// find and strip that bit.
matchRegex: /\s\(\d*-\s(?!.*\s\(\d*-\s)/,
userSelectedDeviceId: userSelectedMicDeviceId,
userSelectedDeviceLabel: userSelectedMicDeviceLabel,
replacement: ' ('
});
}
/**
* Searches known devices for a matching deviceId and fall back to matching on
* label. Returns the stored preferred audioOutputDeviceId if a match is not found.
*
* @param {Object|Function} stateful - The redux state object or
* {@code getState} function.
* @returns {string}
*/
export function getUserSelectedOutputDeviceId(stateful: Object | Function) {
const state = toState(stateful);
const {
userSelectedAudioOutputDeviceId,
userSelectedAudioOutputDeviceLabel
} = state['features/base/settings'];
const { audioOutput } = state['features/base/devices'].availableDevices;
return _getUserSelectedDeviceId({
availableDevices: audioOutput,
matchRegex: undefined,
userSelectedDeviceId: userSelectedAudioOutputDeviceId,
userSelectedDeviceLabel: userSelectedAudioOutputDeviceLabel,
replacement: undefined
});
}
/**
* A helper function to abstract the logic for choosing which device ID to
* use. Falls back to fuzzy matching on label if a device ID match is not found.
*
* @param {Object} options - The arguments used to match find the preferred
* device ID from available devices.
* @param {Array<string>} options.availableDevices - The array of currently
* available devices to match against.
* @param {Object} options.matchRegex - The regex to use to find strings
* appended to the label by the operating system. The matches will be replaced
* with options.replacement, with the intent of matching the same device that
* might have a modified label.
* @param {string} options.userSelectedDeviceId - The device ID the participant
* prefers to use.
* @param {string} options.userSelectedDeviceLabel - The label associated with the
* device ID the participant prefers to use.
* @param {string} options.replacement - The string to use with
* options.matchRegex to remove identifies added to the label by the operating
* system.
* @private
* @returns {string} The preferred device ID to use for media.
*/
function _getUserSelectedDeviceId(options) {
const {
availableDevices,
matchRegex,
userSelectedDeviceId,
userSelectedDeviceLabel,
replacement
} = options;
// If there is no label at all, there is no need to fall back to checking
// the label for a fuzzy match.
if (!userSelectedDeviceLabel || !userSelectedDeviceId) {
return userSelectedDeviceId;
}
const foundMatchingBasedonDeviceId = availableDevices.find(
candidate => candidate.deviceId === userSelectedDeviceId);
// Prioritize matching the deviceId
if (foundMatchingBasedonDeviceId) {
return userSelectedDeviceId;
}
const strippedDeviceLabel
= matchRegex ? userSelectedDeviceLabel.replace(matchRegex, replacement)
: userSelectedDeviceLabel;
const foundMatchBasedOnLabel = availableDevices.find(candidate => {
const { label } = candidate;
if (!label) {
return false;
} else if (strippedDeviceLabel === label) {
return true;
}
const strippedCandidateLabel
= label.replace(matchRegex, replacement);
return strippedDeviceLabel === strippedCandidateLabel;
});
return foundMatchBasedOnLabel
? foundMatchBasedOnLabel.deviceId : userSelectedDeviceId;
}

View File

@ -33,7 +33,10 @@ const DEFAULT_STATE = {
startWithVideoMuted: false,
userSelectedAudioOutputDeviceId: undefined,
userSelectedCameraDeviceId: undefined,
userSelectedMicDeviceId: undefined
userSelectedMicDeviceId: undefined,
userSelectedAudioOutputDeviceLabel: undefined,
userSelectedCameraDeviceLabel: undefined,
userSelectedMicDeviceLabel: undefined
};
const STORE_NAME = 'features/base/settings';

View File

@ -3,6 +3,10 @@
import JitsiMeetJS, { JitsiTrackErrors, JitsiTrackEvents }
from '../lib-jitsi-meet';
import { MEDIA_TYPE } from '../media';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId
} from '../settings';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@ -37,13 +41,13 @@ export function createLocalTracksF(
// reliance on the global variable APP will go away.
store || (store = APP.store); // eslint-disable-line no-param-reassign
const settings = store.getState()['features/base/settings'];
const state = store.getState();
if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) {
cameraDeviceId = settings.userSelectedCameraDeviceId;
cameraDeviceId = getUserSelectedCameraDeviceId(state);
}
if (typeof micDeviceId === 'undefined' || micDeviceId === null) {
micDeviceId = settings.userSelectedMicDeviceId;
micDeviceId = getUserSelectedMicDeviceId(state);
}
}

View File

@ -6,6 +6,7 @@ import {
import { createDeviceChangedEvent, sendAnalytics } from '../analytics';
import {
getDeviceLabelById,
setAudioInputDevice,
setAudioOutputDeviceId,
setVideoInputDevice
@ -112,7 +113,9 @@ export function submitDeviceSelectionTab(newState) {
&& newState.selectedVideoInputId
!== currentState.selectedVideoInputId) {
dispatch(updateSettings({
userSelectedCameraDeviceId: newState.selectedVideoInputId
userSelectedCameraDeviceId: newState.selectedVideoInputId,
userSelectedCameraDeviceLabel:
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
}));
dispatch(
@ -123,7 +126,9 @@ export function submitDeviceSelectionTab(newState) {
&& newState.selectedAudioInputId
!== currentState.selectedAudioInputId) {
dispatch(updateSettings({
userSelectedMicDeviceId: newState.selectedAudioInputId
userSelectedMicDeviceId: newState.selectedAudioInputId,
userSelectedMicDeviceLabel:
getDeviceLabelById(getState(), newState.selectedAudioInputId, 'audioInput')
}));
dispatch(
@ -138,7 +143,8 @@ export function submitDeviceSelectionTab(newState) {
setAudioOutputDeviceId(
newState.selectedAudioOutputId,
dispatch,
true)
true,
getDeviceLabelById(getState(), newState.selectedAudioOutputId, 'audioOutput'))
.then(() => logger.log('changed audio output device'))
.catch(err => {
logger.warn(

View File

@ -113,6 +113,7 @@ class AudioOutputPreview extends Component<Props> {
*/
_setAudioSink() {
this._audioElement
&& this.props.deviceId
&& this._audioElement.setSinkId(this.props.deviceId);
}
}

View File

@ -15,6 +15,11 @@ import {
} from '../base/devices';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { toState } from '../base/redux';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId,
getUserSelectedOutputDeviceId
} from '../base/settings';
/**
* Returns the properties for the device selection dialog from Redux state.
@ -38,9 +43,9 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
// on welcome page we also show only what we have saved as user selected devices
if (!conference) {
disableAudioInputChange = false;
selectedAudioInputId = settings.userSelectedMicDeviceId;
selectedAudioOutputId = settings.userSelectedAudioOutputDeviceId;
selectedVideoInputId = settings.userSelectedCameraDeviceId;
selectedAudioInputId = getUserSelectedMicDeviceId(state);
selectedAudioOutputId = getUserSelectedOutputDeviceId(state);
selectedVideoInputId = getUserSelectedCameraDeviceId(state);
}
// we fill the device selection dialog with the devices that are currently