Files
BYW-VoiceHub/VoiceHub-main/app/composables/useMusicWebSocket.ts
T
Henry_Du 4ce4649aaf
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s
初次提交
2026-09-06 08:19:02 +08:00

248 lines
5.8 KiB
TypeScript

import { onBeforeUnmount, onMounted, ref, readonly } from 'vue'
interface MusicState {
songId?: number
isPlaying: boolean
position: number
duration: number
volume: number
playlistIndex?: number
timestamp: number
}
interface SongInfo {
songId?: number
title: string
artist: string
cover: string
duration: number
playlistIndex?: number
timestamp: number
}
export function useMusicWebSocket() {
const isConnected = ref(false)
const connectionId = ref<string | null>(null)
const lastHeartbeat = ref<number>(0)
let eventSource: EventSource | null = null
let reconnectTimer: NodeJS.Timeout | null = null
let heartbeatTimer: NodeJS.Timeout | null = null
// 事件回调
const onStateUpdate = ref<((state: MusicState) => void) | null>(null)
const onSongChange = ref<((songInfo: SongInfo) => void) | null>(null)
const onPlaylistUpdate = ref<((playlist: any[]) => void) | null>(null)
const onConnectionChange = ref<((connected: boolean) => void) | null>(null)
// 连接WebSocket
const connect = (token?: string) => {
if (eventSource) {
disconnect()
}
try {
const url = new URL('/api/music/websocket', window.location.origin)
if (token) {
url.searchParams.set('token', token)
}
eventSource = new EventSource(url.toString())
eventSource.onopen = () => {
isConnected.value = true
onConnectionChange.value?.(true)
// 清除重连定时器
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
}
eventSource.onmessage = (event) => {
try {
const message = JSON.parse(event.data)
handleMessage(message)
} catch (error) {
console.error('Failed to parse WebSocket message:', error)
}
}
eventSource.onerror = (error) => {
console.error('Music WebSocket error:', error)
isConnected.value = false
onConnectionChange.value?.(false)
// 自动重连
scheduleReconnect()
}
} catch (error) {
console.error('Failed to create WebSocket connection:', error)
scheduleReconnect()
}
}
// 处理消息
const handleMessage = (message: any) => {
const { type, data } = message
switch (type) {
case 'connection_established':
connectionId.value = data.connectionId
break
case 'music_state_update':
onStateUpdate.value?.(data)
break
case 'song_change':
onSongChange.value?.(data)
break
case 'playlist_update':
onPlaylistUpdate.value?.(data.playlist)
break
case 'heartbeat':
lastHeartbeat.value = data.timestamp
break
default:
}
}
// 断开连接
const disconnect = () => {
if (eventSource) {
eventSource.close()
eventSource = null
}
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
if (heartbeatTimer) {
clearTimeout(heartbeatTimer)
heartbeatTimer = null
}
isConnected.value = false
connectionId.value = null
onConnectionChange.value?.(false)
}
// 计划重连
const scheduleReconnect = () => {
if (reconnectTimer) return
reconnectTimer = setTimeout(() => {
connect()
}, 5000) // 5秒后重连
}
// 发送音乐状态更新
const sendStateUpdate = async (state: Partial<MusicState>) => {
try {
await $fetch('/api/music/state', {
method: 'POST',
body: {
type: 'state_update',
data: state
}
})
} catch (error) {
console.error('Failed to send music state update:', error)
}
}
// 发送歌曲切换通知
const sendSongChange = async (songInfo: Partial<SongInfo>) => {
try {
await $fetch('/api/music/state', {
method: 'POST',
body: {
type: 'song_change',
data: songInfo
}
})
} catch (error) {
console.error('Failed to send song change:', error)
}
}
// 发送播放位置更新
const sendPositionUpdate = async (position: number, duration: number, songId?: number) => {
try {
await $fetch('/api/music/state', {
method: 'POST',
body: {
type: 'position_update',
data: {
songId,
position,
duration,
isPlaying: true
}
}
})
} catch (error) {
console.error('Failed to send position update:', error)
}
}
// 设置事件监听器
const setStateUpdateListener = (callback: (state: MusicState) => void) => {
onStateUpdate.value = callback
}
const setSongChangeListener = (callback: (songInfo: SongInfo) => void) => {
onSongChange.value = callback
}
const setPlaylistUpdateListener = (callback: (playlist: any[]) => void) => {
onPlaylistUpdate.value = callback
}
const setConnectionChangeListener = (callback: (connected: boolean) => void) => {
onConnectionChange.value = callback
}
// 生命周期管理
onMounted(() => {
// 启动心跳检测
heartbeatTimer = setInterval(() => {
if (isConnected.value && lastHeartbeat.value > 0) {
const timeSinceLastHeartbeat = Date.now() - lastHeartbeat.value
if (timeSinceLastHeartbeat > 60000) {
// 60秒没有心跳
console.warn('Music WebSocket heartbeat timeout, reconnecting...')
disconnect()
scheduleReconnect()
}
}
}, 30000) // 每30秒检查一次
})
onBeforeUnmount(() => {
disconnect()
})
return {
isConnected: readonly(isConnected),
connectionId: readonly(connectionId),
connect,
disconnect,
sendStateUpdate,
sendSongChange,
sendPositionUpdate,
setStateUpdateListener,
setSongChangeListener,
setPlaylistUpdateListener,
setConnectionChangeListener
}
}