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": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"debug": "^4.4.1",
|
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.7",
|
||||||
|
@ -22,9 +21,11 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/chrome": "^0.0.323",
|
"@types/chrome": "^0.0.323",
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^22.15.21",
|
||||||
"@types/webextension-polyfill": "^0.12.3",
|
"@types/webextension-polyfill": "^0.12.3",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"debug": "^4.4.1",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.1",
|
"vite": "^6.0.1",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^2.1.10"
|
||||||
|
@ -1246,6 +1247,16 @@
|
||||||
"@types/har-format": "*"
|
"@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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||||
|
@ -1276,6 +1287,13 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.15.21",
|
"version": "22.15.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
||||||
|
@ -1618,6 +1636,7 @@
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
|
@ -2317,6 +2336,7 @@
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/muggle-string": {
|
"node_modules/muggle-string": {
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"debug": "^4.4.1",
|
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.7",
|
||||||
|
@ -34,9 +33,11 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/chrome": "^0.0.323",
|
"@types/chrome": "^0.0.323",
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^22.15.21",
|
||||||
"@types/webextension-polyfill": "^0.12.3",
|
"@types/webextension-polyfill": "^0.12.3",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"debug": "^4.4.1",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.1",
|
"vite": "^6.0.1",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^2.1.10"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import MiniPlayer from './components/MiniPlayer.vue'
|
import MiniPlayer from './components/MiniPlayer.vue'
|
||||||
import PreferencePanel from './components/PreferencePanel.vue'
|
import PreferencePanel from './components/PreferencePanel.vue'
|
||||||
|
import Player from './components/Player.vue'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import LeftArrowIcon from './assets/icons/leftarrow.vue'
|
import LeftArrowIcon from './assets/icons/leftarrow.vue'
|
||||||
|
@ -77,6 +78,7 @@ watch(
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<MiniPlayer />
|
<MiniPlayer />
|
||||||
|
<Player />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -140,7 +140,15 @@ watch(
|
||||||
const playQueue = usePlayQueueStore()
|
const playQueue = usePlayQueueStore()
|
||||||
|
|
||||||
async function playTheAlbum(from: number = 0) {
|
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() {
|
function shuffle() {
|
||||||
|
|
|
@ -1,543 +1,54 @@
|
||||||
<!-- Player.vue - 添加预加载功能 -->
|
|
||||||
<script setup lang="ts">
|
<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 { 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'
|
const playQueue = usePlayQueueStore()
|
||||||
import PlayIcon from '../assets/icons/play.vue'
|
|
||||||
import PauseIcon from '../assets/icons/pause.vue'
|
|
||||||
import {
|
|
||||||
audioVisualizer,
|
|
||||||
checkAndRefreshSongResource,
|
|
||||||
supportsWebAudioVisualization,
|
|
||||||
} from '../utils'
|
|
||||||
|
|
||||||
const playQueueStore = usePlayQueueStore()
|
const resourcesUrl = ref<{ [key: string]: string }>({})
|
||||||
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 或不支持的浏览器)')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听播放列表变化
|
// 监听播放列表变化
|
||||||
watch(
|
watch(() => playQueue.queue, async () => {
|
||||||
() => playQueueStore.list.length,
|
debugPlayer(playQueue.queue)
|
||||||
async (newLength) => {
|
let newResourcesUrl: { [key: string]: string } = {}
|
||||||
console.log('[Player] 播放列表长度变化:', newLength)
|
for (const track of playQueue.queue) {
|
||||||
if (newLength === 0) {
|
const res = await apis.getSong(track.song.cid)
|
||||||
console.log('[Player] 播放列表为空,跳过连接')
|
newResourcesUrl[track.song.cid] = track.song.sourceUrl
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
debugPlayer(newResourcesUrl)
|
||||||
|
resourcesUrl.value = newResourcesUrl
|
||||||
|
})
|
||||||
|
|
||||||
// 等待下一帧,确保 audio 元素已经渲染
|
// 判断目前播放状态
|
||||||
await nextTick()
|
function isAutoPlay(cid: string) {
|
||||||
|
// 为了提前缓存播放队列中的歌曲,同时消除两首歌切换时的间隙,因此改用了新的方式来在网页上挂载音频
|
||||||
|
// 现在会将队列中每一首歌曲都挂载一个单独的 <audio> 并添加 preload="auto" 属性
|
||||||
|
// 这样就可以利用浏览器的内置行为来提前缓存队列中的所有歌曲了
|
||||||
|
// 不过,这样就会导致判断到底哪一首歌需要播放就成了难题
|
||||||
|
// 因此就有了这个函数,用于判断哪一个 <audio> 元素需要进行播放
|
||||||
|
// 此函数主要用于 <audio> 元素的 autoplay 属性,以便在专辑或歌单页面点击播放按钮时直接开始播放音乐
|
||||||
|
|
||||||
if (player.value) {
|
// 先判断是否正在播放
|
||||||
if (isAudioVisualizationSupported) {
|
if (!playQueue.isPlaying) return false
|
||||||
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
|
// 再判断是否是目前曲目
|
||||||
|
if (playQueue.currentTrack.song.cid !== cid) return false
|
||||||
|
|
||||||
// 开始预加载第一首歌的下一首
|
return true
|
||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听音量变化事件
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<audio :src="currentAudioSrc" ref="playerRef" :autoplay="playQueueStore.isPlaying"
|
<div class="text-white" v-for="track in playQueue.queue" :key="track.song.cid">
|
||||||
v-if="playQueueStore.list.length !== 0" @volumechange="handleVolumeChange" @ended="() => {
|
<audio
|
||||||
if (playQueueStore.playMode.repeat === 'single') { playQueueStore.isPlaying = true }
|
v-if="resourcesUrl[track.song.cid]"
|
||||||
else { playNext() }
|
:src="resourcesUrl[track.song.cid]"
|
||||||
}" @pause="playQueueStore.isPlaying = false" @play="playQueueStore.isPlaying = true" @playing="() => {
|
preload="auto"
|
||||||
console.log('[Player] 音频开始播放事件')
|
:ref="`audio-${track.song.cid}`"
|
||||||
playQueueStore.isBuffering = false
|
:autoplay="isAutoPlay(track.song.cid)"
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
|
@ -1,15 +1,15 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import apis from '../apis'
|
|
||||||
|
|
||||||
export const usePlayQueueStore = defineStore('queue', () => {
|
export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
// 内部状态
|
// 内部状态
|
||||||
const queue = ref<QueueItem[]>([])
|
const queue = ref<QueueItem[]>([])
|
||||||
const isShuffle = ref<boolean>(false)
|
const isShuffle = ref(false)
|
||||||
const loopingMode = ref<'single' | 'all' | 'off'>('off')
|
const loopingMode = ref<'single' | 'all' | 'off'>('off')
|
||||||
const queueReplaceLock = ref<boolean>(false)
|
const queueReplaceLock = ref(false)
|
||||||
const currentPlaying = ref<number>(0) // 当前播放指针,指针在 queueOrder 中寻址(无论是否开启了随机播放)
|
const currentPlaying = ref(0) // 当前播放指针,指针在 queueOrder 中寻址(无论是否开启了随机播放)
|
||||||
const queueOrder = ref<number[]>([]) // 播放队列顺序
|
const queueOrder = ref<number[]>([]) // 播放队列顺序
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
|
||||||
// 暴露给外部的响应式只读引用
|
// 暴露给外部的响应式只读引用
|
||||||
const queueState = computed(() =>
|
const queueState = computed(() =>
|
||||||
|
@ -27,12 +27,15 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
return queue.value[actualIndex] || null
|
return queue.value[actualIndex] || null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取当前是否正在播放
|
||||||
|
const playingState = computed(() => isPlaying.value)
|
||||||
|
|
||||||
/************
|
/************
|
||||||
* 播放队列相关
|
* 播放队列相关
|
||||||
***********/
|
***********/
|
||||||
// 使用新队列替换老队列
|
// 使用新队列替换老队列
|
||||||
// 队列替换锁开启时启用确认,确认后重置该锁
|
// 队列替换锁开启时启用确认,确认后重置该锁
|
||||||
async function replaceQueue(songs: Song[]) {
|
async function replaceQueue(newQueue: QueueItem[]) {
|
||||||
if (queueReplaceLock.value) {
|
if (queueReplaceLock.value) {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
|
@ -45,27 +48,6 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
queueReplaceLock.value = false
|
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
|
queue.value = newQueue
|
||||||
|
|
||||||
|
@ -78,6 +60,17 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
loopingMode.value = 'off'
|
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 // 状态未改变
|
if (newShuffleState === isShuffle.value) return // 状态未改变
|
||||||
|
|
||||||
|
// TODO: 进行洗牌
|
||||||
/* if (newShuffleState) {
|
/* if (newShuffleState) {
|
||||||
// 开启随机播放:保存当前顺序并打乱
|
// 开启随机播放:保存当前顺序并打乱
|
||||||
const originalOrder = [...queueOrder.value]
|
const originalOrder = [...queueOrder.value]
|
||||||
|
@ -154,10 +148,12 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
loopMode: loopModeState,
|
loopMode: loopModeState,
|
||||||
currentTrack,
|
currentTrack,
|
||||||
currentIndex: currentPlaying,
|
currentIndex: currentPlaying,
|
||||||
|
isPlaying: playingState,
|
||||||
|
|
||||||
// 修改方法
|
// 修改方法
|
||||||
replaceQueue,
|
replaceQueue,
|
||||||
toggleShuffle,
|
toggleShuffle,
|
||||||
toggleLoop,
|
toggleLoop,
|
||||||
|
togglePlay
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue
Block a user