From 7911e4cc5f3eebd69fa4e892fae1c7dce70162f7 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sun, 7 Dec 2025 23:48:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=90=8C=E6=AD=A5=E5=90=84?= =?UTF-8?q?=E7=A7=8D=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 32 +++++- src/app/watch-room/page.tsx | 109 +++++++++++++----- src/components/WatchRoomProvider.tsx | 16 ++- src/hooks/useLiveSync.ts | 1 + src/hooks/usePlaySync.ts | 161 +++++++++++++++++++++------ src/hooks/useWatchRoom.ts | 46 +++++++- src/lib/watch-room-server.ts | 51 ++++++++- src/types/watch-room.ts | 3 + 8 files changed, 346 insertions(+), 73 deletions(-) diff --git a/server.js b/server.js index 40441d9..8b12066 100644 --- a/server.js +++ b/server.js @@ -211,14 +211,25 @@ class WatchRoomServer { // 切换视频/集数 socket.on('play:change', (state) => { + console.log(`[WatchRoom] Received play:change from ${socket.id}:`, state); const roomInfo = this.socketToRoom.get(socket.id); - if (!roomInfo || !roomInfo.isOwner) return; + if (!roomInfo) { + console.log('[WatchRoom] No room info for socket, ignoring play:change'); + return; + } + if (!roomInfo.isOwner) { + console.log('[WatchRoom] User is not owner, ignoring play:change'); + return; + } const room = this.rooms.get(roomInfo.roomId); if (room) { room.currentState = state; this.rooms.set(roomInfo.roomId, room); + console.log(`[WatchRoom] Broadcasting play:change to room ${roomInfo.roomId}`); socket.to(roomInfo.roomId).emit('play:change', state); + } else { + console.log('[WatchRoom] Room not found for play:change'); } }); @@ -387,15 +398,28 @@ class WatchRoomServer { startCleanupTimer() { this.cleanupInterval = setInterval(() => { const now = Date.now(); - const timeout = 5 * 60 * 1000; // 5分钟 + const deleteTimeout = 5 * 60 * 1000; // 5分钟 - 删除房间 + const clearStateTimeout = 30 * 1000; // 30秒 - 清除播放状态 for (const [roomId, room] of this.rooms.entries()) { - if (now - room.lastOwnerHeartbeat > timeout) { + const timeSinceHeartbeat = now - room.lastOwnerHeartbeat; + + // 如果房主心跳超过30秒,清除播放状态 + if (timeSinceHeartbeat > clearStateTimeout && room.currentState !== null) { + console.log(`[WatchRoom] Room ${roomId} owner inactive for 30s, clearing play state`); + room.currentState = null; + this.rooms.set(roomId, room); + // 通知房间内所有成员状态已清除 + this.io.to(roomId).emit('state:cleared'); + } + + // 检查房主是否超时5分钟 - 删除房间 + if (timeSinceHeartbeat > deleteTimeout) { console.log(`[WatchRoom] Room ${roomId} owner timeout, deleting...`); this.deleteRoom(roomId); } } - }, 30000); // 每30秒检查一次 + }, 10000); // 每10秒检查一次,确保更及时的清理 } generateRoomId() { diff --git a/src/app/watch-room/page.tsx b/src/app/watch-room/page.tsx index 56500ea..d30fbf9 100644 --- a/src/app/watch-room/page.tsx +++ b/src/app/watch-room/page.tsx @@ -13,7 +13,8 @@ type TabType = 'create' | 'join' | 'list'; export default function WatchRoomPage() { const router = useRouter(); - const { getRoomList, isConnected, createRoom, joinRoom, currentRoom, isOwner, members } = useWatchRoomContext(); + const watchRoom = useWatchRoomContext(); + const { getRoomList, isConnected, createRoom, joinRoom, currentRoom, isOwner, members, socket } = watchRoom; const [activeTab, setActiveTab] = useState('create'); // 获取当前登录用户(在客户端挂载后读取,避免 hydration 错误) @@ -137,33 +138,53 @@ export default function WatchRoomPage() { useEffect(() => { if (!currentRoom || isOwner) return; - // 房员加入房间后,检查房主的播放状态 - if (currentRoom.currentState) { - const state = currentRoom.currentState; - if (state.type === 'play') { - // 跳转到播放页面,使用完整参数 - console.log('[WatchRoom] Member joining/following, redirecting to play page'); + // 房员加入房间后,不立即跳转 + // 而是监听 play:change 或 live:change 事件(说明房主正在活跃使用) + // 这样可以避免房主已经离开play页面但状态未清除的情况 + // 检查房主的播放状态 - 仅在首次加入且状态是最近更新时才跳转 + // 这里不再自动跳转,而是等待房主的下一次操作 + }, [currentRoom, isOwner]); + + // 监听房主的主动操作(切换视频/频道) + useEffect(() => { + if (!currentRoom || isOwner) return; + + const handlePlayChange = (state: any) => { + console.log('[WatchRoom] Member following owner - play:change event'); + if (state.type === 'play') { const params = new URLSearchParams({ id: state.videoId, source: state.source, episode: String(state.episode || 1), }); - // 添加可选参数 if (state.videoName) params.set('title', state.videoName); if (state.videoYear) params.set('year', state.videoYear); if (state.searchTitle) params.set('stitle', state.searchTitle); router.push(`/play?${params.toString()}`); - } else if (state.type === 'live') { - // 跳转到直播页面 - console.log('[WatchRoom] Member joining/following, redirecting to live page'); + } + }; + + const handleLiveChange = (state: any) => { + console.log('[WatchRoom] Member following owner - live:change event'); + if (state.type === 'live') { router.push(`/live?id=${state.channelId}`); } + }; + + // 监听房主切换视频/频道的事件 + if (socket) { + socket.on('play:change', handlePlayChange); + socket.on('live:change', handleLiveChange); + + return () => { + socket.off('play:change', handlePlayChange); + socket.off('live:change', handleLiveChange); + }; } - // 如果房主还没播放,留在观影室页面等待 - }, [currentRoom?.currentState, isOwner, router]); + }, [currentRoom, isOwner, router, socket]); // 从房间列表加入房间 const handleJoinFromList = (room: Room) => { @@ -197,21 +218,57 @@ export default function WatchRoomPage() {
{/* 房员等待提示 */} - {currentRoom && !isOwner && !currentRoom.currentState && ( + {currentRoom && !isOwner && (
-
-
-
-
-
-

等待房主开始播放

-

- 房间: {currentRoom.name} | 房主: {currentRoom.ownerName} -

-

- 当房主开始播放时,您将自动跳转到相同的视频或频道 -

+
+
+
+
+
+
+

+ {currentRoom.currentState ? '房主正在播放' : '等待房主开始播放'} +

+

+ 房间: {currentRoom.name} | 房主: {currentRoom.ownerName} +

+ {currentRoom.currentState && ( +

+ {currentRoom.currentState.type === 'play' + ? `${currentRoom.currentState.videoName || '未知视频'}` + : `${currentRoom.currentState.channelName || '未知频道'}`} +

+ )} + {!currentRoom.currentState && ( +

+ 当房主开始播放时,您将自动跟随 +

+ )} +
+ {currentRoom.currentState && ( + + )}
)} diff --git a/src/components/WatchRoomProvider.tsx b/src/components/WatchRoomProvider.tsx index 8b447bd..6ef3c35 100644 --- a/src/components/WatchRoomProvider.tsx +++ b/src/components/WatchRoomProvider.tsx @@ -43,6 +43,7 @@ interface WatchRoomContextType { pause: () => void; changeVideo: (state: any) => void; changeLiveChannel: (state: any) => void; + clearRoomState: () => void; } const WatchRoomContext = createContext(null); @@ -91,7 +92,19 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { } }, []); - const watchRoom = useWatchRoom(handleRoomDeleted); + // 处理房间状态清除的回调(房主离开超过30秒) + const handleStateCleared = useCallback(() => { + console.log('[WatchRoomProvider] Room state cleared'); + + setToast({ + message: '房主已离开,播放状态已清除', + type: 'warning', + duration: 4000, + onClose: () => setToast(null), + }); + }, []); + + const watchRoom = useWatchRoom(handleRoomDeleted, handleStateCleared); // 加载配置 useEffect(() => { @@ -164,6 +177,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) { pause: watchRoom.pause, changeVideo: watchRoom.changeVideo, changeLiveChannel: watchRoom.changeLiveChannel, + clearRoomState: watchRoom.clearRoomState, }; return ( diff --git a/src/hooks/useLiveSync.ts b/src/hooks/useLiveSync.ts index 1967156..3885628 100644 --- a/src/hooks/useLiveSync.ts +++ b/src/hooks/useLiveSync.ts @@ -27,6 +27,7 @@ export function useLiveSync({ const currentRoom = watchRoom?.currentRoom; const socket = watchRoom?.socket; + // 房主:广播频道切换 const broadcastChannelChange = useCallback(() => { if (!isOwner || !socket || syncingRef.current || !watchRoom) return; diff --git a/src/hooks/usePlaySync.ts b/src/hooks/usePlaySync.ts index 5274c0a..55d9a66 100644 --- a/src/hooks/usePlaySync.ts +++ b/src/hooks/usePlaySync.ts @@ -32,6 +32,7 @@ export function usePlaySync({ const router = useRouter(); const watchRoom = useWatchRoomContextSafe(); const lastSyncTimeRef = useRef(0); // 上次同步时间 + const isHandlingRemoteCommandRef = useRef(false); // 标记是否正在处理远程命令 // 检查是否在房间内 const isInRoom = !!(watchRoom && watchRoom.currentRoom); @@ -50,7 +51,7 @@ export function usePlaySync({ type: 'play', url: videoUrl, currentTime: player.currentTime || 0, - isPlaying: !player.paused, + isPlaying: player.playing || false, videoId, videoName, videoYear, @@ -86,35 +87,36 @@ export function usePlaySync({ } console.log('[PlaySync] Processing play update - current state:', { - playerPaused: player.paused, + playerPlaying: player.playing, statePlaying: state.isPlaying, playerTime: player.currentTime, stateTime: state.currentTime }); - // 同步播放状态 - 只有状态不一致时才执行操作 - if (state.isPlaying && player.paused) { - console.log('[PlaySync] Player is paused, starting playback'); - player.play().catch((err: any) => { - console.error('[PlaySync] Play error (browser may have blocked autoplay):', err); - }); - } else if (!state.isPlaying && !player.paused) { - console.log('[PlaySync] Player is playing, pausing playback'); - player.pause(); - } else { - console.log('[PlaySync] Player state already matches, no action needed'); - } + // 标记正在处理远程命令 + isHandlingRemoteCommandRef.current = true; - // 同步进度(如果差异超过2秒) + // play:update 只同步进度,不改变播放/暂停状态 + // 播放/暂停状态由 play:play 和 play:pause 命令控制 const timeDiff = Math.abs(player.currentTime - state.currentTime); if (timeDiff > 2) { console.log('[PlaySync] Seeking to:', state.currentTime, '(diff:', timeDiff, 's)'); player.currentTime = state.currentTime; + // 延迟重置标记,确保 seeked 事件已处理完毕 + setTimeout(() => { + isHandlingRemoteCommandRef.current = false; + console.log('[PlaySync] Reset flag after seek'); + }, 500); + } else { + console.log('[PlaySync] Time diff is small, no seek needed'); + // 没有操作,立即重置标记 + isHandlingRemoteCommandRef.current = false; } }; const handlePlayCommand = () => { - console.log('[PlaySync] Received play:play event'); + console.log('[PlaySync] ========== Received play:play event =========='); + console.log('[PlaySync] isHandlingRemoteCommandRef:', isHandlingRemoteCommandRef.current); const player = artPlayerRef.current; if (!player) { @@ -122,17 +124,46 @@ export function usePlaySync({ return; } + console.log('[PlaySync] Player state before play:', { + playing: player.playing, + currentTime: player.currentTime, + readyState: player.video?.readyState, + }); + + // 标记正在处理远程命令 + isHandlingRemoteCommandRef.current = true; + console.log('[PlaySync] Set flag to true'); + // 只有在暂停状态时才执行播放 - if (player.paused) { - console.log('[PlaySync] Executing play command'); - player.play().catch((err: any) => console.error('[PlaySync] Play error:', err)); + if (!player.playing) { + console.log('[PlaySync] Executing play command - calling player.play()'); + player.play() + .then(() => { + console.log('[PlaySync] Play command completed successfully'); + console.log('[PlaySync] Player state after play:', { + playing: player.playing, + currentTime: player.currentTime, + }); + // 等待播放器事件触发后再重置标记 + setTimeout(() => { + isHandlingRemoteCommandRef.current = false; + console.log('[PlaySync] Reset flag after play'); + }, 500); + }) + .catch((err: any) => { + console.error('[PlaySync] Play error:', err); + isHandlingRemoteCommandRef.current = false; + }); } else { console.log('[PlaySync] Player already playing, skipping'); + isHandlingRemoteCommandRef.current = false; } + console.log('[PlaySync] ========== End play:play handling =========='); }; const handlePauseCommand = () => { - console.log('[PlaySync] Received play:pause event'); + console.log('[PlaySync] ========== Received play:pause event =========='); + console.log('[PlaySync] isHandlingRemoteCommandRef:', isHandlingRemoteCommandRef.current); const player = artPlayerRef.current; if (!player) { @@ -140,13 +171,33 @@ export function usePlaySync({ return; } + console.log('[PlaySync] Player state before pause:', { + playing: player.playing, + currentTime: player.currentTime, + }); + + // 标记正在处理远程命令 + isHandlingRemoteCommandRef.current = true; + console.log('[PlaySync] Set flag to true'); + // 只有在播放状态时才执行暂停 - if (!player.paused) { - console.log('[PlaySync] Executing pause command'); + if (player.playing) { + console.log('[PlaySync] Executing pause command - calling player.pause()'); player.pause(); + console.log('[PlaySync] Player state after pause:', { + playing: player.playing, + currentTime: player.currentTime, + }); + // pause 是同步的,但还是延迟重置以确保事件处理完毕 + setTimeout(() => { + isHandlingRemoteCommandRef.current = false; + console.log('[PlaySync] Reset flag after pause'); + }, 500); } else { console.log('[PlaySync] Player already paused, skipping'); + isHandlingRemoteCommandRef.current = false; } + console.log('[PlaySync] ========== End play:pause handling =========='); }; const handleSeekCommand = (currentTime: number) => { @@ -158,12 +209,28 @@ export function usePlaySync({ return; } + // 标记正在处理远程命令 + isHandlingRemoteCommandRef.current = true; + console.log('[PlaySync] Executing seek command'); player.currentTime = currentTime; + + // 延迟重置标记,确保 seeked 事件已处理完毕 + setTimeout(() => { + isHandlingRemoteCommandRef.current = false; + console.log('[PlaySync] Reset flag after seek command'); + }, 500); }; const handleChangeCommand = (state: PlayState) => { console.log('[PlaySync] Received play:change event:', state); + console.log('[PlaySync] Current isOwner:', isOwner); + + // 只有房员才处理视频切换命令 + if (isOwner) { + console.log('[PlaySync] Skipping play:change - user is owner'); + return; + } // 跟随切换视频 // 构建完整的 URL 参数 @@ -179,10 +246,10 @@ export function usePlaySync({ if (state.searchTitle) params.set('stitle', state.searchTitle); const url = `/play?${params.toString()}`; - console.log('[PlaySync] Redirecting to:', url); + console.log('[PlaySync] Member redirecting to:', url); - // 跳转到新的视频页面 - router.push(url); + // 使用 window.location.href 强制完整刷新页面,确保播放器重新加载 + window.location.href = url; }; socket.on('play:update', handlePlayUpdate); @@ -201,7 +268,7 @@ export function usePlaySync({ socket.off('play:seek', handleSeekCommand); socket.off('play:change', handleChangeCommand); }; - }, [socket, currentRoom, artPlayerRef, isInRoom, router]); + }, [socket, currentRoom, isInRoom, isOwner]); // 监听播放器事件并广播(所有成员都可以触发同步) useEffect(() => { @@ -224,34 +291,52 @@ export function usePlaySync({ console.log('[PlaySync] Setting up player event listeners'); const handlePlay = () => { + // 如果正在处理远程命令,不要广播(避免循环) + if (isHandlingRemoteCommandRef.current) { + console.log('[PlaySync] Play event triggered by remote command, not broadcasting'); + return; + } + const player = artPlayerRef.current; if (!player) return; // 确认播放器确实在播放状态才广播 - if (!player.paused) { + if (player.playing) { console.log('[PlaySync] Play event detected, player is playing, broadcasting...'); + // 只发送 play 命令,不发送完整状态(避免重复) watchRoom.play(); - broadcastPlayState(); } else { console.log('[PlaySync] Play event detected but player is paused, not broadcasting'); } }; const handlePause = () => { + // 如果正在处理远程命令,不要广播(避免循环) + if (isHandlingRemoteCommandRef.current) { + console.log('[PlaySync] Pause event triggered by remote command, not broadcasting'); + return; + } + const player = artPlayerRef.current; if (!player) return; // 确认播放器确实在暂停状态才广播 - if (player.paused) { + if (!player.playing) { console.log('[PlaySync] Pause event detected, player is paused, broadcasting...'); + // 只发送 pause 命令,不发送完整状态(避免重复) watchRoom.pause(); - broadcastPlayState(); } else { console.log('[PlaySync] Pause event detected but player is playing, not broadcasting'); } }; const handleSeeked = () => { + // 如果正在处理远程命令,不要广播(避免循环) + if (isHandlingRemoteCommandRef.current) { + console.log('[PlaySync] Seeked event triggered by remote command, not broadcasting'); + return; + } + const player = artPlayerRef.current; if (!player) return; @@ -265,7 +350,7 @@ export function usePlaySync({ // 定期同步播放进度(每5秒) const syncInterval = setInterval(() => { - if (player.paused) return; // 暂停时不同步 + if (!player.playing) return; // 暂停时不同步 console.log('[PlaySync] Periodic sync - broadcasting state'); broadcastPlayState(); @@ -287,13 +372,19 @@ export function usePlaySync({ if (!isOwner || !socket || !currentRoom || !isInRoom || !watchRoom) return; if (!videoId || !videoUrl) return; + console.log('[PlaySync] Video params changed, will broadcast after delay:', { + videoId, + currentSource, + currentEpisode + }); + // 延迟广播,避免初始化时触发 const timer = setTimeout(() => { const state: PlayState = { type: 'play', url: videoUrl, currentTime: artPlayerRef.current?.currentTime || 0, - isPlaying: artPlayerRef.current?.paused === false, + isPlaying: artPlayerRef.current?.playing || false, videoId, videoName, videoYear, @@ -302,12 +393,12 @@ export function usePlaySync({ source: currentSource, }; - console.log('[PlaySync] Video/episode/source changed, broadcasting:', state); + console.log('[PlaySync] Broadcasting play:change:', state); watchRoom.changeVideo(state); - }, 500); + }, 1000); // 1秒延迟,给页面足够时间初始化 return () => clearTimeout(timer); - }, [isOwner, socket, currentRoom, isInRoom, watchRoom, videoId, currentEpisode, currentSource, videoUrl, videoName, videoYear, searchTitle, artPlayerRef]); + }, [isOwner, socket, currentRoom, isInRoom, watchRoom, videoId, currentEpisode, currentSource]); return { isInRoom, diff --git a/src/hooks/useWatchRoom.ts b/src/hooks/useWatchRoom.ts index 0026232..34e7b6f 100644 --- a/src/hooks/useWatchRoom.ts +++ b/src/hooks/useWatchRoom.ts @@ -15,7 +15,10 @@ import type { const STORAGE_KEY = 'watch_room_info'; -export function useWatchRoom(onRoomDeleted?: (data?: { reason?: string }) => void) { +export function useWatchRoom( + onRoomDeleted?: (data?: { reason?: string }) => void, + onStateCleared?: () => void +) { const [socket, setSocket] = useState(null); const [isConnected, setIsConnected] = useState(false); const [currentRoom, setCurrentRoom] = useState(null); @@ -264,8 +267,16 @@ export function useWatchRoom(onRoomDeleted?: (data?: { reason?: string }) => voi const changeVideo = useCallback( (state: PlayState) => { const sock = watchRoomSocketManager.getSocket(); - if (!sock || !isOwner) return; + if (!sock) { + console.log('[WatchRoom] Cannot change video - no socket'); + return; + } + if (!isOwner) { + console.log('[WatchRoom] Cannot change video - not owner'); + return; + } + console.log('[WatchRoom] Emitting play:change with state:', state); sock.emit('play:change', state); }, [isOwner] @@ -282,6 +293,22 @@ export function useWatchRoom(onRoomDeleted?: (data?: { reason?: string }) => voi [isOwner] ); + // 清除房间播放状态(房主离开播放/直播页面时调用) + const clearRoomState = useCallback(() => { + const sock = watchRoomSocketManager.getSocket(); + if (!sock) { + console.log('[WatchRoom] Cannot clear state - no socket'); + return; + } + if (!isOwner) { + console.log('[WatchRoom] Cannot clear state - not owner'); + return; + } + + console.log('[WatchRoom] Emitting state:clear'); + sock.emit('state:clear'); + }, [isOwner]); + // 设置事件监听 useEffect(() => { if (!socket) return; @@ -338,6 +365,17 @@ export function useWatchRoom(onRoomDeleted?: (data?: { reason?: string }) => voi setChatMessages((prev) => [...prev, message]); }); + // 状态清除事件(房主心跳超时) + socket.on('state:cleared', () => { + console.log('[WatchRoom] Room state cleared by server (owner inactive)'); + + // 清除当前房间的播放/直播状态 + setCurrentRoom((prev) => (prev ? { ...prev, currentState: null } : null)); + + // 调用回调显示Toast + onStateCleared?.(); + }); + // 连接状态 socket.on('connect', () => { setIsConnected(true); @@ -356,10 +394,11 @@ export function useWatchRoom(onRoomDeleted?: (data?: { reason?: string }) => voi socket.off('play:change'); socket.off('live:change'); socket.off('chat:message'); + socket.off('state:cleared'); socket.off('connect'); socket.off('disconnect'); }; - }, [socket, currentRoom, onRoomDeleted]); + }, [socket, currentRoom, onRoomDeleted, onStateCleared]); // 清理 useEffect(() => { @@ -390,6 +429,7 @@ export function useWatchRoom(onRoomDeleted?: (data?: { reason?: string }) => voi pause, changeVideo, changeLiveChannel, + clearRoomState, }; } diff --git a/src/lib/watch-room-server.ts b/src/lib/watch-room-server.ts index 24bf9f7..4a3d728 100644 --- a/src/lib/watch-room-server.ts +++ b/src/lib/watch-room-server.ts @@ -248,6 +248,37 @@ export class WatchRoomServer { }); }); + // 清除房间播放状态(房主离开播放/直播页面时调用) + socket.on('state:clear', (callback) => { + console.log('[WatchRoom] Received state:clear from', socket.id); + const roomInfo = this.socketToRoom.get(socket.id); + + if (!roomInfo) { + console.log('[WatchRoom] No room info found for socket'); + if (callback) callback({ success: false, error: 'Not in a room' }); + return; + } + + if (!roomInfo.isOwner) { + console.log('[WatchRoom] User is not owner'); + if (callback) callback({ success: false, error: 'Not owner' }); + return; + } + + const room = this.rooms.get(roomInfo.roomId); + if (room) { + console.log(`[WatchRoom] Clearing room state for ${roomInfo.roomId}`); + room.currentState = null; + this.rooms.set(roomInfo.roomId, room); + // 通知房间内其他成员状态已清除 + socket.to(roomInfo.roomId).emit('state:cleared'); + if (callback) callback({ success: true }); + } else { + console.log('[WatchRoom] Room not found'); + if (callback) callback({ success: false, error: 'Room not found' }); + } + }); + // 心跳 socket.on('heartbeat', () => { const roomInfo = this.socketToRoom.get(socket.id); @@ -324,16 +355,28 @@ export class WatchRoomServer { private startCleanupTimer() { this.cleanupInterval = setInterval(() => { const now = Date.now(); - const timeout = 5 * 60 * 1000; // 5分钟 + const deleteTimeout = 5 * 60 * 1000; // 5分钟 - 删除房间 + const clearStateTimeout = 30 * 1000; // 30秒 - 清除播放状态 this.rooms.forEach((room, roomId) => { - // 检查房主是否超时 - if (now - room.lastOwnerHeartbeat > timeout) { + const timeSinceHeartbeat = now - room.lastOwnerHeartbeat; + + // 如果房主心跳超过30秒,清除播放状态 + if (timeSinceHeartbeat > clearStateTimeout && room.currentState !== null) { + console.log(`[WatchRoom] Room ${roomId} owner inactive for 30s, clearing play state`); + room.currentState = null; + this.rooms.set(roomId, room); + // 通知房间内所有成员状态已清除 + this.io.to(roomId).emit('state:cleared'); + } + + // 检查房主是否超时5分钟 - 删除房间 + if (timeSinceHeartbeat > deleteTimeout) { console.log(`[WatchRoom] Room ${roomId} owner timeout, deleting...`); this.deleteRoom(roomId); } }); - }, 30000); // 每30秒检查一次 + }, 10000); // 每10秒检查一次,确保更及时的清理 } private generateRoomId(): string { diff --git a/src/types/watch-room.ts b/src/types/watch-room.ts index a6c6351..68ae401 100644 --- a/src/types/watch-room.ts +++ b/src/types/watch-room.ts @@ -77,6 +77,7 @@ export interface ServerToClientEvents { 'voice:offer': (data: { userId: string; offer: RTCSessionDescriptionInit }) => void; 'voice:answer': (data: { userId: string; answer: RTCSessionDescriptionInit }) => void; 'voice:ice': (data: { userId: string; candidate: RTCIceCandidateInit }) => void; + 'state:cleared': () => void; 'error': (message: string) => void; } @@ -114,6 +115,8 @@ export interface ClientToServerEvents { 'voice:answer': (data: { targetUserId: string; answer: RTCSessionDescriptionInit }) => void; 'voice:ice': (data: { targetUserId: string; candidate: RTCIceCandidateInit }) => void; + 'state:clear': (callback?: (response: { success: boolean; error?: string }) => void) => void; + 'heartbeat': () => void; }