1. 播放器
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user