修正同步各种问题

This commit is contained in:
mtvpls
2025-12-07 23:53:00 +08:00
parent c38d1bbdad
commit 7911e4cc5f
8 changed files with 346 additions and 73 deletions
+28 -4
View File
@@ -211,14 +211,25 @@ class WatchRoomServer {
// 切换视频/集数 // 切换视频/集数
socket.on('play:change', (state) => { socket.on('play:change', (state) => {
console.log(`[WatchRoom] Received play:change from ${socket.id}:`, state);
const roomInfo = this.socketToRoom.get(socket.id); 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); const room = this.rooms.get(roomInfo.roomId);
if (room) { if (room) {
room.currentState = state; room.currentState = state;
this.rooms.set(roomInfo.roomId, room); this.rooms.set(roomInfo.roomId, room);
console.log(`[WatchRoom] Broadcasting play:change to room ${roomInfo.roomId}`);
socket.to(roomInfo.roomId).emit('play:change', state); 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() { startCleanupTimer() {
this.cleanupInterval = setInterval(() => { this.cleanupInterval = setInterval(() => {
const now = Date.now(); 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()) { 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...`); console.log(`[WatchRoom] Room ${roomId} owner timeout, deleting...`);
this.deleteRoom(roomId); this.deleteRoom(roomId);
} }
} }
}, 30000); // 每30秒检查一次 }, 10000); // 每10秒检查一次,确保更及时的清理
} }
generateRoomId() { generateRoomId() {
+83 -26
View File
@@ -13,7 +13,8 @@ type TabType = 'create' | 'join' | 'list';
export default function WatchRoomPage() { export default function WatchRoomPage() {
const router = useRouter(); 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<TabType>('create'); const [activeTab, setActiveTab] = useState<TabType>('create');
// 获取当前登录用户(在客户端挂载后读取,避免 hydration 错误) // 获取当前登录用户(在客户端挂载后读取,避免 hydration 错误)
@@ -137,33 +138,53 @@ export default function WatchRoomPage() {
useEffect(() => { useEffect(() => {
if (!currentRoom || isOwner) return; if (!currentRoom || isOwner) return;
// 房员加入房间后,检查房主的播放状态 // 房员加入房间后,不立即跳转
if (currentRoom.currentState) { // 而是监听 play:change 或 live:change 事件(说明房主正在活跃使用)
const state = currentRoom.currentState; // 这样可以避免房主已经离开play页面但状态未清除的情况
if (state.type === 'play') {
// 跳转到播放页面,使用完整参数
console.log('[WatchRoom] Member joining/following, redirecting to play page');
// 检查房主的播放状态 - 仅在首次加入且状态是最近更新时才跳转
// 这里不再自动跳转,而是等待房主的下一次操作
}, [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({ const params = new URLSearchParams({
id: state.videoId, id: state.videoId,
source: state.source, source: state.source,
episode: String(state.episode || 1), episode: String(state.episode || 1),
}); });
// 添加可选参数
if (state.videoName) params.set('title', state.videoName); if (state.videoName) params.set('title', state.videoName);
if (state.videoYear) params.set('year', state.videoYear); if (state.videoYear) params.set('year', state.videoYear);
if (state.searchTitle) params.set('stitle', state.searchTitle); if (state.searchTitle) params.set('stitle', state.searchTitle);
router.push(`/play?${params.toString()}`); 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}`); 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, isOwner, router, socket]);
}, [currentRoom?.currentState, isOwner, router]);
// 从房间列表加入房间 // 从房间列表加入房间
const handleJoinFromList = (room: Room) => { const handleJoinFromList = (room: Room) => {
@@ -197,21 +218,57 @@ export default function WatchRoomPage() {
<PageLayout activePath="/watch-room"> <PageLayout activePath="/watch-room">
<div className="flex flex-col gap-4 py-4 px-5 lg:px-[3rem] 2xl:px-20"> <div className="flex flex-col gap-4 py-4 px-5 lg:px-[3rem] 2xl:px-20">
{/* 房员等待提示 */} {/* 房员等待提示 */}
{currentRoom && !isOwner && !currentRoom.currentState && ( {currentRoom && !isOwner && (
<div className="mb-4 bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl p-6 shadow-lg"> <div className="mb-4 bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-center gap-4 text-white"> <div className="flex items-center justify-between gap-4 text-white">
<div className="relative"> <div className="flex items-center gap-4 flex-1">
<div className="w-12 h-12 border-4 border-white/30 border-t-white rounded-full animate-spin" /> <div className="relative">
</div> <div className="w-12 h-12 border-4 border-white/30 border-t-white rounded-full animate-spin" />
<div> </div>
<h3 className="text-lg font-bold mb-1"></h3> <div className="flex-1">
<p className="text-sm text-white/80"> <h3 className="text-lg font-bold mb-1">
: {currentRoom.name} | : {currentRoom.ownerName} {currentRoom.currentState ? '房主正在播放' : '等待房主开始播放'}
</p> </h3>
<p className="text-xs text-white/70 mt-1"> <p className="text-sm text-white/80">
: {currentRoom.name} | : {currentRoom.ownerName}
</p> </p>
{currentRoom.currentState && (
<p className="text-xs text-white/90 mt-1">
{currentRoom.currentState.type === 'play'
? `${currentRoom.currentState.videoName || '未知视频'}`
: `${currentRoom.currentState.channelName || '未知频道'}`}
</p>
)}
{!currentRoom.currentState && (
<p className="text-xs text-white/70 mt-1">
</p>
)}
</div>
</div> </div>
{currentRoom.currentState && (
<button
onClick={() => {
const state = currentRoom.currentState!;
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') {
router.push(`/live?id=${state.channelId}`);
}
}}
className="px-6 py-2 bg-white text-blue-600 font-medium rounded-lg hover:bg-white/90 transition-colors whitespace-nowrap"
>
</button>
)}
</div> </div>
</div> </div>
)} )}
+15 -1
View File
@@ -43,6 +43,7 @@ interface WatchRoomContextType {
pause: () => void; pause: () => void;
changeVideo: (state: any) => void; changeVideo: (state: any) => void;
changeLiveChannel: (state: any) => void; changeLiveChannel: (state: any) => void;
clearRoomState: () => void;
} }
const WatchRoomContext = createContext<WatchRoomContextType | null>(null); const WatchRoomContext = createContext<WatchRoomContextType | null>(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(() => { useEffect(() => {
@@ -164,6 +177,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
pause: watchRoom.pause, pause: watchRoom.pause,
changeVideo: watchRoom.changeVideo, changeVideo: watchRoom.changeVideo,
changeLiveChannel: watchRoom.changeLiveChannel, changeLiveChannel: watchRoom.changeLiveChannel,
clearRoomState: watchRoom.clearRoomState,
}; };
return ( return (
+1
View File
@@ -27,6 +27,7 @@ export function useLiveSync({
const currentRoom = watchRoom?.currentRoom; const currentRoom = watchRoom?.currentRoom;
const socket = watchRoom?.socket; const socket = watchRoom?.socket;
// 房主:广播频道切换 // 房主:广播频道切换
const broadcastChannelChange = useCallback(() => { const broadcastChannelChange = useCallback(() => {
if (!isOwner || !socket || syncingRef.current || !watchRoom) return; if (!isOwner || !socket || syncingRef.current || !watchRoom) return;
+126 -35
View File
@@ -32,6 +32,7 @@ export function usePlaySync({
const router = useRouter(); const router = useRouter();
const watchRoom = useWatchRoomContextSafe(); const watchRoom = useWatchRoomContextSafe();
const lastSyncTimeRef = useRef(0); // 上次同步时间 const lastSyncTimeRef = useRef(0); // 上次同步时间
const isHandlingRemoteCommandRef = useRef(false); // 标记是否正在处理远程命令
// 检查是否在房间内 // 检查是否在房间内
const isInRoom = !!(watchRoom && watchRoom.currentRoom); const isInRoom = !!(watchRoom && watchRoom.currentRoom);
@@ -50,7 +51,7 @@ export function usePlaySync({
type: 'play', type: 'play',
url: videoUrl, url: videoUrl,
currentTime: player.currentTime || 0, currentTime: player.currentTime || 0,
isPlaying: !player.paused, isPlaying: player.playing || false,
videoId, videoId,
videoName, videoName,
videoYear, videoYear,
@@ -86,35 +87,36 @@ export function usePlaySync({
} }
console.log('[PlaySync] Processing play update - current state:', { console.log('[PlaySync] Processing play update - current state:', {
playerPaused: player.paused, playerPlaying: player.playing,
statePlaying: state.isPlaying, statePlaying: state.isPlaying,
playerTime: player.currentTime, playerTime: player.currentTime,
stateTime: state.currentTime stateTime: state.currentTime
}); });
// 同步播放状态 - 只有状态不一致时才执行操作 // 标记正在处理远程命令
if (state.isPlaying && player.paused) { isHandlingRemoteCommandRef.current = true;
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');
}
// 同步进度(如果差异超过2秒) // play:update 只同步进度,不改变播放/暂停状态
// 播放/暂停状态由 play:play 和 play:pause 命令控制
const timeDiff = Math.abs(player.currentTime - state.currentTime); const timeDiff = Math.abs(player.currentTime - state.currentTime);
if (timeDiff > 2) { if (timeDiff > 2) {
console.log('[PlaySync] Seeking to:', state.currentTime, '(diff:', timeDiff, 's)'); console.log('[PlaySync] Seeking to:', state.currentTime, '(diff:', timeDiff, 's)');
player.currentTime = state.currentTime; 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 = () => { 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; const player = artPlayerRef.current;
if (!player) { if (!player) {
@@ -122,17 +124,46 @@ export function usePlaySync({
return; 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) { if (!player.playing) {
console.log('[PlaySync] Executing play command'); console.log('[PlaySync] Executing play command - calling player.play()');
player.play().catch((err: any) => console.error('[PlaySync] Play error:', err)); 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 { } else {
console.log('[PlaySync] Player already playing, skipping'); console.log('[PlaySync] Player already playing, skipping');
isHandlingRemoteCommandRef.current = false;
} }
console.log('[PlaySync] ========== End play:play handling ==========');
}; };
const handlePauseCommand = () => { 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; const player = artPlayerRef.current;
if (!player) { if (!player) {
@@ -140,13 +171,33 @@ export function usePlaySync({
return; 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) { if (player.playing) {
console.log('[PlaySync] Executing pause command'); console.log('[PlaySync] Executing pause command - calling player.pause()');
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 { } else {
console.log('[PlaySync] Player already paused, skipping'); console.log('[PlaySync] Player already paused, skipping');
isHandlingRemoteCommandRef.current = false;
} }
console.log('[PlaySync] ========== End play:pause handling ==========');
}; };
const handleSeekCommand = (currentTime: number) => { const handleSeekCommand = (currentTime: number) => {
@@ -158,12 +209,28 @@ export function usePlaySync({
return; return;
} }
// 标记正在处理远程命令
isHandlingRemoteCommandRef.current = true;
console.log('[PlaySync] Executing seek command'); console.log('[PlaySync] Executing seek command');
player.currentTime = currentTime; player.currentTime = currentTime;
// 延迟重置标记,确保 seeked 事件已处理完毕
setTimeout(() => {
isHandlingRemoteCommandRef.current = false;
console.log('[PlaySync] Reset flag after seek command');
}, 500);
}; };
const handleChangeCommand = (state: PlayState) => { const handleChangeCommand = (state: PlayState) => {
console.log('[PlaySync] Received play:change event:', state); 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 参数 // 构建完整的 URL 参数
@@ -179,10 +246,10 @@ export function usePlaySync({
if (state.searchTitle) params.set('stitle', state.searchTitle); if (state.searchTitle) params.set('stitle', state.searchTitle);
const url = `/play?${params.toString()}`; const url = `/play?${params.toString()}`;
console.log('[PlaySync] Redirecting to:', url); console.log('[PlaySync] Member redirecting to:', url);
// 跳转到新的视频页面 // 使用 window.location.href 强制完整刷新页面,确保播放器重新加载
router.push(url); window.location.href = url;
}; };
socket.on('play:update', handlePlayUpdate); socket.on('play:update', handlePlayUpdate);
@@ -201,7 +268,7 @@ export function usePlaySync({
socket.off('play:seek', handleSeekCommand); socket.off('play:seek', handleSeekCommand);
socket.off('play:change', handleChangeCommand); socket.off('play:change', handleChangeCommand);
}; };
}, [socket, currentRoom, artPlayerRef, isInRoom, router]); }, [socket, currentRoom, isInRoom, isOwner]);
// 监听播放器事件并广播(所有成员都可以触发同步) // 监听播放器事件并广播(所有成员都可以触发同步)
useEffect(() => { useEffect(() => {
@@ -224,34 +291,52 @@ export function usePlaySync({
console.log('[PlaySync] Setting up player event listeners'); console.log('[PlaySync] Setting up player event listeners');
const handlePlay = () => { const handlePlay = () => {
// 如果正在处理远程命令,不要广播(避免循环)
if (isHandlingRemoteCommandRef.current) {
console.log('[PlaySync] Play event triggered by remote command, not broadcasting');
return;
}
const player = artPlayerRef.current; const player = artPlayerRef.current;
if (!player) return; if (!player) return;
// 确认播放器确实在播放状态才广播 // 确认播放器确实在播放状态才广播
if (!player.paused) { if (player.playing) {
console.log('[PlaySync] Play event detected, player is playing, broadcasting...'); console.log('[PlaySync] Play event detected, player is playing, broadcasting...');
// 只发送 play 命令,不发送完整状态(避免重复)
watchRoom.play(); watchRoom.play();
broadcastPlayState();
} else { } else {
console.log('[PlaySync] Play event detected but player is paused, not broadcasting'); console.log('[PlaySync] Play event detected but player is paused, not broadcasting');
} }
}; };
const handlePause = () => { const handlePause = () => {
// 如果正在处理远程命令,不要广播(避免循环)
if (isHandlingRemoteCommandRef.current) {
console.log('[PlaySync] Pause event triggered by remote command, not broadcasting');
return;
}
const player = artPlayerRef.current; const player = artPlayerRef.current;
if (!player) return; if (!player) return;
// 确认播放器确实在暂停状态才广播 // 确认播放器确实在暂停状态才广播
if (player.paused) { if (!player.playing) {
console.log('[PlaySync] Pause event detected, player is paused, broadcasting...'); console.log('[PlaySync] Pause event detected, player is paused, broadcasting...');
// 只发送 pause 命令,不发送完整状态(避免重复)
watchRoom.pause(); watchRoom.pause();
broadcastPlayState();
} else { } else {
console.log('[PlaySync] Pause event detected but player is playing, not broadcasting'); console.log('[PlaySync] Pause event detected but player is playing, not broadcasting');
} }
}; };
const handleSeeked = () => { const handleSeeked = () => {
// 如果正在处理远程命令,不要广播(避免循环)
if (isHandlingRemoteCommandRef.current) {
console.log('[PlaySync] Seeked event triggered by remote command, not broadcasting');
return;
}
const player = artPlayerRef.current; const player = artPlayerRef.current;
if (!player) return; if (!player) return;
@@ -265,7 +350,7 @@ export function usePlaySync({
// 定期同步播放进度(每5秒) // 定期同步播放进度(每5秒)
const syncInterval = setInterval(() => { const syncInterval = setInterval(() => {
if (player.paused) return; // 暂停时不同步 if (!player.playing) return; // 暂停时不同步
console.log('[PlaySync] Periodic sync - broadcasting state'); console.log('[PlaySync] Periodic sync - broadcasting state');
broadcastPlayState(); broadcastPlayState();
@@ -287,13 +372,19 @@ export function usePlaySync({
if (!isOwner || !socket || !currentRoom || !isInRoom || !watchRoom) return; if (!isOwner || !socket || !currentRoom || !isInRoom || !watchRoom) return;
if (!videoId || !videoUrl) return; if (!videoId || !videoUrl) return;
console.log('[PlaySync] Video params changed, will broadcast after delay:', {
videoId,
currentSource,
currentEpisode
});
// 延迟广播,避免初始化时触发 // 延迟广播,避免初始化时触发
const timer = setTimeout(() => { const timer = setTimeout(() => {
const state: PlayState = { const state: PlayState = {
type: 'play', type: 'play',
url: videoUrl, url: videoUrl,
currentTime: artPlayerRef.current?.currentTime || 0, currentTime: artPlayerRef.current?.currentTime || 0,
isPlaying: artPlayerRef.current?.paused === false, isPlaying: artPlayerRef.current?.playing || false,
videoId, videoId,
videoName, videoName,
videoYear, videoYear,
@@ -302,12 +393,12 @@ export function usePlaySync({
source: currentSource, source: currentSource,
}; };
console.log('[PlaySync] Video/episode/source changed, broadcasting:', state); console.log('[PlaySync] Broadcasting play:change:', state);
watchRoom.changeVideo(state); watchRoom.changeVideo(state);
}, 500); }, 1000); // 1秒延迟,给页面足够时间初始化
return () => clearTimeout(timer); 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 { return {
isInRoom, isInRoom,
+43 -3
View File
@@ -15,7 +15,10 @@ import type {
const STORAGE_KEY = 'watch_room_info'; 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<WatchRoomSocket | null>(null); const [socket, setSocket] = useState<WatchRoomSocket | null>(null);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [currentRoom, setCurrentRoom] = useState<Room | null>(null); const [currentRoom, setCurrentRoom] = useState<Room | null>(null);
@@ -264,8 +267,16 @@ export function useWatchRoom(onRoomDeleted?: (data?: { reason?: string }) => voi
const changeVideo = useCallback( const changeVideo = useCallback(
(state: PlayState) => { (state: PlayState) => {
const sock = watchRoomSocketManager.getSocket(); 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); sock.emit('play:change', state);
}, },
[isOwner] [isOwner]
@@ -282,6 +293,22 @@ export function useWatchRoom(onRoomDeleted?: (data?: { reason?: string }) => voi
[isOwner] [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(() => { useEffect(() => {
if (!socket) return; if (!socket) return;
@@ -338,6 +365,17 @@ export function useWatchRoom(onRoomDeleted?: (data?: { reason?: string }) => voi
setChatMessages((prev) => [...prev, message]); 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', () => { socket.on('connect', () => {
setIsConnected(true); setIsConnected(true);
@@ -356,10 +394,11 @@ export function useWatchRoom(onRoomDeleted?: (data?: { reason?: string }) => voi
socket.off('play:change'); socket.off('play:change');
socket.off('live:change'); socket.off('live:change');
socket.off('chat:message'); socket.off('chat:message');
socket.off('state:cleared');
socket.off('connect'); socket.off('connect');
socket.off('disconnect'); socket.off('disconnect');
}; };
}, [socket, currentRoom, onRoomDeleted]); }, [socket, currentRoom, onRoomDeleted, onStateCleared]);
// 清理 // 清理
useEffect(() => { useEffect(() => {
@@ -390,6 +429,7 @@ export function useWatchRoom(onRoomDeleted?: (data?: { reason?: string }) => voi
pause, pause,
changeVideo, changeVideo,
changeLiveChannel, changeLiveChannel,
clearRoomState,
}; };
} }
+47 -4
View File
@@ -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', () => { socket.on('heartbeat', () => {
const roomInfo = this.socketToRoom.get(socket.id); const roomInfo = this.socketToRoom.get(socket.id);
@@ -324,16 +355,28 @@ export class WatchRoomServer {
private startCleanupTimer() { private startCleanupTimer() {
this.cleanupInterval = setInterval(() => { this.cleanupInterval = setInterval(() => {
const now = Date.now(); 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) => { this.rooms.forEach((room, roomId) => {
// 检查房主是否超时 const timeSinceHeartbeat = now - room.lastOwnerHeartbeat;
if (now - room.lastOwnerHeartbeat > timeout) {
// 如果房主心跳超过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...`); console.log(`[WatchRoom] Room ${roomId} owner timeout, deleting...`);
this.deleteRoom(roomId); this.deleteRoom(roomId);
} }
}); });
}, 30000); // 每30秒检查一次 }, 10000); // 每10秒检查一次,确保更及时的清理
} }
private generateRoomId(): string { private generateRoomId(): string {
+3
View File
@@ -77,6 +77,7 @@ export interface ServerToClientEvents {
'voice:offer': (data: { userId: string; offer: RTCSessionDescriptionInit }) => void; 'voice:offer': (data: { userId: string; offer: RTCSessionDescriptionInit }) => void;
'voice:answer': (data: { userId: string; answer: RTCSessionDescriptionInit }) => void; 'voice:answer': (data: { userId: string; answer: RTCSessionDescriptionInit }) => void;
'voice:ice': (data: { userId: string; candidate: RTCIceCandidateInit }) => void; 'voice:ice': (data: { userId: string; candidate: RTCIceCandidateInit }) => void;
'state:cleared': () => void;
'error': (message: string) => void; 'error': (message: string) => void;
} }
@@ -114,6 +115,8 @@ export interface ClientToServerEvents {
'voice:answer': (data: { targetUserId: string; answer: RTCSessionDescriptionInit }) => void; 'voice:answer': (data: { targetUserId: string; answer: RTCSessionDescriptionInit }) => void;
'voice:ice': (data: { targetUserId: string; candidate: RTCIceCandidateInit }) => void; 'voice:ice': (data: { targetUserId: string; candidate: RTCIceCandidateInit }) => void;
'state:clear': (callback?: (response: { success: boolean; error?: string }) => void) => void;
'heartbeat': () => void; 'heartbeat': () => void;
} }