refactor: 使用新的音频挂载逻辑

This commit is contained in:
Astrian Zheng 2025-08-19 16:48:43 +10:00
parent 35f7332bff
commit c2ffb57085
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
6 changed files with 96 additions and 558 deletions

22
package-lock.json generated
View File

@ -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": {

View File

@ -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"

View File

@ -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>

View File

@ -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() {

View File

@ -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
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
})
// audio
await nextTick()
//
function isAutoPlay(cid: string) {
//
// <audio> preload="auto"
//
//
// <audio>
// <audio> autoplay 便
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] ❌ 音频元素不存在')
}
//
if (!playQueue.isPlaying) return false
playQueueStore.visualizer = barHeights.value
//
if (playQueue.currentTrack.song.cid !== cid) return false
//
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
return true
}
//
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')
}
}
}
//
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>

View File

@ -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(() =>
@ -27,12 +27,15 @@ export const usePlayQueueStore = defineStore('queue', () => {
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
}
})