2020-03-30 14:17:18 +00:00
|
|
|
// @flow
|
|
|
|
|
|
|
|
import React, { Component } from 'react';
|
|
|
|
|
|
|
|
import { translate } from '../../../../base/i18n';
|
2022-11-08 10:24:32 +00:00
|
|
|
import { IconMic, IconVolumeUp } from '../../../../base/icons';
|
2020-12-30 14:19:55 +00:00
|
|
|
import JitsiMeetJS from '../../../../base/lib-jitsi-meet';
|
2020-06-25 10:31:14 +00:00
|
|
|
import { equals } from '../../../../base/redux';
|
|
|
|
import { createLocalAudioTracks } from '../../../functions';
|
2020-05-20 10:57:03 +00:00
|
|
|
|
|
|
|
import AudioSettingsHeader from './AudioSettingsHeader';
|
2020-03-30 14:17:18 +00:00
|
|
|
import MicrophoneEntry from './MicrophoneEntry';
|
|
|
|
import SpeakerEntry from './SpeakerEntry';
|
|
|
|
|
2020-12-30 14:19:55 +00:00
|
|
|
const browser = JitsiMeetJS.util.browser;
|
|
|
|
|
2021-05-14 21:37:48 +00:00
|
|
|
/**
|
|
|
|
* Translates the default device label into a more user friendly one.
|
|
|
|
*
|
|
|
|
* @param {string} deviceId - The device Id.
|
|
|
|
* @param {string} label - The device label.
|
|
|
|
* @param {Function} t - The translation function.
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
function transformDefaultDeviceLabel(deviceId, label, t) {
|
|
|
|
return deviceId === 'default'
|
|
|
|
? t('settings.sameAsSystem', { label: label.replace('Default - ', '') })
|
|
|
|
: label;
|
|
|
|
}
|
|
|
|
|
2020-03-30 14:17:18 +00:00
|
|
|
export type Props = {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The deviceId of the microphone in use.
|
|
|
|
*/
|
|
|
|
currentMicDeviceId: string,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The deviceId of the output device in use.
|
|
|
|
*/
|
|
|
|
currentOutputDeviceId: string,
|
|
|
|
|
2022-01-11 16:08:36 +00:00
|
|
|
/**
|
|
|
|
* Used to decide whether to measure audio levels for microphone devices.
|
|
|
|
*/
|
|
|
|
measureAudioLevels: boolean,
|
|
|
|
|
2020-03-30 14:17:18 +00:00
|
|
|
/**
|
|
|
|
* Used to set a new microphone as the current one.
|
|
|
|
*/
|
|
|
|
setAudioInputDevice: Function,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Used to set a new output device as the current one.
|
|
|
|
*/
|
|
|
|
setAudioOutputDevice: Function,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A list of objects containing the labels and deviceIds
|
|
|
|
* of all the output devices.
|
|
|
|
*/
|
|
|
|
outputDevices: Object[],
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A list with objects containing the labels and deviceIds
|
|
|
|
* of all the input devices.
|
|
|
|
*/
|
|
|
|
microphoneDevices: Object[],
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invoked to obtain translated strings.
|
|
|
|
*/
|
|
|
|
t: Function
|
|
|
|
};
|
|
|
|
|
|
|
|
type State = {
|
|
|
|
|
|
|
|
/**
|
2020-06-25 10:31:14 +00:00
|
|
|
* An list of objects, each containing the microphone label, audio track, device id
|
|
|
|
* and track error if the case.
|
2020-03-30 14:17:18 +00:00
|
|
|
*/
|
2020-06-25 10:31:14 +00:00
|
|
|
audioTracks: Object[]
|
2020-03-30 14:17:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-03-16 15:59:33 +00:00
|
|
|
* Implements a React {@link Component} which displays a list of all
|
2020-03-30 14:17:18 +00:00
|
|
|
* the audio input & output devices to choose from.
|
|
|
|
*
|
2021-11-04 21:10:43 +00:00
|
|
|
* @augments Component
|
2020-03-30 14:17:18 +00:00
|
|
|
*/
|
|
|
|
class AudioSettingsContent extends Component<Props, State> {
|
|
|
|
_componentWasUnmounted: boolean;
|
2021-06-10 12:48:44 +00:00
|
|
|
_audioContentRef: Object;
|
|
|
|
microphoneHeaderId = 'microphone_settings_header';
|
|
|
|
speakerHeaderId = 'speaker_settings_header';
|
|
|
|
|
2020-03-30 14:17:18 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Initializes a new {@code AudioSettingsContent} instance.
|
|
|
|
*
|
|
|
|
* @param {Object} props - The read-only properties with which the new
|
|
|
|
* instance is to be initialized.
|
|
|
|
*/
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this._onMicrophoneEntryClick = this._onMicrophoneEntryClick.bind(this);
|
|
|
|
this._onSpeakerEntryClick = this._onSpeakerEntryClick.bind(this);
|
2021-06-10 12:48:44 +00:00
|
|
|
this._onEscClick = this._onEscClick.bind(this);
|
|
|
|
this._audioContentRef = React.createRef();
|
2020-03-30 14:17:18 +00:00
|
|
|
|
|
|
|
this.state = {
|
2020-06-25 10:31:14 +00:00
|
|
|
audioTracks: props.microphoneDevices.map(({ deviceId, label }) => {
|
|
|
|
return {
|
|
|
|
deviceId,
|
|
|
|
hasError: false,
|
|
|
|
jitsiTrack: null,
|
|
|
|
label
|
|
|
|
};
|
|
|
|
})
|
2020-03-30 14:17:18 +00:00
|
|
|
};
|
|
|
|
}
|
2021-06-10 12:48:44 +00:00
|
|
|
_onEscClick: (KeyboardEvent) => void;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Click handler for the speaker entries.
|
|
|
|
*
|
|
|
|
* @param {KeyboardEvent} event - Esc key click to close the popup.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
_onEscClick(event) {
|
|
|
|
if (event.key === 'Escape') {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
this._audioContentRef.current.style.display = 'none';
|
|
|
|
}
|
|
|
|
}
|
2020-03-30 14:17:18 +00:00
|
|
|
|
|
|
|
_onMicrophoneEntryClick: (string) => void;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Click handler for the microphone entries.
|
|
|
|
*
|
|
|
|
* @param {string} deviceId - The deviceId for the clicked microphone.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
_onMicrophoneEntryClick(deviceId) {
|
|
|
|
this.props.setAudioInputDevice(deviceId);
|
|
|
|
}
|
|
|
|
|
|
|
|
_onSpeakerEntryClick: (string) => void;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Click handler for the speaker entries.
|
|
|
|
*
|
|
|
|
* @param {string} deviceId - The deviceId for the clicked speaker.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
_onSpeakerEntryClick(deviceId) {
|
|
|
|
this.props.setAudioOutputDevice(deviceId);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders a single microphone entry.
|
|
|
|
*
|
2020-06-25 10:31:14 +00:00
|
|
|
* @param {Object} data - An object with the deviceId, jitsiTrack & label of the microphone.
|
2020-03-30 14:17:18 +00:00
|
|
|
* @param {number} index - The index of the element, used for creating a key.
|
2021-06-10 12:48:44 +00:00
|
|
|
* @param {length} length - The length of the microphone list.
|
2021-05-14 21:37:48 +00:00
|
|
|
* @param {Function} t - The translation function.
|
2020-03-30 14:17:18 +00:00
|
|
|
* @returns {React$Node}
|
|
|
|
*/
|
2021-06-10 12:48:44 +00:00
|
|
|
_renderMicrophoneEntry(data, index, length, t) {
|
2021-05-14 21:37:48 +00:00
|
|
|
const { deviceId, jitsiTrack, hasError } = data;
|
|
|
|
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
|
2020-03-30 14:17:18 +00:00
|
|
|
const isSelected = deviceId === this.props.currentMicDeviceId;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<MicrophoneEntry
|
|
|
|
deviceId = { deviceId }
|
|
|
|
hasError = { hasError }
|
2021-06-10 12:48:44 +00:00
|
|
|
index = { index }
|
2020-03-30 14:17:18 +00:00
|
|
|
isSelected = { isSelected }
|
|
|
|
jitsiTrack = { jitsiTrack }
|
2020-06-25 10:31:14 +00:00
|
|
|
key = { `me-${index}` }
|
2021-06-10 12:48:44 +00:00
|
|
|
length = { length }
|
|
|
|
listHeaderId = { this.microphoneHeaderId }
|
2022-01-11 16:08:36 +00:00
|
|
|
measureAudioLevels = { this.props.measureAudioLevels }
|
2020-03-30 14:17:18 +00:00
|
|
|
onClick = { this._onMicrophoneEntryClick }>
|
|
|
|
{label}
|
|
|
|
</MicrophoneEntry>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders a single speaker entry.
|
|
|
|
*
|
|
|
|
* @param {Object} data - An object with the deviceId and label of the speaker.
|
|
|
|
* @param {number} index - The index of the element, used for creating a key.
|
2021-06-10 12:48:44 +00:00
|
|
|
* @param {length} length - The length of the speaker list.
|
2021-05-14 21:37:48 +00:00
|
|
|
* @param {Function} t - The translation function.
|
2020-03-30 14:17:18 +00:00
|
|
|
* @returns {React$Node}
|
|
|
|
*/
|
2021-06-10 12:48:44 +00:00
|
|
|
_renderSpeakerEntry(data, index, length, t) {
|
2021-05-14 21:37:48 +00:00
|
|
|
const { deviceId } = data;
|
|
|
|
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
|
2020-03-30 14:17:18 +00:00
|
|
|
const key = `se-${index}`;
|
2021-06-10 12:48:44 +00:00
|
|
|
const isSelected = deviceId === this.props.currentOutputDeviceId;
|
2020-03-30 14:17:18 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<SpeakerEntry
|
|
|
|
deviceId = { deviceId }
|
2021-06-10 12:48:44 +00:00
|
|
|
index = { index }
|
|
|
|
isSelected = { isSelected }
|
2020-03-30 14:17:18 +00:00
|
|
|
key = { key }
|
2021-06-10 12:48:44 +00:00
|
|
|
length = { length }
|
|
|
|
listHeaderId = { this.speakerHeaderId }
|
2020-03-30 14:17:18 +00:00
|
|
|
onClick = { this._onSpeakerEntryClick }>
|
|
|
|
{label}
|
|
|
|
</SpeakerEntry>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-25 10:31:14 +00:00
|
|
|
* Creates and updates the audio tracks.
|
2020-03-30 14:17:18 +00:00
|
|
|
*
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2020-06-25 10:31:14 +00:00
|
|
|
async _setTracks() {
|
2021-03-08 15:47:58 +00:00
|
|
|
if (browser.isWebKitBased()) {
|
2020-12-30 14:19:55 +00:00
|
|
|
|
|
|
|
// It appears that at the time of this writing, creating audio tracks blocks the browser's main thread for
|
|
|
|
// long time on safari. Wasn't able to confirm which part of track creation does the blocking exactly, but
|
|
|
|
// not creating the tracks seems to help and makes the UI much more responsive.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-06-25 10:31:14 +00:00
|
|
|
this._disposeTracks(this.state.audioTracks);
|
2020-03-30 14:17:18 +00:00
|
|
|
|
2021-02-02 00:20:39 +00:00
|
|
|
const audioTracks = await createLocalAudioTracks(this.props.microphoneDevices, 5000);
|
2020-03-30 14:17:18 +00:00
|
|
|
|
|
|
|
if (this._componentWasUnmounted) {
|
2020-06-25 10:31:14 +00:00
|
|
|
this._disposeTracks(audioTracks);
|
2020-03-30 14:17:18 +00:00
|
|
|
} else {
|
|
|
|
this.setState({
|
2020-06-25 10:31:14 +00:00
|
|
|
audioTracks
|
2020-03-30 14:17:18 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-25 10:31:14 +00:00
|
|
|
* Disposes the audio tracks.
|
2020-03-30 14:17:18 +00:00
|
|
|
*
|
2020-06-25 10:31:14 +00:00
|
|
|
* @param {Object} audioTracks - The object holding the audio tracks.
|
|
|
|
* @returns {void}
|
2020-03-30 14:17:18 +00:00
|
|
|
*/
|
2020-06-25 10:31:14 +00:00
|
|
|
_disposeTracks(audioTracks) {
|
|
|
|
audioTracks.forEach(({ jitsiTrack }) => {
|
|
|
|
jitsiTrack && jitsiTrack.dispose();
|
|
|
|
});
|
2020-03-30 14:17:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements React's {@link Component#componentDidMount}.
|
|
|
|
*
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
componentDidMount() {
|
2020-06-25 10:31:14 +00:00
|
|
|
this._setTracks();
|
2020-03-30 14:17:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements React's {@link Component#componentWillUnmount}.
|
|
|
|
*
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
componentWillUnmount() {
|
|
|
|
this._componentWasUnmounted = true;
|
2020-06-25 10:31:14 +00:00
|
|
|
this._disposeTracks(this.state.audioTracks);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements React's {@link Component#componentDidUpdate}.
|
|
|
|
*
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
componentDidUpdate(prevProps) {
|
|
|
|
if (!equals(this.props.microphoneDevices, prevProps.microphoneDevices)) {
|
|
|
|
this._setTracks();
|
|
|
|
}
|
2020-03-30 14:17:18 +00:00
|
|
|
}
|
|
|
|
|
2020-06-25 10:31:14 +00:00
|
|
|
|
2020-03-30 14:17:18 +00:00
|
|
|
/**
|
|
|
|
* Implements React's {@link Component#render}.
|
|
|
|
*
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
render() {
|
2020-06-25 10:31:14 +00:00
|
|
|
const { outputDevices, t } = this.props;
|
2020-03-30 14:17:18 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
2021-06-10 12:48:44 +00:00
|
|
|
<div
|
|
|
|
aria-labelledby = 'audio-settings-button'
|
|
|
|
className = 'audio-preview-content'
|
|
|
|
id = 'audio-settings-dialog'
|
|
|
|
onKeyDown = { this._onEscClick }
|
|
|
|
ref = { this._audioContentRef }
|
|
|
|
role = 'menu'
|
|
|
|
tabIndex = { -1 }>
|
|
|
|
<div role = 'menuitem'>
|
|
|
|
<AudioSettingsHeader
|
2022-11-08 10:24:32 +00:00
|
|
|
IconComponent = { IconMic }
|
2021-06-10 12:48:44 +00:00
|
|
|
id = { this.microphoneHeaderId }
|
|
|
|
text = { t('settings.microphones') } />
|
|
|
|
<ul
|
|
|
|
aria-labelledby = 'microphone_settings_header'
|
|
|
|
className = 'audio-preview-content-ul'
|
|
|
|
role = 'radiogroup'
|
|
|
|
tabIndex = '-1'>
|
|
|
|
{this.state.audioTracks.map((data, i) =>
|
2021-11-04 21:10:43 +00:00
|
|
|
this._renderMicrophoneEntry(data, i, this.state.audioTracks.length, t)
|
2021-06-10 12:48:44 +00:00
|
|
|
)}
|
|
|
|
</ul>
|
|
|
|
</div>
|
2020-12-30 14:19:55 +00:00
|
|
|
{ outputDevices.length > 0 && (
|
2021-06-10 12:48:44 +00:00
|
|
|
<div role = 'menuitem'>
|
2021-02-23 11:09:22 +00:00
|
|
|
<hr className = 'audio-preview-hr' />
|
|
|
|
<AudioSettingsHeader
|
2022-11-08 10:24:32 +00:00
|
|
|
IconComponent = { IconVolumeUp }
|
2021-06-10 12:48:44 +00:00
|
|
|
id = { this.speakerHeaderId }
|
2021-02-23 11:09:22 +00:00
|
|
|
text = { t('settings.speakers') } />
|
2021-06-10 12:48:44 +00:00
|
|
|
<ul
|
|
|
|
aria-labelledby = 'speaker_settings_header'
|
|
|
|
className = 'audio-preview-content-ul'
|
|
|
|
role = 'radiogroup'
|
|
|
|
tabIndex = '-1'>
|
|
|
|
{ outputDevices.map((data, i) =>
|
2021-11-04 21:10:43 +00:00
|
|
|
this._renderSpeakerEntry(data, i, outputDevices.length, t)
|
2021-06-10 12:48:44 +00:00
|
|
|
)}
|
|
|
|
</ul>
|
|
|
|
</div>)
|
2020-12-30 14:19:55 +00:00
|
|
|
}
|
2020-03-30 14:17:18 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default translate(AudioSettingsContent);
|