Files
feishin/src/renderer/features/settings/components/playback/audio-settings.tsx
T

216 lines
7.5 KiB
TypeScript

import { t } from 'i18next';
import isElectron from 'is-electron';
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { usePlaybackType, usePlayerStatus } from '/@/renderer/store';
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { Select } from '/@/shared/components/select/select';
import { Switch } from '/@/shared/components/switch/switch';
import { toast } from '/@/shared/components/toast/toast';
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
const ipc = isElectron() ? window.api.ipc : null;
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
const getAudioDevices = async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
};
const getMpvAudioDevices = async () => {
if (!mpvPlayer) {
console.log('mpvPlayer not found');
return [];
}
try {
return await mpvPlayer.getAudioDevices();
} catch (error) {
console.error('Failed to get MPV audio devices:', error);
return [];
}
};
export const useAudioDevices = () => {
const playbackType = usePlaybackType();
const [audioDevices, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
useEffect(() => {
const fetchAudioDevices = async () => {
if (!isElectron()) {
return;
}
if (playbackType === PlayerType.WEB) {
getAudioDevices()
.then((dev) => {
const uniqueDevices = dev.filter(
(d, index, self) =>
index === self.findIndex((t) => t.deviceId === d.deviceId),
);
setAudioDevices(
uniqueDevices.map((d) => ({ label: d.label, value: d.deviceId })),
);
})
.catch(() =>
toast.error({
message: t('error.audioDeviceFetchError', {
postProcess: 'sentenceCase',
}),
}),
);
} else if (playbackType === PlayerType.LOCAL && mpvPlayer) {
try {
const devices = await getMpvAudioDevices();
const uniqueDevices = devices.filter(
(d, index, self) => index === self.findIndex((t) => t.value === d.value),
);
setAudioDevices(uniqueDevices);
} catch {
toast.error({
message: t('error.audioDeviceFetchError', {
postProcess: 'sentenceCase',
}),
});
}
}
};
fetchAudioDevices();
}, [playbackType]);
return audioDevices;
};
export const AudioSettings = memo(() => {
const { t } = useTranslation();
const settings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions();
const status = usePlayerStatus();
const audioDevices = useAudioDevices();
const audioOptions: SettingOption[] = [
{
control: (
<Select
data={[
{
disabled: !isElectron(),
label: 'MPV',
value: PlayerType.LOCAL,
},
{ label: 'Web', value: PlayerType.WEB },
]}
defaultValue={settings.type}
disabled={status === PlayerStatus.PLAYING}
onChange={(e) => {
setSettings({ playback: { type: e as PlayerType } });
ipc?.send('settings-set', { property: 'playbackType', value: e });
}}
/>
),
description: t('setting.audioPlayer', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
note:
status === PlayerStatus.PLAYING
? t('common.playerMustBePaused', { postProcess: 'sentenceCase' })
: undefined,
title: t('setting.audioPlayer', { postProcess: 'sentenceCase' }),
},
{
control: (
<Select
clearable
data={audioDevices}
defaultValue={settings.audioDeviceId}
disabled={!isElectron()}
onChange={(e) => setSettings({ playback: { audioDeviceId: e } })}
/>
),
description: t('setting.audioDevice', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.audioDevice', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
defaultChecked={settings.webAudio}
onChange={(e) => {
setSettings({
playback: { webAudio: e.currentTarget.checked },
});
}}
/>
),
description: t('setting.webAudio', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: settings.type !== PlayerType.WEB,
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
title: t('setting.webAudio', {
postProcess: 'sentenceCase',
}),
},
{
control: (
<Switch
defaultChecked={settings.preservePitch}
onChange={(e) => {
setSettings({
playback: { preservePitch: e.currentTarget.checked },
});
}}
/>
),
description: t('setting.preservePitch', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: settings.type !== PlayerType.WEB,
title: t('setting.preservePitch', {
postProcess: 'sentenceCase',
}),
},
{
control: (
<Switch
defaultChecked={settings.audioFadeOnStatusChange}
onChange={(e) => {
setSettings({
playback: {
audioFadeOnStatusChange: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.audioFadeOnStatusChange', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.audioFadeOnStatusChange', {
postProcess: 'sentenceCase',
}),
},
];
return (
<SettingsSection
options={audioOptions}
title={t('page.setting.audio', { postProcess: 'sentenceCase' })}
/>
);
});