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