feat(player): set right ctrl vol max to mpv's vol max if player==local (#2198)

This commit is contained in:
Simon Slamka
2026-07-16 08:43:43 +03:00
committed by GitHub
parent 5e79e385d9
commit ddd45ad5ba
9 changed files with 106 additions and 21 deletions
+5 -2
View File
@@ -11,6 +11,7 @@ import { createLog } from '../../../utils';
import { store } from '../settings'; import { store } from '../settings';
import { isMacOS, isWindows } from '/@/main/env'; import { isMacOS, isWindows } from '/@/main/env';
import { MPV_VOLUME_MAX_CEILING } from '/@/shared/constants/volume';
import { PlayerData } from '/@/shared/types/domain-types'; import { PlayerData } from '/@/shared/types/domain-types';
declare module 'node-mpv'; declare module 'node-mpv';
@@ -493,10 +494,12 @@ ipcMain.on('player-auto-next', async (_event, url?: string) => {
} }
}); });
// Sets the volume to the given value (0-100) // Sets the volume to the given value. mpv clamps to its effective --volume-max,
// so the upper bound here is just a sanity guard; mpv itself is the final
// authority on how loud it will actually go.
ipcMain.on('player-volume', async (_event, value: number) => { ipcMain.on('player-volume', async (_event, value: number) => {
try { try {
if (!value || value < 0 || value > 100) { if (value == null || Number.isNaN(value) || value < 0 || value > MPV_VOLUME_MAX_CEILING) {
return; return;
} }
+5 -7
View File
@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
import Player from 'mpris-service'; import Player from 'mpris-service';
import { getMainWindow } from '/@/main/index'; import { getMainWindow } from '/@/main/index';
import { MPV_VOLUME_MAX_CEILING } from '/@/shared/constants/volume';
import { QueueSong } from '/@/shared/types/domain-types'; import { QueueSong } from '/@/shared/types/domain-types';
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types'; import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
@@ -69,14 +70,11 @@ mprisPlayer.on('previous', () => {
} }
}); });
// The renderer clamps to the active backend's maximum (mpv's --volume-max when
// mpv is selected), since that range is derived from settings the renderer owns.
// Only a sanity range is enforced here.
mprisPlayer.on('volume', (vol: number) => { mprisPlayer.on('volume', (vol: number) => {
let volume = Math.round(vol * 100); const volume = Math.min(MPV_VOLUME_MAX_CEILING, Math.max(0, Math.round(vol * 100)));
if (volume > 100) {
volume = 100;
} else if (volume < 0) {
volume = 0;
}
getMainWindow()?.webContents.send('request-volume', { getMainWindow()?.webContents.send('request-volume', {
volume, volume,
@@ -7,6 +7,7 @@ import { eventEmitter } from '/@/renderer/events/event-emitter';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events'; import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { getSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url'; import { getSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url';
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types'; import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
import { resolveVolumeMax } from '/@/renderer/features/player/audio-player/utils/volume';
import { useRadioStore } from '/@/renderer/features/radio/hooks/use-radio-player'; import { useRadioStore } from '/@/renderer/features/radio/hooks/use-radio-player';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-properties'; import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-properties';
import { import {
@@ -16,7 +17,7 @@ import {
usePlayerStore, usePlayerStore,
useSettingsStore, useSettingsStore,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { PlayerStatus } from '/@/shared/types/types'; import { PlayerStatus, PlayerType } from '/@/shared/types/types';
export interface MpvPlayerEngineHandle extends AudioPlayer {} export interface MpvPlayerEngineHandle extends AudioPlayer {}
@@ -334,7 +335,8 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
} }
}, },
increaseVolume(by: number) { increaseVolume(by: number) {
const newVol = Math.min(1, internalVolume + by / 100); const maxVol = resolveVolumeMax(PlayerType.LOCAL, mpvExtraParameters) / 100;
const newVol = Math.min(maxVol, internalVolume + by / 100);
setInternalVolume(newVol); setInternalVolume(newVol);
if (mpvPlayer) { if (mpvPlayer) {
mpvPlayer.volume(newVol * 100); mpvPlayer.volume(newVol * 100);
@@ -0,0 +1,58 @@
import {
DEFAULT_VOLUME_MAX,
MPV_VOLUME_MAX_CEILING,
MPV_VOLUME_MAX_DEFAULT,
} from '/@/shared/constants/volume';
import { PlayerType } from '/@/shared/types/types';
const VOLUME_MAX_FLAG = '--volume-max';
// mpv accepts either "--volume-max=200" or "--volume-max 200" (as two argv
// entries). Repeated options are last-wins, so we scan from the end.
export const parseMpvVolumeMax = (extraParameters: string[] = []): null | number => {
for (let i = extraParameters.length - 1; i >= 0; i -= 1) {
const param = extraParameters[i]?.trim();
if (!param?.startsWith(VOLUME_MAX_FLAG)) {
continue;
}
const raw = param.includes('=')
? param.slice(param.indexOf('=') + 1)
: extraParameters[i + 1];
const parsed = Number(raw);
if (!Number.isFinite(parsed)) {
return null;
}
return Math.min(MPV_VOLUME_MAX_CEILING, Math.max(1, parsed));
}
return null;
};
// The highest value the volume control may reach for the active backend.
export const resolveVolumeMax = (
playbackType: PlayerType,
extraParameters: string[] = [],
): number => {
if (playbackType !== PlayerType.LOCAL) {
return DEFAULT_VOLUME_MAX;
}
const configured = parseMpvVolumeMax(extraParameters) ?? MPV_VOLUME_MAX_DEFAULT;
return Math.min(MPV_VOLUME_MAX_CEILING, Math.max(DEFAULT_VOLUME_MAX, configured));
};
// Constrains an externally sourced volume (mpris, remote) to the active backend's range.
export const clampVolume = (
volume: number,
playbackType: PlayerType,
extraParameters: string[] = [],
): number => {
if (!Number.isFinite(volume)) {
return 0;
}
return Math.min(resolveVolumeMax(playbackType, extraParameters), Math.max(0, volume));
};
@@ -32,6 +32,7 @@ import {
useSettingsStoreActions, useSettingsStoreActions,
useSidebarRightExpanded, useSidebarRightExpanded,
useSideQueueType, useSideQueueType,
useVolumeMax,
useVolumeWheelStep, useVolumeWheelStep,
useVolumeWidth, useVolumeWidth,
} from '/@/renderer/store'; } from '/@/renderer/store';
@@ -56,11 +57,11 @@ import { useThrottledValue } from '/@/shared/hooks/use-throttled-value';
import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types'; import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types';
import { PlayerType } from '/@/shared/types/types'; import { PlayerType } from '/@/shared/types/types';
const calculateVolumeUp = (volume: number, volumeWheelStep: number) => { const calculateVolumeUp = (volume: number, volumeWheelStep: number, volumeMax: number) => {
let volumeToSet: number; let volumeToSet: number;
const newVolumeGreaterThanHundred = volume + volumeWheelStep > 100; const newVolumeGreaterThanMax = volume + volumeWheelStep > volumeMax;
if (newVolumeGreaterThanHundred) { if (newVolumeGreaterThanMax) {
volumeToSet = 100; volumeToSet = volumeMax;
} else { } else {
volumeToSet = volume + volumeWheelStep; volumeToSet = volume + volumeWheelStep;
} }
@@ -508,6 +509,7 @@ const VolumeButton = () => {
const muted = usePlayerMuted(); const muted = usePlayerMuted();
const volumeWheelStep = useVolumeWheelStep(); const volumeWheelStep = useVolumeWheelStep();
const volumeWidth = useVolumeWidth(); const volumeWidth = useVolumeWidth();
const volumeMax = useVolumeMax();
const { decreaseVolume, increaseVolume, mediaToggleMute, setVolume } = usePlayer(); const { decreaseVolume, increaseVolume, mediaToggleMute, setVolume } = usePlayer();
const isMinWidth = useMediaQuery('(max-width: 480px)'); const isMinWidth = useMediaQuery('(max-width: 480px)');
@@ -569,12 +571,12 @@ const VolumeButton = () => {
if (e.deltaY > 0 || e.deltaX > 0) { if (e.deltaY > 0 || e.deltaX > 0) {
volumeToSet = calculateVolumeDown(volume, volumeWheelStep); volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
} else { } else {
volumeToSet = calculateVolumeUp(volume, volumeWheelStep); volumeToSet = calculateVolumeUp(volume, volumeWheelStep, volumeMax);
} }
setVolume(volumeToSet); setVolume(volumeToSet);
}, },
[setVolume, volume, volumeWheelStep], [setVolume, volume, volumeWheelStep, volumeMax],
); );
const handleVolumeDownThrottled = useThrottledCallback(handleVolumeDown, 100); const handleVolumeDownThrottled = useThrottledCallback(handleVolumeDown, 100);
@@ -638,7 +640,7 @@ const VolumeButton = () => {
</ContextMenu> </ContextMenu>
{!isMinWidth ? ( {!isMinWidth ? (
<CustomPlayerbarSlider <CustomPlayerbarSlider
max={100} max={volumeMax}
min={0} min={0}
onChange={handleVolumeSlider} onChange={handleVolumeSlider}
onClick={(e) => { onClick={(e) => {
@@ -3,11 +3,12 @@ import React, { useEffect, useMemo } from 'react';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events'; import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { clampVolume } from '/@/renderer/features/player/audio-player/utils/volume';
import { import {
useIsRadioActive, useIsRadioActive,
useRadioPlayer, useRadioPlayer,
} from '/@/renderer/features/radio/hooks/use-radio-player'; } from '/@/renderer/features/radio/hooks/use-radio-player';
import { usePlayerSong, usePlayerStore } from '/@/renderer/store'; import { usePlayerSong, usePlayerStore, useSettingsStore } from '/@/renderer/store';
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types'; import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
import { PlayerShuffle, ServerType } from '/@/shared/types/types'; import { PlayerShuffle, ServerType } from '/@/shared/types/types';
@@ -134,7 +135,8 @@ export const useMPRIS = () => {
}); });
mpris?.requestVolume((data: { volume: number }) => { mpris?.requestVolume((data: { volume: number }) => {
player.setVolume(data.volume); const { mpvExtraParameters, type } = useSettingsStore.getState().playback;
player.setVolume(clampVolume(data.volume, type, mpvExtraParameters));
}); });
return () => { return () => {
+4 -1
View File
@@ -7,6 +7,7 @@ import { useShallow } from 'zustand/react/shallow';
import { createWithEqualityFn } from 'zustand/traditional'; import { createWithEqualityFn } from 'zustand/traditional';
import { eventEmitter } from '/@/renderer/events/event-emitter'; import { eventEmitter } from '/@/renderer/events/event-emitter';
import { resolveVolumeMax } from '/@/renderer/features/player/audio-player/utils/volume';
import { useRadioStore as useRadioPlayerStore } from '/@/renderer/features/radio/hooks/use-radio-player'; import { useRadioStore as useRadioPlayerStore } from '/@/renderer/features/radio/hooks/use-radio-player';
import { createSelectors } from '/@/renderer/lib/zustand'; import { createSelectors } from '/@/renderer/lib/zustand';
import { useSettingsStore } from '/@/renderer/store/settings.store'; import { useSettingsStore } from '/@/renderer/store/settings.store';
@@ -907,8 +908,10 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
}; };
}, },
increaseVolume: (value: number) => { increaseVolume: (value: number) => {
const { mpvExtraParameters, type } = useSettingsStore.getState().playback;
const volumeMax = resolveVolumeMax(type, mpvExtraParameters);
set((state) => { set((state) => {
state.player.volume = Math.min(100, state.player.volume + value); state.player.volume = Math.min(volumeMax, state.player.volume + value);
}); });
}, },
isFirstTrackInQueue: () => { isFirstTrackInQueue: () => {
+7
View File
@@ -21,6 +21,7 @@ import {
PLAYLIST_TABLE_COLUMNS, PLAYLIST_TABLE_COLUMNS,
SONG_TABLE_COLUMNS, SONG_TABLE_COLUMNS,
} from '/@/renderer/components/item-list/item-table-list/default-columns'; } from '/@/renderer/components/item-list/item-table-list/default-columns';
import { resolveVolumeMax } from '/@/renderer/features/player/audio-player/utils/volume';
import { audiomotionanalyzerPresets } from '/@/renderer/features/visualizer/components/audiomotionanalyzer/presets'; import { audiomotionanalyzerPresets } from '/@/renderer/features/visualizer/components/audiomotionanalyzer/presets';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { getEnvSettingsOverrides } from '/@/renderer/store/env-settings-overrides'; import { getEnvSettingsOverrides } from '/@/renderer/store/env-settings-overrides';
@@ -2638,6 +2639,12 @@ export const useGeneralSettings = () => useSettingsStore((state) => state.genera
export const usePlaybackType = () => useSettingsStore((state) => state.playback.type, shallow); export const usePlaybackType = () => useSettingsStore((state) => state.playback.type, shallow);
export const useVolumeMax = () =>
useSettingsStore(
(state) => resolveVolumeMax(state.playback.type, state.playback.mpvExtraParameters),
shallow,
);
export const usePlayButtonBehavior = () => export const usePlayButtonBehavior = () =>
useSettingsStore((state) => state.general.playButtonBehavior, shallow); useSettingsStore((state) => state.general.playButtonBehavior, shallow);
+10
View File
@@ -0,0 +1,10 @@
// Defines the volume limits shared by the main and renderer processes.
// The active maximum is resolved from these in the renderer, which owns the
// playback settings that determine it.
// The web-audio backend has no headroom above unity gain, so it stays at 100.
export const DEFAULT_VOLUME_MAX = 100;
// mpv's default --volume-max when the user passes nothing.
export const MPV_VOLUME_MAX_DEFAULT = 130;
// mpv's own hard ceiling for --volume-max.
export const MPV_VOLUME_MAX_CEILING = 1000;