diff --git a/src/renderer/features/player/audio-player/mpv-player.tsx b/src/renderer/features/player/audio-player/mpv-player.tsx index 3625962d5..4f9edb4a0 100644 --- a/src/renderer/features/player/audio-player/mpv-player.tsx +++ b/src/renderer/features/player/audio-player/mpv-player.tsx @@ -31,28 +31,39 @@ export function MpvPlayer() { const [localPlayerStatus, setLocalPlayerStatus] = useState(status); const [isTransitioning, setIsTransitioning] = useState(false); + const fadeIntervalRef = useRef(null); const fadeAndSetStatus = useCallback( async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => { - if (isTransitioning) { - return setLocalPlayerStatus(status); + // Cancel any in-progress fade + if (fadeIntervalRef.current) { + clearInterval(fadeIntervalRef.current); + fadeIntervalRef.current = null; } + // Set initial volume immediately to ensure we start from the correct position + // This is especially important when cancelling a previous fade + playerRef.current?.setVolume(startVolume); + const steps = duration / PLAY_PAUSE_FADE_INTERVAL; const volumeStep = (endVolume - startVolume) / steps; let currentStep = 0; - const promise = new Promise((resolve) => { - const interval = setInterval(() => { + const promise = new Promise((resolve) => { + fadeIntervalRef.current = setInterval(() => { currentStep++; const newVolume = startVolume + volumeStep * currentStep; playerRef.current?.setVolume(newVolume); if (currentStep >= steps) { - clearInterval(interval); - setIsTransitioning(false); - resolve(true); + if (fadeIntervalRef.current) { + clearInterval(fadeIntervalRef.current); + fadeIntervalRef.current = null; + } + // Ensure final volume is exactly the target + playerRef.current?.setVolume(endVolume); + resolve(); } }, PLAY_PAUSE_FADE_INTERVAL); }); @@ -65,7 +76,7 @@ export function MpvPlayer() { await promise; } }, - [isTransitioning], + [], ); const onProgress = useCallback(() => { @@ -106,9 +117,19 @@ export function MpvPlayer() { playerRef.current?.setVolume(volume); }, }, - [volume, isTransitioning, fadeAndSetStatus], + [volume, fadeAndSetStatus], ); + // Cleanup fade interval on unmount + useEffect(() => { + return () => { + if (fadeIntervalRef.current) { + clearInterval(fadeIntervalRef.current); + fadeIntervalRef.current = null; + } + }; + }, []); + useEffect(() => { if (localPlayerStatus !== PlayerStatus.PLAYING) { return; diff --git a/src/renderer/features/player/audio-player/web-player.tsx b/src/renderer/features/player/audio-player/web-player.tsx index 0c29b8133..739ac9020 100644 --- a/src/renderer/features/player/audio-player/web-player.tsx +++ b/src/renderer/features/player/audio-player/web-player.tsx @@ -40,31 +40,42 @@ export function WebPlayer() { const [localPlayerStatus, setLocalPlayerStatus] = useState(status); const [isTransitioning, setIsTransitioning] = useState(false); + const fadeIntervalRef = useRef(null); const [player1Source, setPlayer1Source] = useState(null); const [player2Source, setPlayer2Source] = useState(null); const fadeAndSetStatus = useCallback( async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => { - if (isTransitioning) { - return setLocalPlayerStatus(status); + // Cancel any in-progress fade + if (fadeIntervalRef.current) { + clearInterval(fadeIntervalRef.current); + fadeIntervalRef.current = null; } + // Set initial volume immediately to ensure we start from the correct position + // This is especially important when cancelling a previous fade + playerRef.current?.setVolume(startVolume); + const steps = duration / PLAY_PAUSE_FADE_INTERVAL; const volumeStep = (endVolume - startVolume) / steps; let currentStep = 0; - const promise = new Promise((resolve) => { - const interval = setInterval(() => { + const promise = new Promise((resolve) => { + fadeIntervalRef.current = setInterval(() => { currentStep++; const newVolume = startVolume + volumeStep * currentStep; playerRef.current?.setVolume(newVolume); if (currentStep >= steps) { - clearInterval(interval); - setIsTransitioning(false); - resolve(true); + if (fadeIntervalRef.current) { + clearInterval(fadeIntervalRef.current); + fadeIntervalRef.current = null; + } + // Ensure final volume is exactly the target + playerRef.current?.setVolume(endVolume); + resolve(); } }, PLAY_PAUSE_FADE_INTERVAL); }); @@ -77,7 +88,7 @@ export function WebPlayer() { await promise; } }, - [isTransitioning], + [], ); const onProgressPlayer1 = useCallback( @@ -242,6 +253,16 @@ export function WebPlayer() { [volume, num, isTransitioning, transitionType], ); + // Cleanup fade interval on unmount + useEffect(() => { + return () => { + if (fadeIntervalRef.current) { + clearInterval(fadeIntervalRef.current); + fadeIntervalRef.current = null; + } + }; + }, []); + useEffect(() => { if (localPlayerStatus !== PlayerStatus.PLAYING) { return;