theluyuan 38ba663466
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled
init
2025-09-02 14:49:16 +08:00

358 lines
11 KiB
TypeScript

/* eslint-disable react/no-multi-comp */
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState, IStore } from '../../../../app/types';
import { IconMic, IconVolumeUp } from '../../../../base/icons/svg';
import JitsiMeetJS from '../../../../base/lib-jitsi-meet';
import { equals } from '../../../../base/redux/functions';
import Checkbox from '../../../../base/ui/components/web/Checkbox';
import ContextMenu from '../../../../base/ui/components/web/ContextMenu';
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
import ContextMenuItemGroup from '../../../../base/ui/components/web/ContextMenuItemGroup';
import { toggleNoiseSuppression } from '../../../../noise-suppression/actions';
import { isNoiseSuppressionEnabled } from '../../../../noise-suppression/functions';
import { isPrejoinPageVisible } from '../../../../prejoin/functions';
import { createLocalAudioTracks } from '../../../functions.web';
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: string, label: string, t: Function) {
return deviceId === 'default'
? t('settings.sameAsSystem', { label: label.replace('Default - ', '') })
: label;
}
export interface IProps {
/**
* 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;
/**
* A list with objects containing the labels and deviceIds
* of all the input devices.
*/
microphoneDevices: Array<{ deviceId: string; label: string; }>;
/**
* Whether noise suppression is enabled or not.
*/
noiseSuppressionEnabled: boolean;
/**
* A list of objects containing the labels and deviceIds
* of all the output devices.
*/
outputDevices: Array<{ deviceId: string; label: string; }>;
/**
* Whether the prejoin page is visible or not.
*/
prejoinVisible: 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;
/**
* Function to toggle noise suppression.
*/
toggleSuppression: () => void;
}
const useStyles = makeStyles()(theme => {
return {
contextMenu: {
position: 'relative',
right: 'auto',
margin: 0,
marginBottom: theme.spacing(1),
maxHeight: 'calc(100dvh - 100px)',
overflow: 'auto',
width: '300px'
},
header: {
'&:hover': {
backgroundColor: 'initial',
cursor: 'initial'
}
},
list: {
margin: 0,
padding: 0,
listStyleType: 'none'
},
checkboxContainer: {
padding: '10px 16px'
}
};
});
const AudioSettingsContent = ({
currentMicDeviceId,
currentOutputDeviceId,
measureAudioLevels,
microphoneDevices,
noiseSuppressionEnabled,
outputDevices,
prejoinVisible,
setAudioInputDevice,
setAudioOutputDevice,
toggleSuppression
}: IProps) => {
const _componentWasUnmounted = useRef(false);
const microphoneHeaderId = 'microphone_settings_header';
const speakerHeaderId = 'speaker_settings_header';
const { classes } = useStyles();
const [ audioTracks, setAudioTracks ] = useState(microphoneDevices.map(({ deviceId, label }) => {
return {
deviceId,
hasError: false,
jitsiTrack: null,
label
};
}));
const microphoneDevicesRef = useRef(microphoneDevices);
const { t } = useTranslation();
/**
* Click handler for the microphone entries.
*
* @param {string} deviceId - The deviceId for the clicked microphone.
* @returns {void}
*/
const _onMicrophoneEntryClick = useCallback((deviceId: string) => {
setAudioInputDevice(deviceId);
}, [ setAudioInputDevice ]);
/**
* Click handler for the speaker entries.
*
* @param {string} deviceId - The deviceId for the clicked speaker.
* @returns {void}
*/
const _onSpeakerEntryClick = useCallback((deviceId: string) => {
setAudioOutputDevice(deviceId);
}, [ setAudioOutputDevice ]);
/**
* 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.
* @returns {React$Node}
*/
const _renderMicrophoneEntry = (data: { deviceId: string; hasError: boolean; jitsiTrack: any; label: string; },
index: number, length: number) => {
const { deviceId, jitsiTrack, hasError } = data;
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
const isSelected = deviceId === currentMicDeviceId;
return (
<MicrophoneEntry
deviceId = { deviceId }
hasError = { hasError }
index = { index }
isSelected = { isSelected }
jitsiTrack = { jitsiTrack }
key = { `me-${index}` }
length = { length }
measureAudioLevels = { measureAudioLevels }
onClick = { _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.
* @param {length} length - The length of the speaker list.
* @returns {React$Node}
*/
const _renderSpeakerEntry = (data: { deviceId: string; label: string; }, index: number, length: number) => {
const { deviceId } = data;
const label = transformDefaultDeviceLabel(deviceId, data.label, t);
const key = `se-${index}`;
const isSelected = deviceId === currentOutputDeviceId;
return (
<SpeakerEntry
deviceId = { deviceId }
index = { index }
isSelected = { isSelected }
key = { key }
length = { length }
onClick = { _onSpeakerEntryClick }>
{label}
</SpeakerEntry>
);
};
/**
* Disposes the audio tracks.
*
* @param {Object} tracks - The object holding the audio tracks.
* @returns {void}
*/
const _disposeTracks = (tracks: Array<{ jitsiTrack: any; }>) => {
tracks.forEach(({ jitsiTrack }) => {
jitsiTrack?.dispose();
});
};
/**
* Creates and updates the audio tracks.
*
* @returns {void}
*/
const _setTracks = async () => {
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;
}
_disposeTracks(audioTracks);
const newAudioTracks = await createLocalAudioTracks(microphoneDevices, 5000);
if (_componentWasUnmounted.current) {
_disposeTracks(newAudioTracks);
} else {
setAudioTracks(newAudioTracks);
}
};
useEffect(() => {
_setTracks();
return () => {
_componentWasUnmounted.current = true;
_disposeTracks(audioTracks);
};
}, []);
useEffect(() => {
if (!equals(microphoneDevices, microphoneDevicesRef.current)) {
_setTracks();
microphoneDevicesRef.current = microphoneDevices;
}
}, [ microphoneDevices ]);
return (
<ContextMenu
activateFocusTrap = { true }
aria-labelledby = 'audio-settings-button'
className = { classes.contextMenu }
hidden = { false }
id = 'audio-settings-dialog'
role = 'menu'
tabIndex = { -1 }>
<ContextMenuItemGroup
aria-labelledby = { microphoneHeaderId }
role = 'group'>
<ContextMenuItem
className = { classes.header }
icon = { IconMic }
id = { microphoneHeaderId }
text = { t('settings.microphones') } />
<ul
className = { classes.list }
role = 'presentation'>
{audioTracks.map((data, i) =>
_renderMicrophoneEntry(data, i, audioTracks.length)
)}
</ul>
</ContextMenuItemGroup>
{outputDevices.length > 0 && (
<ContextMenuItemGroup
aria-labelledby = { speakerHeaderId }
role = 'group'>
<ContextMenuItem
className = { classes.header }
icon = { IconVolumeUp }
id = { speakerHeaderId }
text = { t('settings.speakers') } />
<ul
className = { classes.list }
role = 'presentation'>
{outputDevices.map((data: any, i: number) =>
_renderSpeakerEntry(data, i, outputDevices.length)
)}
</ul>
</ContextMenuItemGroup>)
}
{!prejoinVisible && (
<ContextMenuItemGroup>
<div
className = { classes.checkboxContainer }
// eslint-disable-next-line react/jsx-no-bind
onClick = { e => e.stopPropagation() }>
<Checkbox
checked = { noiseSuppressionEnabled }
label = { t('toolbar.noiseSuppression') }
onChange = { toggleSuppression } />
</div>
</ContextMenuItemGroup>
)}
</ContextMenu>
);
};
const mapStateToProps = (state: IReduxState) => {
return {
noiseSuppressionEnabled: isNoiseSuppressionEnabled(state),
prejoinVisible: isPrejoinPageVisible(state)
};
};
const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
return {
toggleSuppression() {
dispatch(toggleNoiseSuppression());
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AudioSettingsContent);