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 [];
}
}