Support changing playback rate (#275)
* initial idea for playback rate * Add transparency to dropdown * Move playback speed component to right controls * Set mpv speed on startup --------- Co-authored-by: jeffvli <jeffvictorli@gmail.com>
This commit is contained in:
parent
742b13d65e
commit
2664a80851
7 changed files with 85 additions and 12 deletions
|
@ -100,7 +100,8 @@ export const App = () => {
|
||||||
|
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
||||||
const properties = {
|
const properties: Record<string, any> = {
|
||||||
|
speed: usePlayerStore.getState().current.speed,
|
||||||
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||||
import type { CrossfadeStyle } from '/@/renderer/types';
|
import type { CrossfadeStyle } from '/@/renderer/types';
|
||||||
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
|
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
|
||||||
|
import { useSpeed } from '/@/renderer/store';
|
||||||
|
|
||||||
interface AudioPlayerProps extends ReactPlayerProps {
|
interface AudioPlayerProps extends ReactPlayerProps {
|
||||||
crossfadeDuration: number;
|
crossfadeDuration: number;
|
||||||
|
@ -59,6 +60,7 @@ export const AudioPlayer = forwardRef(
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
|
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
|
||||||
const playback = useSettingsStore((state) => state.playback.mpvProperties);
|
const playback = useSettingsStore((state) => state.playback.mpvProperties);
|
||||||
|
const playbackSpeed = useSpeed();
|
||||||
|
|
||||||
const [webAudio, setWebAudio] = useState<WebAudio | null>(null);
|
const [webAudio, setWebAudio] = useState<WebAudio | null>(null);
|
||||||
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
|
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
|
||||||
|
@ -307,6 +309,7 @@ export const AudioPlayer = forwardRef(
|
||||||
}}
|
}}
|
||||||
height={0}
|
height={0}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
|
playbackRate={playbackSpeed}
|
||||||
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
|
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
|
||||||
progressInterval={isTransitioning ? 10 : 250}
|
progressInterval={isTransitioning ? 10 : 250}
|
||||||
url={player1?.streamUrl}
|
url={player1?.streamUrl}
|
||||||
|
@ -325,6 +328,7 @@ export const AudioPlayer = forwardRef(
|
||||||
}}
|
}}
|
||||||
height={0}
|
height={0}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
|
playbackRate={playbackSpeed}
|
||||||
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
|
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
|
||||||
progressInterval={isTransitioning ? 10 : 250}
|
progressInterval={isTransitioning ? 10 : 250}
|
||||||
url={player2?.streamUrl}
|
url={player2?.streamUrl}
|
||||||
|
|
|
@ -103,6 +103,7 @@ const StyledPlayerButton = styled(UnstyledButton)<StyledPlayerButtonProps>`
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
color: var(--playerbar-btn-fg-hover);
|
||||||
background: var(--playerbar-btn-bg-hover);
|
background: var(--playerbar-btn-bg-hover);
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
|
|
@ -17,18 +17,21 @@ import {
|
||||||
useHotkeySettings,
|
useHotkeySettings,
|
||||||
useMuted,
|
useMuted,
|
||||||
useSidebarStore,
|
useSidebarStore,
|
||||||
|
useSpeed,
|
||||||
useVolume,
|
useVolume,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { useRightControls } from '../hooks/use-right-controls';
|
import { useRightControls } from '../hooks/use-right-controls';
|
||||||
import { PlayerButton } from './player-button';
|
import { PlayerButton } from './player-button';
|
||||||
import { LibraryItem, ServerType, Song } from '/@/renderer/api/types';
|
import { LibraryItem, ServerType, Song } from '/@/renderer/api/types';
|
||||||
import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared';
|
import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared';
|
||||||
import { Rating } from '/@/renderer/components';
|
import { DropdownMenu, Rating } from '/@/renderer/components';
|
||||||
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
||||||
|
|
||||||
const ipc = isElectron() ? window.electron.ipc : null;
|
const ipc = isElectron() ? window.electron.ipc : null;
|
||||||
const remote = isElectron() ? window.electron.remote : null;
|
const remote = isElectron() ? window.electron.remote : null;
|
||||||
|
|
||||||
|
const PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0];
|
||||||
|
|
||||||
export const RightControls = () => {
|
export const RightControls = () => {
|
||||||
const isMinWidth = useMediaQuery('(max-width: 480px)');
|
const isMinWidth = useMediaQuery('(max-width: 480px)');
|
||||||
const volume = useVolume();
|
const volume = useVolume();
|
||||||
|
@ -38,8 +41,16 @@ export const RightControls = () => {
|
||||||
const { setSideBar } = useAppStoreActions();
|
const { setSideBar } = useAppStoreActions();
|
||||||
const { rightExpanded: isQueueExpanded } = useSidebarStore();
|
const { rightExpanded: isQueueExpanded } = useSidebarStore();
|
||||||
const { bindings } = useHotkeySettings();
|
const { bindings } = useHotkeySettings();
|
||||||
const { handleVolumeSlider, handleVolumeWheel, handleMute, handleVolumeDown, handleVolumeUp } =
|
const {
|
||||||
useRightControls();
|
handleVolumeSlider,
|
||||||
|
handleVolumeWheel,
|
||||||
|
handleMute,
|
||||||
|
handleVolumeDown,
|
||||||
|
handleVolumeUp,
|
||||||
|
handleSpeed,
|
||||||
|
} = useRightControls();
|
||||||
|
|
||||||
|
const speed = useSpeed();
|
||||||
|
|
||||||
const updateRatingMutation = useSetRating({});
|
const updateRatingMutation = useSetRating({});
|
||||||
const addToFavoritesMutation = useCreateFavorite({});
|
const addToFavoritesMutation = useCreateFavorite({});
|
||||||
|
@ -184,6 +195,28 @@ export const RightControls = () => {
|
||||||
align="center"
|
align="center"
|
||||||
spacing="xs"
|
spacing="xs"
|
||||||
>
|
>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenu.Target>
|
||||||
|
<PlayerButton
|
||||||
|
icon={<>{speed} x</>}
|
||||||
|
tooltip={{
|
||||||
|
label: 'Playback speed',
|
||||||
|
openDelay: 500,
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
</DropdownMenu.Target>
|
||||||
|
<DropdownMenu.Dropdown>
|
||||||
|
{PLAYBACK_SPEEDS.map((speed) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={`speed-select-${speed}`}
|
||||||
|
onClick={() => handleSpeed(Number(speed))}
|
||||||
|
>
|
||||||
|
{speed}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.Dropdown>
|
||||||
|
</DropdownMenu>
|
||||||
<PlayerButton
|
<PlayerButton
|
||||||
icon={
|
icon={
|
||||||
currentSong?.userFavorite ? (
|
currentSong?.userFavorite ? (
|
||||||
|
@ -209,12 +242,14 @@ export const RightControls = () => {
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={handleToggleFavorite}
|
onClick={handleToggleFavorite}
|
||||||
/>
|
/>
|
||||||
|
{!isMinWidth ? (
|
||||||
<PlayerButton
|
<PlayerButton
|
||||||
icon={<HiOutlineQueueList size="1.1rem" />}
|
icon={<HiOutlineQueueList size="1.1rem" />}
|
||||||
tooltip={{ label: 'View queue', openDelay: 500 }}
|
tooltip={{ label: 'View queue', openDelay: 500 }}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={handleToggleQueue}
|
onClick={handleToggleQueue}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
<Group
|
<Group
|
||||||
noWrap
|
noWrap
|
||||||
spacing="xs"
|
spacing="xs"
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import { useCallback, useEffect, WheelEvent } from 'react';
|
import { useCallback, useEffect, WheelEvent } from 'react';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { useMuted, usePlayerControls, useVolume } from '/@/renderer/store';
|
import {
|
||||||
|
useMuted,
|
||||||
|
usePlayerControls,
|
||||||
|
useSetCurrentSpeed,
|
||||||
|
useSpeed,
|
||||||
|
useVolume,
|
||||||
|
} from '/@/renderer/store';
|
||||||
import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
||||||
|
|
||||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||||
|
@ -37,6 +43,8 @@ export const useRightControls = () => {
|
||||||
const volume = useVolume();
|
const volume = useVolume();
|
||||||
const muted = useMuted();
|
const muted = useMuted();
|
||||||
const { volumeWheelStep } = useGeneralSettings();
|
const { volumeWheelStep } = useGeneralSettings();
|
||||||
|
const speed = useSpeed();
|
||||||
|
const setCurrentSpeed = useSetCurrentSpeed();
|
||||||
|
|
||||||
// Ensure that the mpv player volume is set on startup
|
// Ensure that the mpv player volume is set on startup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -44,6 +52,7 @@ export const useRightControls = () => {
|
||||||
|
|
||||||
if (mpvPlayer) {
|
if (mpvPlayer) {
|
||||||
mpvPlayer.volume(volume);
|
mpvPlayer.volume(volume);
|
||||||
|
mpvPlayer.setProperties({ speed });
|
||||||
|
|
||||||
if (muted) {
|
if (muted) {
|
||||||
mpvPlayer.mute(muted);
|
mpvPlayer.mute(muted);
|
||||||
|
@ -53,6 +62,16 @@ export const useRightControls = () => {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleSpeed = useCallback(
|
||||||
|
(e: number) => {
|
||||||
|
setCurrentSpeed(e);
|
||||||
|
if (mpvPlayer) {
|
||||||
|
mpvPlayer?.setProperties({ speed: e });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setCurrentSpeed],
|
||||||
|
);
|
||||||
|
|
||||||
const handleVolumeSlider = (e: number) => {
|
const handleVolumeSlider = (e: number) => {
|
||||||
mpvPlayer?.volume(e);
|
mpvPlayer?.volume(e);
|
||||||
remote?.updateVolume(e);
|
remote?.updateVolume(e);
|
||||||
|
@ -123,6 +142,7 @@ export const useRightControls = () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleMute,
|
handleMute,
|
||||||
|
handleSpeed,
|
||||||
handleVolumeDown,
|
handleVolumeDown,
|
||||||
handleVolumeSlider,
|
handleVolumeSlider,
|
||||||
handleVolumeSliderState,
|
handleVolumeSliderState,
|
||||||
|
|
|
@ -18,6 +18,7 @@ export interface PlayerState {
|
||||||
seek: boolean;
|
seek: boolean;
|
||||||
shuffledIndex: number;
|
shuffledIndex: number;
|
||||||
song?: QueueSong;
|
song?: QueueSong;
|
||||||
|
speed: number;
|
||||||
status: PlayerStatus;
|
status: PlayerStatus;
|
||||||
time: number;
|
time: number;
|
||||||
};
|
};
|
||||||
|
@ -81,6 +82,7 @@ export interface PlayerSlice extends PlayerState {
|
||||||
reorderQueue: (rowUniqueIds: string[], afterUniqueId?: string) => PlayerData;
|
reorderQueue: (rowUniqueIds: string[], afterUniqueId?: string) => PlayerData;
|
||||||
restoreQueue: (data: Partial<PlayerState>) => PlayerData;
|
restoreQueue: (data: Partial<PlayerState>) => PlayerData;
|
||||||
setCurrentIndex: (index: number) => PlayerData;
|
setCurrentIndex: (index: number) => PlayerData;
|
||||||
|
setCurrentSpeed: (speed: number) => void;
|
||||||
setCurrentTime: (time: number, seek?: boolean) => void;
|
setCurrentTime: (time: number, seek?: boolean) => void;
|
||||||
setCurrentTrack: (uniqueId: string) => PlayerData;
|
setCurrentTrack: (uniqueId: string) => PlayerData;
|
||||||
setFavorite: (ids: string[], favorite: boolean) => string[];
|
setFavorite: (ids: string[], favorite: boolean) => string[];
|
||||||
|
@ -750,6 +752,11 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
|
|
||||||
return get().actions.getPlayerData();
|
return get().actions.getPlayerData();
|
||||||
},
|
},
|
||||||
|
setCurrentSpeed: (speed) => {
|
||||||
|
set((state) => {
|
||||||
|
state.current.speed = speed;
|
||||||
|
});
|
||||||
|
},
|
||||||
setCurrentTime: (time, seek = false) => {
|
setCurrentTime: (time, seek = false) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.current.seek = seek;
|
state.current.seek = seek;
|
||||||
|
@ -924,6 +931,7 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
seek: false,
|
seek: false,
|
||||||
shuffledIndex: 0,
|
shuffledIndex: 0,
|
||||||
song: {} as QueueSong,
|
song: {} as QueueSong,
|
||||||
|
speed: 1.0,
|
||||||
status: PlayerStatus.PAUSED,
|
status: PlayerStatus.PAUSED,
|
||||||
time: 0,
|
time: 0,
|
||||||
},
|
},
|
||||||
|
@ -1038,6 +1046,10 @@ export const useVolume = () => usePlayerStore((state) => state.volume);
|
||||||
|
|
||||||
export const useMuted = () => usePlayerStore((state) => state.muted);
|
export const useMuted = () => usePlayerStore((state) => state.muted);
|
||||||
|
|
||||||
|
export const useSpeed = () => usePlayerStore((state) => state.current.speed);
|
||||||
|
|
||||||
|
export const useSetCurrentSpeed = () => usePlayerStore((state) => state.actions.setCurrentSpeed);
|
||||||
|
|
||||||
export const useSetQueueFavorite = () => usePlayerStore((state) => state.actions.setFavorite);
|
export const useSetQueueFavorite = () => usePlayerStore((state) => state.actions.setFavorite);
|
||||||
|
|
||||||
export const useSetQueueRating = () => usePlayerStore((state) => state.actions.setRating);
|
export const useSetQueueRating = () => usePlayerStore((state) => state.actions.setRating);
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
--input-placeholder-fg: rgb(107, 108, 109);
|
--input-placeholder-fg: rgb(107, 108, 109);
|
||||||
--input-active-fg: rgb(193, 193, 193);
|
--input-active-fg: rgb(193, 193, 193);
|
||||||
--input-active-bg: rgba(255, 255, 255, 10%);
|
--input-active-bg: rgba(255, 255, 255, 10%);
|
||||||
--dropdown-menu-bg: rgb(32, 32, 32);
|
--dropdown-menu-bg: rgba(32, 32, 32, 95%);
|
||||||
--dropdown-menu-fg: rgb(235, 235, 235);
|
--dropdown-menu-fg: rgb(235, 235, 235);
|
||||||
--dropdown-menu-item-padding: 0.8rem;
|
--dropdown-menu-item-padding: 0.8rem;
|
||||||
--dropdown-menu-item-font-size: 1rem;
|
--dropdown-menu-item-font-size: 1rem;
|
||||||
|
|
Reference in a new issue