From 77bfb916ba718c568d247cb9644245e355c29c39 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 2 Apr 2023 21:41:32 -0700 Subject: [PATCH] MPV player enhancements - start the player from the renderer - dynamically modify settings without restart --- src/main/features/core/player/index.ts | 46 +-- src/main/main.ts | 120 +++++--- src/main/preload/mpv-player.ts | 22 ++ src/renderer/app.tsx | 19 ++ .../components/playback/audio-settings.tsx | 82 +---- .../components/playback/mpv-settings.tsx | 281 ++++++++++++++++++ .../{ => playback}/playback-tab.tsx | 9 + .../settings/components/settings-content.tsx | 2 +- src/renderer/store/settings.store.ts | 29 +- 9 files changed, 457 insertions(+), 153 deletions(-) create mode 100644 src/renderer/features/settings/components/playback/mpv-settings.tsx rename src/renderer/features/settings/components/{ => playback}/playback-tab.tsx (55%) diff --git a/src/main/features/core/player/index.ts b/src/main/features/core/player/index.ts index aabcaee7..95a05a8b 100644 --- a/src/main/features/core/player/index.ts +++ b/src/main/features/core/player/index.ts @@ -1,5 +1,5 @@ import { ipcMain } from 'electron'; -import { mpv } from '../../../main'; +import { getMpvInstance } from '../../../main'; import { PlayerData } from '/@/renderer/store'; declare module 'node-mpv'; @@ -13,49 +13,49 @@ function wait(timeout: number) { } ipcMain.on('player-start', async () => { - await mpv.play(); + await getMpvInstance()?.play(); }); // Starts the player ipcMain.on('player-play', async () => { - await mpv.play(); + await getMpvInstance()?.play(); }); // Pauses the player ipcMain.on('player-pause', async () => { - await mpv.pause(); + await getMpvInstance()?.pause(); }); // Stops the player ipcMain.on('player-stop', async () => { - await mpv.stop(); + await getMpvInstance()?.stop(); }); // Goes to the next track in the playlist ipcMain.on('player-next', async () => { - await mpv.next(); + await getMpvInstance()?.next(); }); // Goes to the previous track in the playlist ipcMain.on('player-previous', async () => { - await mpv.prev(); + await getMpvInstance()?.prev(); }); // Seeks forward or backward by the given amount of seconds ipcMain.on('player-seek', async (_event, time: number) => { - await mpv.seek(time); + await getMpvInstance()?.seek(time); }); // Seeks to the given time in seconds ipcMain.on('player-seek-to', async (_event, time: number) => { - await mpv.goToPosition(time); + await getMpvInstance()?.goToPosition(time); }); // Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons ipcMain.on('player-set-queue', async (_event, data: PlayerData) => { if (!data.queue.current && !data.queue.next) { - await mpv.clearPlaylist(); - await mpv.pause(); + await getMpvInstance()?.clearPlaylist(); + await getMpvInstance()?.pause(); return; } @@ -64,11 +64,11 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData) => { while (!complete) { try { if (data.queue.current) { - await mpv.load(data.queue.current.streamUrl, 'replace'); + await getMpvInstance()?.load(data.queue.current.streamUrl, 'replace'); } if (data.queue.next) { - await mpv.load(data.queue.next.streamUrl, 'append'); + await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); } complete = true; @@ -81,14 +81,18 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData) => { // Replaces the queue in position 1 to the given data ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => { - const size = await mpv.getPlaylistSize(); + const size = await getMpvInstance()?.getPlaylistSize(); + + if (!size) { + return; + } if (size > 1) { - await mpv.playlistRemove(1); + await getMpvInstance()?.playlistRemove(1); } if (data.queue.next) { - await mpv.load(data.queue.next.streamUrl, 'append'); + await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); } }); @@ -97,23 +101,23 @@ ipcMain.on('player-auto-next', async (_event, data: PlayerData) => { // Always keep the current song as position 0 in the mpv queue // This allows us to easily set update the next song in the queue without // disturbing the currently playing song - await mpv.playlistRemove(0); + await getMpvInstance()?.playlistRemove(0); if (data.queue.next) { - await mpv.load(data.queue.next.streamUrl, 'append'); + await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); } }); // Sets the volume to the given value (0-100) ipcMain.on('player-volume', async (_event, value: number) => { - await mpv.volume(value); + await getMpvInstance()?.volume(value); }); // Toggles the mute status ipcMain.on('player-mute', async () => { - await mpv.mute(); + await getMpvInstance()?.mute(); }); ipcMain.on('player-quit', async () => { - await mpv.stop(); + await getMpvInstance()?.stop(); }); diff --git a/src/main/main.ts b/src/main/main.ts index 1dee3ab0..f3378475 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -306,13 +306,6 @@ app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,Media const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined; const MPV_PARAMETERS = store.get('mpv_parameters') as Array | undefined; -const gaplessAudioParams = [ - '--gapless-audio=weak', - '--gapless-audio=no', - '--gapless-audio=yes', - '--gapless-audio', -]; - const prefetchPlaylistParams = [ '--prefetch-playlist=no', '--prefetch-playlist=yes', @@ -321,9 +314,6 @@ const prefetchPlaylistParams = [ const DEFAULT_MPV_PARAMETERS = () => { const parameters = []; - if (!MPV_PARAMETERS?.some((param) => gaplessAudioParams.includes(param))) { - parameters.push('--gapless-audio=weak'); - } if (!MPV_PARAMETERS?.some((param) => prefetchPlaylistParams.includes(param))) { parameters.push('--prefetch-playlist=yes'); @@ -332,57 +322,89 @@ const DEFAULT_MPV_PARAMETERS = () => { return parameters; }; -export const mpv = new MpvAPI( - { - audio_only: true, - auto_restart: true, - binary: MPV_BINARY_PATH || '', - time_update: 1, - }, - MPV_PARAMETERS - ? uniq([...DEFAULT_MPV_PARAMETERS(), ...MPV_PARAMETERS]) - : DEFAULT_MPV_PARAMETERS(), -); +let mpvInstance: MpvAPI | null = null; -mpv.start().catch((error) => { - console.log('error starting mpv', error); -}); +const createMpv = (data: { extraParameters?: string[]; properties?: Record }) => { + const { extraParameters, properties } = data; -mpv.on('status', (status) => { - if (status.property === 'playlist-pos') { - if (status.value !== 0) { - getMainWindow()?.webContents.send('renderer-player-auto-next'); + mpvInstance = new MpvAPI( + { + audio_only: true, + auto_restart: false, + binary: MPV_BINARY_PATH || '', + time_update: 1, + }, + MPV_PARAMETERS || extraParameters + ? uniq([...DEFAULT_MPV_PARAMETERS(), ...(MPV_PARAMETERS || []), ...(extraParameters || [])]) + : DEFAULT_MPV_PARAMETERS(), + ); + + mpvInstance.setMultipleProperties(properties || {}); + + mpvInstance.start().catch((error) => { + console.log('error starting mpv', error); + }); + + mpvInstance.on('status', (status) => { + if (status.property === 'playlist-pos') { + if (status.value !== 0) { + getMainWindow()?.webContents.send('renderer-player-auto-next'); + } } + }); + + // Automatically updates the play button when the player is playing + mpvInstance.on('resumed', () => { + getMainWindow()?.webContents.send('renderer-player-play'); + }); + + // Automatically updates the play button when the player is stopped + mpvInstance.on('stopped', () => { + getMainWindow()?.webContents.send('renderer-player-stop'); + }); + + // Automatically updates the play button when the player is paused + mpvInstance.on('paused', () => { + getMainWindow()?.webContents.send('renderer-player-pause'); + }); + + // Event output every interval set by time_update, used to update the current time + mpvInstance.on('timeposition', (time: number) => { + getMainWindow()?.webContents.send('renderer-player-current-time', time); + }); +}; + +export const getMpvInstance = () => { + return mpvInstance; +}; + +ipcMain.on('player-set-properties', async (_event, data: Record) => { + if (data.length === 0) { + return; + } + + if (data.length === 1) { + getMpvInstance()?.setProperty(Object.keys(data)[0], Object.values(data)[0]); + } else { + getMpvInstance()?.setMultipleProperties(data); } }); -// Automatically updates the play button when the player is playing -mpv.on('resumed', () => { - getMainWindow()?.webContents.send('renderer-player-play'); -}); - -// Automatically updates the play button when the player is stopped -mpv.on('stopped', () => { - getMainWindow()?.webContents.send('renderer-player-stop'); -}); - -// Automatically updates the play button when the player is paused -mpv.on('paused', () => { - getMainWindow()?.webContents.send('renderer-player-pause'); -}); - -// Event output every interval set by time_update, used to update the current time -mpv.on('timeposition', (time: number) => { - getMainWindow()?.webContents.send('renderer-player-current-time', time); -}); +ipcMain.on( + 'player-restart', + async (_event, data: { extraParameters?: string[]; properties?: Record }) => { + getMpvInstance()?.quit(); + createMpv(data); + }, +); app.on('before-quit', () => { - mpv.stop(); + getMpvInstance()?.stop(); }); app.on('window-all-closed', () => { globalShortcut.unregisterAll(); - + getMpvInstance()?.quit(); // Respect the OSX convention of having the application in memory even // after all windows have been closed if (isMacOS()) { diff --git a/src/main/preload/mpv-player.ts b/src/main/preload/mpv-player.ts index 9e54eece..f6c04296 100644 --- a/src/main/preload/mpv-player.ts +++ b/src/main/preload/mpv-player.ts @@ -1,6 +1,15 @@ import { ipcRenderer, IpcRendererEvent } from 'electron'; import { PlayerData } from '/@/renderer/store'; +const restart = (data: { extraParameters?: string[]; properties?: Record }) => { + ipcRenderer.send('player-restart', data); +}; + +const setProperties = (data: Record) => { + console.log('Setting property :>>', data); + ipcRenderer.send('player-set-properties', data); +}; + const autoNext = (data: PlayerData) => { ipcRenderer.send('player-auto-next', data); }; @@ -8,36 +17,47 @@ const autoNext = (data: PlayerData) => { const currentTime = () => { ipcRenderer.send('player-current-time'); }; + const mute = () => { ipcRenderer.send('player-mute'); }; + const next = () => { ipcRenderer.send('player-next'); }; + const pause = () => { ipcRenderer.send('player-pause'); }; + const play = () => { ipcRenderer.send('player-play'); }; + const previous = () => { ipcRenderer.send('player-previous'); }; + const seek = (seconds: number) => { ipcRenderer.send('player-seek', seconds); }; + const seekTo = (seconds: number) => { ipcRenderer.send('player-seek-to', seconds); }; + const setQueue = (data: PlayerData) => { ipcRenderer.send('player-set-queue', data); }; + const setQueueNext = (data: PlayerData) => { ipcRenderer.send('player-set-queue-next', data); }; + const stop = () => { ipcRenderer.send('player-stop'); }; + const volume = (value: number) => { ipcRenderer.send('player-volume', value); }; @@ -91,8 +111,10 @@ export const mpvPlayer = { play, previous, quit, + restart, seek, seekTo, + setProperties, setQueue, setQueueNext, stop, diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index ca004dfb..838b4189 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -15,11 +15,16 @@ import { ContextMenuProvider } from '/@/renderer/features/context-menu'; import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add'; import { PlayQueueHandlerContext } from '/@/renderer/features/player'; import { AddToPlaylistContextModal } from '/@/renderer/features/playlists'; +import isElectron from 'is-electron'; +import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings'; +import { usePlayerStore } from '/@/renderer/store'; ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]); initSimpleImg({ threshold: 0.05 }, true); +const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; + export const App = () => { const theme = useTheme(); const contentFont = useSettingsStore((state) => state.general.fontContent); @@ -31,6 +36,20 @@ export const App = () => { root.style.setProperty('--content-font-family', contentFont); }, [contentFont]); + // Start the mpv instance on startup + useEffect(() => { + const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters; + const properties = { + ...getMpvProperties(useSettingsStore.getState().playback.mpvProperties), + volume: usePlayerStore.getState().volume, + }; + + mpvPlayer.restart({ + extraParameters, + properties, + }); + }, []); + return ( { @@ -24,30 +23,6 @@ export const AudioSettings = () => { const status = useCurrentStatus(); const [audioDevices, setAudioDevices] = useState([]); - const [mpvPath, setMpvPath] = useState(''); - const [mpvParameters, setMpvParameters] = useState(''); - - const handleSetMpvPath = (e: File) => { - localSettings.set('mpv_path', e.path); - }; - - useEffect(() => { - const getMpvPath = async () => { - if (!isElectron()) return setMpvPath(''); - const mpvPath = (await localSettings.get('mpv_path')) as string; - return setMpvPath(mpvPath); - }; - - const getMpvParameters = async () => { - if (!isElectron()) return setMpvPath(''); - const mpvParametersFromSettings = (await localSettings.get('mpv_parameters')) as string[]; - const mpvParameters = mpvParametersFromSettings?.join('\n'); - return setMpvParameters(mpvParameters); - }; - - getMpvPath(); - getMpvParameters(); - }, []); useEffect(() => { const getAudioDevices = () => { @@ -89,59 +64,6 @@ export const AudioSettings = () => { note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined, title: 'Audio player', }, - { - control: ( - - ), - description: 'The location of your mpv executable', - isHidden: settings.type !== PlaybackType.LOCAL, - note: 'Restart required', - title: 'MPV executable path', - }, - { - control: ( - -