feat(device_selection): Implement popup
This commit is contained in:
parent
2c002c875d
commit
96e83989a5
2
Makefile
2
Makefile
|
@ -31,6 +31,8 @@ deploy-appbundle:
|
|||
$(BUILD_DIR)/do_external_connect.min.map \
|
||||
$(BUILD_DIR)/external_api.min.js \
|
||||
$(BUILD_DIR)/external_api.min.map \
|
||||
$(BUILD_DIR)/device_selection_popup_bundle.min.js \
|
||||
$(BUILD_DIR)/device_selection_popup_bundle.min.map \
|
||||
$(OUTPUT_DIR)/analytics.js \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
|
|
|
@ -136,7 +136,7 @@
|
|||
&__videos-filmstripOnly {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
padding-right: $defaultToolbarSize;
|
||||
padding-right: $defaultFilmStripOnlyToolbarSize;
|
||||
}
|
||||
|
||||
.remote-videos-container {
|
||||
|
|
|
@ -200,7 +200,13 @@
|
|||
height: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: $defaultToolbarSize;
|
||||
width: $defaultFilmStripOnlyToolbarSize;
|
||||
|
||||
.button {
|
||||
height: 37px;
|
||||
line-height: 37px !important;
|
||||
width: 37px;
|
||||
}
|
||||
|
||||
.button:first-child {
|
||||
border-top-left-radius: 3px;
|
||||
|
|
|
@ -34,6 +34,7 @@ $tooltipBg: rgba(0,0,0, 0.7);
|
|||
* Toolbar
|
||||
*/
|
||||
$defaultToolbarSize: 50px;
|
||||
$defaultFilmStripOnlyToolbarSize: 37px;
|
||||
$splitterToolbarButtonMargin: 18px;
|
||||
$toolbarBackground: rgba(0, 0, 0, 0.5);
|
||||
$toolbarBadgeBackground: #165ECC;
|
||||
|
|
|
@ -36,14 +36,14 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
|
|||
*/
|
||||
TOOLBAR_BUTTONS: [
|
||||
//main toolbar
|
||||
'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup',
|
||||
'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup', 'deviceselection', // jshint ignore:line
|
||||
//extended toolbar
|
||||
'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
|
||||
/**
|
||||
* Main Toolbar Buttons
|
||||
* All of them should be in TOOLBAR_BUTTONS
|
||||
*/
|
||||
MAIN_TOOLBAR_BUTTONS: ['microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup'], // jshint ignore:line
|
||||
MAIN_TOOLBAR_BUTTONS: ['microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup', 'deviceselection'], // jshint ignore:line
|
||||
SETTINGS_SECTIONS: ['language', 'devices', 'moderator'],
|
||||
// Determines how the video would fit the screen. 'both' would fit the whole
|
||||
// screen, 'height' would fit the original video height to the height of the
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { hideDialog } from '../actions';
|
||||
import { dialogPropTypes } from '../constants';
|
||||
|
||||
/**
|
||||
* Abstract dialog to display dialogs.
|
||||
|
@ -13,57 +14,12 @@ export default class AbstractDialog extends Component {
|
|||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Whether cancel button is disabled. Enabled by default.
|
||||
*/
|
||||
cancelDisabled: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Optional i18n key to change the cancel button title.
|
||||
*/
|
||||
cancelTitleKey: React.PropTypes.string,
|
||||
...dialogPropTypes,
|
||||
|
||||
/**
|
||||
* Used to show/hide the dialog on cancel.
|
||||
*/
|
||||
dispatch: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Is ok button enabled/disabled. Enabled by default.
|
||||
*/
|
||||
okDisabled: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Optional i18n key to change the ok button title.
|
||||
*/
|
||||
okTitleKey: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* The handler for onCancel event.
|
||||
*/
|
||||
onCancel: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* The handler for the event when submitting the dialog.
|
||||
*/
|
||||
onSubmit: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Used to obtain translations in children classes.
|
||||
*/
|
||||
t: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Key to use for showing a title.
|
||||
*/
|
||||
titleKey: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* The string to use as a title instead of {@code titleKey}. If a truthy
|
||||
* value is specified, it takes precedence over {@code titleKey} i.e.
|
||||
* the latter is unused.
|
||||
*/
|
||||
titleString: React.PropTypes.string
|
||||
dispatch: React.PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import AKButton from '@atlaskit/button';
|
||||
import AKButtonGroup from '@atlaskit/button-group';
|
||||
import ModalDialog from '@atlaskit/modal-dialog';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../i18n';
|
||||
|
||||
import AbstractDialog from './AbstractDialog';
|
||||
import StatelessDialog from './StatelessDialog';
|
||||
|
||||
/**
|
||||
* Web dialog that uses atlaskit modal-dialog to display dialogs.
|
||||
|
@ -19,6 +15,8 @@ class Dialog extends AbstractDialog {
|
|||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
...AbstractDialog.propTypes,
|
||||
|
||||
/**
|
||||
* This is the body of the dialog, the component children.
|
||||
*/
|
||||
|
@ -30,6 +28,11 @@ class Dialog extends AbstractDialog {
|
|||
*/
|
||||
isModal: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Disables rendering of the submit button.
|
||||
*/
|
||||
submitDisabled: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Width of the dialog, can be:
|
||||
* - 'small' (400px), 'medium' (600px), 'large' (800px),
|
||||
|
@ -40,6 +43,19 @@ class Dialog extends AbstractDialog {
|
|||
width: React.PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new Dialog instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
|
@ -47,101 +63,15 @@ class Dialog extends AbstractDialog {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<ModalDialog
|
||||
footer = { this._renderFooter() }
|
||||
header = { this._renderHeader() }
|
||||
isOpen = { true }
|
||||
onDialogDismissed = { this._onCancel }
|
||||
width = { this.props.width || 'medium' }>
|
||||
<div>
|
||||
<form
|
||||
className = 'modal-dialog-form'
|
||||
id = 'modal-dialog-form'
|
||||
onSubmit = { this._onSubmit }>
|
||||
{ this.props.children }
|
||||
</form>
|
||||
</div>
|
||||
</ModalDialog>);
|
||||
}
|
||||
const props = {
|
||||
...this.props,
|
||||
onSubmit: this._onSubmit,
|
||||
onCancel: this._onCancel
|
||||
};
|
||||
|
||||
/**
|
||||
* Render cancel button.
|
||||
*
|
||||
* @returns {*} The cancel button if enabled and dialog is not modal.
|
||||
* @private
|
||||
*/
|
||||
_renderCancelButton() {
|
||||
if (this.props.cancelDisabled || this.props.isModal) {
|
||||
return null;
|
||||
}
|
||||
delete props.dispatch;
|
||||
|
||||
return (
|
||||
<AKButton
|
||||
appearance = 'subtle'
|
||||
id = 'modal-dialog-cancel-button'
|
||||
onClick = { this._onCancel }>
|
||||
{ this.props.t(this.props.cancelTitleKey || 'dialog.Cancel') }
|
||||
</AKButton>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render component in dialog footer.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderFooter() {
|
||||
return (
|
||||
<footer className = 'modal-dialog-footer'>
|
||||
<AKButtonGroup>
|
||||
{ this._renderCancelButton() }
|
||||
{ this._renderOKButton() }
|
||||
</AKButtonGroup>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render component in dialog header.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderHeader() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<header>
|
||||
<h2>
|
||||
{ this.props.titleString || t(this.props.titleKey) }
|
||||
</h2>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render ok button.
|
||||
*
|
||||
* @returns {*} The ok button if enabled.
|
||||
* @private
|
||||
*/
|
||||
_renderOKButton() {
|
||||
if (this.props.submitDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AKButton
|
||||
appearance = 'primary'
|
||||
form = 'modal-dialog-form'
|
||||
id = 'modal-dialog-ok-button'
|
||||
isDisabled = { this.props.okDisabled }
|
||||
onClick = { this._onSubmit }>
|
||||
{ this.props.t(this.props.okTitleKey || 'dialog.Ok') }
|
||||
</AKButton>
|
||||
);
|
||||
return <StatelessDialog { ...props } />;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -158,4 +88,4 @@ class Dialog extends AbstractDialog {
|
|||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(Dialog));
|
||||
export default connect()(Dialog);
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
import AKButton from '@atlaskit/button';
|
||||
import AKButtonGroup from '@atlaskit/button-group';
|
||||
import ModalDialog from '@atlaskit/modal-dialog';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../i18n';
|
||||
|
||||
import { dialogPropTypes } from '../constants';
|
||||
|
||||
/**
|
||||
* Web dialog that uses atlaskit modal-dialog to display dialogs.
|
||||
*/
|
||||
class StatelessDialog extends Component {
|
||||
|
||||
/**
|
||||
* Web dialog component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
...dialogPropTypes,
|
||||
|
||||
/**
|
||||
* This is the body of the dialog, the component children.
|
||||
*/
|
||||
children: React.PropTypes.node,
|
||||
|
||||
/**
|
||||
* Disables dismissing the dialog when the blanket is clicked. Enabled
|
||||
* by default.
|
||||
*/
|
||||
disableBlanketClickDismiss: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Whether the dialog is modal. This means clicking on the blanket will
|
||||
* leave the dialog open. No cancel button.
|
||||
*/
|
||||
isModal: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Disables rendering of the submit button.
|
||||
*/
|
||||
submitDisabled: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Width of the dialog, can be:
|
||||
* - 'small' (400px), 'medium' (600px), 'large' (800px),
|
||||
* 'x-large' (968px)
|
||||
* - integer value for pixel width
|
||||
* - string value for percentage
|
||||
*/
|
||||
width: React.PropTypes.string
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new Dialog instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onDialogDismissed = this._onDialogDismissed.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<ModalDialog
|
||||
footer = { this._renderFooter() }
|
||||
header = { this._renderHeader() }
|
||||
isOpen = { true }
|
||||
onDialogDismissed = { this._onDialogDismissed }
|
||||
width = { this.props.width || 'medium' }>
|
||||
<div>
|
||||
<form
|
||||
className = 'modal-dialog-form'
|
||||
id = 'modal-dialog-form'
|
||||
onSubmit = { this._onSubmit }>
|
||||
{ this.props.children }
|
||||
</form>
|
||||
</div>
|
||||
</ModalDialog>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click on the blanket area.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDialogDismissed() {
|
||||
if (!this.props.disableBlanketClickDismiss) {
|
||||
this._onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render cancel button.
|
||||
*
|
||||
* @returns {*} The cancel button if enabled and dialog is not modal.
|
||||
* @private
|
||||
*/
|
||||
_renderCancelButton() {
|
||||
if (this.props.cancelDisabled || this.props.isModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AKButton
|
||||
appearance = 'subtle'
|
||||
id = 'modal-dialog-cancel-button'
|
||||
onClick = { this._onCancel }>
|
||||
{ this.props.t(this.props.cancelTitleKey || 'dialog.Cancel') }
|
||||
</AKButton>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render component in dialog footer.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderFooter() {
|
||||
return (
|
||||
<footer className = 'modal-dialog-footer'>
|
||||
<AKButtonGroup>
|
||||
{ this._renderCancelButton() }
|
||||
{ this._renderOKButton() }
|
||||
</AKButtonGroup>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render component in dialog header.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderHeader() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<header>
|
||||
<h2>
|
||||
{ this.props.titleString || t(this.props.titleKey) }
|
||||
</h2>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render ok button.
|
||||
*
|
||||
* @returns {*} The ok button if enabled.
|
||||
* @private
|
||||
*/
|
||||
_renderOKButton() {
|
||||
if (this.props.submitDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AKButton
|
||||
appearance = 'primary'
|
||||
form = 'modal-dialog-form'
|
||||
id = 'modal-dialog-ok-button'
|
||||
isDisabled = { this.props.okDisabled }
|
||||
onClick = { this._onSubmit }>
|
||||
{ this.props.t(this.props.okTitleKey || 'dialog.Ok') }
|
||||
</AKButton>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches action to hide the dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
if (this.props.isModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the action when submitting the dialog.
|
||||
*
|
||||
* @private
|
||||
* @param {string} value - The submitted value if any.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit(value) {
|
||||
this.props.onSubmit(value);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(StatelessDialog);
|
|
@ -1,2 +1,3 @@
|
|||
export { default as DialogContainer } from './DialogContainer';
|
||||
export { default as Dialog } from './Dialog';
|
||||
export { default as StatelessDialog } from './StatelessDialog';
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
|
||||
export const dialogPropTypes = {
|
||||
/**
|
||||
* Whether cancel button is disabled. Enabled by default.
|
||||
*/
|
||||
cancelDisabled: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Optional i18n key to change the cancel button title.
|
||||
*/
|
||||
cancelTitleKey: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* Is ok button enabled/disabled. Enabled by default.
|
||||
*/
|
||||
okDisabled: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Optional i18n key to change the ok button title.
|
||||
*/
|
||||
okTitleKey: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* The handler for onCancel event.
|
||||
*/
|
||||
onCancel: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* The handler for the event when submitting the dialog.
|
||||
*/
|
||||
onSubmit: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Used to obtain translations in children classes.
|
||||
*/
|
||||
t: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Key to use for showing a title.
|
||||
*/
|
||||
titleKey: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* The string to use as a title instead of {@code titleKey}. If a truthy
|
||||
* value is specified, it takes precedence over {@code titleKey} i.e.
|
||||
* the latter is unused.
|
||||
*/
|
||||
titleString: React.PropTypes.string
|
||||
};
|
|
@ -0,0 +1,300 @@
|
|||
import Logger from 'jitsi-meet-logger';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
|
||||
import {
|
||||
PostMessageTransportBackend,
|
||||
Transport
|
||||
} from '../../../modules/transport';
|
||||
import { parseURLParams } from '../base/config';
|
||||
|
||||
import DeviceSelectionDialogBase from './components/DeviceSelectionDialogBase';
|
||||
|
||||
declare var JitsiMeetJS: Object;
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Implements a class that renders the React components for the device selection
|
||||
* popup page and handles the communication between the components and Jitsi
|
||||
* Meet.
|
||||
*/
|
||||
export default class DeviceSelectionPopup {
|
||||
/**
|
||||
* Initializes a new DeviceSelectionPopup instance.
|
||||
*
|
||||
* @param {Object} i18next - The i18next instance used for translation.
|
||||
*/
|
||||
constructor(i18next) {
|
||||
this.close = this.close.bind(this);
|
||||
this._setVideoInputDevice = this._setVideoInputDevice.bind(this);
|
||||
this._setAudioInputDevice = this._setAudioInputDevice.bind(this);
|
||||
this._setAudioOutputDevice = this._setAudioOutputDevice.bind(this);
|
||||
this._i18next = i18next;
|
||||
const { scope } = parseURLParams(window.location);
|
||||
|
||||
this._transport = new Transport({
|
||||
backend: new PostMessageTransportBackend({
|
||||
postisOptions: {
|
||||
scope,
|
||||
window: window.opener
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
this._transport.on('event', event => {
|
||||
if (event.name === 'deviceListChanged') {
|
||||
this._updateAvailableDevices();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this._dialogProps = {
|
||||
availableDevices: {},
|
||||
currentAudioInputId: '',
|
||||
currentAudioOutputId: '',
|
||||
currentVideoInputId: '',
|
||||
disableAudioInputChange: true,
|
||||
disableDeviceChange: true,
|
||||
hasAudioPermission: JitsiMeetJS.mediaDevices
|
||||
.isDevicePermissionGranted('audio'),
|
||||
hasVideoPermission: JitsiMeetJS.mediaDevices
|
||||
.isDevicePermissionGranted('video'),
|
||||
hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
|
||||
hideAudioOutputSelect: true
|
||||
};
|
||||
this._initState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends event to Jitsi Meet to close the popup dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
close() {
|
||||
this._transport.sendEvent({
|
||||
type: 'devices-dialog',
|
||||
name: 'close'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the properties of the react component and re-renders it.
|
||||
*
|
||||
* @param {Object} newProps - The new properties that will be assigned to
|
||||
* the current ones.
|
||||
* @returns {void}
|
||||
*/
|
||||
_changeDialogProps(newProps) {
|
||||
this._dialogProps = {
|
||||
...this._dialogProps,
|
||||
...newProps
|
||||
};
|
||||
this._render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Promise that resolves with result an list of available devices.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_getAvailableDevices() {
|
||||
return this._transport.sendRequest({
|
||||
type: 'devices',
|
||||
name: 'getAvailableDevices'
|
||||
}).catch(e => {
|
||||
logger.error(e);
|
||||
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Promise that resolves with current selected devices.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_getCurrentDevices() {
|
||||
return this._transport.sendRequest({
|
||||
type: 'devices',
|
||||
name: 'getCurrentDevices'
|
||||
}).catch(e => {
|
||||
logger.error(e);
|
||||
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the state.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_initState() {
|
||||
return Promise.all([
|
||||
this._getAvailableDevices(),
|
||||
this._isDeviceListAvailable(),
|
||||
this._isDeviceChangeAvailable(),
|
||||
this._getCurrentDevices(),
|
||||
this._isMultipleAudioInputSupported()
|
||||
]).then(([
|
||||
availableDevices,
|
||||
listAvailable,
|
||||
changeAvailable,
|
||||
currentDevices,
|
||||
multiAudioInputSupported
|
||||
]) => {
|
||||
this._changeDialogProps({
|
||||
availableDevices,
|
||||
currentAudioInputId: currentDevices.audioInput,
|
||||
currentAudioOutputId: currentDevices.audioOutput,
|
||||
currentVideoInputId: currentDevices.videoInput,
|
||||
disableAudioInputChange: !multiAudioInputSupported,
|
||||
disableDeviceChange: !listAvailable || !changeAvailable,
|
||||
hideAudioOutputSelect: !changeAvailable
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Promise that resolves with true if the device change is available
|
||||
* and with false if not.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_isDeviceChangeAvailable() {
|
||||
return this._transport.sendRequest({
|
||||
type: 'devices',
|
||||
name: 'isDeviceChangeAvailable'
|
||||
}).catch(e => {
|
||||
logger.error(e);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Promise that resolves with true if the device list is available
|
||||
* and with false if not.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_isDeviceListAvailable() {
|
||||
return this._transport.sendRequest({
|
||||
type: 'devices',
|
||||
name: 'isDeviceListAvailable'
|
||||
}).catch(e => {
|
||||
logger.error(e);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Promise that resolves with true if the device list is available
|
||||
* and with false if not.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_isMultipleAudioInputSupported() {
|
||||
return this._transport.sendRequest({
|
||||
type: 'devices',
|
||||
name: 'isMultipleAudioInputSupported'
|
||||
}).catch(e => {
|
||||
logger.error(e);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the React components for the popup page.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_render() {
|
||||
const props = {
|
||||
...this._dialogProps,
|
||||
closeModal: this.close,
|
||||
disableBlanketClickDismiss: true,
|
||||
setAudioInputDevice: this._setAudioInputDevice,
|
||||
setAudioOutputDevice: this._setAudioOutputDevice,
|
||||
setVideoInputDevice: this._setVideoInputDevice
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nextProvider
|
||||
i18n = { this._i18next }>
|
||||
<DeviceSelectionDialogBase { ...props } />
|
||||
</I18nextProvider>,
|
||||
document.getElementById('react'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio input device to the one with the id that is passed.
|
||||
*
|
||||
* @param {string} id - The id of the new device.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_setAudioInputDevice(id) {
|
||||
return this._setDevice({
|
||||
id,
|
||||
kind: 'audioinput'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio output device to the one with the id that is passed.
|
||||
*
|
||||
* @param {string} id - The id of the new device.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_setAudioOutputDevice(id) {
|
||||
return this._setDevice({
|
||||
id,
|
||||
kind: 'audiooutput'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currently used device to the one that is passed.
|
||||
*
|
||||
* @param {Object} device - The new device to be used.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_setDevice(device) {
|
||||
return this._transport.sendRequest({
|
||||
type: 'devices',
|
||||
name: 'setDevice',
|
||||
device
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the video input device to the one with the id that is passed.
|
||||
*
|
||||
* @param {string} id - The id of the new device.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_setVideoInputDevice(id) {
|
||||
return this._setDevice({
|
||||
id,
|
||||
kind: 'videoinput'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the available devices.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateAvailableDevices() {
|
||||
this._getAvailableDevices().then(devices =>
|
||||
this._changeDialogProps({ availableDevices: devices })
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* The type of Redux action which Sets information about device selection popup.
|
||||
*
|
||||
* {{
|
||||
* type: SET_DEVICE_SELECTION_POPUP_DATA,
|
||||
* popupDialogData: Object
|
||||
* }}
|
||||
*/
|
||||
export const SET_DEVICE_SELECTION_POPUP_DATA
|
||||
= Symbol('SET_DEVICE_SELECTION_POPUP_DATA');
|
|
@ -1,8 +1,20 @@
|
|||
/* globals APP */
|
||||
/* globals APP, interfaceConfig */
|
||||
|
||||
import { openDialog } from '../base/dialog';
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
import { API_ID } from '../../../modules/API/constants';
|
||||
import {
|
||||
setAudioInputDevice,
|
||||
setAudioOutputDevice,
|
||||
setVideoInputDevice
|
||||
} from '../base/devices';
|
||||
import { i18next } from '../base/i18n';
|
||||
import {
|
||||
PostMessageTransportBackend,
|
||||
Transport
|
||||
} from '../../../modules/transport';
|
||||
|
||||
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
|
||||
import { DeviceSelectionDialog } from './components';
|
||||
|
||||
/**
|
||||
|
@ -13,6 +25,21 @@ import { DeviceSelectionDialog } from './components';
|
|||
*/
|
||||
export function openDeviceSelectionDialog() {
|
||||
return dispatch => {
|
||||
if (interfaceConfig.filmStripOnly) {
|
||||
dispatch(_openDeviceSelectionDialogInPopup());
|
||||
} else {
|
||||
dispatch(_openDeviceSelectionDialogHere());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the DeviceSelectionDialog in the same window.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
function _openDeviceSelectionDialogHere() {
|
||||
return dispatch =>
|
||||
JitsiMeetJS.mediaDevices.isDeviceListAvailable()
|
||||
.then(isDeviceListAvailable => {
|
||||
dispatch(openDialog(DeviceSelectionDialog, {
|
||||
|
@ -33,5 +60,152 @@ export function openDeviceSelectionDialog() {
|
|||
.isDeviceChangeAvailable('output')
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a popup window with the device selection dialog in it.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
function _openDeviceSelectionDialogInPopup() {
|
||||
return (dispatch, getState) => {
|
||||
const { popupDialogData } = getState()['features/device-selection'];
|
||||
|
||||
if (popupDialogData) {
|
||||
popupDialogData.popup.focus();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// API_ID will always be defined because the iframe api is enabled
|
||||
const scope = `dialog_${API_ID}`;
|
||||
const url = `static/deviceSelectionPopup.html#scope=${
|
||||
encodeURIComponent(JSON.stringify(scope))}`;
|
||||
const popup
|
||||
= window.open(
|
||||
url,
|
||||
'device-selection-popup',
|
||||
'toolbar=no,scrollbars=no,resizable=no,width=720,height=458');
|
||||
|
||||
popup.addEventListener('DOMContentLoaded', () => {
|
||||
popup.init(i18next);
|
||||
});
|
||||
|
||||
const transport = new Transport({
|
||||
backend: new PostMessageTransportBackend({
|
||||
postisOptions: {
|
||||
scope,
|
||||
window: popup
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
transport.on('request',
|
||||
_processRequest.bind(undefined, dispatch, getState));
|
||||
transport.on('event', event => {
|
||||
if (event.type === 'devices-dialog' && event.name === 'close') {
|
||||
popup.close();
|
||||
transport.dispose();
|
||||
dispatch(_setDeviceSelectionPopupData());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
dispatch(_setDeviceSelectionPopupData({
|
||||
popup,
|
||||
transport
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes device requests from external applications.
|
||||
*
|
||||
* @param {Dispatch} dispatch - The redux {@code dispatch} function.
|
||||
* @param {Function} getState - The redux function that gets/retrieves the redux
|
||||
* state.
|
||||
* @param {Object} request - The request to be processed.
|
||||
* @param {Function} responseCallback - The callback that will send the
|
||||
* response.
|
||||
* @returns {boolean}
|
||||
*/ // eslint-disable-next-line max-params
|
||||
function _processRequest(dispatch, getState, request, responseCallback) {
|
||||
if (request.type === 'devices') {
|
||||
switch (request.name) {
|
||||
case 'isDeviceListAvailable':
|
||||
JitsiMeetJS.mediaDevices.isDeviceListAvailable()
|
||||
.then(isDeviceListAvailable =>
|
||||
responseCallback(isDeviceListAvailable))
|
||||
.catch(e => responseCallback(null, e));
|
||||
break;
|
||||
case 'isDeviceChangeAvailable':
|
||||
responseCallback(
|
||||
JitsiMeetJS.mediaDevices.isDeviceChangeAvailable());
|
||||
break;
|
||||
case 'isMultipleAudioInputSupported':
|
||||
responseCallback(JitsiMeetJS.isMultipleAudioInputSupported());
|
||||
break;
|
||||
case 'getCurrentDevices':
|
||||
responseCallback({
|
||||
audioInput: APP.settings.getMicDeviceId(),
|
||||
audioOutput: APP.settings.getAudioOutputDeviceId(),
|
||||
videoInput: APP.settings.getCameraDeviceId()
|
||||
});
|
||||
break;
|
||||
case 'getAvailableDevices':
|
||||
responseCallback(getState()['features/base/devices']);
|
||||
break;
|
||||
case 'setDevice': {
|
||||
let action;
|
||||
const { device } = request;
|
||||
|
||||
switch (device.kind) {
|
||||
case 'audioinput':
|
||||
action = setAudioInputDevice;
|
||||
break;
|
||||
case 'audiooutput':
|
||||
action = setAudioOutputDevice;
|
||||
break;
|
||||
case 'videoinput':
|
||||
action = setVideoInputDevice;
|
||||
break;
|
||||
default:
|
||||
|
||||
}
|
||||
dispatch(action(device.id));
|
||||
responseCallback(true);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets information about device selection popup in the store.
|
||||
*
|
||||
* @param {Object} popupDialogData - Information about the popup.
|
||||
* @param {Object} popupDialog.popup - The popup object returned from
|
||||
* window.open.
|
||||
* @param {Object} popupDialogData.transport - The transport instance used for
|
||||
* communication with the popup window.
|
||||
* @returns {{
|
||||
* type: SET_DEVICE_SELECTION_POPUP_DATA,
|
||||
* popupDialogData: Object
|
||||
* }}
|
||||
*/
|
||||
function _setDeviceSelectionPopupData(popupDialogData) {
|
||||
return {
|
||||
type: SET_DEVICE_SELECTION_POPUP_DATA,
|
||||
popupDialogData
|
||||
};
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ class AudioInputPreview extends PureComponent {
|
|||
*/
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this._listenForAudioUpdates(nextProps.track);
|
||||
this._updateAudioLevel(0);
|
||||
this._updateAudioLevel(undefined, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,17 +6,9 @@ import {
|
|||
setAudioOutputDevice,
|
||||
setVideoInputDevice
|
||||
} from '../../base/devices';
|
||||
import {
|
||||
Dialog,
|
||||
hideDialog
|
||||
} from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { createLocalTrack } from '../../base/lib-jitsi-meet';
|
||||
import { hideDialog } from '../../base/dialog';
|
||||
|
||||
import AudioInputPreview from './AudioInputPreview';
|
||||
import AudioOutputPreview from './AudioOutputPreview';
|
||||
import DeviceSelector from './DeviceSelector';
|
||||
import VideoInputPreview from './VideoInputPreview';
|
||||
import DeviceSelectionDialogBase from './DeviceSelectionDialogBase';
|
||||
|
||||
/**
|
||||
* React component for previewing and selecting new audio and video sources.
|
||||
|
@ -96,417 +88,58 @@ class DeviceSelectionDialog extends Component {
|
|||
* 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
|
||||
hideAudioOutputSelect: React.PropTypes.bool
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
const { _availableDevices } = this.props;
|
||||
|
||||
this.state = {
|
||||
// JitsiLocalTrack to use for live previewing of audio input.
|
||||
previewAudioTrack: null,
|
||||
|
||||
// JitsiLocalTrack to use for live previewing of video input.
|
||||
previewVideoTrack: null,
|
||||
|
||||
// An message describing a problem with obtaining a video preview.
|
||||
previewVideoTrackError: null,
|
||||
|
||||
// The audio input device id to show as selected by default.
|
||||
selectedAudioInputId: this.props.currentAudioInputId || '',
|
||||
|
||||
// The audio output device id to show as selected by default.
|
||||
selectedAudioOutputId: this.props.currentAudioOutputId || '',
|
||||
|
||||
// The video input device id to show as selected by default.
|
||||
// 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
|
||||
// is re-used from the previous device settings implementation.
|
||||
selectedVideoInputId: this.props.currentVideoInputId
|
||||
|| (_availableDevices.videoInput
|
||||
&& _availableDevices.videoInput[0]
|
||||
&& _availableDevices.videoInput[0].deviceId)
|
||||
|| ''
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._closeModal = this._closeModal.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._updateAudioOutput = this._updateAudioOutput.bind(this);
|
||||
this._updateAudioInput = this._updateAudioInput.bind(this);
|
||||
this._updateVideoInput = this._updateVideoInput.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default device choices so a choice is pre-selected in the dropdowns
|
||||
* and live previews are created.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updateAudioOutput(this.state.selectedAudioOutputId);
|
||||
this._updateAudioInput(this.state.selectedAudioInputId);
|
||||
this._updateVideoInput(this.state.selectedVideoInputId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes preview tracks that might not already be disposed.
|
||||
*
|
||||
* @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 column-video'>
|
||||
<div className = 'device-selection-video-container'>
|
||||
<VideoInputPreview
|
||||
error = { this.state.previewVideoTrackError }
|
||||
track = { this.state.previewVideoTrack } />
|
||||
</div>
|
||||
{ this._renderAudioInputPreview() }
|
||||
</div>
|
||||
<div className = 'device-selection-column column-selectors'>
|
||||
<div className = 'device-selectors'>
|
||||
{ this._renderSelectors() }
|
||||
</div>
|
||||
{ this._renderAudioOutputPreview() }
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
const {
|
||||
currentAudioInputId,
|
||||
currentAudioOutputId,
|
||||
currentVideoInputId,
|
||||
disableAudioInputChange,
|
||||
disableDeviceChange,
|
||||
dispatch,
|
||||
hasAudioPermission,
|
||||
hasVideoPermission,
|
||||
hideAudioInputPreview,
|
||||
hideAudioOutputSelect
|
||||
} = this.props;
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
return Promise.all([
|
||||
this._disposeVideoPreview(),
|
||||
this._disposeAudioPreview()
|
||||
]);
|
||||
}
|
||||
const props = {
|
||||
availableDevices: this.props._availableDevices,
|
||||
closeModal: () => dispatch(hideDialog()),
|
||||
currentAudioInputId,
|
||||
currentAudioOutputId,
|
||||
currentVideoInputId,
|
||||
disableAudioInputChange,
|
||||
disableDeviceChange,
|
||||
hasAudioPermission,
|
||||
hasVideoPermission,
|
||||
hideAudioInputPreview,
|
||||
hideAudioOutputSelect,
|
||||
setAudioInputDevice: id => {
|
||||
dispatch(setAudioInputDevice(id));
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes preview tracks and signals to close DeviceSelectionDialog.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies 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 = this._attemptPreviewTrackCleanup()
|
||||
.then(() => {
|
||||
if (this.state.selectedVideoInputId
|
||||
!== this.props.currentVideoInputId) {
|
||||
this.props.dispatch(
|
||||
setVideoInputDevice(this.state.selectedVideoInputId));
|
||||
}
|
||||
|
||||
if (this.state.selectedAudioInputId
|
||||
!== this.props.currentAudioInputId) {
|
||||
this.props.dispatch(
|
||||
setAudioInputDevice(this.state.selectedAudioInputId));
|
||||
}
|
||||
|
||||
if (this.state.selectedAudioOutputId
|
||||
!== this.props.currentAudioOutputId) {
|
||||
this.props.dispatch(
|
||||
setAudioOutputDevice(this.state.selectedAudioOutputId));
|
||||
}
|
||||
});
|
||||
|
||||
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 } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.selectedAudioOutputId } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
const configurations = [
|
||||
{
|
||||
devices: _availableDevices.videoInput,
|
||||
hasPermission: this.props.hasVideoPermission,
|
||||
icon: 'icon-camera',
|
||||
isDisabled: this.props.disableDeviceChange,
|
||||
key: 'videoInput',
|
||||
label: 'settings.selectCamera',
|
||||
onSelect: this._updateVideoInput,
|
||||
selectedDeviceId: this.state.selectedVideoInputId
|
||||
return Promise.resolve();
|
||||
},
|
||||
{
|
||||
devices: _availableDevices.audioInput,
|
||||
hasPermission: this.props.hasAudioPermission,
|
||||
icon: 'icon-microphone',
|
||||
isDisabled: this.props.disableAudioInputChange
|
||||
|| this.props.disableDeviceChange,
|
||||
key: 'audioInput',
|
||||
label: 'settings.selectMic',
|
||||
onSelect: this._updateAudioInput,
|
||||
selectedDeviceId: this.state.selectedAudioInputId
|
||||
setAudioOutputDevice: id => {
|
||||
dispatch(setAudioOutputDevice(id));
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
setVideoInputDevice: id => {
|
||||
dispatch(setVideoInputDevice(id));
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
if (!this.props.hideAudioOutputSelect) {
|
||||
configurations.push({
|
||||
devices: _availableDevices.audioOutput,
|
||||
hasPermission: this.props.hasAudioPermission
|
||||
|| this.props.hasVideoPermission,
|
||||
icon: 'icon-volume',
|
||||
isDisabled: this.props.disableDeviceChange,
|
||||
key: 'audioOutput',
|
||||
label: 'settings.selectAudioOutput',
|
||||
onSelect: this._updateAudioOutput,
|
||||
selectedDeviceId: this.state.selectedAudioOutputId
|
||||
});
|
||||
}
|
||||
|
||||
return configurations.map(this._renderSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {string} deviceId - The id of the chosen audio input device.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateAudioInput(deviceId) {
|
||||
this.setState({
|
||||
selectedAudioInputId: deviceId
|
||||
}, () => {
|
||||
this._disposeAudioPreview()
|
||||
.then(() => createLocalTrack('audio', deviceId))
|
||||
.then(jitsiLocalTrack => {
|
||||
this.setState({
|
||||
previewAudioTrack: jitsiLocalTrack
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewAudioTrack: null
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
_updateAudioOutput(deviceId) {
|
||||
this.setState({
|
||||
selectedAudioOutputId: deviceId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {string} deviceId - The id of the chosen video input device.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateVideoInput(deviceId) {
|
||||
this.setState({
|
||||
selectedVideoInputId: deviceId
|
||||
}, () => {
|
||||
this._disposeVideoPreview()
|
||||
.then(() => createLocalTrack('video', deviceId))
|
||||
.then(jitsiLocalTrack => {
|
||||
this.setState({
|
||||
previewVideoTrack: jitsiLocalTrack,
|
||||
previewVideoTrackError: null
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewVideoTrack: null,
|
||||
previewVideoTrackError:
|
||||
this.props.t('deviceSelection.previewUnavailable')
|
||||
});
|
||||
});
|
||||
});
|
||||
return <DeviceSelectionDialogBase { ...props } />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -526,4 +159,4 @@ function _mapStateToProps(state) {
|
|||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(DeviceSelectionDialog));
|
||||
export default connect(_mapStateToProps)(DeviceSelectionDialog);
|
||||
|
|
|
@ -0,0 +1,529 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { StatelessDialog } 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 DeviceSelectionDialogBase extends Component {
|
||||
/**
|
||||
* DeviceSelectionDialogBase component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* All known audio and video devices split by type. This prop comes from
|
||||
* the app state.
|
||||
*/
|
||||
availableDevices: React.PropTypes.object,
|
||||
|
||||
/**
|
||||
* Closes the dialog.
|
||||
*/
|
||||
closeModal: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Device id for the current audio input device. This device will be set
|
||||
* as the default audio input device to preview.
|
||||
*/
|
||||
currentAudioInputId: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* Device id for the current audio output device. This device will be
|
||||
* set as the default audio output device to preview.
|
||||
*/
|
||||
currentAudioOutputId: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* Device id for the current video input device. This device will be set
|
||||
* as the default video input device to preview.
|
||||
*/
|
||||
currentVideoInputId: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
/**
|
||||
* Disables dismissing the dialog when the blanket is clicked. Enabled
|
||||
* by default.
|
||||
*/
|
||||
disableBlanketClickDismiss: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* True if device changing is configured to be disallowed. Selectors
|
||||
* will display as disabled.
|
||||
*/
|
||||
disableDeviceChange: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Whether or not a new audio input source can be selected.
|
||||
*/
|
||||
hasAudioPermission: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Whether or not a 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,
|
||||
|
||||
/**
|
||||
* Function that sets the audio input device.
|
||||
*/
|
||||
setAudioInputDevice: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Function that sets the audio output device.
|
||||
*/
|
||||
setAudioOutputDevice: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Function that sets the video input device.
|
||||
*/
|
||||
setVideoInputDevice: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: React.PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new DeviceSelectionDialogBase instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { availableDevices } = this.props;
|
||||
|
||||
this.state = {
|
||||
// JitsiLocalTrack to use for live previewing of audio input.
|
||||
previewAudioTrack: null,
|
||||
|
||||
// JitsiLocalTrack to use for live previewing of video input.
|
||||
previewVideoTrack: null,
|
||||
|
||||
// An message describing a problem with obtaining a video preview.
|
||||
previewVideoTrackError: null,
|
||||
|
||||
// The audio input device id to show as selected by default.
|
||||
selectedAudioInputId: this.props.currentAudioInputId || '',
|
||||
|
||||
// The audio output device id to show as selected by default.
|
||||
selectedAudioOutputId: this.props.currentAudioOutputId || '',
|
||||
|
||||
// The video input device id to show as selected by default.
|
||||
// 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
|
||||
// is re-used from the previous device settings implementation.
|
||||
selectedVideoInputId: this.props.currentVideoInputId
|
||||
|| (availableDevices.videoInput
|
||||
&& availableDevices.videoInput[0]
|
||||
&& availableDevices.videoInput[0].deviceId)
|
||||
|| ''
|
||||
};
|
||||
|
||||
// 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._setDevicesAndClose = this._setDevicesAndClose.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._updateAudioOutput = this._updateAudioOutput.bind(this);
|
||||
this._updateAudioInput = this._updateAudioInput.bind(this);
|
||||
this._updateVideoInput = this._updateVideoInput.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default device choices so a choice is pre-selected in the dropdowns
|
||||
* and live previews are created.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updateAudioOutput(this.state.selectedAudioOutputId);
|
||||
this._updateAudioInput(this.state.selectedAudioInputId);
|
||||
this._updateVideoInput(this.state.selectedVideoInputId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes preview tracks that might not already be disposed.
|
||||
*
|
||||
* @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 (
|
||||
<StatelessDialog
|
||||
cancelTitleKey = { 'dialog.Cancel' }
|
||||
disableBlanketClickDismiss
|
||||
= { this.props.disableBlanketClickDismiss }
|
||||
okTitleKey = { 'dialog.Save' }
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'deviceSelection.deviceSettings'>
|
||||
<div className = 'device-selection'>
|
||||
<div className = 'device-selection-column column-video'>
|
||||
<div className = 'device-selection-video-container'>
|
||||
<VideoInputPreview
|
||||
error = { this.state.previewVideoTrackError }
|
||||
track = { this.state.previewVideoTrack } />
|
||||
</div>
|
||||
{ this._renderAudioInputPreview() }
|
||||
</div>
|
||||
<div className = 'device-selection-column column-selectors'>
|
||||
<div className = 'device-selectors'>
|
||||
{ this._renderSelectors() }
|
||||
</div>
|
||||
{ this._renderAudioOutputPreview() }
|
||||
</div>
|
||||
</div>
|
||||
</StatelessDialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
return Promise.all([
|
||||
this._disposeVideoPreview(),
|
||||
this._disposeAudioPreview()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes preview tracks and signals to
|
||||
* close DeviceSelectionDialogBase.
|
||||
*
|
||||
* @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.props.closeModal)
|
||||
.catch(this.props.closeModal);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies 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;
|
||||
|
||||
this._attemptPreviewTrackCleanup()
|
||||
.then(this._setDevicesAndClose, this._setDevicesAndClose);
|
||||
|
||||
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 } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.selectedAudioOutputId } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
const configurations = [
|
||||
{
|
||||
devices: availableDevices.videoInput,
|
||||
hasPermission: this.props.hasVideoPermission,
|
||||
icon: 'icon-camera',
|
||||
isDisabled: this.props.disableDeviceChange,
|
||||
key: 'videoInput',
|
||||
label: 'settings.selectCamera',
|
||||
onSelect: this._updateVideoInput,
|
||||
selectedDeviceId: this.state.selectedVideoInputId
|
||||
},
|
||||
{
|
||||
devices: availableDevices.audioInput,
|
||||
hasPermission: this.props.hasAudioPermission,
|
||||
icon: 'icon-microphone',
|
||||
isDisabled: this.props.disableAudioInputChange
|
||||
|| this.props.disableDeviceChange,
|
||||
key: 'audioInput',
|
||||
label: 'settings.selectMic',
|
||||
onSelect: this._updateAudioInput,
|
||||
selectedDeviceId: this.state.selectedAudioInputId
|
||||
}
|
||||
];
|
||||
|
||||
if (!this.props.hideAudioOutputSelect) {
|
||||
configurations.push({
|
||||
devices: availableDevices.audioOutput,
|
||||
hasPermission: this.props.hasAudioPermission
|
||||
|| this.props.hasVideoPermission,
|
||||
icon: 'icon-volume',
|
||||
isDisabled: this.props.disableDeviceChange,
|
||||
key: 'audioOutput',
|
||||
label: 'settings.selectAudioOutput',
|
||||
onSelect: this._updateAudioOutput,
|
||||
selectedDeviceId: this.state.selectedAudioOutputId
|
||||
});
|
||||
}
|
||||
|
||||
return configurations.map(this._renderSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected devices and closes the dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_setDevicesAndClose() {
|
||||
const {
|
||||
setVideoInputDevice,
|
||||
setAudioInputDevice,
|
||||
setAudioOutputDevice,
|
||||
closeModal
|
||||
} = this.props;
|
||||
|
||||
const promises = [];
|
||||
|
||||
if (this.state.selectedVideoInputId
|
||||
!== this.props.currentVideoInputId) {
|
||||
promises.push(setVideoInputDevice(this.state.selectedVideoInputId));
|
||||
}
|
||||
|
||||
if (this.state.selectedAudioInputId
|
||||
!== this.props.currentAudioInputId) {
|
||||
promises.push(setAudioInputDevice(this.state.selectedAudioInputId));
|
||||
}
|
||||
|
||||
if (this.state.selectedAudioOutputId
|
||||
!== this.props.currentAudioOutputId) {
|
||||
promises.push(
|
||||
setAudioOutputDevice(this.state.selectedAudioOutputId));
|
||||
}
|
||||
Promise.all(promises).then(closeModal, closeModal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {string} deviceId - The id of the chosen audio input device.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateAudioInput(deviceId) {
|
||||
this.setState({
|
||||
selectedAudioInputId: deviceId
|
||||
}, () => {
|
||||
this._disposeAudioPreview()
|
||||
.then(() => createLocalTrack('audio', deviceId))
|
||||
.then(jitsiLocalTrack => {
|
||||
this.setState({
|
||||
previewAudioTrack: jitsiLocalTrack
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewAudioTrack: null
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
_updateAudioOutput(deviceId) {
|
||||
this.setState({
|
||||
selectedAudioOutputId: deviceId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {string} deviceId - The id of the chosen video input device.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateVideoInput(deviceId) {
|
||||
this.setState({
|
||||
selectedVideoInputId: deviceId
|
||||
}, () => {
|
||||
this._disposeVideoPreview()
|
||||
.then(() => createLocalTrack('video', deviceId))
|
||||
.then(jitsiLocalTrack => {
|
||||
this.setState({
|
||||
previewVideoTrack: jitsiLocalTrack,
|
||||
previewVideoTrackError: null
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewVideoTrack: null,
|
||||
previewVideoTrackError:
|
||||
this.props.t('deviceSelection.previewUnavailable')
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(DeviceSelectionDialogBase);
|
|
@ -83,7 +83,7 @@ class DeviceSelector extends Component {
|
|||
return this._renderNoPermission();
|
||||
}
|
||||
|
||||
if (!this.props.devices.length) {
|
||||
if (!this.props.devices || !this.props.devices.length) {
|
||||
return this._renderNoDevices();
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export { default as DeviceSelectionDialog } from './DeviceSelectionDialog';
|
||||
export { default as DeviceSelectionDialogBase }
|
||||
from './DeviceSelectionDialogBase';
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { UPDATE_DEVICE_LIST } from '../base/devices';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature device-selection.
|
||||
*
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
if (action.type === UPDATE_DEVICE_LIST) {
|
||||
const { popupDialogData }
|
||||
= store.getState()['features/device-selection'];
|
||||
|
||||
if (popupDialogData) {
|
||||
popupDialogData.transport.sendEvent({ name: 'deviceListChanged' });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
import 'aui-css';
|
||||
import 'aui-experimental-css';
|
||||
|
||||
import DeviceSelectionPopup from './DeviceSelectionPopup';
|
||||
|
||||
let deviceSelectionPopup;
|
||||
|
||||
window.init = function(i18next) {
|
||||
deviceSelectionPopup = new DeviceSelectionPopup(i18next);
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', () =>
|
||||
deviceSelectionPopup.close());
|
|
@ -0,0 +1,28 @@
|
|||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Listen for actions which changes the state of the popup window for the device
|
||||
* selection.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature
|
||||
* features/device-selection.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @param {Object} action.popupDialogData - Object that stores the current
|
||||
* Window object of the popup and the Transport instance. If no popup is shown
|
||||
* the value will be undefined.
|
||||
* @returns {Object}
|
||||
*/
|
||||
ReducerRegistry.register('features/device-selection',
|
||||
(state = {}, action) => {
|
||||
if (action.type === SET_DEVICE_SELECTION_POPUP_DATA) {
|
||||
return {
|
||||
...state,
|
||||
popupDialogData: action.popupDialogData
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { TouchableHighlight } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Icon } from '../../base/font-icons';
|
||||
|
||||
|
@ -10,13 +11,20 @@ import AbstractToolbarButton from './AbstractToolbarButton';
|
|||
*
|
||||
* @extends AbstractToolbarButton
|
||||
*/
|
||||
export default class ToolbarButton extends AbstractToolbarButton {
|
||||
class ToolbarButton extends AbstractToolbarButton {
|
||||
/**
|
||||
* ToolbarButton component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = AbstractToolbarButton.propTypes
|
||||
static propTypes = {
|
||||
...AbstractToolbarButton.propTypes,
|
||||
|
||||
/**
|
||||
* Used to dispatch an action when the button is clicked.
|
||||
*/
|
||||
dispatch: React.PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the button of this Toolbar button.
|
||||
|
@ -29,7 +37,13 @@ export default class ToolbarButton extends AbstractToolbarButton {
|
|||
_renderButton(children) {
|
||||
const props = {};
|
||||
|
||||
'onClick' in this.props && (props.onPress = this.props.onClick);
|
||||
'onClick' in this.props && (props.onPress = () => {
|
||||
const action = this.props.onClick(event);
|
||||
|
||||
if (action) {
|
||||
this.props.dispatch(action);
|
||||
}
|
||||
});
|
||||
'style' in this.props && (props.style = this.props.style);
|
||||
'underlayColor' in this.props
|
||||
&& (props.underlayColor = this.props.underlayColor);
|
||||
|
@ -45,3 +59,5 @@ export default class ToolbarButton extends AbstractToolbarButton {
|
|||
return super._renderIcon(Icon);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(ToolbarButton);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
|
@ -36,6 +37,11 @@ class ToolbarButton extends AbstractToolbarButton {
|
|||
*/
|
||||
button: React.PropTypes.object.isRequired,
|
||||
|
||||
/**
|
||||
* Used to dispatch an action when the button is clicked.
|
||||
*/
|
||||
dispatch: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Handler for component mount.
|
||||
*/
|
||||
|
@ -151,7 +157,11 @@ class ToolbarButton extends AbstractToolbarButton {
|
|||
} = button;
|
||||
|
||||
if (enabled && !unclickable && onClick) {
|
||||
onClick(event);
|
||||
const action = onClick(event);
|
||||
|
||||
if (action) {
|
||||
this.props.dispatch(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,4 +238,4 @@ class ToolbarButton extends AbstractToolbarButton {
|
|||
}
|
||||
}
|
||||
|
||||
export default translate(ToolbarButton);
|
||||
export default translate(connect()(ToolbarButton));
|
||||
|
|
|
@ -2,25 +2,26 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { openDeviceSelectionDialog } from '../device-selection';
|
||||
import { openDialOutDialog } from '../dial-out';
|
||||
import { openInviteDialog } from '../invite';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
import { openInviteDialog } from '../invite';
|
||||
import { openDialOutDialog } from '../dial-out';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
declare var JitsiMeetJS: Object;
|
||||
|
||||
/**
|
||||
* All toolbar buttons' descriptors.
|
||||
*/
|
||||
export default {
|
||||
const buttons: Object = {
|
||||
/**
|
||||
* The descriptor of the camera toolbar button.
|
||||
*/
|
||||
camera: {
|
||||
classNames: [ 'button', 'icon-camera' ],
|
||||
enabled: true,
|
||||
filmstripOnlyEnabled: true,
|
||||
isDisplayed: () => true,
|
||||
id: 'toolbar_button_camera',
|
||||
onClick() {
|
||||
if (APP.conference.videoMuted) {
|
||||
|
@ -153,11 +154,32 @@ export default {
|
|||
id: 'toolbar_button_dial_out',
|
||||
onClick() {
|
||||
JitsiMeetJS.analytics.sendEvent('toolbar.sip.clicked');
|
||||
APP.store.dispatch(openDialOutDialog());
|
||||
|
||||
return openDialOutDialog();
|
||||
},
|
||||
tooltipKey: 'dialOut.dialOut'
|
||||
},
|
||||
|
||||
/**
|
||||
* The descriptor of the device selection toolbar button.
|
||||
*/
|
||||
fodeviceselection: {
|
||||
classNames: [ 'button', 'icon-settings' ],
|
||||
enabled: true,
|
||||
isDisplayed() {
|
||||
return interfaceConfig.filmStripOnly;
|
||||
},
|
||||
id: 'toolbar_button_fodeviceselection',
|
||||
onClick() {
|
||||
JitsiMeetJS.analytics.sendEvent(
|
||||
'toolbar.fodeviceselection.toggled');
|
||||
|
||||
return openDeviceSelectionDialog();
|
||||
},
|
||||
sideContainerId: 'settings_container',
|
||||
tooltipKey: 'toolbar.Settings'
|
||||
},
|
||||
|
||||
/**
|
||||
* The descriptor of the dialpad toolbar button.
|
||||
*/
|
||||
|
@ -217,7 +239,7 @@ export default {
|
|||
hangup: {
|
||||
classNames: [ 'button', 'icon-hangup', 'button_hangup' ],
|
||||
enabled: true,
|
||||
filmstripOnlyEnabled: true,
|
||||
isDisplayed: () => true,
|
||||
id: 'toolbar_button_hangup',
|
||||
onClick() {
|
||||
JitsiMeetJS.analytics.sendEvent('toolbar.hangup');
|
||||
|
@ -235,7 +257,8 @@ export default {
|
|||
id: 'toolbar_button_link',
|
||||
onClick() {
|
||||
JitsiMeetJS.analytics.sendEvent('toolbar.invite.clicked');
|
||||
APP.store.dispatch(openInviteDialog());
|
||||
|
||||
return openInviteDialog();
|
||||
},
|
||||
tooltipKey: 'toolbar.invite'
|
||||
},
|
||||
|
@ -246,7 +269,7 @@ export default {
|
|||
microphone: {
|
||||
classNames: [ 'button', 'icon-microphone' ],
|
||||
enabled: true,
|
||||
filmstripOnlyEnabled: true,
|
||||
isDisplayed: () => true,
|
||||
id: 'toolbar_button_mute',
|
||||
onClick() {
|
||||
const sharedVideoManager = APP.UI.getSharedVideoManager();
|
||||
|
@ -386,3 +409,14 @@ export default {
|
|||
tooltipKey: 'toolbar.sharedvideo'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Object.keys(buttons).forEach(name => {
|
||||
const button = buttons[name];
|
||||
|
||||
if (!button.isDisplayed) {
|
||||
button.isDisplayed = () => !interfaceConfig.filmStripOnly;
|
||||
}
|
||||
});
|
||||
|
||||
export default buttons;
|
||||
|
|
|
@ -64,7 +64,6 @@ export function getDefaultToolboxButtons(buttonHandlers: Object): Object {
|
|||
|
||||
if (typeof interfaceConfig !== 'undefined'
|
||||
&& interfaceConfig.TOOLBAR_BUTTONS) {
|
||||
const { filmStripOnly } = interfaceConfig;
|
||||
|
||||
toolbarButtons
|
||||
= interfaceConfig.TOOLBAR_BUTTONS.reduce(
|
||||
|
@ -84,9 +83,9 @@ export function getDefaultToolboxButtons(buttonHandlers: Object): Object {
|
|||
};
|
||||
}
|
||||
|
||||
// In filmstrip-only mode we only add a button if it's
|
||||
// filmstrip-only enabled.
|
||||
if (!filmStripOnly || button.filmstripOnlyEnabled) {
|
||||
// If isDisplayed method is not defined, display the
|
||||
// button only for non-filmstripOnly mode
|
||||
if (button.isDisplayed()) {
|
||||
acc[place].set(buttonName, button);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
SET_TOOLBOX_TIMEOUT_MS,
|
||||
SET_TOOLBOX_VISIBLE
|
||||
} from './actionTypes';
|
||||
import defaultToolbarButtons from './defaultToolbarButtons';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
|
@ -208,6 +209,15 @@ ReducerRegistry.register(
|
|||
* @returns {Object}
|
||||
*/
|
||||
function _setButton(state, { button, buttonName }): Object {
|
||||
const buttonDefinition = defaultToolbarButtons[buttonName];
|
||||
|
||||
// We don't need to update if the button shouldn't be displayed
|
||||
if (!buttonDefinition || !buttonDefinition.isDisplayed()) {
|
||||
return {
|
||||
...state
|
||||
};
|
||||
}
|
||||
|
||||
const { primaryToolbarButtons, secondaryToolbarButtons } = state;
|
||||
let selectedButton = primaryToolbarButtons.get(buttonName);
|
||||
let place = 'primaryToolbarButtons';
|
||||
|
@ -222,18 +232,6 @@ function _setButton(state, { button, buttonName }): Object {
|
|||
...button
|
||||
};
|
||||
|
||||
// In filmstrip-only mode we only show buttons if they're filmstrip-only
|
||||
// enabled, so we don't need to update if this isn't the case.
|
||||
// FIXME A reducer should be a pure function of the current state and the
|
||||
// specified action so it should not use the global variable
|
||||
// interfaceConfig. Anyway, we'll move interfaceConfig into the (redux)
|
||||
// store so we'll surely revisit the source code bellow.
|
||||
if (interfaceConfig.filmStripOnly && !selectedButton.filmstripOnlyEnabled) {
|
||||
return {
|
||||
...state
|
||||
};
|
||||
}
|
||||
|
||||
const updatedToolbar = state[place].set(buttonName, selectedButton);
|
||||
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<html itemscope itemtype="http://schema.org/Product" prefix="og: http://ogp.me/ns#" xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<base href="../" />
|
||||
<!--#include virtual="/title.html" -->
|
||||
<script><!--#include virtual="/interface_config.js" --></script>
|
||||
<script>
|
||||
window.config = {};
|
||||
window.JitsiMeetJS = window.opener.window.JitsiMeetJS;
|
||||
</script>
|
||||
<script src="libs/device_selection_popup_bundle.min.js"></script>
|
||||
<link rel="stylesheet" href="css/all.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="react"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -193,6 +193,16 @@ const configs = [
|
|||
output: Object.assign({}, config.output, {
|
||||
library: 'JitsiMeetExternalAPI'
|
||||
})
|
||||
}),
|
||||
|
||||
// The Webpack configuration to bundle popup_bundle.js (js file for the
|
||||
// device selection popup dialog).
|
||||
Object.assign({}, config, {
|
||||
entry: {
|
||||
'device_selection_popup_bundle':
|
||||
'./react/features/device-selection/popup.js'
|
||||
},
|
||||
output: config.output
|
||||
})
|
||||
];
|
||||
|
||||
|
|
Loading…
Reference in New Issue