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;
|
let currentPlayerData: null | PlayerData = null;
|
||||||
const socketPath = isWindows() ? `\\\\.\\pipe\\mpvserver-${pid}` : `/tmp/node-mpv-${pid}.sock`;
|
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 = {
|
const NodeMpvErrorCode = {
|
||||||
0: 'Unable to load file or stream',
|
0: 'Unable to load file or stream',
|
||||||
1: 'Invalid argument',
|
1: 'Invalid argument',
|
||||||
@@ -167,6 +182,16 @@ const createMpv = async (data: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let previousPlaylistPos: number | undefined;
|
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) => {
|
mpv.on('status', (status) => {
|
||||||
if (status.property === 'playlist-pos') {
|
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).
|
// mpv uses playlist-pos = -1 when nothing is playing (ended, cleared, load failure, etc).
|
||||||
if (currentPos === -1) {
|
if (currentPos === -1) {
|
||||||
if (previousPlaylistPos === 0) {
|
if (previousPlaylistPos === 0) {
|
||||||
getMainWindow()?.webContents.send('renderer-player-track-ended');
|
sendIfCurrent('renderer-player-track-ended');
|
||||||
}
|
}
|
||||||
mpv?.pause();
|
mpv?.pause();
|
||||||
previousPlaylistPos = currentPos;
|
previousPlaylistPos = currentPos;
|
||||||
@@ -185,7 +210,7 @@ const createMpv = async (data: {
|
|||||||
// In our 2-item queue model, playlist-pos should normally be 0.
|
// 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).
|
// When mpv auto-advances to the next track it becomes > 0 (typically 1).
|
||||||
if (typeof currentPos === 'number' && currentPos > 0) {
|
if (typeof currentPos === 'number' && currentPos > 0) {
|
||||||
getMainWindow()?.webContents.send('renderer-player-auto-next');
|
sendIfCurrent('renderer-player-auto-next');
|
||||||
}
|
}
|
||||||
|
|
||||||
previousPlaylistPos = currentPos;
|
previousPlaylistPos = currentPos;
|
||||||
@@ -194,21 +219,24 @@ const createMpv = async (data: {
|
|||||||
|
|
||||||
// Automatically updates the play button when the player is playing
|
// Automatically updates the play button when the player is playing
|
||||||
mpv.on('resumed', () => {
|
mpv.on('resumed', () => {
|
||||||
getMainWindow()?.webContents.send('renderer-player-play');
|
sendIfCurrent('renderer-player-play');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Automatically updates the play button when the player is stopped
|
// Automatically updates the play button when the player is stopped
|
||||||
mpv.on('stopped', () => {
|
mpv.on('stopped', () => {
|
||||||
getMainWindow()?.webContents.send('renderer-player-stop');
|
sendIfCurrent('renderer-player-stop');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Automatically updates the play button when the player is paused
|
// Automatically updates the play button when the player is paused
|
||||||
mpv.on('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
|
// Event output every interval set by time_update, used to update the current time
|
||||||
mpv.on('timeposition', (time: number) => {
|
mpv.on('timeposition', (time: number) => {
|
||||||
|
if (eventGeneration !== playbackEventGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
getMainWindow()?.webContents.send('renderer-player-current-time', time);
|
getMainWindow()?.webContents.send('renderer-player-current-time', time);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -235,6 +263,8 @@ const killMpvProcess = (mpv: MpvAPI) => {
|
|||||||
const quit = async (instance?: MpvAPI | null) => {
|
const quit = async (instance?: MpvAPI | null) => {
|
||||||
const mpv = instance || getMpvInstance();
|
const mpv = instance || getMpvInstance();
|
||||||
if (mpv) {
|
if (mpv) {
|
||||||
|
suppressRendererPlaybackEvents = true;
|
||||||
|
playbackEventGeneration += 1;
|
||||||
try {
|
try {
|
||||||
// mpv.quit() resolves only when mpv replies over IPC. If mpv's command queue
|
// 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
|
// 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
|
// Clean up previous mpv instance
|
||||||
|
suppressRendererPlaybackEvents = true;
|
||||||
|
playbackEventGeneration += 1;
|
||||||
getMpvInstance()?.stop();
|
getMpvInstance()?.stop();
|
||||||
getMpvInstance()
|
getMpvInstance()
|
||||||
?.quit()
|
?.quit()
|
||||||
@@ -338,6 +370,9 @@ ipcMain.handle(
|
|||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.on('player-quit', async () => {
|
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 {
|
try {
|
||||||
await getMpvInstance()?.stop();
|
await getMpvInstance()?.stop();
|
||||||
await quit();
|
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 {
|
try {
|
||||||
if (current) {
|
if (current) {
|
||||||
try {
|
try {
|
||||||
@@ -462,6 +504,10 @@ ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, p
|
|||||||
}
|
}
|
||||||
} catch (err: any | NodeMpvError) {
|
} catch (err: any | NodeMpvError) {
|
||||||
mpvLog({ action: `Failed to set play queue` }, err);
|
mpvLog({ action: `Failed to set play queue` }, err);
|
||||||
|
} finally {
|
||||||
|
if (shouldSuppressLoadEvents) {
|
||||||
|
suppressRendererPlaybackEvents = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,9 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (currentSongUrl && nextSongUrl && !hasPopulatedQueueRef.current && mpvPlayer) {
|
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;
|
hasPopulatedQueueRef.current = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import isElectron from 'is-electron';
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import {
|
import {
|
||||||
getDefaultAudioDevice,
|
getDefaultAudioDevice,
|
||||||
useAudioDevices,
|
useAudioDevices,
|
||||||
@@ -241,36 +243,54 @@ export const PlayerConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AudioPlayerTypeConfig = () => {
|
const AudioPlayerTypeConfig = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const status = usePlayerStatus();
|
const status = usePlayerStatus();
|
||||||
const playbackSettings = usePlaybackSettings();
|
const playbackSettings = usePlaybackSettings();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
const { mediaStop } = usePlayer();
|
||||||
|
|
||||||
|
const showRefreshButton = playbackSettings.type === PlayerType.LOCAL;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Group gap="xs" wrap="nowrap">
|
||||||
comboboxProps={{ withinPortal: false }}
|
<Select
|
||||||
data={[
|
comboboxProps={{ withinPortal: false }}
|
||||||
{
|
data={[
|
||||||
disabled: !isElectron(),
|
{
|
||||||
label: 'MPV',
|
disabled: !isElectron(),
|
||||||
value: PlayerType.LOCAL,
|
label: 'MPV',
|
||||||
},
|
value: PlayerType.LOCAL,
|
||||||
{ label: 'Web', value: PlayerType.WEB },
|
},
|
||||||
{ label: 'Jukebox', value: PlayerType.JUKEBOX },
|
{ label: 'Web', value: PlayerType.WEB },
|
||||||
]}
|
{ label: 'Jukebox', value: PlayerType.JUKEBOX },
|
||||||
defaultValue={playbackSettings.type}
|
]}
|
||||||
disabled={status === PlayerStatus.PLAYING}
|
defaultValue={playbackSettings.type}
|
||||||
onChange={(e) => {
|
disabled={status === PlayerStatus.PLAYING}
|
||||||
setSettings({
|
onChange={(e) => {
|
||||||
playback: { ...playbackSettings, type: e as PlayerType },
|
setSettings({
|
||||||
});
|
playback: { ...playbackSettings, type: e as PlayerType },
|
||||||
ipc?.send('settings-set', {
|
});
|
||||||
property: 'playbackType',
|
ipc?.send('settings-set', {
|
||||||
value: e,
|
property: 'playbackType',
|
||||||
});
|
value: e,
|
||||||
}}
|
});
|
||||||
variant="filled"
|
}}
|
||||||
width="100%"
|
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 }}
|
comboboxProps={{ withinPortal: false }}
|
||||||
data={audioDevices}
|
data={audioDevices}
|
||||||
disabled={status === PlayerStatus.PLAYING}
|
disabled={status === PlayerStatus.PLAYING}
|
||||||
|
key={playbackType}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSettings({
|
setSettings({
|
||||||
playback: {
|
playback: {
|
||||||
|
|||||||
@@ -3,14 +3,22 @@ import isElectron from 'is-electron';
|
|||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
|
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import {
|
import {
|
||||||
SettingOption,
|
SettingOption,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from '/@/renderer/features/settings/components/settings-section';
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
import { useCurrentServer, usePlaybackType, usePlayerStatus } from '/@/renderer/store';
|
import { useCurrentServer, usePlayerStatus } from '/@/renderer/store';
|
||||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import {
|
||||||
|
usePlaybackSettings,
|
||||||
|
usePlaybackType,
|
||||||
|
useSettingsStoreActions,
|
||||||
|
} from '/@/renderer/store/settings.store';
|
||||||
import { logger } from '/@/renderer/utils/logger';
|
import { logger } from '/@/renderer/utils/logger';
|
||||||
import { hasFeature } from '/@/shared/api/utils';
|
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 { Select } from '/@/shared/components/select/select';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
@@ -100,10 +108,12 @@ export const AudioSettings = memo(() => {
|
|||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
const status = usePlayerStatus();
|
const status = usePlayerStatus();
|
||||||
const playbackType = usePlaybackType();
|
const playbackType = usePlaybackType();
|
||||||
|
const { mediaStop } = usePlayer();
|
||||||
|
|
||||||
// Cleaned up server feature logic via requested hooks/utilities
|
// Cleaned up server feature logic via requested hooks/utilities
|
||||||
const currentServer = useCurrentServer();
|
const currentServer = useCurrentServer();
|
||||||
const isJukeboxSupported = hasFeature(currentServer, ServerFeature.JUKEBOX);
|
const isJukeboxSupported = hasFeature(currentServer, ServerFeature.JUKEBOX);
|
||||||
|
const showRefreshButton = settings.type === PlayerType.LOCAL;
|
||||||
|
|
||||||
const audioDevices = useAudioDevices(playbackType);
|
const audioDevices = useAudioDevices(playbackType);
|
||||||
const audioDeviceId =
|
const audioDeviceId =
|
||||||
@@ -126,15 +136,29 @@ export const AudioSettings = memo(() => {
|
|||||||
const audioOptions: SettingOption[] = [
|
const audioOptions: SettingOption[] = [
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Select
|
<Group gap="xs" wrap="nowrap">
|
||||||
data={selectData}
|
<Select
|
||||||
defaultValue={settings.type}
|
data={selectData}
|
||||||
disabled={status === PlayerStatus.PLAYING}
|
defaultValue={settings.type}
|
||||||
onChange={(e) => {
|
disabled={status === PlayerStatus.PLAYING}
|
||||||
setSettings({ playback: { type: e as PlayerType } });
|
onChange={(e) => {
|
||||||
ipc?.send('settings-set', { property: 'playbackType', value: e });
|
setSettings({ playback: { type: e as PlayerType } });
|
||||||
}}
|
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' }),
|
description: t('setting.audioPlayer', { context: 'description' }),
|
||||||
isHidden: !isElectron() && !isJukeboxSupported,
|
isHidden: !isElectron() && !isJukeboxSupported,
|
||||||
|
|||||||
Reference in New Issue
Block a user