refactor: 使用新的音频挂载逻辑
This commit is contained in:
		
							parent
							
								
									35f7332bff
								
							
						
					
					
						commit
						c2ffb57085
					
				
							
								
								
									
										22
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -10,7 +10,6 @@
 | 
			
		|||
			"dependencies": {
 | 
			
		||||
				"@tailwindcss/vite": "^4.1.7",
 | 
			
		||||
				"axios": "^1.9.0",
 | 
			
		||||
				"debug": "^4.4.1",
 | 
			
		||||
				"gsap": "^3.13.0",
 | 
			
		||||
				"pinia": "^3.0.2",
 | 
			
		||||
				"tailwindcss": "^4.1.7",
 | 
			
		||||
| 
						 | 
				
			
			@ -22,9 +21,11 @@
 | 
			
		|||
			"devDependencies": {
 | 
			
		||||
				"@biomejs/biome": "1.9.4",
 | 
			
		||||
				"@types/chrome": "^0.0.323",
 | 
			
		||||
				"@types/debug": "^4.1.12",
 | 
			
		||||
				"@types/node": "^22.15.21",
 | 
			
		||||
				"@types/webextension-polyfill": "^0.12.3",
 | 
			
		||||
				"@vitejs/plugin-vue": "^5.2.1",
 | 
			
		||||
				"debug": "^4.4.1",
 | 
			
		||||
				"typescript": "~5.6.2",
 | 
			
		||||
				"vite": "^6.0.1",
 | 
			
		||||
				"vue-tsc": "^2.1.10"
 | 
			
		||||
| 
						 | 
				
			
			@ -1246,6 +1247,16 @@
 | 
			
		|||
				"@types/har-format": "*"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@types/debug": {
 | 
			
		||||
			"version": "4.1.12",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
 | 
			
		||||
			"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"@types/ms": "*"
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@types/estree": {
 | 
			
		||||
			"version": "1.0.7",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -1276,6 +1287,13 @@
 | 
			
		|||
			"dev": true,
 | 
			
		||||
			"license": "MIT"
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@types/ms": {
 | 
			
		||||
			"version": "2.1.0",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
 | 
			
		||||
			"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT"
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@types/node": {
 | 
			
		||||
			"version": "22.15.21",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -1618,6 +1636,7 @@
 | 
			
		|||
			"version": "4.4.1",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
 | 
			
		||||
			"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT",
 | 
			
		||||
			"dependencies": {
 | 
			
		||||
				"ms": "^2.1.3"
 | 
			
		||||
| 
						 | 
				
			
			@ -2317,6 +2336,7 @@
 | 
			
		|||
			"version": "2.1.3",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
 | 
			
		||||
			"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
 | 
			
		||||
			"dev": true,
 | 
			
		||||
			"license": "MIT"
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/muggle-string": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,6 @@
 | 
			
		|||
	"dependencies": {
 | 
			
		||||
		"@tailwindcss/vite": "^4.1.7",
 | 
			
		||||
		"axios": "^1.9.0",
 | 
			
		||||
		"debug": "^4.4.1",
 | 
			
		||||
		"gsap": "^3.13.0",
 | 
			
		||||
		"pinia": "^3.0.2",
 | 
			
		||||
		"tailwindcss": "^4.1.7",
 | 
			
		||||
| 
						 | 
				
			
			@ -34,9 +33,11 @@
 | 
			
		|||
	"devDependencies": {
 | 
			
		||||
		"@biomejs/biome": "1.9.4",
 | 
			
		||||
		"@types/chrome": "^0.0.323",
 | 
			
		||||
		"@types/debug": "^4.1.12",
 | 
			
		||||
		"@types/node": "^22.15.21",
 | 
			
		||||
		"@types/webextension-polyfill": "^0.12.3",
 | 
			
		||||
		"@vitejs/plugin-vue": "^5.2.1",
 | 
			
		||||
		"debug": "^4.4.1",
 | 
			
		||||
		"typescript": "~5.6.2",
 | 
			
		||||
		"vite": "^6.0.1",
 | 
			
		||||
		"vue-tsc": "^2.1.10"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
import { useRoute, useRouter } from 'vue-router'
 | 
			
		||||
import MiniPlayer from './components/MiniPlayer.vue'
 | 
			
		||||
import PreferencePanel from './components/PreferencePanel.vue'
 | 
			
		||||
import Player from './components/Player.vue'
 | 
			
		||||
import { ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
import LeftArrowIcon from './assets/icons/leftarrow.vue'
 | 
			
		||||
| 
						 | 
				
			
			@ -77,6 +78,7 @@ watch(
 | 
			
		|||
						</button>
 | 
			
		||||
 | 
			
		||||
						<MiniPlayer />
 | 
			
		||||
						<Player />
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -140,7 +140,15 @@ watch(
 | 
			
		|||
const playQueue = usePlayQueueStore()
 | 
			
		||||
 | 
			
		||||
async function playTheAlbum(from: number = 0) {
 | 
			
		||||
	await playQueue.replaceQueue(album.value?.songs ?? [])
 | 
			
		||||
	let newQueue = []
 | 
			
		||||
	for (const track of album.value?.songs ?? []) {
 | 
			
		||||
		newQueue.push({
 | 
			
		||||
			song: track,
 | 
			
		||||
			album: album.value
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	await playQueue.replaceQueue(newQueue)
 | 
			
		||||
	await playQueue.togglePlay(true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function shuffle() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,543 +1,54 @@
 | 
			
		|||
<!-- Player.vue - 添加预加载功能 -->
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
 | 
			
		||||
import { useRoute } from 'vue-router'
 | 
			
		||||
import { useFavourites } from '../stores/useFavourites'
 | 
			
		||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
			
		||||
import { debugPlayer } from '../utils/debug'
 | 
			
		||||
import { watch, ref } from 'vue'
 | 
			
		||||
import apis from '../apis'
 | 
			
		||||
 | 
			
		||||
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
 | 
			
		||||
import PlayIcon from '../assets/icons/play.vue'
 | 
			
		||||
import PauseIcon from '../assets/icons/pause.vue'
 | 
			
		||||
import {
 | 
			
		||||
	audioVisualizer,
 | 
			
		||||
	checkAndRefreshSongResource,
 | 
			
		||||
	supportsWebAudioVisualization,
 | 
			
		||||
} from '../utils'
 | 
			
		||||
const playQueue = usePlayQueueStore()
 | 
			
		||||
 | 
			
		||||
const playQueueStore = usePlayQueueStore()
 | 
			
		||||
const favourites = useFavourites()
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
const player = useTemplateRef('playerRef')
 | 
			
		||||
 | 
			
		||||
// [调试] 检查 store 方法类型
 | 
			
		||||
console.log('[Player] 检查 store 方法:', {
 | 
			
		||||
	preloadNext: typeof playQueueStore.preloadNext,
 | 
			
		||||
	getPreloadedAudio: typeof playQueueStore.getPreloadedAudio,
 | 
			
		||||
	clearPreloadedAudio: typeof playQueueStore.clearPreloadedAudio,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 获取当前歌曲的计算属性
 | 
			
		||||
const currentTrack = computed(() => {
 | 
			
		||||
	if (
 | 
			
		||||
		playQueueStore.playMode.shuffle &&
 | 
			
		||||
		playQueueStore.shuffleList.length > 0
 | 
			
		||||
	) {
 | 
			
		||||
		return playQueueStore.list[
 | 
			
		||||
			playQueueStore.shuffleList[playQueueStore.currentIndex]
 | 
			
		||||
		]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return playQueueStore.list[playQueueStore.currentIndex]
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 获取当前歌曲的音频源
 | 
			
		||||
const currentAudioSrc = computed(() => {
 | 
			
		||||
	const track = currentTrack.value
 | 
			
		||||
	return track ? track.song.sourceUrl : ''
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueueStore.isPlaying,
 | 
			
		||||
	(newValue) => {
 | 
			
		||||
		if (newValue) {
 | 
			
		||||
			player.value?.play()
 | 
			
		||||
			setMetadata()
 | 
			
		||||
		} else {
 | 
			
		||||
			player.value?.pause()
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 监听当前索引变化,处理预加载逻辑
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueueStore.currentIndex,
 | 
			
		||||
	async () => {
 | 
			
		||||
		console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
 | 
			
		||||
 | 
			
		||||
		// 检查是否可以使用预加载的音频
 | 
			
		||||
		const track = currentTrack.value
 | 
			
		||||
		if (track) {
 | 
			
		||||
			const songId = track.song.cid
 | 
			
		||||
 | 
			
		||||
			try {
 | 
			
		||||
				// 首先检查和刷新当前歌曲的资源
 | 
			
		||||
				console.log('[Player] 检查当前歌曲资源:', track.song.name)
 | 
			
		||||
				const updatedSong = await checkAndRefreshSongResource(
 | 
			
		||||
					track.song,
 | 
			
		||||
					(updated) => {
 | 
			
		||||
						// 更新播放队列中的歌曲信息
 | 
			
		||||
						// 在随机播放模式下,currentIndex 是 shuffleList 的索引
 | 
			
		||||
						// 需要通过 shuffleList[currentIndex] 获取实际的 list 索引
 | 
			
		||||
						const actualIndex =
 | 
			
		||||
							playQueueStore.playMode.shuffle &&
 | 
			
		||||
							playQueueStore.shuffleList.length > 0
 | 
			
		||||
								? playQueueStore.shuffleList[playQueueStore.currentIndex]
 | 
			
		||||
								: playQueueStore.currentIndex
 | 
			
		||||
						if (playQueueStore.list[actualIndex]) {
 | 
			
		||||
							playQueueStore.list[actualIndex].song = updated
 | 
			
		||||
						}
 | 
			
		||||
						// 如果歌曲在收藏夹中,也更新收藏夹
 | 
			
		||||
						favourites.updateSongInFavourites(songId, updated)
 | 
			
		||||
					},
 | 
			
		||||
				)
 | 
			
		||||
 | 
			
		||||
				// 使用更新后的歌曲信息
 | 
			
		||||
				const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
 | 
			
		||||
 | 
			
		||||
				if (preloadedAudio && updatedSong.sourceUrl === track.song.sourceUrl) {
 | 
			
		||||
					console.log(`[Player] 使用预加载的音频: ${track.song.name}`)
 | 
			
		||||
 | 
			
		||||
					// 直接使用预加载的音频数据
 | 
			
		||||
					if (player.value) {
 | 
			
		||||
						// 复制预加载音频的状态到主播放器
 | 
			
		||||
						player.value.src = preloadedAudio.src
 | 
			
		||||
						player.value.currentTime = 0
 | 
			
		||||
 | 
			
		||||
						// 清理使用过的预加载音频
 | 
			
		||||
						playQueueStore.clearPreloadedAudio(songId)
 | 
			
		||||
 | 
			
		||||
						// 如果正在播放状态,立即播放
 | 
			
		||||
						if (playQueueStore.isPlaying) {
 | 
			
		||||
							await nextTick()
 | 
			
		||||
							player.value.play().catch(console.error)
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						playQueueStore.isBuffering = false
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					console.log(`[Player] 正常加载音频: ${track.song.name}`)
 | 
			
		||||
					playQueueStore.isBuffering = true
 | 
			
		||||
 | 
			
		||||
					// 如果资源地址已更新,清除旧的预加载音频
 | 
			
		||||
					if (updatedSong.sourceUrl !== track.song.sourceUrl) {
 | 
			
		||||
						playQueueStore.clearPreloadedAudio(songId)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				console.error('[Player] 处理预加载音频时出错:', error)
 | 
			
		||||
				playQueueStore.isBuffering = true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		setMetadata()
 | 
			
		||||
 | 
			
		||||
		// 延迟预加载下一首歌,避免影响当前歌曲加载
 | 
			
		||||
		setTimeout(async () => {
 | 
			
		||||
			try {
 | 
			
		||||
				console.log('[Player] 尝试预加载下一首歌')
 | 
			
		||||
 | 
			
		||||
				// 检查函数是否存在
 | 
			
		||||
				if (typeof playQueueStore.preloadNext === 'function') {
 | 
			
		||||
					await playQueueStore.preloadNext()
 | 
			
		||||
 | 
			
		||||
					// 预加载完成后,检查播放队列是否有更新,同步到收藏夹
 | 
			
		||||
					playQueueStore.list.forEach((item) => {
 | 
			
		||||
						if (favourites.isFavourite(item.song.cid)) {
 | 
			
		||||
							favourites.updateSongInFavourites(item.song.cid, item.song)
 | 
			
		||||
						}
 | 
			
		||||
					})
 | 
			
		||||
 | 
			
		||||
					playQueueStore.limitPreloadCache()
 | 
			
		||||
				} else {
 | 
			
		||||
					console.error('[Player] preloadNext 不是一个函数')
 | 
			
		||||
				}
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				console.error('[Player] 预加载失败:', error)
 | 
			
		||||
			}
 | 
			
		||||
		}, 1000)
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
function artistsOrganize(list: string[]) {
 | 
			
		||||
	if (list.length === 0) {
 | 
			
		||||
		return '未知音乐人'
 | 
			
		||||
	}
 | 
			
		||||
	return list
 | 
			
		||||
		.map((artist) => {
 | 
			
		||||
			return artist
 | 
			
		||||
		})
 | 
			
		||||
		.join(' / ')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setMetadata() {
 | 
			
		||||
	if ('mediaSession' in navigator) {
 | 
			
		||||
		const current = currentTrack.value
 | 
			
		||||
		if (!current) return
 | 
			
		||||
 | 
			
		||||
		navigator.mediaSession.metadata = new MediaMetadata({
 | 
			
		||||
			title: current.song.name,
 | 
			
		||||
			artist: artistsOrganize(current.song.artists ?? []),
 | 
			
		||||
			album: current.album?.name,
 | 
			
		||||
			artwork: [
 | 
			
		||||
				{
 | 
			
		||||
					src: current.album?.coverUrl ?? '',
 | 
			
		||||
					sizes: '500x500',
 | 
			
		||||
					type: 'image/png',
 | 
			
		||||
				},
 | 
			
		||||
			],
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		navigator.mediaSession.setActionHandler('previoustrack', playPrevious)
 | 
			
		||||
		navigator.mediaSession.setActionHandler('nexttrack', playNext)
 | 
			
		||||
 | 
			
		||||
		playQueueStore.duration = player.value?.duration ?? 0
 | 
			
		||||
		playQueueStore.currentTime = player.value?.currentTime ?? 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	watch(
 | 
			
		||||
		() => playQueueStore.updatedCurrentTime,
 | 
			
		||||
		(newValue) => {
 | 
			
		||||
			if (newValue === null) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if (player.value) player.value.currentTime = newValue
 | 
			
		||||
			playQueueStore.updatedCurrentTime = null
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function playNext() {
 | 
			
		||||
	if (playQueueStore.currentIndex === playQueueStore.list.length - 1) {
 | 
			
		||||
		console.log('at the bottom, pause')
 | 
			
		||||
		playQueueStore.currentIndex = 0
 | 
			
		||||
		if (playQueueStore.playMode.repeat === 'all') {
 | 
			
		||||
			playQueueStore.currentIndex = 0
 | 
			
		||||
			playQueueStore.isPlaying = true
 | 
			
		||||
		} else {
 | 
			
		||||
			player.value?.pause()
 | 
			
		||||
			playQueueStore.isPlaying = false
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		playQueueStore.currentIndex++
 | 
			
		||||
		playQueueStore.isPlaying = true
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function playPrevious() {
 | 
			
		||||
	if (
 | 
			
		||||
		player.value &&
 | 
			
		||||
		(player.value.currentTime ?? 0) < 5 &&
 | 
			
		||||
		playQueueStore.currentIndex > 0
 | 
			
		||||
	) {
 | 
			
		||||
		playQueueStore.currentIndex--
 | 
			
		||||
		playQueueStore.isPlaying = true
 | 
			
		||||
	} else {
 | 
			
		||||
		if (player.value) {
 | 
			
		||||
			player.value.currentTime = 0
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateCurrentTime() {
 | 
			
		||||
	playQueueStore.currentTime = player.value?.currentTime ?? 0
 | 
			
		||||
 | 
			
		||||
	// 智能预加载策略:支持动态配置
 | 
			
		||||
	if (playQueueStore.duration > 0) {
 | 
			
		||||
		const progress = playQueueStore.currentTime / playQueueStore.duration
 | 
			
		||||
		const remainingTime = playQueueStore.duration - playQueueStore.currentTime
 | 
			
		||||
 | 
			
		||||
		// 从 localStorage 获取配置,如果没有则使用默认值
 | 
			
		||||
		const config = JSON.parse(localStorage.getItem('preloadConfig') || '{}')
 | 
			
		||||
		const preloadTrigger = (config.preloadTrigger || 50) / 100 // 转换为小数
 | 
			
		||||
		const remainingTimeThreshold = config.remainingTimeThreshold || 30
 | 
			
		||||
 | 
			
		||||
		if (
 | 
			
		||||
			(progress > preloadTrigger || remainingTime < remainingTimeThreshold) &&
 | 
			
		||||
			!playQueueStore.isPreloading
 | 
			
		||||
		) {
 | 
			
		||||
			try {
 | 
			
		||||
				if (typeof playQueueStore.preloadNext === 'function') {
 | 
			
		||||
					playQueueStore.preloadNext()
 | 
			
		||||
				} else {
 | 
			
		||||
					console.error('[Player] preloadNext 不是一个函数')
 | 
			
		||||
				}
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				console.error('[Player] 智能预加载失败:', error)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 检查浏览器是否支持音频可视化
 | 
			
		||||
const isAudioVisualizationSupported = supportsWebAudioVisualization()
 | 
			
		||||
console.log('[Player] 音频可视化支持状态:', isAudioVisualizationSupported)
 | 
			
		||||
 | 
			
		||||
// 只在支持的浏览器上初始化音频可视化
 | 
			
		||||
let barHeights = ref<number[]>([0, 0, 0, 0, 0, 0])
 | 
			
		||||
let connectAudio = (_audio: HTMLAudioElement) => {}
 | 
			
		||||
let isAnalyzing = ref(false)
 | 
			
		||||
let error = ref<string | null>(null)
 | 
			
		||||
 | 
			
		||||
if (isAudioVisualizationSupported) {
 | 
			
		||||
	console.log('[Player] 初始化 audioVisualizer')
 | 
			
		||||
	const visualizer = audioVisualizer({
 | 
			
		||||
		sensitivity: 1.5,
 | 
			
		||||
		barCount: 6,
 | 
			
		||||
		maxDecibels: -10,
 | 
			
		||||
		bassBoost: 0.8,
 | 
			
		||||
		midBoost: 1.2,
 | 
			
		||||
		trebleBoost: 1.4,
 | 
			
		||||
		threshold: 0,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	barHeights = visualizer.barHeights
 | 
			
		||||
	connectAudio = visualizer.connectAudio
 | 
			
		||||
	isAnalyzing = visualizer.isAnalyzing
 | 
			
		||||
	error = visualizer.error
 | 
			
		||||
 | 
			
		||||
	debugPlayer('audioVisualizer 返回值:', {
 | 
			
		||||
		barHeights: barHeights.value,
 | 
			
		||||
		isAnalyzing: isAnalyzing.value,
 | 
			
		||||
	})
 | 
			
		||||
} else {
 | 
			
		||||
	console.log('[Player] 音频可视化被禁用(Safari 或不支持的浏览器)')
 | 
			
		||||
}
 | 
			
		||||
const resourcesUrl = ref<{ [key: string]: string }>({})
 | 
			
		||||
 | 
			
		||||
// 监听播放列表变化
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueueStore.list.length,
 | 
			
		||||
	async (newLength) => {
 | 
			
		||||
		console.log('[Player] 播放列表长度变化:', newLength)
 | 
			
		||||
		if (newLength === 0) {
 | 
			
		||||
			console.log('[Player] 播放列表为空,跳过连接')
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 等待下一帧,确保 audio 元素已经渲染
 | 
			
		||||
		await nextTick()
 | 
			
		||||
 | 
			
		||||
		if (player.value) {
 | 
			
		||||
			if (isAudioVisualizationSupported) {
 | 
			
		||||
				console.log('[Player] 连接音频元素到可视化器')
 | 
			
		||||
				console.log('[Player] 音频元素状态:', {
 | 
			
		||||
					src: player.value.src?.substring(0, 50) + '...',
 | 
			
		||||
					readyState: player.value.readyState,
 | 
			
		||||
					paused: player.value.paused,
 | 
			
		||||
				})
 | 
			
		||||
				connectAudio(player.value)
 | 
			
		||||
			} else {
 | 
			
		||||
				console.log('[Player] 跳过音频可视化连接(不支持的浏览器)')
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			console.log('[Player] ❌ 音频元素不存在')
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		playQueueStore.visualizer = barHeights.value
 | 
			
		||||
 | 
			
		||||
		// 开始预加载第一首歌的下一首
 | 
			
		||||
		setTimeout(() => {
 | 
			
		||||
			playQueueStore.preloadNext()
 | 
			
		||||
		}, 2000)
 | 
			
		||||
 | 
			
		||||
		// 初始化音量
 | 
			
		||||
		if (player.value) {
 | 
			
		||||
			initializeVolume()
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 监听音频元素变化
 | 
			
		||||
watch(
 | 
			
		||||
	() => player.value,
 | 
			
		||||
	(audioElement) => {
 | 
			
		||||
		if (
 | 
			
		||||
			audioElement &&
 | 
			
		||||
			playQueueStore.list.length > 0 &&
 | 
			
		||||
			isAudioVisualizationSupported
 | 
			
		||||
		) {
 | 
			
		||||
			connectAudio(audioElement)
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 监听可视化器数据变化
 | 
			
		||||
watch(
 | 
			
		||||
	() => barHeights.value,
 | 
			
		||||
	(newHeights) => {
 | 
			
		||||
		playQueueStore.visualizer = newHeights
 | 
			
		||||
	},
 | 
			
		||||
	{ deep: true },
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 监听错误
 | 
			
		||||
watch(
 | 
			
		||||
	() => error.value,
 | 
			
		||||
	(newError) => {
 | 
			
		||||
		if (newError) {
 | 
			
		||||
			console.error('[Player] 可视化器错误:', newError)
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 切换播放模式
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueueStore.playMode.shuffle,
 | 
			
		||||
	(isShuffle) => {
 | 
			
		||||
		if (isShuffle) {
 | 
			
		||||
			const currentIndex = playQueueStore.currentIndex
 | 
			
		||||
			const trackCount = playQueueStore.list.length
 | 
			
		||||
 | 
			
		||||
			// 1. 已播放部分:不变
 | 
			
		||||
			let shuffledList = [...Array(currentIndex).keys()]
 | 
			
		||||
 | 
			
		||||
			// 2. 构建待打乱的列表
 | 
			
		||||
			const shuffleSpace = [...Array(trackCount).keys()].filter((index) =>
 | 
			
		||||
				playQueueStore.shuffleCurrent
 | 
			
		||||
					? index >= currentIndex
 | 
			
		||||
					: index > currentIndex,
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			// 3. 随机打乱
 | 
			
		||||
			shuffleSpace.sort(() => Math.random() - 0.5)
 | 
			
		||||
 | 
			
		||||
			// 4. 如果当前曲目不参与打乱,插入回当前位置(即 currentIndex 处)
 | 
			
		||||
			if (!playQueueStore.shuffleCurrent) {
 | 
			
		||||
				shuffledList.push(currentIndex)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 5. 拼接:已播放部分 + 当前(可选)+ 打乱后的剩余部分
 | 
			
		||||
			shuffledList = shuffledList.concat(shuffleSpace)
 | 
			
		||||
 | 
			
		||||
			// 6. 应用 shuffleList
 | 
			
		||||
			playQueueStore.shuffleList = shuffledList
 | 
			
		||||
 | 
			
		||||
			// 清除 shuffleCurrent 状态
 | 
			
		||||
			playQueueStore.shuffleCurrent = undefined
 | 
			
		||||
		} else {
 | 
			
		||||
			// 退出随机播放:恢复当前播放曲目的原索引
 | 
			
		||||
			playQueueStore.currentIndex =
 | 
			
		||||
				playQueueStore.shuffleList[playQueueStore.currentIndex]
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 切换播放模式后重新预加载
 | 
			
		||||
		setTimeout(() => {
 | 
			
		||||
			playQueueStore.clearAllPreloadedAudio()
 | 
			
		||||
			playQueueStore.preloadNext()
 | 
			
		||||
		}, 500)
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
function getCurrentTrack() {
 | 
			
		||||
	return currentTrack.value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 初始化音量
 | 
			
		||||
function initializeVolume() {
 | 
			
		||||
	if (player.value) {
 | 
			
		||||
		const savedVolume = localStorage.getItem('audioVolume')
 | 
			
		||||
		if (savedVolume) {
 | 
			
		||||
			const volumeValue = Number.parseFloat(savedVolume)
 | 
			
		||||
			player.value.volume = volumeValue
 | 
			
		||||
			console.log('[Player] 初始化音量:', volumeValue)
 | 
			
		||||
		} else {
 | 
			
		||||
			// 设置默认音量
 | 
			
		||||
			player.value.volume = 1
 | 
			
		||||
			localStorage.setItem('audioVolume', '1')
 | 
			
		||||
		}
 | 
			
		||||
watch(() => playQueue.queue, async () => {
 | 
			
		||||
	debugPlayer(playQueue.queue)
 | 
			
		||||
	let newResourcesUrl: { [key: string]: string } = {}
 | 
			
		||||
	for (const track of playQueue.queue) {
 | 
			
		||||
		const res = await apis.getSong(track.song.cid)
 | 
			
		||||
		newResourcesUrl[track.song.cid] = track.song.sourceUrl
 | 
			
		||||
	}
 | 
			
		||||
	debugPlayer(newResourcesUrl)
 | 
			
		||||
	resourcesUrl.value = newResourcesUrl
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 判断目前播放状态
 | 
			
		||||
function isAutoPlay(cid: string) {
 | 
			
		||||
	// 为了提前缓存播放队列中的歌曲,同时消除两首歌切换时的间隙,因此改用了新的方式来在网页上挂载音频
 | 
			
		||||
	// 现在会将队列中每一首歌曲都挂载一个单独的 <audio> 并添加 preload="auto" 属性
 | 
			
		||||
	// 这样就可以利用浏览器的内置行为来提前缓存队列中的所有歌曲了
 | 
			
		||||
	// 不过,这样就会导致判断到底哪一首歌需要播放就成了难题
 | 
			
		||||
	// 因此就有了这个函数,用于判断哪一个 <audio> 元素需要进行播放
 | 
			
		||||
	// 此函数主要用于 <audio> 元素的 autoplay 属性,以便在专辑或歌单页面点击播放按钮时直接开始播放音乐
 | 
			
		||||
 | 
			
		||||
	// 先判断是否正在播放
 | 
			
		||||
	if (!playQueue.isPlaying) return false
 | 
			
		||||
 | 
			
		||||
	// 再判断是否是目前曲目
 | 
			
		||||
	if (playQueue.currentTrack.song.cid !== cid) return false
 | 
			
		||||
 | 
			
		||||
	return true	
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 监听音量变化事件
 | 
			
		||||
function handleVolumeChange(event: Event) {
 | 
			
		||||
	const target = event.target as HTMLAudioElement
 | 
			
		||||
	if (target) {
 | 
			
		||||
		// 保存音量变化到localStorage
 | 
			
		||||
		localStorage.setItem('audioVolume', target.volume.toString())
 | 
			
		||||
		console.log('[Player] 音量变化:', target.volume)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 监听localStorage中音量的变化,同步到音频元素
 | 
			
		||||
function syncVolumeFromStorage() {
 | 
			
		||||
	if (player.value) {
 | 
			
		||||
		const savedVolume = localStorage.getItem('audioVolume')
 | 
			
		||||
		if (savedVolume) {
 | 
			
		||||
			const volumeValue = Number.parseFloat(savedVolume)
 | 
			
		||||
			if (player.value.volume !== volumeValue) {
 | 
			
		||||
				player.value.volume = volumeValue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定期检查音量同步(可选,或者使用storage事件)
 | 
			
		||||
setInterval(syncVolumeFromStorage, 100)
 | 
			
		||||
 | 
			
		||||
// 组件卸载时清理预加载
 | 
			
		||||
// onUnmounted(() => {
 | 
			
		||||
//   playQueueStore.clearAllPreloadedAudio()
 | 
			
		||||
// })
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<div>
 | 
			
		||||
		<audio :src="currentAudioSrc" ref="playerRef" :autoplay="playQueueStore.isPlaying"
 | 
			
		||||
			v-if="playQueueStore.list.length !== 0" @volumechange="handleVolumeChange" @ended="() => {
 | 
			
		||||
				if (playQueueStore.playMode.repeat === 'single') { playQueueStore.isPlaying = true }
 | 
			
		||||
				else { playNext() }
 | 
			
		||||
			}" @pause="playQueueStore.isPlaying = false" @play="playQueueStore.isPlaying = true" @playing="() => {
 | 
			
		||||
				console.log('[Player] 音频开始播放事件')
 | 
			
		||||
				playQueueStore.isBuffering = false
 | 
			
		||||
				setMetadata()
 | 
			
		||||
				initializeVolume()
 | 
			
		||||
			}" @waiting="playQueueStore.isBuffering = true" @loadeddata="() => {
 | 
			
		||||
				console.log('[Player] 音频数据加载完成')
 | 
			
		||||
				playQueueStore.isBuffering = false
 | 
			
		||||
				initializeVolume()
 | 
			
		||||
			}" @canplay="() => {
 | 
			
		||||
				console.log('[Player] 音频可以播放')
 | 
			
		||||
				playQueueStore.isBuffering = false
 | 
			
		||||
			}" @error="(e) => {
 | 
			
		||||
				console.error('[Player] 音频错误:', e)
 | 
			
		||||
				playQueueStore.isBuffering = false
 | 
			
		||||
			}" crossorigin="anonymous" @timeupdate="updateCurrentTime">
 | 
			
		||||
		</audio>
 | 
			
		||||
 | 
			
		||||
		<!-- 预加载进度指示器(可选显示) -->
 | 
			
		||||
		<!-- <div v-if="playQueueStore.isPreloading"
 | 
			
		||||
			class="fixed top-4 right-4 bg-black/80 text-white px-3 py-1 rounded text-xs z-50">
 | 
			
		||||
			预加载中... {{ Math.round(playQueueStore.preloadProgress) }}%
 | 
			
		||||
		</div> -->
 | 
			
		||||
 | 
			
		||||
		<div
 | 
			
		||||
			class="text-white h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex gap-2 overflow-hidden select-none"
 | 
			
		||||
			v-if="playQueueStore.list.length !== 0 && route.path !== '/playroom'">
 | 
			
		||||
			<RouterLink to="/playroom">
 | 
			
		||||
				<img :src="getCurrentTrack()?.album?.coverUrl ?? ''" class="rounded-full h-8 w-8 mt-[.0625rem]" />
 | 
			
		||||
			</RouterLink>
 | 
			
		||||
 | 
			
		||||
			<RouterLink to="/playroom">
 | 
			
		||||
				<div class="flex items-center w-32 h-9">
 | 
			
		||||
					<span class="truncate text-xs">{{ getCurrentTrack()?.song.name }}</span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</RouterLink>
 | 
			
		||||
 | 
			
		||||
			<button class="h-9 w-12 flex justify-center items-center" @click.stop="() => {
 | 
			
		||||
				playQueueStore.isPlaying = !playQueueStore.isPlaying
 | 
			
		||||
			}">
 | 
			
		||||
				<div v-if="playQueueStore.isPlaying">
 | 
			
		||||
					<LoadingIndicator v-if="playQueueStore.isBuffering === true" :size="4" />
 | 
			
		||||
					<!-- 在支持的浏览器上显示可视化,否则显示暂停图标 -->
 | 
			
		||||
					<div v-else-if="isAudioVisualizationSupported" class="h-4 flex justify-center items-center gap-[.125rem]">
 | 
			
		||||
						<div class="bg-white/75 w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
 | 
			
		||||
							:key="index" :style="{
 | 
			
		||||
								height: `${Math.max(10, bar)}%`
 | 
			
		||||
							}" />
 | 
			
		||||
					</div>
 | 
			
		||||
					<PauseIcon v-else :size="4" />
 | 
			
		||||
				</div>
 | 
			
		||||
				<PlayIcon v-else :size="4" />
 | 
			
		||||
			</button>
 | 
			
		||||
		<div class="text-white"  v-for="track in playQueue.queue" :key="track.song.cid">
 | 
			
		||||
			<audio 
 | 
			
		||||
				v-if="resourcesUrl[track.song.cid]" 
 | 
			
		||||
				:src="resourcesUrl[track.song.cid]" 
 | 
			
		||||
				preload="auto" 
 | 
			
		||||
				:ref="`audio-${track.song.cid}`"
 | 
			
		||||
				:autoplay="isAutoPlay(track.song.cid)"
 | 
			
		||||
			/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</template>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,15 @@
 | 
			
		|||
import { defineStore } from 'pinia'
 | 
			
		||||
import { ref, computed } from 'vue'
 | 
			
		||||
import apis from '../apis'
 | 
			
		||||
 | 
			
		||||
export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		||||
	// 内部状态
 | 
			
		||||
	const queue = ref<QueueItem[]>([])
 | 
			
		||||
	const isShuffle = ref<boolean>(false)
 | 
			
		||||
	const isShuffle = ref(false)
 | 
			
		||||
	const loopingMode = ref<'single' | 'all' | 'off'>('off')
 | 
			
		||||
	const queueReplaceLock = ref<boolean>(false)
 | 
			
		||||
	const currentPlaying = ref<number>(0) // 当前播放指针,指针在 queueOrder 中寻址(无论是否开启了随机播放)
 | 
			
		||||
	const queueReplaceLock = ref(false)
 | 
			
		||||
	const currentPlaying = ref(0) // 当前播放指针,指针在 queueOrder 中寻址(无论是否开启了随机播放)
 | 
			
		||||
	const queueOrder = ref<number[]>([]) // 播放队列顺序
 | 
			
		||||
	const isPlaying = ref(false)
 | 
			
		||||
 | 
			
		||||
	// 暴露给外部的响应式只读引用
 | 
			
		||||
	const queueState = computed(() =>
 | 
			
		||||
| 
						 | 
				
			
			@ -26,13 +26,16 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
		const actualIndex = queueOrder.value[currentPlaying.value]
 | 
			
		||||
		return queue.value[actualIndex] || null
 | 
			
		||||
	})
 | 
			
		||||
	
 | 
			
		||||
	// 获取当前是否正在播放
 | 
			
		||||
	const playingState = computed(() => isPlaying.value)
 | 
			
		||||
 | 
			
		||||
	/************
 | 
			
		||||
	 *	播放队列相关
 | 
			
		||||
	 ***********/
 | 
			
		||||
	// 使用新队列替换老队列
 | 
			
		||||
	// 队列替换锁开启时启用确认,确认后重置该锁
 | 
			
		||||
	async function replaceQueue(songs: Song[]) {
 | 
			
		||||
	async function replaceQueue(newQueue: QueueItem[]) {
 | 
			
		||||
		if (queueReplaceLock.value) {
 | 
			
		||||
			if (
 | 
			
		||||
				!confirm(
 | 
			
		||||
| 
						 | 
				
			
			@ -45,27 +48,6 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
			queueReplaceLock.value = false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let newQueue: QueueItem[] = []
 | 
			
		||||
 | 
			
		||||
		// 专辑信息缓存空间
 | 
			
		||||
		let albums: { [key: string]: Album } = {}
 | 
			
		||||
 | 
			
		||||
		for (let i in songs) {
 | 
			
		||||
			// 写入新队列
 | 
			
		||||
			newQueue[newQueue.length] = {
 | 
			
		||||
				song: songs[i],
 | 
			
		||||
				album: await (async () => {
 | 
			
		||||
					if (albums[songs[i].albumCid ?? '0'])
 | 
			
		||||
						return albums[songs[i].albumCid ?? '0']
 | 
			
		||||
					else {
 | 
			
		||||
						const album = await apis.getAlbum(songs[i].albumCid ?? '0')
 | 
			
		||||
						albums[songs[i].albumCid ?? '0'] = album
 | 
			
		||||
						return album
 | 
			
		||||
					}
 | 
			
		||||
				})(),
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 将新队列替换已有队列
 | 
			
		||||
		queue.value = newQueue
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -78,6 +60,17 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
		loopingMode.value = 'off'
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/***********
 | 
			
		||||
	 * 播放控制相关
 | 
			
		||||
	 *
 | 
			
		||||
	 **********/
 | 
			
		||||
	// 控制播放
 | 
			
		||||
	const togglePlay = (turnTo?: boolean) => {
 | 
			
		||||
		const newPlayState = turnTo ?? !isPlaying.value
 | 
			
		||||
		if (newPlayState === isPlaying.value) return
 | 
			
		||||
		isPlaying.value = newPlayState
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/************
 | 
			
		||||
	 * 播放模式相关
 | 
			
		||||
	 **********/
 | 
			
		||||
| 
						 | 
				
			
			@ -88,6 +81,7 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
 | 
			
		||||
		if (newShuffleState === isShuffle.value) return // 状态未改变
 | 
			
		||||
 | 
			
		||||
		// TODO: 进行洗牌
 | 
			
		||||
		/* if (newShuffleState) {
 | 
			
		||||
			// 开启随机播放:保存当前顺序并打乱
 | 
			
		||||
			const originalOrder = [...queueOrder.value]
 | 
			
		||||
| 
						 | 
				
			
			@ -154,10 +148,12 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
		loopMode: loopModeState,
 | 
			
		||||
		currentTrack,
 | 
			
		||||
		currentIndex: currentPlaying,
 | 
			
		||||
		isPlaying: playingState,
 | 
			
		||||
 | 
			
		||||
		// 修改方法
 | 
			
		||||
		replaceQueue,
 | 
			
		||||
		toggleShuffle,
 | 
			
		||||
		toggleLoop,
 | 
			
		||||
		togglePlay
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user