mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-21 18:06:30 +02:00
feat(player): set right ctrl vol max to mpv's vol max if player==local (#2198)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user