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",
"volumeWidth_description": "the width of the volume slider",
"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": "use web audio",
"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 { 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 { BarAlign, usePlayerbarSlider, usePlayerSong, usePlayerTimestamp } from '/@/renderer/store';
import { useAppThemeColors, useColorScheme } from '/@/renderer/themes/use-app-theme';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Text } from '/@/shared/components/text/text';
export const PlayerbarWaveform = () => {
@@ -18,6 +18,7 @@ export const PlayerbarWaveform = () => {
const playerbarSlider = usePlayerbarSlider();
const currentTime = usePlayerTimestamp();
const containerRef = useRef<HTMLDivElement>(null);
const audioElementRef = useRef<HTMLAudioElement>(document.createElement('audio'));
const { mediaSeekToTimestamp } = usePlayer();
const [isLoading, setIsLoading] = useState(true);
const [isDragging, setIsDragging] = useState(false);
@@ -29,7 +30,7 @@ export const PlayerbarWaveform = () => {
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 primaryColor = (color['--theme-colors-primary'] as string) || 'rgb(53, 116, 252)';
@@ -56,28 +57,20 @@ export const PlayerbarWaveform = () => {
fillParent: true,
height: 18,
interact: false,
media: audioElementRef.current,
normalize: false,
progressColor: primaryColor,
url: streamUrl || undefined,
waveColor,
});
// Reset loading state when stream URL changes and ensure media is muted
useEffect(() => {
setIsLoading(true);
if (wavesurfer) {
wavesurfer.setVolume(0);
const mediaElement = wavesurfer.getMediaElement();
if (mediaElement) {
mediaElement.muted = true;
mediaElement.volume = 0;
}
}
}, [streamUrl, wavesurfer]);
}, [streamUrl]);
// Handle waveform ready state
useEffect(() => {
if (!wavesurfer) return;
if (!wavesurfer || !streamUrl) return;
const handleReady = () => {
setIsLoading(false);
@@ -90,20 +83,18 @@ export const PlayerbarWaveform = () => {
wavesurfer.on('ready', handleReady);
// Check if already loaded
if (wavesurfer.getDuration() > 0) {
setIsLoading(false);
const mediaElement = wavesurfer.getMediaElement();
if (mediaElement) {
mediaElement.muted = true;
mediaElement.volume = 0;
}
}
const waveformTimeout = setTimeout(
() => {
wavesurfer.load(streamUrl);
},
playerbarSlider?.loadingDelay ? playerbarSlider.loadingDelay * 1000 : 2000,
);
return () => {
wavesurfer.un('ready', handleReady);
clearTimeout(waveformTimeout);
};
}, [wavesurfer]);
}, [wavesurfer, streamUrl, playerbarSlider.loadingDelay]);
useEffect(() => {
if (!wavesurfer) return;
@@ -363,12 +354,12 @@ export const PlayerbarWaveform = () => {
height: '100%',
left: 0,
position: 'absolute',
top: 0,
top: 3,
width: '100%',
}}
transition={{ duration: 0.2 }}
>
<Spinner container />
<PlayerbarSeekSlider max={songDuration} min={0} />
</motion.div>
)}
</AnimatePresence>
@@ -477,6 +477,36 @@ export const ControlSettings = memo(() => {
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(),
barRadius: z.number(),
barWidth: z.number(),
loadingDelay: z.number(),
type: PlayerbarSliderTypeSchema,
});
@@ -1148,6 +1149,7 @@ const initialState: SettingsState = {
barGap: 1,
barRadius: 4,
barWidth: 2,
loadingDelay: 2,
type: PlayerbarSliderType.SLIDER,
},
playerItems,