mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-30 15:57:38 +02:00
add "stopped" playback state and event handlers
This commit is contained in:
@@ -124,7 +124,12 @@ ipcMain.on('update-volume', (_event, volume) => {
|
||||
});
|
||||
|
||||
ipcMain.on('update-playback', (_event, status: PlayerStatus) => {
|
||||
mprisPlayer.playbackStatus = status === PlayerStatus.PLAYING ? 'Playing' : 'Paused';
|
||||
mprisPlayer.playbackStatus =
|
||||
status === PlayerStatus.PLAYING
|
||||
? 'Playing'
|
||||
: status === PlayerStatus.STOPPED
|
||||
? 'Stopped'
|
||||
: 'Paused';
|
||||
});
|
||||
|
||||
const REPEAT_TO_MPRIS: Record<PlayerRepeat, string> = {
|
||||
|
||||
@@ -128,7 +128,7 @@ export const RemoteContainer = () => {
|
||||
onClick={() => {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
send({ event: 'pause' });
|
||||
} else if (status === PlayerStatus.PAUSED) {
|
||||
} else {
|
||||
send({ event: 'play' });
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -13,6 +13,7 @@ export type EventMap = {
|
||||
MPV_RELOAD: MpvReloadEventPayload;
|
||||
PLAYER_PLAY: PlayerPlayEventPayload;
|
||||
PLAYER_REPEATED: PlayerRepeatedEventPayload;
|
||||
PLAYER_STOP: PlayerStopEventPayload;
|
||||
PLAYLIST_MOVE_DOWN: PlaylistMoveEventPayload;
|
||||
PLAYLIST_MOVE_TO_BOTTOM: PlaylistMoveEventPayload;
|
||||
PLAYLIST_MOVE_TO_TOP: PlaylistMoveEventPayload;
|
||||
@@ -54,6 +55,12 @@ export type PlayerRepeatedEventPayload = {
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type PlayerStopEventPayload = {
|
||||
id?: string;
|
||||
index?: number;
|
||||
reset: boolean;
|
||||
};
|
||||
|
||||
export type PlaylistMoveEventPayload = {
|
||||
playlistId: string;
|
||||
sourceIds: string[];
|
||||
|
||||
@@ -212,7 +212,7 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
||||
|
||||
if (playerStatus === PlayerStatus.PLAYING) {
|
||||
mpvPlayer.play();
|
||||
} else if (playerStatus === PlayerStatus.PAUSED) {
|
||||
} else {
|
||||
mpvPlayer.pause();
|
||||
}
|
||||
}, [playerStatus]);
|
||||
|
||||
@@ -47,6 +47,7 @@ interface PlayerEventsCallbacks {
|
||||
) => void;
|
||||
onPlayerSpeed?: (properties: { speed: number }, prev: { speed: number }) => void;
|
||||
onPlayerStatus?: (properties: { status: PlayerStatus }, prev: { status: PlayerStatus }) => void;
|
||||
onPlayerStop?: (properties: { id?: string; index?: number; reset: boolean }) => void;
|
||||
onPlayerVolume?: (properties: { volume: number }, prev: { volume: number }) => void;
|
||||
onQueueCleared?: () => void;
|
||||
onQueueRestored?: (properties: { data: Song[]; index: number; position: number }) => void;
|
||||
@@ -166,6 +167,10 @@ function createPlayerEvents(callbacks: PlayerEventsCallbacks): PlayerEvents {
|
||||
eventEmitter.on('PLAYER_REPEATED', callbacks.onPlayerRepeated);
|
||||
}
|
||||
|
||||
if (callbacks.onPlayerStop) {
|
||||
eventEmitter.on('PLAYER_STOP', callbacks.onPlayerStop);
|
||||
}
|
||||
|
||||
if (callbacks.onQueueRestored) {
|
||||
eventEmitter.on('QUEUE_RESTORED', callbacks.onQueueRestored);
|
||||
}
|
||||
@@ -193,6 +198,9 @@ function createPlayerEvents(callbacks: PlayerEventsCallbacks): PlayerEvents {
|
||||
if (callbacks.onPlayerRepeated) {
|
||||
eventEmitter.off('PLAYER_REPEATED', callbacks.onPlayerRepeated);
|
||||
}
|
||||
if (callbacks.onPlayerStop) {
|
||||
eventEmitter.off('PLAYER_STOP', callbacks.onPlayerStop);
|
||||
}
|
||||
if (callbacks.onQueueRestored) {
|
||||
eventEmitter.off('QUEUE_RESTORED', callbacks.onQueueRestored);
|
||||
}
|
||||
|
||||
@@ -69,12 +69,12 @@ export function MpvPlayer() {
|
||||
}, PLAY_PAUSE_FADE_INTERVAL);
|
||||
});
|
||||
|
||||
if (status === PlayerStatus.PAUSED) {
|
||||
await promise;
|
||||
setLocalPlayerStatus(status);
|
||||
} else if (status === PlayerStatus.PLAYING) {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
setLocalPlayerStatus(status);
|
||||
await promise;
|
||||
} else {
|
||||
await promise;
|
||||
setLocalPlayerStatus(status);
|
||||
}
|
||||
},
|
||||
[],
|
||||
@@ -111,18 +111,18 @@ export function MpvPlayer() {
|
||||
const status = properties.status;
|
||||
const volume = usePlayerStore.getState().player.volume;
|
||||
if (audioFadeOnStatusChange) {
|
||||
if (status === PlayerStatus.PAUSED) {
|
||||
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PAUSED);
|
||||
} else if (status === PlayerStatus.PLAYING) {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
fadeAndSetStatus(0, volume, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PLAYING);
|
||||
} else {
|
||||
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, status);
|
||||
}
|
||||
} else {
|
||||
if (status === PlayerStatus.PAUSED) {
|
||||
playerRef.current?.setVolume(0);
|
||||
setLocalPlayerStatus(PlayerStatus.PAUSED);
|
||||
} else if (status === PlayerStatus.PLAYING) {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
playerRef.current?.setVolume(volume);
|
||||
setLocalPlayerStatus(PlayerStatus.PLAYING);
|
||||
} else {
|
||||
playerRef.current?.setVolume(0);
|
||||
setLocalPlayerStatus(status);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -60,12 +60,12 @@ export function WaveSurferPlayer() {
|
||||
}, PLAY_PAUSE_FADE_INTERVAL);
|
||||
});
|
||||
|
||||
if (status === PlayerStatus.PAUSED) {
|
||||
await promise;
|
||||
setLocalPlayerStatus(status);
|
||||
} else if (status === PlayerStatus.PLAYING) {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
setLocalPlayerStatus(status);
|
||||
await promise;
|
||||
} else {
|
||||
await promise;
|
||||
setLocalPlayerStatus(status);
|
||||
}
|
||||
},
|
||||
[isTransitioning],
|
||||
@@ -190,10 +190,10 @@ export function WaveSurferPlayer() {
|
||||
},
|
||||
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) {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
fadeAndSetStatus(0, volume, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PLAYING);
|
||||
} else {
|
||||
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, status);
|
||||
}
|
||||
},
|
||||
onPlayerVolume: (properties) => {
|
||||
|
||||
@@ -89,13 +89,13 @@ export function WebPlayer() {
|
||||
}, PLAY_PAUSE_FADE_INTERVAL);
|
||||
});
|
||||
|
||||
if (status === PlayerStatus.PAUSED) {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
setLocalPlayerStatus(status);
|
||||
await promise;
|
||||
} else {
|
||||
await promise;
|
||||
setLocalPlayerStatus(status);
|
||||
playerRef.current?.setVolume(startVolume);
|
||||
} else if (status === PlayerStatus.PLAYING) {
|
||||
setLocalPlayerStatus(status);
|
||||
await promise;
|
||||
}
|
||||
},
|
||||
[],
|
||||
@@ -241,7 +241,7 @@ export function WebPlayer() {
|
||||
// If mediaAutoNext resulted in a 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.PAUSED) {
|
||||
if (currentStatus !== PlayerStatus.PLAYING) {
|
||||
playerRef.current?.pause();
|
||||
} else {
|
||||
playerRef.current?.setVolume(volume);
|
||||
@@ -260,7 +260,7 @@ export function WebPlayer() {
|
||||
playerRef.current?.player2()?.ref?.getInternalPlayer().pause();
|
||||
|
||||
const currentStatus = usePlayerStoreBase.getState().player.status;
|
||||
if (currentStatus === PlayerStatus.PAUSED) {
|
||||
if (currentStatus !== PlayerStatus.PLAYING) {
|
||||
playerRef.current?.pause();
|
||||
} else {
|
||||
playerRef.current?.setVolume(volume);
|
||||
@@ -313,9 +313,9 @@ export function WebPlayer() {
|
||||
|
||||
const status = properties.status;
|
||||
|
||||
// Reset crossfade transition if paused during a crossfade transition
|
||||
// Reset crossfade transition if paused/stopped during a crossfade transition
|
||||
if (
|
||||
status === PlayerStatus.PAUSED &&
|
||||
status !== PlayerStatus.PLAYING &&
|
||||
isTransitioning &&
|
||||
transitionType === PlayerStyle.CROSSFADE
|
||||
) {
|
||||
@@ -331,18 +331,18 @@ export function WebPlayer() {
|
||||
}
|
||||
|
||||
if (audioFadeOnStatusChange) {
|
||||
if (status === PlayerStatus.PAUSED) {
|
||||
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PAUSED);
|
||||
} else if (status === PlayerStatus.PLAYING) {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
fadeAndSetStatus(0, volume, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PLAYING);
|
||||
} else {
|
||||
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, status);
|
||||
}
|
||||
} else {
|
||||
if (status === PlayerStatus.PAUSED) {
|
||||
playerRef.current?.setVolume(volume);
|
||||
setLocalPlayerStatus(PlayerStatus.PAUSED);
|
||||
} else if (status === PlayerStatus.PLAYING) {
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
playerRef.current?.setVolume(volume);
|
||||
setLocalPlayerStatus(PlayerStatus.PLAYING);
|
||||
} else {
|
||||
playerRef.current?.setVolume(volume);
|
||||
setLocalPlayerStatus(status);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -203,7 +203,7 @@ const CenterPlayButton = ({ disabled }: { disabled?: boolean }) => {
|
||||
return (
|
||||
<MainPlayButton
|
||||
disabled={disabled || currentSongId === undefined}
|
||||
isPaused={status === PlayerStatus.PAUSED}
|
||||
isPaused={status !== PlayerStatus.PLAYING}
|
||||
onClick={mediaTogglePlayPause}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -51,7 +51,7 @@ export const MobileFullscreenPlayerControls = memo(
|
||||
/>
|
||||
<MainPlayButton
|
||||
disabled={currentSongId === undefined}
|
||||
isPaused={status === PlayerStatus.PAUSED}
|
||||
isPaused={status !== PlayerStatus.PLAYING}
|
||||
onClick={mediaTogglePlayPause}
|
||||
style={{
|
||||
height: '50px',
|
||||
|
||||
@@ -213,7 +213,7 @@ export const MobilePlayerbar = () => {
|
||||
/>
|
||||
<MainPlayButton
|
||||
disabled={currentSong?.id === undefined}
|
||||
isPaused={status === PlayerStatus.PAUSED}
|
||||
isPaused={status !== PlayerStatus.PLAYING}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
mediaTogglePlayPause();
|
||||
|
||||
@@ -131,6 +131,7 @@ export const useScrobble = () => {
|
||||
const previousSongRef = useRef<QueueSong | undefined>(undefined);
|
||||
const previousTimestampRef = useRef<number>(0);
|
||||
const stopPositionRef = useRef<number>(0);
|
||||
const stoppedSongIdRef = useRef<string | undefined>(undefined);
|
||||
const lastProgressEventRef = useRef<number>(0);
|
||||
const lastSeekEventRef = useRef<number>(0);
|
||||
const songChangeTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
@@ -499,6 +500,12 @@ export const useScrobble = () => {
|
||||
|
||||
const currentStatus = usePlayerStore.getState().player.status;
|
||||
|
||||
// Stop resets seek position; the stop event is reported by handleScrobbleFromStatus.
|
||||
if (currentStatus === PlayerStatus.STOPPED) {
|
||||
flushScrobbleDebug();
|
||||
return;
|
||||
}
|
||||
|
||||
sendScrobble.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: currentSong._serverId || '' },
|
||||
@@ -608,6 +615,71 @@ export const useScrobble = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Send start event when resuming the same song that was stopped.
|
||||
if (
|
||||
properties.status === PlayerStatus.PLAYING &&
|
||||
prev.status === PlayerStatus.STOPPED &&
|
||||
stoppedSongIdRef.current === currentSong._uniqueId
|
||||
) {
|
||||
stoppedSongIdRef.current = undefined;
|
||||
sendScrobble.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: currentSong._serverId || '' },
|
||||
query: {
|
||||
albumId: currentSong.albumId,
|
||||
event: 'start',
|
||||
id: currentSong.id,
|
||||
mediaType: mediaType,
|
||||
playbackRate: playbackRate,
|
||||
position: getPositionValue(currentTimestamp, useTicks),
|
||||
submission: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledStart, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: currentSong.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Send stop event when status changes to stopped (from an active state)
|
||||
if (
|
||||
properties.status === PlayerStatus.STOPPED &&
|
||||
prev.status !== PlayerStatus.STOPPED
|
||||
) {
|
||||
stoppedSongIdRef.current = currentSong._uniqueId;
|
||||
sendScrobble.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: currentSong._serverId || '' },
|
||||
query: {
|
||||
albumId: currentSong.albumId,
|
||||
event: 'stop',
|
||||
id: currentSong.id,
|
||||
mediaType: mediaType,
|
||||
playbackRate: playbackRate,
|
||||
position: getPositionValue(currentTimestamp, useTicks),
|
||||
submission: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledStop, {
|
||||
category: LogCategory.SCROBBLE,
|
||||
meta: {
|
||||
id: currentSong.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
flushScrobbleDebug();
|
||||
},
|
||||
[isScrobbleEnabled, isPrivateModeEnabled, flushScrobbleDebug, sendScrobble, playbackRate],
|
||||
|
||||
@@ -1235,12 +1235,26 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
||||
mediaStop: (options?: { reset?: boolean }) => {
|
||||
const reset = options?.reset !== false;
|
||||
set((state) => {
|
||||
state.player.status = PlayerStatus.PAUSED;
|
||||
state.player.status = PlayerStatus.STOPPED;
|
||||
setTimestampStore(0);
|
||||
if (reset) {
|
||||
state.player.seekToTimestamp = uniqueSeekToTimestamp(0);
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
},
|
||||
mediaToggleMute: () => {
|
||||
set((state) => {
|
||||
|
||||
@@ -149,6 +149,7 @@ export enum PlayerShuffle {
|
||||
export enum PlayerStatus {
|
||||
PAUSED = 'paused',
|
||||
PLAYING = 'playing',
|
||||
STOPPED = 'stopped',
|
||||
}
|
||||
|
||||
export enum PlayerStyle {
|
||||
|
||||
Reference in New Issue
Block a user