trigger stop event on queue end

This commit is contained in:
jeffvli
2026-07-09 20:42:59 -07:00
parent 511875689a
commit e3c2605e1d
6 changed files with 81 additions and 29 deletions
+12 -2
View File
@@ -161,19 +161,29 @@ const createMpv = async (data: {
await mpv.setMultipleProperties(properties || {}); await mpv.setMultipleProperties(properties || {});
} }
let previousPlaylistPos: number | undefined;
mpv.on('status', (status) => { mpv.on('status', (status) => {
if (status.property === 'playlist-pos') { 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). // 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(); mpv?.pause();
previousPlaylistPos = currentPos;
return; return;
} }
// In our 2-item queue model, playlist-pos should normally be 0. // 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). // 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'); getMainWindow()?.webContents.send('renderer-player-auto-next');
} }
previousPlaylistPos = currentPos;
} }
}); });
+5
View File
@@ -102,6 +102,10 @@ const getAudioDevices = async () => {
return ipcRenderer.invoke('player-get-audio-devices'); return ipcRenderer.invoke('player-get-audio-devices');
}; };
const rendererTrackEnded = (cb: () => void) => {
ipcRenderer.on('renderer-player-track-ended', () => cb());
};
const rendererAutoNext = (cb: (data: PlayerData) => void) => { const rendererAutoNext = (cb: (data: PlayerData) => void) => {
ipcRenderer.on('renderer-player-auto-next', (_, data) => cb(data)); ipcRenderer.on('renderer-player-auto-next', (_, data) => cb(data));
}; };
@@ -222,6 +226,7 @@ export const mpvPlayerListener = {
rendererStop, rendererStop,
rendererToggleRepeat, rendererToggleRepeat,
rendererToggleShuffle, rendererToggleShuffle,
rendererTrackEnded,
rendererVolumeDown, rendererVolumeDown,
rendererVolumeMute, rendererVolumeMute,
rendererVolumeUp, rendererVolumeUp,
@@ -276,10 +276,21 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
handleMpvAutoNext(transcode); handleMpvAutoNext(transcode);
}; };
const handleTrackEnded = () => {
const { player } = usePlayerStore.getState();
if (player.status !== PlayerStatus.PLAYING) {
return;
}
mediaAutoNext();
};
mpvPlayerListener.rendererAutoNext(handleOnAutoNext); mpvPlayerListener.rendererAutoNext(handleOnAutoNext);
mpvPlayerListener.rendererTrackEnded(handleTrackEnded);
return () => { return () => {
ipc?.removeAllListeners('renderer-player-auto-next'); ipc?.removeAllListeners('renderer-player-auto-next');
ipc?.removeAllListeners('renderer-player-track-ended');
}; };
}, [mediaAutoNext, onEnded, transcode]); }, [mediaAutoNext, onEnded, transcode]);
@@ -16,6 +16,7 @@ import {
usePlayerData, usePlayerData,
usePlayerMuted, usePlayerMuted,
usePlayerProperties, usePlayerProperties,
usePlayerStoreBase,
usePlayerVolume, usePlayerVolume,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { PlayerStatus, PlayerStyle } from '/@/shared/types/types'; import { PlayerStatus, PlayerStyle } from '/@/shared/types/types';
@@ -153,7 +154,13 @@ export function WaveSurferPlayer() {
promise.then(() => { promise.then(() => {
playerRef.current?.player1()?.ref?.pause(); playerRef.current?.player1()?.ref?.pause();
const currentStatus = usePlayerStoreBase.getState().player.status;
if (currentStatus !== PlayerStatus.PLAYING) {
playerRef.current?.pause();
} else {
playerRef.current?.setVolume(volume); playerRef.current?.setVolume(volume);
}
setIsTransitioning(false); setIsTransitioning(false);
}); });
}, [mediaAutoNext, volume]); }, [mediaAutoNext, volume]);
@@ -166,7 +173,13 @@ export function WaveSurferPlayer() {
promise.then(() => { promise.then(() => {
playerRef.current?.player2()?.ref?.pause(); playerRef.current?.player2()?.ref?.pause();
const currentStatus = usePlayerStoreBase.getState().player.status;
if (currentStatus !== PlayerStatus.PLAYING) {
playerRef.current?.pause();
} else {
playerRef.current?.setVolume(volume); playerRef.current?.setVolume(volume);
}
setIsTransitioning(false); setIsTransitioning(false);
}); });
}, [mediaAutoNext, volume]); }, [mediaAutoNext, volume]);
@@ -238,7 +238,7 @@ export function WebPlayer() {
promise.then(() => { promise.then(() => {
playerRef.current?.player1()?.ref?.getInternalPlayer().pause(); 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. // or pauseOnNextSongEnd flag), stop all audio instead of restoring volume.
const currentStatus = usePlayerStoreBase.getState().player.status; const currentStatus = usePlayerStoreBase.getState().player.status;
if (currentStatus !== PlayerStatus.PLAYING) { if (currentStatus !== PlayerStatus.PLAYING) {
+37 -24
View File
@@ -207,25 +207,25 @@ function calculateNextIndex(
currentIndex: number, currentIndex: number,
queueLength: number, queueLength: number,
repeat: PlayerRepeat, repeat: PlayerRepeat,
): { nextIndex: number; shouldPause: boolean } { ): { nextIndex: number; shouldStop: boolean } {
const isLastTrack = currentIndex === queueLength - 1; const isLastTrack = currentIndex === queueLength - 1;
if (repeat === PlayerRepeat.ONE) { if (repeat === PlayerRepeat.ONE) {
// Repeat one: stay on the same track // Repeat one: stay on the same track
return { nextIndex: currentIndex, shouldPause: false }; return { nextIndex: currentIndex, shouldStop: false };
} else if (repeat === PlayerRepeat.ALL) { } else if (repeat === PlayerRepeat.ALL) {
// Repeat all: loop to first track if at the end // Repeat all: loop to first track if at the end
if (isLastTrack) { if (isLastTrack) {
return { nextIndex: 0, shouldPause: false }; return { nextIndex: 0, shouldStop: false };
} else { } else {
return { nextIndex: currentIndex + 1, shouldPause: false }; return { nextIndex: currentIndex + 1, shouldStop: false };
} }
} else { } 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) { if (isLastTrack) {
return { nextIndex: currentIndex, shouldPause: true }; return { nextIndex: currentIndex, shouldStop: true };
} else { } 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 // Helper function to find shuffled position for a given queue index
function findShuffledPositionForQueueIndex( function findShuffledPositionForQueueIndex(
queueIndex: number, queueIndex: number,
@@ -921,11 +934,12 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
? stateSnapshot.queue.shuffled.length ? stateSnapshot.queue.shuffled.length
: queue.items.length; : queue.items.length;
const { nextIndex: nextPlaybackIndex, shouldPause } = calculateNextIndex( const { nextIndex: nextPlaybackIndex, shouldStop } = calculateNextIndex(
currentIndex, currentIndex,
playbackLength, playbackLength,
repeat, repeat,
); );
const isRepeatOneSameTrack = const isRepeatOneSameTrack =
repeat === PlayerRepeat.ONE && nextPlaybackIndex === currentIndex; repeat === PlayerRepeat.ONE && nextPlaybackIndex === currentIndex;
// Dual web players alternate for gapless/crossfade between tracks. Repeat-one // Dual web players alternate for gapless/crossfade between tracks. Repeat-one
@@ -937,9 +951,12 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
? 2 ? 2
: 1; : 1;
const pauseOnNext = player.pauseOnNextSongEnd; const pauseOnNext = player.pauseOnNextSongEnd;
const newStatus = const newStatus = shouldStop
shouldPause || pauseOnNext ? PlayerStatus.PAUSED : PlayerStatus.PLAYING; ? PlayerStatus.STOPPED
const shouldKeepCurrentPlayer = newStatus === PlayerStatus.PAUSED; : pauseOnNext
? PlayerStatus.PAUSED
: PlayerStatus.PLAYING;
const shouldKeepCurrentPlayer = newStatus !== PlayerStatus.PLAYING;
const shouldSwapPlayer = !isRepeatOneSameTrack && !shouldKeepCurrentPlayer; const shouldSwapPlayer = !isRepeatOneSameTrack && !shouldKeepCurrentPlayer;
set((state) => { set((state) => {
@@ -948,11 +965,19 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
setTimestampStore(0); setTimestampStore(0);
state.player.status = newStatus; state.player.status = newStatus;
if (shouldStop) {
state.player.seekToTimestamp = uniqueSeekToTimestamp(0);
}
if (pauseOnNext) { if (pauseOnNext) {
state.player.pauseOnNextSongEnd = false; state.player.pauseOnNextSongEnd = false;
} }
}); });
if (shouldStop) {
emitPlayerStop(get, true);
}
if (repeat === PlayerRepeat.ONE && nextPlaybackIndex === currentIndex) { if (repeat === PlayerRepeat.ONE && nextPlaybackIndex === currentIndex) {
eventEmitter.emit('PLAYER_REPEATED', { eventEmitter.emit('PLAYER_REPEATED', {
index: nextPlaybackIndex, index: nextPlaybackIndex,
@@ -1242,19 +1267,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
} }
}); });
const currentState = get(); emitPlayerStop(get, reset);
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,
});
}, },
mediaToggleMute: () => { mediaToggleMute: () => {
set((state) => { set((state) => {