diff --git a/src/app/api/music/v2/stream/route.ts b/src/app/api/music/v2/stream/route.ts index 64e984d..87b47ea 100644 --- a/src/app/api/music/v2/stream/route.ts +++ b/src/app/api/music/v2/stream/route.ts @@ -156,7 +156,7 @@ export async function GET(request: NextRequest) { try { upstream = await fetch(upstreamUrl, { headers: upstreamHeaders, - signal: AbortSignal.timeout(30000), + signal: AbortSignal.timeout(90000), cache: 'no-store', }); } catch (error) { @@ -165,7 +165,7 @@ export async function GET(request: NextRequest) { upstreamHeaders = buildUpstreamHeaders(request, upstreamUrl); upstream = await fetch(upstreamUrl, { headers: upstreamHeaders, - signal: AbortSignal.timeout(30000), + signal: AbortSignal.timeout(90000), cache: 'no-store', }); console.warn('[music-v2/stream] upstream fetch failed once, retried with refreshed URL', { diff --git a/src/app/music/page.tsx b/src/app/music/page.tsx index f9688e2..56b3be3 100644 --- a/src/app/music/page.tsx +++ b/src/app/music/page.tsx @@ -285,6 +285,7 @@ export default function MusicPage() { const [loadingMoreSearch, setLoadingMoreSearch] = useState(false); const [currentSong, setCurrentSong] = useState(null); const [isPlaying, setIsPlaying] = useState(false); + const [isBuffering, setIsBuffering] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [volume, setVolume] = useState(100); @@ -660,6 +661,7 @@ export default function MusicPage() { setCurrentSongUrl(streamUrl); if (audioRef.current) { + setIsBuffering(true); audioRef.current.src = streamUrl; audioRef.current.addEventListener('loadedmetadata', restoreTime, { once: true }); audioRef.current.load(); @@ -679,6 +681,7 @@ export default function MusicPage() { const data = await fetchPlayData(latestDbSong, platform, selectedQuality, true); if (data.success && data.data?.play?.directUrl && audioRef.current) { setCurrentSongUrl(data.data.play.directUrl); + setIsBuffering(true); audioRef.current.src = data.data.play.directUrl; audioRef.current.addEventListener('loadedmetadata', restoreTime, { once: true }); audioRef.current.load(); @@ -1323,10 +1326,12 @@ export default function MusicPage() { setCurrentSongUrl(streamUrl); if (audioRef.current) { + setIsBuffering(true); audioRef.current.src = streamUrl; audioRef.current.load(); audioRef.current.play().catch(err => { console.error('播放失败:', err); + setIsBuffering(false); }); setIsPlaying(true); } @@ -1372,10 +1377,12 @@ export default function MusicPage() { setCurrentSongUrl(data.data.play.directUrl); if (audioRef.current) { + setIsBuffering(true); audioRef.current.src = data.data.play.directUrl; audioRef.current.load(); audioRef.current.play().catch(err => { console.error('播放失败:', err); + setIsBuffering(false); }); setIsPlaying(true); } @@ -1385,6 +1392,7 @@ export default function MusicPage() { } } catch (error) { console.error('播放失败:', error); + setIsBuffering(false); } finally { endResolving(); } @@ -1457,8 +1465,10 @@ export default function MusicPage() { }); } } else { + setIsBuffering(true); audioRef.current.play().catch(err => { console.error('播放失败:', err); + setIsBuffering(false); }); setIsPlaying(true); } @@ -1612,6 +1622,7 @@ export default function MusicPage() { .catch((error) => { console.error('切换音质后播放失败:', error); setIsPlaying(false); + setIsBuffering(false); }); } else { setIsPlaying(false); @@ -1619,12 +1630,14 @@ export default function MusicPage() { }; audio.pause(); + setIsBuffering(true); audio.src = nextSongUrl; audio.addEventListener('loadedmetadata', resumeAfterMetadata, { once: true }); audio.load(); setIsPlaying(shouldResume); } catch (error) { console.error('切换音质失败:', error); + setIsBuffering(false); setToast({ message: (error as Error).message || '切换音质失败', type: 'error', @@ -1801,6 +1814,16 @@ export default function MusicPage() { } }; + const handleBufferingStart = () => { + if (audio.src && !audio.ended) { + setIsBuffering(true); + } + }; + + const handleBufferingEnd = () => { + setIsBuffering(false); + }; + const handleDurationChange = () => { setDuration(audio.duration); @@ -1830,6 +1853,7 @@ export default function MusicPage() { } }; const handleEnded = () => { + setIsBuffering(false); if (playMode === 'single') { audio.currentTime = 0; audio.play(); @@ -1849,12 +1873,28 @@ export default function MusicPage() { }; audio.addEventListener('timeupdate', handleTimeUpdate); + audio.addEventListener('loadstart', handleBufferingStart); + audio.addEventListener('waiting', handleBufferingStart); + audio.addEventListener('stalled', handleBufferingStart); + audio.addEventListener('canplay', handleBufferingEnd); + audio.addEventListener('canplaythrough', handleBufferingEnd); + audio.addEventListener('playing', handleBufferingEnd); + audio.addEventListener('pause', handleBufferingEnd); + audio.addEventListener('error', handleBufferingEnd); audio.addEventListener('loadedmetadata', handleLoadedMetadata); audio.addEventListener('durationchange', handleDurationChange); audio.addEventListener('ended', handleEnded); return () => { audio.removeEventListener('timeupdate', handleTimeUpdate); + audio.removeEventListener('loadstart', handleBufferingStart); + audio.removeEventListener('waiting', handleBufferingStart); + audio.removeEventListener('stalled', handleBufferingStart); + audio.removeEventListener('canplay', handleBufferingEnd); + audio.removeEventListener('canplaythrough', handleBufferingEnd); + audio.removeEventListener('playing', handleBufferingEnd); + audio.removeEventListener('pause', handleBufferingEnd); + audio.removeEventListener('error', handleBufferingEnd); audio.removeEventListener('loadedmetadata', handleLoadedMetadata); audio.removeEventListener('durationchange', handleDurationChange); audio.removeEventListener('ended', handleEnded); @@ -1918,6 +1958,7 @@ export default function MusicPage() { // 进度条拖动 const handleProgressChange = (e: React.ChangeEvent) => { const newTime = (parseFloat(e.target.value) / 100) * duration; + setCurrentTime(newTime); if (audioRef.current) { audioRef.current.currentTime = newTime; } @@ -2002,6 +2043,7 @@ export default function MusicPage() { }; const progress = duration > 0 ? (currentTime / duration) * 100 : 0; + const showStreamBuffering = Boolean(currentSong && isBuffering); const toggleSpectrum = () => { setShowSpectrum(prev => !prev); @@ -2267,6 +2309,92 @@ export default function MusicPage() { opacity: 1; } } + @keyframes music-buffer-scan { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(200%); + } + } + @keyframes music-buffer-stripes { + 0% { + background-position: 0 0; + } + 100% { + background-position: 24px 0; + } + } + .music-buffer-track { + background-image: repeating-linear-gradient( + 115deg, + rgba(16, 185, 129, 0.08) 0, + rgba(16, 185, 129, 0.08) 6px, + rgba(110, 231, 183, 0.28) 6px, + rgba(110, 231, 183, 0.28) 12px + ); + background-size: 24px 100%; + animation: music-buffer-stripes 0.65s linear infinite; + } + .music-buffer-track::before { + content: ''; + position: absolute; + inset: 0; + width: 42%; + background: linear-gradient( + 90deg, + transparent, + rgba(167, 243, 208, 0.95), + transparent + ); + filter: drop-shadow(0 0 8px rgba(52, 211, 153, 0.7)); + animation: music-buffer-scan 0.95s ease-in-out infinite; + } + `} + {resolvingCount > 0 && (
@@ -2729,9 +2857,12 @@ export default function MusicPage() { {/* Progress Bar */}
+ {showStreamBuffering && ( +
+ )}
-
{currentSong.name}
+
+
{currentSong.name}
+ {showStreamBuffering && ( + + + 缓冲中 + + )} +
{currentSong.artist}
@@ -3169,9 +3308,17 @@ export default function MusicPage() {
{/* 进度条 */} -
+
+ {showStreamBuffering && ( +
+ + + 缓冲中 + +
+ )} {showSpectrum && ( -
+
{formatTime(currentTime)}
@@ -3183,9 +3330,12 @@ export default function MusicPage() { {formatTime(currentTime)}
+ {showStreamBuffering && ( +
+ )}