Waveform playerbar improvements (#1781)

* Defer waveform loading & show default seek bar as fallback

* Add configurable waveform loading delay

* Add 2s default value for waveform loading delay

* disable transcoding config on waveform url

---------

Co-authored-by: jeffvli <jeffvictorli@gmail.com>
This commit is contained in:
Darius
2026-03-24 18:06:25 +01:00
committed by GitHub
parent f91dcc6af6
commit 816adfa6c7
4 changed files with 50 additions and 25 deletions
+2
View File
@@ -1082,6 +1082,8 @@
"volumeWheelStep": "volume wheel step", "volumeWheelStep": "volume wheel step",
"volumeWidth_description": "the width of the volume slider", "volumeWidth_description": "the width of the volume slider",
"volumeWidth": "volume slider width", "volumeWidth": "volume slider width",
"waveformLoadingDelay": "waveform loading delay",
"waveformLoadingDelay_description": "delay in seconds before loading waveform. increase this value if you are experiencing stutters when using the web player.",
"webAudio_description": "use web audio. this enables advanced features like replaygain. disable if you experience otherwise", "webAudio_description": "use web audio. this enables advanced features like replaygain. disable if you experience otherwise",
"webAudio": "use web audio", "webAudio": "use web audio",
"windowBarStyle_description": "select the style of the window bar", "windowBarStyle_description": "select the style of the window bar",
@@ -7,10 +7,10 @@ import { CustomPlayerbarSlider } from './playerbar-slider';
import styles from './playerbar-waveform.module.css'; import styles from './playerbar-waveform.module.css';
import { useSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url'; import { useSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url';
import { PlayerbarSeekSlider } from '/@/renderer/features/player/components/playerbar-seek-slider';
import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { BarAlign, usePlayerbarSlider, usePlayerSong, usePlayerTimestamp } from '/@/renderer/store'; import { BarAlign, usePlayerbarSlider, usePlayerSong, usePlayerTimestamp } from '/@/renderer/store';
import { useAppThemeColors, useColorScheme } from '/@/renderer/themes/use-app-theme'; import { useAppThemeColors, useColorScheme } from '/@/renderer/themes/use-app-theme';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
export const PlayerbarWaveform = () => { export const PlayerbarWaveform = () => {
@@ -18,6 +18,7 @@ export const PlayerbarWaveform = () => {
const playerbarSlider = usePlayerbarSlider(); const playerbarSlider = usePlayerbarSlider();
const currentTime = usePlayerTimestamp(); const currentTime = usePlayerTimestamp();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const audioElementRef = useRef<HTMLAudioElement>(document.createElement('audio'));
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);
@@ -29,7 +30,7 @@ export const PlayerbarWaveform = () => {
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0; const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
const streamUrl = useSongUrl(currentSong, true, { bitrate: 64, enabled: true, format: 'mp3' }); const streamUrl = useSongUrl(currentSong, true, { bitrate: 64, enabled: false, format: 'mp3' });
const { color } = useAppThemeColors(); const { color } = useAppThemeColors();
const primaryColor = (color['--theme-colors-primary'] as string) || 'rgb(53, 116, 252)'; const primaryColor = (color['--theme-colors-primary'] as string) || 'rgb(53, 116, 252)';
@@ -56,28 +57,20 @@ export const PlayerbarWaveform = () => {
fillParent: true, fillParent: true,
height: 18, height: 18,
interact: false, interact: false,
media: audioElementRef.current,
normalize: false, normalize: false,
progressColor: primaryColor, progressColor: primaryColor,
url: streamUrl || undefined,
waveColor, waveColor,
}); });
// Reset loading state when stream URL changes and ensure media is muted // Reset loading state when stream URL changes and ensure media is muted
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
if (wavesurfer) { }, [streamUrl]);
wavesurfer.setVolume(0);
const mediaElement = wavesurfer.getMediaElement();
if (mediaElement) {
mediaElement.muted = true;
mediaElement.volume = 0;
}
}
}, [streamUrl, wavesurfer]);
// Handle waveform ready state // Handle waveform ready state
useEffect(() => { useEffect(() => {
if (!wavesurfer) return; if (!wavesurfer || !streamUrl) return;
const handleReady = () => { const handleReady = () => {
setIsLoading(false); setIsLoading(false);
@@ -90,20 +83,18 @@ export const PlayerbarWaveform = () => {
wavesurfer.on('ready', handleReady); wavesurfer.on('ready', handleReady);
// Check if already loaded const waveformTimeout = setTimeout(
if (wavesurfer.getDuration() > 0) { () => {
setIsLoading(false); wavesurfer.load(streamUrl);
const mediaElement = wavesurfer.getMediaElement(); },
if (mediaElement) { playerbarSlider?.loadingDelay ? playerbarSlider.loadingDelay * 1000 : 2000,
mediaElement.muted = true; );
mediaElement.volume = 0;
}
}
return () => { return () => {
wavesurfer.un('ready', handleReady); wavesurfer.un('ready', handleReady);
clearTimeout(waveformTimeout);
}; };
}, [wavesurfer]); }, [wavesurfer, streamUrl, playerbarSlider.loadingDelay]);
useEffect(() => { useEffect(() => {
if (!wavesurfer) return; if (!wavesurfer) return;
@@ -363,12 +354,12 @@ export const PlayerbarWaveform = () => {
height: '100%', height: '100%',
left: 0, left: 0,
position: 'absolute', position: 'absolute',
top: 0, top: 3,
width: '100%', width: '100%',
}} }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
> >
<Spinner container /> <PlayerbarSeekSlider max={songDuration} min={0} />
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
@@ -477,6 +477,36 @@ export const ControlSettings = memo(() => {
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
}), }),
}, },
{
control: (
<NumberInput
defaultValue={playerbarSlider?.loadingDelay ?? 2}
max={30}
min={0}
onBlur={(e) => {
setSettings({
general: {
...settings,
playerbarSlider: {
...playerbarSlider,
loadingDelay: e.currentTarget.value
? Number(e.currentTarget.value)
: 2,
},
},
});
}}
rightSection={<Text size="sm">s</Text>}
width={75}
/>
),
description: t('setting.waveformLoadingDelay', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.waveformLoadingDelay', { postProcess: 'sentenceCase' }),
},
] ]
: []), : []),
]; ];
+2
View File
@@ -305,6 +305,7 @@ const PlayerbarSliderSchema = z.object({
barGap: z.number(), barGap: z.number(),
barRadius: z.number(), barRadius: z.number(),
barWidth: z.number(), barWidth: z.number(),
loadingDelay: z.number(),
type: PlayerbarSliderTypeSchema, type: PlayerbarSliderTypeSchema,
}); });
@@ -1148,6 +1149,7 @@ const initialState: SettingsState = {
barGap: 1, barGap: 1,
barRadius: 4, barRadius: 4,
barWidth: 2, barWidth: 2,
loadingDelay: 2,
type: PlayerbarSliderType.SLIDER, type: PlayerbarSliderType.SLIDER,
}, },
playerItems, playerItems,