更改socketApi封装

main
落雨楓 3 months ago
parent 6d845164ca
commit 52ea5a936e

@ -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"

@ -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

@ -1,41 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

@ -1,10 +1,10 @@
<script lang="ts" setup>
import { inject, nextTick, onBeforeUnmount, onMounted, reactive, watch } from 'vue';
import { socket } from '../api/socketApi';
import { nextTick, onBeforeUnmount, onMounted, reactive, watch } from 'vue';
import { socketApi } from '../api/socketApi';
import { ref } from 'vue';
import DPlayer, { DPlayerEvents, type DPlayerAPIBackend, type DPlayerDanmaku, type DPlayerVideo, type DPlayerVideoQuality } from 'dplayer';
import DPlayer, { DPlayerEvents, type DPlayerAPIBackend, type DPlayerVideo, type DPlayerVideoQuality } from 'dplayer';
import type { UserInfo } from '../types/user';
import type { PlayerState, ClientUserStatus, RoomState, ClientMessage, RoomStateChangedMessage, SetTimeMessage } from '../types/message';
import type { PlayerState, ClientUserStatus, RoomState, RoomStateChangedMessage, SetTimeMessage } from '../types/message';
import { arrayEquals, getUserPlayTimeSeconds, unixTimestampWithOffset } from '../utils/utils';
import { useToast } from 'primevue/usetoast';
@ -49,12 +49,10 @@ const handleUserStateUpdated = (force: boolean = false) => {
playing: isPlaying
}
socket.emit("updateUserState", {
username: props.userInfo?.username,
room: props.roomId,
socketApi.updateUserState({
playTime: playerTime,
playing: isPlaying,
} as ClientMessage)
})
}
};
@ -65,11 +63,9 @@ const handleSeeked = () => {
const currentTime = Math.floor(player.video.currentTime)
console.log('seeked to', currentTime)
if (props.userInfo?.isAdmin) {
socket.emit("setTime", {
username: props.userInfo.username,
socketApi.setTime({
playTime: currentTime,
room: props.roomId
} as ClientMessage)
})
}
}
@ -93,16 +89,10 @@ const handlePlay = () => {
//
if (props.userInfo?.isAdmin) {
socket.emit("play", {
username: props.userInfo.username,
room: props.roomId
} as ClientMessage)
socket.emit("setTime", {
socketApi.sendPlay()
socketApi.setTime({
playTime: Math.floor(player.video.currentTime),
room: props.roomId,
username: props.userInfo.username,
} as ClientMessage)
})
} else {
//
if (!prevRoomState.playing) {
@ -128,10 +118,10 @@ const handlePause = () => {
console.log('paused')
if (props.userInfo?.isAdmin) {
socket.emit("pause", {
username: props.userInfo.username,
room: props.roomId
} as ClientMessage)
socketApi.sendPause()
socketApi.setTime({
playTime: Math.floor(player.video.currentTime),
})
}
handleUserStateUpdated(true)
@ -139,7 +129,6 @@ const handlePause = () => {
const onDisconnect = (e: any, d: any) => {
console.log('disconnected, reason', e, d)
socket.connect()
}
const onRoomInfo = (d: any) => {
@ -249,21 +238,17 @@ const initOrUpdatePlayer = async () => {
},
send: (options: any) => {
const danmakuData = options.data
socket.emit("danmaku", {
room: props.roomId,
username: props.userInfo?.username,
danmaku: {
type: danmakuData.type,
color: danmakuData.color,
text: danmakuData.text,
}
} as ClientMessage)
socketApi.sendDanmaku({
type: danmakuData.type,
color: danmakuData.color,
text: danmakuData.text,
})
options.success()
},
}
if (!player) {
for (let i = 0; i < 5; i ++) {
for (let i = 0; i < 5; i++) {
if (!playerDiv.value) {
await nextTick()
}
@ -315,11 +300,11 @@ watch(() => playerState.videoUrls, (newUrls) => {
let updateUserStateInterval: number | undefined;
onMounted(() => {
socket.on("disconnect", onDisconnect)
socket.on("roomInfo", onRoomInfo)
socket.on("pause", onPause)
socket.on("play", onPlay)
socket.on("setTime", onSetTime)
socketApi.on("disconnect", onDisconnect)
socketApi.on("roomInfo", onRoomInfo)
socketApi.on("pause", onPause)
socketApi.on("play", onPlay)
socketApi.on("setTime", onSetTime)
updateUserStateInterval = window.setInterval(() => {
handleUserStateUpdated()
@ -327,11 +312,11 @@ onMounted(() => {
})
onBeforeUnmount(() => {
socket.off("disconnect", onDisconnect)
socket.off("roomInfo", onRoomInfo)
socket.off("pause", onPause)
socket.off("play", onPlay)
socket.off("setTime", onSetTime)
socketApi.off("disconnect", onDisconnect)
socketApi.off("roomInfo", onRoomInfo)
socketApi.off("pause", onPause)
socketApi.off("play", onPlay)
socketApi.off("setTime", onSetTime)
updateUserStateInterval && clearInterval(updateUserStateInterval)
@ -342,9 +327,7 @@ onBeforeUnmount(() => {
</script>
<template>
<div
ref="playerDiv"
class="w-full aspect-video bg-slate-900 text-white font-sans overflow-hidden rounded-md">
<div ref="playerDiv" class="w-full aspect-video bg-slate-900 text-white font-sans overflow-hidden rounded-md">
<div class="w-full h-full flex items-center justify-center">
<span class="text-gray-500 text-2xl">正在等待房管设置视频</span>
</div>

@ -1,9 +1,9 @@
<script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, provide, reactive, ref, toRef } from 'vue';
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { useRoute } from 'vue-router';
import type { UserInfo } from '../types/user';
import type { ClientMessage, RoomStateChangedMessage, ServerNotificationMessage, UserJoinLeaveMessage } from '../types/message';
import { socket } from '../api/socketApi';
import type { ClientMessage, RoomStateChangedMessage, ServerNotificationMessage } from '../types/message';
import { socketApi } from '../api/socketApi';
import { arrayEquals } from '../utils/utils';
import { useToast } from 'primevue/usetoast';
import Toast from 'primevue/toast';
@ -45,19 +45,34 @@ const state = reactive({
serverTimeAdjust: 0,
})
const onConnect = () => {
const onConnect = async () => {
console.log('socket connected')
socket.emit("init", {
room: roomId.value,
} as ClientMessage)
try {
await socketApi.init({
room: roomId.value,
})
} catch (err: any) {
toast.add({ severity: 'error', summary: '错误', detail: `连接服务器失败: ${err.message}`, life: 5000 })
console.error('init socket failed', err)
return
}
if (state.isJoined) {
//
socket.emit('join', {
username: userInfo.username,
room: roomId.value,
password: userInfo.password,
} as ClientMessage)
try {
var result = await socketApi.joinRoom({
username: userInfo.username,
room: roomId.value,
password: userInfo.password,
})
state.isJoined = true
userInfo.isAdmin = result.data?.isAdmin || false
console.log('re-joined room', result)
} catch (err: any) {
toast.add({ severity: 'error', summary: '错误', detail: `重新加入房间失败: ${err.message}`, life: 5000 })
console.error('re-join room failed', err)
state.isJoined = false
}
}
}
@ -92,16 +107,6 @@ const onRoomInfo = (d: any) => {
prevRoomState = d;
}
const onJoined = (d: any) => {
const msg = d as UserJoinLeaveMessage
console.log('user joined', msg)
if (!msg.userInfo) {
return
}
state.isJoined = true
userInfo.isAdmin = msg.userInfo.isAdmin
}
const onServerMessage = (d: any) => {
const msg = d as ServerNotificationMessage;
switch (msg.severity) {
@ -117,21 +122,33 @@ const onServerMessage = (d: any) => {
}
}
const handleJoinClick = () => {
socket.connect()
socket.emit('join', {
username: userInfo.username,
room: roomId.value,
password: userInfo.password,
} as ClientMessage)
const handleJoinClick = async () => {
try {
const result = await socketApi.joinRoom({
username: userInfo.username,
room: roomId.value,
password: userInfo.password,
} as ClientMessage)
state.isJoined = true
userInfo.isAdmin = result.data?.isAdmin || false
console.log('joined room', result)
} catch (err: any) {
toast.add({ severity: 'error', summary: '错误', detail: `加入房间失败: ${err.message}`, life: 5000 })
console.error('join room failed', err)
}
}
const handleSetUrlClick = () => {
socket.emit('setUrl', {
room: roomId.value,
username: userInfo.username,
videoUrls: state.urlList,
} as ClientMessage)
const handleSetUrlClick = async () => {
try {
await socketApi.setUrl({
room: roomId.value,
username: userInfo.username,
videoUrls: state.urlList,
} as ClientMessage)
} catch (err: any) {
toast.add({ severity: 'error', summary: '错误', detail: `设置视频链接失败: ${err.message}`, life: 5000 })
console.error('set url failed', err)
}
}
const isAllowedLogin = computed(() => {
@ -146,21 +163,19 @@ const isAllowedLogin = computed(() => {
})
onMounted(() => {
socket.on('connect', onConnect)
socket.on('roomInfo', onRoomInfo)
socket.on('joined', onJoined)
socket.on('message', onServerMessage)
socketApi.on('connect', onConnect)
socketApi.on('roomInfo', onRoomInfo)
socketApi.on('message', onServerMessage)
if (socket.connected) {
if (socketApi.socket?.connected) {
onConnect()
}
})
onBeforeUnmount(() => {
socket.off('connect', onConnect)
socket.off('roomInfo', onRoomInfo)
socket.off('joined', onJoined)
socket.off('message', onServerMessage)
socketApi.off('connect', onConnect)
socketApi.off('roomInfo', onRoomInfo)
socketApi.off('message', onServerMessage)
})
</script>

@ -1,73 +1,73 @@
export interface DanmakuData {
type: number; // 弹幕类型1-滚动2-顶部3-底部
color: string; // 弹幕颜色,十六进制字符串,如 #FFFFFF
text: string; // 弹幕内容
type: number // 弹幕类型1-滚动2-顶部3-底部
color: string // 弹幕颜色,十六进制字符串,如 #FFFFFF
text: string // 弹幕内容
}
export interface ClientMessage {
username?: string;
room?: string;
videoUrls?: string[];
playTime?: number;
playing?: boolean;
payload?: string;
danmaku?: DanmakuData;
username?: string
room?: string
videoUrls?: string[]
playTime?: number
playing?: boolean
payload?: string
danmaku?: DanmakuData
}
export interface ServerMessage {
type: string;
type: string
}
export type RoomStateChangedMessage = ServerMessage & RoomState;
export type RoomStateChangedMessage = ServerMessage & RoomState
export interface UserJoinLeaveMessage extends ServerMessage {
userInfo?: UserStatus;
userInfo?: UserStatus
}
export interface UserActionMessage extends ServerMessage {
fromUser?: UserStatus;
fromUser?: UserStatus
}
export interface SetTimeMessage extends UserActionMessage {
playTime?: number;
playTime?: number
}
export interface ServerNotificationMessage extends ServerMessage {
severity: 'info' | 'warning' | 'error';
message: string;
title?: string;
severity: 'info' | 'warning' | 'error'
message: string
title?: string
}
export interface RoomState {
/** 播放地址 */
videoUrls?: string[];
videoUrls?: string[]
/** 服务器时间 */
serverTime?: number;
serverTime?: number
/** 播放时间更新时间 */
playTimeUpdateTime?: number;
playTimeUpdateTime?: number
/** 播放时间 */
playTime?: number;
playTime?: number
/** 是否正在播放 */
playing?: boolean;
playing?: boolean
/** 当前房间状态的用户状态 */
userList?: UserStatus[];
userList?: UserStatus[]
}
export interface ClientUserStatus {
playTime?: number;
updateTime?: number;
playing?: boolean;
playTime?: number
updateTime?: number
playing?: boolean
}
export interface UserStatus extends ClientUserStatus {
userID?: string;
username?: string;
isAdmin?: boolean;
userID?: string
username?: string
isAdmin?: boolean
}
export interface PlayerState {
paused?: boolean;
videoUrls?: string[];
playTime?: number;
inited?: boolean;
paused?: boolean
videoUrls?: string[]
playTime?: number
inited?: boolean
}
Loading…
Cancel
Save