Refactor mpv initialization/cleanup

- Don't re-initialize the player on re-render
- Fixes the player potentially crashing on hot reload
This commit is contained in:
jeffvli 2023-06-06 10:48:47 -07:00
parent d97fe4c621
commit c3c1f4cc5f
5 changed files with 157 additions and 52 deletions

View file

@ -1,3 +1,4 @@
import console from 'console';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { getMainWindow, getMpvInstance } from '../../../main'; import { getMainWindow, getMpvInstance } from '../../../main';
import { PlayerData } from '/@/renderer/store'; import { PlayerData } from '/@/renderer/store';
@ -12,50 +13,99 @@ function wait(timeout: number) {
}); });
} }
ipcMain.handle('player-is-running', async () => {
return getMpvInstance()?.isRunning();
});
ipcMain.handle('player-clean-up', async () => {
getMpvInstance()?.stop();
getMpvInstance()?.clearPlaylist();
});
ipcMain.on('player-start', async () => { ipcMain.on('player-start', async () => {
await getMpvInstance()?.play(); await getMpvInstance()
?.play()
.catch((err) => {
console.log('MPV failed to play', err);
});
}); });
// Starts the player // Starts the player
ipcMain.on('player-play', async () => { ipcMain.on('player-play', async () => {
await getMpvInstance()?.play(); await getMpvInstance()
?.play()
.catch((err) => {
console.log('MPV failed to play', err);
});
}); });
// Pauses the player // Pauses the player
ipcMain.on('player-pause', async () => { ipcMain.on('player-pause', async () => {
await getMpvInstance()?.pause(); await getMpvInstance()
?.pause()
.catch((err) => {
console.log('MPV failed to pause', err);
});
}); });
// Stops the player // Stops the player
ipcMain.on('player-stop', async () => { ipcMain.on('player-stop', async () => {
await getMpvInstance()?.stop(); await getMpvInstance()
?.stop()
.catch((err) => {
console.log('MPV failed to stop', err);
});
}); });
// Goes to the next track in the playlist // Goes to the next track in the playlist
ipcMain.on('player-next', async () => { ipcMain.on('player-next', async () => {
await getMpvInstance()?.next(); await getMpvInstance()
?.next()
.catch((err) => {
console.log('MPV failed to go to next', err);
});
}); });
// Goes to the previous track in the playlist // Goes to the previous track in the playlist
ipcMain.on('player-previous', async () => { ipcMain.on('player-previous', async () => {
await getMpvInstance()?.prev(); await getMpvInstance()
?.prev()
.catch((err) => {
console.log('MPV failed to go to previous', err);
});
}); });
// Seeks forward or backward by the given amount of seconds // Seeks forward or backward by the given amount of seconds
ipcMain.on('player-seek', async (_event, time: number) => { ipcMain.on('player-seek', async (_event, time: number) => {
await getMpvInstance()?.seek(time); await getMpvInstance()
?.seek(time)
.catch((err) => {
console.log('MPV failed to seek', err);
});
}); });
// Seeks to the given time in seconds // Seeks to the given time in seconds
ipcMain.on('player-seek-to', async (_event, time: number) => { ipcMain.on('player-seek-to', async (_event, time: number) => {
await getMpvInstance()?.goToPosition(time); await getMpvInstance()
?.goToPosition(time)
.catch((err) => {
console.log(`MPV failed to seek to ${time}`, err);
});
}); });
// Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons // 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, pause?: boolean) => { ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) => {
if (!data.queue.current && !data.queue.next) { if (!data.queue.current && !data.queue.next) {
await getMpvInstance()?.clearPlaylist(); await getMpvInstance()
await getMpvInstance()?.pause(); ?.clearPlaylist()
.catch((err) => {
console.log('MPV failed to clear playlist', err);
});
await getMpvInstance()
?.pause()
.catch((err) => {
console.log('MPV failed to pause', err);
});
return; return;
} }
@ -69,11 +119,19 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean)
} else { } else {
try { try {
if (data.queue.current) { if (data.queue.current) {
await getMpvInstance()?.load(data.queue.current.streamUrl, 'replace'); await getMpvInstance()
?.load(data.queue.current.streamUrl, 'replace')
.catch((err) => {
console.log('MPV failed to load song', err);
});
} }
if (data.queue.next) { if (data.queue.next) {
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); await getMpvInstance()
?.load(data.queue.next.streamUrl, 'append')
.catch((err) => {
console.log('MPV failed to load next song', err);
});
} }
complete = true; complete = true;
@ -92,18 +150,30 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean)
// Replaces the queue in position 1 to the given data // Replaces the queue in position 1 to the given data
ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => { ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => {
const size = await getMpvInstance()?.getPlaylistSize(); const size = await getMpvInstance()
?.getPlaylistSize()
.catch((err) => {
console.log('MPV failed to get playlist size', err);
});
if (!size) { if (!size) {
return; return;
} }
if (size > 1) { if (size > 1) {
await getMpvInstance()?.playlistRemove(1); await getMpvInstance()
?.playlistRemove(1)
.catch((err) => {
console.log('MPV failed to remove song from playlist', err);
});
} }
if (data.queue.next) { if (data.queue.next) {
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); await getMpvInstance()
?.load(data.queue.next.streamUrl, 'append')
.catch((err) => {
console.log('MPV failed to load next song', err);
});
} }
}); });
@ -112,21 +182,37 @@ ipcMain.on('player-auto-next', async (_event, data: PlayerData) => {
// Always keep the current song as position 0 in the mpv queue // 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 // This allows us to easily set update the next song in the queue without
// disturbing the currently playing song // disturbing the currently playing song
await getMpvInstance()?.playlistRemove(0); await getMpvInstance()
?.playlistRemove(0)
.catch((err) => {
console.log('MPV failed to remove song from playlist', err);
});
if (data.queue.next) { if (data.queue.next) {
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append'); await getMpvInstance()
?.load(data.queue.next.streamUrl, 'append')
.catch((err) => {
console.log('MPV failed to load next song', err);
});
} }
}); });
// Sets the volume to the given value (0-100) // Sets the volume to the given value (0-100)
ipcMain.on('player-volume', async (_event, value: number) => { ipcMain.on('player-volume', async (_event, value: number) => {
await getMpvInstance()?.volume(value); await getMpvInstance()
?.volume(value)
.catch((err) => {
console.log('MPV failed to set volume', err);
});
}); });
// Toggles the mute status // Toggles the mute status
ipcMain.on('player-mute', async () => { ipcMain.on('player-mute', async () => {
await getMpvInstance()?.mute(); await getMpvInstance()
?.mute()
.catch((err) => {
console.log('MPV failed to toggle mute', err);
});
}); });
ipcMain.handle('player-get-time', async (): Promise<number | undefined> => { ipcMain.handle('player-get-time', async (): Promise<number | undefined> => {

View file

@ -439,10 +439,10 @@ const createMpv = (data: { extraParameters?: string[]; properties?: Record<strin
const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]); const params = uniq([...DEFAULT_MPV_PARAMETERS(extraParameters), ...(extraParameters || [])]);
console.log('Setting mpv params: ', params); console.log('Setting mpv params: ', params);
mpvInstance = new MpvAPI( const mpv = new MpvAPI(
{ {
audio_only: true, audio_only: true,
auto_restart: true, auto_restart: false,
binary: MPV_BINARY_PATH || '', binary: MPV_BINARY_PATH || '',
time_update: 1, time_update: 1,
}, },
@ -450,17 +450,17 @@ const createMpv = (data: { extraParameters?: string[]; properties?: Record<strin
); );
console.log('Setting MPV properties: ', properties); console.log('Setting MPV properties: ', properties);
mpvInstance.setMultipleProperties(properties || {}); mpv.setMultipleProperties(properties || {});
mpvInstance.start().catch((error) => { mpv.start().catch((error) => {
console.log('MPV Event: start error', error); console.log('MPV failed to start', error);
}); });
mpvInstance.on('status', (status, ...rest) => { mpv.on('status', (status, ...rest) => {
console.log('MPV Event: status', status.property, status.value, rest); console.log('MPV Event: status', status.property, status.value, rest);
if (status.property === 'playlist-pos') { if (status.property === 'playlist-pos') {
if (status.value === -1) { if (status.value === -1) {
mpvInstance?.stop(); mpv?.stop();
} }
if (status.value !== 0) { if (status.value !== 0) {
@ -470,31 +470,33 @@ const createMpv = (data: { extraParameters?: string[]; properties?: Record<strin
}); });
// Automatically updates the play button when the player is playing // Automatically updates the play button when the player is playing
mpvInstance.on('resumed', () => { mpv.on('resumed', () => {
console.log('MPV Event: resumed'); console.log('MPV Event: resumed');
getMainWindow()?.webContents.send('renderer-player-play'); getMainWindow()?.webContents.send('renderer-player-play');
}); });
// Automatically updates the play button when the player is stopped // Automatically updates the play button when the player is stopped
mpvInstance.on('stopped', () => { mpv.on('stopped', () => {
console.log('MPV Event: stopped'); console.log('MPV Event: stopped');
getMainWindow()?.webContents.send('renderer-player-stop'); getMainWindow()?.webContents.send('renderer-player-stop');
}); });
// Automatically updates the play button when the player is paused // Automatically updates the play button when the player is paused
mpvInstance.on('paused', () => { mpv.on('paused', () => {
console.log('MPV Event: paused'); console.log('MPV Event: paused');
getMainWindow()?.webContents.send('renderer-player-pause'); getMainWindow()?.webContents.send('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
mpvInstance.on('timeposition', (time: number) => { mpv.on('timeposition', (time: number) => {
getMainWindow()?.webContents.send('renderer-player-current-time', time); getMainWindow()?.webContents.send('renderer-player-current-time', time);
}); });
mpvInstance.on('quit', () => { mpv.on('quit', () => {
console.log('MPV Event: quit'); console.log('MPV Event: quit');
}); });
return mpv;
}; };
export const getMpvInstance = () => { export const getMpvInstance = () => {
@ -517,14 +519,15 @@ ipcMain.on(
'player-restart', 'player-restart',
async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => { async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
mpvInstance?.quit(); mpvInstance?.quit();
createMpv(data); mpvInstance = createMpv(data);
}, },
); );
ipcMain.on( ipcMain.on(
'player-initialize', 'player-initialize',
async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => { async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
createMpv(data); console.log('Initializing MPV with data: ', data);
mpvInstance = createMpv(data);
}, },
); );
@ -614,6 +617,7 @@ ipcMain.on(
app.on('before-quit', () => { app.on('before-quit', () => {
getMpvInstance()?.stop(); getMpvInstance()?.stop();
getMpvInstance()?.quit();
}); });
app.on('window-all-closed', () => { app.on('window-all-closed', () => {

View file

@ -9,6 +9,14 @@ const restart = (data: { extraParameters?: string[]; properties?: Record<string,
ipcRenderer.send('player-restart', data); ipcRenderer.send('player-restart', data);
}; };
const isRunning = () => {
return ipcRenderer.invoke('player-is-running');
};
const cleanup = () => {
return ipcRenderer.invoke('player-clean-up');
};
const setProperties = (data: Record<string, any>) => { const setProperties = (data: Record<string, any>) => {
console.log('Setting property :>>', data); console.log('Setting property :>>', data);
ipcRenderer.send('player-set-properties', data); ipcRenderer.send('player-set-properties', data);
@ -160,9 +168,11 @@ const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
export const mpvPlayer = { export const mpvPlayer = {
autoNext, autoNext,
cleanup,
currentTime, currentTime,
getCurrentTime, getCurrentTime,
initialize, initialize,
isRunning,
mute, mute,
next, next,
pause, pause,

View file

@ -34,7 +34,7 @@ export const App = () => {
const { type: playbackType } = usePlaybackSettings(); const { type: playbackType } = usePlaybackSettings();
const { bindings } = useHotkeySettings(); const { bindings } = useHotkeySettings();
const handlePlayQueueAdd = useHandlePlayQueueAdd(); const handlePlayQueueAdd = useHandlePlayQueueAdd();
const { restoreQueue } = useQueueControls(); const { clearQueue, restoreQueue } = useQueueControls();
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
@ -43,7 +43,10 @@ export const App = () => {
// Start the mpv instance on startup // Start the mpv instance on startup
useEffect(() => { useEffect(() => {
if (isElectron() && playbackType === PlaybackType.LOCAL) { const initializeMpv = async () => {
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
if (!isRunning) {
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters; const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
const properties = { const properties = {
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties), ...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
@ -56,11 +59,18 @@ export const App = () => {
mpvPlayer?.volume(properties.volume); mpvPlayer?.volume(properties.volume);
} }
};
if (isElectron() && playbackType === PlaybackType.LOCAL) {
initializeMpv();
}
return () => { return () => {
mpvPlayer?.quit(); clearQueue();
mpvPlayer?.stop();
mpvPlayer?.cleanup();
}; };
}, [playbackType]); }, [clearQueue, playbackType]);
useEffect(() => { useEffect(() => {
if (isElectron()) { if (isElectron()) {

View file

@ -613,10 +613,6 @@ export const useCenterControls = (args: { playersRef: any }) => {
handleAutoNext(); handleAutoNext();
}); });
mpvPlayerListener.rendererQuit(() => {
handleQuit();
});
mpvPlayerListener.rendererToggleShuffle(() => { mpvPlayerListener.rendererToggleShuffle(() => {
handleToggleShuffle(); handleToggleShuffle();
}); });
@ -639,7 +635,6 @@ export const useCenterControls = (args: { playersRef: any }) => {
ipc?.removeAllListeners('renderer-player-stop'); ipc?.removeAllListeners('renderer-player-stop');
ipc?.removeAllListeners('renderer-player-current-time'); ipc?.removeAllListeners('renderer-player-current-time');
ipc?.removeAllListeners('renderer-player-auto-next'); ipc?.removeAllListeners('renderer-player-auto-next');
ipc?.removeAllListeners('renderer-player-quit');
ipc?.removeAllListeners('renderer-player-toggle-shuffle'); ipc?.removeAllListeners('renderer-player-toggle-shuffle');
ipc?.removeAllListeners('renderer-player-toggle-repeat'); ipc?.removeAllListeners('renderer-player-toggle-repeat');
ipc?.removeAllListeners('renderer-player-error'); ipc?.removeAllListeners('renderer-player-error');