// @flow import React, { Component } from 'react'; import { translate } from '../../../../base/i18n'; import { IconMic, IconVolumeUp } from '../../../../base/icons'; import JitsiMeetJS from '../../../../base/lib-jitsi-meet'; import { equals } from '../../../../base/redux'; import { createLocalAudioTracks } from '../../../functions'; import AudioSettingsHeader from './AudioSettingsHeader'; import MicrophoneEntry from './MicrophoneEntry'; import SpeakerEntry from './SpeakerEntry'; const browser = JitsiMeetJS.util.browser; /** * 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; } export type Props = { /** * The deviceId of the microphone in use. */ currentMicDeviceId: string, /** * The deviceId of the output device in use. */ currentOutputDeviceId: string, /** * Used to decide whether to measure audio levels for microphone devices. */ measureAudioLevels: boolean, /** * 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 = { /** * An list of objects, each containing the microphone label, audio track, device id * and track error if the case. */ audioTracks: Object[] } /** * Implements a React {@link Component} which displays a list of all * the audio input & output devices to choose from. * * @augments Component */ class AudioSettingsContent extends Component { _componentWasUnmounted: boolean; _audioContentRef: Object; microphoneHeaderId = 'microphone_settings_header'; speakerHeaderId = 'speaker_settings_header'; /** * 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); this._onEscClick = this._onEscClick.bind(this); this._audioContentRef = React.createRef(); this.state = { audioTracks: props.microphoneDevices.map(({ deviceId, label }) => { return { deviceId, hasError: false, jitsiTrack: null, label }; }) }; } _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'; } } _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. * * @param {Object} data - An object with the deviceId, jitsiTrack & label of the microphone. * @param {number} index - The index of the element, used for creating a key. * @param {length} length - The length of the microphone list. * @param {Function} t - The translation function. * @returns {React$Node} */ _renderMicrophoneEntry(data, index, length, t) { const { deviceId, jitsiTrack, hasError } = data; const label = transformDefaultDeviceLabel(deviceId, data.label, t); const isSelected = deviceId === this.props.currentMicDeviceId; return ( {label} ); } /** * 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. * @param {length} length - The length of the speaker list. * @param {Function} t - The translation function. * @returns {React$Node} */ _renderSpeakerEntry(data, index, length, t) { const { deviceId } = data; const label = transformDefaultDeviceLabel(deviceId, data.label, t); const key = `se-${index}`; const isSelected = deviceId === this.props.currentOutputDeviceId; return ( {label} ); } /** * Creates and updates the audio tracks. * * @returns {void} */ async _setTracks() { if (browser.isWebKitBased()) { // 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; } this._disposeTracks(this.state.audioTracks); const audioTracks = await createLocalAudioTracks(this.props.microphoneDevices, 5000); if (this._componentWasUnmounted) { this._disposeTracks(audioTracks); } else { this.setState({ audioTracks }); } } /** * Disposes the audio tracks. * * @param {Object} audioTracks - The object holding the audio tracks. * @returns {void} */ _disposeTracks(audioTracks) { audioTracks.forEach(({ jitsiTrack }) => { jitsiTrack && jitsiTrack.dispose(); }); } /** * Implements React's {@link Component#componentDidMount}. * * @inheritdoc */ componentDidMount() { this._setTracks(); } /** * Implements React's {@link Component#componentWillUnmount}. * * @inheritdoc */ componentWillUnmount() { this._componentWasUnmounted = true; this._disposeTracks(this.state.audioTracks); } /** * Implements React's {@link Component#componentDidUpdate}. * * @inheritdoc */ componentDidUpdate(prevProps) { if (!equals(this.props.microphoneDevices, prevProps.microphoneDevices)) { this._setTracks(); } } /** * Implements React's {@link Component#render}. * * @inheritdoc */ render() { const { outputDevices, t } = this.props; return (
); } } export default translate(AudioSettingsContent);