Merge pull request #1447 from virtuacoplenny/device-picker
New device selection modal
This commit is contained in:
commit
98004c2328
|
@ -30,6 +30,9 @@ import {
|
||||||
conferenceLeft,
|
conferenceLeft,
|
||||||
EMAIL_COMMAND
|
EMAIL_COMMAND
|
||||||
} from './react/features/base/conference';
|
} from './react/features/base/conference';
|
||||||
|
import {
|
||||||
|
updateDeviceList
|
||||||
|
} from './react/features/base/devices';
|
||||||
import {
|
import {
|
||||||
isFatalJitsiConnectionError
|
isFatalJitsiConnectionError
|
||||||
} from './react/features/base/lib-jitsi-meet';
|
} from './react/features/base/lib-jitsi-meet';
|
||||||
|
@ -1029,6 +1032,15 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current local video track in use.
|
||||||
|
*
|
||||||
|
* @returns {JitsiLocalTrack}
|
||||||
|
*/
|
||||||
|
getLocalVideoTrack() {
|
||||||
|
return room.getLocalVideoTrack();
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start using provided audio stream.
|
* Start using provided audio stream.
|
||||||
* Stops previous audio stream.
|
* Stops previous audio stream.
|
||||||
|
@ -1058,6 +1070,15 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current local audio track in use.
|
||||||
|
*
|
||||||
|
* @returns {JitsiLocalTrack}
|
||||||
|
*/
|
||||||
|
getLocalAudioTrack() {
|
||||||
|
return room.getLocalAudioTrack();
|
||||||
|
},
|
||||||
|
|
||||||
videoSwitchInProgress: false,
|
videoSwitchInProgress: false,
|
||||||
toggleScreenSharing (shareScreen = !this.isSharingScreen) {
|
toggleScreenSharing (shareScreen = !this.isSharingScreen) {
|
||||||
if (this.videoSwitchInProgress) {
|
if (this.videoSwitchInProgress) {
|
||||||
|
@ -1622,7 +1643,6 @@ export default {
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
APP.UI.showDeviceErrorDialog(null, err);
|
APP.UI.showDeviceErrorDialog(null, err);
|
||||||
APP.UI.setSelectedCameraFromSettings();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1644,7 +1664,6 @@ export default {
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
APP.UI.showDeviceErrorDialog(err, null);
|
APP.UI.showDeviceErrorDialog(err, null);
|
||||||
APP.UI.setSelectedMicFromSettings();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1660,7 +1679,6 @@ export default {
|
||||||
logger.warn('Failed to change audio output device. ' +
|
logger.warn('Failed to change audio output device. ' +
|
||||||
'Default or previously set audio output device ' +
|
'Default or previously set audio output device ' +
|
||||||
'will be used instead.', err);
|
'will be used instead.', err);
|
||||||
APP.UI.setSelectedAudioOutputFromSettings();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1756,8 +1774,8 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaDeviceHelper.setCurrentMediaDevices(devices);
|
mediaDeviceHelper.setCurrentMediaDevices(devices);
|
||||||
|
|
||||||
APP.UI.onAvailableDevicesChanged(devices);
|
APP.UI.onAvailableDevicesChanged(devices);
|
||||||
|
APP.store.dispatch(updateDeviceList(devices));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.deviceChangeListener = (devices) =>
|
this.deviceChangeListener = (devices) =>
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
.settingsContent {
|
|
||||||
display: flex;
|
|
||||||
display: -webkit-flex;
|
|
||||||
|
|
||||||
#localVideoPreview {
|
|
||||||
width: 50%;
|
|
||||||
align-self: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deviceSelection {
|
|
||||||
display: flex;
|
|
||||||
display: -webkit-flex;
|
|
||||||
-webkit-flex: 1;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: left;
|
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
.device {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
|
|
||||||
select {
|
|
||||||
flex: 1;
|
|
||||||
margin_right: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -113,6 +113,12 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#deviceOptionsWrapper {
|
||||||
|
button {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Profile
|
* Profile
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
@import 'inlay';
|
@import 'inlay';
|
||||||
@import 'reload_overlay/reload_overlay';
|
@import 'reload_overlay/reload_overlay';
|
||||||
@import 'modals/desktop-picker/desktop-picker';
|
@import 'modals/desktop-picker/desktop-picker';
|
||||||
|
@import 'modals/device-selection/device-selection';
|
||||||
@import 'modals/dialog';
|
@import 'modals/dialog';
|
||||||
@import 'modals/feedback/feedback';
|
@import 'modals/feedback/feedback';
|
||||||
@import 'modals/speaker_stats/speaker_stats';
|
@import 'modals/speaker_stats/speaker_stats';
|
||||||
|
@ -54,7 +55,6 @@
|
||||||
@import 'welcome_page';
|
@import 'welcome_page';
|
||||||
@import 'toolbars';
|
@import 'toolbars';
|
||||||
@import 'side_toolbar_container';
|
@import 'side_toolbar_container';
|
||||||
@import 'device_settings_dialog';
|
|
||||||
@import 'jquery.contextMenu';
|
@import 'jquery.contextMenu';
|
||||||
@import 'keyboard-shortcuts';
|
@import 'keyboard-shortcuts';
|
||||||
@import 'redirect_page';
|
@import 'redirect_page';
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
.device-selection {
|
||||||
|
color: $feedbackInputTextColor;
|
||||||
|
|
||||||
|
.device-selectors {
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:last-child {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-selection-column-selectors,
|
||||||
|
.device-selection-column-video {
|
||||||
|
padding: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.device-selection-column-selectors {
|
||||||
|
width: 46%;
|
||||||
|
}
|
||||||
|
.device-selection-column-video {
|
||||||
|
width: 49%;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-selection-video-container {
|
||||||
|
background: black;
|
||||||
|
height: 156px;
|
||||||
|
margin: 15px 0 5px;
|
||||||
|
|
||||||
|
.video-input-preview {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.video-input-preview-muted {
|
||||||
|
color: $participantNameColor;
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-muted .video-input-preview-muted {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-input-preview-display {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-output-preview {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-input-preview {
|
||||||
|
background: #f4f5f7;
|
||||||
|
border-radius: 5px;
|
||||||
|
height: 6px;
|
||||||
|
|
||||||
|
.audio-input-preview-level {
|
||||||
|
background: #0052cc;
|
||||||
|
border-radius: 5px;
|
||||||
|
height: 100%;
|
||||||
|
-webkit-transition: width .1s ease-in-out;
|
||||||
|
-moz-transition: width .1s ease-in-out;
|
||||||
|
-o-transition: width .1s ease-in-out;
|
||||||
|
transition: width .1s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -422,5 +422,11 @@
|
||||||
"seconds": "__count__s",
|
"seconds": "__count__s",
|
||||||
"speakerStats": "Speaker Stats",
|
"speakerStats": "Speaker Stats",
|
||||||
"speakerTime": "Speaker Time"
|
"speakerTime": "Speaker Time"
|
||||||
|
},
|
||||||
|
"deviceSelection": {
|
||||||
|
"deviceSettings": "Device settings",
|
||||||
|
"noOtherDevices": "No other devices available",
|
||||||
|
"selectADevice": "Select a device",
|
||||||
|
"testAudio": "Test sound"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,10 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||||
|
|
||||||
var UI = {};
|
var UI = {};
|
||||||
|
|
||||||
|
import {
|
||||||
|
updateDeviceList
|
||||||
|
} from '../../react/features/base/devices';
|
||||||
|
|
||||||
import Chat from "./side_pannels/chat/Chat";
|
import Chat from "./side_pannels/chat/Chat";
|
||||||
import SidePanels from "./side_pannels/SidePanels";
|
import SidePanels from "./side_pannels/SidePanels";
|
||||||
import Avatar from "./avatar/Avatar";
|
import Avatar from "./avatar/Avatar";
|
||||||
|
@ -1080,29 +1084,7 @@ UI.onLocalRaiseHandChanged = function (isRaisedHand) {
|
||||||
* @param {object[]} devices new list of available devices
|
* @param {object[]} devices new list of available devices
|
||||||
*/
|
*/
|
||||||
UI.onAvailableDevicesChanged = function (devices) {
|
UI.onAvailableDevicesChanged = function (devices) {
|
||||||
SettingsMenu.changeDevicesList(devices);
|
APP.store.dispatch(updateDeviceList(devices));
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets microphone's <select> element to select microphone ID from settings.
|
|
||||||
*/
|
|
||||||
UI.setSelectedMicFromSettings = function () {
|
|
||||||
SettingsMenu.setSelectedMicFromSettings();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets camera's <select> element to select camera ID from settings.
|
|
||||||
*/
|
|
||||||
UI.setSelectedCameraFromSettings = function () {
|
|
||||||
SettingsMenu.setSelectedCameraFromSettings();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets audio outputs's <select> element to select audio output ID from
|
|
||||||
* settings.
|
|
||||||
*/
|
|
||||||
UI.setSelectedAudioOutputFromSettings = function () {
|
|
||||||
SettingsMenu.setSelectedAudioOutputFromSettings();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
/* global $, APP, AJS, interfaceConfig, JitsiMeetJS */
|
/* global $, APP, AJS, interfaceConfig, JitsiMeetJS */
|
||||||
|
import { openDialog } from '../../../../react/features/base/dialog';
|
||||||
import { LANGUAGES } from "../../../../react/features/base/i18n";
|
import { LANGUAGES } from "../../../../react/features/base/i18n";
|
||||||
|
import { DeviceSelectionDialog }
|
||||||
|
from '../../../../react/features/device-selection';
|
||||||
|
|
||||||
import UIUtil from "../../util/UIUtil";
|
import UIUtil from "../../util/UIUtil";
|
||||||
import UIEvents from "../../../../service/UI/UIEvents";
|
import UIEvents from "../../../../service/UI/UIEvents";
|
||||||
import Settings from '../../../settings/Settings';
|
|
||||||
|
|
||||||
const sidePanelsContainerId = 'sideToolbarContainer';
|
const sidePanelsContainerId = 'sideToolbarContainer';
|
||||||
|
const deviceSelectionButtonClasses
|
||||||
|
= 'button-control button-control_primary button-control_full-width';
|
||||||
const htmlStr = `
|
const htmlStr = `
|
||||||
<div id="settings_container" class="sideToolbarContainer__inner">
|
<div id="settings_container" class="sideToolbarContainer__inner">
|
||||||
<div class="title" data-i18n="settings.title"></div>
|
<div class="title" data-i18n="settings.title"></div>
|
||||||
|
@ -19,17 +22,11 @@ const htmlStr = `
|
||||||
<div id="deviceOptionsTitle" class="subTitle hide"
|
<div id="deviceOptionsTitle" class="subTitle hide"
|
||||||
data-i18n="settings.audioVideo"></div>
|
data-i18n="settings.audioVideo"></div>
|
||||||
<div class="sideToolbarBlock first">
|
<div class="sideToolbarBlock first">
|
||||||
<label class="first" data-i18n="settings.selectCamera">
|
<button
|
||||||
</label>
|
class="${deviceSelectionButtonClasses}"
|
||||||
<select id="selectCamera"></select>
|
data-i18n="deviceSelection.deviceSettings"
|
||||||
</div>
|
id="deviceSelection"
|
||||||
<div class="sideToolbarBlock">
|
type="button"></button>
|
||||||
<label data-i18n="settings.selectMic"></label>
|
|
||||||
<select id="selectMic"></select>
|
|
||||||
</div>
|
|
||||||
<div class="sideToolbarBlock">
|
|
||||||
<label data-i18n="settings.selectAudioOutput"></label>
|
|
||||||
<select id="selectAudioOutput"></select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="moderatorOptionsWrapper" class="hide">
|
<div id="moderatorOptionsWrapper" class="hide">
|
||||||
|
@ -89,40 +86,6 @@ function generateLanguagesOptions(items, currentLang) {
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate html select options for available physical devices.
|
|
||||||
*
|
|
||||||
* @param {{ deviceId, label }[]} items available devices
|
|
||||||
* @param {string} [selectedId] id of selected device
|
|
||||||
* @param {boolean} permissionGranted if permission to use selected device type
|
|
||||||
* is granted
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function generateDevicesOptions(items, selectedId, permissionGranted) {
|
|
||||||
if (!permissionGranted && items.length) {
|
|
||||||
return '<option data-i18n="settings.noPermission"></option>';
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = items.map(function (item) {
|
|
||||||
let attrs = {
|
|
||||||
value: item.deviceId
|
|
||||||
};
|
|
||||||
|
|
||||||
if (item.deviceId === selectedId) {
|
|
||||||
attrs.selected = 'selected';
|
|
||||||
}
|
|
||||||
|
|
||||||
let attrsStr = UIUtil.attrsToString(attrs);
|
|
||||||
return `<option ${attrsStr}>${item.label}</option>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!items.length) {
|
|
||||||
options.unshift('<option data-i18n="settings.noDevice"></option>');
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace html select element to select2 custom dropdown
|
* Replace html select element to select2 custom dropdown
|
||||||
*
|
*
|
||||||
|
@ -138,6 +101,34 @@ function initSelect2($el, onSelectedCb) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open DeviceSelectionDialog with a configuration based on the environment's
|
||||||
|
* supported abilities.
|
||||||
|
*
|
||||||
|
* @param {boolean} isDeviceListAvailable - Whether or not device enumeration
|
||||||
|
* is possible. This is a value obtained through an async operation whereas all
|
||||||
|
* other configurations for the modal are obtained synchronously.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _openDeviceSelectionModal(isDeviceListAvailable) {
|
||||||
|
APP.store.dispatch(openDialog(DeviceSelectionDialog, {
|
||||||
|
currentAudioOutputId: APP.settings.getAudioOutputDeviceId(),
|
||||||
|
currentAudioTrack: APP.conference.getLocalAudioTrack(),
|
||||||
|
currentVideoTrack: APP.conference.getLocalVideoTrack(),
|
||||||
|
disableAudioInputChange: !JitsiMeetJS.isMultipleAudioInputSupported(),
|
||||||
|
disableDeviceChange: !isDeviceListAvailable
|
||||||
|
|| !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
|
||||||
|
hasAudioPermission: JitsiMeetJS.mediaDevices
|
||||||
|
.isDevicePermissionGranted('audio'),
|
||||||
|
hasVideoPermission: JitsiMeetJS.mediaDevices
|
||||||
|
.isDevicePermissionGranted('video'),
|
||||||
|
hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
|
||||||
|
hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
|
||||||
|
.isDeviceChangeAvailable('output')
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
init (emitter) {
|
init (emitter) {
|
||||||
initHTML();
|
initHTML();
|
||||||
|
@ -181,11 +172,11 @@ export default {
|
||||||
|
|
||||||
JitsiMeetJS.mediaDevices.isDeviceListAvailable()
|
JitsiMeetJS.mediaDevices.isDeviceListAvailable()
|
||||||
.then((isDeviceListAvailable) => {
|
.then((isDeviceListAvailable) => {
|
||||||
if (isDeviceListAvailable &&
|
$('#deviceSelection').on('click', () => {
|
||||||
JitsiMeetJS.mediaDevices.isDeviceChangeAvailable()) {
|
_openDeviceSelectionModal(isDeviceListAvailable);
|
||||||
this._initializeDeviceSelectionSettings(emitter);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Only show the subtitle if this isn't the only setting section.
|
// Only show the subtitle if this isn't the only setting section.
|
||||||
if (interfaceConfig.SETTINGS_SECTIONS.length > 1)
|
if (interfaceConfig.SETTINGS_SECTIONS.length > 1)
|
||||||
UIUtil.setVisible("deviceOptionsTitle", true);
|
UIUtil.setVisible("deviceOptionsTitle", true);
|
||||||
|
@ -219,30 +210,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_initializeDeviceSelectionSettings(emitter) {
|
|
||||||
this.changeDevicesList([]);
|
|
||||||
|
|
||||||
$('#selectCamera').change(function () {
|
|
||||||
let cameraDeviceId = $(this).val();
|
|
||||||
if (cameraDeviceId !== Settings.getCameraDeviceId()) {
|
|
||||||
emitter.emit(UIEvents.VIDEO_DEVICE_CHANGED, cameraDeviceId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#selectMic').change(function () {
|
|
||||||
let micDeviceId = $(this).val();
|
|
||||||
if (micDeviceId !== Settings.getMicDeviceId()) {
|
|
||||||
emitter.emit(UIEvents.AUDIO_DEVICE_CHANGED, micDeviceId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#selectAudioOutput').change(function () {
|
|
||||||
let audioOutputDeviceId = $(this).val();
|
|
||||||
if (audioOutputDeviceId !== Settings.getAudioOutputDeviceId()) {
|
|
||||||
emitter.emit(
|
|
||||||
UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, audioOutputDeviceId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If start audio muted/start video muted options should be visible or not.
|
* If start audio muted/start video muted options should be visible or not.
|
||||||
* @param {boolean} show
|
* @param {boolean} show
|
||||||
|
@ -286,91 +253,5 @@ export default {
|
||||||
*/
|
*/
|
||||||
isVisible () {
|
isVisible () {
|
||||||
return UIUtil.isVisible(document.getElementById("settings_container"));
|
return UIUtil.isVisible(document.getElementById("settings_container"));
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets microphone's <select> element to select microphone ID from settings.
|
|
||||||
*/
|
|
||||||
setSelectedMicFromSettings () {
|
|
||||||
$('#selectMic').val(Settings.getMicDeviceId());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets camera's <select> element to select camera ID from settings.
|
|
||||||
*/
|
|
||||||
setSelectedCameraFromSettings () {
|
|
||||||
$('#selectCamera').val(Settings.getCameraDeviceId());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets audio outputs's <select> element to select audio output ID from
|
|
||||||
* settings.
|
|
||||||
*/
|
|
||||||
setSelectedAudioOutputFromSettings () {
|
|
||||||
$('#selectAudioOutput').val(Settings.getAudioOutputDeviceId());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change available cameras/microphones or hide selects completely if
|
|
||||||
* no devices available.
|
|
||||||
* @param {{ deviceId, label, kind }[]} devices list of available devices
|
|
||||||
*/
|
|
||||||
changeDevicesList (devices) {
|
|
||||||
let $selectCamera= AJS.$('#selectCamera'),
|
|
||||||
$selectMic = AJS.$('#selectMic'),
|
|
||||||
$selectAudioOutput = AJS.$('#selectAudioOutput'),
|
|
||||||
$selectAudioOutputParent = $selectAudioOutput.parent();
|
|
||||||
|
|
||||||
let audio = devices.filter(device => device.kind === 'audioinput'),
|
|
||||||
video = devices.filter(device => device.kind === 'videoinput'),
|
|
||||||
audioOutput = devices
|
|
||||||
.filter(device => device.kind === 'audiooutput'),
|
|
||||||
selectedAudioDevice = audio.find(
|
|
||||||
d => d.deviceId === Settings.getMicDeviceId()) || audio[0],
|
|
||||||
selectedVideoDevice = video.find(
|
|
||||||
d => d.deviceId === Settings.getCameraDeviceId()) || video[0],
|
|
||||||
selectedAudioOutputDevice = audioOutput.find(
|
|
||||||
d => d.deviceId === Settings.getAudioOutputDeviceId()),
|
|
||||||
videoPermissionGranted =
|
|
||||||
JitsiMeetJS.mediaDevices.isDevicePermissionGranted('video'),
|
|
||||||
audioPermissionGranted =
|
|
||||||
JitsiMeetJS.mediaDevices.isDevicePermissionGranted('audio');
|
|
||||||
|
|
||||||
$selectCamera
|
|
||||||
.html(generateDevicesOptions(
|
|
||||||
video,
|
|
||||||
selectedVideoDevice ? selectedVideoDevice.deviceId : '',
|
|
||||||
videoPermissionGranted))
|
|
||||||
.prop('disabled', !video.length || !videoPermissionGranted);
|
|
||||||
|
|
||||||
initSelect2($selectCamera);
|
|
||||||
|
|
||||||
$selectMic
|
|
||||||
.html(generateDevicesOptions(
|
|
||||||
audio,
|
|
||||||
selectedAudioDevice ? selectedAudioDevice.deviceId : '',
|
|
||||||
audioPermissionGranted))
|
|
||||||
.prop('disabled', !audio.length || !audioPermissionGranted);
|
|
||||||
|
|
||||||
initSelect2($selectMic);
|
|
||||||
|
|
||||||
if (JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
|
|
||||||
$selectAudioOutput
|
|
||||||
.html(generateDevicesOptions(
|
|
||||||
audioOutput,
|
|
||||||
selectedAudioOutputDevice
|
|
||||||
? selectedAudioOutputDevice.deviceId
|
|
||||||
: 'default',
|
|
||||||
videoPermissionGranted || audioPermissionGranted))
|
|
||||||
.prop('disabled', !audioOutput.length ||
|
|
||||||
(!videoPermissionGranted && !audioPermissionGranted));
|
|
||||||
initSelect2($selectAudioOutput);
|
|
||||||
|
|
||||||
$selectAudioOutputParent.show();
|
|
||||||
} else {
|
|
||||||
$selectAudioOutputParent.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
APP.translation.translateElement($('#settings_container option'));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"@atlaskit/button-group": "1.0.0",
|
"@atlaskit/button-group": "1.0.0",
|
||||||
"@atlaskit/field-text": "2.0.3",
|
"@atlaskit/field-text": "2.0.3",
|
||||||
"@atlaskit/modal-dialog": "1.2.4",
|
"@atlaskit/modal-dialog": "1.2.4",
|
||||||
|
"@atlaskit/single-select": "1.6.1",
|
||||||
"@atlaskit/tabs": "1.2.5",
|
"@atlaskit/tabs": "1.2.5",
|
||||||
"async": "0.9.0",
|
"async": "0.9.0",
|
||||||
"autosize": "1.18.13",
|
"autosize": "1.18.13",
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Symbol } from '../react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of Redux action which signals that the currently used audio
|
||||||
|
* input device should be changed.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: SET_AUDIO_INPUT_DEVICE,
|
||||||
|
* deviceId: string,
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const SET_AUDIO_INPUT_DEVICE = Symbol('SET_AUDIO_INPUT_DEVICE');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of Redux action which signals that the currently used audio
|
||||||
|
* output device should be changed.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: SET_AUDIO_OUTPUT_DEVICE,
|
||||||
|
* deviceId: string,
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const SET_AUDIO_OUTPUT_DEVICE = Symbol('SET_AUDIO_OUTPUT_DEVICE');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of Redux action which signals that the currently used video
|
||||||
|
* input device should be changed.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: SET_VIDEO_INPUT_DEVICE,
|
||||||
|
* deviceId: string,
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const SET_VIDEO_INPUT_DEVICE = Symbol('SET_VIDEO_INPUT_DEVICE');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of Redux action which signals that the list of known available
|
||||||
|
* audio and video sources has changed.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: UPDATE_DEVICE_LIST,
|
||||||
|
* devices: Array<MediaDeviceInfo>,
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const UPDATE_DEVICE_LIST = Symbol('UPDATE_DEVICE_LIST');
|
|
@ -0,0 +1,71 @@
|
||||||
|
import {
|
||||||
|
SET_AUDIO_INPUT_DEVICE,
|
||||||
|
SET_AUDIO_OUTPUT_DEVICE,
|
||||||
|
SET_VIDEO_INPUT_DEVICE,
|
||||||
|
UPDATE_DEVICE_LIST
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals to update the currently used audio input device.
|
||||||
|
*
|
||||||
|
* @param {string} deviceId - The id of the new audio input device.
|
||||||
|
* @returns {{
|
||||||
|
* type: SET_AUDIO_INPUT_DEVICE,
|
||||||
|
* deviceId: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function setAudioInputDevice(deviceId) {
|
||||||
|
return {
|
||||||
|
type: SET_AUDIO_INPUT_DEVICE,
|
||||||
|
deviceId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals to update the currently used audio output device.
|
||||||
|
*
|
||||||
|
* @param {string} deviceId - The id of the new audio ouput device.
|
||||||
|
* @returns {{
|
||||||
|
* type: SET_AUDIO_OUTPUT_DEVICE,
|
||||||
|
* deviceId: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function setAudioOutputDevice(deviceId) {
|
||||||
|
return {
|
||||||
|
type: SET_AUDIO_OUTPUT_DEVICE,
|
||||||
|
deviceId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals to update the currently used video input device.
|
||||||
|
*
|
||||||
|
* @param {string} deviceId - The id of the new video input device.
|
||||||
|
* @returns {{
|
||||||
|
* type: SET_VIDEO_INPUT_DEVICE,
|
||||||
|
* deviceId: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function setVideoInputDevice(deviceId) {
|
||||||
|
return {
|
||||||
|
type: SET_VIDEO_INPUT_DEVICE,
|
||||||
|
deviceId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals to update the list of known audio and video devices.
|
||||||
|
*
|
||||||
|
* @param {Array<MediaDeviceInfo>} devices - All known available audio input,
|
||||||
|
* audio output, and video input devices.
|
||||||
|
* @returns {{
|
||||||
|
* type: UPDATE_DEVICE_LIST,
|
||||||
|
* devices: Array<MediaDeviceInfo>
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function updateDeviceList(devices) {
|
||||||
|
return {
|
||||||
|
type: UPDATE_DEVICE_LIST,
|
||||||
|
devices
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './actions';
|
||||||
|
export * from './actionTypes';
|
||||||
|
|
||||||
|
import './middleware';
|
||||||
|
import './reducer';
|
|
@ -0,0 +1,34 @@
|
||||||
|
/* global APP */
|
||||||
|
|
||||||
|
import UIEvents from '../../../../service/UI/UIEvents';
|
||||||
|
|
||||||
|
import { MiddlewareRegistry } from '../redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SET_AUDIO_INPUT_DEVICE,
|
||||||
|
SET_AUDIO_OUTPUT_DEVICE,
|
||||||
|
SET_VIDEO_INPUT_DEVICE
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the middleware of the feature base/devices.
|
||||||
|
*
|
||||||
|
* @param {Store} store - Redux store.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_AUDIO_INPUT_DEVICE:
|
||||||
|
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
|
||||||
|
break;
|
||||||
|
case SET_AUDIO_OUTPUT_DEVICE:
|
||||||
|
APP.UI.emitEvent(UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, action.deviceId);
|
||||||
|
break;
|
||||||
|
case SET_VIDEO_INPUT_DEVICE:
|
||||||
|
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
});
|
|
@ -0,0 +1,64 @@
|
||||||
|
import {
|
||||||
|
SET_AUDIO_INPUT_DEVICE,
|
||||||
|
SET_AUDIO_OUTPUT_DEVICE,
|
||||||
|
SET_VIDEO_INPUT_DEVICE,
|
||||||
|
UPDATE_DEVICE_LIST
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
import { ReducerRegistry } from '../redux';
|
||||||
|
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
audioInput: [],
|
||||||
|
audioOutput: [],
|
||||||
|
videoInput: []
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for actions which changes the state of known and used devices.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state of the feature features/base/devices.
|
||||||
|
* @param {Object} action - Action object.
|
||||||
|
* @param {string} action.type - Type of action.
|
||||||
|
* @param {Array<MediaDeviceInfo>} action.devices - All available audio and
|
||||||
|
* video devices.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
ReducerRegistry.register(
|
||||||
|
'features/base/devices',
|
||||||
|
(state = DEFAULT_STATE, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case UPDATE_DEVICE_LIST: {
|
||||||
|
const deviceList = _groupDevicesByKind(action.devices);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...deviceList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Changing of current audio and video device id is currently
|
||||||
|
// handled outside of react/redux. Fall through to default logic for
|
||||||
|
// now.
|
||||||
|
case SET_AUDIO_INPUT_DEVICE:
|
||||||
|
case SET_VIDEO_INPUT_DEVICE:
|
||||||
|
case SET_AUDIO_OUTPUT_DEVICE:
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an array of media devices into an object organized by device kind.
|
||||||
|
*
|
||||||
|
* @param {Array<MediaDeviceInfo>} devices - Available media devices.
|
||||||
|
* @private
|
||||||
|
* @returns {Object} An object with the media devices split by type. The keys
|
||||||
|
* are device type and the values are arrays with devices matching the device
|
||||||
|
* type.
|
||||||
|
*/
|
||||||
|
function _groupDevicesByKind(devices) {
|
||||||
|
return {
|
||||||
|
audioInput: devices.filter(device => device.kind === 'audioinput'),
|
||||||
|
audioOutput: devices.filter(device => device.kind === 'audiooutput'),
|
||||||
|
videoInput: devices.filter(device => device.kind === 'videoinput')
|
||||||
|
};
|
||||||
|
}
|
|
@ -60,3 +60,24 @@ export function loadConfig(host: string, path: string = '/config.js') {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a JitsiLocalTrack model from the given device id.
|
||||||
|
*
|
||||||
|
* @param {string} type - The media type of track being created. Expected values
|
||||||
|
* are "video" or "audio".
|
||||||
|
* @param {string} deviceId - The id of the target media source.
|
||||||
|
* @returns {Promise<JitsiLocalTrack>}
|
||||||
|
*/
|
||||||
|
export function createLocalTrack(type, deviceId) {
|
||||||
|
return JitsiMeetJS
|
||||||
|
.createLocalTracks({
|
||||||
|
devices: [ type ],
|
||||||
|
micDeviceId: deviceId,
|
||||||
|
cameraDeviceId: deviceId,
|
||||||
|
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
firefox_fake_device: window.config
|
||||||
|
&& window.config.firefox_fake_device
|
||||||
|
}).then(([ jitsiLocalTrack ]) => jitsiLocalTrack);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { JitsiTrackEvents } from '../../base/lib-jitsi-meet';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React component for displaying a audio level meter for a JitsiLocalTrack.
|
||||||
|
*/
|
||||||
|
class AudioInputPreview extends PureComponent {
|
||||||
|
/**
|
||||||
|
* AudioInputPreview component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/*
|
||||||
|
* The JitsiLocalTrack to show an audio level meter for.
|
||||||
|
*/
|
||||||
|
track: React.PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new AudioInputPreview instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only React Component props with which
|
||||||
|
* the new instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
audioLevel: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
this._updateAudioLevel = this._updateAudioLevel.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts listening for audio level updates after the initial render.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
this._listenForAudioUpdates(this.props.track);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops listening for audio level updates on the old track and starts
|
||||||
|
* listening instead on the new track.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this._listenForAudioUpdates(nextProps.track);
|
||||||
|
this._updateAudioLevel(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from audio level updates.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._stopListeningForAudioUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const audioMeterFill = {
|
||||||
|
width: `${Math.floor(this.state.audioLevel * 100)}%`
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className = 'audio-input-preview' >
|
||||||
|
<div
|
||||||
|
className = 'audio-input-preview-level'
|
||||||
|
style = { audioMeterFill } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts listening for audio level updates from the library.
|
||||||
|
*
|
||||||
|
* @param {JitstiLocalTrack} track - The track to listen to for audio level
|
||||||
|
* updates.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_listenForAudioUpdates(track) {
|
||||||
|
this._stopListeningForAudioUpdates();
|
||||||
|
|
||||||
|
track && track.on(
|
||||||
|
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||||
|
this._updateAudioLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops listening to further updates from the current track.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_stopListeningForAudioUpdates() {
|
||||||
|
this.props.track && this.props.track.off(
|
||||||
|
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||||
|
this._updateAudioLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the internal state of the last know audio level. The level should
|
||||||
|
* be between 0 and 1, as the level will be used as a percentage out of 1.
|
||||||
|
*
|
||||||
|
* @param {number} audioLevel - The new audio level for the track.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_updateAudioLevel(audioLevel) {
|
||||||
|
this.setState({
|
||||||
|
audioLevel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AudioInputPreview;
|
|
@ -0,0 +1,122 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
|
||||||
|
const TEST_SOUND_PATH = 'sounds/ring.wav';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React component for playing a test sound through a specified audio device.
|
||||||
|
*
|
||||||
|
* @extends Component
|
||||||
|
*/
|
||||||
|
class AudioOutputPreview extends Component {
|
||||||
|
/**
|
||||||
|
* AudioOutputPreview component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* The device id of the audio output device to use.
|
||||||
|
*/
|
||||||
|
deviceId: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: React.PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new AudioOutputPreview instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only React Component props with which
|
||||||
|
* the new instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._audioElement = null;
|
||||||
|
|
||||||
|
this._onClick = this._onClick.bind(this);
|
||||||
|
this._setAudioElement = this._setAudioElement.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the target output device on the component's audio element after
|
||||||
|
* initial render.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
this._setAudioSink();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the audio element when the target output device changes and the
|
||||||
|
* audio element has re-rendered.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
componentDidUpdate() {
|
||||||
|
this._setAudioSink();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className = 'audio-output-preview'>
|
||||||
|
<a onClick = { this._onClick }>
|
||||||
|
{ this.props.t('deviceSelection.testAudio') }
|
||||||
|
</a>
|
||||||
|
<audio
|
||||||
|
preload = 'auto'
|
||||||
|
ref = { this._setAudioElement }
|
||||||
|
src = { TEST_SOUND_PATH } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays a test sound.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onClick() {
|
||||||
|
this._audioElement
|
||||||
|
&& this._audioElement.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the instance variable for the component's audio element so it can be
|
||||||
|
* accessed directly.
|
||||||
|
*
|
||||||
|
* @param {Object} element - The DOM element for the component's audio.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_setAudioElement(element) {
|
||||||
|
this._audioElement = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the target output device for playing the test sound.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_setAudioSink() {
|
||||||
|
this._audioElement
|
||||||
|
&& this._audioElement.setSinkId(this.props.deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(AudioOutputPreview);
|
|
@ -0,0 +1,597 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
setAudioInputDevice,
|
||||||
|
setAudioOutputDevice,
|
||||||
|
setVideoInputDevice
|
||||||
|
} from '../../base/devices';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
hideDialog
|
||||||
|
} from '../../base/dialog';
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
import { createLocalTrack } from '../../base/lib-jitsi-meet';
|
||||||
|
|
||||||
|
import AudioInputPreview from './AudioInputPreview';
|
||||||
|
import AudioOutputPreview from './AudioOutputPreview';
|
||||||
|
import DeviceSelector from './DeviceSelector';
|
||||||
|
import VideoInputPreview from './VideoInputPreview';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React component for previewing and selecting new audio and video sources.
|
||||||
|
*
|
||||||
|
* @extends Component
|
||||||
|
*/
|
||||||
|
class DeviceSelectionDialog extends Component {
|
||||||
|
/**
|
||||||
|
* DeviceSelectionDialog component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* All known audio and video devices split by type. This prop comes from
|
||||||
|
* the app state.
|
||||||
|
*/
|
||||||
|
_devices: React.PropTypes.object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device id for the current audio output device.
|
||||||
|
*/
|
||||||
|
currentAudioOutputId: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JitsiLocalTrack for the current local audio.
|
||||||
|
*
|
||||||
|
* JitsiLocalTracks for the current audio and video, if any, should be
|
||||||
|
* passed in for re-use in the previews. This is needed for Internet
|
||||||
|
* Explorer, which cannot get multiple tracks from the same device, even
|
||||||
|
* across tabs.
|
||||||
|
*/
|
||||||
|
currentAudioTrack: React.PropTypes.object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JitsiLocalTrack for the current local video.
|
||||||
|
*
|
||||||
|
* Needed for reuse. See comment for propTypes.currentAudioTrack.
|
||||||
|
*/
|
||||||
|
currentVideoTrack: React.PropTypes.object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the audio selector can be interacted with. If true,
|
||||||
|
* the audio input selector will be rendered as disabled. This is
|
||||||
|
* specifically used to prevent audio device changing in Firefox, which
|
||||||
|
* currently does not work due to a browser-side regression.
|
||||||
|
*/
|
||||||
|
disableAudioInputChange: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if device changing is configured to be disallowed. Selectors
|
||||||
|
* will display as disabled.
|
||||||
|
*/
|
||||||
|
disableDeviceChange: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to notify the store of app state changes.
|
||||||
|
*/
|
||||||
|
dispatch: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not new audio input source can be selected.
|
||||||
|
*/
|
||||||
|
hasAudioPermission: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not new video input sources can be selected.
|
||||||
|
*/
|
||||||
|
hasVideoPermission: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the audio meter will not display. Necessary for browsers or
|
||||||
|
* configurations that do not support local stats to prevent a
|
||||||
|
* non-responsive mic preview from displaying.
|
||||||
|
*/
|
||||||
|
hideAudioInputPreview: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the audio output source selector should display. If
|
||||||
|
* true, the audio output selector and test audio link will not be
|
||||||
|
* rendered. This is specifically used for hiding audio output on
|
||||||
|
* temasys browsers which do not support such change.
|
||||||
|
*/
|
||||||
|
hideAudioOutputSelect: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: React.PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new DeviceSelectionDialog instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only React Component props with which
|
||||||
|
* the new instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
// JitsiLocalTracks to use for live previewing.
|
||||||
|
previewAudioTrack: null,
|
||||||
|
previewVideoTrack: null,
|
||||||
|
|
||||||
|
// Device ids to keep track of new selections.
|
||||||
|
videInput: null,
|
||||||
|
audioInput: null,
|
||||||
|
audioOutput: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preventing closing while cleaning up previews is important for
|
||||||
|
// supporting temasys video cleanup. Temasys requires its video object
|
||||||
|
// to be in the dom and visible for proper detaching of tracks. Delaying
|
||||||
|
// closure until cleanup is complete ensures no errors in the process.
|
||||||
|
this._isClosing = false;
|
||||||
|
|
||||||
|
this._closeModal = this._closeModal.bind(this);
|
||||||
|
this._getAndSetAudioOutput = this._getAndSetAudioOutput.bind(this);
|
||||||
|
this._getAndSetAudioTrack = this._getAndSetAudioTrack.bind(this);
|
||||||
|
this._getAndSetVideoTrack = this._getAndSetVideoTrack.bind(this);
|
||||||
|
this._onCancel = this._onCancel.bind(this);
|
||||||
|
this._onSubmit = this._onSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up any preview tracks that might not have been cleaned up already.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
componentWillUnmount() {
|
||||||
|
// This handles the case where neither submit nor cancel were triggered,
|
||||||
|
// such as on modal switch. In that case, make a dying attempt to clean
|
||||||
|
// up previews.
|
||||||
|
if (!this._isClosing) {
|
||||||
|
this._attemptPreviewTrackCleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
cancelTitleKey = { 'dialog.Cancel' }
|
||||||
|
okTitleKey = { 'dialog.Save' }
|
||||||
|
onCancel = { this._onCancel }
|
||||||
|
onSubmit = { this._onSubmit }
|
||||||
|
titleKey = 'deviceSelection.deviceSettings' >
|
||||||
|
<div className = 'device-selection'>
|
||||||
|
<div className = 'device-selection-column-selectors'>
|
||||||
|
<div className = 'device-selectors'>
|
||||||
|
{ this._renderSelectors() }
|
||||||
|
</div>
|
||||||
|
{ this._renderAudioOutputPreview() }
|
||||||
|
</div>
|
||||||
|
<div className = 'device-selection-column-video'>
|
||||||
|
<div className = 'device-selection-video-container'>
|
||||||
|
<VideoInputPreview
|
||||||
|
track = { this.state.previewVideoTrack
|
||||||
|
|| this.props.currentVideoTrack } />
|
||||||
|
</div>
|
||||||
|
{ this._renderAudioInputPreview() }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up preview tracks if they are not active tracks.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {Array<Promise>} Zero to two promises will be returned. One
|
||||||
|
* promise can be for video cleanup and another for audio cleanup.
|
||||||
|
*/
|
||||||
|
_attemptPreviewTrackCleanup() {
|
||||||
|
const cleanupPromises = [];
|
||||||
|
|
||||||
|
if (!this._isPreviewingCurrentVideoTrack()) {
|
||||||
|
cleanupPromises.push(this._disposeVideoPreview());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._isPreviewingCurrentAudioTrack()) {
|
||||||
|
cleanupPromises.push(this._disposeAudioPreview());
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanupPromises;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals to close DeviceSelectionDialog.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_closeModal() {
|
||||||
|
this.props.dispatch(hideDialog());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function for disposing the current audio preview.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_disposeAudioPreview() {
|
||||||
|
return this.state.previewAudioTrack
|
||||||
|
? this.state.previewAudioTrack.dispose() : Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function for disposing the current video preview.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_disposeVideoPreview() {
|
||||||
|
return this.state.previewVideoTrack
|
||||||
|
? this.state.previewVideoTrack.dispose() : Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked when a new audio output device has been selected.
|
||||||
|
* Updates the internal state of the user's selection.
|
||||||
|
*
|
||||||
|
* @param {string} deviceId - The id of the chosen audio output device.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_getAndSetAudioOutput(deviceId) {
|
||||||
|
this.setState({
|
||||||
|
audioOutput: deviceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked when a new audio input device has been selected.
|
||||||
|
* Updates the internal state of the user's selection as well as the audio
|
||||||
|
* track that should display in the preview. Will reuse the current local
|
||||||
|
* audio track if it has been selected.
|
||||||
|
*
|
||||||
|
* @param {string} deviceId - The id of the chosen audio input device.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_getAndSetAudioTrack(deviceId) {
|
||||||
|
this.setState({
|
||||||
|
audioInput: deviceId
|
||||||
|
}, () => {
|
||||||
|
const cleanupPromise = this._isPreviewingCurrentAudioTrack()
|
||||||
|
? Promise.resolve() : this._disposeAudioPreview();
|
||||||
|
|
||||||
|
if (this._isCurrentAudioTrack(deviceId)) {
|
||||||
|
cleanupPromise
|
||||||
|
.then(() => {
|
||||||
|
this.setState({
|
||||||
|
previewAudioTrack: this.props.currentAudioTrack
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cleanupPromise
|
||||||
|
.then(() => createLocalTrack('audio', deviceId))
|
||||||
|
.then(jitsiLocalTrack => {
|
||||||
|
this.setState({
|
||||||
|
previewAudioTrack: jitsiLocalTrack
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked when a new video input device has been selected. Updates
|
||||||
|
* the internal state of the user's selection as well as the video track
|
||||||
|
* that should display in the preview. Will reuse the current local video
|
||||||
|
* track if it has been selected.
|
||||||
|
*
|
||||||
|
* @param {string} deviceId - The id of the chosen video input device.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_getAndSetVideoTrack(deviceId) {
|
||||||
|
this.setState({
|
||||||
|
videoInput: deviceId
|
||||||
|
}, () => {
|
||||||
|
const cleanupPromise = this._isPreviewingCurrentVideoTrack()
|
||||||
|
? Promise.resolve() : this._disposeVideoPreview();
|
||||||
|
|
||||||
|
if (this._isCurrentVideoTrack(deviceId)) {
|
||||||
|
cleanupPromise
|
||||||
|
.then(() => {
|
||||||
|
this.setState({
|
||||||
|
previewVideoTrack: this.props.currentVideoTrack
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cleanupPromise
|
||||||
|
.then(() => createLocalTrack('video', deviceId))
|
||||||
|
.then(jitsiLocalTrack => {
|
||||||
|
this.setState({
|
||||||
|
previewVideoTrack: jitsiLocalTrack
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function for determining if the current local audio track has the
|
||||||
|
* passed in device id.
|
||||||
|
*
|
||||||
|
* @param {string} deviceId - The device id to match against.
|
||||||
|
* @private
|
||||||
|
* @returns {boolean} True if the device id is being used by the local audio
|
||||||
|
* track.
|
||||||
|
*/
|
||||||
|
_isCurrentAudioTrack(deviceId) {
|
||||||
|
return this.props.currentAudioTrack
|
||||||
|
&& this.props.currentAudioTrack.getDeviceId() === deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function for determining if the current local video track has the
|
||||||
|
* passed in device id.
|
||||||
|
*
|
||||||
|
* @param {string} deviceId - The device id to match against.
|
||||||
|
* @private
|
||||||
|
* @returns {boolean} True if the device id is being used by the local
|
||||||
|
* video track.
|
||||||
|
*/
|
||||||
|
_isCurrentVideoTrack(deviceId) {
|
||||||
|
return this.props.currentVideoTrack
|
||||||
|
&& this.props.currentVideoTrack.getDeviceId() === deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function for detecting if the current audio preview track is not
|
||||||
|
* the currently used audio track.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {boolean} True if the current audio track is being used for
|
||||||
|
* the preview.
|
||||||
|
*/
|
||||||
|
_isPreviewingCurrentAudioTrack() {
|
||||||
|
return !this.state.previewAudioTrack
|
||||||
|
|| this.state.previewAudioTrack === this.props.currentAudioTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function for detecting if the current video preview track is not
|
||||||
|
* the currently used video track.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {boolean} True if the current video track is being used as the
|
||||||
|
* preview.
|
||||||
|
*/
|
||||||
|
_isPreviewingCurrentVideoTrack() {
|
||||||
|
return !this.state.previewVideoTrack
|
||||||
|
|| this.state.previewVideoTrack === this.props.currentVideoTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans existing preview tracks and signal to closeDeviceSelectionDialog.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {boolean} Returns false to prevent closure until cleanup is
|
||||||
|
* complete.
|
||||||
|
*/
|
||||||
|
_onCancel() {
|
||||||
|
if (this._isClosing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._isClosing = true;
|
||||||
|
|
||||||
|
const cleanupPromises = this._attemptPreviewTrackCleanup();
|
||||||
|
|
||||||
|
Promise.all(cleanupPromises)
|
||||||
|
.then(this._closeModal)
|
||||||
|
.catch(this._closeModal);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identify changes to the preferred input/output devices and perform
|
||||||
|
* necessary cleanup and requests to use those devices. Closes the modal
|
||||||
|
* after cleanup and device change requests complete.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {boolean} Returns false to prevent closure until cleanup is
|
||||||
|
* complete.
|
||||||
|
*/
|
||||||
|
_onSubmit() {
|
||||||
|
if (this._isClosing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._isClosing = true;
|
||||||
|
|
||||||
|
const deviceChangePromises = [];
|
||||||
|
|
||||||
|
if (this.state.videoInput && !this._isPreviewingCurrentVideoTrack()) {
|
||||||
|
const changeVideoPromise = this._disposeVideoPreview()
|
||||||
|
.then(() => {
|
||||||
|
this.props.dispatch(setVideoInputDevice(
|
||||||
|
this.state.videoInput));
|
||||||
|
});
|
||||||
|
|
||||||
|
deviceChangePromises.push(changeVideoPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.audioInput && !this._isPreviewingCurrentAudioTrack()) {
|
||||||
|
const changeAudioPromise = this._disposeAudioPreview()
|
||||||
|
.then(() => {
|
||||||
|
this.props.dispatch(setAudioInputDevice(
|
||||||
|
this.state.audioInput));
|
||||||
|
});
|
||||||
|
|
||||||
|
deviceChangePromises.push(changeAudioPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.audioOutput
|
||||||
|
&& this.state.audioOutput !== this.props.currentAudioOutputId) {
|
||||||
|
this.props.dispatch(setAudioOutputDevice(this.state.audioOutput));
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(deviceChangePromises)
|
||||||
|
.then(this._closeModal)
|
||||||
|
.catch(this._closeModal);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an AudioInputPreview for previewing if audio is being received.
|
||||||
|
* Null will be returned if local stats for tracking audio input levels
|
||||||
|
* cannot be obtained.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {ReactComponent|null}
|
||||||
|
*/
|
||||||
|
_renderAudioInputPreview() {
|
||||||
|
if (this.props.hideAudioInputPreview) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AudioInputPreview
|
||||||
|
track = { this.state.previewAudioTrack
|
||||||
|
|| this.props.currentAudioTrack } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an AudioOutputPreview instance for playing a test sound with the
|
||||||
|
* passed in device id. Null will be returned if hideAudioOutput is truthy.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {ReactComponent|null}
|
||||||
|
*/
|
||||||
|
_renderAudioOutputPreview() {
|
||||||
|
if (this.props.hideAudioOutputSelect) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AudioOutputPreview
|
||||||
|
deviceId = { this.state.audioOutput
|
||||||
|
|| this.props.currentAudioOutputId } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a DeviceSelector instance based on the passed in configuration.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {Object} props - The props for the DeviceSelector.
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
_renderSelector(props) {
|
||||||
|
return (
|
||||||
|
<DeviceSelector { ...props } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates DeviceSelector instances for video output, audio input, and audio
|
||||||
|
* output.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {Array<ReactElement>} DeviceSelector instances.
|
||||||
|
*/
|
||||||
|
_renderSelectors() {
|
||||||
|
const availableDevices = this.props._devices;
|
||||||
|
const currentAudioId = this.state.audioInput
|
||||||
|
|| (this.props.currentAudioTrack
|
||||||
|
&& this.props.currentAudioTrack.getDeviceId());
|
||||||
|
const currentAudioOutId = this.state.audioOutput
|
||||||
|
|| this.props.currentAudioOutputId;
|
||||||
|
|
||||||
|
// FIXME: On temasys, without a device selected and put into local
|
||||||
|
// storage as the default device to use, the current video device id is
|
||||||
|
// a blank string. This is because the library gets a local video track
|
||||||
|
// and then maps the track's device id by matching the track's label to
|
||||||
|
// the MediaDeviceInfos returned from enumerateDevices. In WebRTC, the
|
||||||
|
// track label is expected to return the camera device label. However,
|
||||||
|
// temasys video track labels refer to track id, not device label, so
|
||||||
|
// the library cannot match the track to a device. The workaround of
|
||||||
|
// defaulting to the first videoInput available has been re-used from
|
||||||
|
// the previous device settings implementation.
|
||||||
|
const currentVideoId = this.state.videoInput
|
||||||
|
|| (this.props.currentVideoTrack
|
||||||
|
&& this.props.currentVideoTrack.getDeviceId())
|
||||||
|
|| (availableDevices.videoInput[0]
|
||||||
|
&& availableDevices.videoInput[0].deviceId)
|
||||||
|
|| ''; // DeviceSelector expects a string for prop selectedDeviceId.
|
||||||
|
|
||||||
|
const configurations = [
|
||||||
|
{
|
||||||
|
devices: availableDevices.videoInput,
|
||||||
|
hasPermission: this.props.hasVideoPermission,
|
||||||
|
isDisabled: this.props.disableDeviceChange,
|
||||||
|
key: 'videoInput',
|
||||||
|
label: 'settings.selectCamera',
|
||||||
|
onSelect: this._getAndSetVideoTrack,
|
||||||
|
selectedDeviceId: currentVideoId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
devices: availableDevices.audioInput,
|
||||||
|
hasPermission: this.props.hasAudioPermission,
|
||||||
|
isDisabled: this.props.disableAudioInputChange
|
||||||
|
|| this.props.disableDeviceChange,
|
||||||
|
key: 'audioInput',
|
||||||
|
label: 'settings.selectMic',
|
||||||
|
onSelect: this._getAndSetAudioTrack,
|
||||||
|
selectedDeviceId: currentAudioId
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!this.props.hideAudioOutputSelect) {
|
||||||
|
configurations.push({
|
||||||
|
devices: availableDevices.audioOutput,
|
||||||
|
hasPermission: this.props.hasAudioPermission
|
||||||
|
|| this.props.hasVideoPermission,
|
||||||
|
isDisabled: this.props.disableDeviceChange,
|
||||||
|
key: 'audioOutput',
|
||||||
|
label: 'settings.selectAudioOutput',
|
||||||
|
onSelect: this._getAndSetAudioOutput,
|
||||||
|
selectedDeviceId: currentAudioOutId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return configurations.map(this._renderSelector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps (parts of) the Redux state to the associated DeviceSelectionDialog's
|
||||||
|
* props.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @private
|
||||||
|
* @returns {{
|
||||||
|
* _devices: Object
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
function _mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
_devices: state['features/base/devices']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps)(DeviceSelectionDialog));
|
|
@ -0,0 +1,180 @@
|
||||||
|
import Select from '@atlaskit/single-select';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React component for selecting a device from a select element. Wraps Select
|
||||||
|
* with device selection specific logic.
|
||||||
|
*
|
||||||
|
* @extends Component
|
||||||
|
*/
|
||||||
|
class DeviceSelector extends Component {
|
||||||
|
/**
|
||||||
|
* DeviceSelector component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* MediaDeviceInfos used for display in the select element.
|
||||||
|
*/
|
||||||
|
devices: React.PropTypes.array,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If false, will return a selector with no selection options.
|
||||||
|
*/
|
||||||
|
hasPermission: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, will render the selector disabled with a default selection.
|
||||||
|
*/
|
||||||
|
isDisabled: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The translation key to display as a menu label.
|
||||||
|
*/
|
||||||
|
label: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The callback to invoke when a selection is made.
|
||||||
|
*/
|
||||||
|
onSelect: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default device to display as selected.
|
||||||
|
*/
|
||||||
|
selectedDeviceId: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: React.PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new DeviceSelector instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only React Component props with which
|
||||||
|
* the new instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._onSelect = this._onSelect.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
if (!this.props.hasPermission) {
|
||||||
|
return this._renderNoPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.props.devices.length) {
|
||||||
|
return this._renderNoDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = this.props.devices.map(this._createSelectItem);
|
||||||
|
const defaultSelected = items.find(item =>
|
||||||
|
item.value === this.props.selectedDeviceId
|
||||||
|
);
|
||||||
|
|
||||||
|
return this._createSelector({
|
||||||
|
defaultSelected,
|
||||||
|
isDisabled: this.props.isDisabled,
|
||||||
|
items,
|
||||||
|
placeholder: 'deviceSelection.selectADevice'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an object in the format expected by Select for an option element.
|
||||||
|
*
|
||||||
|
* @param {MediaDeviceInfo} device - An object with a label and a deviceId.
|
||||||
|
* @private
|
||||||
|
* @returns {Object} The passed in media device description converted to a
|
||||||
|
* format recognized as a valid Select item.
|
||||||
|
*/
|
||||||
|
_createSelectItem(device) {
|
||||||
|
return {
|
||||||
|
content: device.label,
|
||||||
|
value: device.deviceId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Select Component using passed in props and options.
|
||||||
|
*
|
||||||
|
* @param {Object} options - Additional configuration for display Select.
|
||||||
|
* @param {Object} options.defaultSelected - The option that should be set
|
||||||
|
* as currently chosen.
|
||||||
|
* @param {boolean} options.isDisabled - If true Select will not open on
|
||||||
|
* click.
|
||||||
|
* @param {Array} options.items - All the selectable options to display.
|
||||||
|
* @param {string} options.placeholder - The translation key to display when
|
||||||
|
* no selection has been made.
|
||||||
|
* @private
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
_createSelector(options) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
defaultSelected = { options.defaultSelected }
|
||||||
|
isDisabled = { options.isDisabled }
|
||||||
|
isFirstChild = { true }
|
||||||
|
items = { [ { items: options.items || [] } ] }
|
||||||
|
label = { this.props.t(this.props.label) }
|
||||||
|
noMatchesFound
|
||||||
|
= { this.props.t('deviceSelection.noOtherDevices') }
|
||||||
|
onSelected = { this._onSelect }
|
||||||
|
placeholder = { this.props.t(options.placeholder) }
|
||||||
|
shouldFitContainer = { true } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the passed in callback to notify of selection changes.
|
||||||
|
*
|
||||||
|
* @param {Object} selection - Event returned from Select.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onSelect(selection) {
|
||||||
|
this.props.onSelect(selection.item.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Select Component that is disabled and has a placeholder
|
||||||
|
* indicating there are no devices to select.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
_renderNoDevices() {
|
||||||
|
return this._createSelector({
|
||||||
|
isDisabled: true,
|
||||||
|
placeholder: 'settings.noDevice'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Select Component that is disabled and has a placeholder stating
|
||||||
|
* there is no permission to display the devices.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
_renderNoPermission() {
|
||||||
|
return this._createSelector({
|
||||||
|
isDisabled: true,
|
||||||
|
placeholder: 'settings.noPermission'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(DeviceSelector);
|
|
@ -0,0 +1,203 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
|
||||||
|
const VIDEO_MUTE_CLASS = 'video-muted';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React component for displaying video. This component defers to lib-jitsi-meet
|
||||||
|
* logic for rendering the video.
|
||||||
|
*
|
||||||
|
* @extends Component
|
||||||
|
*/
|
||||||
|
class VideoInputPreview extends Component {
|
||||||
|
/**
|
||||||
|
* VideoInputPreview component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: React.PropTypes.func,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The JitsiLocalTrack to display.
|
||||||
|
*/
|
||||||
|
track: React.PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new VideoInputPreview instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only React Component props with which
|
||||||
|
* the new instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._rootElement = null;
|
||||||
|
this._videoElement = null;
|
||||||
|
|
||||||
|
this._setRootElement = this._setRootElement.bind(this);
|
||||||
|
this._setVideoElement = this._setVideoElement.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the library for rendering the video on initial display.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
this._attachTrack(this.props.track);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove any existing associations between the current previewed track and
|
||||||
|
* the component's video element.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._detachTrack(this.props.track);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className = 'video-input-preview'
|
||||||
|
ref = { this._setRootElement }>
|
||||||
|
<video
|
||||||
|
autoPlay = { true }
|
||||||
|
className = 'video-input-preview-display flipVideoX'
|
||||||
|
ref = { this._setVideoElement } />
|
||||||
|
<div className = 'video-input-preview-muted'>
|
||||||
|
{ this.props.t('videothumbnail.muted') }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only update when the deviceId has changed. This component is somewhat
|
||||||
|
* black-boxed from React's rendering so lib-jitsi-meet can instead handle
|
||||||
|
* updating of the video preview, which takes browser differences into
|
||||||
|
* consideration. For example, temasys's video object must be visible to
|
||||||
|
* update the displayed track, but React's re-rendering could potentially
|
||||||
|
* remove the video object from the page.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
if (nextProps.track !== this.props.track) {
|
||||||
|
this._detachTrack(this.props.track);
|
||||||
|
this._attachTrack(nextProps.track);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls into the passed in track to associate the track with the
|
||||||
|
* component's video element and render video. Also sets the instance
|
||||||
|
* variable for the video element as the element the track attached to,
|
||||||
|
* which could be an Object if on a temasys supported browser.
|
||||||
|
*
|
||||||
|
* @param {JitsiLocalTrack} track - The library's track model which will be
|
||||||
|
* displayed.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_attachTrack(track) {
|
||||||
|
if (!track) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not attempt to display a preview if the track is muted, as the
|
||||||
|
// library will simply return a falsy value for the element anyway.
|
||||||
|
if (track.isMuted()) {
|
||||||
|
this._showMuteOverlay(true);
|
||||||
|
} else {
|
||||||
|
this._showMuteOverlay(false);
|
||||||
|
|
||||||
|
const updatedVideoElement = track.attach(this._videoElement);
|
||||||
|
|
||||||
|
this._setVideoElement(updatedVideoElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the association to the component's video element from the passed
|
||||||
|
* in JitsiLocalTrack to stop the track from rendering. With temasys, the
|
||||||
|
* video element must still be visible for detaching to complete.
|
||||||
|
*
|
||||||
|
* @param {JitsiLocalTrack} track - The library's track model which needs
|
||||||
|
* to stop previewing in the video element.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_detachTrack(track) {
|
||||||
|
// Detach the video element from the track only if it has already
|
||||||
|
// been attached. This accounts for a special case with temasys
|
||||||
|
// where if detach is being called before attach, the video
|
||||||
|
// element is converted to Object without updating this
|
||||||
|
// component's reference to the video element.
|
||||||
|
if (this._videoElement
|
||||||
|
&& track
|
||||||
|
&& track.containers.includes(this._videoElement)) {
|
||||||
|
track.detach(this._videoElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the component's root element.
|
||||||
|
*
|
||||||
|
* @param {Object} element - The highest DOM element in the component.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_setRootElement(element) {
|
||||||
|
this._rootElement = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an instance variable for the component's video element so it can be
|
||||||
|
* referenced later for attaching and detaching a JitsiLocalTrack.
|
||||||
|
*
|
||||||
|
* @param {Object} element - DOM element for the component's video display.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_setVideoElement(element) {
|
||||||
|
this._videoElement = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or removes a class to the component's parent node to indicate mute
|
||||||
|
* status.
|
||||||
|
*
|
||||||
|
* @param {boolean} shouldShow - True if the mute class should be added and
|
||||||
|
* false if the class should be removed.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_showMuteOverlay(shouldShow) {
|
||||||
|
if (shouldShow) {
|
||||||
|
this._rootElement.classList.add(VIDEO_MUTE_CLASS);
|
||||||
|
} else {
|
||||||
|
this._rootElement.classList.remove(VIDEO_MUTE_CLASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(VideoInputPreview);
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as DeviceSelectionDialog } from './DeviceSelectionDialog';
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './components';
|
Loading…
Reference in New Issue