123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560 |
- import PropTypes from 'prop-types';
- import React, { Component } from 'react';
-
- import { StatelessDialog } from '../../base/dialog';
- import { translate } from '../../base/i18n';
- import { createLocalTrack } from '../../base/lib-jitsi-meet';
-
- import { shouldShowOnlyDeviceSelection } from '../../settings';
-
- 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: PropTypes.object,
-
- /**
- * Closes the dialog.
- */
- closeModal: PropTypes.func,
-
- /**
- * Device id for the current audio input device. This device will be set
- * as the default audio input device to preview.
- */
- currentAudioInputId: PropTypes.string,
-
- /**
- * Device id for the current audio output device. This device will be
- * set as the default audio output device to preview.
- */
- currentAudioOutputId: PropTypes.string,
-
- /**
- * Device id for the current video input device. This device will be set
- * as the default video input device to preview.
- */
- currentVideoInputId: 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: PropTypes.bool,
-
- /**
- * Disables dismissing the dialog when the blanket is clicked. Enabled
- * by default.
- */
- disableBlanketClickDismiss: PropTypes.bool,
-
- /**
- * True if device changing is configured to be disallowed. Selectors
- * will display as disabled.
- */
- disableDeviceChange: PropTypes.bool,
-
- /**
- * Function that checks whether or not a new audio input source can be
- * selected.
- */
- hasAudioPermission: PropTypes.func,
-
- /**
- * Function that checks whether or not a new video input sources can be
- * selected.
- */
- hasVideoPermission: PropTypes.func,
-
- /**
- * 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: 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: PropTypes.bool,
-
- /**
- * Function that sets the audio input device.
- */
- setAudioInputDevice: PropTypes.func,
-
- /**
- * Function that sets the audio output device.
- */
- setAudioOutputDevice: PropTypes.func,
-
- /**
- * Function that sets the video input device.
- */
- setVideoInputDevice: PropTypes.func,
-
- /**
- * Invoked to obtain translated strings.
- */
- t: 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 = { this._getModalTitle() }>
- <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();
- }
-
- /**
- * Returns what the title of the device selection modal should be.
- *
- * Note: This is temporary logic to appease design sooner. Device selection
- * and all other settings will be combined into one modal.
- *
- * @returns {string}
- */
- _getModalTitle() {
- if (shouldShowOnlyDeviceSelection()) {
- return 'settings.title';
- }
-
- return 'deviceSelection.deviceSettings';
- }
-
- /**
- * 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} deviceSelectorProps - The props for the DeviceSelector.
- * @returns {ReactElement}
- */
- _renderSelector(deviceSelectorProps) {
-
- return (
- <div key = { deviceSelectorProps.label }>
- <div className = 'device-selector-label'>
- { this.props.t(deviceSelectorProps.label) }
- </div>
- <DeviceSelector { ...deviceSelectorProps } />
- </div>
- );
- }
-
- /**
- * 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(config => this._renderSelector(config));
- }
-
- /**
- * 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 => {
- if (!jitsiLocalTrack) {
- return Promise.reject();
- }
-
- this.setState({
- previewVideoTrack: jitsiLocalTrack,
- previewVideoTrackError: null
- });
- })
- .catch(() => {
- this.setState({
- previewVideoTrack: null,
- previewVideoTrackError:
- this.props.t('deviceSelection.previewUnavailable')
- });
- });
- });
- }
- }
-
- export default translate(DeviceSelectionDialogBase);
|