diff --git a/src/renderer/components/card/poster-card.tsx b/src/renderer/components/card/poster-card.tsx
index ba314d8b9..8a2036957 100644
--- a/src/renderer/components/card/poster-card.tsx
+++ b/src/renderer/components/card/poster-card.tsx
@@ -59,7 +59,6 @@ export const PosterCard = ({
void;
- handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
isHovered?: boolean;
itemData: any;
itemType: LibraryItem;
@@ -39,17 +38,18 @@ export const GridCardControls = ({
const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite);
const playButtonBehavior = usePlayButtonBehavior();
+ const player = usePlayerContext();
+
const handlePlay = async (e: MouseEvent, playType?: Play) => {
e.preventDefault();
e.stopPropagation();
- handlePlayQueueAdd?.({
- byItemType: {
- id: [itemData.id],
- type: itemType,
- },
- playType: playType || playButtonBehavior,
- });
+ player.addToQueueByFetch(
+ itemData._serverId,
+ [itemData.id],
+ itemType,
+ playType || playButtonBehavior,
+ );
};
const handleFavorites = async (e: MouseEvent, serverId: string) => {
diff --git a/src/renderer/components/virtual-table/hooks/use-current-song-row-styles.ts b/src/renderer/components/virtual-table/hooks/use-current-song-row-styles.ts
index bbf46ec54..04e67a4fe 100644
--- a/src/renderer/components/virtual-table/hooks/use-current-song-row-styles.ts
+++ b/src/renderer/components/virtual-table/hooks/use-current-song-row-styles.ts
@@ -3,7 +3,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { RowClassRules, RowNode } from '@ag-grid-community/core';
import { MutableRefObject, useEffect, useMemo, useRef } from 'react';
-import { usePlayerEvents } from '/@/renderer/features/player/audio-player/listener/use-player-events';
+import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { useAppFocus } from '/@/renderer/hooks';
import { usePlayerSong } from '/@/renderer/store';
import { Song } from '/@/shared/types/domain-types';
@@ -75,8 +75,8 @@ export const useCurrentSongRowStyles = ({ tableRef }: UseCurrentSongRowStylesPro
api.redrawRows({ rowNodes });
}
},
- onPlayerStatus: (properties) => {
- const song = properties.song;
+ onPlayerStatus: () => {
+ const song = currentSong;
if (tableRef?.current) {
const { api, columnApi } = tableRef?.current || {};
diff --git a/src/renderer/features/now-playing/components/play-queue.tsx b/src/renderer/features/now-playing/components/play-queue.tsx
index 1866bcdfa..72a508c93 100644
--- a/src/renderer/features/now-playing/components/play-queue.tsx
+++ b/src/renderer/features/now-playing/components/play-queue.tsx
@@ -1,15 +1,9 @@
-import type {
- CellDoubleClickedEvent,
- RowClassRules,
- RowDragEvent,
- RowNode,
-} from '@ag-grid-community/core';
+import type { RowClassRules, RowDragEvent, RowNode } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import type { Ref } from 'react';
-import { useMergedRef } from '@mantine/hooks';
import '@ag-grid-community/styles/ag-theme-alpine.css';
-import isElectron from 'is-electron';
+import { useMergedRef } from '@mantine/hooks';
import debounce from 'lodash/debounce';
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
@@ -19,29 +13,22 @@ import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-tabl
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
-import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
-import { updateSong } from '/@/renderer/features/player/update-remote-song';
import { useAppFocus } from '/@/renderer/hooks';
import {
useAppStoreActions,
usePlayerQueue,
usePlayerSong,
usePlayerStatus,
- usePlayerVolume,
} from '/@/renderer/store';
import {
PersistedTableColumn,
- usePlaybackType,
useSettingsStore,
useSettingsStoreActions,
useTableSettings,
} from '/@/renderer/store/settings.store';
import { searchSongs } from '/@/renderer/utils/search-songs';
-import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
-import { PlayerType, TableType } from '/@/shared/types/types';
-
-const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
+import { TableType } from '/@/shared/types/types';
type QueueProps = {
searchTerm?: string;
@@ -52,17 +39,12 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
const tableRef = useRef(null);
const mergedRef = useMergedRef(ref, tableRef);
const queue = usePlayerQueue();
- // const { reorderQueue, setCurrentTrack } = useQueueControls();
const currentSong = usePlayerSong();
- // const previousSong = usePreviousSong();
const status = usePlayerStatus();
const { setSettings } = useSettingsStoreActions();
const { setAppStore } = useAppStoreActions();
const tableConfig = useTableSettings(type);
const [gridApi, setGridApi] = useState();
- const playbackType = usePlaybackType();
- // const { play } = usePlayerControls();
- const volume = usePlayerVolume();
const isFocused = useAppFocus();
const isFocusedRef = useRef(isFocused);
diff --git a/src/renderer/features/player/audio-player/engine/mpv-player-engine.tsx b/src/renderer/features/player/audio-player/engine/mpv-player-engine.tsx
new file mode 100644
index 000000000..9f83d18d6
--- /dev/null
+++ b/src/renderer/features/player/audio-player/engine/mpv-player-engine.tsx
@@ -0,0 +1,221 @@
+import type { RefObject } from 'react';
+
+import isElectron from 'is-electron';
+import { useEffect, useImperativeHandle, useRef, useState } from 'react';
+
+import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
+import { PlayerStatus } from '/@/shared/types/types';
+
+export interface MpvPlayerEngineHandle extends AudioPlayer {}
+
+interface MpvPlayerEngineProps {
+ currentSrc: string | undefined;
+ isMuted: boolean;
+ isTransitioning: boolean;
+ nextSrc: string | undefined;
+ onEnded: () => void;
+ onProgress: (e: PlayerOnProgressProps) => void;
+ playerRef: RefObject;
+ playerStatus: PlayerStatus;
+ speed?: number;
+ volume: number;
+}
+
+const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
+const mpvPlayerListener = isElectron() ? window.api.mpvPlayerListener : null;
+const ipc = isElectron() ? window.api.ipc : null;
+
+const PROGRESS_UPDATE_INTERVAL = 250;
+const TRANSITION_PROGRESS_INTERVAL = 10;
+
+export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
+ const {
+ currentSrc,
+ isMuted,
+ isTransitioning,
+ nextSrc,
+ onEnded,
+ onProgress,
+ playerRef,
+ playerStatus,
+ speed,
+ volume,
+ } = props;
+
+ const [internalVolume, setInternalVolume] = useState(volume / 100 || 0);
+ const [duration] = useState(0);
+ const [previousCurrentSrc, setPreviousCurrentSrc] = useState(currentSrc);
+
+ const progressIntervalRef = useRef(null);
+
+ // Update volume
+ useEffect(() => {
+ if (!mpvPlayer) {
+ return;
+ }
+
+ const vol = volume / 100 || 0;
+ setInternalVolume(vol);
+ mpvPlayer.volume(volume);
+ }, [volume]);
+
+ // Update mute status
+ useEffect(() => {
+ if (!mpvPlayer) {
+ return;
+ }
+
+ mpvPlayer.mute(isMuted);
+ }, [isMuted]);
+
+ // Update speed/playback rate
+ useEffect(() => {
+ if (!mpvPlayer) {
+ return;
+ }
+
+ if (!speed) {
+ return;
+ }
+
+ mpvPlayer.setProperties({ speed });
+ }, [speed]);
+
+ // Handle current song changes - update queue position 0
+ // When currentSrc changes, we need to update the queue
+ useEffect(() => {
+ if (!mpvPlayer) {
+ return;
+ }
+
+ // If currentSrc changed, update the queue
+ if (currentSrc !== previousCurrentSrc) {
+ if (currentSrc) {
+ // Set current song at position 0 and next song at position 1
+ mpvPlayer.setQueue(currentSrc, nextSrc, playerStatus !== PlayerStatus.PLAYING);
+ setPreviousCurrentSrc(currentSrc);
+ } else {
+ // Clear queue if no current song
+ mpvPlayer.setQueue(undefined, undefined, true);
+ setPreviousCurrentSrc(undefined);
+ }
+ } else {
+ // If currentSrc hasn't changed but nextSrc has, update position 1
+ // This happens when the next song changes but current song stays the same
+ if (currentSrc) {
+ mpvPlayer.setQueueNext(nextSrc);
+ }
+ }
+ }, [currentSrc, previousCurrentSrc, nextSrc, playerStatus]);
+
+ // Handle play/pause status
+ useEffect(() => {
+ if (!mpvPlayer) {
+ return;
+ }
+
+ if (playerStatus === PlayerStatus.PLAYING) {
+ mpvPlayer.play();
+ } else if (playerStatus === PlayerStatus.PAUSED) {
+ mpvPlayer.pause();
+ }
+ }, [playerStatus]);
+
+ // Set up progress tracking
+ useEffect(() => {
+ if (progressIntervalRef.current) {
+ clearInterval(progressIntervalRef.current);
+ }
+
+ const updateProgress = async () => {
+ if (!mpvPlayer) {
+ return;
+ }
+
+ try {
+ const time = await mpvPlayer.getCurrentTime();
+ if (time !== undefined) {
+ onProgress({
+ played: time / (duration || time + 10),
+ playedSeconds: time,
+ });
+ }
+ } catch {
+ // Handle error silently
+ }
+ };
+
+ if (currentSrc) {
+ const interval = isTransitioning
+ ? TRANSITION_PROGRESS_INTERVAL
+ : PROGRESS_UPDATE_INTERVAL;
+ progressIntervalRef.current = setInterval(updateProgress, interval);
+ updateProgress();
+ }
+
+ return () => {
+ if (progressIntervalRef.current) {
+ clearInterval(progressIntervalRef.current);
+ }
+ };
+ }, [currentSrc, isTransitioning, duration, onProgress]);
+
+ useEffect(() => {
+ if (!mpvPlayerListener) {
+ return;
+ }
+
+ const handleOnEnded = () => {
+ onEnded();
+ };
+
+ mpvPlayerListener.rendererAutoNext(handleOnEnded);
+
+ return () => {
+ ipc?.removeAllListeners('renderer-player-auto-next');
+ };
+ }, [nextSrc, onEnded]);
+
+ useImperativeHandle(playerRef, () => ({
+ decreaseVolume(by: number) {
+ const newVol = Math.max(0, internalVolume - by / 100);
+ setInternalVolume(newVol);
+ if (mpvPlayer) {
+ mpvPlayer.volume(newVol * 100);
+ }
+ },
+ increaseVolume(by: number) {
+ const newVol = Math.min(1, internalVolume + by / 100);
+ setInternalVolume(newVol);
+ if (mpvPlayer) {
+ mpvPlayer.volume(newVol * 100);
+ }
+ },
+ pause() {
+ if (mpvPlayer) {
+ mpvPlayer.pause();
+ }
+ },
+ play() {
+ if (mpvPlayer) {
+ mpvPlayer.play();
+ }
+ },
+ seekTo(seekTo: number) {
+ if (mpvPlayer) {
+ mpvPlayer.seekTo(seekTo);
+ }
+ },
+ setVolume(vol: number) {
+ const volDecimal = vol / 100 || 0;
+ setInternalVolume(volDecimal);
+ if (mpvPlayer) {
+ mpvPlayer.volume(vol);
+ }
+ },
+ }));
+
+ return ;
+};
+
+MpvPlayerEngine.displayName = 'MpvPlayerEngine';
diff --git a/src/renderer/features/player/audio-player/engine/web-player-engine.tsx b/src/renderer/features/player/audio-player/engine/web-player-engine.tsx
index 4f30a4834..0de875aa2 100644
--- a/src/renderer/features/player/audio-player/engine/web-player-engine.tsx
+++ b/src/renderer/features/player/audio-player/engine/web-player-engine.tsx
@@ -3,17 +3,10 @@ import type { RefObject } from 'react';
import { useImperativeHandle, useRef, useState } from 'react';
import ReactPlayer from 'react-player';
-import { AudioPlayer } from '/@/renderer/features/player/audio-player/types';
+import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
import { convertToLogVolume } from '/@/renderer/features/player/audio-player/utils/player-utils';
import { PlayerStatus } from '/@/shared/types/types';
-export interface OnProgressProps {
- loaded: number;
- loadedSeconds: number;
- played: number;
- playedSeconds: number;
-}
-
export interface WebPlayerEngineHandle extends AudioPlayer {
player1(): {
ref: null | ReactPlayer;
@@ -30,8 +23,8 @@ interface WebPlayerEngineProps {
isTransitioning: boolean;
onEndedPlayer1: () => void;
onEndedPlayer2: () => void;
- onProgressPlayer1: (e: OnProgressProps) => void;
- onProgressPlayer2: (e: OnProgressProps) => void;
+ onProgressPlayer1: (e: PlayerOnProgressProps) => void;
+ onProgressPlayer2: (e: PlayerOnProgressProps) => void;
playerNum: number;
playerRef: RefObject;
playerStatus: PlayerStatus;
diff --git a/src/renderer/features/player/audio-player/hooks/use-main-player-listener.tsx b/src/renderer/features/player/audio-player/hooks/use-main-player-listener.tsx
new file mode 100644
index 000000000..615958212
--- /dev/null
+++ b/src/renderer/features/player/audio-player/hooks/use-main-player-listener.tsx
@@ -0,0 +1,147 @@
+import { t } from 'i18next';
+import isElectron from 'is-electron';
+import { useCallback, useEffect } from 'react';
+
+import { usePlayerActions } from '/@/renderer/store';
+import { toast } from '/@/shared/components/toast/toast';
+
+const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
+const mpvPlayerListener = isElectron() ? window.api.mpvPlayerListener : null;
+const ipc = isElectron() ? window.api.ipc : null;
+
+export const useMainPlayerListener = () => {
+ const {
+ decreaseVolume,
+ increaseVolume,
+ mediaAutoNext,
+ mediaNext,
+ mediaPause,
+ mediaPlay,
+ mediaPrevious,
+ mediaSkipBackward,
+ mediaSkipForward,
+ mediaStop,
+ mediaToggleMute,
+ mediaTogglePlayPause,
+ toggleRepeat,
+ toggleShuffle,
+ } = usePlayerActions();
+
+ const handleMpvError = useCallback(
+ (message: string) => {
+ toast.error({
+ id: 'mpv-error',
+ message,
+ title: t('error.playbackError', { postProcess: 'sentenceCase' }) as string,
+ });
+ mediaPause();
+ mpvPlayer!.pause();
+ },
+ [mediaPause],
+ );
+
+ useEffect(() => {
+ if (!mpvPlayerListener) {
+ return;
+ }
+
+ mpvPlayerListener.rendererPlayPause(() => {
+ mediaTogglePlayPause();
+ });
+
+ mpvPlayerListener.rendererNext(() => {
+ mediaNext();
+ });
+
+ mpvPlayerListener.rendererPrevious(() => {
+ mediaPrevious();
+ });
+
+ mpvPlayerListener.rendererPlayPause(() => {
+ mediaTogglePlayPause();
+ });
+
+ mpvPlayerListener.rendererPlay(() => {
+ mediaPlay();
+ });
+
+ mpvPlayerListener.rendererPause(() => {
+ mediaPause();
+ });
+
+ mpvPlayerListener.rendererStop(() => {
+ mediaStop();
+ });
+
+ mpvPlayerListener.rendererSkipForward(() => {
+ mediaSkipForward();
+ });
+
+ mpvPlayerListener.rendererSkipBackward(() => {
+ mediaSkipBackward();
+ });
+
+ mpvPlayerListener.rendererAutoNext(() => {
+ mediaAutoNext();
+ });
+
+ mpvPlayerListener.rendererToggleShuffle(() => {
+ toggleShuffle();
+ });
+
+ mpvPlayerListener.rendererToggleRepeat(() => {
+ toggleRepeat();
+ });
+
+ mpvPlayerListener.rendererVolumeMute(() => {
+ mediaToggleMute();
+ });
+
+ mpvPlayerListener.rendererVolumeUp(() => {
+ increaseVolume(1);
+ });
+
+ mpvPlayerListener.rendererVolumeDown(() => {
+ decreaseVolume(1);
+ });
+
+ mpvPlayerListener.rendererError((_event: any, message: string) => {
+ handleMpvError(message);
+ });
+
+ return () => {
+ ipc?.removeAllListeners('renderer-player-play-pause');
+ ipc?.removeAllListeners('renderer-player-next');
+ ipc?.removeAllListeners('renderer-player-previous');
+ ipc?.removeAllListeners('renderer-player-play-pause');
+ ipc?.removeAllListeners('renderer-player-play');
+ ipc?.removeAllListeners('renderer-player-pause');
+ ipc?.removeAllListeners('renderer-player-stop');
+ ipc?.removeAllListeners('renderer-player-skip-forward');
+ ipc?.removeAllListeners('renderer-player-skip-backward');
+ ipc?.removeAllListeners('renderer-player-auto-next');
+ ipc?.removeAllListeners('renderer-player-toggle-shuffle');
+ ipc?.removeAllListeners('renderer-player-toggle-repeat');
+ ipc?.removeAllListeners('renderer-player-volume-mute');
+ ipc?.removeAllListeners('renderer-player-volume-up');
+ ipc?.removeAllListeners('renderer-player-volume-down');
+ ipc?.removeAllListeners('renderer-player-error');
+ };
+ }, [
+ decreaseVolume,
+ handleMpvError,
+ increaseVolume,
+ mediaAutoNext,
+ mediaNext,
+ mediaPause,
+ mediaPlay,
+ mediaPrevious,
+ mediaSkipForward,
+ mediaSkipBackward,
+ mediaStop,
+ mediaToggleMute,
+ mediaTogglePlayPause,
+ toggleRepeat,
+ toggleShuffle,
+ ]);
+};
diff --git a/src/renderer/features/player/audio-player/listener/player-events.tsx b/src/renderer/features/player/audio-player/hooks/use-player-events.ts
similarity index 84%
rename from src/renderer/features/player/audio-player/listener/player-events.tsx
rename to src/renderer/features/player/audio-player/hooks/use-player-events.ts
index bfd467cce..e5ac3d471 100644
--- a/src/renderer/features/player/audio-player/listener/player-events.tsx
+++ b/src/renderer/features/player/audio-player/hooks/use-player-events.ts
@@ -1,5 +1,6 @@
+import { useEffect } from 'react';
+
import {
- QueueData,
subscribeCurrentTrack,
subscribePlayerMute,
subscribePlayerProgress,
@@ -9,14 +10,14 @@ import {
subscribePlayerStatus,
subscribePlayerVolume,
} from '/@/renderer/store';
-import { QueueSong } from '/@/shared/types/domain-types';
+import { QueueData, QueueSong } from '/@/shared/types/domain-types';
import { PlayerStatus } from '/@/shared/types/types';
-export interface PlayerEvents {
+interface PlayerEvents {
cleanup: () => void;
}
-export interface PlayerEventsCallbacks {
+interface PlayerEventsCallbacks {
onCurrentSongChange?: (
properties: { index: number; song: QueueSong | undefined },
prev: { index: number; song: QueueSong | undefined },
@@ -34,7 +35,18 @@ export interface PlayerEventsCallbacks {
onPlayerVolume?: (properties: { volume: number }, prev: { volume: number }) => void;
}
-export function createPlayerEvents(callbacks: PlayerEventsCallbacks): PlayerEvents {
+export function usePlayerEvents(callbacks: PlayerEventsCallbacks, deps: React.DependencyList) {
+ useEffect(() => {
+ const engine = createPlayerEvents(callbacks);
+
+ return () => {
+ engine.cleanup();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [...deps]);
+}
+
+function createPlayerEvents(callbacks: PlayerEventsCallbacks): PlayerEvents {
const unsubscribers: (() => void)[] = [];
// Subscribe to current track changes
diff --git a/src/renderer/features/player/audio-player/listener/use-player-events.ts b/src/renderer/features/player/audio-player/listener/use-player-events.ts
deleted file mode 100644
index 758686bbb..000000000
--- a/src/renderer/features/player/audio-player/listener/use-player-events.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useEffect } from 'react';
-
-import {
- createPlayerEvents,
- PlayerEventsCallbacks,
-} from '/@/renderer/features/player/audio-player/listener/player-events';
-
-export function usePlayerEvents(callbacks: PlayerEventsCallbacks, deps: React.DependencyList) {
- useEffect(() => {
- const engine = createPlayerEvents(callbacks);
-
- return () => {
- engine.cleanup();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [...deps]);
-}
diff --git a/src/renderer/features/player/audio-player/mpv-player.tsx b/src/renderer/features/player/audio-player/mpv-player.tsx
new file mode 100644
index 000000000..532f29ba1
--- /dev/null
+++ b/src/renderer/features/player/audio-player/mpv-player.tsx
@@ -0,0 +1,126 @@
+import { useCallback, useRef, useState } from 'react';
+
+import { MpvPlayerEngine, MpvPlayerEngineHandle } from './engine/mpv-player-engine';
+
+import { useMainPlayerListener } from '/@/renderer/features/player/audio-player/hooks/use-main-player-listener';
+import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
+import { PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
+import {
+ usePlayerActions,
+ usePlayerData,
+ usePlayerMuted,
+ usePlayerProperties,
+ usePlayerVolume,
+} from '/@/renderer/store';
+import { PlayerStatus } from '/@/shared/types/types';
+
+const PLAY_PAUSE_FADE_DURATION = 300;
+const PLAY_PAUSE_FADE_INTERVAL = 10;
+
+export function MpvPlayer() {
+ const playerRef = useRef(null);
+ const { currentSong, nextSong, status } = usePlayerData();
+ const { mediaAutoNext, setTimestamp } = usePlayerActions();
+ const { speed } = usePlayerProperties();
+ const isMuted = usePlayerMuted();
+ const volume = usePlayerVolume();
+
+ const [localPlayerStatus, setLocalPlayerStatus] = useState(status);
+ const [isTransitioning, setIsTransitioning] = useState(false);
+
+ const fadeAndSetStatus = useCallback(
+ async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => {
+ if (isTransitioning) {
+ return setLocalPlayerStatus(status);
+ }
+
+ const steps = duration / PLAY_PAUSE_FADE_INTERVAL;
+ const volumeStep = (endVolume - startVolume) / steps;
+ let currentStep = 0;
+
+ const promise = new Promise((resolve) => {
+ const interval = setInterval(() => {
+ currentStep++;
+ const newVolume = startVolume + volumeStep * currentStep;
+
+ playerRef.current?.setVolume(newVolume);
+
+ if (currentStep >= steps) {
+ clearInterval(interval);
+ setIsTransitioning(false);
+ resolve(true);
+ }
+ }, PLAY_PAUSE_FADE_INTERVAL);
+ });
+
+ if (status === PlayerStatus.PAUSED) {
+ await promise;
+ setLocalPlayerStatus(status);
+ } else if (status === PlayerStatus.PLAYING) {
+ setLocalPlayerStatus(status);
+ await promise;
+ }
+ },
+ [isTransitioning],
+ );
+
+ const onProgress = useCallback(
+ (e: PlayerOnProgressProps) => {
+ setTimestamp(Number(e.playedSeconds.toFixed(0)));
+ },
+ [setTimestamp],
+ );
+
+ const handleOnEnded = useCallback(() => {
+ // When mpv auto-advances to the next song (position 1 becomes position 0),
+ // we need to update the player store first, then update the mpv queue with the new next song
+ // This follows the same pattern as the old useCenterControls implementation
+ const playerData = mediaAutoNext();
+
+ // Update the mpv queue with the new next song
+ // The engine will handle the queue update through the useEffect when nextSong changes
+ playerRef.current?.setVolume(volume);
+ setIsTransitioning(false);
+
+ return playerData;
+ }, [mediaAutoNext, volume, setIsTransitioning]);
+
+ usePlayerEvents(
+ {
+ onPlayerSeekToTimestamp: (properties) => {
+ const timestamp = properties.timestamp;
+ playerRef.current?.seekTo(timestamp);
+ },
+ onPlayerStatus: async (properties) => {
+ const status = properties.status;
+ if (status === PlayerStatus.PAUSED) {
+ fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PAUSED);
+ } else if (status === PlayerStatus.PLAYING) {
+ fadeAndSetStatus(0, volume, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PLAYING);
+ }
+ },
+ onPlayerVolume: (properties) => {
+ const volume = properties.volume;
+ playerRef.current?.setVolume(volume);
+ },
+ },
+ [volume, isTransitioning, fadeAndSetStatus],
+ );
+
+ useMainPlayerListener();
+
+ return (
+
+ );
+}
diff --git a/src/renderer/features/player/audio-player/types.ts b/src/renderer/features/player/audio-player/types.ts
index 6273510e7..e9d19776d 100644
--- a/src/renderer/features/player/audio-player/types.ts
+++ b/src/renderer/features/player/audio-player/types.ts
@@ -6,3 +6,8 @@ export interface AudioPlayer {
seekTo(seekTo: number): void;
setVolume(volume: number): void;
}
+
+export interface PlayerOnProgressProps {
+ played: number;
+ playedSeconds: number;
+}
diff --git a/src/renderer/features/player/audio-player/web-player.tsx b/src/renderer/features/player/audio-player/web-player.tsx
index 4d675af8e..2d7ba6f9b 100644
--- a/src/renderer/features/player/audio-player/web-player.tsx
+++ b/src/renderer/features/player/audio-player/web-player.tsx
@@ -2,11 +2,14 @@ import type { Dispatch } from 'react';
import type ReactPlayer from 'react-player';
import { useCallback, useRef, useState } from 'react';
-import { OnProgressProps } from 'react-player/base';
-import { WebPlayerEngine, WebPlayerEngineHandle } from './engine/web-player-engine';
-
-import { usePlayerEvents } from '/@/renderer/features/player/audio-player/listener/use-player-events';
+import {
+ WebPlayerEngine,
+ WebPlayerEngineHandle,
+} from '/@/renderer/features/player/audio-player/engine/web-player-engine';
+import { useMainPlayerListener } from '/@/renderer/features/player/audio-player/hooks/use-main-player-listener';
+import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
+import { PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
import {
usePlayerActions,
usePlayerData,
@@ -22,7 +25,7 @@ const PLAY_PAUSE_FADE_INTERVAL = 10;
export function WebPlayer() {
const playerRef = useRef(null);
const { num, player1, player2, status } = usePlayerData();
- const { mediaAutoNext, setProgress } = usePlayerActions();
+ const { mediaAutoNext, setTimestamp } = usePlayerActions();
const { crossfadeDuration, speed, transitionType } = usePlayerProperties();
const isMuted = usePlayerMuted();
const volume = usePlayerVolume();
@@ -67,11 +70,11 @@ export function WebPlayer() {
);
const onProgressPlayer1 = useCallback(
- (e: OnProgressProps) => {
+ (e: PlayerOnProgressProps) => {
if (transitionType === 'crossfade' && num === 1) {
- setProgress(Number(e.playedSeconds.toFixed(0)));
+ setTimestamp(Number(e.playedSeconds.toFixed(0)));
} else if (transitionType === 'gapless') {
- setProgress(Number(e.playedSeconds.toFixed(0)));
+ setTimestamp(Number(e.playedSeconds.toFixed(0)));
}
if (!playerRef.current?.player1()) {
@@ -105,15 +108,15 @@ export function WebPlayer() {
break;
}
},
- [crossfadeDuration, isTransitioning, num, setProgress, transitionType, volume],
+ [crossfadeDuration, isTransitioning, num, setTimestamp, transitionType, volume],
);
const onProgressPlayer2 = useCallback(
- (e: OnProgressProps) => {
+ (e: PlayerOnProgressProps) => {
if (transitionType === PlayerStyle.CROSSFADE && num === 2) {
- setProgress(Number(e.playedSeconds.toFixed(0)));
+ setTimestamp(Number(e.playedSeconds.toFixed(0)));
} else if (transitionType === PlayerStyle.GAPLESS) {
- setProgress(Number(e.playedSeconds.toFixed(0)));
+ setTimestamp(Number(e.playedSeconds.toFixed(0)));
}
if (!playerRef.current?.player2()) {
@@ -147,7 +150,7 @@ export function WebPlayer() {
break;
}
},
- [crossfadeDuration, isTransitioning, num, setProgress, transitionType, volume],
+ [crossfadeDuration, isTransitioning, num, setTimestamp, transitionType, volume],
);
const handleOnEndedPlayer1 = useCallback(() => {
@@ -202,6 +205,8 @@ export function WebPlayer() {
[volume, num, isTransitioning],
);
+ useMainPlayerListener();
+
return (
{
{playbackType === PlayerType.WEB && }
+ {playbackType === PlayerType.LOCAL && }
>
);
};
diff --git a/src/renderer/features/player/hooks/use-center-controls.ts b/src/renderer/features/player/hooks/use-center-controls.ts
index 1623c853a..ea9687b06 100644
--- a/src/renderer/features/player/hooks/use-center-controls.ts
+++ b/src/renderer/features/player/hooks/use-center-controls.ts
@@ -4,7 +4,7 @@
// import { useTranslation } from 'react-i18next';
// import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
-// import { updateSong } from '/@/renderer/features/player/update-remote-song';
+// import { updateSong } from '/@/renderer/features/plaayer/update-remote-song';
// import {
// usePlayerNum,
// usePlayerStatus,
diff --git a/src/renderer/features/settings/components/playback/audio-settings.tsx b/src/renderer/features/settings/components/playback/audio-settings.tsx
index 8c41b0973..4e80439bb 100644
--- a/src/renderer/features/settings/components/playback/audio-settings.tsx
+++ b/src/renderer/features/settings/components/playback/audio-settings.tsx
@@ -6,14 +6,13 @@ import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
-import { usePlayerStatus, usePlayerStore } from '/@/renderer/store';
+import { usePlayerStatus } from '/@/renderer/store';
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
-import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
import { Select } from '/@/shared/components/select/select';
import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch';
import { toast } from '/@/shared/components/toast/toast';
-import { CrossfadeStyle, PlayerStyle, PlayerType, PlayerStatus } from '/@/shared/types/types';
+import { CrossfadeStyle, PlayerStatus, PlayerStyle, PlayerType } from '/@/shared/types/types';
const ipc = isElectron() ? window.api.ipc : null;
@@ -65,10 +64,6 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
onChange={(e) => {
setSettings({ playback: { ...settings, type: e as PlayerType } });
ipc?.send('settings-set', { property: 'playbackType', value: e });
- if (isElectron() && e === PlayerType.LOCAL) {
- const queueData = usePlayerStore.getState().actions.getPlayerData();
- setQueue(queueData);
- }
}}
/>
),
diff --git a/src/renderer/store/player.store.ts b/src/renderer/store/player.store.ts
index e4fc7f487..c7ba1b35e 100644
--- a/src/renderer/store/player.store.ts
+++ b/src/renderer/store/player.store.ts
@@ -8,7 +8,7 @@ import { useShallow } from 'zustand/react/shallow';
import { createSelectors } from '/@/renderer/lib/zustand';
import { useSettingsStore } from '/@/renderer/store/settings.store';
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
-import { QueueSong, Song } from '/@/shared/types/domain-types';
+import { PlayerData, QueueData, QueueSong, Song } from '/@/shared/types/domain-types';
import {
Play,
PlayerQueueType,
@@ -18,34 +18,8 @@ import {
PlayerStyle,
} from '/@/shared/types/types';
-export interface PlayerData {
- currentSong: QueueSong | undefined;
- index: number;
- muted: boolean;
- nextSong: QueueSong | undefined;
- num: 1 | 2;
- player1: QueueSong | undefined;
- player2: QueueSong | undefined;
- previousSong: QueueSong | undefined;
- queue: QueueData;
- queueLength: number;
- repeat: PlayerRepeat;
- shuffle: PlayerShuffle;
- speed: number;
- status: PlayerStatus;
- transitionType: PlayerStyle;
- volume: number;
-}
-
export interface PlayerState extends Actions, State {}
-export interface QueueData {
- default: string[];
- priority: string[];
- shuffled: string[];
- songs: QueueSong[];
-}
-
export type QueueGroupingProperty = keyof QueueSong;
interface Actions {
@@ -77,11 +51,11 @@ interface Actions {
moveSelectedToNext: (items: QueueSong[]) => void;
moveSelectedToTop: (items: QueueSong[]) => void;
setCrossfadeDuration: (duration: number) => void;
- setProgress: (timestamp: number) => void;
setQueueType: (queueType: PlayerQueueType) => void;
setRepeat: (repeat: PlayerRepeat) => void;
setShuffle: (shuffle: PlayerShuffle) => void;
setSpeed: (speed: number) => void;
+ setTimestamp: (timestamp: number) => void;
setTransitionType: (transitionType: PlayerStyle) => void;
setVolume: (volume: number) => void;
shuffle: () => void;
@@ -131,15 +105,9 @@ export const usePlayerStoreBase = create()(
case Play.LAST: {
set((state) => {
const currentIndex = state.player.index;
- const existingIds = new Set(
- state.queue.songs.map((s) => s._uniqueId),
- );
-
- // Add new songs to songs array (avoiding duplicates)
+ // Add new songs to songs object
newItems.forEach((item) => {
- if (!existingIds.has(item._uniqueId)) {
- state.queue.songs.push(item);
- }
+ state.queue.songs[item._uniqueId] = item;
});
state.queue.default = [
@@ -163,15 +131,9 @@ export const usePlayerStoreBase = create()(
case Play.NEXT: {
set((state) => {
const currentIndex = state.player.index;
- const existingIds = new Set(
- state.queue.songs.map((s) => s._uniqueId),
- );
-
- // Add new songs to songs array (avoiding duplicates)
+ // Add new songs to songs object
newItems.forEach((item) => {
- if (!existingIds.has(item._uniqueId)) {
- state.queue.songs.push(item);
- }
+ state.queue.songs[item._uniqueId] = item;
});
state.queue.default = [
@@ -195,15 +157,9 @@ export const usePlayerStoreBase = create()(
}
case Play.NOW: {
set((state) => {
- const existingIds = new Set(
- state.queue.songs.map((s) => s._uniqueId),
- );
-
- // Add new songs to songs array (avoiding duplicates)
+ // Add new songs to songs object
newItems.forEach((item) => {
- if (!existingIds.has(item._uniqueId)) {
- state.queue.songs.push(item);
- }
+ state.queue.songs[item._uniqueId] = item;
});
state.queue.default = [];
@@ -226,15 +182,9 @@ export const usePlayerStoreBase = create()(
switch (playType) {
case Play.LAST: {
set((state) => {
- const existingIds = new Set(
- state.queue.songs.map((s) => s._uniqueId),
- );
-
- // Add new songs to songs array (avoiding duplicates)
+ // Add new songs to songs object
newItems.forEach((item) => {
- if (!existingIds.has(item._uniqueId)) {
- state.queue.songs.push(item);
- }
+ state.queue.songs[item._uniqueId] = item;
});
state.queue.priority = [
@@ -254,15 +204,10 @@ export const usePlayerStoreBase = create()(
const currentIndex = state.player.index;
const isInPriority =
currentIndex < state.queue.priority.length;
- const existingIds = new Set(
- state.queue.songs.map((s) => s._uniqueId),
- );
- // Add new songs to songs array (avoiding duplicates)
+ // Add new songs to songs object
newItems.forEach((item) => {
- if (!existingIds.has(item._uniqueId)) {
- state.queue.songs.push(item);
- }
+ state.queue.songs[item._uniqueId] = item;
});
if (isInPriority) {
@@ -291,15 +236,9 @@ export const usePlayerStoreBase = create()(
}
case Play.NOW: {
set((state) => {
- const existingIds = new Set(
- state.queue.songs.map((s) => s._uniqueId),
- );
-
- // Add new songs to songs array (avoiding duplicates)
+ // Add new songs to songs object
newItems.forEach((item) => {
- if (!existingIds.has(item._uniqueId)) {
- state.queue.songs.push(item);
- }
+ state.queue.songs[item._uniqueId] = item;
});
state.queue.default = [];
@@ -376,13 +315,9 @@ export const usePlayerStoreBase = create()(
const queueType = getQueueType();
set((state) => {
- const existingIds = new Set(state.queue.songs.map((s) => s._uniqueId));
-
- // Add new songs to songs array (avoiding duplicates)
+ // Add new songs to songs object
newItems.forEach((item) => {
- if (!existingIds.has(item._uniqueId)) {
- state.queue.songs.push(item);
- }
+ state.queue.songs[item._uniqueId] = item;
});
if (queueType === PlayerQueueType.DEFAULT) {
@@ -459,7 +394,7 @@ export const usePlayerStoreBase = create()(
state.player.index = -1;
state.queue.default = [];
state.queue.priority = [];
- state.queue.songs = [];
+ state.queue.songs = {};
});
},
clearSelected: (items: QueueSong[]) => {
@@ -474,14 +409,18 @@ export const usePlayerStoreBase = create()(
(id) => !uniqueIds.includes(id),
);
- // Remove songs from songs array if they're not in default or priority
+ // Remove songs from songs object if they're not in default or priority
const remainingIds = new Set([
...state.queue.default,
...state.queue.priority,
]);
- state.queue.songs = state.queue.songs.filter((song) =>
- remainingIds.has(song._uniqueId),
- );
+ const filteredSongs: Record = {};
+ Object.values(state.queue.songs).forEach((song) => {
+ if (remainingIds.has(song._uniqueId)) {
+ filteredSongs[song._uniqueId] = song;
+ }
+ });
+ state.queue.songs = filteredSongs;
const newQueue = [...state.queue.priority, ...state.queue.default];
@@ -539,9 +478,7 @@ export const usePlayerStoreBase = create()(
getQueueOrder: () => {
const queueType = getQueueType();
const state = get();
- const songsMap = new Map(
- state.queue.songs.map((song) => [song._uniqueId, song]),
- );
+ const songsMap = new Map(Object.entries(state.queue.songs));
if (queueType === PlayerQueueType.PRIORITY) {
const defaultIds = state.queue.default;
@@ -704,7 +641,7 @@ export const usePlayerStoreBase = create()(
state.player.index = -1;
state.queue.default = [];
state.queue.priority = [];
- state.queue.songs = [];
+ state.queue.songs = {};
});
},
mediaToggleMute: () => {
@@ -726,12 +663,12 @@ export const usePlayerStoreBase = create()(
const itemUniqueIds = items.map((item) => item._uniqueId);
set((state) => {
- const existingIds = new Set(state.queue.songs.map((s) => s._uniqueId));
+ const existingIds = new Set(Object.keys(state.queue.songs));
- // Add new songs to songs array (avoiding duplicates)
+ // Add new songs to songs object (avoiding duplicates)
items.forEach((item) => {
if (!existingIds.has(item._uniqueId)) {
- state.queue.songs.push(item);
+ state.queue.songs[item._uniqueId] = item;
}
});
@@ -829,13 +766,10 @@ export const usePlayerStoreBase = create()(
moveSelectedToBottom: (items: QueueSong[]) => {
set((state) => {
const uniqueIds = items.map((item) => item._uniqueId);
- const existingIds = new Set(state.queue.songs.map((s) => s._uniqueId));
- // Add new songs to songs array (avoiding duplicates)
+ // Add new songs to songs object
items.forEach((item) => {
- if (!existingIds.has(item._uniqueId)) {
- state.queue.songs.push(item);
- }
+ state.queue.songs[item._uniqueId] = item;
});
if (state.player.queueType === PlayerQueueType.PRIORITY) {
@@ -878,13 +812,10 @@ export const usePlayerStoreBase = create()(
const uniqueId = currentTrack?._uniqueId;
const uniqueIds = items.map((item) => item._uniqueId);
- const existingIds = new Set(state.queue.songs.map((s) => s._uniqueId));
- // Add new songs to songs array (avoiding duplicates)
+ // Add new songs to songs object
items.forEach((item) => {
- if (!existingIds.has(item._uniqueId)) {
- state.queue.songs.push(item);
- }
+ state.queue.songs[item._uniqueId] = item;
});
if (queueType === PlayerQueueType.DEFAULT) {
@@ -966,13 +897,10 @@ export const usePlayerStoreBase = create()(
moveSelectedToTop: (items: QueueSong[]) => {
set((state) => {
const uniqueIds = items.map((item) => item._uniqueId);
- const existingIds = new Set(state.queue.songs.map((s) => s._uniqueId));
- // Add new songs to songs array (avoiding duplicates)
+ // Add new songs to songs object
items.forEach((item) => {
- if (!existingIds.has(item._uniqueId)) {
- state.queue.songs.push(item);
- }
+ state.queue.songs[item._uniqueId] = item;
});
if (state.player.queueType === PlayerQueueType.PRIORITY) {
@@ -1026,7 +954,7 @@ export const usePlayerStoreBase = create()(
default: [],
priority: [],
shuffled: [],
- songs: [],
+ songs: {},
},
setCrossfadeDuration: (duration: number) => {
set((state) => {
@@ -1034,11 +962,6 @@ export const usePlayerStoreBase = create()(
state.player.crossfadeDuration = normalizedDuration;
});
},
- setProgress: (timestamp: number) => {
- set((state) => {
- state.player.timestamp = timestamp;
- });
- },
setQueueType: (queueType: PlayerQueueType) => {
set((state) => {
// From default -> priority, move all items from default to priority
@@ -1075,6 +998,11 @@ export const usePlayerStoreBase = create()(
state.player.speed = normalizedSpeed;
});
},
+ setTimestamp: (timestamp: number) => {
+ set((state) => {
+ state.player.timestamp = timestamp;
+ });
+ },
setTransitionType: (transitionType: PlayerStyle) => {
set((state) => {
state.player.transitionType = transitionType;
@@ -1178,10 +1106,10 @@ export const usePlayerActions = () => {
moveSelectedToNext: state.moveSelectedToNext,
moveSelectedToTop: state.moveSelectedToTop,
setCrossfadeDuration: state.setCrossfadeDuration,
- setProgress: state.setProgress,
setRepeat: state.setRepeat,
setShuffle: state.setShuffle,
setSpeed: state.setSpeed,
+ setTimestamp: state.setTimestamp,
setTransitionType: state.setTransitionType,
setVolume: state.setVolume,
shuffle: state.shuffle,
@@ -1379,13 +1307,12 @@ export const usePlayerData = (): PlayerData => {
export const updateQueueFavorites = (ids: string[], favorite: boolean) => {
usePlayerStoreBase.setState((state) => {
- // Update songs in the songs array
- state.queue.songs.forEach((song) => {
+ // Update songs in the songs object
+ Object.values(state.queue.songs).forEach((song) => {
if (ids.includes(song.id)) {
song.userFavorite = favorite;
}
});
- // default and priority are just uniqueId arrays, so they don't need updating
});
};
@@ -1434,18 +1361,26 @@ export const usePlayerNum = () => {
};
export const usePlayerQueue = () => {
- return usePlayerStoreBase((state) => {
- const queueType = state.player.queueType;
+ return usePlayerStoreBase(
+ useShallow((state) => {
+ const queueType = state.player.queueType;
- switch (queueType) {
- case PlayerQueueType.DEFAULT:
- return state.queue.default;
- case PlayerQueueType.PRIORITY:
- return state.queue.priority;
- default:
- return state.queue.default;
- }
- });
+ switch (queueType) {
+ case PlayerQueueType.DEFAULT: {
+ const queue = state.queue.default;
+ return queue.map((id) => state.queue.songs[id]);
+ }
+ case PlayerQueueType.PRIORITY: {
+ const priorityQueue = state.queue.priority;
+ return priorityQueue.map((id) => state.queue.songs[id]);
+ }
+ default: {
+ const defaultQueue = state.queue.default;
+ return defaultQueue.map((id) => state.queue.songs[id]);
+ }
+ }
+ }),
+ );
};
function getQueueType() {
diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts
index a5e0c67b9..afc14ab4e 100644
--- a/src/shared/types/domain-types.ts
+++ b/src/shared/types/domain-types.ts
@@ -25,7 +25,7 @@ import {
NDUserListSort,
} from '/@/shared/api/navidrome/navidrome-types';
import { ServerFeatures } from '/@/shared/types/features-types';
-import { PlayerStatus } from '/@/shared/types/types';
+import { PlayerRepeat, PlayerShuffle, PlayerStatus, PlayerStyle } from '/@/shared/types/types';
export enum LibraryItem {
ALBUM = 'album',
@@ -58,25 +58,29 @@ export type AnyLibraryItems =
| Song[];
export interface PlayerData {
- current: {
- index: number;
- nextIndex?: number;
- player: 1 | 2;
- previousIndex?: number;
- shuffledIndex: number;
- song?: QueueSong;
- status: PlayerStatus;
- };
- player1?: QueueSong;
- player2?: QueueSong;
+ currentSong: QueueSong | undefined;
+ index: number;
+ muted: boolean;
+ nextSong: QueueSong | undefined;
+ num: 1 | 2;
+ player1: QueueSong | undefined;
+ player2: QueueSong | undefined;
+ previousSong: QueueSong | undefined;
queue: QueueData;
+ queueLength: number;
+ repeat: PlayerRepeat;
+ shuffle: PlayerShuffle;
+ speed: number;
+ status: PlayerStatus;
+ transitionType: PlayerStyle;
+ volume: number;
}
export interface QueueData {
- current?: QueueSong;
- length: number;
- next?: QueueSong;
- previous?: QueueSong;
+ default: string[];
+ priority: string[];
+ shuffled: string[];
+ songs: Record;
}
export type QueueSong = Song & {