prevent duplicate audio device in selector (#1598)

This commit is contained in:
jeffvli
2026-01-22 00:33:30 -08:00
parent 3405f853e3
commit 0c1537e5ef
2 changed files with 74 additions and 82 deletions
@@ -1,10 +1,10 @@
import isElectron from 'is-electron';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAudioDevices } from '/@/renderer/features/settings/components/playback/audio-settings';
import { ListConfigTable } from '/@/renderer/features/shared/components/list-config-menu';
import {
usePlaybackType,
usePlayerActions,
usePlayerProperties,
usePlayerSongProperties,
@@ -25,29 +25,9 @@ import { SegmentedControl } from '/@/shared/components/segmented-control/segment
import { Select } from '/@/shared/components/select/select';
import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch';
import { toast } from '/@/shared/components/toast/toast';
import { CrossfadeStyle, PlayerStatus, PlayerStyle, PlayerType } from '/@/shared/types/types';
const ipc = isElectron() ? window.api.ipc : null;
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
const getAudioDevice = async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
};
const getMpvAudioDevices = async () => {
if (!mpvPlayer) {
return [];
}
try {
return await mpvPlayer.getAudioDevices();
} catch (error) {
console.error('Failed to get MPV audio devices:', error);
return [];
}
};
export const PlayerConfig = () => {
const { t } = useTranslation();
@@ -252,47 +232,11 @@ const AudioPlayerTypeConfig = () => {
};
const AudioDeviceConfig = () => {
const { t } = useTranslation();
const status = usePlayerStatus();
const playbackType = usePlaybackType();
const playbackSettings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions();
const [audioDevices, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
useEffect(() => {
const fetchAudioDevices = async () => {
if (!isElectron()) {
return;
}
if (playbackType === PlayerType.WEB) {
getAudioDevice()
.then((dev) =>
setAudioDevices(dev.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();
setAudioDevices(devices);
} catch {
toast.error({
message: t('error.audioDeviceFetchError', {
postProcess: 'sentenceCase',
}),
});
}
}
};
fetchAudioDevices();
}, [playbackType, t]);
const audioDevices = useAudioDevices();
return (
<Select
@@ -1,3 +1,4 @@
import { t } from 'i18next';
import isElectron from 'is-electron';
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -6,7 +7,7 @@ import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { usePlayerStatus } from '/@/renderer/store';
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';
@@ -14,37 +15,85 @@ 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 getAudioDevice = async () => {
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, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
useEffect(() => {
const getAudioDevices = () => {
getAudioDevice()
.then((dev) =>
setAudioDevices(dev.map((d) => ({ label: d.label, value: d.deviceId }))),
)
.catch(() =>
toast.error({
message: t('error.audioDeviceFetchError', { postProcess: 'sentenceCase' }),
}),
);
};
if (settings.type === PlayerType.WEB) {
getAudioDevices();
}
}, [settings.type, t]);
const audioDevices = useAudioDevices();
const audioOptions: SettingOption[] = [
{
@@ -83,7 +132,7 @@ export const AudioSettings = memo(() => {
clearable
data={audioDevices}
defaultValue={settings.audioDeviceId}
disabled={settings.type !== PlayerType.WEB}
disabled={!isElectron()}
onChange={(e) => setSettings({ playback: { audioDeviceId: e } })}
/>
),
@@ -91,7 +140,6 @@ export const AudioSettings = memo(() => {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron() || settings.type !== PlayerType.WEB,
title: t('setting.audioDevice', { postProcess: 'sentenceCase' }),
},
{