From 17c2338b02b617c5c0d2898ad90ac909b1becf3c Mon Sep 17 00:00:00 2001 From: mtvpls Date: Tue, 9 Dec 2025 21:16:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96socket.io=E9=87=8D=E8=BF=9E?= =?UTF-8?q?=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 29 +-- src/components/WatchRoomProvider.tsx | 51 +++++ .../watch-room/ChatFloatingWindow.tsx | 94 +++++++++- src/lib/watch-room-socket.ts | 176 +++++++++++++++++- src/types/watch-room.ts | 1 + 5 files changed, 334 insertions(+), 17 deletions(-) diff --git a/server.js b/server.js index d149e83..ebf7e94 100644 --- a/server.js +++ b/server.js @@ -321,22 +321,27 @@ class WatchRoomServer { // 心跳 socket.on('heartbeat', () => { const roomInfo = this.socketToRoom.get(socket.id); - if (!roomInfo) return; - const roomMembers = this.members.get(roomInfo.roomId); - const member = roomMembers?.get(roomInfo.userId); - if (member) { - member.lastHeartbeat = Date.now(); - roomMembers?.set(roomInfo.userId, member); - } + // 如果用户在房间中,更新心跳时间 + if (roomInfo) { + const roomMembers = this.members.get(roomInfo.roomId); + const member = roomMembers?.get(roomInfo.userId); + if (member) { + member.lastHeartbeat = Date.now(); + roomMembers?.set(roomInfo.userId, member); + } - if (roomInfo.isOwner) { - const room = this.rooms.get(roomInfo.roomId); - if (room) { - room.lastOwnerHeartbeat = Date.now(); - this.rooms.set(roomInfo.roomId, room); + if (roomInfo.isOwner) { + const room = this.rooms.get(roomInfo.roomId); + if (room) { + room.lastOwnerHeartbeat = Date.now(); + this.rooms.set(roomInfo.roomId, room); + } } } + + // 无论是否在房间中,都响应心跳包(pong) + socket.emit('heartbeat:pong', { timestamp: Date.now() }); }); // 断开连接 diff --git a/src/components/WatchRoomProvider.tsx b/src/components/WatchRoomProvider.tsx index ae9ae76..4c47709 100644 --- a/src/components/WatchRoomProvider.tsx +++ b/src/components/WatchRoomProvider.tsx @@ -10,6 +10,7 @@ import Toast, { ToastProps } from '@/components/Toast'; interface WatchRoomContextType { socket: WatchRoomSocket | null; isConnected: boolean; + reconnectFailed: boolean; currentRoom: Room | null; members: Member[]; chatMessages: ChatMessage[]; @@ -44,6 +45,9 @@ interface WatchRoomContextType { changeVideo: (state: any) => void; changeLiveChannel: (state: any) => void; clearRoomState: () => void; + + // 重连 + manualReconnect: () => Promise; } const WatchRoomContext = createContext(null); @@ -69,6 +73,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { const [config, setConfig] = useState(null); const [isEnabled, setIsEnabled] = useState(false); const [toast, setToast] = useState(null); + const [reconnectFailed, setReconnectFailed] = useState(false); // 处理房间删除的回调 const handleRoomDeleted = useCallback((data?: { reason?: string }) => { @@ -106,6 +111,37 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { const watchRoom = useWatchRoom(handleRoomDeleted, handleStateCleared); + // 手动重连 + const manualReconnect = useCallback(async () => { + console.log('[WatchRoomProvider] Manual reconnect initiated'); + setReconnectFailed(false); + + const { watchRoomSocketManager } = await import('@/lib/watch-room-socket'); + const success = await watchRoomSocketManager.reconnect(); + + if (success) { + console.log('[WatchRoomProvider] Manual reconnect succeeded'); + // 尝试重新加入房间 + const storedInfo = localStorage.getItem('watch_room_info'); + if (storedInfo && watchRoom.socket) { + try { + const info = JSON.parse(storedInfo); + console.log('[WatchRoomProvider] Attempting to rejoin room after reconnect'); + await watchRoom.joinRoom({ + roomId: info.roomId, + password: info.password, + userName: info.userName, + }); + } catch (error) { + console.error('[WatchRoomProvider] Failed to rejoin room after reconnect:', error); + } + } + } else { + console.error('[WatchRoomProvider] Manual reconnect failed'); + setReconnectFailed(true); + } + }, [watchRoom]); + // 加载配置 useEffect(() => { const loadConfig = async () => { @@ -127,6 +163,19 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { // 只在启用了观影室时才连接 if (watchRoomConfig.enabled) { console.log('[WatchRoom] Connecting with config:', watchRoomConfig); + + // 设置重连回调 + const { watchRoomSocketManager } = await import('@/lib/watch-room-socket'); + watchRoomSocketManager.setReconnectFailedCallback(() => { + console.log('[WatchRoomProvider] Reconnect failed callback triggered'); + setReconnectFailed(true); + }); + + watchRoomSocketManager.setReconnectSuccessCallback(() => { + console.log('[WatchRoomProvider] Reconnect success callback triggered'); + setReconnectFailed(false); + }); + await watchRoom.connect(watchRoomConfig); } else { console.log('[WatchRoom] Watch room is disabled, skipping connection'); @@ -164,6 +213,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { const contextValue: WatchRoomContextType = { socket: watchRoom.socket, isConnected: watchRoom.isConnected, + reconnectFailed, currentRoom: watchRoom.currentRoom, members: watchRoom.members, chatMessages: watchRoom.chatMessages, @@ -182,6 +232,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { changeVideo: watchRoom.changeVideo, changeLiveChannel: watchRoom.changeLiveChannel, clearRoomState: watchRoom.clearRoomState, + manualReconnect, }; return ( diff --git a/src/components/watch-room/ChatFloatingWindow.tsx b/src/components/watch-room/ChatFloatingWindow.tsx index 51db1ee..09089b3 100644 --- a/src/components/watch-room/ChatFloatingWindow.tsx +++ b/src/components/watch-room/ChatFloatingWindow.tsx @@ -2,7 +2,7 @@ 'use client'; import { useState, useEffect, useRef } from 'react'; -import { MessageCircle, X, Send, Smile, Minimize2, Maximize2, Info, Users, LogOut, XCircle, Mic, MicOff, Volume2, VolumeX } from 'lucide-react'; +import { MessageCircle, X, Send, Smile, Minimize2, Maximize2, Info, Users, LogOut, XCircle, Mic, MicOff, Volume2, VolumeX, AlertCircle } from 'lucide-react'; import { useWatchRoomContextSafe } from '@/components/WatchRoomProvider'; import { useVoiceChat } from '@/hooks/useVoiceChat'; @@ -25,6 +25,7 @@ export default function ChatFloatingWindow() { // 语音聊天状态 const [isMicEnabled, setIsMicEnabled] = useState(false); const [isSpeakerEnabled, setIsSpeakerEnabled] = useState(true); + const [isReconnecting, setIsReconnecting] = useState(false); // 使用语音聊天hook const voiceChat = useVoiceChat({ @@ -95,8 +96,43 @@ export default function ChatFloatingWindow() { } }, [isOpen, isMinimized]); - // 如果没有加入房间,不显示聊天按钮 + // 处理手动重连 + const handleReconnect = async () => { + if (!watchRoom?.manualReconnect) return; + + setIsReconnecting(true); + try { + await watchRoom.manualReconnect(); + } catch (error) { + console.error('[ChatFloatingWindow] Reconnect failed:', error); + } finally { + setIsReconnecting(false); + } + }; + + // 如果没有加入房间,只显示重连按钮(如果需要) if (!watchRoom?.currentRoom) { + // 重连失败时显示重连按钮 + if (watchRoom?.reconnectFailed) { + return ( +
+ +
+ ); + } return null; } @@ -141,6 +177,24 @@ export default function ChatFloatingWindow() { if (!isOpen && !showRoomInfo) { return (
+ {/* 重连失败提示气泡 */} + {watchRoom?.reconnectFailed && ( + + )} + {/* 房间信息按钮 */} + )} + {/* 房间信息按钮 */} + )} + {/* 房间信息按钮 */}