diff --git a/conference.js b/conference.js index bf6f5a2da..2f8947c41 100644 --- a/conference.js +++ b/conference.js @@ -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, diff --git a/lang/main.json b/lang/main.json index 1802094fb..b68a5de6a 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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 fully supported browsers.", - "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", diff --git a/modules/devices/mediaDeviceHelper.js b/modules/devices/mediaDeviceHelper.js index ab0f90e7c..c47654ed8 100644 --- a/modules/devices/mediaDeviceHelper.js +++ b/modules/devices/mediaDeviceHelper.js @@ -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; + } } /** diff --git a/react/features/base/devices/actionTypes.js b/react/features/base/devices/actionTypes.js index 2928afab5..942c1414f 100644 --- a/react/features/base/devices/actionTypes.js +++ b/react/features/base/devices/actionTypes.js @@ -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 + * oldDevices: Array + * } + */ +export const CHECK_AND_NOTIFY_FOR_NEW_DEVICE = 'CHECK_AND_NOTIFY_FOR_NEW_DEVICE'; diff --git a/react/features/base/devices/actions.js b/react/features/base/devices/actions.js index 5f1bafd18..fa3a1ab9e 100644 --- a/react/features/base/devices/actions.js +++ b/react/features/base/devices/actions.js @@ -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} newDevices - Array of the new devices. + * @param {Array} oldDevices - Array of the old devices. + * @returns {{ + * type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE, + * newDevices: Array, + * oldDevices: Array + * }} + */ +export function checkAndNotifyForNewDevice(newDevices, oldDevices) { + return { + type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE, + newDevices, + oldDevices + }; +} diff --git a/react/features/base/devices/middleware.js b/react/features/base/devices/middleware.js index decb73bea..3be6e1440 100644 --- a/react/features/base/devices/middleware.js +++ b/react/features/base/devices/middleware.js @@ -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; +} diff --git a/react/features/device-selection/actions.js b/react/features/device-selection/actions.js index 8c6ebec30..6c08e13ab 100644 --- a/react/features/device-selection/actions.js +++ b/react/features/device-selection/actions.js @@ -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 })); diff --git a/react/features/notifications/components/AbstractNotification.js b/react/features/notifications/components/AbstractNotification.js index a6eb08105..ae25749b2 100644 --- a/react/features/notifications/components/AbstractNotification.js +++ b/react/features/notifications/components/AbstractNotification.js @@ -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. diff --git a/react/features/notifications/components/web/Notification.js b/react/features/notifications/components/web/Notification.js index b21c0f988..d262cd19a 100644 --- a/react/features/notifications/components/web/Notification.js +++ b/react/features/notifications/components/web/Notification.js @@ -136,6 +136,19 @@ class Notification extends AbstractNotification { ]; default: + if (this.props.customActionNameKey && this.props.customActionHandler) { + return [ + { + content: this.props.t(this.props.customActionNameKey), + onClick: () => { + if (this.props.customActionHandler()) { + this._onDismissed(); + } + } + } + ]; + } + return []; } }