diff --git a/src/main/features/core/player/index.ts b/src/main/features/core/player/index.ts index 920bab1aa..f3961984e 100644 --- a/src/main/features/core/player/index.ts +++ b/src/main/features/core/player/index.ts @@ -161,19 +161,29 @@ const createMpv = async (data: { await mpv.setMultipleProperties(properties || {}); } + let previousPlaylistPos: number | undefined; + mpv.on('status', (status) => { if (status.property === 'playlist-pos') { + const currentPos = typeof status.value === 'number' ? status.value : undefined; + // mpv uses playlist-pos = -1 when nothing is playing (ended, cleared, load failure, etc). - if (status.value === -1) { + if (currentPos === -1) { + if (previousPlaylistPos === 0) { + getMainWindow()?.webContents.send('renderer-player-track-ended'); + } mpv?.pause(); + previousPlaylistPos = currentPos; return; } // In our 2-item queue model, playlist-pos should normally be 0. // When mpv auto-advances to the next track it becomes > 0 (typically 1). - if (typeof status.value === 'number' && status.value > 0) { + if (typeof currentPos === 'number' && currentPos > 0) { getMainWindow()?.webContents.send('renderer-player-auto-next'); } + + previousPlaylistPos = currentPos; } }); diff --git a/src/preload/mpv-player.ts b/src/preload/mpv-player.ts index 6bcf72f3f..5b1fd3fd9 100644 --- a/src/preload/mpv-player.ts +++ b/src/preload/mpv-player.ts @@ -102,6 +102,10 @@ const getAudioDevices = async () => { return ipcRenderer.invoke('player-get-audio-devices'); }; +const rendererTrackEnded = (cb: () => void) => { + ipcRenderer.on('renderer-player-track-ended', () => cb()); +}; + const rendererAutoNext = (cb: (data: PlayerData) => void) => { ipcRenderer.on('renderer-player-auto-next', (_, data) => cb(data)); }; @@ -222,6 +226,7 @@ export const mpvPlayerListener = { rendererStop, rendererToggleRepeat, rendererToggleShuffle, + rendererTrackEnded, rendererVolumeDown, rendererVolumeMute, rendererVolumeUp, 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 index 8cf355644..db7a33fa2 100644 --- a/src/renderer/features/player/audio-player/engine/mpv-player-engine.tsx +++ b/src/renderer/features/player/audio-player/engine/mpv-player-engine.tsx @@ -276,10 +276,21 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => { handleMpvAutoNext(transcode); }; + const handleTrackEnded = () => { + const { player } = usePlayerStore.getState(); + if (player.status !== PlayerStatus.PLAYING) { + return; + } + + mediaAutoNext(); + }; + mpvPlayerListener.rendererAutoNext(handleOnAutoNext); + mpvPlayerListener.rendererTrackEnded(handleTrackEnded); return () => { ipc?.removeAllListeners('renderer-player-auto-next'); + ipc?.removeAllListeners('renderer-player-track-ended'); }; }, [mediaAutoNext, onEnded, transcode]); diff --git a/src/renderer/features/player/audio-player/wavesurfer-player.tsx b/src/renderer/features/player/audio-player/wavesurfer-player.tsx index 5d56a3e6d..c64d6d452 100644 --- a/src/renderer/features/player/audio-player/wavesurfer-player.tsx +++ b/src/renderer/features/player/audio-player/wavesurfer-player.tsx @@ -16,6 +16,7 @@ import { usePlayerData, usePlayerMuted, usePlayerProperties, + usePlayerStoreBase, usePlayerVolume, } from '/@/renderer/store'; import { PlayerStatus, PlayerStyle } from '/@/shared/types/types'; @@ -153,7 +154,13 @@ export function WaveSurferPlayer() { promise.then(() => { playerRef.current?.player1()?.ref?.pause(); - playerRef.current?.setVolume(volume); + + const currentStatus = usePlayerStoreBase.getState().player.status; + if (currentStatus !== PlayerStatus.PLAYING) { + playerRef.current?.pause(); + } else { + playerRef.current?.setVolume(volume); + } setIsTransitioning(false); }); }, [mediaAutoNext, volume]); @@ -166,7 +173,13 @@ export function WaveSurferPlayer() { promise.then(() => { playerRef.current?.player2()?.ref?.pause(); - playerRef.current?.setVolume(volume); + + const currentStatus = usePlayerStoreBase.getState().player.status; + if (currentStatus !== PlayerStatus.PLAYING) { + playerRef.current?.pause(); + } else { + playerRef.current?.setVolume(volume); + } setIsTransitioning(false); }); }, [mediaAutoNext, volume]); diff --git a/src/renderer/features/player/audio-player/web-player.tsx b/src/renderer/features/player/audio-player/web-player.tsx index ead7bd114..96e646aa6 100644 --- a/src/renderer/features/player/audio-player/web-player.tsx +++ b/src/renderer/features/player/audio-player/web-player.tsx @@ -238,7 +238,7 @@ export function WebPlayer() { promise.then(() => { playerRef.current?.player1()?.ref?.getInternalPlayer().pause(); - // If mediaAutoNext resulted in a paused state (e.g. end of queue, + // If mediaAutoNext resulted in a stopped/paused state (e.g. end of queue, // or pauseOnNextSongEnd flag), stop all audio instead of restoring volume. const currentStatus = usePlayerStoreBase.getState().player.status; if (currentStatus !== PlayerStatus.PLAYING) { diff --git a/src/renderer/store/player.store.ts b/src/renderer/store/player.store.ts index 40514aa60..9ce04e517 100644 --- a/src/renderer/store/player.store.ts +++ b/src/renderer/store/player.store.ts @@ -207,25 +207,25 @@ function calculateNextIndex( currentIndex: number, queueLength: number, repeat: PlayerRepeat, -): { nextIndex: number; shouldPause: boolean } { +): { nextIndex: number; shouldStop: boolean } { const isLastTrack = currentIndex === queueLength - 1; if (repeat === PlayerRepeat.ONE) { // Repeat one: stay on the same track - return { nextIndex: currentIndex, shouldPause: false }; + return { nextIndex: currentIndex, shouldStop: false }; } else if (repeat === PlayerRepeat.ALL) { // Repeat all: loop to first track if at the end if (isLastTrack) { - return { nextIndex: 0, shouldPause: false }; + return { nextIndex: 0, shouldStop: false }; } else { - return { nextIndex: currentIndex + 1, shouldPause: false }; + return { nextIndex: currentIndex + 1, shouldStop: false }; } } else { - // Repeat none: move to next track, or pause if at the end + // Repeat none: move to next track, or stop if at the end if (isLastTrack) { - return { nextIndex: currentIndex, shouldPause: true }; + return { nextIndex: currentIndex, shouldStop: true }; } else { - return { nextIndex: currentIndex + 1, shouldPause: false }; + return { nextIndex: currentIndex + 1, shouldStop: false }; } } } @@ -292,6 +292,19 @@ function emitPlayerPlayEvent( } } +function emitPlayerStop(get: () => PlayerState, reset: boolean): void { + const currentState = get(); + const queue = currentState.getQueue(); + const currentIndex = currentState.player.index; + const currentSong = queue.items[currentIndex]; + + eventEmitter.emit('PLAYER_STOP', { + id: currentSong?._uniqueId, + index: currentIndex !== undefined && currentIndex >= 0 ? currentIndex : undefined, + reset, + }); +} + // Helper function to find shuffled position for a given queue index function findShuffledPositionForQueueIndex( queueIndex: number, @@ -921,11 +934,12 @@ export const usePlayerStoreBase = createWithEqualityFn()( ? stateSnapshot.queue.shuffled.length : queue.items.length; - const { nextIndex: nextPlaybackIndex, shouldPause } = calculateNextIndex( + const { nextIndex: nextPlaybackIndex, shouldStop } = calculateNextIndex( currentIndex, playbackLength, repeat, ); + const isRepeatOneSameTrack = repeat === PlayerRepeat.ONE && nextPlaybackIndex === currentIndex; // Dual web players alternate for gapless/crossfade between tracks. Repeat-one @@ -937,9 +951,12 @@ export const usePlayerStoreBase = createWithEqualityFn()( ? 2 : 1; const pauseOnNext = player.pauseOnNextSongEnd; - const newStatus = - shouldPause || pauseOnNext ? PlayerStatus.PAUSED : PlayerStatus.PLAYING; - const shouldKeepCurrentPlayer = newStatus === PlayerStatus.PAUSED; + const newStatus = shouldStop + ? PlayerStatus.STOPPED + : pauseOnNext + ? PlayerStatus.PAUSED + : PlayerStatus.PLAYING; + const shouldKeepCurrentPlayer = newStatus !== PlayerStatus.PLAYING; const shouldSwapPlayer = !isRepeatOneSameTrack && !shouldKeepCurrentPlayer; set((state) => { @@ -948,11 +965,19 @@ export const usePlayerStoreBase = createWithEqualityFn()( setTimestampStore(0); state.player.status = newStatus; + if (shouldStop) { + state.player.seekToTimestamp = uniqueSeekToTimestamp(0); + } + if (pauseOnNext) { state.player.pauseOnNextSongEnd = false; } }); + if (shouldStop) { + emitPlayerStop(get, true); + } + if (repeat === PlayerRepeat.ONE && nextPlaybackIndex === currentIndex) { eventEmitter.emit('PLAYER_REPEATED', { index: nextPlaybackIndex, @@ -1242,19 +1267,7 @@ export const usePlayerStoreBase = createWithEqualityFn()( } }); - const currentState = get(); - const queue = currentState.getQueue(); - const currentIndex = currentState.player.index; - const currentSong = queue.items[currentIndex]; - - eventEmitter.emit('PLAYER_STOP', { - id: currentSong?._uniqueId, - index: - currentIndex !== undefined && currentIndex >= 0 - ? currentIndex - : undefined, - reset, - }); + emitPlayerStop(get, reset); }, mediaToggleMute: () => { set((state) => {