MPV player enhancements
- start the player from the renderer - dynamically modify settings without restart
This commit is contained in:
parent
f35152a169
commit
77bfb916ba
9 changed files with 457 additions and 153 deletions
|
@ -1,5 +1,5 @@
|
||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
import { mpv } from '../../../main';
|
import { getMpvInstance } from '../../../main';
|
||||||
import { PlayerData } from '/@/renderer/store';
|
import { PlayerData } from '/@/renderer/store';
|
||||||
|
|
||||||
declare module 'node-mpv';
|
declare module 'node-mpv';
|
||||||
|
@ -13,49 +13,49 @@ function wait(timeout: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.on('player-start', async () => {
|
ipcMain.on('player-start', async () => {
|
||||||
await mpv.play();
|
await getMpvInstance()?.play();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Starts the player
|
// Starts the player
|
||||||
ipcMain.on('player-play', async () => {
|
ipcMain.on('player-play', async () => {
|
||||||
await mpv.play();
|
await getMpvInstance()?.play();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pauses the player
|
// Pauses the player
|
||||||
ipcMain.on('player-pause', async () => {
|
ipcMain.on('player-pause', async () => {
|
||||||
await mpv.pause();
|
await getMpvInstance()?.pause();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stops the player
|
// Stops the player
|
||||||
ipcMain.on('player-stop', async () => {
|
ipcMain.on('player-stop', async () => {
|
||||||
await mpv.stop();
|
await getMpvInstance()?.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 mpv.next();
|
await getMpvInstance()?.next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 mpv.prev();
|
await getMpvInstance()?.prev();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 mpv.seek(time);
|
await getMpvInstance()?.seek(time);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 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
|
// 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) => {
|
ipcMain.on('player-set-queue', async (_event, data: PlayerData) => {
|
||||||
if (!data.queue.current && !data.queue.next) {
|
if (!data.queue.current && !data.queue.next) {
|
||||||
await mpv.clearPlaylist();
|
await getMpvInstance()?.clearPlaylist();
|
||||||
await mpv.pause();
|
await getMpvInstance()?.pause();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,11 +64,11 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData) => {
|
||||||
while (!complete) {
|
while (!complete) {
|
||||||
try {
|
try {
|
||||||
if (data.queue.current) {
|
if (data.queue.current) {
|
||||||
await mpv.load(data.queue.current.streamUrl, 'replace');
|
await getMpvInstance()?.load(data.queue.current.streamUrl, 'replace');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.queue.next) {
|
if (data.queue.next) {
|
||||||
await mpv.load(data.queue.next.streamUrl, 'append');
|
await getMpvInstance()?.load(data.queue.next.streamUrl, 'append');
|
||||||
}
|
}
|
||||||
|
|
||||||
complete = true;
|
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
|
// 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 mpv.getPlaylistSize();
|
const size = await getMpvInstance()?.getPlaylistSize();
|
||||||
|
|
||||||
|
if (!size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (size > 1) {
|
if (size > 1) {
|
||||||
await mpv.playlistRemove(1);
|
await getMpvInstance()?.playlistRemove(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.queue.next) {
|
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
|
// 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 mpv.playlistRemove(0);
|
await getMpvInstance()?.playlistRemove(0);
|
||||||
|
|
||||||
if (data.queue.next) {
|
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)
|
// 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 mpv.volume(value);
|
await getMpvInstance()?.volume(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggles the mute status
|
// Toggles the mute status
|
||||||
ipcMain.on('player-mute', async () => {
|
ipcMain.on('player-mute', async () => {
|
||||||
await mpv.mute();
|
await getMpvInstance()?.mute();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('player-quit', async () => {
|
ipcMain.on('player-quit', async () => {
|
||||||
await mpv.stop();
|
await getMpvInstance()?.stop();
|
||||||
});
|
});
|
||||||
|
|
120
src/main/main.ts
120
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_BINARY_PATH = store.get('mpv_path') as string | undefined;
|
||||||
const MPV_PARAMETERS = store.get('mpv_parameters') as Array<string> | undefined;
|
const MPV_PARAMETERS = store.get('mpv_parameters') as Array<string> | undefined;
|
||||||
|
|
||||||
const gaplessAudioParams = [
|
|
||||||
'--gapless-audio=weak',
|
|
||||||
'--gapless-audio=no',
|
|
||||||
'--gapless-audio=yes',
|
|
||||||
'--gapless-audio',
|
|
||||||
];
|
|
||||||
|
|
||||||
const prefetchPlaylistParams = [
|
const prefetchPlaylistParams = [
|
||||||
'--prefetch-playlist=no',
|
'--prefetch-playlist=no',
|
||||||
'--prefetch-playlist=yes',
|
'--prefetch-playlist=yes',
|
||||||
|
@ -321,9 +314,6 @@ const prefetchPlaylistParams = [
|
||||||
|
|
||||||
const DEFAULT_MPV_PARAMETERS = () => {
|
const DEFAULT_MPV_PARAMETERS = () => {
|
||||||
const parameters = [];
|
const parameters = [];
|
||||||
if (!MPV_PARAMETERS?.some((param) => gaplessAudioParams.includes(param))) {
|
|
||||||
parameters.push('--gapless-audio=weak');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!MPV_PARAMETERS?.some((param) => prefetchPlaylistParams.includes(param))) {
|
if (!MPV_PARAMETERS?.some((param) => prefetchPlaylistParams.includes(param))) {
|
||||||
parameters.push('--prefetch-playlist=yes');
|
parameters.push('--prefetch-playlist=yes');
|
||||||
|
@ -332,57 +322,89 @@ const DEFAULT_MPV_PARAMETERS = () => {
|
||||||
return parameters;
|
return parameters;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mpv = new MpvAPI(
|
let mpvInstance: MpvAPI | null = null;
|
||||||
{
|
|
||||||
audio_only: true,
|
|
||||||
auto_restart: true,
|
|
||||||
binary: MPV_BINARY_PATH || '',
|
|
||||||
time_update: 1,
|
|
||||||
},
|
|
||||||
MPV_PARAMETERS
|
|
||||||
? uniq([...DEFAULT_MPV_PARAMETERS(), ...MPV_PARAMETERS])
|
|
||||||
: DEFAULT_MPV_PARAMETERS(),
|
|
||||||
);
|
|
||||||
|
|
||||||
mpv.start().catch((error) => {
|
const createMpv = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
||||||
console.log('error starting mpv', error);
|
const { extraParameters, properties } = data;
|
||||||
});
|
|
||||||
|
|
||||||
mpv.on('status', (status) => {
|
mpvInstance = new MpvAPI(
|
||||||
if (status.property === 'playlist-pos') {
|
{
|
||||||
if (status.value !== 0) {
|
audio_only: true,
|
||||||
getMainWindow()?.webContents.send('renderer-player-auto-next');
|
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<string, any>) => {
|
||||||
|
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
|
ipcMain.on(
|
||||||
mpv.on('resumed', () => {
|
'player-restart',
|
||||||
getMainWindow()?.webContents.send('renderer-player-play');
|
async (_event, data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
||||||
});
|
getMpvInstance()?.quit();
|
||||||
|
createMpv(data);
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', () => {
|
||||||
mpv.stop();
|
getMpvInstance()?.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
|
getMpvInstance()?.quit();
|
||||||
// Respect the OSX convention of having the application in memory even
|
// Respect the OSX convention of having the application in memory even
|
||||||
// after all windows have been closed
|
// after all windows have been closed
|
||||||
if (isMacOS()) {
|
if (isMacOS()) {
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
import { PlayerData } from '/@/renderer/store';
|
import { PlayerData } from '/@/renderer/store';
|
||||||
|
|
||||||
|
const restart = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
|
||||||
|
ipcRenderer.send('player-restart', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setProperties = (data: Record<string, any>) => {
|
||||||
|
console.log('Setting property :>>', data);
|
||||||
|
ipcRenderer.send('player-set-properties', data);
|
||||||
|
};
|
||||||
|
|
||||||
const autoNext = (data: PlayerData) => {
|
const autoNext = (data: PlayerData) => {
|
||||||
ipcRenderer.send('player-auto-next', data);
|
ipcRenderer.send('player-auto-next', data);
|
||||||
};
|
};
|
||||||
|
@ -8,36 +17,47 @@ const autoNext = (data: PlayerData) => {
|
||||||
const currentTime = () => {
|
const currentTime = () => {
|
||||||
ipcRenderer.send('player-current-time');
|
ipcRenderer.send('player-current-time');
|
||||||
};
|
};
|
||||||
|
|
||||||
const mute = () => {
|
const mute = () => {
|
||||||
ipcRenderer.send('player-mute');
|
ipcRenderer.send('player-mute');
|
||||||
};
|
};
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
ipcRenderer.send('player-next');
|
ipcRenderer.send('player-next');
|
||||||
};
|
};
|
||||||
|
|
||||||
const pause = () => {
|
const pause = () => {
|
||||||
ipcRenderer.send('player-pause');
|
ipcRenderer.send('player-pause');
|
||||||
};
|
};
|
||||||
|
|
||||||
const play = () => {
|
const play = () => {
|
||||||
ipcRenderer.send('player-play');
|
ipcRenderer.send('player-play');
|
||||||
};
|
};
|
||||||
|
|
||||||
const previous = () => {
|
const previous = () => {
|
||||||
ipcRenderer.send('player-previous');
|
ipcRenderer.send('player-previous');
|
||||||
};
|
};
|
||||||
|
|
||||||
const seek = (seconds: number) => {
|
const seek = (seconds: number) => {
|
||||||
ipcRenderer.send('player-seek', seconds);
|
ipcRenderer.send('player-seek', seconds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const seekTo = (seconds: number) => {
|
const seekTo = (seconds: number) => {
|
||||||
ipcRenderer.send('player-seek-to', seconds);
|
ipcRenderer.send('player-seek-to', seconds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setQueue = (data: PlayerData) => {
|
const setQueue = (data: PlayerData) => {
|
||||||
ipcRenderer.send('player-set-queue', data);
|
ipcRenderer.send('player-set-queue', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setQueueNext = (data: PlayerData) => {
|
const setQueueNext = (data: PlayerData) => {
|
||||||
ipcRenderer.send('player-set-queue-next', data);
|
ipcRenderer.send('player-set-queue-next', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
ipcRenderer.send('player-stop');
|
ipcRenderer.send('player-stop');
|
||||||
};
|
};
|
||||||
|
|
||||||
const volume = (value: number) => {
|
const volume = (value: number) => {
|
||||||
ipcRenderer.send('player-volume', value);
|
ipcRenderer.send('player-volume', value);
|
||||||
};
|
};
|
||||||
|
@ -91,8 +111,10 @@ export const mpvPlayer = {
|
||||||
play,
|
play,
|
||||||
previous,
|
previous,
|
||||||
quit,
|
quit,
|
||||||
|
restart,
|
||||||
seek,
|
seek,
|
||||||
seekTo,
|
seekTo,
|
||||||
|
setProperties,
|
||||||
setQueue,
|
setQueue,
|
||||||
setQueueNext,
|
setQueueNext,
|
||||||
stop,
|
stop,
|
||||||
|
|
|
@ -15,11 +15,16 @@ import { ContextMenuProvider } from '/@/renderer/features/context-menu';
|
||||||
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
||||||
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
|
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
|
||||||
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
|
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]);
|
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
|
||||||
|
|
||||||
initSimpleImg({ threshold: 0.05 }, true);
|
initSimpleImg({ threshold: 0.05 }, true);
|
||||||
|
|
||||||
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const contentFont = useSettingsStore((state) => state.general.fontContent);
|
const contentFont = useSettingsStore((state) => state.general.fontContent);
|
||||||
|
@ -31,6 +36,20 @@ export const App = () => {
|
||||||
root.style.setProperty('--content-font-family', contentFont);
|
root.style.setProperty('--content-font-family', contentFont);
|
||||||
}, [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 (
|
return (
|
||||||
<MantineProvider
|
<MantineProvider
|
||||||
withGlobalStyles
|
withGlobalStyles
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { SelectItem, Stack } from '@mantine/core';
|
import { SelectItem } from '@mantine/core';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { Select, FileInput, Slider, Textarea, Text, toast } from '/@/renderer/components';
|
import { Select, Slider, toast } from '/@/renderer/components';
|
||||||
import {
|
import {
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
SettingOption,
|
SettingOption,
|
||||||
|
@ -10,7 +10,6 @@ import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
|
||||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||||
import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle } from '/@/renderer/types';
|
import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle } from '/@/renderer/types';
|
||||||
|
|
||||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
|
||||||
const getAudioDevice = async () => {
|
const getAudioDevice = async () => {
|
||||||
|
@ -24,30 +23,6 @@ export const AudioSettings = () => {
|
||||||
const status = useCurrentStatus();
|
const status = useCurrentStatus();
|
||||||
|
|
||||||
const [audioDevices, setAudioDevices] = useState<SelectItem[]>([]);
|
const [audioDevices, setAudioDevices] = useState<SelectItem[]>([]);
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
const getAudioDevices = () => {
|
const getAudioDevices = () => {
|
||||||
|
@ -89,59 +64,6 @@ export const AudioSettings = () => {
|
||||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||||
title: 'Audio player',
|
title: 'Audio player',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<FileInput
|
|
||||||
placeholder={mpvPath}
|
|
||||||
width={225}
|
|
||||||
onChange={handleSetMpvPath}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: 'The location of your mpv executable',
|
|
||||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
|
||||||
note: 'Restart required',
|
|
||||||
title: 'MPV executable path',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Stack spacing="xs">
|
|
||||||
<Textarea
|
|
||||||
autosize
|
|
||||||
defaultValue={mpvParameters}
|
|
||||||
minRows={4}
|
|
||||||
placeholder={'(Add one per line):\n--gapless-audio=weak\n--prefetch-playlist=yes'}
|
|
||||||
width={225}
|
|
||||||
onBlur={(e) => {
|
|
||||||
if (isElectron()) {
|
|
||||||
localSettings.set('mpv_parameters', e.currentTarget.value.split('\n'));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
description: (
|
|
||||||
<Stack spacing={0}>
|
|
||||||
<Text
|
|
||||||
$noSelect
|
|
||||||
$secondary
|
|
||||||
>
|
|
||||||
Options to pass to the player
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
<a
|
|
||||||
href="https://mpv.io/manual/stable/#audio"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
https://mpv.io/manual/stable/#audio
|
|
||||||
</a>
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
|
||||||
note: 'Restart required.',
|
|
||||||
title: 'MPV parameters',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -0,0 +1,281 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Divider, Stack } from '@mantine/core';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
|
import { FileInput, Textarea, Text, Select, NumberInput, Switch } from '/@/renderer/components';
|
||||||
|
import {
|
||||||
|
SettingsSection,
|
||||||
|
SettingOption,
|
||||||
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
|
import {
|
||||||
|
SettingsState,
|
||||||
|
usePlaybackSettings,
|
||||||
|
useSettingsStoreActions,
|
||||||
|
} from '/@/renderer/store/settings.store';
|
||||||
|
import { PlaybackType } from '/@/renderer/types';
|
||||||
|
|
||||||
|
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||||
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
|
||||||
|
export const getMpvSetting = (
|
||||||
|
key: keyof SettingsState['playback']['mpvProperties'],
|
||||||
|
value: any,
|
||||||
|
) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'audioExclusiveMode':
|
||||||
|
return { 'audio-exclusive': value || 'no' };
|
||||||
|
case 'audioSampleRateHz':
|
||||||
|
return { 'audio-samplerate': value };
|
||||||
|
case 'gaplessAudio':
|
||||||
|
return { 'gapless-audio': value || 'weak' };
|
||||||
|
case 'replayGainMode':
|
||||||
|
return { replaygain: value || 'no' };
|
||||||
|
case 'replayGainClip':
|
||||||
|
return { 'replaygain-clip': value || 'no' };
|
||||||
|
case 'replayGainFallbackDB':
|
||||||
|
return { 'replaygain-fallback': value };
|
||||||
|
case 'replayGainPreampDB':
|
||||||
|
return { 'replaygain-preamp': value || 0 };
|
||||||
|
default:
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMpvProperties = (settings: SettingsState['playback']['mpvProperties']) => {
|
||||||
|
const properties: Record<string, any> = {
|
||||||
|
'audio-exclusive': settings.audioExclusiveMode || 'no',
|
||||||
|
'audio-samplerate': settings.audioSampleRateHz,
|
||||||
|
'gapless-audio': settings.gaplessAudio || 'weak',
|
||||||
|
replaygain: settings.replayGainMode || 'no',
|
||||||
|
'replaygain-clip': settings.replayGainClip || 'no',
|
||||||
|
'replaygain-fallback': settings.replayGainFallbackDB,
|
||||||
|
'replaygain-preamp': settings.replayGainPreampDB || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(properties).forEach((key) =>
|
||||||
|
properties[key] === undefined ? delete properties[key] : {},
|
||||||
|
);
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MpvSettings = () => {
|
||||||
|
const settings = usePlaybackSettings();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const [mpvPath, setMpvPath] = 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
getMpvPath();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSetMpvProperty = (
|
||||||
|
setting: keyof SettingsState['playback']['mpvProperties'],
|
||||||
|
value: any,
|
||||||
|
) => {
|
||||||
|
setSettings({
|
||||||
|
playback: {
|
||||||
|
...settings,
|
||||||
|
mpvProperties: {
|
||||||
|
...settings.mpvProperties,
|
||||||
|
[setting]: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mpvSetting = getMpvSetting(setting, value);
|
||||||
|
|
||||||
|
mpvPlayer?.setProperties(mpvSetting);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetExtraParameters = (data: string[]) => {
|
||||||
|
setSettings({
|
||||||
|
playback: {
|
||||||
|
...settings,
|
||||||
|
mpvExtraParameters: data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<FileInput
|
||||||
|
placeholder={mpvPath}
|
||||||
|
width={225}
|
||||||
|
onChange={handleSetMpvPath}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'The location of your mpv executable',
|
||||||
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||||
|
note: 'Restart required',
|
||||||
|
title: 'MPV executable path',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<Textarea
|
||||||
|
autosize
|
||||||
|
defaultValue={settings.mpvExtraParameters.join('\n')}
|
||||||
|
minRows={4}
|
||||||
|
placeholder={'(Add one per line):\n--gapless-audio=weak\n--prefetch-playlist=yes'}
|
||||||
|
width={225}
|
||||||
|
onBlur={(e) => {
|
||||||
|
handleSetExtraParameters(e.currentTarget.value.split('\n'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
description: (
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Text
|
||||||
|
$noSelect
|
||||||
|
$secondary
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Options to pass to the player
|
||||||
|
</Text>
|
||||||
|
<Text size="sm">
|
||||||
|
<a
|
||||||
|
href="https://mpv.io/manual/stable/#audio"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
https://mpv.io/manual/stable/#audio
|
||||||
|
</a>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||||
|
note: 'Requires restart',
|
||||||
|
title: 'MPV parameters',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const generalOptions: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
{ label: 'No', value: 'no' },
|
||||||
|
{ label: 'Yes', value: 'yes' },
|
||||||
|
{ label: 'Weak (recommended)', value: 'weak' },
|
||||||
|
]}
|
||||||
|
defaultValue={settings.mpvProperties.gaplessAudio}
|
||||||
|
onChange={(e) => handleSetMpvProperty('gaplessAudio', e)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'Try to play consecutive audio files with no silence or disruption at the point of file change (--gapless-audio)',
|
||||||
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||||
|
title: 'Gapless audio',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={settings.mpvProperties.replayGainPreampDB}
|
||||||
|
width={100}
|
||||||
|
onBlur={(e) => handleSetMpvProperty('audioSampleRateHz', e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'Select the output sample rate to be used (of course sound cards have limits on this). If the sample frequency selected is different from that of the current media',
|
||||||
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||||
|
title: 'Sample rate',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={settings.mpvProperties.audioExclusiveMode === 'yes'}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleSetMpvProperty('audioExclusiveMode', e.currentTarget.checked ? 'yes' : 'no')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
|
||||||
|
description:
|
||||||
|
'Enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio (--audio-exclusive)',
|
||||||
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||||
|
title: 'Audio exclusive mode',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const replayGainOptions: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
{ label: 'None', value: 'no' },
|
||||||
|
{ label: 'Track', value: 'track' },
|
||||||
|
{ label: 'Album', value: 'album' },
|
||||||
|
]}
|
||||||
|
defaultValue={settings.mpvProperties.replayGainMode}
|
||||||
|
onChange={(e) => handleSetMpvProperty('replayGainMode', e)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'Adjust volume gain according to replaygain values stored in the file metadata (--replaygain)',
|
||||||
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||||
|
note: 'Restart required',
|
||||||
|
title: 'ReplayGain mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={settings.mpvProperties.replayGainPreampDB}
|
||||||
|
width={75}
|
||||||
|
onChange={(e) => handleSetMpvProperty('replayGainPreampDB', e)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'Pre-amplification gain in dB to apply to the selected replaygain gain (--replaygain-preamp)',
|
||||||
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||||
|
title: 'ReplayGain preamp (dB)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={settings.mpvProperties.replayGainClip}
|
||||||
|
onChange={(e) => handleSetMpvProperty('replayGainClip', e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'Prevent clipping caused by replaygain by automatically lowering the gain (--replaygain-clip)',
|
||||||
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||||
|
title: 'ReplayGain clipping',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={settings.mpvProperties.replayGainFallbackDB}
|
||||||
|
width={75}
|
||||||
|
onChange={(e) => handleSetMpvProperty('replayGainFallbackDB', e)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'Gain in dB to apply if the file has no replay gain tags. This option is always applied if the replaygain logic is somehow inactive. If this is applied, no other replaygain options are applied',
|
||||||
|
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||||
|
title: 'ReplayGain fallback',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingsSection options={options} />
|
||||||
|
<Divider />
|
||||||
|
<SettingsSection options={generalOptions} />
|
||||||
|
<Divider />
|
||||||
|
<SettingsSection options={replayGainOptions} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,11 +1,20 @@
|
||||||
|
import { lazy, Suspense } from 'react';
|
||||||
import { Divider, Stack } from '@mantine/core';
|
import { Divider, Stack } from '@mantine/core';
|
||||||
import { AudioSettings } from '/@/renderer/features/settings/components/playback/audio-settings';
|
import { AudioSettings } from '/@/renderer/features/settings/components/playback/audio-settings';
|
||||||
import { ScrobbleSettings } from '/@/renderer/features/settings/components/playback/scrobble-settings';
|
import { ScrobbleSettings } from '/@/renderer/features/settings/components/playback/scrobble-settings';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
|
|
||||||
|
const MpvSettings = lazy(() =>
|
||||||
|
import('/@/renderer/features/settings/components/playback/mpv-settings').then((module) => {
|
||||||
|
return { default: module.MpvSettings };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export const PlaybackTab = () => {
|
export const PlaybackTab = () => {
|
||||||
return (
|
return (
|
||||||
<Stack spacing="md">
|
<Stack spacing="md">
|
||||||
<AudioSettings />
|
<AudioSettings />
|
||||||
|
<Suspense fallback={<></>}>{isElectron() && <MpvSettings />}</Suspense>
|
||||||
<Divider />
|
<Divider />
|
||||||
<ScrobbleSettings />
|
<ScrobbleSettings />
|
||||||
</Stack>
|
</Stack>
|
|
@ -11,7 +11,7 @@ const GeneralTab = lazy(() =>
|
||||||
);
|
);
|
||||||
|
|
||||||
const PlaybackTab = lazy(() =>
|
const PlaybackTab = lazy(() =>
|
||||||
import('/@/renderer/features/settings/components/playback-tab').then((module) => ({
|
import('/@/renderer/features/settings/components/playback/playback-tab').then((module) => ({
|
||||||
default: module.PlaybackTab,
|
default: module.PlaybackTab,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
|
@ -31,6 +31,17 @@ export type DataTableProps = {
|
||||||
|
|
||||||
export type SideQueueType = 'sideQueue' | 'sideDrawerQueue';
|
export type SideQueueType = 'sideQueue' | 'sideDrawerQueue';
|
||||||
|
|
||||||
|
type MpvSettings = {
|
||||||
|
audioExclusiveMode: 'yes' | 'no';
|
||||||
|
audioFormat?: 's16' | 's32' | 'float';
|
||||||
|
audioSampleRateHz?: number;
|
||||||
|
gaplessAudio: 'yes' | 'no' | 'weak';
|
||||||
|
replayGainClip: boolean;
|
||||||
|
replayGainFallbackDB?: number;
|
||||||
|
replayGainMode: 'no' | 'track' | 'album';
|
||||||
|
replayGainPreampDB?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export interface SettingsState {
|
export interface SettingsState {
|
||||||
general: {
|
general: {
|
||||||
followSystemTheme: boolean;
|
followSystemTheme: boolean;
|
||||||
|
@ -55,13 +66,14 @@ export interface SettingsState {
|
||||||
audioDeviceId?: string | null;
|
audioDeviceId?: string | null;
|
||||||
crossfadeDuration: number;
|
crossfadeDuration: number;
|
||||||
crossfadeStyle: CrossfadeStyle;
|
crossfadeStyle: CrossfadeStyle;
|
||||||
|
mpvExtraParameters: string[];
|
||||||
|
mpvProperties: MpvSettings;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
scrobble: {
|
scrobble: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
scrobbleAtDuration: number;
|
scrobbleAtDuration: number;
|
||||||
scrobbleAtPercentage: number;
|
scrobbleAtPercentage: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
style: PlaybackStyle;
|
style: PlaybackStyle;
|
||||||
type: PlaybackType;
|
type: PlaybackType;
|
||||||
};
|
};
|
||||||
|
@ -112,13 +124,23 @@ const initialState: SettingsState = {
|
||||||
audioDeviceId: undefined,
|
audioDeviceId: undefined,
|
||||||
crossfadeDuration: 5,
|
crossfadeDuration: 5,
|
||||||
crossfadeStyle: CrossfadeStyle.EQUALPOWER,
|
crossfadeStyle: CrossfadeStyle.EQUALPOWER,
|
||||||
|
mpvExtraParameters: [],
|
||||||
|
mpvProperties: {
|
||||||
|
audioExclusiveMode: 'no',
|
||||||
|
audioFormat: undefined,
|
||||||
|
audioSampleRateHz: undefined,
|
||||||
|
gaplessAudio: 'weak',
|
||||||
|
replayGainClip: true,
|
||||||
|
replayGainFallbackDB: undefined,
|
||||||
|
replayGainMode: 'no',
|
||||||
|
replayGainPreampDB: 0,
|
||||||
|
},
|
||||||
muted: false,
|
muted: false,
|
||||||
scrobble: {
|
scrobble: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scrobbleAtDuration: 240,
|
scrobbleAtDuration: 240,
|
||||||
scrobbleAtPercentage: 75,
|
scrobbleAtPercentage: 75,
|
||||||
},
|
},
|
||||||
|
|
||||||
style: PlaybackStyle.GAPLESS,
|
style: PlaybackStyle.GAPLESS,
|
||||||
type: PlaybackType.LOCAL,
|
type: PlaybackType.LOCAL,
|
||||||
},
|
},
|
||||||
|
@ -304,3 +326,6 @@ export const usePlayButtonBehavior = () =>
|
||||||
export const useWindowSettings = () => useSettingsStore((state) => state.window, shallow);
|
export const useWindowSettings = () => useSettingsStore((state) => state.window, shallow);
|
||||||
|
|
||||||
export const useHotkeySettings = () => useSettingsStore((state) => state.hotkeys, shallow);
|
export const useHotkeySettings = () => useSettingsStore((state) => state.hotkeys, shallow);
|
||||||
|
|
||||||
|
export const useMpvSettings = () =>
|
||||||
|
useSettingsStore((state) => state.playback.mpvProperties, shallow);
|
||||||
|
|
Reference in a new issue