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"> <script setup lang="ts">
import { usePlayQueueStore } from '../stores/usePlayQueueStore' import { usePlayQueueStore } from '../stores/usePlayQueueStore'
import { useTemplateRef, watch, nextTick } from 'vue' import { useTemplateRef, watch, nextTick, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import PlayIcon from '../assets/icons/play.vue' import PlayIcon from '../assets/icons/play.vue'
@ -12,6 +13,28 @@ const playQueueStore = usePlayQueueStore()
const route = useRoute() const route = useRoute()
const player = useTemplateRef('playerRef') 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) => { watch(() => playQueueStore.isPlaying, (newValue) => {
if (newValue) { if (newValue) {
player.value?.play() player.value?.play()
@ -20,10 +43,66 @@ watch(() => playQueueStore.isPlaying, (newValue) => {
else { player.value?.pause() } else { player.value?.pause() }
}) })
watch(() => playQueueStore.currentIndex, () => { //
watch(() => playQueueStore.currentIndex, async () => {
console.log('[Player] 当前索引变化:', playQueueStore.currentIndex) console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
// 使
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() setMetadata()
playQueueStore.isBuffering = true
//
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[]) { function artistsOrganize(list: string[]) {
@ -35,13 +114,9 @@ function artistsOrganize(list: string[]) {
function setMetadata() { function setMetadata() {
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
let current = (() => { let current = currentTrack.value
if (playQueueStore.playMode.shuffle) { if (!current) return
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
} else {
return playQueueStore.list[playQueueStore.currentIndex]
}
})()
navigator.mediaSession.metadata = new MediaMetadata({ navigator.mediaSession.metadata = new MediaMetadata({
title: current.song.name, title: current.song.name,
artist: artistsOrganize(current.song.artists ?? []), artist: artistsOrganize(current.song.artists ?? []),
@ -54,8 +129,8 @@ function setMetadata() {
navigator.mediaSession.setActionHandler('previoustrack', playPrevious) navigator.mediaSession.setActionHandler('previoustrack', playPrevious)
navigator.mediaSession.setActionHandler('nexttrack', playNext) navigator.mediaSession.setActionHandler('nexttrack', playNext)
playQueueStore.duration = player.value?.duration?? 0 playQueueStore.duration = player.value?.duration ?? 0
playQueueStore.currentTime = player.value?.currentTime?? 0 playQueueStore.currentTime = player.value?.currentTime ?? 0
} }
watch(() => playQueueStore.updatedCurrentTime, (newValue) => { watch(() => playQueueStore.updatedCurrentTime, (newValue) => {
@ -69,7 +144,7 @@ function playNext() {
if (playQueueStore.currentIndex === playQueueStore.list.length - 1) { if (playQueueStore.currentIndex === playQueueStore.list.length - 1) {
console.log("at the bottom, pause") console.log("at the bottom, pause")
playQueueStore.currentIndex = 0 playQueueStore.currentIndex = 0
if(playQueueStore.playMode.repeat === 'all') { if (playQueueStore.playMode.repeat === 'all') {
playQueueStore.currentIndex = 0 playQueueStore.currentIndex = 0
playQueueStore.isPlaying = true playQueueStore.isPlaying = true
} else { } else {
@ -92,18 +167,43 @@ function playPrevious() {
} }
function updateCurrentTime() { function updateCurrentTime() {
playQueueStore.currentTime = player.value?.currentTime?? 0 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') console.log('[Player] 初始化 audioVisualizer')
const { barHeights, connectAudio, isAnalyzing, error } = audioVisualizer({ const { barHeights, connectAudio, isAnalyzing, error } = audioVisualizer({
sensitivity: 1.5, sensitivity: 1.5,
barCount: 6, barCount: 6,
maxDecibels: -10, maxDecibels: -10,
bassBoost: 0.8, bassBoost: 0.8,
midBoost: 1.2, midBoost: 1.2,
trebleBoost: 1.4, trebleBoost: 1.4,
threshold: 0 threshold: 0
}) })
console.log('[Player] audioVisualizer 返回值:', { barHeights: barHeights.value, isAnalyzing: isAnalyzing.value }) console.log('[Player] audioVisualizer 返回值:', { barHeights: barHeights.value, isAnalyzing: isAnalyzing.value })
@ -111,15 +211,15 @@ console.log('[Player] audioVisualizer 返回值:', { barHeights: barHeights.valu
// //
watch(() => playQueueStore.list.length, async (newLength) => { watch(() => playQueueStore.list.length, async (newLength) => {
console.log('[Player] 播放列表长度变化:', newLength) console.log('[Player] 播放列表长度变化:', newLength)
if (newLength === 0) { if (newLength === 0) {
console.log('[Player] 播放列表为空,跳过连接') console.log('[Player] 播放列表为空,跳过连接')
return return
} }
// audio // audio
await nextTick() await nextTick()
if (player.value) { if (player.value) {
console.log('[Player] 连接音频元素到可视化器') console.log('[Player] 连接音频元素到可视化器')
console.log('[Player] 音频元素状态:', { console.log('[Player] 音频元素状态:', {
src: player.value.src?.substring(0, 50) + '...', src: player.value.src?.substring(0, 50) + '...',
@ -130,8 +230,13 @@ watch(() => playQueueStore.list.length, async (newLength) => {
} else { } else {
console.log('[Player] ❌ 音频元素不存在') console.log('[Player] ❌ 音频元素不存在')
} }
playQueueStore.visualizer = barHeights.value playQueueStore.visualizer = barHeights.value
//
setTimeout(() => {
playQueueStore.preloadNext()
}, 2000)
}) })
// //
@ -185,60 +290,62 @@ watch(() => playQueueStore.playMode.shuffle, (isShuffle) => {
// currentIndex // currentIndex
playQueueStore.currentIndex = playQueueStore.shuffleList[playQueueStore.currentIndex] playQueueStore.currentIndex = playQueueStore.shuffleList[playQueueStore.currentIndex]
} }
//
setTimeout(() => {
playQueueStore.clearAllPreloadedAudio()
playQueueStore.preloadNext()
}, 500)
}) })
function getCurrentTrack() { function getCurrentTrack() {
if (playQueueStore.playMode.shuffle) { return currentTrack.value
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
} else {
return playQueueStore.list[playQueueStore.currentIndex]
}
} }
//
// onUnmounted(() => {
// playQueueStore.clearAllPreloadedAudio()
// })
</script> </script>
<template> <template>
<div> <div>
<audio <audio :src="currentAudioSrc" ref="playerRef" :autoplay="playQueueStore.isPlaying"
:src="(() => { v-if="playQueueStore.list.length !== 0" @ended="() => {
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="() => {
if (playQueueStore.playMode.repeat === 'single') { playQueueStore.isPlaying = true } if (playQueueStore.playMode.repeat === 'single') { playQueueStore.isPlaying = true }
else { playNext() } else { playNext() }
}" }" @pause="playQueueStore.isPlaying = false" @play="playQueueStore.isPlaying = true" @playing="() => {
@pause="playQueueStore.isPlaying = false"
@play="playQueueStore.isPlaying = true"
@playing="() => {
console.log('[Player] 音频开始播放事件') console.log('[Player] 音频开始播放事件')
playQueueStore.isBuffering = false playQueueStore.isBuffering = false
setMetadata() setMetadata()
}" }" @waiting="playQueueStore.isBuffering = true" @loadeddata="() => {
@waiting="playQueueStore.isBuffering = true" console.log('[Player] 音频数据加载完成')
@loadeddata="() => console.log('[Player] 音频数据加载完成')" playQueueStore.isBuffering = false
@canplay="() => console.log('[Player] 音频可以播放')" }" @canplay="() => {
@error="(e) => console.error('[Player] 音频错误:', e)" console.log('[Player] 音频可以播放')
crossorigin="anonymous" playQueueStore.isBuffering = false
@timeupdate="updateCurrentTime"> }" @error="(e) => {
console.error('[Player] 音频错误:', e)
playQueueStore.isBuffering = false
}" crossorigin="anonymous" @timeupdate="updateCurrentTime">
</audio> </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 <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" 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"> <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>
<RouterLink to="/playroom"> <RouterLink to="/playroom">
<div class="flex items-center w-32 h-9"> <div class="flex items-center w-32 h-9">
<span class="truncate">{{ getCurrentTrack().song.name }}</span> <span class="truncate">{{ getCurrentTrack()?.song.name }}</span>
</div> </div>
</RouterLink> </RouterLink>
@ -248,9 +355,10 @@ function getCurrentTrack() {
<div v-if="playQueueStore.isPlaying"> <div v-if="playQueueStore.isPlaying">
<LoadingIndicator v-if="playQueueStore.isBuffering === true" :size="4" /> <LoadingIndicator v-if="playQueueStore.isBuffering === true" :size="4" />
<div v-else class="h-4 flex justify-center items-center gap-[.125rem]"> <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"
height: `${Math.max(10, bar)}%` :key="index" :style="{
}" /> height: `${Math.max(10, bar)}%`
}" />
</div> </div>
</div> </div>
<PlayIcon v-else :size="4" /> <PlayIcon v-else :size="4" />

View File

@ -1,9 +1,9 @@
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { ref } from "vue" import { ref, computed } from "vue"
export const usePlayQueueStore = defineStore('queue', () =>{ export const usePlayQueueStore = defineStore('queue', () => {
const list = ref<QueueItem[]>([]) const list = ref<QueueItem[]>([])
const currentIndex = ref<number>(0) const currentIndex = ref<number>(0)
const isPlaying = ref<boolean>(false) const isPlaying = ref<boolean>(false)
const queueReplaceLock = ref<boolean>(false) const queueReplaceLock = ref<boolean>(false)
const isBuffering = ref<boolean>(false) const isBuffering = ref<boolean>(false)
@ -21,5 +21,183 @@ export const usePlayQueueStore = defineStore('queue', () =>{
}) })
const shuffleCurrent = ref<boolean | undefined>(undefined) 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
}
}) })