revert waveform playerbar changes

This commit is contained in:
jeffvli
2025-12-05 11:31:02 -08:00
parent 7db3eab38c
commit 2d5671323f
@@ -1,10 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { useWavesurfer } from '@wavesurfer/react'; import { useWavesurfer } from '@wavesurfer/react';
import formatDuration from 'format-duration'; import formatDuration from 'format-duration';
import { AnimatePresence, motion } from 'motion/react'; import { AnimatePresence, motion } from 'motion/react';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { PlayerbarSeekSlider } from './playerbar-seek-slider';
import { CustomPlayerbarSlider } from './playerbar-slider'; import { CustomPlayerbarSlider } from './playerbar-slider';
import styles from './playerbar-waveform.module.css'; import styles from './playerbar-waveform.module.css';
@@ -19,8 +17,6 @@ import {
usePrimaryColor, usePrimaryColor,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { useColorScheme } from '/@/renderer/themes/use-app-theme'; import { useColorScheme } from '/@/renderer/themes/use-app-theme';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { Spinner } from '/@/shared/components/spinner/spinner'; import { Spinner } from '/@/shared/components/spinner/spinner';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
@@ -33,41 +29,16 @@ export const PlayerbarWaveform = () => {
const { mediaSeekToTimestamp } = usePlayer(); const { mediaSeekToTimestamp } = usePlayer();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState<null | { x: number; y: number }>(null); const [tooltipPosition, setTooltipPosition] = useState<null | { x: number; y: number }>(null);
const [tooltipValue, setTooltipValue] = useState(0); const [tooltipValue, setTooltipValue] = useState(0);
const [shouldFallback, setShouldFallback] = useState(false);
const seekTimeoutRef = useRef<NodeJS.Timeout | null>(null); const seekTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastSeekValueRef = useRef<null | number>(null); const lastSeekValueRef = useRef<null | number>(null);
const containerPositionRef = useRef<DOMRect | null>(null); const containerPositionRef = useRef<DOMRect | null>(null);
const loadingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const wavesurferLoadFailureRef = useRef(false);
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0; const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
const streamUrl = useSongUrl(currentSong, true, transcode); const streamUrl = useSongUrl(currentSong, true, transcode);
// Fetch blob from stream URL
const {
data: streamBlob,
failureCount,
isError: isStreamBlobError,
} = useQuery({
enabled: !!streamUrl && !!currentSong && !shouldFallback,
queryFn: async () => {
if (!streamUrl) return undefined;
const response = await fetch(streamUrl);
if (!response.ok) {
throw new Error('Failed to fetch stream blob');
}
return await response.blob();
},
queryKey: [currentSong?._serverId, streamUrl],
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff, max 30s
});
const primaryColor = usePrimaryColor(); const primaryColor = usePrimaryColor();
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
@@ -94,76 +65,28 @@ export const PlayerbarWaveform = () => {
interact: false, interact: false,
normalize: false, normalize: false,
progressColor: primaryColor, progressColor: primaryColor,
url: undefined, // URL will be loaded separately via useEffect url: streamUrl || undefined,
waveColor, waveColor,
}); });
// Update wavesurfer with blob when it becomes available // Reset loading state when stream URL changes and ensure media is muted
useEffect(() => { useEffect(() => {
if (!wavesurfer || !streamBlob) return;
if (loadingTimeoutRef.current) {
clearTimeout(loadingTimeoutRef.current);
loadingTimeoutRef.current = null;
}
// Reset wavesurfer failure flag when loading new blob
wavesurferLoadFailureRef.current = false;
try {
wavesurfer.loadBlob(streamBlob);
setIsLoading(true); setIsLoading(true);
if (wavesurfer) {
wavesurfer.setVolume(0); wavesurfer.setVolume(0);
const mediaElement = wavesurfer.getMediaElement(); const mediaElement = wavesurfer.getMediaElement();
if (mediaElement) { if (mediaElement) {
mediaElement.muted = true; mediaElement.muted = true;
mediaElement.volume = 0; mediaElement.volume = 0;
} }
} catch (error) {
logFn.error(logMsg[LogCategory.OTHER].error, {
category: LogCategory.OTHER,
meta: {
error,
message: 'Failed to load blob into wavesurfer',
},
});
wavesurferLoadFailureRef.current = true;
setIsLoading(false);
setShouldFallback(true);
} }
}, [streamBlob, wavesurfer]); }, [streamUrl, wavesurfer]);
// Reset loading state when song changes // Handle waveform ready state
useEffect(() => { useEffect(() => {
if (!wavesurfer) return; if (!wavesurfer) return;
setIsLoading(true);
setShouldFallback(false);
wavesurferLoadFailureRef.current = false;
wavesurfer.setVolume(0);
const mediaElement = wavesurfer.getMediaElement();
if (mediaElement) {
mediaElement.muted = true;
mediaElement.volume = 0;
}
}, [currentSong?._serverId, currentSong?.id, wavesurfer]);
// Handle waveform ready state and errors
useEffect(() => {
if (!wavesurfer) return;
// Clear any existing loading timeout
if (loadingTimeoutRef.current) {
clearTimeout(loadingTimeoutRef.current);
loadingTimeoutRef.current = null;
}
const handleReady = () => { const handleReady = () => {
if (loadingTimeoutRef.current) {
clearTimeout(loadingTimeoutRef.current);
loadingTimeoutRef.current = null;
}
setIsLoading(false); setIsLoading(false);
const mediaElement = wavesurfer.getMediaElement(); const mediaElement = wavesurfer.getMediaElement();
if (mediaElement) { if (mediaElement) {
@@ -172,23 +95,7 @@ export const PlayerbarWaveform = () => {
} }
}; };
const handleError = (error: Error) => {
logFn.error(logMsg[LogCategory.OTHER].error, {
category: LogCategory.OTHER,
meta: { error },
});
if (loadingTimeoutRef.current) {
clearTimeout(loadingTimeoutRef.current);
loadingTimeoutRef.current = null;
}
wavesurferLoadFailureRef.current = true;
setIsLoading(false);
setShouldFallback(true);
};
wavesurfer.on('ready', handleReady); wavesurfer.on('ready', handleReady);
wavesurfer.on('error', handleError);
// Check if already loaded // Check if already loaded
if (wavesurfer.getDuration() > 0) { if (wavesurfer.getDuration() > 0) {
@@ -198,33 +105,12 @@ export const PlayerbarWaveform = () => {
mediaElement.muted = true; mediaElement.muted = true;
mediaElement.volume = 0; mediaElement.volume = 0;
} }
} else if (isLoading) {
// Set up a timeout fallback to prevent infinite loading
loadingTimeoutRef.current = setTimeout(() => {
if (wavesurfer.getDuration() <= 0) {
logFn.warn(logMsg[LogCategory.OTHER].warning, {
category: LogCategory.OTHER,
meta: {
message: 'Wavesurfer loading timeout - falling back to regular slider',
},
});
wavesurferLoadFailureRef.current = true;
setIsLoading(false);
setShouldFallback(true);
}
loadingTimeoutRef.current = null;
}, 30000); // 30 second timeout
} }
return () => { return () => {
wavesurfer.un('ready', handleReady); wavesurfer.un('ready', handleReady);
wavesurfer.un('error', handleError);
if (loadingTimeoutRef.current) {
clearTimeout(loadingTimeoutRef.current);
loadingTimeoutRef.current = null;
}
}; };
}, [wavesurfer, isLoading]); }, [wavesurfer]);
useEffect(() => { useEffect(() => {
if (!wavesurfer) return; if (!wavesurfer) return;
@@ -442,21 +328,7 @@ export const PlayerbarWaveform = () => {
} }
}, [wavesurfer, currentTime, songDuration, isDragging]); }, [wavesurfer, currentTime, songDuration, isDragging]);
// Handle error state - if blob fetch failed after all retries, fallback to regular slider // Show disabled slider when there's no current song
useEffect(() => {
if (isStreamBlobError && failureCount !== undefined && failureCount >= 4) {
logFn.warn(logMsg[LogCategory.OTHER].warning, {
category: LogCategory.OTHER,
meta: {
message: `Stream blob fetch failed after all retries (${failureCount} attempts) - falling back to regular slider`,
},
});
setIsLoading(false);
setShouldFallback(true);
}
}, [isStreamBlobError, failureCount]);
// Show regular disabled slider when there's no current song
if (!currentSong) { if (!currentSong) {
return ( return (
<CustomPlayerbarSlider <CustomPlayerbarSlider
@@ -473,19 +345,12 @@ export const PlayerbarWaveform = () => {
); );
} }
// Fallback to regular seek slider when waveform fails
if (shouldFallback || (isStreamBlobError && failureCount !== undefined && failureCount >= 4)) {
return <PlayerbarSeekSlider max={songDuration} min={0} />;
}
return ( return (
<div <div
className={styles.wavesurferContainer} className={styles.wavesurferContainer}
onClick={(e) => { onClick={(e) => {
e?.stopPropagation(); e?.stopPropagation();
}} }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{ position: 'relative' }} style={{ position: 'relative' }}
> >
<motion.div <motion.div
@@ -496,7 +361,7 @@ export const PlayerbarWaveform = () => {
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
/> />
<AnimatePresence> <AnimatePresence>
{isLoading && !isHovered && ( {isLoading && (
<motion.div <motion.div
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
@@ -514,19 +379,6 @@ export const PlayerbarWaveform = () => {
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
{isLoading && isHovered && (
<div
style={{
height: '100%',
left: 0,
position: 'absolute',
top: 0,
width: '100%',
}}
>
<PlayerbarSeekSlider max={songDuration} min={0} />
</div>
)}
{tooltipPosition && isDragging && ( {tooltipPosition && isDragging && (
<motion.div <motion.div
animate={{ opacity: 1, scale: 1, x: '-50%' }} animate={{ opacity: 1, scale: 1, x: '-50%' }}