feat(Player): implement audio preloading functionality with state management
This commit is contained in:
parent
096e74a9bd
commit
7a0c638d2c
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user