fix(player): refactor audio element handling and improve auto-play reliability

- Replace detached `new Audio()` with rendered `<audio>` element to better support mobile behaviors and standard DOM events
- Introduce `autoAdvanceLockRef` to prevent race conditions where the next song might be triggered multiple times
- Add manual time check near the end of the track to trigger auto-advance, acting as a fallback for the `ended` event
- Update `playNext` logic to handle immediate playback transitions more robustly
This commit is contained in:
史悦
2026-01-07 10:08:35 +08:00
parent a244347999
commit c3877be35d

View File

@@ -875,14 +875,18 @@
const [showPlaylist, setShowPlaylist] = useState(false);
const [showSideDrawer, setShowSideDrawer] = useState(false);
const audioRef = useRef(new Audio());
const audioRef = useRef(null);
const autoAdvanceLockRef = useRef(false);
const currentSongRef = useRef(null);
useEffect(() => {
currentSongRef.current = currentSong;
}, [currentSong]);
useEffect(() => {
autoAdvanceLockRef.current = false;
}, [currentSong]);
// Media Session Refs
const playNextRef = useRef(null);
const playPrevRef = useRef(null);
@@ -997,12 +1001,27 @@
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
audio.volume = volume;
const triggerAutoNext = () => {
if (autoAdvanceLockRef.current) return;
autoAdvanceLockRef.current = true;
playNext(true, { immediate: true });
};
const updateTime = () => {
setCurrentTime(audio.currentTime);
if (autoAdvanceLockRef.current) return;
const durationSeconds = resolveDurationSeconds(audio, currentSongRef.current);
if (Number.isFinite(durationSeconds) && durationSeconds > 0) {
if (audio.currentTime >= durationSeconds - 0.35) {
triggerAutoNext();
}
}
};
const updateDuration = () => setDuration(resolveDurationSeconds(audio, currentSongRef.current));
const onEnded = () => playNext(true);
const onEnded = () => triggerAutoNext();
audio.addEventListener('timeupdate', updateTime);
audio.addEventListener('loadedmetadata', updateDuration);
@@ -1013,23 +1032,25 @@
audio.removeEventListener('loadedmetadata', updateDuration);
audio.removeEventListener('ended', onEnded);
};
}, [playlist, currentSong, mode]);
}, [playlist, currentSong, mode, volume, quality]);
useEffect(() => {
if (currentSong) {
const audio = audioRef.current;
if (!audio) return;
// Update URL when quality changes or song changes
const url = api.getSongUrl(currentSong.id, currentSong.platform || currentSong.source, quality);
// Only update src if it's different to avoid reloading same song on re-render (unless quality changed)
// Note: audioRef.current.src returns full absolute URL
const currentSrc = audioRef.current.src;
const currentSrc = audio.src;
// Simple check if src changed significantly (avoiding minor encoding diffs if possible, but exact match is safer)
if (currentSrc !== url) {
const wasPlaying = isPlaying;
audioRef.current.src = url;
audio.src = url;
if (wasPlaying) {
audioRef.current.play()
audio.play()
.then(() => setIsPlaying(true))
.catch(e => {
console.warn("Auto-play prevented:", e);
@@ -1042,8 +1063,10 @@
useEffect(() => {
if (currentSong) {
if (isPlaying) audioRef.current.play().catch(() => setIsPlaying(false));
else audioRef.current.pause();
const audio = audioRef.current;
if (!audio) return;
if (isPlaying) audio.play().catch(() => setIsPlaying(false));
else audio.pause();
}
}, [isPlaying]);
@@ -1117,12 +1140,15 @@
const togglePlay = () => setIsPlaying(!isPlaying);
const playNext = (auto = false) => {
const playNext = (auto = false, options = {}) => {
if (playlist.length === 0) return;
if (auto && mode === 'one') {
audioRef.current.currentTime = 0;
audioRef.current.play();
const audio = audioRef.current;
if (!audio) return;
audio.currentTime = 0;
audio.play().catch(() => setIsPlaying(false));
autoAdvanceLockRef.current = false;
return;
}
@@ -1135,8 +1161,24 @@
nextIdx = (currIdx + 1) % playlist.length;
}
setCurrentSong(playlist[nextIdx]);
const nextSong = playlist[nextIdx];
setCurrentSong(nextSong);
setIsPlaying(true);
if (options.immediate) {
const audio = audioRef.current;
if (!audio || !nextSong) return;
const url = api.getSongUrl(nextSong.id, nextSong.platform || nextSong.source, quality);
if (audio.src !== url) {
audio.src = url;
}
audio.play()
.then(() => setIsPlaying(true))
.catch(e => {
console.warn("Auto-play prevented:", e);
setIsPlaying(false);
});
}
};
const playPrev = () => {
@@ -1172,7 +1214,9 @@
const handleSeek = (time) => {
if (Number.isFinite(time)) {
audioRef.current.currentTime = time;
const audio = audioRef.current;
if (!audio) return;
audio.currentTime = time;
setCurrentTime(time);
updateMediaSessionPosition();
}
@@ -1263,6 +1307,7 @@
if (!('mediaSession' in navigator)) return;
const audio = audioRef.current;
if (!audio) return;
const updateState = () => {
if (!audio) return;
@@ -1317,6 +1362,14 @@
return (
<div className="flex flex-col h-full bg-darker font-sans">
<audio
ref={audioRef}
className="hidden"
preload="auto"
playsInline
webkit-playsinline="true"
aria-hidden="true"
/>
<SideDrawer
isOpen={showSideDrawer}
onClose={() => setShowSideDrawer(false)}