diff --git a/entities/message.go b/entities/message.go index bbd7963..476a2d1 100644 --- a/entities/message.go +++ b/entities/message.go @@ -32,6 +32,13 @@ type ClientUpdateUserStateMessage struct { Playing bool `json:"playing,omitempty"` } +type ClientDanmakuMessage struct { + ClientMessage + Text string `json:"text,omitempty"` + Type int `json:"type,omitempty"` // 弹幕类型,1-滚动,2-顶部,3-底部 + Color int `json:"color,omitempty"` // 弹幕颜色,对应16进制颜色值,如白色为0xFFFFFF +} + type ResponseMessage struct { RequestID string `json:"rid,omitempty"` Code int `json:"code,omitempty"` // 200 for success @@ -94,6 +101,7 @@ type DanmakuData struct { } var MessageTypeRoomInfo = "roomInfo" +var MessageTypeJoined = "joined" var MessageTypeUserJoin = "userJoin" var MessageTypeUserLeave = "userLeave" var MessageTypeMessage = "message" diff --git a/frontend/src/api/socketApi.ts b/frontend/src/api/socketApi.ts index 4c0db51..7d5e53a 100644 --- a/frontend/src/api/socketApi.ts +++ b/frontend/src/api/socketApi.ts @@ -1,6 +1,391 @@ -import { io } from "socket.io-client"; +import EventEmitter from 'eventemitter3' +import io, { Socket } from 'socket.io-client' -export const socket = io({ - transports: ["websocket"], - autoConnect: true, -}) \ No newline at end of file +export type DisconnectDescription = Error | { + description: string; + context?: unknown; +}; + +// 基础响应类型 +export interface BaseResponse { + rid?: string + code: number + message: string + data?: T +} + +// 房间状态类型 +export interface RoomState { + /** 播放地址 */ + videoUrls?: string[] + /** 服务器时间 */ + serverTime?: number + /** 播放时间更新时间 */ + playTimeUpdateTime?: number + /** 播放时间 */ + playTime?: number + /** 是否正在播放 */ + playing?: boolean + /** 当前房间状态的用户状态 */ + userList?: UserStatus[] +} + +// 用户状态类型 +export interface UserStatus { + userID?: string + username?: string + isAdmin?: boolean + playTime?: number + updateTime?: number + playing?: boolean +} + +// 客户端消息类型 +export interface ClientInitMessage { + room?: string +} + +export interface ClientJoinMessage { + username?: string + room?: string + password?: string +} + +export interface ClientSetUrlMessage { + videoUrls?: string[] +} + +export interface ClientSetTimeMessage { + playTime?: number +} + +export interface ClientUpdateUserStateMessage { + playTime?: number + playing?: boolean +} + +export interface ClientDanmakuMessage { + text: string + type: number // 弹幕类型,1-滚动,2-顶部,3-底部 + color: number // 弹幕颜色,对应16进制颜色值,如白色为0xFFFFFF +} + +// 服务器消息类型 +export interface ServerMessage { + type: string +} + +export interface RoomStateChangedMessage extends ServerMessage { + type: 'roomInfo' | 'stateChanged' + serverTime?: number + videoUrls?: string[] + playTime?: number + playing?: boolean + userList?: UserStatus[] +} + +export interface UserJoinLeaveMessage extends ServerMessage { + type: 'userJoin' | 'userLeave' + userInfo?: UserStatus +} + +export interface UserActionMessage extends ServerMessage { + type: 'play' | 'pause' + fromUser?: UserStatus +} + +export interface SetTimeMessage extends ServerMessage { + type: 'setTime' + playTime?: number + fromUser?: UserStatus +} + +export interface ServerNotificationMessage extends ServerMessage { + type: 'message' + severity: 'info' | 'warning' | 'error' + message: string + title?: string +} + +// 错误类型 +export class ServerSideError extends Error { + public data?: any + + constructor(message: string, data?: any) { + super(message) + this.name = 'ServerSideError' + this.data = data + } +} + +// 事件类型定义 +export type SocketApiEvents = { + connect: () => void + disconnect: (reason: Socket.DisconnectReason, description?: DisconnectDescription) => void + response: (data: BaseResponse) => void + roomInfo: (data: RoomStateChangedMessage) => void + stateChanged: (data: RoomStateChangedMessage) => void + userJoin: (data: UserJoinLeaveMessage) => void + userLeave: (data: UserJoinLeaveMessage) => void + play: (data: UserActionMessage) => void + pause: (data: UserActionMessage) => void + setTime: (data: SetTimeMessage) => void + setUrl: (data: RoomStateChangedMessage) => void + message: (data: ServerNotificationMessage) => void +} + +// 待处理的请求类型 +interface PendingRequest { + requestId: string + resolve: (value: any) => void + reject: (reason: Error) => void + timestamp: number + timeoutId: number +} + +export class SocketApi { + public socket: Socket | null = null + public eventEmitter: EventEmitter = new EventEmitter() + public on = this.eventEmitter.on.bind(this.eventEmitter) + public off = this.eventEmitter.off.bind(this.eventEmitter) + + private pendingRequests: Map = new Map() + private requestTimeout: number = 30000 // 30秒超时 + + public constructor() { + this.socket = io('/', { + transports: ['websocket'], + reconnectionDelay: 2000, + timeout: 10000, + }) + + this.setupSocketListeners() + } + + private setupSocketListeners(): void { + if (!this.socket) return + + // 连接事件 + this.socket.on('connect', () => { + console.log('Socket connected') + this.eventEmitter.emit('connect') + }) + + this.socket.on('disconnect', (reason, description) => { + console.log('Socket disconnected') + this.eventEmitter.emit('disconnect', reason, description) + // 清理所有待处理的请求 + this.clearAllPendingRequests() + }) + + // 服务器事件监听 + this.socket.on('response', (data: BaseResponse) => { + this.handleResponse(data) + this.eventEmitter.emit('response', data) + }) + + this.socket.on('roomInfo', (data: RoomStateChangedMessage) => { + this.eventEmitter.emit('roomInfo', data) + }) + + this.socket.on('stateChanged', (data: RoomStateChangedMessage) => { + this.eventEmitter.emit('stateChanged', data) + }) + + this.socket.on('userJoin', (data: UserJoinLeaveMessage) => { + this.eventEmitter.emit('userJoin', data) + }) + + this.socket.on('userLeave', (data: UserJoinLeaveMessage) => { + this.eventEmitter.emit('userLeave', data) + }) + + this.socket.on('play', (data: UserActionMessage) => { + this.eventEmitter.emit('play', data) + }) + + this.socket.on('pause', (data: UserActionMessage) => { + this.eventEmitter.emit('pause', data) + }) + + this.socket.on('setTime', (data: SetTimeMessage) => { + this.eventEmitter.emit('setTime', data) + }) + + this.socket.on('setUrl', (data: RoomStateChangedMessage) => { + this.eventEmitter.emit('setUrl', data) + }) + + this.socket.on('message', (data: ServerNotificationMessage) => { + this.eventEmitter.emit('message', data) + }) + } + + // 生成请求ID + private generateRequestId(): string { + return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + } + + // 发送请求的通用方法 + private sendRequest(eventName: string, data?: any): Promise { + return new Promise((resolve, reject) => { + if (!this.socket || !this.socket.connected) { + reject(new Error('Socket not connected')) + return + } + + const requestId = this.generateRequestId() + + // 设置超时 + const timeoutId = setTimeout(() => { + this.pendingRequests.delete(requestId) + reject(new Error(`Request timeout: ${eventName}`)) + }, this.requestTimeout) + + // 存储待处理的请求 + this.pendingRequests.set(requestId, { + requestId, + resolve, + reject, + timestamp: Date.now(), + timeoutId + }) + + // 准备发送的数据 + const requestData = data ? { rid: requestId, ...data } : { rid: requestId } + + // 发送请求 + this.socket.emit(eventName, requestData) + }) + } + + // 处理响应 + private handleResponse(response: BaseResponse): void { + if (!response.rid) return + + const pendingRequest = this.pendingRequests.get(response.rid) + if (pendingRequest) { + clearTimeout(pendingRequest.timeoutId) + this.pendingRequests.delete(response.rid) + + if (response.code === 200) { + pendingRequest.resolve(response) + } else { + pendingRequest.reject(new ServerSideError(response.message || '服务器内部错误', response.data)) + } + } + } + + // 清理所有待处理的请求 + private clearAllPendingRequests(): void { + this.pendingRequests.forEach(request => { + clearTimeout(request.timeoutId) + request.reject(new Error('Connection lost')) + }) + this.pendingRequests.clear() + } + + // API方法 + + /** + * 初始化房间连接 + */ + public init(data: ClientInitMessage): Promise { + return this.sendRequest('init', data) + } + + /** + * 加入房间 + */ + public joinRoom(data: ClientJoinMessage): Promise> { + return this.sendRequest('join', data) + } + + /** + * 获取房间信息 + */ + public getRoomInfo(): Promise { + return this.sendRequest('getRoomInfo') + } + + /** + * 更新用户状态(播放时间和播放状态) + */ + public updateUserState(data: ClientUpdateUserStateMessage): Promise { + return this.sendRequest('updateUserState', data) + } + + /** + * 设置视频URL(房管权限) + */ + public setUrl(data: ClientSetUrlMessage): Promise { + return this.sendRequest('setUrl', data) + } + + /** + * 播放视频(房管权限) + */ + public sendPlay(): Promise { + return this.sendRequest('play') + } + + /** + * 暂停视频(房管权限) + */ + public sendPause(): Promise { + return this.sendRequest('pause') + } + + /** + * 设置播放时间(房管权限) + */ + public setTime(data: ClientSetTimeMessage): Promise { + return this.sendRequest('setTime', data) + } + + /** + * 发送弹幕 + */ + public sendDanmaku(data: ClientDanmakuMessage): Promise { + return this.sendRequest('danmaku', data) + } + + /** + * 检查连接状态 + */ + public isConnected(): boolean { + return this.socket?.connected ?? false + } + + /** + * 设置请求超时时间 + */ + public setRequestTimeout(timeout: number): void { + this.requestTimeout = timeout + } + + /** + * 获取待处理请求数量 + */ + public getPendingRequestsCount(): number { + return this.pendingRequests.size + } + + /** + * 销毁实例 + */ + public close(): void { + this.clearAllPendingRequests() + this.eventEmitter.removeAllListeners() + if (this.socket) { + this.socket.disconnect() + this.socket = null + } + } +} + +// 导出单例实例 +export const socketApi = new SocketApi() + +// 保持原有的 socket 导出以兼容现有代码 +export const socket = socketApi.socket \ No newline at end of file diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue deleted file mode 100644 index b58e52b..0000000 --- a/frontend/src/components/HelloWorld.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/frontend/src/components/Player.vue b/frontend/src/components/Player.vue index 70b35e1..3ff8c43 100644 --- a/frontend/src/components/Player.vue +++ b/frontend/src/components/Player.vue @@ -1,10 +1,10 @@