|
|
|
|
@ -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,
|
|
|
|
|
})
|
|
|
|
|
export type DisconnectDescription = Error | {
|
|
|
|
|
description: string;
|
|
|
|
|
context?: unknown;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 基础响应类型
|
|
|
|
|
export interface BaseResponse<T = any> {
|
|
|
|
|
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<SocketApiEvents> = new EventEmitter<SocketApiEvents>()
|
|
|
|
|
public on = this.eventEmitter.on.bind(this.eventEmitter)
|
|
|
|
|
public off = this.eventEmitter.off.bind(this.eventEmitter)
|
|
|
|
|
|
|
|
|
|
private pendingRequests: Map<string, PendingRequest> = 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<T = BaseResponse>(eventName: string, data?: any): Promise<T> {
|
|
|
|
|
return new Promise<T>((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<BaseResponse> {
|
|
|
|
|
return this.sendRequest('init', data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 加入房间
|
|
|
|
|
*/
|
|
|
|
|
public joinRoom(data: ClientJoinMessage): Promise<BaseResponse<UserStatus>> {
|
|
|
|
|
return this.sendRequest('join', data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取房间信息
|
|
|
|
|
*/
|
|
|
|
|
public getRoomInfo(): Promise<BaseResponse> {
|
|
|
|
|
return this.sendRequest('getRoomInfo')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新用户状态(播放时间和播放状态)
|
|
|
|
|
*/
|
|
|
|
|
public updateUserState(data: ClientUpdateUserStateMessage): Promise<BaseResponse> {
|
|
|
|
|
return this.sendRequest('updateUserState', data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置视频URL(房管权限)
|
|
|
|
|
*/
|
|
|
|
|
public setUrl(data: ClientSetUrlMessage): Promise<BaseResponse> {
|
|
|
|
|
return this.sendRequest('setUrl', data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 播放视频(房管权限)
|
|
|
|
|
*/
|
|
|
|
|
public sendPlay(): Promise<BaseResponse> {
|
|
|
|
|
return this.sendRequest('play')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 暂停视频(房管权限)
|
|
|
|
|
*/
|
|
|
|
|
public sendPause(): Promise<BaseResponse> {
|
|
|
|
|
return this.sendRequest('pause')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置播放时间(房管权限)
|
|
|
|
|
*/
|
|
|
|
|
public setTime(data: ClientSetTimeMessage): Promise<BaseResponse> {
|
|
|
|
|
return this.sendRequest('setTime', data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 发送弹幕
|
|
|
|
|
*/
|
|
|
|
|
public sendDanmaku(data: ClientDanmakuMessage): Promise<BaseResponse> {
|
|
|
|
|
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
|