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:
Kendall Garner 2023-10-23 00:47:44 +00:00 committed by GitHub
parent 742b13d65e
commit 2664a80851
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 85 additions and 12 deletions

View file

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

View file

@ -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}

View file

@ -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 {

View file

@ -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"

View file

@ -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,

View file

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

View file

@ -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;