[enhancement]: Support disabling MPV entirely

Supports running Feishin solely using web audio (useful for clients with problems with MPV).
Also moves save/restore queue to utils, as MPV object is now optional
This commit is contained in:
Kendall Garner 2024-02-11 13:56:29 -08:00
parent ae8fc6df13
commit f82da2e76b
No known key found for this signature in database
GPG key ID: 18D2767419676C87
10 changed files with 127 additions and 74 deletions

View file

@ -428,6 +428,8 @@
"customFontPath_description": "sets the path to the custom font to use for the application", "customFontPath_description": "sets the path to the custom font to use for the application",
"disableAutomaticUpdates": "disable automatic updates", "disableAutomaticUpdates": "disable automatic updates",
"disableLibraryUpdateOnStartup": "disable checking for new versions on startup", "disableLibraryUpdateOnStartup": "disable checking for new versions on startup",
"disableMpv": "Disable MPV",
"disableMpv_description": "If checked, prevent MPV from starting and bypass MPV requirement.",
"discordApplicationId": "{{discord}} application id", "discordApplicationId": "{{discord}} application id",
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})", "discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
"discordIdleStatus": "show rich presence idle status", "discordIdleStatus": "show rich presence idle status",

View file

@ -316,7 +316,7 @@ const createWindow = async () => {
} }
const queue = JSON.parse(data.toString()); const queue = JSON.parse(data.toString());
getMainWindow()?.webContents.send('renderer-player-restore-queue', queue); getMainWindow()?.webContents.send('renderer-restore-queue', queue);
}); });
}); });
}); });
@ -362,7 +362,7 @@ const createWindow = async () => {
event.preventDefault(); event.preventDefault();
saved = true; saved = true;
getMainWindow()?.webContents.send('renderer-player-save-queue'); getMainWindow()?.webContents.send('renderer-save-queue');
ipcMain.once('player-save-queue', async (_event, data: Record<string, any>) => { ipcMain.once('player-save-queue', async (_event, data: Record<string, any>) => {
const queueLocation = join(app.getPath('userData'), 'queue'); const queueLocation = join(app.getPath('userData'), 'queue');

View file

@ -9,6 +9,8 @@ import { mpvPlayer, mpvPlayerListener } from './preload/mpv-player';
import { remote } from './preload/remote'; import { remote } from './preload/remote';
import { utils } from './preload/utils'; import { utils } from './preload/utils';
const disableMpv = localSettings.get('disable_mpv');
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electron', {
browser, browser,
discordRpc, discordRpc,
@ -16,8 +18,8 @@ contextBridge.exposeInMainWorld('electron', {
localSettings, localSettings,
lyrics, lyrics,
mpris, mpris,
mpvPlayer, mpvPlayer: disableMpv ? undefined : mpvPlayer,
mpvPlayerListener, mpvPlayerListener: disableMpv ? undefined : mpvPlayerListener,
remote, remote,
utils, utils,
}); });

View file

@ -1,5 +1,5 @@
import { ipcRenderer, IpcRendererEvent } from 'electron'; import { ipcRenderer, IpcRendererEvent } from 'electron';
import { PlayerData, PlayerState } from '/@/renderer/store'; import { PlayerData } from '/@/renderer/store';
const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => { const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
return ipcRenderer.invoke('player-initialize', data); return ipcRenderer.invoke('player-initialize', data);
@ -50,14 +50,6 @@ const previous = () => {
ipcRenderer.send('player-previous'); ipcRenderer.send('player-previous');
}; };
const restoreQueue = () => {
ipcRenderer.send('player-restore-queue');
};
const saveQueue = (data: Record<string, any>) => {
ipcRenderer.send('player-save-queue', data);
};
const seek = (seconds: number) => { const seek = (seconds: number) => {
ipcRenderer.send('player-seek', seconds); ipcRenderer.send('player-seek', seconds);
}; };
@ -154,16 +146,6 @@ const rendererQuit = (cb: (event: IpcRendererEvent) => void) => {
ipcRenderer.on('renderer-player-quit', cb); ipcRenderer.on('renderer-player-quit', cb);
}; };
const rendererSaveQueue = (cb: (event: IpcRendererEvent) => void) => {
ipcRenderer.on('renderer-player-save-queue', cb);
};
const rendererRestoreQueue = (
cb: (event: IpcRendererEvent, data: Partial<PlayerState>) => void,
) => {
ipcRenderer.on('renderer-player-restore-queue', cb);
};
const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => { const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
ipcRenderer.on('renderer-player-error', cb); ipcRenderer.on('renderer-player-error', cb);
}; };
@ -182,8 +164,6 @@ export const mpvPlayer = {
previous, previous,
quit, quit,
restart, restart,
restoreQueue,
saveQueue,
seek, seek,
seekTo, seekTo,
setProperties, setProperties,
@ -203,8 +183,6 @@ export const mpvPlayerListener = {
rendererPlayPause, rendererPlayPause,
rendererPrevious, rendererPrevious,
rendererQuit, rendererQuit,
rendererRestoreQueue,
rendererSaveQueue,
rendererSkipBackward, rendererSkipBackward,
rendererSkipForward, rendererSkipForward,
rendererStop, rendererStop,

View file

@ -1,9 +1,31 @@
import { IpcRendererEvent, ipcRenderer } from 'electron';
import { isMacOS, isWindows, isLinux } from '../utils'; import { isMacOS, isWindows, isLinux } from '../utils';
import { PlayerState } from '/@/renderer/store';
const saveQueue = (data: Record<string, any>) => {
ipcRenderer.send('player-save-queue', data);
};
const restoreQueue = () => {
ipcRenderer.send('player-restore-queue');
};
const onSaveQueue = (cb: (event: IpcRendererEvent) => void) => {
ipcRenderer.on('renderer-save-queue', cb);
};
const onRestoreQueue = (cb: (event: IpcRendererEvent, data: Partial<PlayerState>) => void) => {
ipcRenderer.on('renderer-restore-queue', cb);
};
export const utils = { export const utils = {
isLinux, isLinux,
isMacOS, isMacOS,
isWindows, isWindows,
onRestoreQueue,
onSaveQueue,
restoreQueue,
saveQueue,
}; };
export type Utils = typeof utils; export type Utils = typeof utils;

View file

@ -33,9 +33,9 @@ ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule
initSimpleImg({ threshold: 0.05 }, true); initSimpleImg({ threshold: 0.05 }, true);
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
const ipc = isElectron() ? window.electron.ipc : null; const ipc = isElectron() ? window.electron.ipc : null;
const remote = isElectron() ? window.electron.remote : null; const remote = isElectron() ? window.electron.remote : null;
const utils = isElectron() ? window.electron.utils : null;
export const App = () => { export const App = () => {
const theme = useTheme(); const theme = useTheme();
@ -97,6 +97,7 @@ export const App = () => {
// Start the mpv instance on startup // Start the mpv instance on startup
useEffect(() => { useEffect(() => {
const initializeMpv = async () => { const initializeMpv = async () => {
if (playbackType === PlaybackType.LOCAL) {
const isRunning: boolean | undefined = await mpvPlayer?.isRunning(); const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
mpvPlayer?.stop(); mpvPlayer?.stop();
@ -115,10 +116,12 @@ export const App = () => {
mpvPlayer?.volume(properties.volume); mpvPlayer?.volume(properties.volume);
} }
mpvPlayer?.restoreQueue(); }
utils?.restoreQueue();
}; };
if (isElectron() && playbackType === PlaybackType.LOCAL) { if (isElectron()) {
initializeMpv(); initializeMpv();
} }
@ -136,8 +139,8 @@ export const App = () => {
}, [bindings]); }, [bindings]);
useEffect(() => { useEffect(() => {
if (isElectron()) { if (utils) {
mpvPlayerListener!.rendererSaveQueue(() => { utils.onSaveQueue(() => {
const { current, queue } = usePlayerStore.getState(); const { current, queue } = usePlayerStore.getState();
const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = { const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = {
current: { current: {
@ -146,10 +149,10 @@ export const App = () => {
}, },
queue, queue,
}; };
mpvPlayer!.saveQueue(stateToSave); utils.saveQueue(stateToSave);
}); });
mpvPlayerListener!.rendererRestoreQueue((_event: any, data) => { utils.onRestoreQueue((_event: any, data) => {
const playerData = restoreQueue(data); const playerData = restoreQueue(data);
if (playbackType === PlaybackType.LOCAL) { if (playbackType === PlaybackType.LOCAL) {
mpvPlayer!.setQueue(playerData, true); mpvPlayer!.setQueue(playerData, true);
@ -158,8 +161,8 @@ export const App = () => {
} }
return () => { return () => {
ipc?.removeAllListeners('renderer-player-restore-queue'); ipc?.removeAllListeners('renderer-restore-queue');
ipc?.removeAllListeners('renderer-player-save-queue'); ipc?.removeAllListeners('renderer-save-queue');
}; };
}, [playbackType, restoreQueue]); }, [playbackType, restoreQueue]);

View file

@ -1,23 +1,36 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { FileInput, Text, Button } from '/@/renderer/components'; import { FileInput, Text, Button, Checkbox } from '/@/renderer/components';
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store';
import { PlaybackType } from '/@/renderer/types';
import { useTranslation } from 'react-i18next';
const localSettings = isElectron() ? window.electron.localSettings : null; const localSettings = isElectron() ? window.electron.localSettings : null;
export const MpvRequired = () => { export const MpvRequired = () => {
const [mpvPath, setMpvPath] = useState(''); const [mpvPath, setMpvPath] = useState('');
const settings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions();
const [disabled, setDisabled] = useState(false);
const { t } = useTranslation();
const handleSetMpvPath = (e: File) => { const handleSetMpvPath = (e: File) => {
localSettings?.set('mpv_path', e.path); localSettings?.set('mpv_path', e.path);
}; };
const handleSetDisableMpv = (disabled: boolean) => {
setDisabled(disabled);
localSettings?.set('disable_mpv', disabled);
setSettings({
playback: { ...settings, type: disabled ? PlaybackType.WEB : PlaybackType.LOCAL },
});
};
useEffect(() => { useEffect(() => {
const getMpvPath = async () => {
if (!localSettings) return setMpvPath(''); if (!localSettings) return setMpvPath('');
const mpvPath = localSettings.get('mpv_path') as string; const mpvPath = localSettings.get('mpv_path') as string;
return setMpvPath(mpvPath); return setMpvPath(mpvPath);
};
getMpvPath();
}, []); }, []);
return ( return (
@ -34,9 +47,15 @@ export const MpvRequired = () => {
</a> </a>
</Text> </Text>
<FileInput <FileInput
disabled={disabled}
placeholder={mpvPath} placeholder={mpvPath}
onChange={handleSetMpvPath} onChange={handleSetMpvPath}
/> />
<Text>{t('setting.disable_mpv', { context: 'description' })}</Text>
<Checkbox
label={t('setting.disableMpv')}
onChange={(e) => handleSetDisableMpv(e.currentTarget.checked)}
/>
<Button onClick={() => localSettings?.restart()}>Restart</Button> <Button onClick={() => localSettings?.restart()}>Restart</Button>
</> </>
); );

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useMemo } from 'react';
import { Center, Group, Stack } from '@mantine/core'; import { Center, Group, Stack } from '@mantine/core';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -18,23 +18,19 @@ const localSettings = isElectron() ? window.electron.localSettings : null;
const ActionRequiredRoute = () => { const ActionRequiredRoute = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const currentServer = useCurrentServer(); const currentServer = useCurrentServer();
const [isMpvRequired, setIsMpvRequired] = useState(false);
const isServerRequired = !currentServer; const isServerRequired = !currentServer;
const isCredentialRequired = false; const isCredentialRequired = false;
useEffect(() => { const isMpvRequired = useMemo(() => {
const getMpvPath = async () => { if (!localSettings) return false;
if (!localSettings) return setIsMpvRequired(false);
const mpvPath = await localSettings.get('mpv_path');
const mpvPath = localSettings.get('mpv_path');
if (mpvPath) { if (mpvPath) {
return setIsMpvRequired(false); return false;
} }
return setIsMpvRequired(true); const mpvDisabled = localSettings.get('disable_mpv');
}; return !mpvDisabled;
getMpvPath();
}, []); }, []);
const checks = [ const checks = [

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { SelectItem } from '@mantine/core'; import { SelectItem } from '@mantine/core';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { Select, Slider, toast } from '/@/renderer/components'; import { Checkbox, Select, Slider, toast } from '/@/renderer/components';
import { import {
SettingsSection, SettingsSection,
SettingOption, SettingOption,
@ -12,12 +12,15 @@ import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle } from '/@/re
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const localSettings = isElectron() ? window.electron.localSettings : 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 initialDisable = (localSettings?.get('disable_mpv') as boolean | undefined) ?? false;
export const AudioSettings = () => { export const AudioSettings = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const settings = usePlaybackSettings(); const settings = usePlaybackSettings();
@ -25,6 +28,18 @@ export const AudioSettings = () => {
const status = useCurrentStatus(); const status = useCurrentStatus();
const [audioDevices, setAudioDevices] = useState<SelectItem[]>([]); const [audioDevices, setAudioDevices] = useState<SelectItem[]>([]);
const [disableMpv, setDisableMpv] = useState(initialDisable);
const handleSetDisableMpv = (disabled: boolean) => {
setDisableMpv(disabled);
localSettings?.set('disable_mpv', disabled);
if (disabled) {
setSettings({
playback: { ...settings, type: disabled ? PlaybackType.WEB : PlaybackType.LOCAL },
});
}
};
useEffect(() => { useEffect(() => {
const getAudioDevices = () => { const getAudioDevices = () => {
@ -45,6 +60,18 @@ export const AudioSettings = () => {
}, [settings.type, t]); }, [settings.type, t]);
const audioOptions: SettingOption[] = [ const audioOptions: SettingOption[] = [
{
control: (
<Checkbox
defaultChecked={disableMpv}
onChange={(e) => handleSetDisableMpv(e.currentTarget.checked)}
/>
),
description: t('setting.disableMpv', { context: 'description' }),
isHidden: !isElectron(),
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
title: t('setting.disableMpv'),
},
{ {
control: ( control: (
<Select <Select
@ -71,7 +98,7 @@ export const AudioSettings = () => {
context: 'description', context: 'description',
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
}), }),
isHidden: !isElectron(), isHidden: !isElectron() || initialDisable || disableMpv,
note: note:
status === PlayerStatus.PLAYING status === PlayerStatus.PLAYING
? t('common.playerMustBePaused', { postProcess: 'sentenceCase' }) ? t('common.playerMustBePaused', { postProcess: 'sentenceCase' })

View file

@ -13,8 +13,12 @@ export const AppOutlet = () => {
const isMpvRequired = () => { const isMpvRequired = () => {
if (!localSettings) return false; if (!localSettings) return false;
const mpvPath = localSettings.get('mpv_path'); const mpvPath = localSettings.get('mpv_path');
if (mpvPath) return false; if (mpvPath) {
return true; return false;
}
const mpvDisabled = localSettings.get('disable_mpv');
return !mpvDisabled;
}; };
const isServerRequired = !currentServer; const isServerRequired = !currentServer;