1. 播放器

This commit is contained in:
李琦
2026-08-01 17:59:07 +08:00
parent 366e3b329f
commit d824b9945e
9 changed files with 621 additions and 67 deletions

View File

@@ -2,52 +2,70 @@ import { ref, computed, onUnmounted } from 'vue'
/**
* 背景音乐控制 composable支持多曲目
* - tracks: [{ url, name }] 播放列表
* - tracks: [{ url, name, coverUrl?, lrcUrl? }]
* - activeUrl: 当前选中的曲目地址
* - 处理浏览器自动播放限制:首次用户交互后再尝试播放
*/
export function useAudio() {
const isPlaying = ref(false)
const isReady = ref(false)
const tracks = ref([]) // [{ url, name }]
const activeUrl = ref('') // 当前选中曲目 url
const tracks = ref([])
const activeUrl = ref('')
const currentTime = ref(0)
const duration = ref(0)
const volume = ref(0.5)
let audio = null
let unlockHandler = null
const currentName = computed(() => {
const t = tracks.value.find((t) => t.url === activeUrl.value)
return t?.name || '背景音乐'
})
const currentTrack = computed(() => tracks.value.find((t) => t.url === activeUrl.value) || null)
const currentName = computed(() => currentTrack.value?.name || '背景音乐')
const currentCover = computed(() => currentTrack.value?.coverUrl || '')
const currentLrcUrl = computed(() => currentTrack.value?.lrcUrl || '')
const syncMeta = () => {
if (!audio) return
currentTime.value = audio.currentTime || 0
duration.value = Number.isFinite(audio.duration) ? audio.duration : 0
volume.value = audio.volume
}
const ensureAudio = () => {
if (audio) return audio
audio = new Audio()
audio.loop = true
audio.volume = 0.5
audio.volume = volume.value
audio.preload = 'auto'
audio.addEventListener('canplay', () => (isReady.value = true))
audio.addEventListener('play', () => (isPlaying.value = true))
audio.addEventListener('pause', () => (isPlaying.value = false))
audio.addEventListener('canplay', () => {
isReady.value = true
syncMeta()
})
audio.addEventListener('loadedmetadata', syncMeta)
audio.addEventListener('timeupdate', syncMeta)
audio.addEventListener('play', () => { isPlaying.value = true })
audio.addEventListener('pause', () => { isPlaying.value = false })
return audio
}
const playUrl = async (url) => {
if (!url) return false
const a = ensureAudio()
if (a.src !== url) a.src = url
if (a.src !== url) {
a.src = url
currentTime.value = 0
duration.value = 0
}
activeUrl.value = url
try {
await a.play()
isPlaying.value = true
syncMeta()
return true
} catch (e) {
// 被浏览器拦截,等待用户手势
} catch {
isPlaying.value = false
return false
}
}
// 设置播放列表与默认选中项
const setTracks = (list, active) => {
tracks.value = Array.isArray(list) ? list.slice().filter((t) => t && t.url) : []
const first = tracks.value[0]?.url || ''
@@ -56,16 +74,18 @@ export function useAudio() {
if (audio && isPlaying.value && chosen) playUrl(chosen)
}
// 切换当前播放曲目(若正在播放则立即切换)
const setActive = (url) => {
if (!tracks.value.some((t) => t.url === url)) return
if (isPlaying.value) playUrl(url)
else activeUrl.value = url
else {
activeUrl.value = url
const a = ensureAudio()
if (a.src !== url) a.src = url
}
}
const play = () => playUrl(activeUrl.value)
// 尝试自动播放,返回是否成功(被浏览器拦截则返回 false由调用方决定是否弹授权框
const attemptAutoplay = async () => {
if (!activeUrl.value) return false
return playUrl(activeUrl.value)
@@ -80,7 +100,22 @@ export function useAudio() {
else play()
}
// 首次交互解锁自动播放
const seek = (seconds) => {
const a = ensureAudio()
if (!Number.isFinite(seconds)) return
const d = Number.isFinite(a.duration) ? a.duration : duration.value
const next = Math.max(0, Math.min(d || seconds, seconds))
a.currentTime = next
currentTime.value = next
}
const setVolume = (v) => {
const next = Math.max(0, Math.min(1, Number(v) || 0))
volume.value = next
const a = ensureAudio()
a.volume = next
}
const armAutoplay = () => {
const handler = async () => {
const ok = await play()
@@ -107,5 +142,26 @@ export function useAudio() {
}
})
return { isPlaying, isReady, tracks, activeUrl, currentName, setTracks, setActive, play, pause, toggle, attemptAutoplay, armAutoplay }
return {
isPlaying,
isReady,
tracks,
activeUrl,
currentName,
currentTrack,
currentCover,
currentLrcUrl,
currentTime,
duration,
volume,
setTracks,
setActive,
play,
pause,
toggle,
seek,
setVolume,
attemptAutoplay,
armAutoplay,
}
}