mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-08 13:23:16 +02:00
add MPV reload button to audio player selector
This commit is contained in:
@@ -26,6 +26,21 @@ let mpvInstance: MpvAPI | null = null;
|
||||
let currentPlayerData: null | PlayerData = null;
|
||||
const socketPath = isWindows() ? `\\\\.\\pipe\\mpvserver-${pid}` : `/tmp/node-mpv-${pid}.sock`;
|
||||
|
||||
// While quitting/restarting mpv, playlist-pos goes to -1 and node-mpv emits stopped/paused/
|
||||
// resumed. Those look identical to a real track end and must not reach the renderer — otherwise
|
||||
// handleTrackEnded runs mediaAutoNext while status is STOPPED and flips the UI back to Playing
|
||||
// on the next queue item (e.g. MPV reload after mediaStop).
|
||||
let suppressRendererPlaybackEvents = false;
|
||||
// Bumped on quit so late events from a dying instance are ignored after a new one starts.
|
||||
let playbackEventGeneration = 0;
|
||||
|
||||
const sendRendererPlaybackEvent = (channel: string, ...args: unknown[]) => {
|
||||
if (suppressRendererPlaybackEvents) {
|
||||
return;
|
||||
}
|
||||
getMainWindow()?.webContents.send(channel, ...args);
|
||||
};
|
||||
|
||||
const NodeMpvErrorCode = {
|
||||
0: 'Unable to load file or stream',
|
||||
1: 'Invalid argument',
|
||||
@@ -167,6 +182,16 @@ const createMpv = async (data: {
|
||||
}
|
||||
|
||||
let previousPlaylistPos: number | undefined;
|
||||
const eventGeneration = playbackEventGeneration;
|
||||
|
||||
suppressRendererPlaybackEvents = false;
|
||||
|
||||
const sendIfCurrent = (channel: string, ...args: unknown[]) => {
|
||||
if (eventGeneration !== playbackEventGeneration) {
|
||||
return;
|
||||
}
|
||||
sendRendererPlaybackEvent(channel, ...args);
|
||||
};
|
||||
|
||||
mpv.on('status', (status) => {
|
||||
if (status.property === 'playlist-pos') {
|
||||
@@ -175,7 +200,7 @@ const createMpv = async (data: {
|
||||
// mpv uses playlist-pos = -1 when nothing is playing (ended, cleared, load failure, etc).
|
||||
if (currentPos === -1) {
|
||||
if (previousPlaylistPos === 0) {
|
||||
getMainWindow()?.webContents.send('renderer-player-track-ended');
|
||||
sendIfCurrent('renderer-player-track-ended');
|
||||
}
|
||||
mpv?.pause();
|
||||
previousPlaylistPos = currentPos;
|
||||
@@ -185,7 +210,7 @@ const createMpv = async (data: {
|
||||
// In our 2-item queue model, playlist-pos should normally be 0.
|
||||
// When mpv auto-advances to the next track it becomes > 0 (typically 1).
|
||||
if (typeof currentPos === 'number' && currentPos > 0) {
|
||||
getMainWindow()?.webContents.send('renderer-player-auto-next');
|
||||
sendIfCurrent('renderer-player-auto-next');
|
||||
}
|
||||
|
||||
previousPlaylistPos = currentPos;
|
||||
@@ -194,21 +219,24 @@ const createMpv = async (data: {
|
||||
|
||||
// Automatically updates the play button when the player is playing
|
||||
mpv.on('resumed', () => {
|
||||
getMainWindow()?.webContents.send('renderer-player-play');
|
||||
sendIfCurrent('renderer-player-play');
|
||||
});
|
||||
|
||||
// Automatically updates the play button when the player is stopped
|
||||
mpv.on('stopped', () => {
|
||||
getMainWindow()?.webContents.send('renderer-player-stop');
|
||||
sendIfCurrent('renderer-player-stop');
|
||||
});
|
||||
|
||||
// Automatically updates the play button when the player is paused
|
||||
mpv.on('paused', () => {
|
||||
getMainWindow()?.webContents.send('renderer-player-pause');
|
||||
sendIfCurrent('renderer-player-pause');
|
||||
});
|
||||
|
||||
// Event output every interval set by time_update, used to update the current time
|
||||
mpv.on('timeposition', (time: number) => {
|
||||
if (eventGeneration !== playbackEventGeneration) {
|
||||
return;
|
||||
}
|
||||
getMainWindow()?.webContents.send('renderer-player-current-time', time);
|
||||
});
|
||||
|
||||
@@ -235,6 +263,8 @@ const killMpvProcess = (mpv: MpvAPI) => {
|
||||
const quit = async (instance?: MpvAPI | null) => {
|
||||
const mpv = instance || getMpvInstance();
|
||||
if (mpv) {
|
||||
suppressRendererPlaybackEvents = true;
|
||||
playbackEventGeneration += 1;
|
||||
try {
|
||||
// mpv.quit() resolves only when mpv replies over IPC. If mpv's command queue
|
||||
// is wedged (e.g. blocked on a dead network stream after the system resumes
|
||||
@@ -302,6 +332,8 @@ ipcMain.handle(
|
||||
});
|
||||
|
||||
// Clean up previous mpv instance
|
||||
suppressRendererPlaybackEvents = true;
|
||||
playbackEventGeneration += 1;
|
||||
getMpvInstance()?.stop();
|
||||
getMpvInstance()
|
||||
?.quit()
|
||||
@@ -338,6 +370,9 @@ ipcMain.handle(
|
||||
);
|
||||
|
||||
ipcMain.on('player-quit', async () => {
|
||||
// stop() also drives playlist-pos to -1; suppress before that so reload does not look like a track end.
|
||||
suppressRendererPlaybackEvents = true;
|
||||
playbackEventGeneration += 1;
|
||||
try {
|
||||
await getMpvInstance()?.stop();
|
||||
await quit();
|
||||
@@ -440,6 +475,13 @@ ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, p
|
||||
}
|
||||
}
|
||||
|
||||
// When pause is requested (e.g. preload after reload while UI is STOPPED/PAUSED), mpv still
|
||||
// briefly resumes on load. Suppress those events so they do not overwrite renderer status.
|
||||
const shouldSuppressLoadEvents = pause === true;
|
||||
if (shouldSuppressLoadEvents) {
|
||||
suppressRendererPlaybackEvents = true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (current) {
|
||||
try {
|
||||
@@ -462,6 +504,10 @@ ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, p
|
||||
}
|
||||
} catch (err: any | NodeMpvError) {
|
||||
mpvLog({ action: `Failed to set play queue` }, err);
|
||||
} finally {
|
||||
if (shouldSuppressLoadEvents) {
|
||||
suppressRendererPlaybackEvents = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -147,7 +147,9 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
||||
: undefined;
|
||||
|
||||
if (currentSongUrl && nextSongUrl && !hasPopulatedQueueRef.current && mpvPlayer) {
|
||||
mpvPlayer.setQueue(currentSongUrl, nextSongUrl, true);
|
||||
const shouldPause =
|
||||
usePlayerStore.getState().player.status !== PlayerStatus.PLAYING;
|
||||
mpvPlayer.setQueue(currentSongUrl, nextSongUrl, shouldPause);
|
||||
hasPopulatedQueueRef.current = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import isElectron from 'is-electron';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import {
|
||||
getDefaultAudioDevice,
|
||||
useAudioDevices,
|
||||
@@ -241,11 +243,16 @@ export const PlayerConfig = () => {
|
||||
};
|
||||
|
||||
const AudioPlayerTypeConfig = () => {
|
||||
const { t } = useTranslation();
|
||||
const status = usePlayerStatus();
|
||||
const playbackSettings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const { mediaStop } = usePlayer();
|
||||
|
||||
const showRefreshButton = playbackSettings.type === PlayerType.LOCAL;
|
||||
|
||||
return (
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Select
|
||||
comboboxProps={{ withinPortal: false }}
|
||||
data={[
|
||||
@@ -271,6 +278,19 @@ const AudioPlayerTypeConfig = () => {
|
||||
variant="filled"
|
||||
width="100%"
|
||||
/>
|
||||
{showRefreshButton && (
|
||||
<ActionIcon
|
||||
icon="refresh"
|
||||
iconProps={{ size: 'md' }}
|
||||
onClick={() => {
|
||||
mediaStop();
|
||||
eventEmitter.emit('MPV_RELOAD', {});
|
||||
}}
|
||||
tooltip={{ label: t('common.reload') }}
|
||||
variant="transparent"
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -291,6 +311,7 @@ const AudioDeviceConfig = () => {
|
||||
comboboxProps={{ withinPortal: false }}
|
||||
data={audioDevices}
|
||||
disabled={status === PlayerStatus.PLAYING}
|
||||
key={playbackType}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
playback: {
|
||||
|
||||
@@ -3,14 +3,22 @@ import isElectron from 'is-electron';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import {
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { useCurrentServer, usePlaybackType, usePlayerStatus } from '/@/renderer/store';
|
||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { useCurrentServer, usePlayerStatus } from '/@/renderer/store';
|
||||
import {
|
||||
usePlaybackSettings,
|
||||
usePlaybackType,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { logger } from '/@/renderer/utils/logger';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
@@ -100,10 +108,12 @@ export const AudioSettings = memo(() => {
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const status = usePlayerStatus();
|
||||
const playbackType = usePlaybackType();
|
||||
const { mediaStop } = usePlayer();
|
||||
|
||||
// Cleaned up server feature logic via requested hooks/utilities
|
||||
const currentServer = useCurrentServer();
|
||||
const isJukeboxSupported = hasFeature(currentServer, ServerFeature.JUKEBOX);
|
||||
const showRefreshButton = settings.type === PlayerType.LOCAL;
|
||||
|
||||
const audioDevices = useAudioDevices(playbackType);
|
||||
const audioDeviceId =
|
||||
@@ -126,6 +136,7 @@ export const AudioSettings = memo(() => {
|
||||
const audioOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Select
|
||||
data={selectData}
|
||||
defaultValue={settings.type}
|
||||
@@ -135,6 +146,19 @@ export const AudioSettings = memo(() => {
|
||||
ipc?.send('settings-set', { property: 'playbackType', value: e });
|
||||
}}
|
||||
/>
|
||||
{showRefreshButton && (
|
||||
<ActionIcon
|
||||
icon="refresh"
|
||||
iconProps={{ size: 'md' }}
|
||||
onClick={() => {
|
||||
mediaStop();
|
||||
eventEmitter.emit('MPV_RELOAD', {});
|
||||
}}
|
||||
tooltip={{ label: t('common.reload') }}
|
||||
variant="transparent"
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
),
|
||||
description: t('setting.audioPlayer', { context: 'description' }),
|
||||
isHidden: !isElectron() && !isJukeboxSupported,
|
||||
|
||||
Reference in New Issue
Block a user