fix(player): clean up audio listeners + keep position on URL change
- register named listeners with cleanup so a remount (StrictMode) can't leave duplicates double-dispatching - when the same track's URL changes (quality switch or token refresh), resume at the current position instead of restarting from 0 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+38
-21
@@ -31,38 +31,43 @@ export function useAudioPlayer() {
|
|||||||
// on demand; a cache miss transparently falls back to the original.
|
// on demand; a cache miss transparently falls back to the original.
|
||||||
const { data: settings } = useGetSettingsQuery();
|
const { data: settings } = useGetSettingsQuery();
|
||||||
const streamQuality = settings?.streamQuality;
|
const streamQuality = settings?.streamQuality;
|
||||||
const isSetup = useRef(false);
|
// `ended` reads the latest loop flag through a ref so the listener doesn't
|
||||||
// `ended` is registered once below; read the latest loop flag through a ref
|
// need re-binding on every queue change.
|
||||||
// so the listener doesn't need to be re-bound on every queue change.
|
|
||||||
const loopRef = useRef(queue.loop);
|
const loopRef = useRef(queue.loop);
|
||||||
loopRef.current = queue.loop;
|
loopRef.current = queue.loop;
|
||||||
|
// The track id the current <audio>.src points at — lets us tell a real track
|
||||||
|
// change (start at 0) from a same-track URL change (preserve position).
|
||||||
|
const loadedTrackRef = useRef<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSetup.current) return;
|
|
||||||
isSetup.current = true;
|
|
||||||
|
|
||||||
const audio = getAudio();
|
const audio = getAudio();
|
||||||
|
// Named handlers + cleanup so a remount (React StrictMode, or the hook
|
||||||
audio.addEventListener('timeupdate', () => {
|
// mounting elsewhere) can't leave duplicate listeners double-dispatching.
|
||||||
dispatch(setPosition(audio.currentTime));
|
const onTime = () => dispatch(setPosition(audio.currentTime));
|
||||||
});
|
const onDuration = () => dispatch(setDuration(audio.duration || 0));
|
||||||
audio.addEventListener('durationchange', () => {
|
const onEnded = () => {
|
||||||
dispatch(setDuration(audio.duration || 0));
|
|
||||||
});
|
|
||||||
audio.addEventListener('ended', () => {
|
|
||||||
if (loopRef.current) {
|
if (loopRef.current) {
|
||||||
audio.currentTime = 0;
|
audio.currentTime = 0;
|
||||||
void audio.play();
|
void audio.play();
|
||||||
} else {
|
} else {
|
||||||
dispatch(nextTrack());
|
dispatch(nextTrack());
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
audio.addEventListener('pause', () => {
|
const onPause = () => dispatch(pause());
|
||||||
dispatch(pause());
|
const onPlay = () => dispatch(resume());
|
||||||
});
|
|
||||||
audio.addEventListener('play', () => {
|
audio.addEventListener('timeupdate', onTime);
|
||||||
dispatch(resume());
|
audio.addEventListener('durationchange', onDuration);
|
||||||
});
|
audio.addEventListener('ended', onEnded);
|
||||||
|
audio.addEventListener('pause', onPause);
|
||||||
|
audio.addEventListener('play', onPlay);
|
||||||
|
return () => {
|
||||||
|
audio.removeEventListener('timeupdate', onTime);
|
||||||
|
audio.removeEventListener('durationchange', onDuration);
|
||||||
|
audio.removeEventListener('ended', onEnded);
|
||||||
|
audio.removeEventListener('pause', onPause);
|
||||||
|
audio.removeEventListener('play', onPlay);
|
||||||
|
};
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -70,8 +75,20 @@ export function useAudioPlayer() {
|
|||||||
const audio = getAudio();
|
const audio = getAudio();
|
||||||
const url = getStreamUrl(player.currentTrackId, accessToken, streamQuality);
|
const url = getStreamUrl(player.currentTrackId, accessToken, streamQuality);
|
||||||
if (audio.src !== url) {
|
if (audio.src !== url) {
|
||||||
|
// Same track, new URL (quality changed, or the access token was refreshed
|
||||||
|
// mid-play) → keep the playback position instead of restarting from 0.
|
||||||
|
const sameTrack = loadedTrackRef.current === player.currentTrackId;
|
||||||
|
const resumeAt = sameTrack ? audio.currentTime : 0;
|
||||||
audio.src = url;
|
audio.src = url;
|
||||||
audio.load();
|
audio.load();
|
||||||
|
loadedTrackRef.current = player.currentTrackId;
|
||||||
|
if (resumeAt > 0) {
|
||||||
|
const onMeta = () => {
|
||||||
|
audio.currentTime = resumeAt;
|
||||||
|
audio.removeEventListener('loadedmetadata', onMeta);
|
||||||
|
};
|
||||||
|
audio.addEventListener('loadedmetadata', onMeta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (player.isPlaying) {
|
if (player.isPlaying) {
|
||||||
void audio.play();
|
void audio.play();
|
||||||
|
|||||||
Reference in New Issue
Block a user