530 lines
17 KiB
JavaScript
530 lines
17 KiB
JavaScript
import React, { Component } from 'react';
|
|
import { connect } from 'react-redux';
|
|
|
|
import {
|
|
setAudioInputDevice,
|
|
setAudioOutputDevice,
|
|
setVideoInputDevice
|
|
} from '../../base/devices';
|
|
import {
|
|
Dialog,
|
|
hideDialog
|
|
} from '../../base/dialog';
|
|
import { translate } from '../../base/i18n';
|
|
import { createLocalTrack } from '../../base/lib-jitsi-meet';
|
|
|
|
import AudioInputPreview from './AudioInputPreview';
|
|
import AudioOutputPreview from './AudioOutputPreview';
|
|
import DeviceSelector from './DeviceSelector';
|
|
import VideoInputPreview from './VideoInputPreview';
|
|
|
|
/**
|
|
* React component for previewing and selecting new audio and video sources.
|
|
*
|
|
* @extends Component
|
|
*/
|
|
class DeviceSelectionDialog extends Component {
|
|
/**
|
|
* DeviceSelectionDialog component's property types.
|
|
*
|
|
* @static
|
|
*/
|
|
static propTypes = {
|
|
/**
|
|
* All known audio and video devices split by type. This prop comes from
|
|
* the app state.
|
|
*/
|
|
_availableDevices: React.PropTypes.object,
|
|
|
|
/**
|
|
* 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,
|
|
|
|
/**
|
|
* True if device changing is configured to be disallowed. Selectors
|
|
* will display as disabled.
|
|
*/
|
|
disableDeviceChange: React.PropTypes.bool,
|
|
|
|
/**
|
|
* Invoked to notify the store of app state changes.
|
|
*/
|
|
dispatch: React.PropTypes.func,
|
|
|
|
/**
|
|
* Whether or not 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,
|
|
|
|
/**
|
|
* Invoked to obtain translated strings.
|
|
*/
|
|
t: React.PropTypes.func
|
|
};
|
|
|
|
/**
|
|
* Initializes a new DeviceSelectionDialog instance.
|
|
*
|
|
* @param {Object} props - The read-only React Component props with which
|
|
* the new instance is to be initialized.
|
|
*/
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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()
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
},
|
|
{
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* 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')
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps (parts of) the Redux state to the associated DeviceSelectionDialog's
|
|
* props.
|
|
*
|
|
* @param {Object} state - The Redux state.
|
|
* @private
|
|
* @returns {{
|
|
* _availableDevices: Object
|
|
* }}
|
|
*/
|
|
function _mapStateToProps(state) {
|
|
return {
|
|
_availableDevices: state['features/base/devices']
|
|
};
|
|
}
|
|
|
|
export default translate(connect(_mapStateToProps)(DeviceSelectionDialog));
|