mirror of https://github.com/jitsi/jitsi-meet
parent
2c002c875d
commit
96e83989a5
@ -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'); |
@ -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); |
@ -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; |
||||
}); |
@ -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> |
Loading…
Reference in new issue