Notify for new device (#4165)

* Fix detecting preferred audio output.

Fixes detecting when a new output device is found and we have stored user preference of using that device.

* Does not store which is the currently open device on save.

Does not save the currently opened device when saving settings dialog, this will be done once we successfully replace the tracks to use the new devices.

* Saves opened audio device after successfully changing it.

If we do it earlier _updateAudioDeviceId is using localAudio and can store wrong value.

* Adds notification for new non preferred devices.

A notification is shown which gives an option to the user to select and use the newly plugged devices.
Adding custom button and handler for the action to the notifications.

* Changes logic to search and handle all newly added devices from array.

* Moves some utility methods to features/base/devices.
This commit is contained in:
Дамян Минков 2019-05-03 18:25:33 +01:00 committed by GitHub
parent 384f0d4317
commit 768cff48a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 212 additions and 6 deletions

View File

@ -49,6 +49,7 @@ import {
setDesktopSharingEnabled
} from './react/features/base/conference';
import {
checkAndNotifyForNewDevice,
getAvailableDevices,
setAudioOutputDeviceId,
updateDeviceList
@ -2123,8 +2124,8 @@ export default {
return stream;
})
.then(stream => {
this.useAudioStream(stream);
.then(stream => this.useAudioStream(stream))
.then(() => {
logger.log('switched local audio device');
this._updateAudioDeviceId();
@ -2344,6 +2345,8 @@ export default {
* @returns {Promise}
*/
_onDeviceListChanged(devices) {
const oldDevices = APP.store.getState()['features/base/devices'].availableDevices;
APP.store.dispatch(updateDeviceList(devices));
const newDevices
@ -2382,6 +2385,25 @@ export default {
this.localVideo.stopStream();
}
// Let's handle unknown/non-preferred devices
const newAvailDevices
= APP.store.getState()['features/base/devices'].availableDevices;
if (typeof newDevices.audiooutput === 'undefined') {
APP.store.dispatch(
checkAndNotifyForNewDevice(newAvailDevices.audioOutput, oldDevices.audioOutput));
}
if (!requestedInput.audio) {
APP.store.dispatch(
checkAndNotifyForNewDevice(newAvailDevices.audioInput, oldDevices.audioInput));
}
if (!requestedInput.video) {
APP.store.dispatch(
checkAndNotifyForNewDevice(newAvailDevices.videoInput, oldDevices.videoInput));
}
promises.push(
mediaDeviceHelper.createLocalTracksAfterDeviceListChanged(
createLocalTracksF,

View File

@ -476,7 +476,11 @@
"raisedHand": "__name__ would like to speak.",
"somebody": "Somebody",
"suboptimalExperienceDescription": "Eer... we are afraid your experience with __appName__ isn't going to be that great here. We are looking for ways to improve this but, until then, please try using one of the <a href='static/recommendedBrowsers.html' target='_blank'>fully supported browsers</a>.",
"suboptimalExperienceTitle": "Browser Warning"
"suboptimalExperienceTitle": "Browser Warning",
"newDeviceCameraTitle": "New camera detected",
"newDeviceMicTitle": "New microphone detected",
"newDeviceCameraTitle": "New audio output detected",
"newDeviceAction": "Use"
},
"passwordSetRemotely": "set by another member",
"poweredby": "powered by",

View File

@ -25,6 +25,17 @@ function getNewAudioOutputDevice(newDevices) {
d.deviceId === selectedAudioOutputDeviceId)) {
return 'default';
}
const settings = APP.store.getState()['features/base/settings'];
const preferredAudioOutputDeviceId = settings.userSelectedAudioOutputDeviceId;
// 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
if (preferredAudioOutputDeviceId
&& preferredAudioOutputDeviceId !== selectedAudioOutputDeviceId
&& availableAudioOutputDevices.find(d => d.deviceId === preferredAudioOutputDeviceId)) {
return preferredAudioOutputDeviceId;
}
}
/**

View File

@ -50,3 +50,15 @@ export const ADD_PENDING_DEVICE_REQUEST = 'ADD_PENDING_DEVICE_REQUEST';
* }
*/
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';

View File

@ -3,6 +3,7 @@ import { updateSettings } from '../settings';
import {
ADD_PENDING_DEVICE_REQUEST,
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
REMOVE_PENDING_DEVICE_REQUESTS,
SET_AUDIO_INPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE,
@ -186,3 +187,21 @@ export function updateDeviceList(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, oldDevices) {
return {
type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
newDevices,
oldDevices
};
}

View File

@ -5,11 +5,21 @@ import { processExternalDeviceRequest } from '../../device-selection';
import { MiddlewareRegistry } from '../redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { removePendingDeviceRequests } from './actions';
import {
removePendingDeviceRequests,
setAudioInputDevice,
setVideoInputDevice
} from './actions';
import {
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
SET_AUDIO_INPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE
} from './actionTypes';
import { showNotification } from '../../notifications';
import { updateSettings } from '../settings';
import { setAudioOutputDeviceId } from './functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Implements the middleware of the feature base/devices.
@ -28,6 +38,9 @@ MiddlewareRegistry.register(store => next => action => {
case SET_VIDEO_INPUT_DEVICE:
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
break;
case CHECK_AND_NOTIFY_FOR_NEW_DEVICE:
_checkAndNotifyForNewDevice(store, action.newDevices, action.oldDevices);
break;
}
return next(action);
@ -63,3 +76,107 @@ function _conferenceJoined({ dispatch, getState }, next, action) {
return result;
}
/**
* Finds a new device by comparing new and old array of devices and dispatches
* notification with the new device.
*
* @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, newDevices, oldDevices) {
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));
onlyNewDevices.forEach(newDevice => {
// we want to strip any device details that are not very
// user friendly, like usb ids put in brackets at the end
let description = newDevice.label;
const ix = description.lastIndexOf('(');
if (ix !== -1) {
description = description.substr(0, ix);
}
let titleKey;
switch (newDevice.kind) {
case 'videoinput': {
titleKey = 'notify.newDeviceCameraTitle';
break;
}
case 'audioinput': {
titleKey = 'notify.newDeviceMicTitle';
break;
}
case 'audiooutput': {
titleKey = 'notify.newDeviceCameraTitle';
break;
}
}
dispatch(showNotification({
description,
titleKey,
customActionNameKey: 'notify.newDeviceAction',
customActionHandler: _useDevice.bind(undefined, store, newDevice)
}));
});
}
/**
* 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 {MediaDeviceInfo} device - The device to save.
* @returns {boolean} - Returns true in order notifications to be dismissed.
* @private
*/
function _useDevice({ dispatch }, device) {
switch (device.kind) {
case 'videoinput': {
dispatch(updateSettings({
userSelectedCameraDeviceId: device.deviceId
}));
dispatch(setVideoInputDevice(device.deviceId));
break;
}
case 'audioinput': {
dispatch(updateSettings({
userSelectedMicDeviceId: device.deviceId
}));
dispatch(setAudioInputDevice(device.deviceId));
break;
}
case 'audiooutput': {
setAudioOutputDeviceId(
device.deviceId,
dispatch,
true)
.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;
}

View File

@ -112,7 +112,6 @@ export function submitDeviceSelectionTab(newState) {
&& newState.selectedVideoInputId
!== currentState.selectedVideoInputId) {
dispatch(updateSettings({
cameraDeviceId: newState.selectedVideoInputId,
userSelectedCameraDeviceId: newState.selectedVideoInputId
}));
@ -124,7 +123,6 @@ export function submitDeviceSelectionTab(newState) {
&& newState.selectedAudioInputId
!== currentState.selectedAudioInputId) {
dispatch(updateSettings({
micDeviceId: newState.selectedAudioInputId,
userSelectedMicDeviceId: newState.selectedAudioInputId
}));

View File

@ -12,6 +12,16 @@ export type Props = {
*/
appearance: string,
/**
* Callback invoked when the custom button is clicked.
*/
customActionHandler: Function,
/**
* The text to display as button in the notification for the custom action.
*/
customActionNameKey: string,
/**
* The text to display in the body of the notification. If not passed
* in, the passed in descriptionKey will be used.

View File

@ -136,6 +136,19 @@ class Notification extends AbstractNotification<Props> {
];
default:
if (this.props.customActionNameKey && this.props.customActionHandler) {
return [
{
content: this.props.t(this.props.customActionNameKey),
onClick: () => {
if (this.props.customActionHandler()) {
this._onDismissed();
}
}
}
];
}
return [];
}
}