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 { isMacOS, isWindows } from '/@/main/env';
import { MPV_VOLUME_MAX_CEILING } from '/@/shared/constants/volume';
import { PlayerData } from '/@/shared/types/domain-types';
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) => {
try {
if (!value || value < 0 || value > 100) {
if (value == null || Number.isNaN(value) || value < 0 || value > MPV_VOLUME_MAX_CEILING) {
return;
}
+5 -7
View File
@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
import Player from 'mpris-service';
import { getMainWindow } from '/@/main/index';
import { MPV_VOLUME_MAX_CEILING } from '/@/shared/constants/volume';
import { QueueSong } from '/@/shared/types/domain-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) => {
let volume = Math.round(vol * 100);
if (volume > 100) {
volume = 100;
} else if (volume < 0) {
volume = 0;
}
const volume = Math.min(MPV_VOLUME_MAX_CEILING, Math.max(0, Math.round(vol * 100)));
getMainWindow()?.webContents.send('request-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 { getSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url';
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 { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-properties';
import {
@@ -16,7 +17,7 @@ import {
usePlayerStore,
useSettingsStore,
} from '/@/renderer/store';
import { PlayerStatus } from '/@/shared/types/types';
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
export interface MpvPlayerEngineHandle extends AudioPlayer {}
@@ -334,7 +335,8 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
}
},
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);
if (mpvPlayer) {
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,
useSidebarRightExpanded,
useSideQueueType,
useVolumeMax,
useVolumeWheelStep,
useVolumeWidth,
} 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 { PlayerType } from '/@/shared/types/types';
const calculateVolumeUp = (volume: number, volumeWheelStep: number) => {
const calculateVolumeUp = (volume: number, volumeWheelStep: number, volumeMax: number) => {
let volumeToSet: number;
const newVolumeGreaterThanHundred = volume + volumeWheelStep > 100;
if (newVolumeGreaterThanHundred) {
volumeToSet = 100;
const newVolumeGreaterThanMax = volume + volumeWheelStep > volumeMax;
if (newVolumeGreaterThanMax) {
volumeToSet = volumeMax;
} else {
volumeToSet = volume + volumeWheelStep;
}
@@ -508,6 +509,7 @@ const VolumeButton = () => {
const muted = usePlayerMuted();
const volumeWheelStep = useVolumeWheelStep();
const volumeWidth = useVolumeWidth();
const volumeMax = useVolumeMax();
const { decreaseVolume, increaseVolume, mediaToggleMute, setVolume } = usePlayer();
const isMinWidth = useMediaQuery('(max-width: 480px)');
@@ -569,12 +571,12 @@ const VolumeButton = () => {
if (e.deltaY > 0 || e.deltaX > 0) {
volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
} else {
volumeToSet = calculateVolumeUp(volume, volumeWheelStep);
volumeToSet = calculateVolumeUp(volume, volumeWheelStep, volumeMax);
}
setVolume(volumeToSet);
},
[setVolume, volume, volumeWheelStep],
[setVolume, volume, volumeWheelStep, volumeMax],
);
const handleVolumeDownThrottled = useThrottledCallback(handleVolumeDown, 100);
@@ -638,7 +640,7 @@ const VolumeButton = () => {
</ContextMenu>
{!isMinWidth ? (
<CustomPlayerbarSlider
max={100}
max={volumeMax}
min={0}
onChange={handleVolumeSlider}
onClick={(e) => {
@@ -3,11 +3,12 @@ import React, { useEffect, useMemo } from 'react';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { clampVolume } from '/@/renderer/features/player/audio-player/utils/volume';
import {
useIsRadioActive,
useRadioPlayer,
} 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 { PlayerShuffle, ServerType } from '/@/shared/types/types';
@@ -134,7 +135,8 @@ export const useMPRIS = () => {
});
mpris?.requestVolume((data: { volume: number }) => {
player.setVolume(data.volume);
const { mpvExtraParameters, type } = useSettingsStore.getState().playback;
player.setVolume(clampVolume(data.volume, type, mpvExtraParameters));
});
return () => {
+4 -1
View File
@@ -7,6 +7,7 @@ import { useShallow } from 'zustand/react/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
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 { createSelectors } from '/@/renderer/lib/zustand';
import { useSettingsStore } from '/@/renderer/store/settings.store';
@@ -907,8 +908,10 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
};
},
increaseVolume: (value: number) => {
const { mpvExtraParameters, type } = useSettingsStore.getState().playback;
const volumeMax = resolveVolumeMax(type, mpvExtraParameters);
set((state) => {
state.player.volume = Math.min(100, state.player.volume + value);
state.player.volume = Math.min(volumeMax, state.player.volume + value);
});
},
isFirstTrackInQueue: () => {
+7
View File
@@ -21,6 +21,7 @@ import {
PLAYLIST_TABLE_COLUMNS,
SONG_TABLE_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 { AppRoute } from '/@/renderer/router/routes';
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 useVolumeMax = () =>
useSettingsStore(
(state) => resolveVolumeMax(state.playback.type, state.playback.mpvExtraParameters),
shallow,
);
export const usePlayButtonBehavior = () =>
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;