2018-06-20 20:19:53 +00:00
|
|
|
// @flow
|
|
|
|
|
|
|
|
import React from 'react';
|
|
|
|
|
2019-09-13 13:03:40 +00:00
|
|
|
import AbstractDialogTab, {
|
|
|
|
type Props as AbstractDialogTabProps
|
|
|
|
} from '../../base/dialog/components/web/AbstractDialogTab';
|
|
|
|
import { translate } from '../../base/i18n/functions';
|
|
|
|
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
|
2019-08-21 14:50:00 +00:00
|
|
|
import logger from '../logger';
|
|
|
|
|
2018-06-20 20:19:53 +00:00
|
|
|
import AudioInputPreview from './AudioInputPreview';
|
|
|
|
import AudioOutputPreview from './AudioOutputPreview';
|
|
|
|
import DeviceSelector from './DeviceSelector';
|
|
|
|
import VideoInputPreview from './VideoInputPreview';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The type of the React {@code Component} props of {@link DeviceSelection}.
|
|
|
|
*/
|
|
|
|
export type Props = {
|
|
|
|
...$Exact<AbstractDialogTabProps>,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* All known audio and video devices split by type. This prop comes from
|
|
|
|
* the app state.
|
|
|
|
*/
|
|
|
|
availableDevices: Object,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether or not the audio selector can be interacted with. If true,
|
|
|
|
* the audio input selector will be rendered as disabled. This is
|
|
|
|
* specifically used to prevent audio device changing in Firefox, which
|
|
|
|
* currently does not work due to a browser-side regression.
|
|
|
|
*/
|
|
|
|
disableAudioInputChange: boolean,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* True if device changing is configured to be disallowed. Selectors
|
|
|
|
* will display as disabled.
|
|
|
|
*/
|
|
|
|
disableDeviceChange: boolean,
|
|
|
|
|
2021-09-09 17:23:36 +00:00
|
|
|
/**
|
|
|
|
* Whether video input dropdown should be enabled or not.
|
|
|
|
*/
|
|
|
|
disableVideoInputSelect: boolean,
|
|
|
|
|
2021-02-02 00:20:39 +00:00
|
|
|
/**
|
|
|
|
* Whether or not the audio permission was granted.
|
|
|
|
*/
|
|
|
|
hasAudioPermission: boolean,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether or not the audio permission was granted.
|
|
|
|
*/
|
|
|
|
hasVideoPermission: boolean,
|
|
|
|
|
2018-06-20 20:19:53 +00:00
|
|
|
/**
|
|
|
|
* 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: boolean,
|
|
|
|
|
2021-09-09 17:23:36 +00:00
|
|
|
/**
|
|
|
|
* If true, the button to play a test sound on the selected speaker will not be displayed.
|
|
|
|
* This needs to be hidden on browsers that do not support selecting an audio output device.
|
|
|
|
*/
|
|
|
|
hideAudioOutputPreview: boolean,
|
|
|
|
|
2018-06-20 20:19:53 +00:00
|
|
|
/**
|
|
|
|
* Whether or not the audio output source selector should display. If
|
|
|
|
* true, the audio output selector and test audio link will not be
|
2018-06-27 09:43:13 +00:00
|
|
|
* rendered.
|
2018-06-20 20:19:53 +00:00
|
|
|
*/
|
|
|
|
hideAudioOutputSelect: boolean,
|
|
|
|
|
2021-06-09 06:22:16 +00:00
|
|
|
/**
|
|
|
|
* Whether video input preview should be displayed or not.
|
|
|
|
* (In the case of iOS Safari)
|
|
|
|
*/
|
|
|
|
hideVideoInputPreview: boolean,
|
|
|
|
|
2018-08-06 15:24:59 +00:00
|
|
|
/**
|
|
|
|
* An optional callback to invoke after the component has completed its
|
|
|
|
* mount logic.
|
|
|
|
*/
|
|
|
|
mountCallback?: Function,
|
|
|
|
|
2018-06-20 20:19:53 +00:00
|
|
|
/**
|
|
|
|
* The id of the audio input device to preview.
|
|
|
|
*/
|
|
|
|
selectedAudioInputId: string,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The id of the audio output device to preview.
|
|
|
|
*/
|
|
|
|
selectedAudioOutputId: string,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The id of the video input device to preview.
|
|
|
|
*/
|
|
|
|
selectedVideoInputId: string,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invoked to obtain translated strings.
|
|
|
|
*/
|
|
|
|
t: Function
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The type of the React {@code Component} state of {@link DeviceSelection}.
|
|
|
|
*/
|
|
|
|
type State = {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The JitsiTrack to use for previewing audio input.
|
|
|
|
*/
|
|
|
|
previewAudioTrack: ?Object,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The JitsiTrack to use for previewing video input.
|
|
|
|
*/
|
|
|
|
previewVideoTrack: ?Object,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The error message from trying to use a video input device.
|
|
|
|
*/
|
|
|
|
previewVideoTrackError: ?string
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* React {@code Component} for previewing audio and video input/output devices.
|
|
|
|
*
|
|
|
|
* @extends Component
|
|
|
|
*/
|
|
|
|
class DeviceSelection extends AbstractDialogTab<Props, State> {
|
2019-04-29 10:41:54 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether current component is mounted or not.
|
|
|
|
*
|
|
|
|
* In component did mount we start a Promise to create tracks and
|
|
|
|
* set the tracks in the state, if we unmount the component in the meanwhile
|
|
|
|
* tracks will be created and will never been disposed (dispose tracks is
|
|
|
|
* in componentWillUnmount). When tracks are created and component is
|
|
|
|
* unmounted we dispose the tracks.
|
|
|
|
*/
|
|
|
|
_unMounted: boolean;
|
|
|
|
|
2018-06-20 20:19:53 +00:00
|
|
|
/**
|
|
|
|
* Initializes a new DeviceSelection instance.
|
|
|
|
*
|
|
|
|
* @param {Object} props - The read-only React Component props with which
|
|
|
|
* the new instance is to be initialized.
|
|
|
|
*/
|
|
|
|
constructor(props: Props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
previewAudioTrack: null,
|
|
|
|
previewVideoTrack: null,
|
|
|
|
previewVideoTrackError: null
|
|
|
|
};
|
2019-04-29 10:41:54 +00:00
|
|
|
this._unMounted = true;
|
2018-06-20 20:19:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate the initial previews for audio input and video input.
|
|
|
|
*
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
componentDidMount() {
|
2019-04-29 10:41:54 +00:00
|
|
|
this._unMounted = false;
|
2018-08-06 15:24:59 +00:00
|
|
|
Promise.all([
|
|
|
|
this._createAudioInputTrack(this.props.selectedAudioInputId),
|
|
|
|
this._createVideoInputTrack(this.props.selectedVideoInputId)
|
|
|
|
])
|
|
|
|
.catch(err => logger.warn('Failed to initialize preview tracks', err))
|
|
|
|
.then(() => this.props.mountCallback && this.props.mountCallback());
|
2018-06-20 20:19:53 +00:00
|
|
|
}
|
|
|
|
|
2018-09-20 13:22:18 +00:00
|
|
|
/**
|
2018-10-29 19:10:10 +00:00
|
|
|
* Checks if audio / video permissions were granted. Updates audio input and
|
|
|
|
* video input previews.
|
2018-09-20 13:22:18 +00:00
|
|
|
*
|
|
|
|
* @param {Object} prevProps - Previous props this component received.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2021-02-02 00:20:39 +00:00
|
|
|
componentDidUpdate(prevProps) {
|
2018-10-29 19:10:10 +00:00
|
|
|
if (prevProps.selectedAudioInputId
|
|
|
|
!== this.props.selectedAudioInputId) {
|
|
|
|
this._createAudioInputTrack(this.props.selectedAudioInputId);
|
2018-06-20 20:19:53 +00:00
|
|
|
}
|
|
|
|
|
2018-10-29 19:10:10 +00:00
|
|
|
if (prevProps.selectedVideoInputId
|
|
|
|
!== this.props.selectedVideoInputId) {
|
|
|
|
this._createVideoInputTrack(this.props.selectedVideoInputId);
|
2018-06-20 20:19:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ensure preview tracks are destroyed to prevent continued use.
|
|
|
|
*
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
componentWillUnmount() {
|
2019-04-29 10:41:54 +00:00
|
|
|
this._unMounted = true;
|
2018-06-20 20:19:53 +00:00
|
|
|
this._disposeAudioInputPreview();
|
|
|
|
this._disposeVideoInputPreview();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements React's {@link Component#render()}.
|
|
|
|
*
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
hideAudioInputPreview,
|
2021-09-09 17:23:36 +00:00
|
|
|
hideAudioOutputPreview,
|
2021-06-09 06:22:16 +00:00
|
|
|
hideVideoInputPreview,
|
2018-06-20 20:19:53 +00:00
|
|
|
selectedAudioOutputId
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
return (
|
2021-06-09 06:22:16 +00:00
|
|
|
<div className = { `device-selection${hideVideoInputPreview ? ' video-hidden' : ''}` }>
|
2018-06-20 20:19:53 +00:00
|
|
|
<div className = 'device-selection-column column-video'>
|
2021-06-09 06:22:16 +00:00
|
|
|
{ !hideVideoInputPreview
|
|
|
|
&& <div className = 'device-selection-video-container'>
|
|
|
|
<VideoInputPreview
|
|
|
|
error = { this.state.previewVideoTrackError }
|
|
|
|
track = { this.state.previewVideoTrack } />
|
|
|
|
</div>
|
|
|
|
}
|
2018-06-20 20:19:53 +00:00
|
|
|
{ !hideAudioInputPreview
|
|
|
|
&& <AudioInputPreview
|
|
|
|
track = { this.state.previewAudioTrack } /> }
|
|
|
|
</div>
|
|
|
|
<div className = 'device-selection-column column-selectors'>
|
2021-06-10 12:48:44 +00:00
|
|
|
<div
|
|
|
|
aria-live = 'polite all'
|
|
|
|
className = 'device-selectors'>
|
2018-06-20 20:19:53 +00:00
|
|
|
{ this._renderSelectors() }
|
|
|
|
</div>
|
2021-09-09 17:23:36 +00:00
|
|
|
{ !hideAudioOutputPreview
|
2018-06-20 20:19:53 +00:00
|
|
|
&& <AudioOutputPreview
|
|
|
|
deviceId = { selectedAudioOutputId } /> }
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates the JitiTrack for the audio input preview.
|
|
|
|
*
|
|
|
|
* @param {string} deviceId - The id of audio input device to preview.
|
|
|
|
* @private
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
_createAudioInputTrack(deviceId) {
|
2021-09-16 19:05:43 +00:00
|
|
|
const { hideAudioInputPreview } = this.props;
|
|
|
|
|
|
|
|
if (hideAudioInputPreview) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-08-06 15:24:59 +00:00
|
|
|
return this._disposeAudioInputPreview()
|
2021-02-02 00:20:39 +00:00
|
|
|
.then(() => createLocalTrack('audio', deviceId, 5000))
|
2018-06-20 20:19:53 +00:00
|
|
|
.then(jitsiLocalTrack => {
|
2019-04-29 10:41:54 +00:00
|
|
|
if (this._unMounted) {
|
|
|
|
jitsiLocalTrack.dispose();
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-20 20:19:53 +00:00
|
|
|
this.setState({
|
|
|
|
previewAudioTrack: jitsiLocalTrack
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
this.setState({
|
|
|
|
previewAudioTrack: null
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates the JitiTrack for the video input preview.
|
|
|
|
*
|
|
|
|
* @param {string} deviceId - The id of video device to preview.
|
|
|
|
* @private
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
_createVideoInputTrack(deviceId) {
|
2021-06-09 06:22:16 +00:00
|
|
|
const { hideVideoInputPreview } = this.props;
|
|
|
|
|
|
|
|
if (hideVideoInputPreview) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-08-06 15:24:59 +00:00
|
|
|
return this._disposeVideoInputPreview()
|
2021-02-02 00:20:39 +00:00
|
|
|
.then(() => createLocalTrack('video', deviceId, 5000))
|
2018-06-20 20:19:53 +00:00
|
|
|
.then(jitsiLocalTrack => {
|
|
|
|
if (!jitsiLocalTrack) {
|
|
|
|
return Promise.reject();
|
|
|
|
}
|
|
|
|
|
2019-04-29 10:41:54 +00:00
|
|
|
if (this._unMounted) {
|
|
|
|
jitsiLocalTrack.dispose();
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-20 20:19:53 +00:00
|
|
|
this.setState({
|
|
|
|
previewVideoTrack: jitsiLocalTrack,
|
|
|
|
previewVideoTrackError: null
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
this.setState({
|
|
|
|
previewVideoTrack: null,
|
|
|
|
previewVideoTrackError:
|
|
|
|
this.props.t('deviceSelection.previewUnavailable')
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Utility function for disposing the current audio input preview.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @returns {Promise}
|
|
|
|
*/
|
|
|
|
_disposeAudioInputPreview(): Promise<*> {
|
|
|
|
return this.state.previewAudioTrack
|
|
|
|
? this.state.previewAudioTrack.dispose() : Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Utility function for disposing the current video input preview.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @returns {Promise}
|
|
|
|
*/
|
|
|
|
_disposeVideoInputPreview(): Promise<*> {
|
|
|
|
return this.state.previewVideoTrack
|
|
|
|
? this.state.previewVideoTrack.dispose() : Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a DeviceSelector instance based on the passed in configuration.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {Object} deviceSelectorProps - The props for the DeviceSelector.
|
|
|
|
* @returns {ReactElement}
|
|
|
|
*/
|
|
|
|
_renderSelector(deviceSelectorProps) {
|
|
|
|
return (
|
|
|
|
<div key = { deviceSelectorProps.label }>
|
2021-06-10 12:48:44 +00:00
|
|
|
<label
|
|
|
|
className = 'device-selector-label'
|
|
|
|
htmlFor = { deviceSelectorProps.id }>
|
2018-06-20 20:19:53 +00:00
|
|
|
{ this.props.t(deviceSelectorProps.label) }
|
2021-06-10 12:48:44 +00:00
|
|
|
</label>
|
2018-06-20 20:19:53 +00:00
|
|
|
<DeviceSelector { ...deviceSelectorProps } />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates DeviceSelector instances for video output, audio input, and audio
|
|
|
|
* output.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @returns {Array<ReactElement>} DeviceSelector instances.
|
|
|
|
*/
|
|
|
|
_renderSelectors() {
|
2021-02-02 00:20:39 +00:00
|
|
|
const { availableDevices, hasAudioPermission, hasVideoPermission } = this.props;
|
2018-06-20 20:19:53 +00:00
|
|
|
|
|
|
|
const configurations = [
|
|
|
|
{
|
|
|
|
devices: availableDevices.audioInput,
|
2018-09-20 13:22:18 +00:00
|
|
|
hasPermission: hasAudioPermission,
|
2018-06-20 20:19:53 +00:00
|
|
|
icon: 'icon-microphone',
|
2021-09-09 17:23:36 +00:00
|
|
|
isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
|
2018-06-20 20:19:53 +00:00
|
|
|
key: 'audioInput',
|
2021-06-10 12:48:44 +00:00
|
|
|
id: 'audioInput',
|
2018-06-20 20:19:53 +00:00
|
|
|
label: 'settings.selectMic',
|
2021-09-09 17:23:36 +00:00
|
|
|
onSelect: selectedAudioInputId => super._onChange({ selectedAudioInputId }),
|
2019-06-27 16:04:47 +00:00
|
|
|
selectedDeviceId: this.state.previewAudioTrack
|
2021-09-09 17:23:36 +00:00
|
|
|
? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
|
|
|
|
},
|
|
|
|
{
|
2021-06-09 06:22:16 +00:00
|
|
|
devices: availableDevices.videoInput,
|
|
|
|
hasPermission: hasVideoPermission,
|
|
|
|
icon: 'icon-camera',
|
2021-09-09 17:23:36 +00:00
|
|
|
isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
|
2021-06-09 06:22:16 +00:00
|
|
|
key: 'videoInput',
|
2021-06-10 12:48:44 +00:00
|
|
|
id: 'videoInput',
|
2021-06-09 06:22:16 +00:00
|
|
|
label: 'settings.selectCamera',
|
2021-09-09 17:23:36 +00:00
|
|
|
onSelect: selectedVideoInputId => super._onChange({ selectedVideoInputId }),
|
2021-06-09 06:22:16 +00:00
|
|
|
selectedDeviceId: this.state.previewVideoTrack
|
2021-09-09 17:23:36 +00:00
|
|
|
? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
|
|
|
|
}
|
|
|
|
];
|
2021-06-09 06:22:16 +00:00
|
|
|
|
2018-06-20 20:19:53 +00:00
|
|
|
if (!this.props.hideAudioOutputSelect) {
|
|
|
|
configurations.push({
|
|
|
|
devices: availableDevices.audioOutput,
|
2018-09-20 13:22:18 +00:00
|
|
|
hasPermission: hasAudioPermission || hasVideoPermission,
|
2018-06-28 21:59:07 +00:00
|
|
|
icon: 'icon-speaker',
|
2018-06-20 20:19:53 +00:00
|
|
|
isDisabled: this.props.disableDeviceChange,
|
|
|
|
key: 'audioOutput',
|
2021-06-10 12:48:44 +00:00
|
|
|
id: 'audioOutput',
|
2018-06-20 20:19:53 +00:00
|
|
|
label: 'settings.selectAudioOutput',
|
2021-09-09 17:23:36 +00:00
|
|
|
onSelect: selectedAudioOutputId => super._onChange({ selectedAudioOutputId }),
|
2018-06-20 20:19:53 +00:00
|
|
|
selectedDeviceId: this.props.selectedAudioOutputId
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return configurations.map(config => this._renderSelector(config));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default translate(DeviceSelection);
|