MPV player enhancements

- start the player from the renderer
- dynamically modify settings without restart
This commit is contained in:
jeffvli 2023-04-02 21:41:32 -07:00
parent f35152a169
commit 77bfb916ba
9 changed files with 457 additions and 153 deletions

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { SelectItem, Stack } from '@mantine/core';
import { SelectItem } from '@mantine/core';
import isElectron from 'is-electron';
import { Select, FileInput, Slider, Textarea, Text, toast } from '/@/renderer/components';
import { Select, Slider, toast } from '/@/renderer/components';
import {
SettingsSection,
SettingOption,
@ -10,7 +10,6 @@ import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle } from '/@/renderer/types';
const localSettings = isElectron() ? window.electron.localSettings : null;
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const getAudioDevice = async () => {
@ -24,30 +23,6 @@ export const AudioSettings = () => {
const status = useCurrentStatus();
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(() => {
const getAudioDevices = () => {
@ -89,59 +64,6 @@ export const AudioSettings = () => {
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
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: (
<Select

View file

@ -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} />
</>
);
};

View file

@ -0,0 +1,22 @@
import { lazy, Suspense } from 'react';
import { Divider, Stack } from '@mantine/core';
import { AudioSettings } from '/@/renderer/features/settings/components/playback/audio-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 = () => {
return (
<Stack spacing="md">
<AudioSettings />
<Suspense fallback={<></>}>{isElectron() && <MpvSettings />}</Suspense>
<Divider />
<ScrobbleSettings />
</Stack>
);
};