优化socket.io重连机制
This commit is contained in:
@@ -321,8 +321,9 @@ class WatchRoomServer {
|
|||||||
// 心跳
|
// 心跳
|
||||||
socket.on('heartbeat', () => {
|
socket.on('heartbeat', () => {
|
||||||
const roomInfo = this.socketToRoom.get(socket.id);
|
const roomInfo = this.socketToRoom.get(socket.id);
|
||||||
if (!roomInfo) return;
|
|
||||||
|
|
||||||
|
// 如果用户在房间中,更新心跳时间
|
||||||
|
if (roomInfo) {
|
||||||
const roomMembers = this.members.get(roomInfo.roomId);
|
const roomMembers = this.members.get(roomInfo.roomId);
|
||||||
const member = roomMembers?.get(roomInfo.userId);
|
const member = roomMembers?.get(roomInfo.userId);
|
||||||
if (member) {
|
if (member) {
|
||||||
@@ -337,6 +338,10 @@ class WatchRoomServer {
|
|||||||
this.rooms.set(roomInfo.roomId, room);
|
this.rooms.set(roomInfo.roomId, room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无论是否在房间中,都响应心跳包(pong)
|
||||||
|
socket.emit('heartbeat:pong', { timestamp: Date.now() });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 断开连接
|
// 断开连接
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Toast, { ToastProps } from '@/components/Toast';
|
|||||||
interface WatchRoomContextType {
|
interface WatchRoomContextType {
|
||||||
socket: WatchRoomSocket | null;
|
socket: WatchRoomSocket | null;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
|
reconnectFailed: boolean;
|
||||||
currentRoom: Room | null;
|
currentRoom: Room | null;
|
||||||
members: Member[];
|
members: Member[];
|
||||||
chatMessages: ChatMessage[];
|
chatMessages: ChatMessage[];
|
||||||
@@ -44,6 +45,9 @@ interface WatchRoomContextType {
|
|||||||
changeVideo: (state: any) => void;
|
changeVideo: (state: any) => void;
|
||||||
changeLiveChannel: (state: any) => void;
|
changeLiveChannel: (state: any) => void;
|
||||||
clearRoomState: () => void;
|
clearRoomState: () => void;
|
||||||
|
|
||||||
|
// 重连
|
||||||
|
manualReconnect: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WatchRoomContext = createContext<WatchRoomContextType | null>(null);
|
const WatchRoomContext = createContext<WatchRoomContextType | null>(null);
|
||||||
@@ -69,6 +73,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
|||||||
const [config, setConfig] = useState<WatchRoomConfig | null>(null);
|
const [config, setConfig] = useState<WatchRoomConfig | null>(null);
|
||||||
const [isEnabled, setIsEnabled] = useState(false);
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
const [toast, setToast] = useState<ToastProps | null>(null);
|
const [toast, setToast] = useState<ToastProps | null>(null);
|
||||||
|
const [reconnectFailed, setReconnectFailed] = useState(false);
|
||||||
|
|
||||||
// 处理房间删除的回调
|
// 处理房间删除的回调
|
||||||
const handleRoomDeleted = useCallback((data?: { reason?: string }) => {
|
const handleRoomDeleted = useCallback((data?: { reason?: string }) => {
|
||||||
@@ -106,6 +111,37 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
|||||||
|
|
||||||
const watchRoom = useWatchRoom(handleRoomDeleted, handleStateCleared);
|
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(() => {
|
useEffect(() => {
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
@@ -127,6 +163,19 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
|||||||
// 只在启用了观影室时才连接
|
// 只在启用了观影室时才连接
|
||||||
if (watchRoomConfig.enabled) {
|
if (watchRoomConfig.enabled) {
|
||||||
console.log('[WatchRoom] Connecting with config:', watchRoomConfig);
|
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);
|
await watchRoom.connect(watchRoomConfig);
|
||||||
} else {
|
} else {
|
||||||
console.log('[WatchRoom] Watch room is disabled, skipping connection');
|
console.log('[WatchRoom] Watch room is disabled, skipping connection');
|
||||||
@@ -164,6 +213,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
|||||||
const contextValue: WatchRoomContextType = {
|
const contextValue: WatchRoomContextType = {
|
||||||
socket: watchRoom.socket,
|
socket: watchRoom.socket,
|
||||||
isConnected: watchRoom.isConnected,
|
isConnected: watchRoom.isConnected,
|
||||||
|
reconnectFailed,
|
||||||
currentRoom: watchRoom.currentRoom,
|
currentRoom: watchRoom.currentRoom,
|
||||||
members: watchRoom.members,
|
members: watchRoom.members,
|
||||||
chatMessages: watchRoom.chatMessages,
|
chatMessages: watchRoom.chatMessages,
|
||||||
@@ -182,6 +232,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
|||||||
changeVideo: watchRoom.changeVideo,
|
changeVideo: watchRoom.changeVideo,
|
||||||
changeLiveChannel: watchRoom.changeLiveChannel,
|
changeLiveChannel: watchRoom.changeLiveChannel,
|
||||||
clearRoomState: watchRoom.clearRoomState,
|
clearRoomState: watchRoom.clearRoomState,
|
||||||
|
manualReconnect,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
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 { useWatchRoomContextSafe } from '@/components/WatchRoomProvider';
|
||||||
import { useVoiceChat } from '@/hooks/useVoiceChat';
|
import { useVoiceChat } from '@/hooks/useVoiceChat';
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ export default function ChatFloatingWindow() {
|
|||||||
// 语音聊天状态
|
// 语音聊天状态
|
||||||
const [isMicEnabled, setIsMicEnabled] = useState(false);
|
const [isMicEnabled, setIsMicEnabled] = useState(false);
|
||||||
const [isSpeakerEnabled, setIsSpeakerEnabled] = useState(true);
|
const [isSpeakerEnabled, setIsSpeakerEnabled] = useState(true);
|
||||||
|
const [isReconnecting, setIsReconnecting] = useState(false);
|
||||||
|
|
||||||
// 使用语音聊天hook
|
// 使用语音聊天hook
|
||||||
const voiceChat = useVoiceChat({
|
const voiceChat = useVoiceChat({
|
||||||
@@ -95,8 +96,43 @@ export default function ChatFloatingWindow() {
|
|||||||
}
|
}
|
||||||
}, [isOpen, isMinimized]);
|
}, [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?.currentRoom) {
|
||||||
|
// 重连失败时显示重连按钮
|
||||||
|
if (watchRoom?.reconnectFailed) {
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-20 right-4 z-[700] flex flex-col gap-3 md:bottom-4">
|
||||||
|
<button
|
||||||
|
onClick={handleReconnect}
|
||||||
|
disabled={isReconnecting}
|
||||||
|
className="group relative flex h-14 w-14 items-center justify-center rounded-full bg-red-500 text-white shadow-2xl transition-all hover:scale-110 hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed animate-pulse"
|
||||||
|
aria-label="连接失败,点击重连"
|
||||||
|
title="连接失败,点击重连"
|
||||||
|
>
|
||||||
|
<AlertCircle className="h-6 w-6" />
|
||||||
|
{isReconnecting && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="h-10 w-10 animate-spin rounded-full border-4 border-white border-t-transparent"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +177,24 @@ export default function ChatFloatingWindow() {
|
|||||||
if (!isOpen && !showRoomInfo) {
|
if (!isOpen && !showRoomInfo) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-20 right-4 z-[700] flex flex-col gap-3 md:bottom-4">
|
<div className="fixed bottom-20 right-4 z-[700] flex flex-col gap-3 md:bottom-4">
|
||||||
|
{/* 重连失败提示气泡 */}
|
||||||
|
{watchRoom?.reconnectFailed && (
|
||||||
|
<button
|
||||||
|
onClick={handleReconnect}
|
||||||
|
disabled={isReconnecting}
|
||||||
|
className="group relative flex h-14 w-14 items-center justify-center rounded-full bg-red-500 text-white shadow-2xl transition-all hover:scale-110 hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed animate-pulse"
|
||||||
|
aria-label="连接失败,点击重连"
|
||||||
|
title="连接失败,点击重连"
|
||||||
|
>
|
||||||
|
<AlertCircle className="h-6 w-6" />
|
||||||
|
{isReconnecting && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="h-10 w-10 animate-spin rounded-full border-4 border-white border-t-transparent"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 房间信息按钮 */}
|
{/* 房间信息按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowRoomInfo(true)}
|
onClick={() => setShowRoomInfo(true)}
|
||||||
@@ -300,6 +354,24 @@ export default function ChatFloatingWindow() {
|
|||||||
if (isMinimized) {
|
if (isMinimized) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* 重连失败提示气泡 */}
|
||||||
|
{watchRoom?.reconnectFailed && (
|
||||||
|
<button
|
||||||
|
onClick={handleReconnect}
|
||||||
|
disabled={isReconnecting}
|
||||||
|
className="fixed bottom-[13.5rem] right-4 z-[700] group relative flex h-12 w-12 items-center justify-center rounded-full bg-red-500 text-white shadow-2xl transition-all hover:scale-110 hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed animate-pulse md:bottom-[11rem]"
|
||||||
|
aria-label="连接失败,点击重连"
|
||||||
|
title="连接失败,点击重连"
|
||||||
|
>
|
||||||
|
<AlertCircle className="h-5 w-5" />
|
||||||
|
{isReconnecting && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white border-t-transparent"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 房间信息按钮 */}
|
{/* 房间信息按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowRoomInfo(true)}
|
onClick={() => setShowRoomInfo(true)}
|
||||||
@@ -335,6 +407,24 @@ export default function ChatFloatingWindow() {
|
|||||||
// 完整聊天窗口
|
// 完整聊天窗口
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* 重连失败提示气泡 */}
|
||||||
|
{watchRoom?.reconnectFailed && (
|
||||||
|
<button
|
||||||
|
onClick={handleReconnect}
|
||||||
|
disabled={isReconnecting}
|
||||||
|
className="fixed bottom-[32.5rem] right-4 z-[700] group relative flex h-12 w-12 items-center justify-center rounded-full bg-red-500 text-white shadow-2xl transition-all hover:scale-110 hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed animate-pulse md:bottom-[30rem]"
|
||||||
|
aria-label="连接失败,点击重连"
|
||||||
|
title="连接失败,点击重连"
|
||||||
|
>
|
||||||
|
<AlertCircle className="h-5 w-5" />
|
||||||
|
{isReconnecting && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white border-t-transparent"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 房间信息按钮 */}
|
{/* 房间信息按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowRoomInfo(true)}
|
onClick={() => setShowRoomInfo(true)}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ class WatchRoomSocketManager {
|
|||||||
private socket: WatchRoomSocket | null = null;
|
private socket: WatchRoomSocket | null = null;
|
||||||
private config: WatchRoomConfig | null = null;
|
private config: WatchRoomConfig | null = null;
|
||||||
private heartbeatInterval: NodeJS.Timeout | null = null;
|
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||||
|
private heartbeatTimeoutCheck: NodeJS.Timeout | null = null;
|
||||||
|
private lastHeartbeatResponse: number = Date.now();
|
||||||
|
private visibilityChangeHandler: (() => void) | null = null;
|
||||||
|
private reconnectFailedCallback: (() => void) | null = null;
|
||||||
|
private reconnectSuccessCallback: (() => void) | null = null;
|
||||||
|
|
||||||
async connect(config: WatchRoomConfig): Promise<WatchRoomSocket> {
|
async connect(config: WatchRoomConfig): Promise<WatchRoomSocket> {
|
||||||
if (this.socket?.connected) {
|
if (this.socket?.connected) {
|
||||||
@@ -55,19 +60,26 @@ class WatchRoomSocketManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置事件监听
|
// 设置事件监听(包括 heartbeat:pong)
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
|
||||||
// 开始心跳
|
// 开始心跳
|
||||||
this.startHeartbeat();
|
this.startHeartbeat();
|
||||||
|
|
||||||
|
// 启动心跳超时检查
|
||||||
|
this.startHeartbeatTimeoutCheck();
|
||||||
|
|
||||||
|
// 设置浏览器可见性监听
|
||||||
|
this.setupVisibilityListener();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!this.socket) {
|
if (!this.socket) {
|
||||||
reject(new Error('Socket not initialized'));
|
reject(new Error('Socket not initialized'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.socket.on('connect', () => {
|
// 使用 once 而不是 on,避免重复注册
|
||||||
|
this.socket.once('connect', () => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('[WatchRoom] Connected to server');
|
console.log('[WatchRoom] Connected to server');
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
@@ -75,7 +87,7 @@ class WatchRoomSocketManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('connect_error', (error) => {
|
this.socket.once('connect_error', (error) => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('[WatchRoom] Connection error:', error);
|
console.error('[WatchRoom] Connection error:', error);
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -89,7 +101,24 @@ class WatchRoomSocketManager {
|
|||||||
this.heartbeatInterval = null;
|
this.heartbeatInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.heartbeatTimeoutCheck) {
|
||||||
|
clearInterval(this.heartbeatTimeoutCheck);
|
||||||
|
this.heartbeatTimeoutCheck = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除浏览器可见性监听
|
||||||
|
this.removeVisibilityListener();
|
||||||
|
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
|
// 移除所有事件监听器
|
||||||
|
this.socket.off('connect');
|
||||||
|
this.socket.off('disconnect');
|
||||||
|
this.socket.off('error');
|
||||||
|
this.socket.off('heartbeat:pong');
|
||||||
|
this.socket.io.off('reconnect_attempt');
|
||||||
|
this.socket.io.off('reconnect');
|
||||||
|
this.socket.io.off('reconnect_failed');
|
||||||
|
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
}
|
}
|
||||||
@@ -109,6 +138,8 @@ class WatchRoomSocketManager {
|
|||||||
this.socket.on('connect', () => {
|
this.socket.on('connect', () => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('[WatchRoom] Socket connected');
|
console.log('[WatchRoom] Socket connected');
|
||||||
|
// 重置心跳响应时间
|
||||||
|
this.lastHeartbeatResponse = Date.now();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('disconnect', (reason) => {
|
this.socket.on('disconnect', (reason) => {
|
||||||
@@ -120,6 +151,33 @@ class WatchRoomSocketManager {
|
|||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('[WatchRoom] Socket error:', error);
|
console.error('[WatchRoom] Socket error:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听心跳响应
|
||||||
|
this.socket.on('heartbeat:pong', (data: { timestamp: number }) => {
|
||||||
|
this.lastHeartbeatResponse = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听重连尝试
|
||||||
|
this.socket.io.on('reconnect_attempt', (attemptNumber) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[WatchRoom] Reconnect attempt:', attemptNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听重连成功
|
||||||
|
this.socket.io.on('reconnect', (attemptNumber) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[WatchRoom] Reconnected after', attemptNumber, 'attempts');
|
||||||
|
// 重置心跳响应时间
|
||||||
|
this.lastHeartbeatResponse = Date.now();
|
||||||
|
this.reconnectSuccessCallback?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听重连失败
|
||||||
|
this.socket.io.on('reconnect_failed', () => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('[WatchRoom] Reconnect failed after all attempts');
|
||||||
|
this.reconnectFailedCallback?.();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private startHeartbeat() {
|
private startHeartbeat() {
|
||||||
@@ -133,6 +191,118 @@ class WatchRoomSocketManager {
|
|||||||
}
|
}
|
||||||
}, 5000); // 每5秒发送一次心跳
|
}, 5000); // 每5秒发送一次心跳
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启动心跳超时检查
|
||||||
|
private startHeartbeatTimeoutCheck() {
|
||||||
|
if (this.heartbeatTimeoutCheck) {
|
||||||
|
clearInterval(this.heartbeatTimeoutCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每3秒检查一次心跳超时
|
||||||
|
this.heartbeatTimeoutCheck = setInterval(() => {
|
||||||
|
if (!this.socket?.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLastResponse = now - this.lastHeartbeatResponse;
|
||||||
|
|
||||||
|
// 如果超过15秒没有收到心跳响应,认为连接可能有问题
|
||||||
|
if (timeSinceLastResponse > 15000) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('[WatchRoom] Heartbeat timeout detected, last response was', timeSinceLastResponse, 'ms ago');
|
||||||
|
|
||||||
|
// 尝试重连
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.disconnect();
|
||||||
|
this.socket.connect();
|
||||||
|
// 重置心跳响应时间,避免重复触发
|
||||||
|
this.lastHeartbeatResponse = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置浏览器可见性监听
|
||||||
|
private setupVisibilityListener() {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
|
this.visibilityChangeHandler = () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[WatchRoom] Page became visible, checking connection...');
|
||||||
|
|
||||||
|
// 页面可见时检查连接状态
|
||||||
|
if (this.socket && !this.socket.connected) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[WatchRoom] Socket disconnected, attempting to reconnect...');
|
||||||
|
this.socket.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', this.visibilityChangeHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除浏览器可见性监听
|
||||||
|
private removeVisibilityListener() {
|
||||||
|
if (typeof document === 'undefined' || !this.visibilityChangeHandler) return;
|
||||||
|
|
||||||
|
document.removeEventListener('visibilitychange', this.visibilityChangeHandler);
|
||||||
|
this.visibilityChangeHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置重连失败回调
|
||||||
|
setReconnectFailedCallback(callback: () => void) {
|
||||||
|
this.reconnectFailedCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置重连成功回调
|
||||||
|
setReconnectSuccessCallback(callback: () => void) {
|
||||||
|
this.reconnectSuccessCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动重连
|
||||||
|
async reconnect(): Promise<boolean> {
|
||||||
|
if (!this.config) {
|
||||||
|
console.error('[WatchRoom] No config available for reconnection');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('[WatchRoom] Manual reconnection initiated...');
|
||||||
|
|
||||||
|
// 如果socket存在且未连接,尝试重新连接
|
||||||
|
if (this.socket && !this.socket.connected) {
|
||||||
|
this.socket.connect();
|
||||||
|
|
||||||
|
// 等待连接结果
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
resolve(false);
|
||||||
|
}, 5000); // 5秒超时
|
||||||
|
|
||||||
|
this.socket!.once('connect', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket!.once('connect_error', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果socket不存在,重新创建连接
|
||||||
|
await this.connect(this.config);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WatchRoom] Manual reconnection failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单例实例
|
// 单例实例
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export interface ServerToClientEvents {
|
|||||||
'voice:mic-enabled': (data: { userId: string }) => void;
|
'voice:mic-enabled': (data: { userId: string }) => void;
|
||||||
'voice:audio-chunk': (data: { userId: string; audioData: number[]; sampleRate?: number }) => void;
|
'voice:audio-chunk': (data: { userId: string; audioData: number[]; sampleRate?: number }) => void;
|
||||||
'state:cleared': () => void;
|
'state:cleared': () => void;
|
||||||
|
'heartbeat:pong': (data: { timestamp: number }) => void;
|
||||||
'error': (message: string) => void;
|
'error': (message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user