feat(Player): implement audio preloading functionality with state management

This commit is contained in:
Astrian Zheng 2025-05-27 13:44:04 +10:00
parent 096e74a9bd
commit 7a0c638d2c
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
2 changed files with 354 additions and 68 deletions

View File

@ -1,7 +1,8 @@
<!-- Player.vue - 添加调试信息 -->
<!-- Player.vue - 添加预加载功能 -->
<script setup lang="ts">
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
import { useTemplateRef, watch, nextTick } from 'vue'
import { useTemplateRef, watch, nextTick, computed } from 'vue'
import { useRoute } from 'vue-router'
import PlayIcon from '../assets/icons/play.vue'
@ -12,6 +13,28 @@ const playQueueStore = usePlayQueueStore()
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]]
} else {
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()
@ -20,10 +43,66 @@ watch(() => playQueueStore.isPlaying, (newValue) => {
else { player.value?.pause() }
})
watch(() => playQueueStore.currentIndex, () => {
//
watch(() => playQueueStore.currentIndex, async () => {
console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
setMetadata()
// 使
const track = currentTrack.value
if (track) {
const songId = track.song.cid
try {
const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
if (preloadedAudio) {
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
}
} catch (error) {
console.error('[Player] 处理预加载音频时出错:', error)
playQueueStore.isBuffering = true
}
}
setMetadata()
//
setTimeout(() => {
try {
console.log('[Player] 尝试预加载下一首歌')
//
if (typeof playQueueStore.preloadNext === 'function') {
playQueueStore.preloadNext()
playQueueStore.limitPreloadCache()
} else {
console.error('[Player] preloadNext 不是一个函数')
}
} catch (error) {
console.error('[Player] 预加载失败:', error)
}
}, 1000)
})
function artistsOrganize(list: string[]) {
@ -35,13 +114,9 @@ function artistsOrganize(list: string[]) {
function setMetadata() {
if ('mediaSession' in navigator) {
let current = (() => {
if (playQueueStore.playMode.shuffle) {
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
} else {
return playQueueStore.list[playQueueStore.currentIndex]
}
})()
let current = currentTrack.value
if (!current) return
navigator.mediaSession.metadata = new MediaMetadata({
title: current.song.name,
artist: artistsOrganize(current.song.artists ?? []),
@ -93,6 +168,31 @@ function playPrevious() {
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) {
console.log(`[Player] 触发预加载 - 进度: ${Math.round(progress * 100)}%, 剩余: ${Math.round(remainingTime)}s`)
try {
if (typeof playQueueStore.preloadNext === 'function') {
playQueueStore.preloadNext()
} else {
console.error('[Player] preloadNext 不是一个函数')
}
} catch (error) {
console.error('[Player] 智能预加载失败:', error)
}
}
}
}
console.log('[Player] 初始化 audioVisualizer')
@ -132,6 +232,11 @@ watch(() => playQueueStore.list.length, async (newLength) => {
}
playQueueStore.visualizer = barHeights.value
//
setTimeout(() => {
playQueueStore.preloadNext()
}, 2000)
})
//
@ -185,60 +290,62 @@ watch(() => playQueueStore.playMode.shuffle, (isShuffle) => {
// currentIndex
playQueueStore.currentIndex = playQueueStore.shuffleList[playQueueStore.currentIndex]
}
//
setTimeout(() => {
playQueueStore.clearAllPreloadedAudio()
playQueueStore.preloadNext()
}, 500)
})
function getCurrentTrack() {
if (playQueueStore.playMode.shuffle) {
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
} else {
return playQueueStore.list[playQueueStore.currentIndex]
}
return currentTrack.value
}
//
// onUnmounted(() => {
// playQueueStore.clearAllPreloadedAudio()
// })
</script>
<template>
<div>
<audio
:src="(() => {
if (playQueueStore.playMode.shuffle) {
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]] ? playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]].song.sourceUrl : ''
} else {
return playQueueStore.list[playQueueStore.currentIndex] ? playQueueStore.list[playQueueStore.currentIndex].song.sourceUrl : ''
}
})()"
ref="playerRef"
:autoplay="playQueueStore.isPlaying"
v-if="playQueueStore.list.length !== 0"
@ended="() => {
<audio :src="currentAudioSrc" ref="playerRef" :autoplay="playQueueStore.isPlaying"
v-if="playQueueStore.list.length !== 0" @ended="() => {
if (playQueueStore.playMode.repeat === 'single') { playQueueStore.isPlaying = true }
else { playNext() }
}"
@pause="playQueueStore.isPlaying = false"
@play="playQueueStore.isPlaying = true"
@playing="() => {
}" @pause="playQueueStore.isPlaying = false" @play="playQueueStore.isPlaying = true" @playing="() => {
console.log('[Player] 音频开始播放事件')
playQueueStore.isBuffering = false
setMetadata()
}"
@waiting="playQueueStore.isBuffering = true"
@loadeddata="() => console.log('[Player] 音频数据加载完成')"
@canplay="() => console.log('[Player] 音频可以播放')"
@error="(e) => console.error('[Player] 音频错误:', e)"
crossorigin="anonymous"
@timeupdate="updateCurrentTime">
}" @waiting="playQueueStore.isBuffering = true" @loadeddata="() => {
console.log('[Player] 音频数据加载完成')
playQueueStore.isBuffering = false
}" @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'"
>
v-if="playQueueStore.list.length !== 0 && route.path !== '/playroom'">
<RouterLink to="/playroom">
<img :src="getCurrentTrack().album?.coverUrl ?? ''" class="rounded-full h-9 w-9" />
<img :src="getCurrentTrack()?.album?.coverUrl ?? ''" class="rounded-full h-9 w-9" />
</RouterLink>
<RouterLink to="/playroom">
<div class="flex items-center w-32 h-9">
<span class="truncate">{{ getCurrentTrack().song.name }}</span>
<span class="truncate">{{ getCurrentTrack()?.song.name }}</span>
</div>
</RouterLink>
@ -248,7 +355,8 @@ function getCurrentTrack() {
<div v-if="playQueueStore.isPlaying">
<LoadingIndicator v-if="playQueueStore.isBuffering === true" :size="4" />
<div v-else 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="{
<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>

View File

@ -1,5 +1,5 @@
import { defineStore } from "pinia"
import { ref } from "vue"
import { ref, computed } from "vue"
export const usePlayQueueStore = defineStore('queue', () => {
const list = ref<QueueItem[]>([])
@ -21,5 +21,183 @@ export const usePlayQueueStore = defineStore('queue', () =>{
})
const shuffleCurrent = ref<boolean | undefined>(undefined)
return { list, currentIndex, isPlaying, queueReplaceLock, isBuffering, currentTime, duration, updatedCurrentTime, visualizer, shuffleList, playMode, shuffleCurrent }
// 预加载相关状态
const preloadedAudio = ref<Map<string, HTMLAudioElement>>(new Map())
const isPreloading = ref<boolean>(false)
const preloadProgress = ref<number>(0)
// 获取下一首歌的索引
const getNextIndex = computed(() => {
if (list.value.length === 0) return -1
if (playMode.value.repeat === 'single') {
return currentIndex.value
}
if (playMode.value.shuffle && shuffleList.value.length > 0) {
const currentShuffleIndex = shuffleList.value.indexOf(currentIndex.value)
if (currentShuffleIndex < shuffleList.value.length - 1) {
return shuffleList.value[currentShuffleIndex + 1]
} else if (playMode.value.repeat === 'all') {
return shuffleList.value[0]
}
return -1
}
if (currentIndex.value < list.value.length - 1) {
return currentIndex.value + 1
} else if (playMode.value.repeat === 'all') {
return 0
}
return -1
})
// 预加载下一首歌
const preloadNext = async () => {
console.log('[Store] preloadNext 被调用')
const nextIndex = getNextIndex.value
if (nextIndex === -1) {
console.log('[Store] 没有下一首歌,跳过预加载')
return
}
// 获取下一首歌曲对象
let nextSong
if (playMode.value.shuffle && shuffleList.value.length > 0) {
nextSong = list.value[shuffleList.value[nextIndex]]
} else {
nextSong = list.value[nextIndex]
}
if (!nextSong || !nextSong.song) {
console.log('[Store] 下一首歌曲不存在,跳过预加载')
return
}
const songId = nextSong.song.cid
// 如果已经预加载过,跳过
if (preloadedAudio.value.has(songId)) {
console.log(`[Store] 歌曲 ${songId} 已预加载`)
return
}
// 检查是否有有效的音频源
if (!nextSong.song.sourceUrl) {
return
}
try {
isPreloading.value = true
preloadProgress.value = 0
const audio = new Audio()
audio.preload = 'auto'
audio.crossOrigin = 'anonymous'
// 监听加载进度
audio.addEventListener('progress', () => {
if (audio.buffered.length > 0) {
const buffered = audio.buffered.end(0)
const total = audio.duration || 1
preloadProgress.value = (buffered / total) * 100
}
})
// 监听加载完成
audio.addEventListener('canplaythrough', () => {
preloadedAudio.value.set(songId, audio)
isPreloading.value = false
preloadProgress.value = 100
})
// 监听加载错误
audio.addEventListener('error', (e) => {
console.error(`[Store] 预加载音频失败: ${e}`)
isPreloading.value = false
preloadProgress.value = 0
})
// 设置音频源并开始加载
audio.src = nextSong.song.sourceUrl
} catch (error) {
isPreloading.value = false
}
}
// 获取预加载的音频对象
const getPreloadedAudio = (songId: string): HTMLAudioElement | null => {
const audio = preloadedAudio.value.get(songId) || null
return audio
}
// 清理预加载的音频
const clearPreloadedAudio = (songId: string) => {
const audio = preloadedAudio.value.get(songId)
if (audio) {
audio.pause()
audio.src = ''
preloadedAudio.value.delete(songId)
}
}
// 清理所有预加载的音频
const clearAllPreloadedAudio = () => {
preloadedAudio.value.forEach((_audio, songId) => {
clearPreloadedAudio(songId)
})
preloadedAudio.value.clear()
}
// 限制预加载缓存大小最多保留3首歌
const limitPreloadCache = () => {
while (preloadedAudio.value.size > 3) {
const oldestKey = preloadedAudio.value.keys().next().value
if (oldestKey) {
clearPreloadedAudio(oldestKey)
} else {
break
}
}
}
// 调试函数:打印当前状态
const debugPreloadState = () => {
console.log('[Store] 预加载状态:', {
isPreloading: isPreloading.value,
progress: preloadProgress.value,
cacheSize: preloadedAudio.value.size,
cachedSongs: Array.from(preloadedAudio.value.keys()),
nextIndex: getNextIndex.value
})
}
return {
list,
currentIndex,
isPlaying,
queueReplaceLock,
isBuffering,
currentTime,
duration,
updatedCurrentTime,
visualizer,
shuffleList,
playMode,
shuffleCurrent,
// 预加载相关 - 确保所有函数都在返回对象中
preloadedAudio,
isPreloading,
preloadProgress,
getNextIndex,
preloadNext,
getPreloadedAudio,
clearPreloadedAudio,
clearAllPreloadedAudio,
limitPreloadCache,
debugPreloadState
}
})