/* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; import { usePathname, useRouter } from 'next/navigation'; import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import AddToPlaylistModal from '@/components/AddToPlaylistModal'; import Toast, { ToastProps } from '@/components/Toast'; import LyricsPiPWindow from '@/components/LyricsPiPWindow'; import MusicSidebarDrawer from '@/components/music/MusicSidebarDrawer'; import { useWatchRoomContextSafe } from '@/components/WatchRoomProvider'; import { getSourceDisplayLabel, normalizeSource, SourcePill } from '@/lib/music/shared'; import type { MusicQuality, MusicSource, Song } from '@/lib/music/types'; import type { MusicQueueItem, MusicSyncState } from '@/types/watch-room'; const SPECTRUM_BIN_COUNT = 96; const SPECTRUM_IDLE_LEVEL = 0.02; const SPECTRUM_EDGE_TRIM = 8; const SPECTRUM_REFERENCE_VOLUME = 10; const SPECTRUM_MIN_VOLUME = 5; const SPECTRUM_MAX_REFERENCE_VOLUME = 15; interface PlayRecord { platform: MusicSource; id: string; playTime: number; // 播放时间(秒) duration: number; // 总时长(秒) timestamp: number; // 添加时间戳 } interface LyricLine { time: number; text: string; translation?: string; } interface DbRecord { source: MusicSource; songId: string; id: string; playProgressSec: number; durationSec: number; createdAt: number; lastPlayedAt: number; name: string; artist: string; album?: string; cover?: string; durationText?: string; songmid?: string; } function getMusicQueueItemKey(song: { id: string; platform?: string }, fallbackPlatform = '') { return `${song.platform || fallbackPlatform}:${song.id}`; } function AudioSpectrumCanvas({ bars, compact = false, }: { bars: number[]; compact?: boolean; }) { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const draw = () => { const rect = canvas.getBoundingClientRect(); if (!rect.width || !rect.height) return; const dpr = window.devicePixelRatio || 1; const width = Math.round(rect.width * dpr); const height = Math.round(rect.height * dpr); if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } const ctx = canvas.getContext('2d'); if (!ctx) return; ctx.clearRect(0, 0, width, height); const targetPitch = compact ? 4.2 : 4.6; const gap = Math.max(1, Math.round(dpr)); const count = Math.max(1, Math.floor(rect.width / targetPitch)); const barWidth = Math.max(2 * dpr, (width - gap * (count - 1)) / count); const cubeHeight = compact ? Math.max(2, Math.round(2 * dpr)) : Math.max(2, Math.round(2.5 * dpr)); const cubeGap = 1; const scaleBase = compact ? height * 1.55 : height * 1.42; const themeColor = '#10b981'; const sampleBar = (index: number) => { const usableLength = Math.max(1, bars.length - SPECTRUM_EDGE_TRIM * 2); const mappedStart = SPECTRUM_EDGE_TRIM + Math.floor((index / count) * usableLength); const start = Math.min(bars.length - 1, mappedStart); const mappedEnd = SPECTRUM_EDGE_TRIM + Math.max(mappedStart + 1, Math.floor(((index + 1) / count) * usableLength)); const end = Math.min(bars.length, Math.max(start + 1, mappedEnd)); let total = 0; for (let i = start; i < end; i++) total += bars[i] ?? 0; return total / Math.max(1, end - start); }; ctx.fillStyle = themeColor; ctx.strokeStyle = themeColor; for (let i = 0; i < count; i++) { const q = Math.max(SPECTRUM_IDLE_LEVEL, sampleBar(i)) * scaleBase; const cubeCount = Math.max(1, Math.ceil(q / Math.max(1, barWidth * 0.9))); const x = i === count - 1 ? width - barWidth : i * (barWidth + gap); for (let segment = 0; segment < cubeCount; segment++) { const y = height - segment * (cubeHeight + cubeGap); ctx.beginPath(); ctx.roundRect(x, y - cubeHeight, barWidth, cubeHeight, Math.min(2 * dpr, cubeHeight / 2)); ctx.fill(); } } }; draw(); const observer = new ResizeObserver(draw); observer.observe(canvas); return () => observer.disconnect(); }, [bars, compact]); return ( ); } const VINYL_NEEDLE_SVG = `url('data:image/svg+xml;utf8,')`; function VinylTurntable({ cover, title, isPlaying, className = '', }: { cover?: string; title: string; isPlaying: boolean; className?: string; }) { return (
{cover ? ( {title} { e.currentTarget.style.display = 'none'; }} /> ) : ( )}
); } // 扩展 Window 接口以支持 Document PiP API declare global { interface Window { documentPictureInPicture?: { requestWindow: (options: { width: number; height: number }) => Promise; window: Window | null; }; } } export default function MusicClient({ children: _children }: { children?: React.ReactNode }) { const router = useRouter(); const pathname = usePathname(); const watchRoom = useWatchRoomContextSafe(); const [currentSource, setCurrentSource] = useState('wy'); 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); const [quality, setQuality] = useState('320k'); const [playMode, setPlayMode] = useState<'loop' | 'single' | 'random'>('loop'); const [currentSongIndex, setCurrentSongIndex] = useState(-1); const [showPlayer, setShowPlayer] = useState(false); const [showLyrics, setShowLyrics] = useState(false); const [mobileLyricsView, setMobileLyricsView] = useState<'lyrics' | 'vinyl'>('lyrics'); const [musicProxyEnabled, setMusicProxyEnabled] = useState(() => { if (typeof window === 'undefined') return true; return (window as any).RUNTIME_CONFIG?.MUSIC_PROXY_ENABLED !== false; }); const [lyrics, setLyrics] = useState([]); const [currentLyricIndex, setCurrentLyricIndex] = useState(-1); const [currentSongUrl, setCurrentSongUrl] = useState(''); const [playRecords, setPlayRecords] = useState([]); // 播放记录(只存平台和ID) const [playlist, setPlaylist] = useState([]); // 完整歌曲信息(用于显示) const [showPlaylist, setShowPlaylist] = useState(false); const [playlistIndex, setPlaylistIndex] = useState(-1); // 当前在播放列表中的索引 const [showQualityMenu, setShowQualityMenu] = useState(false); // 音质选择菜单 const [showSleepTimerMenu, setShowSleepTimerMenu] = useState(false); // 睡眠定时菜单 const [sleepTimerEndAt, setSleepTimerEndAt] = useState(null); // 睡眠定时结束时间 const [sleepTimerRemaining, setSleepTimerRemaining] = useState(0); // 睡眠定时剩余秒数 const [customSleepHours, setCustomSleepHours] = useState(0); // 自定义睡眠定时小时 const [customSleepMinutes, setCustomSleepMinutes] = useState(30); // 自定义睡眠定时分钟 const [showSidebarDrawer, setShowSidebarDrawer] = useState(false); // 左侧抽屉菜单 const [showVolumeSlider, setShowVolumeSlider] = useState(false); // 音量滑块显示状态 const [pendingSongToPlay, setPendingSongToPlay] = useState<{ platform: string; id: string } | null>(null); // 待播放的歌曲信息 const [resolvingCount, setResolvingCount] = useState(0); // 当前解析中的歌曲数量 const [showAddToPlaylistModal, setShowAddToPlaylistModal] = useState(false); // 添加到歌单弹窗 const [songToAddToPlaylist, setSongToAddToPlaylist] = useState(null); // 要添加到歌单的歌曲 useEffect(() => { if (typeof window !== 'undefined' && !(window as any).RUNTIME_CONFIG?.MUSIC_ENABLED) { router.replace('/'); } }, [router]); // Toast 和 Confirm Modal 状态 const [toast, setToast] = useState(null); const [confirmModal, setConfirmModal] = useState<{ isOpen: boolean; title: string; message: string; onConfirm: () => void; onCancel: () => void; }>({ isOpen: false, title: '', message: '', onConfirm: () => {}, onCancel: () => {}, }); // PiP 相关状态 const [showPiPLyrics, setShowPiPLyrics] = useState(false); const [pipOpacity, setPipOpacity] = useState(0.9); const [pipMinimized, setPipMinimized] = useState(false); const [showSpectrum, setShowSpectrum] = useState(() => { if (typeof window === 'undefined') return true; return localStorage.getItem('musicShowSpectrum') !== '0'; }); const [spectrumBars, setSpectrumBars] = useState( () => Array.from({ length: SPECTRUM_BIN_COUNT }, () => SPECTRUM_IDLE_LEVEL) ); const audioRef = useRef(null); const lyricsContainerRef = useRef(null); const sleepHoursPickerRef = useRef(null); const sleepMinutesPickerRef = useRef(null); const lastSaveTimeRef = useRef(0); const restoredTimeRef = useRef(0); const songStartTimeRef = useRef(0); // 歌曲开始播放的时间戳 const audioContextRef = useRef(null); const analyserRef = useRef(null); const mediaSourceRef = useRef(null); const spectrumDataRef = useRef(null); const spectrumFrameRef = useRef(null); const currentTimeRef = useRef(0); const volumeRef = useRef(volume); const spectrumSeedRef = useRef(Math.random() * Math.PI * 2); const qualitySwitchRequestRef = useRef(0); const currentSongRef = useRef(null); const currentSourceRef = useRef(currentSource); const plannedNextSongRef = useRef(null); const plannedNextSongKeyRef = useRef(''); const isMusicRoomOwner = Boolean( watchRoom?.isOwner && watchRoom.currentRoom?.roomType === 'music' ); const toMusicQueueItem = (song: Song): MusicQueueItem => ({ id: song.id, name: song.name, artist: song.artist, album: song.album, pic: song.pic, platform: song.platform || currentSourceRef.current, songmid: song.songmid, duration: song.duration, durationText: song.durationText, }); const buildMusicRoomState = ( song: Song, options: { currentTime?: number; isPlaying?: boolean; nextSong?: Song | null; } = {} ): MusicSyncState => { const songPlatform = song.platform || currentSourceRef.current; const currentSong = { ...song, platform: songPlatform }; const playlistSnapshot = playlist .map((item) => ({ ...item, platform: item.platform || songPlatform })) .filter((item) => item.id); const currentKey = getMusicQueueItemKey(currentSong, songPlatform); const playlistKey = playlistSnapshot.map((item) => getMusicQueueItemKey(item)).join('|'); const cacheKey = `${currentKey}|${playMode}|${playlistKey}`; const resolveNextSong = (): Song | null => { if (typeof options.nextSong !== 'undefined') return options.nextSong || null; if (plannedNextSongKeyRef.current === cacheKey) { return plannedNextSongRef.current; } let currentIndex = playlistSnapshot.findIndex((item) => getMusicQueueItemKey(item) === currentKey); if (currentIndex < 0) { playlistSnapshot.push(currentSong); currentIndex = playlistSnapshot.length - 1; } let nextSong: Song | null = null; if (playMode === 'single') { nextSong = playlistSnapshot[currentIndex] || null; } else if (playMode === 'random') { if (playlistSnapshot.length === 1) { nextSong = playlistSnapshot[0] || null; } else { const candidates = playlistSnapshot.filter((_, index) => index !== currentIndex); nextSong = candidates[Math.floor(Math.random() * candidates.length)] || null; } } else { const nextIndex = currentIndex < playlistSnapshot.length - 1 ? currentIndex + 1 : 0; nextSong = playlistSnapshot[nextIndex] || null; } plannedNextSongKeyRef.current = cacheKey; plannedNextSongRef.current = nextSong; return nextSong; }; const nextSong = resolveNextSong(); return { type: 'music', song: toMusicQueueItem(currentSong), nextSong: nextSong ? toMusicQueueItem(nextSong) : null, currentTime: options.currentTime ?? audioRef.current?.currentTime ?? currentTimeRef.current ?? 0, isPlaying: options.isPlaying ?? isPlaying, quality, playMode, updatedAt: Date.now(), }; }; const emitMusicChange = (state: MusicSyncState | null) => { if (!isMusicRoomOwner || !watchRoom || !state) return; watchRoom.changeMusic(state); }; const buildStreamUrl = (song: Song, source: MusicSource, songQuality: MusicQuality) => { const params = new URLSearchParams({ songId: song.id, source, quality: songQuality, songmid: song.songmid || song.id.split('_').slice(1).join('_'), name: song.name, artist: song.artist, }); if (song.durationText) params.set('durationText', song.durationText); return `/api/music/v2/stream?${params.toString()}`; }; const getMusicProxyEnabled = () => { if (typeof window === 'undefined') return true; return (window as any).RUNTIME_CONFIG?.MUSIC_PROXY_ENABLED !== false; }; const fetchPlayData = async ( song: Song, source: MusicSource, songQuality: MusicQuality, includeUrl = false ) => { const response = await fetch('/api/music/v2/play', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ includeUrl, song: { songId: song.id, source, songmid: song.songmid, name: song.name, artist: song.artist, album: song.album, cover: song.pic, durationSec: song.duration, durationText: song.durationText, }, quality: songQuality, }), }); return response.json(); }; const beginResolving = () => { setResolvingCount((prev) => prev + 1); }; const endResolving = () => { setResolvingCount((prev) => Math.max(0, prev - 1)); }; const saveHistoryRecord = async ( record: PlayRecord, song: Song, playTime: number, totalDuration: number, lastPlayedAt = Date.now(), recordQuality: MusicQuality = quality ) => { await fetch('/api/music/v2/history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ song: { songId: record.id, source: record.platform, songmid: song.songmid, name: song.name, artist: song.artist, album: song.album, cover: song.pic, durationSec: totalDuration || song.duration || 0, durationText: song.durationText, }, playProgressSec: playTime, lastPlayedAt, lastQuality: recordQuality, createdAt: record.timestamp, }), }); }; const saveHistoryRecordSafely = ( record: PlayRecord, song: Song, playTime = 0, totalDuration = 0, lastPlayedAt?: number, recordQuality?: MusicQuality ) => { saveHistoryRecord(record, song, playTime, totalDuration, lastPlayedAt, recordQuality).catch(err => { console.error('保存播放记录到数据库失败:', err); }); }; // 保存播放状态到 localStorage const savePlayState = () => { if (!currentSong) return; const playState = { currentSong, currentSongIndex, currentSource, quality, playMode, volume, currentTime: audioRef.current?.currentTime || 0, currentSongUrl, lyrics, playRecords, // 只保存播放记录(平台+ID+播放信息) playlist, // 保存完整歌曲信息(用于显示) playlistIndex, }; localStorage.setItem('musicPlayState', JSON.stringify(playState)); }; // 清空当前播放状态,并在需要时停止正在播放的音频 const clearCurrentPlaybackState = () => { const audio = audioRef.current; if (audio) { audio.pause(); audio.removeAttribute('src'); audio.load(); } setIsPlaying(false); setCurrentSong(null); setCurrentSongIndex(-1); setCurrentSongUrl(''); setCurrentTime(0); setDuration(0); setLyrics([]); setCurrentLyricIndex(-1); setShowPlayer(false); setShowLyrics(false); setShowPiPLyrics(false); setPendingSongToPlay(null); restoredTimeRef.current = 0; lastSaveTimeRef.current = 0; currentTimeRef.current = 0; localStorage.removeItem('musicPlayState'); }; // 从 localStorage 恢复播放状态(已废弃,现在统一使用数据库) const restorePlayState = async () => { // 此函数已不再使用,所有状态恢复都在 initializePlayState 中完成 }; useEffect(() => { setMusicProxyEnabled(getMusicProxyEnabled()); }, []); // 页面加载时恢复播放状态和数据库记录 useEffect(() => { const initializePlayState = async () => { try { const response = await fetch('/api/music/v2/history'); const history = await response.json(); const dbRecords = (history.data?.records || []) as DbRecord[]; const queueRecords = dbRecords; const sortedRecords: PlayRecord[] = queueRecords.map((record) => ({ platform: record.source, id: record.songId, playTime: record.playProgressSec, duration: record.durationSec || 0, timestamp: record.createdAt || record.lastPlayedAt || 0, })); const sortedSongs: Song[] = queueRecords.map((record) => ({ id: record.songId, name: record.name, artist: record.artist, album: record.album, pic: record.cover, platform: record.source, duration: record.durationSec, durationText: record.durationText, songmid: record.songmid, })); // 2. 更新播放列表 if (sortedRecords.length > 0) { setPlayRecords(sortedRecords); setPlaylist(sortedSongs); } // 3. 获取 localStorage 配置(只获取配置,不获取歌曲信息) const savedPlayState = localStorage.getItem('musicPlayState'); const playState = savedPlayState ? JSON.parse(savedPlayState) : {}; // 恢复配置状态(不包括歌曲) setCurrentSource(normalizeSource(playState.currentSource)); setQuality(playState.quality || '320k'); setPlayMode(playState.playMode || 'loop'); setVolume(playState.volume || 100); // 4. 使用数据库的最新记录(歌曲和进度都从数据库获取) if (sortedRecords.length > 0) { const proxyEnabled = getMusicProxyEnabled(); setMusicProxyEnabled(proxyEnabled); const latestIndex = queueRecords.reduce((bestIndex, record, index) => { if (bestIndex < 0) return index; return (record.lastPlayedAt || 0) > (queueRecords[bestIndex].lastPlayedAt || 0) ? index : bestIndex; }, -1); const activeIndex = latestIndex >= 0 ? latestIndex : 0; const latestDbRecord = sortedRecords[activeIndex]; const latestDbSong = sortedSongs[activeIndex]; // 使用数据库的歌曲信息 setCurrentSong(latestDbSong); setPlaylistIndex(activeIndex); setShowPlayer(true); // 从数据库恢复播放进度 const dbPlayTime = latestDbRecord.playTime || 0; songStartTimeRef.current = Date.now(); const platform = latestDbSong.platform || 'kw'; const selectedQuality = playState.quality || '320k'; const restoreTime = () => { if (audioRef.current && dbPlayTime > 0) { audioRef.current.currentTime = dbPlayTime; } }; if (proxyEnabled) { const streamUrl = buildStreamUrl(latestDbSong, platform, selectedQuality); setCurrentSongUrl(streamUrl); if (audioRef.current) { setIsBuffering(true); audioRef.current.src = streamUrl; audioRef.current.addEventListener('loadedmetadata', restoreTime, { once: true }); audioRef.current.load(); } fetchPlayData(latestDbSong, platform, selectedQuality, false) .then((data) => { if (data.success && data.data?.lyric?.lyric) { const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric); setLyrics(parsedLyrics); } }) .catch((error) => { console.error('加载歌词失败:', error); }); } else { 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(); if (data.data.lyric?.lyric) { const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric); setLyrics(parsedLyrics); } } } } } catch (error) { console.error('加载播放记录失败:', error); } }; initializePlayState(); }, []); // 恢复 PiP 偏好设置 useEffect(() => { const savedOpacity = localStorage.getItem('lyricsPiPOpacity'); const savedMinimized = localStorage.getItem('lyricsPiPMinimized'); if (savedOpacity) setPipOpacity(parseFloat(savedOpacity)); if (savedMinimized) setPipMinimized(savedMinimized === 'true'); }, []); // 监听来自 PiP 窗口的消息 useEffect(() => { const handleMessage = (event: MessageEvent) => { switch (event.data.type) { case 'PIP_OPACITY_CHANGE': setPipOpacity(event.data.opacity); localStorage.setItem('lyricsPiPOpacity', event.data.opacity.toString()); break; case 'PIP_MINIMIZED_CHANGE': setPipMinimized(event.data.minimized); localStorage.setItem('lyricsPiPMinimized', event.data.minimized.toString()); break; case 'PIP_CLOSE': setShowPiPLyrics(false); break; } }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, []); // 监听播放状态变化,自动保存 useEffect(() => { if (currentSong) { savePlayState(); } }, [currentSong, currentSongIndex, currentSource, quality, playMode, volume, currentSongUrl, lyrics, playRecords, playlistIndex]); useEffect(() => { currentSongRef.current = currentSong; }, [currentSong]); useEffect(() => { currentSourceRef.current = currentSource; }, [currentSource]); useEffect(() => { if (!isMusicRoomOwner || !watchRoom || !currentSong) return; watchRoom.updateMusicState(buildMusicRoomState(currentSong)); }, [isMusicRoomOwner, watchRoom, currentSong, playlist, playlistIndex, playMode, quality]); useEffect(() => { if (!isMusicRoomOwner || !watchRoom || !currentSong || !isPlaying) return; const interval = window.setInterval(() => { watchRoom.updateMusicState(buildMusicRoomState(currentSong, { currentTime: audioRef.current?.currentTime || currentTimeRef.current || 0, isPlaying: true, })); }, 5000); return () => window.clearInterval(interval); }, [isMusicRoomOwner, watchRoom, currentSong, isPlaying, playlist, playlistIndex, playMode, quality]); // 监听 playRecords 变化,更新 playlistIndex useEffect(() => { if (pendingSongToPlay) { const index = playRecords.findIndex( r => r.platform === pendingSongToPlay.platform && r.id === pendingSongToPlay.id ); setPlaylistIndex(index); setPendingSongToPlay(null); } }, [playRecords, pendingSongToPlay]); // 同步音量状态到 audio 元素 useEffect(() => { volumeRef.current = volume; if (audioRef.current) { audioRef.current.volume = volume / 100; } }, [volume]); const handlePlayAllCurrentSongsWith = async (targetSongs: Song[], title: string) => { try { if (targetSongs.length === 0) { setToast({ message: '当前列表为空', type: 'error', onClose: () => setToast(null) }); return; } await fetch('/api/music/v2/history', { method: 'DELETE' }); const baseTime = Date.now(); const recordsToAdd = targetSongs.map((song, i) => ({ song: { songId: song.id, source: song.platform, songmid: song.songmid, name: song.name, artist: song.artist, album: song.album, cover: song.pic, durationSec: song.duration || 0, durationText: song.durationText, }, playProgressSec: 0, lastPlayedAt: baseTime + i, playCount: 1, lastQuality: quality, createdAt: baseTime + i, })); await fetch('/api/music/v2/history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ records: recordsToAdd }), }); const newRecords: PlayRecord[] = targetSongs.map((song, i) => ({ platform: song.platform, id: song.id, playTime: 0, duration: song.duration || 0, timestamp: baseTime + i, })); setPlayRecords(newRecords); setPlaylist(targetSongs); setPlaylistIndex(0); await playSong(targetSongs[0], 0); setToast({ message: `已开始播放 ${title}`, type: 'success', onClose: () => setToast(null) }); } catch (error) { console.error('播放全部失败:', error); setToast({ message: '播放全部失败', type: 'error', onClose: () => setToast(null) }); } }; const addSongToQueue = async (song: Song) => { if (!currentSong && playlist.length === 0 && playRecords.length === 0) { await playSong(song, -1); return; } const platform = song.platform || currentSource; const exists = playlist.some((item) => item.id === song.id && item.platform === platform); if (exists) { setToast({ message: '歌曲已在播放列表中', type: 'info', onClose: () => setToast(null) }); return; } const record: PlayRecord = { platform, id: song.id, playTime: 0, duration: song.duration || 0, timestamp: Date.now() }; setPlayRecords((prev) => [...prev, record]); setPlaylist((prev) => [...prev, { ...song, platform }]); saveHistoryRecordSafely(record, { ...song, platform }, 0, song.duration || 0); setToast({ message: '已添加到稍后播放', type: 'success', onClose: () => setToast(null) }); }; // 播放歌曲 const playSong = async (song: Song, index: number) => { beginResolving(); try { // 使用歌曲自己的平台信息,如果没有则使用当前选择的平台 const platform = song.platform || currentSource; const proxyEnabled = getMusicProxyEnabled(); setMusicProxyEnabled(proxyEnabled); const syncSong = { ...song, platform }; // 记录歌曲开始播放的时间 songStartTimeRef.current = Date.now(); // 先设置当前歌曲和显示播放器 setCurrentSong(song); setCurrentSongIndex(index); setShowPlayer(true); setLyrics([]); // 清空旧歌词 // 添加到播放记录和播放列表。timestamp 表示入队时间,不能在再次播放时刷新, // 否则会破坏按 createdAt/timestamp 维护的播放队列顺序。 const existingRecord = playRecords.find(r => r.platform === platform && r.id === song.id); const record: PlayRecord = existingRecord || { platform: platform, id: song.id, playTime: 0, // 初始播放时间 duration: song.duration || 0, // 将在音频加载后更新 timestamp: Date.now(), }; // 设置待播放歌曲信息,用于在 playRecords 更新后找到索引 setPendingSongToPlay({ platform, id: song.id }); setPlayRecords(prev => { const existingIndex = prev.findIndex(r => r.platform === record.platform && r.id === record.id); if (existingIndex >= 0) { // 记录已存在:保持原位置和原 timestamp,只补齐可能变化的时长信息。 const updated = [...prev]; updated[existingIndex] = { ...updated[existingIndex], duration: updated[existingIndex].duration || song.duration || 0, }; return updated; } else { // 新记录,添加到列表末尾 return [...prev, record]; } }); setPlaylist(prev => { const existingIndex = prev.findIndex(s => s.id === song.id && s.platform === platform); if (existingIndex >= 0) { return prev; } else { return [...prev, { ...song, platform }]; } }); saveHistoryRecordSafely(record, { ...song, platform }, 0, song.duration || 0); emitMusicChange(buildMusicRoomState(syncSong, { currentTime: 0, isPlaying: true, })); if (proxyEnabled) { const streamUrl = buildStreamUrl(song, platform, quality); 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); } fetchPlayData(song, platform, quality, false) .then((data) => { if (data.success) { if (data.data.song?.cover) { setCurrentSong({ ...song, pic: data.data.song.cover, platform, }); } if (data.data.lyric?.lyric) { const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric); setLyrics(parsedLyrics); } } else { console.error('播放信息获取失败:', data); } }) .catch((error) => { console.error('加载歌词失败:', error); }); } else { const data = await fetchPlayData(song, platform, quality, true); if (data.success && data.data?.play?.directUrl) { if (data.data.song?.cover) { setCurrentSong({ ...song, pic: data.data.song.cover, platform, }); } if (data.data.lyric?.lyric) { const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric); setLyrics(parsedLyrics); } 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); } } else { console.error('播放信息获取失败:', data); } } } catch (error) { console.error('播放失败:', error); setIsBuffering(false); } finally { endResolving(); } }; // 解析歌词文本 const parseLyric = (lyricText: string, tlyricText?: string): LyricLine[] => { if (!lyricText && !tlyricText) return []; // 匹配 [mm:ss.xx] 或 [mm:ss] 格式 const timeRegex = /\[(\d{2}):(\d{2})(?:\.(\d{2,3}))?\]/g; const parseLyricText = (text: string) => { const parsed = new Map(); const lines = text.split('\n'); lines.forEach(line => { const matches = Array.from(line.matchAll(timeRegex)); if (matches.length > 0) { const content = line.replace(timeRegex, '').trim(); if (content) { matches.forEach(match => { const minutes = parseInt(match[1]); const seconds = parseInt(match[2]); const milliseconds = match[3] ? parseInt(match[3].padEnd(3, '0')) : 0; const time = minutes * 60 + seconds + milliseconds / 1000; parsed.set(time, content); }); } } }); return parsed; }; const mainMap = parseLyricText(lyricText || ''); const transMap = parseLyricText(tlyricText || ''); const times = Array.from(new Set([ ...Array.from(mainMap.keys()), ...Array.from(transMap.keys()), ])).sort((a, b) => a - b); return times .map(time => ({ time, text: mainMap.get(time) || '', translation: transMap.get(time) || undefined, })) .filter(line => line.text || line.translation); }; // 切换播放/暂停 const togglePlay = () => { if (audioRef.current) { if (isPlaying) { audioRef.current.pause(); setIsPlaying(false); if (isMusicRoomOwner) { if (currentSong) { watchRoom?.pauseMusic(buildMusicRoomState(currentSong, { currentTime: audioRef.current.currentTime || currentTimeRef.current || 0, isPlaying: false, })); } } // 暂停时保存状态到 localStorage 和数据库 savePlayState(); // 前5秒不保存(避免加载时的跳转触发保存) if (Date.now() - songStartTimeRef.current < 5000) { return; } // 保存到数据库 if (currentSong && playlistIndex >= 0 && playRecords[playlistIndex]) { const record = playRecords[playlistIndex]; saveHistoryRecord(record, currentSong, audioRef.current.currentTime, audioRef.current.duration || 0).catch(err => { console.error('暂停时保存播放记录失败:', err); }); } } else { setIsBuffering(true); audioRef.current.play().catch(err => { console.error('播放失败:', err); setIsBuffering(false); }); setIsPlaying(true); if (isMusicRoomOwner) { if (currentSong) { watchRoom?.playMusic(buildMusicRoomState(currentSong, { currentTime: audioRef.current.currentTime || currentTimeRef.current || 0, isPlaying: true, })); } } } } }; // 上一曲 const playPrev = () => { if (playlist.length > 0) { const prevIndex = playlistIndex > 0 ? playlistIndex - 1 : playlist.length - 1; setPlaylistIndex(prevIndex); playSong(playlist[prevIndex], -1); } }; // 下一曲 const playNext = () => { if (playlist.length > 0) { const nextIndex = playlistIndex < playlist.length - 1 ? playlistIndex + 1 : 0; setPlaylistIndex(nextIndex); playSong(playlist[nextIndex], -1); } }; // 切换音质 const handleQualityChange = async (nextQuality: MusicQuality) => { setShowQualityMenu(false); if (nextQuality === quality) return; const targetSong = currentSong; const audio = audioRef.current; const targetPlatform = targetSong?.platform || currentSource; currentSongRef.current = targetSong; currentSourceRef.current = currentSource; setQuality(nextQuality); // 没有正在播放的歌曲时,仅保存偏好;下次播放会使用新音质。 if (!targetSong || !audio) return; const requestId = ++qualitySwitchRequestRef.current; const targetSongKey = `${targetPlatform}:${targetSong.id}`; const resumeTime = Number.isFinite(audio.currentTime) ? audio.currentTime : currentTimeRef.current; const shouldResume = isPlaying || (!audio.paused && !audio.ended); const isStillTargetSong = () => { const activeSong = currentSongRef.current; if (!activeSong) return false; const activePlatform = activeSong.platform || currentSourceRef.current; return ( requestId === qualitySwitchRequestRef.current && `${activePlatform}:${activeSong.id}` === targetSongKey ); }; beginResolving(); try { const proxyEnabled = getMusicProxyEnabled(); setMusicProxyEnabled(proxyEnabled); let nextSongUrl = ''; if (proxyEnabled) { nextSongUrl = buildStreamUrl(targetSong, targetPlatform, nextQuality); } else { const data = await fetchPlayData(targetSong, targetPlatform, nextQuality, true); if (!isStillTargetSong()) return; if (!data.success || !data.data?.play?.directUrl) { throw new Error(data.error?.message || '获取播放地址失败'); } nextSongUrl = data.data.play.directUrl; if (data.data.song?.cover) { setCurrentSong({ ...targetSong, pic: data.data.song.cover, platform: targetPlatform, }); } if (data.data.lyric?.lyric) { const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric); setLyrics(parsedLyrics); } } if (!isStillTargetSong()) return; const activeRecord = playRecords[playlistIndex]?.platform === targetPlatform && playRecords[playlistIndex]?.id === targetSong.id ? playRecords[playlistIndex] : playRecords.find((record) => record.platform === targetPlatform && record.id === targetSong.id); const totalDuration = Number.isFinite(audio.duration) && audio.duration > 0 ? audio.duration : duration || targetSong.duration || 0; if (activeRecord) { saveHistoryRecordSafely( activeRecord, { ...targetSong, platform: targetPlatform }, resumeTime, totalDuration, Date.now(), nextQuality ); } setCurrentSongUrl(nextSongUrl); setCurrentTime(resumeTime); songStartTimeRef.current = Date.now(); restoredTimeRef.current = resumeTime; const resumeAfterMetadata = () => { if (!isStillTargetSong()) return; if (resumeTime > 0) { try { const maxSeekTime = Number.isFinite(audio.duration) && audio.duration > 0 ? Math.max(0, audio.duration - 0.25) : resumeTime; const seekTime = Math.min(resumeTime, maxSeekTime); if (Math.abs(audio.currentTime - seekTime) > 1) { audio.currentTime = seekTime; } } catch (error) { console.warn('切换音质后恢复播放进度失败:', error); } } setCurrentTime(audio.currentTime || resumeTime); if (shouldResume) { audio.play() .then(() => setIsPlaying(true)) .catch((error) => { console.error('切换音质后播放失败:', error); setIsPlaying(false); setIsBuffering(false); }); } else { setIsPlaying(false); } }; 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', onClose: () => setToast(null), }); } finally { endResolving(); } }; const cycleQuality = () => { const qualities: MusicQuality[] = ['128k', '320k', 'flac', 'flac24bit']; const currentIndex = qualities.indexOf(quality); const nextIndex = (currentIndex + 1) % qualities.length; void handleQualityChange(qualities[nextIndex]); }; // 清空播放记录 const handleClearPlayRecords = () => { setConfirmModal({ isOpen: true, title: '确认清空', message: '确定要清空全部播放记录吗?', onConfirm: async () => { try { await fetch('/api/music/v2/history', { method: 'DELETE' }); clearCurrentPlaybackState(); setPlaylist([]); setPlayRecords([]); setPlaylistIndex(-1); setToast({ message: '播放记录已清空', type: 'success', onClose: () => setToast(null), }); } catch (error) { console.error('清空播放记录失败:', error); setToast({ message: '清空播放记录失败', type: 'error', onClose: () => setToast(null), }); } finally { setConfirmModal({ isOpen: false, title: '', message: '', onConfirm: () => {}, onCancel: () => {}, }); } }, onCancel: () => { setConfirmModal({ isOpen: false, title: '', message: '', onConfirm: () => {}, onCancel: () => {}, }); }, }); }; // 切换播放模式 const toggleMode = () => { const modes: Array<'loop' | 'single' | 'random'> = ['loop', 'single', 'random']; const currentIndex = modes.indexOf(playMode); const nextIndex = (currentIndex + 1) % modes.length; setPlayMode(modes[nextIndex]); }; // 下载歌曲 const downloadSong = () => { if (!currentSongUrl || !currentSong) return; // 创建一个临时的 a 标签来触发下载 const link = document.createElement('a'); link.href = currentSongUrl; link.download = `${currentSong.name} - ${currentSong.artist}.mp3`; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; // 音频事件监听 useEffect(() => { const audio = audioRef.current; if (!audio) return; const handleTimeUpdate = () => { setCurrentTime(audio.currentTime); // 更新当前歌词索引 if (lyrics.length > 0) { let index = -1; for (let i = 0; i < lyrics.length; i++) { if (lyrics[i].time <= audio.currentTime) { index = i; } else { break; } } setCurrentLyricIndex(index); } // 每20秒保存一次播放进度和播放时间 const now = Date.now(); if (now - lastSaveTimeRef.current > 20000) { lastSaveTimeRef.current = now; // 前5秒不保存(避免加载时的跳转触发保存) if (Date.now() - songStartTimeRef.current < 5000) { return; } // 更新当前播放记录的播放时间 if (currentSong && playlistIndex >= 0) { setPlayRecords(prev => { const updated = [...prev]; if (updated[playlistIndex]) { updated[playlistIndex] = { ...updated[playlistIndex], playTime: audio.currentTime, }; // 保存到数据库 const record = updated[playlistIndex]; saveHistoryRecord(record, currentSong, audio.currentTime, audio.duration || 0).catch(err => { console.error('保存播放记录到数据库失败:', err); }); } return updated; }); } savePlayState(); } }; const handleLoadedMetadata = () => { // 恢复播放进度 if (restoredTimeRef.current > 0) { audio.currentTime = restoredTimeRef.current; restoredTimeRef.current = 0; // 清除标记 } }; const handleBufferingStart = () => { if (audio.src && !audio.ended) { setIsBuffering(true); } }; const handleBufferingEnd = () => { setIsBuffering(false); }; const handleDurationChange = () => { setDuration(audio.duration); // 前5秒不保存(避免加载时的跳转触发保存) if (Date.now() - songStartTimeRef.current < 5000) { return; } // 更新当前播放记录的总时长 if (currentSong && playlistIndex >= 0) { setPlayRecords(prev => { const updated = [...prev]; if (updated[playlistIndex]) { updated[playlistIndex] = { ...updated[playlistIndex], duration: audio.duration, }; // 保存到数据库(包含时长信息) const record = updated[playlistIndex]; saveHistoryRecord(record, currentSong, record.playTime, audio.duration).catch(err => { console.error('保存播放记录到数据库失败:', err); }); } return updated; }); } }; const handleEnded = () => { setIsBuffering(false); if (playMode === 'single') { audio.currentTime = 0; audio.play(); } else if (playMode === 'random') { if (playlist.length > 0) { const randomIndex = Math.floor(Math.random() * playlist.length); setPlaylistIndex(randomIndex); playSong(playlist[randomIndex], -1); } } else { playNext(); } }; 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); }; }, [playMode, currentSongIndex, lyrics, currentSong, playlistIndex, playRecords, quality]); // 歌词自动滚动 useEffect(() => { if (lyricsContainerRef.current && currentLyricIndex >= 0) { const container = lyricsContainerRef.current; const activeLine = container.querySelector(`[data-index="${currentLyricIndex}"]`); if (activeLine) { activeLine.scrollIntoView({ behavior: 'smooth', block: 'center', }); } } }, [currentLyricIndex]); // 进度条拖动 const handleProgressChange = (e: React.ChangeEvent) => { const newTime = (parseFloat(e.target.value) / 100) * duration; setCurrentTime(newTime); if (audioRef.current) { audioRef.current.currentTime = newTime; } if (isMusicRoomOwner) { const syncSong = currentSongRef.current || currentSong; if (syncSong) { watchRoom?.seekMusic(buildMusicRoomState(syncSong, { currentTime: newTime, isPlaying, })); } } }; const seekToLyric = (line: LyricLine, index: number) => { const audio = audioRef.current; if (!audio || !Number.isFinite(line.time)) return; const maxSeekTime = Number.isFinite(audio.duration) && audio.duration > 0 ? Math.max(0, audio.duration - 0.25) : line.time; const nextTime = Math.max(0, Math.min(line.time, maxSeekTime)); audio.currentTime = nextTime; setCurrentTime(nextTime); setCurrentLyricIndex(index); if (isMusicRoomOwner) { const syncSong = currentSongRef.current || currentSong; if (syncSong) { watchRoom?.seekMusic(buildMusicRoomState(syncSong, { currentTime: nextTime, isPlaying, })); } } }; // 音量调节 const handleVolumeChange = (e: React.ChangeEvent) => { const newVolume = parseInt(e.target.value); setVolume(newVolume); if (audioRef.current) { audioRef.current.volume = newVolume / 100; } }; // 触摸/鼠标滑动音量调节(移动端兼容) const handleVolumeSliderInteraction = (e: React.MouseEvent | React.TouchEvent) => { e.preventDefault(); e.stopPropagation(); const slider = e.currentTarget; const rect = slider.getBoundingClientRect(); const updateVolume = (clientY: number) => { // 计算相对于滑块顶部的位置 const y = clientY - rect.top; // 限制在滑块范围内 const clampedY = Math.max(0, Math.min(rect.height, y)); // 从上到下:0% -> 100%,从下到上:100% -> 0% const percentage = 100 - (clampedY / rect.height) * 100; const newVolume = Math.round(percentage); setVolume(newVolume); if (audioRef.current) { audioRef.current.volume = newVolume / 100; } }; // 获取初始触摸/点击位置 const clientY = 'touches' in e ? e.touches[0]?.clientY || 0 : e.clientY; updateVolume(clientY); const handleMove = (moveEvent: MouseEvent | TouchEvent) => { moveEvent.preventDefault(); const moveClientY = 'touches' in moveEvent ? moveEvent.touches[0]?.clientY || 0 : moveEvent.clientY; updateVolume(moveClientY); }; const handleEnd = () => { document.removeEventListener('mousemove', handleMove); document.removeEventListener('mouseup', handleEnd); document.removeEventListener('touchmove', handleMove); document.removeEventListener('touchend', handleEnd); }; document.addEventListener('mousemove', handleMove); document.addEventListener('mouseup', handleEnd); document.addEventListener('touchmove', handleMove, { passive: false }); document.addEventListener('touchend', handleEnd); }; // PiP 窗口管理 const togglePiPLyrics = () => { if (!('documentPictureInPicture' in window)) { setToast({ message: '您的浏览器不支持画中画功能,请使用 Chrome 116+ 版本', type: 'error', }); // 降级方案:打开全屏歌词 setShowLyrics(true); return; } if (!currentSong) { setToast({ message: '请先播放歌曲', type: 'info', }); return; } setShowPiPLyrics(!showPiPLyrics); }; const progress = duration > 0 ? (currentTime / duration) * 100 : 0; const showStreamBuffering = Boolean(currentSong && isBuffering); const toggleSpectrum = () => { setShowSpectrum(prev => !prev); }; useEffect(() => { if (typeof window === 'undefined') return; localStorage.setItem('musicShowSpectrum', showSpectrum ? '1' : '0'); }, [showSpectrum]); const getQualityLabel = () => { switch (quality) { case '128k': return '标准'; case '320k': return 'HQ'; case 'flac': return 'SQ'; case 'flac24bit': return 'HR'; } }; const getSourceLabel = () => { return getSourceDisplayLabel(currentSource, false); }; const formatTime = (seconds: number) => { if (isNaN(seconds) || seconds === 0) return '0:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }; const sleepPickerItemHeight = 40; const sleepPickerVisibleCount = 5; const sleepPickerHeight = sleepPickerItemHeight * sleepPickerVisibleCount; const clampSleepPickerValue = (value: number, max: number) => Math.max(0, Math.min(max, value)); const formatSleepTimer = (seconds: number) => { if (!Number.isFinite(seconds) || seconds <= 0) return '已关闭'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); if (mins >= 60) { const hours = Math.floor(mins / 60); const restMins = mins % 60; return `${hours}:${restMins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } return `${mins}:${secs.toString().padStart(2, '0')}`; }; const setSleepTimer = (minutes: number) => { const endAt = Date.now() + minutes * 60 * 1000; setSleepTimerEndAt(endAt); setSleepTimerRemaining(minutes * 60); setShowSleepTimerMenu(false); setToast({ message: `已设置 ${minutes} 分钟后暂停播放`, type: 'success', onClose: () => setToast(null), }); }; const setCustomSleepTimer = () => { const totalMinutes = customSleepHours * 60 + customSleepMinutes; if (totalMinutes <= 0) { setToast({ message: '请选择大于 0 的定时时长', type: 'info', onClose: () => setToast(null), }); return; } setSleepTimer(totalMinutes); }; const cancelSleepTimer = () => { setSleepTimerEndAt(null); setSleepTimerRemaining(0); setShowSleepTimerMenu(false); setToast({ message: '已关闭睡眠定时', type: 'info', onClose: () => setToast(null), }); }; const handleSleepHourScroll = (e: React.UIEvent) => { const nextValue = clampSleepPickerValue(Math.round(e.currentTarget.scrollTop / sleepPickerItemHeight), 12); if (nextValue !== customSleepHours) setCustomSleepHours(nextValue); }; const handleSleepMinuteScroll = (e: React.UIEvent) => { const nextValue = clampSleepPickerValue(Math.round(e.currentTarget.scrollTop / sleepPickerItemHeight), 59); if (nextValue !== customSleepMinutes) setCustomSleepMinutes(nextValue); }; useEffect(() => { if (!sleepTimerEndAt) return; const updateSleepTimer = () => { const remaining = Math.max(0, Math.ceil((sleepTimerEndAt - Date.now()) / 1000)); setSleepTimerRemaining(remaining); if (remaining > 0) return; setSleepTimerEndAt(null); setShowSleepTimerMenu(false); if (audioRef.current && !audioRef.current.paused) { audioRef.current.pause(); setIsPlaying(false); savePlayState(); } setToast({ message: '睡眠定时结束,已暂停播放', type: 'info', onClose: () => setToast(null), }); }; updateSleepTimer(); const timerId = window.setInterval(updateSleepTimer, 1000); return () => window.clearInterval(timerId); }, [sleepTimerEndAt]); useEffect(() => { if (!showSleepTimerMenu) return; const scrollToSelected = (el: HTMLDivElement | null, value: number) => { if (!el) return; window.requestAnimationFrame(() => { el.scrollTop = value * sleepPickerItemHeight; }); }; // 只在弹窗打开时定位一次。不要把 customSleepHours/customSleepMinutes // 放进依赖,否则滚动触发 setState 后又反向改 scrollTop,会造成频闪。 scrollToSelected(sleepHoursPickerRef.current, customSleepHours); scrollToSelected(sleepMinutesPickerRef.current, customSleepMinutes); // eslint-disable-next-line react-hooks/exhaustive-deps }, [showSleepTimerMenu]); useEffect(() => { currentTimeRef.current = currentTime; }, [currentTime]); useEffect(() => { const audio = audioRef.current; if (!audio || typeof window === 'undefined') return; let cancelled = false; const ensureAnalyser = async () => { try { const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext; }).webkitAudioContext; if (!AudioContextClass) return; if (!audioContextRef.current) { audioContextRef.current = new AudioContextClass(); } if (!mediaSourceRef.current) { mediaSourceRef.current = audioContextRef.current.createMediaElementSource(audio); } if (!analyserRef.current) { const analyser = audioContextRef.current.createAnalyser(); analyser.fftSize = 512; analyser.smoothingTimeConstant = 0.8; mediaSourceRef.current.connect(analyser); analyser.connect(audioContextRef.current.destination); analyserRef.current = analyser; spectrumDataRef.current = new Uint8Array(analyser.frequencyBinCount); } if (audioContextRef.current.state === 'suspended') { await audioContextRef.current.resume(); } } catch (error) { console.warn('初始化频谱分析器失败,将使用模拟动画:', error); } }; const tick = () => { if (cancelled) return; const analyser = analyserRef.current; const data = spectrumDataRef.current; const isActive = !audio.paused && !audio.ended; let nextBars = Array.from({ length: SPECTRUM_BIN_COUNT }, () => SPECTRUM_IDLE_LEVEL); if (isActive && analyser && data) { analyser.getByteFrequencyData(data); const usableBins = Math.max(1, Math.floor(data.length * 0.88)); const visualVolume = Math.max(SPECTRUM_MIN_VOLUME, volumeRef.current || SPECTRUM_REFERENCE_VOLUME); const visualVolumeScale = visualVolume > SPECTRUM_MAX_REFERENCE_VOLUME ? Math.sqrt(SPECTRUM_MAX_REFERENCE_VOLUME / visualVolume) : SPECTRUM_REFERENCE_VOLUME / visualVolume; nextBars = Array.from({ length: SPECTRUM_BIN_COUNT }, (_, index) => { const start = Math.floor((index / SPECTRUM_BIN_COUNT) * usableBins); const end = Math.max(start + 1, Math.floor(((index + 1) / SPECTRUM_BIN_COUNT) * usableBins)); let total = 0; for (let i = start; i < end; i++) { total += data[i] ?? 0; } const average = (total / Math.max(1, end - start)) * visualVolumeScale; const rightBias = index / Math.max(1, SPECTRUM_BIN_COUNT - 1); const highFreqCompensation = 1 + rightBias * 0.85; const floorLift = rightBias * 0.035; return Math.max( SPECTRUM_IDLE_LEVEL, Math.min(1, (average / 255) * highFreqCompensation + floorLift) ); }); } else if (isActive) { nextBars = Array.from({ length: SPECTRUM_BIN_COUNT }, (_, index) => { const wave = Math.sin(currentTimeRef.current * 5.2 + index * 0.28 + spectrumSeedRef.current) * 0.12 + Math.sin(currentTimeRef.current * 2.6 + index * 0.16) * 0.08 + 0.22; return Math.max(SPECTRUM_IDLE_LEVEL, Math.min(0.65, wave)); }); } setSpectrumBars(prev => nextBars.map((value, index) => { const previous = prev[index] ?? SPECTRUM_IDLE_LEVEL; return previous + (value - previous) * (isActive ? 0.34 : 0.12); }) ); spectrumFrameRef.current = window.requestAnimationFrame(tick); }; void ensureAnalyser(); spectrumFrameRef.current = window.requestAnimationFrame(tick); return () => { cancelled = true; if (spectrumFrameRef.current) { window.cancelAnimationFrame(spectrumFrameRef.current); spectrumFrameRef.current = null; } }; }, [currentSong]); useEffect(() => { return () => { if (typeof window !== 'undefined' && spectrumFrameRef.current) { window.cancelAnimationFrame(spectrumFrameRef.current); } analyserRef.current?.disconnect(); mediaSourceRef.current?.disconnect(); audioContextRef.current?.close().catch(() => undefined); }; }, []); useEffect(() => { const handlePlaySongEvent = (event: Event) => { const detail = (event as CustomEvent<{ song: Song; index?: number }>).detail; if (detail?.song) void playSong(detail.song, detail.index ?? -1); }; const handlePlayAllEvent = (event: Event) => { const detail = (event as CustomEvent<{ songs: Song[]; title?: string }>).detail; if (!detail?.songs?.length) return; void handlePlayAllCurrentSongsWith(detail.songs, detail.title || '当前列表'); }; const handleAddToPlaylistEvent = (event: Event) => { const detail = (event as CustomEvent<{ song: Song }>).detail; if (detail?.song) { setSongToAddToPlaylist(detail.song); setShowAddToPlaylistModal(true); } }; const handlePlayLaterEvent = (event: Event) => { const detail = (event as CustomEvent<{ song: Song }>).detail; if (!detail?.song) return; void addSongToQueue(detail.song); }; window.addEventListener('music:play-song', handlePlaySongEvent); window.addEventListener('music:play-all', handlePlayAllEvent); window.addEventListener('music:add-to-playlist', handleAddToPlaylistEvent); window.addEventListener('music:play-later', handlePlayLaterEvent); return () => { window.removeEventListener('music:play-song', handlePlaySongEvent); window.removeEventListener('music:play-all', handlePlayAllEvent); window.removeEventListener('music:add-to-playlist', handleAddToPlaylistEvent); window.removeEventListener('music:play-later', handlePlayLaterEvent); }; }, [playlist, playRecords, currentSong, quality, currentSource]); return (
<> {resolvingCount > 0 && (
解析中
{resolvingCount}
)} {/* Header */}
音乐
{/* Main Content */}
{_children}
{/* Player */} {showPlayer && currentSong && (
{showSpectrum && (
)}
{/* Progress Bar */}
{showStreamBuffering && (
)}
{/* Song Info */}
setShowLyrics(true)} > {currentSong.pic ? ( {currentSong.name} { // 图片加载失败时显示默认图标 e.currentTarget.style.display = 'none'; }} /> ) : ( )}
{currentSong.name}
{currentSong.artist}
{/* Controls */}
{showStreamBuffering && ( 缓冲中 )}
{/* Right Controls */}
)} {/* Audio Element */}