mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-16 16:34:24 +02:00
fix mpv player queue behavior to handle gapless playback
This commit is contained in:
@@ -371,16 +371,6 @@ ipcMain.on('player-set-queue', async (_event, current?: string, next?: string, p
|
|||||||
// Replaces the queue in position 1 to the given data
|
// Replaces the queue in position 1 to the given data
|
||||||
ipcMain.on('player-set-queue-next', async (_event, url?: string) => {
|
ipcMain.on('player-set-queue-next', async (_event, url?: string) => {
|
||||||
try {
|
try {
|
||||||
const size = await getMpvInstance()?.getPlaylistSize();
|
|
||||||
|
|
||||||
if (!size) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size > 1) {
|
|
||||||
await getMpvInstance()?.playlistRemove(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
await getMpvInstance()?.load(url, 'append');
|
await getMpvInstance()?.load(url, 'append');
|
||||||
}
|
}
|
||||||
@@ -394,6 +384,7 @@ ipcMain.on('player-auto-next', async (_event, url?: string) => {
|
|||||||
// Always keep the current song as position 0 in the mpv queue
|
// Always keep the current song as position 0 in the mpv queue
|
||||||
// This allows us to easily set update the next song in the queue without
|
// This allows us to easily set update the next song in the queue without
|
||||||
// disturbing the currently playing song
|
// disturbing the currently playing song
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await getMpvInstance()
|
await getMpvInstance()
|
||||||
?.playlistRemove(0)
|
?.playlistRemove(0)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { LibraryItem } from '/@/shared/types/domain-types';
|
|||||||
export type EventMap = {
|
export type EventMap = {
|
||||||
ITEM_LIST_REFRESH: ItemListRefreshEventPayload;
|
ITEM_LIST_REFRESH: ItemListRefreshEventPayload;
|
||||||
ITEM_LIST_UPDATE_ITEM: ItemListUpdateItemEventPayload;
|
ITEM_LIST_UPDATE_ITEM: ItemListUpdateItemEventPayload;
|
||||||
|
MEDIA_NEXT: MediaNextEventPayload;
|
||||||
|
MEDIA_PREV: MediaPrevEventPayload;
|
||||||
|
PLAYER_PLAY: PlayerPlayEventPayload;
|
||||||
PLAYLIST_MOVE_DOWN: PlaylistMoveEventPayload;
|
PLAYLIST_MOVE_DOWN: PlaylistMoveEventPayload;
|
||||||
PLAYLIST_MOVE_TO_BOTTOM: PlaylistMoveEventPayload;
|
PLAYLIST_MOVE_TO_BOTTOM: PlaylistMoveEventPayload;
|
||||||
PLAYLIST_MOVE_TO_TOP: PlaylistMoveEventPayload;
|
PLAYLIST_MOVE_TO_TOP: PlaylistMoveEventPayload;
|
||||||
@@ -22,6 +25,21 @@ export type ItemListUpdateItemEventPayload = {
|
|||||||
key: string;
|
key: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MediaNextEventPayload = {
|
||||||
|
currentIndex: number;
|
||||||
|
nextIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MediaPrevEventPayload = {
|
||||||
|
currentIndex: number;
|
||||||
|
prevIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlayerPlayEventPayload = {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type PlaylistMoveEventPayload = {
|
export type PlaylistMoveEventPayload = {
|
||||||
playlistId: string;
|
playlistId: string;
|
||||||
sourceIds: string[];
|
sourceIds: string[];
|
||||||
|
|||||||
@@ -3,18 +3,23 @@ import type { RefObject } from 'react';
|
|||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { useEffect, useImperativeHandle, useRef, useState } from 'react';
|
import { useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
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 { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
|
||||||
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
||||||
import { useSettingsStore } from '/@/renderer/store';
|
import {
|
||||||
|
usePlaybackSettings,
|
||||||
|
usePlayerActions,
|
||||||
|
usePlayerStore,
|
||||||
|
useSettingsStore,
|
||||||
|
} from '/@/renderer/store';
|
||||||
import { PlayerStatus } from '/@/shared/types/types';
|
import { PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
export interface MpvPlayerEngineHandle extends AudioPlayer {}
|
export interface MpvPlayerEngineHandle extends AudioPlayer {}
|
||||||
|
|
||||||
interface MpvPlayerEngineProps {
|
interface MpvPlayerEngineProps {
|
||||||
currentSrc: string | undefined;
|
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
isTransitioning: boolean;
|
isTransitioning: boolean;
|
||||||
nextSrc: string | undefined;
|
|
||||||
onEnded: () => void;
|
onEnded: () => void;
|
||||||
onProgress: (e: PlayerOnProgressProps) => void;
|
onProgress: (e: PlayerOnProgressProps) => void;
|
||||||
playerRef: RefObject<MpvPlayerEngineHandle | null>;
|
playerRef: RefObject<MpvPlayerEngineHandle | null>;
|
||||||
@@ -28,14 +33,11 @@ const mpvPlayerListener = isElectron() ? window.api.mpvPlayerListener : null;
|
|||||||
const ipc = isElectron() ? window.api.ipc : null;
|
const ipc = isElectron() ? window.api.ipc : null;
|
||||||
|
|
||||||
const PROGRESS_UPDATE_INTERVAL = 250;
|
const PROGRESS_UPDATE_INTERVAL = 250;
|
||||||
const TRANSITION_PROGRESS_INTERVAL = 10;
|
|
||||||
|
|
||||||
export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
||||||
const {
|
const {
|
||||||
currentSrc,
|
|
||||||
isMuted,
|
isMuted,
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
nextSrc,
|
|
||||||
onEnded,
|
onEnded,
|
||||||
onProgress,
|
onProgress,
|
||||||
playerRef,
|
playerRef,
|
||||||
@@ -46,49 +48,74 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
|||||||
|
|
||||||
const [internalVolume, setInternalVolume] = useState(volume / 100 || 0);
|
const [internalVolume, setInternalVolume] = useState(volume / 100 || 0);
|
||||||
const [duration] = useState(0);
|
const [duration] = useState(0);
|
||||||
const [previousCurrentSrc, setPreviousCurrentSrc] = useState<string | undefined>(currentSrc);
|
|
||||||
|
|
||||||
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const isInitializedRef = useRef<boolean>(false);
|
const isInitializedRef = useRef<boolean>(false);
|
||||||
const hasPopulatedQueueRef = useRef<boolean>(false);
|
const hasPopulatedQueueRef = useRef<boolean>(false);
|
||||||
const isMountedRef = useRef<boolean>(true);
|
const isMountedRef = useRef<boolean>(true);
|
||||||
const currentSrcRef = useRef<string | undefined>(currentSrc);
|
// const currentSrcRef = useRef<string | undefined>(currentSrc);
|
||||||
const nextSrcRef = useRef<string | undefined>(nextSrc);
|
// const nextSrcRef = useRef<string | undefined>(nextSrc);
|
||||||
|
|
||||||
|
const { transcode } = usePlaybackSettings();
|
||||||
const mpvExtraParameters = useSettingsStore((store) => store.playback.mpvExtraParameters);
|
const mpvExtraParameters = useSettingsStore((store) => store.playback.mpvExtraParameters);
|
||||||
const mpvProperties = useSettingsStore((store) => store.playback.mpvProperties);
|
const mpvProperties = useSettingsStore((store) => store.playback.mpvProperties);
|
||||||
|
|
||||||
|
// const [previousCurrentSrc, setPreviousCurrentSrc] = useState<string | undefined>(currentSrc);
|
||||||
|
// const [previousNextSrc, setPreviousNextSrc] = useState<string | undefined>(nextSrc);
|
||||||
|
|
||||||
// Start the mpv instance on startup
|
// Start the mpv instance on startup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isMountedRef.current = true;
|
isMountedRef.current = true;
|
||||||
|
|
||||||
const initializeMpv = async () => {
|
const initializeMpv = async () => {
|
||||||
|
// Always quit mpv first to ensure clean state, especially during HMR remounts
|
||||||
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
|
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
|
||||||
|
if (isRunning) {
|
||||||
|
mpvPlayer?.quit();
|
||||||
|
|
||||||
if (!isRunning) {
|
let attempts = 0;
|
||||||
const properties: Record<string, any> = {
|
const maxAttempts = 20;
|
||||||
// speed: usePlayerStore.getState().speed,
|
while (attempts < maxAttempts) {
|
||||||
...getMpvProperties(mpvProperties),
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
};
|
const stillRunning = await mpvPlayer?.isRunning();
|
||||||
|
if (!stillRunning) {
|
||||||
await mpvPlayer?.initialize({
|
break;
|
||||||
extraParameters: mpvExtraParameters,
|
}
|
||||||
properties,
|
attempts++;
|
||||||
});
|
}
|
||||||
|
|
||||||
mpvPlayer?.volume(properties.volume);
|
|
||||||
isInitializedRef.current = true;
|
|
||||||
} else {
|
|
||||||
isInitializedRef.current = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset initialization state
|
||||||
|
isInitializedRef.current = false;
|
||||||
|
hasPopulatedQueueRef.current = false;
|
||||||
|
|
||||||
|
// Initialize mpv with fresh state
|
||||||
|
const properties: Record<string, any> = {
|
||||||
|
...getMpvProperties(mpvProperties),
|
||||||
|
speed: speed,
|
||||||
|
};
|
||||||
|
|
||||||
|
await mpvPlayer?.initialize({
|
||||||
|
extraParameters: mpvExtraParameters,
|
||||||
|
properties,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set volume from the current app volume
|
||||||
|
mpvPlayer?.volume(volume);
|
||||||
|
isInitializedRef.current = true;
|
||||||
|
|
||||||
// After initialization, populate the queue if currentSrc is available
|
// After initialization, populate the queue if currentSrc is available
|
||||||
const latestCurrentSrc = currentSrcRef.current;
|
const playerData = usePlayerStore.getState().getPlayerData();
|
||||||
const latestNextSrc = nextSrcRef.current;
|
const currentSongUrl = playerData.currentSong
|
||||||
if (latestCurrentSrc && !hasPopulatedQueueRef.current && mpvPlayer) {
|
? getSongUrl(playerData.currentSong, transcode)
|
||||||
mpvPlayer.setQueue(latestCurrentSrc, latestNextSrc, true);
|
: undefined;
|
||||||
|
const nextSongUrl = playerData.nextSong
|
||||||
|
? getSongUrl(playerData.nextSong, transcode)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (currentSongUrl && nextSongUrl && !hasPopulatedQueueRef.current && mpvPlayer) {
|
||||||
|
mpvPlayer.setQueue(currentSongUrl, nextSongUrl, true);
|
||||||
hasPopulatedQueueRef.current = true;
|
hasPopulatedQueueRef.current = true;
|
||||||
setPreviousCurrentSrc(latestCurrentSrc);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,16 +123,12 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isMountedRef.current = false;
|
isMountedRef.current = false;
|
||||||
|
// Quit mpv on unmount
|
||||||
mpvPlayer?.quit();
|
mpvPlayer?.quit();
|
||||||
isInitializedRef.current = false;
|
isInitializedRef.current = false;
|
||||||
hasPopulatedQueueRef.current = false;
|
hasPopulatedQueueRef.current = false;
|
||||||
};
|
};
|
||||||
}, [mpvExtraParameters, mpvProperties]);
|
}, [mpvExtraParameters, mpvProperties, speed, transcode, volume]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
currentSrcRef.current = currentSrc;
|
|
||||||
nextSrcRef.current = nextSrc;
|
|
||||||
}, [currentSrc, nextSrc]);
|
|
||||||
|
|
||||||
// Update volume
|
// Update volume
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -142,35 +165,6 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
|||||||
mpvPlayer.setProperties({ speed });
|
mpvPlayer.setProperties({ speed });
|
||||||
}, [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 {
|
|
||||||
// Only clear queue if we had a previous currentSrc (intentional clear)
|
|
||||||
if (previousCurrentSrc !== undefined) {
|
|
||||||
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
|
// Handle play/pause status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mpvPlayer) {
|
if (!mpvPlayer) {
|
||||||
@@ -208,13 +202,9 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentSrc) {
|
const interval = PROGRESS_UPDATE_INTERVAL;
|
||||||
const interval = isTransitioning
|
progressIntervalRef.current = setInterval(updateProgress, interval);
|
||||||
? TRANSITION_PROGRESS_INTERVAL
|
updateProgress();
|
||||||
: PROGRESS_UPDATE_INTERVAL;
|
|
||||||
progressIntervalRef.current = setInterval(updateProgress, interval);
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isMountedRef.current = false;
|
isMountedRef.current = false;
|
||||||
@@ -223,23 +213,66 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
|||||||
progressIntervalRef.current = null;
|
progressIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [currentSrc, isTransitioning, duration, onProgress]);
|
}, [isTransitioning, duration, onProgress]);
|
||||||
|
|
||||||
|
const { mediaAutoNext } = usePlayerActions();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mpvPlayerListener) {
|
if (!mpvPlayerListener) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOnEnded = () => {
|
const handleOnAutoNext = () => {
|
||||||
onEnded();
|
mediaAutoNext();
|
||||||
|
const playerData = usePlayerStore.getState().getPlayerData();
|
||||||
|
const nextSongUrl = playerData.nextSong
|
||||||
|
? getSongUrl(playerData.nextSong, transcode)
|
||||||
|
: undefined;
|
||||||
|
mpvPlayer?.setQueueNext(nextSongUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
mpvPlayerListener.rendererAutoNext(handleOnEnded);
|
mpvPlayerListener.rendererAutoNext(handleOnAutoNext);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ipc?.removeAllListeners('renderer-player-auto-next');
|
ipc?.removeAllListeners('renderer-player-auto-next');
|
||||||
};
|
};
|
||||||
}, [nextSrc, onEnded]);
|
}, [mediaAutoNext, onEnded, transcode]);
|
||||||
|
|
||||||
|
usePlayerEvents(
|
||||||
|
{
|
||||||
|
onMediaNext: () => {
|
||||||
|
const playerData = usePlayerStore.getState().getPlayerData();
|
||||||
|
const currentSongUrl = playerData.currentSong
|
||||||
|
? getSongUrl(playerData.currentSong, transcode)
|
||||||
|
: undefined;
|
||||||
|
const nextSongUrl = playerData.nextSong
|
||||||
|
? getSongUrl(playerData.nextSong, transcode)
|
||||||
|
: undefined;
|
||||||
|
mpvPlayer?.setQueue(currentSongUrl, nextSongUrl, false);
|
||||||
|
},
|
||||||
|
onMediaPrev: () => {
|
||||||
|
const playerData = usePlayerStore.getState().getPlayerData();
|
||||||
|
const currentSongUrl = playerData.currentSong
|
||||||
|
? getSongUrl(playerData.currentSong, transcode)
|
||||||
|
: undefined;
|
||||||
|
const nextSongUrl = playerData.nextSong
|
||||||
|
? getSongUrl(playerData.nextSong, transcode)
|
||||||
|
: undefined;
|
||||||
|
mpvPlayer?.setQueue(currentSongUrl, nextSongUrl, false);
|
||||||
|
},
|
||||||
|
onPlayerPlay: () => {
|
||||||
|
const playerData = usePlayerStore.getState().getPlayerData();
|
||||||
|
const currentSongUrl = playerData.currentSong
|
||||||
|
? getSongUrl(playerData.currentSong, transcode)
|
||||||
|
: undefined;
|
||||||
|
const nextSongUrl = playerData.nextSong
|
||||||
|
? getSongUrl(playerData.nextSong, transcode)
|
||||||
|
: undefined;
|
||||||
|
mpvPlayer?.setQueue(currentSongUrl, nextSongUrl, false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[mpvPlayer, transcode],
|
||||||
|
);
|
||||||
|
|
||||||
useImperativeHandle<MpvPlayerEngineHandle, MpvPlayerEngineHandle>(playerRef, () => ({
|
useImperativeHandle<MpvPlayerEngineHandle, MpvPlayerEngineHandle>(playerRef, () => ({
|
||||||
decreaseVolume(by: number) {
|
decreaseVolume(by: number) {
|
||||||
|
|||||||
@@ -22,10 +22,13 @@ interface PlayerEvents {
|
|||||||
|
|
||||||
interface PlayerEventsCallbacks {
|
interface PlayerEventsCallbacks {
|
||||||
onCurrentSongChange?: (
|
onCurrentSongChange?: (
|
||||||
properties: { index: number; remaining: number; song: QueueSong | undefined },
|
properties: { index: number; song: QueueSong | undefined },
|
||||||
prev: { index: number; remaining: number; song: QueueSong | undefined },
|
prev: { index: number; song: QueueSong | undefined },
|
||||||
) => void;
|
) => void;
|
||||||
|
onMediaNext?: (properties: { currentIndex: number; nextIndex: number }) => void;
|
||||||
|
onMediaPrev?: (properties: { currentIndex: number; prevIndex: number }) => void;
|
||||||
onPlayerMute?: (properties: { muted: boolean }, prev: { muted: boolean }) => void;
|
onPlayerMute?: (properties: { muted: boolean }, prev: { muted: boolean }) => void;
|
||||||
|
onPlayerPlay?: (properties: { id: string; index: number }) => void;
|
||||||
onPlayerProgress?: (properties: { timestamp: number }, prev: { timestamp: number }) => void;
|
onPlayerProgress?: (properties: { timestamp: number }, prev: { timestamp: number }) => void;
|
||||||
onPlayerQueueChange?: (queue: QueueData, prev: QueueData) => void;
|
onPlayerQueueChange?: (queue: QueueData, prev: QueueData) => void;
|
||||||
onPlayerRepeat?: (properties: { repeat: PlayerRepeat }, prev: { repeat: PlayerRepeat }) => void;
|
onPlayerRepeat?: (properties: { repeat: PlayerRepeat }, prev: { repeat: PlayerRepeat }) => void;
|
||||||
@@ -129,23 +132,44 @@ function createPlayerEvents(callbacks: PlayerEventsCallbacks): PlayerEvents {
|
|||||||
unsubscribers.push(unsubscribe);
|
unsubscribers.push(unsubscribe);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callbacks.onUserRating) {
|
if (callbacks.onMediaNext) {
|
||||||
eventEmitter.on('USER_RATING', callbacks.onUserRating);
|
eventEmitter.on('MEDIA_NEXT', callbacks.onMediaNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callbacks.onMediaPrev) {
|
||||||
|
eventEmitter.on('MEDIA_PREV', callbacks.onMediaPrev);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callbacks.onPlayerPlay) {
|
||||||
|
eventEmitter.on('PLAYER_PLAY', callbacks.onPlayerPlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callbacks.onUserFavorite) {
|
if (callbacks.onUserFavorite) {
|
||||||
eventEmitter.on('USER_FAVORITE', callbacks.onUserFavorite);
|
eventEmitter.on('USER_FAVORITE', callbacks.onUserFavorite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (callbacks.onUserRating) {
|
||||||
|
eventEmitter.on('USER_RATING', callbacks.onUserRating);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
||||||
if (callbacks.onUserRating) {
|
if (callbacks.onMediaNext) {
|
||||||
eventEmitter.off('USER_RATING', callbacks.onUserRating);
|
eventEmitter.off('MEDIA_NEXT', callbacks.onMediaNext);
|
||||||
|
}
|
||||||
|
if (callbacks.onMediaPrev) {
|
||||||
|
eventEmitter.off('MEDIA_PREV', callbacks.onMediaPrev);
|
||||||
|
}
|
||||||
|
if (callbacks.onPlayerPlay) {
|
||||||
|
eventEmitter.off('PLAYER_PLAY', callbacks.onPlayerPlay);
|
||||||
}
|
}
|
||||||
if (callbacks.onUserFavorite) {
|
if (callbacks.onUserFavorite) {
|
||||||
eventEmitter.off('USER_FAVORITE', callbacks.onUserFavorite);
|
eventEmitter.off('USER_FAVORITE', callbacks.onUserFavorite);
|
||||||
}
|
}
|
||||||
|
if (callbacks.onUserRating) {
|
||||||
|
eventEmitter.off('USER_RATING', callbacks.onUserRating);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,3 +47,15 @@ export function useSongUrl(
|
|||||||
transcode.enabled,
|
transcode.enabled,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getSongUrl = (song: QueueSong, transcode: TranscodingConfig) => {
|
||||||
|
return api.controller.getStreamUrl({
|
||||||
|
apiClientProps: { serverId: song._serverId },
|
||||||
|
query: {
|
||||||
|
bitrate: transcode.bitrate,
|
||||||
|
format: transcode.format,
|
||||||
|
id: song.id,
|
||||||
|
transcode: transcode.enabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|||||||
import { MpvPlayerEngine, MpvPlayerEngineHandle } from './engine/mpv-player-engine';
|
import { MpvPlayerEngine, MpvPlayerEngineHandle } from './engine/mpv-player-engine';
|
||||||
|
|
||||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||||
import { useSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url';
|
|
||||||
import {
|
import {
|
||||||
usePlaybackSettings,
|
usePlaybackSettings,
|
||||||
usePlayerActions,
|
usePlayerActions,
|
||||||
@@ -22,12 +21,12 @@ const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
|||||||
|
|
||||||
export function MpvPlayer() {
|
export function MpvPlayer() {
|
||||||
const playerRef = useRef<MpvPlayerEngineHandle>(null);
|
const playerRef = useRef<MpvPlayerEngineHandle>(null);
|
||||||
const { currentSong, nextSong, status } = usePlayerData();
|
const { status } = usePlayerData();
|
||||||
const { mediaAutoNext, setTimestamp } = usePlayerActions();
|
const { mediaAutoNext, setTimestamp } = usePlayerActions();
|
||||||
const { speed } = usePlayerProperties();
|
const { speed } = usePlayerProperties();
|
||||||
const isMuted = usePlayerMuted();
|
const isMuted = usePlayerMuted();
|
||||||
const volume = usePlayerVolume();
|
const volume = usePlayerVolume();
|
||||||
const { audioFadeOnStatusChange, transcode } = usePlaybackSettings();
|
const { audioFadeOnStatusChange } = usePlaybackSettings();
|
||||||
|
|
||||||
const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status);
|
const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status);
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
@@ -163,15 +162,10 @@ export function MpvPlayer() {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [localPlayerStatus, setTimestamp]);
|
}, [localPlayerStatus, setTimestamp]);
|
||||||
|
|
||||||
const currentUrl = useSongUrl(currentSong, true, transcode);
|
|
||||||
const nextUrl = useSongUrl(nextSong, false, transcode);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MpvPlayerEngine
|
<MpvPlayerEngine
|
||||||
currentSrc={currentUrl}
|
|
||||||
isMuted={isMuted}
|
isMuted={isMuted}
|
||||||
isTransitioning={isTransitioning}
|
isTransitioning={isTransitioning}
|
||||||
nextSrc={nextUrl}
|
|
||||||
onEnded={handleOnEnded}
|
onEnded={handleOnEnded}
|
||||||
onProgress={onProgress}
|
onProgress={onProgress}
|
||||||
playerRef={playerRef}
|
playerRef={playerRef}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { immer } from 'zustand/middleware/immer';
|
|||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { createWithEqualityFn } from 'zustand/traditional';
|
import { createWithEqualityFn } from 'zustand/traditional';
|
||||||
|
|
||||||
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
import { createSelectors } from '/@/renderer/lib/zustand';
|
import { createSelectors } from '/@/renderer/lib/zustand';
|
||||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||||
import {
|
import {
|
||||||
@@ -40,6 +41,7 @@ interface Actions {
|
|||||||
clearSelected: (items: QueueSong[]) => void;
|
clearSelected: (items: QueueSong[]) => void;
|
||||||
decreaseVolume: (value: number) => void;
|
decreaseVolume: (value: number) => void;
|
||||||
getCurrentSong: () => QueueSong | undefined;
|
getCurrentSong: () => QueueSong | undefined;
|
||||||
|
getPlayerData: () => PlayerData;
|
||||||
getQueue: (groupBy?: QueueGroupingProperty) => GroupedQueue;
|
getQueue: (groupBy?: QueueGroupingProperty) => GroupedQueue;
|
||||||
getQueueOrder: () => {
|
getQueueOrder: () => {
|
||||||
groups: { count: number; name: string }[];
|
groups: { count: number; name: string }[];
|
||||||
@@ -1130,6 +1132,74 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
|
|
||||||
return queue.items[index];
|
return queue.items[index];
|
||||||
},
|
},
|
||||||
|
getPlayerData: () => {
|
||||||
|
const state = get();
|
||||||
|
const queue = state.getQueue();
|
||||||
|
const index = state.player.index;
|
||||||
|
|
||||||
|
// If shuffle is enabled and not in priority mode, map shuffled position to actual queue position for display
|
||||||
|
let queueIndex = index;
|
||||||
|
if (isShuffleEnabled(state)) {
|
||||||
|
queueIndex = mapShuffledToQueueIndex(index, state.queue.shuffled);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSong = queue.items[queueIndex];
|
||||||
|
const repeat = state.player.repeat;
|
||||||
|
|
||||||
|
// For previousSong calculation, we need to consider the shuffled order (only if not in priority mode)
|
||||||
|
let previousSong: QueueSong | undefined;
|
||||||
|
if (isShuffleEnabled(state)) {
|
||||||
|
// Calculate previous in shuffled order
|
||||||
|
const previousShuffledIndex = index - 1;
|
||||||
|
if (previousShuffledIndex >= 0) {
|
||||||
|
const previousQueueIndex = state.queue.shuffled[previousShuffledIndex];
|
||||||
|
previousSong = queue.items[previousQueueIndex];
|
||||||
|
} else if (repeat === PlayerRepeat.ALL) {
|
||||||
|
// Wrap to last in shuffled order
|
||||||
|
const lastShuffledIndex = state.queue.shuffled.length - 1;
|
||||||
|
const lastQueueIndex = state.queue.shuffled[lastShuffledIndex];
|
||||||
|
previousSong = queue.items[lastQueueIndex];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
previousSong = queueIndex > 0 ? queue.items[queueIndex - 1] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For nextSong calculation, we need to consider the shuffled order (only if not in priority mode)
|
||||||
|
let nextSong: QueueSong | undefined;
|
||||||
|
if (isShuffleEnabled(state)) {
|
||||||
|
// Calculate next in shuffled order
|
||||||
|
const nextShuffledIndex = index + 1;
|
||||||
|
if (nextShuffledIndex < state.queue.shuffled.length) {
|
||||||
|
const nextQueueIndex = state.queue.shuffled[nextShuffledIndex];
|
||||||
|
nextSong = queue.items[nextQueueIndex];
|
||||||
|
} else if (repeat === PlayerRepeat.ALL) {
|
||||||
|
// Wrap to first in shuffled order
|
||||||
|
const firstQueueIndex = state.queue.shuffled[0];
|
||||||
|
nextSong = queue.items[firstQueueIndex];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextSong = calculateNextSong(queueIndex, queue.items, repeat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentSong,
|
||||||
|
index: queueIndex, // Return the actual queue position for display
|
||||||
|
muted: state.player.muted,
|
||||||
|
nextSong,
|
||||||
|
num: state.player.playerNum,
|
||||||
|
player1: state.player.playerNum === 1 ? currentSong : nextSong,
|
||||||
|
player2: state.player.playerNum === 2 ? currentSong : nextSong,
|
||||||
|
previousSong,
|
||||||
|
queue: state.queue,
|
||||||
|
queueLength: state.queue.default.length + state.queue.priority.length,
|
||||||
|
repeat: state.player.repeat,
|
||||||
|
shuffle: state.player.shuffle,
|
||||||
|
speed: state.player.speed,
|
||||||
|
status: state.player.status,
|
||||||
|
transitionType: state.player.transitionType,
|
||||||
|
volume: state.player.volume,
|
||||||
|
};
|
||||||
|
},
|
||||||
getQueue: (groupBy?: QueueGroupingProperty) => {
|
getQueue: (groupBy?: QueueGroupingProperty) => {
|
||||||
const queue = get().getQueueOrder();
|
const queue = get().getQueueOrder();
|
||||||
const queueType = getQueueType();
|
const queueType = getQueueType();
|
||||||
@@ -1301,6 +1371,11 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
state.player.playerNum = 1;
|
state.player.playerNum = 1;
|
||||||
setTimestampStore(0);
|
setTimestampStore(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
eventEmitter.emit('MEDIA_NEXT', {
|
||||||
|
currentIndex,
|
||||||
|
nextIndex,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
mediaPause: () => {
|
mediaPause: () => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -1308,6 +1383,8 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
mediaPlay: (id?: string) => {
|
mediaPlay: (id?: string) => {
|
||||||
|
let playIndex: number | undefined;
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (id) {
|
if (id) {
|
||||||
const queue = state.getQueue();
|
const queue = state.getQueue();
|
||||||
@@ -1328,11 +1405,14 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
);
|
);
|
||||||
if (shuffledPosition !== -1) {
|
if (shuffledPosition !== -1) {
|
||||||
state.player.index = shuffledPosition;
|
state.player.index = shuffledPosition;
|
||||||
|
playIndex = shuffledPosition;
|
||||||
} else {
|
} else {
|
||||||
state.player.index = queueIndex;
|
state.player.index = queueIndex;
|
||||||
|
playIndex = queueIndex;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.player.index = queueIndex;
|
state.player.index = queueIndex;
|
||||||
|
playIndex = queueIndex;
|
||||||
}
|
}
|
||||||
setTimestampStore(0);
|
setTimestampStore(0);
|
||||||
}
|
}
|
||||||
@@ -1340,8 +1420,18 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
|
|
||||||
state.player.status = PlayerStatus.PLAYING;
|
state.player.status = PlayerStatus.PLAYING;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (id && playIndex !== undefined) {
|
||||||
|
eventEmitter.emit('PLAYER_PLAY', {
|
||||||
|
id,
|
||||||
|
index: playIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mediaPlayByIndex: (index: number) => {
|
mediaPlayByIndex: (index: number) => {
|
||||||
|
let playIndex: number | undefined;
|
||||||
|
let songId: string | undefined;
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const queue = state.getQueue();
|
const queue = state.getQueue();
|
||||||
|
|
||||||
@@ -1350,6 +1440,12 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the song's unique ID from the queue
|
||||||
|
const song = queue.items[index];
|
||||||
|
if (song) {
|
||||||
|
songId = song._uniqueId;
|
||||||
|
}
|
||||||
|
|
||||||
// index is the position in the original queue
|
// index is the position in the original queue
|
||||||
if (isShuffleEnabled(state)) {
|
if (isShuffleEnabled(state)) {
|
||||||
// Find the shuffled position for this queue index
|
// Find the shuffled position for this queue index
|
||||||
@@ -1357,15 +1453,23 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
index,
|
index,
|
||||||
state.queue.shuffled,
|
state.queue.shuffled,
|
||||||
);
|
);
|
||||||
state.player.index =
|
playIndex = shuffledPosition !== undefined ? shuffledPosition : index;
|
||||||
shuffledPosition !== undefined ? shuffledPosition : index;
|
state.player.index = playIndex;
|
||||||
} else {
|
} else {
|
||||||
|
playIndex = index;
|
||||||
state.player.index = index;
|
state.player.index = index;
|
||||||
}
|
}
|
||||||
setTimestampStore(0);
|
setTimestampStore(0);
|
||||||
|
|
||||||
state.player.status = PlayerStatus.PLAYING;
|
state.player.status = PlayerStatus.PLAYING;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (songId && playIndex !== undefined) {
|
||||||
|
eventEmitter.emit('PLAYER_PLAY', {
|
||||||
|
id: songId,
|
||||||
|
index: playIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mediaPrevious: () => {
|
mediaPrevious: () => {
|
||||||
const currentIndex = get().player.index;
|
const currentIndex = get().player.index;
|
||||||
@@ -1400,6 +1504,11 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
state.player.playerNum = 1;
|
state.player.playerNum = 1;
|
||||||
setTimestampStore(0);
|
setTimestampStore(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
eventEmitter.emit('MEDIA_PREV', {
|
||||||
|
currentIndex,
|
||||||
|
prevIndex: previousIndex,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
mediaSeekToTimestamp: (timestamp: number) => {
|
mediaSeekToTimestamp: (timestamp: number) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user