add cancellation to player status fade

This commit is contained in:
jeffvli
2025-11-30 19:25:30 -08:00
parent 02144db221
commit 2ceca9c034
2 changed files with 59 additions and 17 deletions
@@ -31,28 +31,39 @@ export function MpvPlayer() {
const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status); const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status);
const [isTransitioning, setIsTransitioning] = useState(false); const [isTransitioning, setIsTransitioning] = useState(false);
const fadeIntervalRef = useRef<NodeJS.Timeout | null>(null);
const fadeAndSetStatus = useCallback( const fadeAndSetStatus = useCallback(
async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => { async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => {
if (isTransitioning) { // Cancel any in-progress fade
return setLocalPlayerStatus(status); 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 steps = duration / PLAY_PAUSE_FADE_INTERVAL;
const volumeStep = (endVolume - startVolume) / steps; const volumeStep = (endVolume - startVolume) / steps;
let currentStep = 0; let currentStep = 0;
const promise = new Promise((resolve) => { const promise = new Promise<void>((resolve) => {
const interval = setInterval(() => { fadeIntervalRef.current = setInterval(() => {
currentStep++; currentStep++;
const newVolume = startVolume + volumeStep * currentStep; const newVolume = startVolume + volumeStep * currentStep;
playerRef.current?.setVolume(newVolume); playerRef.current?.setVolume(newVolume);
if (currentStep >= steps) { if (currentStep >= steps) {
clearInterval(interval); if (fadeIntervalRef.current) {
setIsTransitioning(false); clearInterval(fadeIntervalRef.current);
resolve(true); fadeIntervalRef.current = null;
}
// Ensure final volume is exactly the target
playerRef.current?.setVolume(endVolume);
resolve();
} }
}, PLAY_PAUSE_FADE_INTERVAL); }, PLAY_PAUSE_FADE_INTERVAL);
}); });
@@ -65,7 +76,7 @@ export function MpvPlayer() {
await promise; await promise;
} }
}, },
[isTransitioning], [],
); );
const onProgress = useCallback(() => { const onProgress = useCallback(() => {
@@ -106,9 +117,19 @@ export function MpvPlayer() {
playerRef.current?.setVolume(volume); 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(() => { useEffect(() => {
if (localPlayerStatus !== PlayerStatus.PLAYING) { if (localPlayerStatus !== PlayerStatus.PLAYING) {
return; return;
@@ -40,31 +40,42 @@ export function WebPlayer() {
const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status); const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status);
const [isTransitioning, setIsTransitioning] = useState<boolean | string>(false); const [isTransitioning, setIsTransitioning] = useState<boolean | string>(false);
const fadeIntervalRef = useRef<NodeJS.Timeout | null>(null);
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(null); const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(null);
const [player2Source, setPlayer2Source] = useState<MediaElementAudioSourceNode | null>(null); const [player2Source, setPlayer2Source] = useState<MediaElementAudioSourceNode | null>(null);
const fadeAndSetStatus = useCallback( const fadeAndSetStatus = useCallback(
async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => { async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => {
if (isTransitioning) { // Cancel any in-progress fade
return setLocalPlayerStatus(status); 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 steps = duration / PLAY_PAUSE_FADE_INTERVAL;
const volumeStep = (endVolume - startVolume) / steps; const volumeStep = (endVolume - startVolume) / steps;
let currentStep = 0; let currentStep = 0;
const promise = new Promise((resolve) => { const promise = new Promise<void>((resolve) => {
const interval = setInterval(() => { fadeIntervalRef.current = setInterval(() => {
currentStep++; currentStep++;
const newVolume = startVolume + volumeStep * currentStep; const newVolume = startVolume + volumeStep * currentStep;
playerRef.current?.setVolume(newVolume); playerRef.current?.setVolume(newVolume);
if (currentStep >= steps) { if (currentStep >= steps) {
clearInterval(interval); if (fadeIntervalRef.current) {
setIsTransitioning(false); clearInterval(fadeIntervalRef.current);
resolve(true); fadeIntervalRef.current = null;
}
// Ensure final volume is exactly the target
playerRef.current?.setVolume(endVolume);
resolve();
} }
}, PLAY_PAUSE_FADE_INTERVAL); }, PLAY_PAUSE_FADE_INTERVAL);
}); });
@@ -77,7 +88,7 @@ export function WebPlayer() {
await promise; await promise;
} }
}, },
[isTransitioning], [],
); );
const onProgressPlayer1 = useCallback( const onProgressPlayer1 = useCallback(
@@ -242,6 +253,16 @@ export function WebPlayer() {
[volume, num, isTransitioning, transitionType], [volume, num, isTransitioning, transitionType],
); );
// Cleanup fade interval on unmount
useEffect(() => {
return () => {
if (fadeIntervalRef.current) {
clearInterval(fadeIntervalRef.current);
fadeIntervalRef.current = null;
}
};
}, []);
useEffect(() => { useEffect(() => {
if (localPlayerStatus !== PlayerStatus.PLAYING) { if (localPlayerStatus !== PlayerStatus.PLAYING) {
return; return;