mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
add audio device selection for mpv
This commit is contained in:
@@ -525,6 +525,63 @@ ipcMain.handle(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
'player-get-audio-devices',
|
||||||
|
async (): Promise<{ label: string; value: string }[]> => {
|
||||||
|
try {
|
||||||
|
const instance = getMpvInstance();
|
||||||
|
let tempInstance: MpvAPI | null = null;
|
||||||
|
let mpvToUse: MpvAPI | null = null;
|
||||||
|
|
||||||
|
if (instance && instance.isRunning()) {
|
||||||
|
mpvToUse = instance;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
tempInstance = await createMpv({});
|
||||||
|
mpvToUse = tempInstance;
|
||||||
|
} catch (err: any | NodeMpvError) {
|
||||||
|
mpvLog(
|
||||||
|
{ action: 'Failed to create temporary MPV instance for audio device list' },
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deviceList = await mpvToUse.getProperty('audio-device-list');
|
||||||
|
|
||||||
|
if (!deviceList || !Array.isArray(deviceList)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const devices = deviceList.map((device: any) => {
|
||||||
|
const name = device.name || device.description || 'Unknown Device';
|
||||||
|
const description = device.description || '';
|
||||||
|
const label = description ? `${name} (${description})` : name;
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value: name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return devices;
|
||||||
|
} finally {
|
||||||
|
if (tempInstance && tempInstance !== instance) {
|
||||||
|
try {
|
||||||
|
await quit(tempInstance);
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any | NodeMpvError) {
|
||||||
|
mpvLog({ action: 'Failed to get audio devices' }, err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
enum MpvState {
|
enum MpvState {
|
||||||
STARTED,
|
STARTED,
|
||||||
IN_PROGRESS,
|
IN_PROGRESS,
|
||||||
|
|||||||
@@ -98,6 +98,10 @@ const getStreamMetadata = async () => {
|
|||||||
return ipcRenderer.invoke('player-stream-metadata');
|
return ipcRenderer.invoke('player-stream-metadata');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAudioDevices = async () => {
|
||||||
|
return ipcRenderer.invoke('player-get-audio-devices');
|
||||||
|
};
|
||||||
|
|
||||||
const rendererAutoNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
const rendererAutoNext = (cb: (event: IpcRendererEvent, data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-auto-next', cb);
|
ipcRenderer.on('renderer-player-auto-next', cb);
|
||||||
};
|
};
|
||||||
@@ -174,6 +178,7 @@ export const mpvPlayer = {
|
|||||||
autoNext,
|
autoNext,
|
||||||
cleanup,
|
cleanup,
|
||||||
currentTime,
|
currentTime,
|
||||||
|
getAudioDevices,
|
||||||
getCurrentTime,
|
getCurrentTime,
|
||||||
getMetadata,
|
getMetadata,
|
||||||
getStreamMetadata,
|
getStreamMetadata,
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
|||||||
const hasPopulatedQueueRef = useRef<boolean>(false);
|
const hasPopulatedQueueRef = useRef<boolean>(false);
|
||||||
const isMountedRef = useRef<boolean>(true);
|
const isMountedRef = useRef<boolean>(true);
|
||||||
|
|
||||||
const { transcode } = usePlaybackSettings();
|
const { audioDeviceId, transcode } = usePlaybackSettings();
|
||||||
const mpvExtraParameters = useSettingsStore((store) => store.playback.mpvExtraParameters);
|
const mpvExtraParameters = useSettingsStore((store) => store.playback.mpvExtraParameters);
|
||||||
const mpvProperties = useSettingsStore((store) => store.playback.mpvProperties);
|
const mpvProperties = useSettingsStore((store) => store.playback.mpvProperties);
|
||||||
const [reloadTrigger, setReloadTrigger] = useState(0);
|
const [reloadTrigger, setReloadTrigger] = useState(0);
|
||||||
@@ -106,8 +106,14 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
|||||||
volume: volume,
|
volume: volume,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extraParameters: string[] = [...mpvExtraParameters];
|
||||||
|
|
||||||
|
if (audioDeviceId) {
|
||||||
|
extraParameters.push(`--audio-device=${audioDeviceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
await mpvPlayer?.initialize({
|
await mpvPlayer?.initialize({
|
||||||
extraParameters: mpvExtraParameters,
|
extraParameters,
|
||||||
properties,
|
properties,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,7 +154,7 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
|||||||
// update callbacks in usePlayerEvents.
|
// update callbacks in usePlayerEvents.
|
||||||
// reloadTrigger is included to allow manual reload via MPV_RELOAD event.
|
// reloadTrigger is included to allow manual reload via MPV_RELOAD event.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [mpvExtraParameters, mpvProperties, reloadTrigger]);
|
}, [mpvExtraParameters, mpvProperties, audioDeviceId, reloadTrigger]);
|
||||||
|
|
||||||
// Update volume
|
// Update volume
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -89,8 +89,16 @@ export const AudioPlayers = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Not standard, just used in chromium-based browsers. See
|
// Not standard, just used in chromium-based browsers. See
|
||||||
// https://developer.chrome.com/blog/audiocontext-setsinkid/.
|
// https://developer.chrome.com/blog/audiocontext-setsinkid/.
|
||||||
// If the isElectron() check is every removed, fix this.
|
|
||||||
if (isElectron() && audioContext && 'setSinkId' in audioContext.context && audioDeviceId) {
|
if (!isElectron()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playbackType !== PlayerType.WEB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioContext && 'setSinkId' in audioContext.context && audioDeviceId) {
|
||||||
const setSink = async () => {
|
const setSink = async () => {
|
||||||
try {
|
try {
|
||||||
if (audioContext.context.state !== 'closed') {
|
if (audioContext.context.state !== 'closed') {
|
||||||
@@ -103,7 +111,7 @@ export const AudioPlayers = () => {
|
|||||||
|
|
||||||
setSink();
|
setSink();
|
||||||
}
|
}
|
||||||
}, [audioContext, audioDeviceId]);
|
}, [audioContext, audioDeviceId, playbackType]);
|
||||||
|
|
||||||
// Listen to favorite and rating events to update queue songs
|
// Listen to favorite and rating events to update queue songs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { ListConfigTable } from '/@/renderer/features/shared/components/list-config-menu';
|
import { ListConfigTable } from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
import {
|
import {
|
||||||
|
usePlaybackType,
|
||||||
usePlayerActions,
|
usePlayerActions,
|
||||||
usePlayerData,
|
usePlayerData,
|
||||||
usePlayerProperties,
|
usePlayerProperties,
|
||||||
@@ -35,18 +36,33 @@ import {
|
|||||||
} from '/@/shared/types/types';
|
} from '/@/shared/types/types';
|
||||||
|
|
||||||
const ipc = isElectron() ? window.api.ipc : null;
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
|
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||||
|
|
||||||
const getAudioDevice = async () => {
|
const getAudioDevice = async () => {
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
|
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 = () => {
|
export const PlayerConfig = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentSong } = usePlayerData();
|
const { currentSong } = usePlayerData();
|
||||||
const speed = usePlayerSpeed();
|
const speed = usePlayerSpeed();
|
||||||
const queueType = usePlayerQueueType();
|
const queueType = usePlayerQueueType();
|
||||||
const status = usePlayerStatus();
|
const status = usePlayerStatus();
|
||||||
|
const playbackType = usePlaybackType();
|
||||||
const { crossfadeDuration, crossfadeStyle, transitionType } = usePlayerProperties();
|
const { crossfadeDuration, crossfadeStyle, transitionType } = usePlayerProperties();
|
||||||
const { setCrossfadeDuration, setCrossfadeStyle, setQueueType, setSpeed, setTransitionType } =
|
const { setCrossfadeDuration, setCrossfadeStyle, setQueueType, setSpeed, setTransitionType } =
|
||||||
usePlayerActions();
|
usePlayerActions();
|
||||||
@@ -70,22 +86,39 @@ export const PlayerConfig = () => {
|
|||||||
const [audioDevices, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
|
const [audioDevices, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAudioDevices = () => {
|
const fetchAudioDevices = async () => {
|
||||||
getAudioDevice()
|
if (!isElectron()) {
|
||||||
.then((dev) =>
|
return;
|
||||||
setAudioDevices(dev.map((d) => ({ label: d.label, value: d.deviceId }))),
|
}
|
||||||
)
|
|
||||||
.catch(() =>
|
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({
|
toast.error({
|
||||||
message: t('error.audioDeviceFetchError', { postProcess: 'sentenceCase' }),
|
message: t('error.audioDeviceFetchError', {
|
||||||
}),
|
postProcess: 'sentenceCase',
|
||||||
);
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (playbackSettings.type === PlayerType.WEB) {
|
fetchAudioDevices();
|
||||||
fetchAudioDevices();
|
}, [playbackType, t]);
|
||||||
}
|
|
||||||
}, [playbackSettings.type, t]);
|
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
const formatPlaybackSpeedSliderLabel = (value: number) => {
|
const formatPlaybackSpeedSliderLabel = (value: number) => {
|
||||||
@@ -161,15 +194,15 @@ export const PlayerConfig = () => {
|
|||||||
comboboxProps={{ withinPortal: false }}
|
comboboxProps={{ withinPortal: false }}
|
||||||
data={audioDevices}
|
data={audioDevices}
|
||||||
defaultValue={playbackSettings.audioDeviceId}
|
defaultValue={playbackSettings.audioDeviceId}
|
||||||
disabled={playbackSettings.type !== PlayerType.WEB}
|
disabled={status === PlayerStatus.PLAYING}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setSettings({
|
setSettings({
|
||||||
playback: {
|
playback: {
|
||||||
...playbackSettings,
|
...playbackSettings,
|
||||||
audioDeviceId: e,
|
audioDeviceId: e,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}}
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user