屏幕共享优化
This commit is contained in:
@@ -583,7 +583,7 @@ export default function WatchRoomPage() {
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100">进度同步</div>
|
||||
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">统一播放进度</div>
|
||||
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">统一播放进度(适合双方网络稳定的情况)</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -595,7 +595,7 @@ export default function WatchRoomPage() {
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100">屏幕共享</div>
|
||||
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">房员直接观看房主共享的浏览器画面</div>
|
||||
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">房员直接观看房主共享的浏览器画面(适合完全实时同步的情况)</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
import { Monitor, MonitorPlay, Users } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import Toast, { ToastProps } from '@/components/Toast';
|
||||
import { useWatchRoomContext } from '@/components/WatchRoomProvider';
|
||||
import { useScreenShare } from '@/hooks/useScreenShare';
|
||||
import { screenShareQualityOptions, type ScreenShareQualityPreset, useScreenShare } from '@/hooks/useScreenShare';
|
||||
|
||||
const NEW_TAB_KEY_PREFIX = 'watch_room_screen_home_opened_';
|
||||
const WATCH_ROOM_NO_CONNECT_KEY = 'watch_room_no_connect';
|
||||
const SCREEN_SHARE_QUALITY_KEY = 'watch_room_screen_quality';
|
||||
|
||||
function getScreenShareHostSupportError() {
|
||||
if (typeof window === 'undefined') return null;
|
||||
@@ -44,17 +46,19 @@ export default function WatchRoomScreenPage() {
|
||||
const watchRoom = useWatchRoomContext();
|
||||
const { currentRoom, members, leaveRoom } = watchRoom;
|
||||
const [toast, setToast] = useState<ToastProps | null>(null);
|
||||
const [qualityPreset, setQualityPreset] = useState<ScreenShareQualityPreset>('smooth');
|
||||
const {
|
||||
currentRoom: screenRoom,
|
||||
isOwner,
|
||||
isSharing,
|
||||
isStarting,
|
||||
error,
|
||||
captureSettings,
|
||||
localVideoRef,
|
||||
remoteVideoRef,
|
||||
startSharing,
|
||||
stopSharing,
|
||||
} = useScreenShare();
|
||||
} = useScreenShare(qualityPreset);
|
||||
|
||||
const showToast = (message: string, type: ToastProps['type'] = 'info') => {
|
||||
setToast({
|
||||
@@ -65,6 +69,24 @@ export default function WatchRoomScreenPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const openDetachedPage = useCallback(() => {
|
||||
window.open('/', '_blank', 'noopener,noreferrer');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const saved = window.localStorage.getItem(SCREEN_SHARE_QUALITY_KEY);
|
||||
if (saved === 'smooth' || saved === 'hd' || saved === 'ultra') {
|
||||
setQualityPreset(saved);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.setItem(SCREEN_SHARE_QUALITY_KEY, qualityPreset);
|
||||
}, [qualityPreset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentRoom) {
|
||||
router.replace('/watch-room');
|
||||
@@ -92,12 +114,17 @@ export default function WatchRoomScreenPage() {
|
||||
useEffect(() => {
|
||||
if (!screenRoom || !isOwner) return;
|
||||
|
||||
localStorage.setItem(WATCH_ROOM_NO_CONNECT_KEY, '1');
|
||||
const key = `${NEW_TAB_KEY_PREFIX}${screenRoom.id}`;
|
||||
if (sessionStorage.getItem(key)) return;
|
||||
if (!sessionStorage.getItem(key)) {
|
||||
sessionStorage.setItem(key, '1');
|
||||
openDetachedPage();
|
||||
}
|
||||
|
||||
sessionStorage.setItem(key, '1');
|
||||
window.open('/?watchRoomNoConnect=1', '_blank', 'noopener,noreferrer');
|
||||
}, [isOwner, screenRoom?.id]);
|
||||
return () => {
|
||||
localStorage.removeItem(WATCH_ROOM_NO_CONNECT_KEY);
|
||||
};
|
||||
}, [isOwner, openDetachedPage, screenRoom?.id]);
|
||||
|
||||
if (!screenRoom || screenRoom.roomType !== 'screen') {
|
||||
return null;
|
||||
@@ -111,6 +138,15 @@ export default function WatchRoomScreenPage() {
|
||||
router.push('/watch-room');
|
||||
};
|
||||
|
||||
const captureSettingsText = captureSettings
|
||||
? [
|
||||
captureSettings.width && captureSettings.height
|
||||
? `${captureSettings.width}x${captureSettings.height}`
|
||||
: '分辨率未知',
|
||||
captureSettings.frameRate ? `${Math.round(captureSettings.frameRate)} fps` : '帧率未知',
|
||||
].join(' / ')
|
||||
: '未开始';
|
||||
|
||||
return (
|
||||
<div className='min-h-screen bg-white text-gray-900 dark:bg-black dark:text-gray-200'>
|
||||
<div className='mx-auto flex min-h-screen max-w-7xl flex-col gap-4 px-4 py-4 lg:px-8'>
|
||||
@@ -127,9 +163,13 @@ export default function WatchRoomScreenPage() {
|
||||
<div className='flex items-center gap-2'>
|
||||
{isOwner && (
|
||||
<Link
|
||||
href='/?watchRoomNoConnect=1'
|
||||
href='/'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
openDetachedPage();
|
||||
}}
|
||||
className='rounded-lg bg-blue-500 px-4 py-2 text-white'
|
||||
>
|
||||
新开主页
|
||||
@@ -188,12 +228,41 @@ export default function WatchRoomScreenPage() {
|
||||
<p>成员:{members.length} 人</p>
|
||||
</div>
|
||||
|
||||
{isOwner && (
|
||||
<div className='mt-2 text-sm text-gray-600 dark:text-gray-400'>
|
||||
实际采集:{captureSettingsText}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className='mt-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-300'>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOwner && (
|
||||
<div className='mt-4'>
|
||||
<label className='mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
共享画质
|
||||
</label>
|
||||
<select
|
||||
value={qualityPreset}
|
||||
onChange={(event) => setQualityPreset(event.target.value as ScreenShareQualityPreset)}
|
||||
disabled={isStarting || isSharing}
|
||||
className='w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 disabled:cursor-not-allowed disabled:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:disabled:bg-gray-900'
|
||||
>
|
||||
{screenShareQualityOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className='mt-2 text-xs text-gray-500 dark:text-gray-400'>
|
||||
画质越高越清晰,但更依赖网络和设备性能。共享开始后不可切换。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='mt-4 flex gap-3'>
|
||||
{isOwner ? (
|
||||
<>
|
||||
@@ -243,7 +312,7 @@ export default function WatchRoomScreenPage() {
|
||||
</div>
|
||||
|
||||
<div className='rounded-xl border border-blue-200 bg-blue-50 p-4 text-sm text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200'>
|
||||
本页不再包裹站点导航,便于直接共享。建议使用桌面版 Chrome / Edge,并优先共享标签页。
|
||||
建议使用桌面版 Chrome / Edge,并优先共享标签页。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useCallback,useContext, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { useWatchRoom } from '@/hooks/useWatchRoom';
|
||||
|
||||
@@ -14,6 +13,8 @@ import type { ChatMessage, Member, Room, RoomType, ScreenState, WatchRoomConfig
|
||||
|
||||
// Import type from watch-room-socket
|
||||
type WatchRoomSocket = import('@/lib/watch-room-socket').WatchRoomSocket;
|
||||
const WATCH_ROOM_NO_CONNECT_KEY = 'watch_room_no_connect';
|
||||
const WATCH_ROOM_SCREEN_PATH = '/watch-room/screen';
|
||||
|
||||
interface WatchRoomContextType {
|
||||
socket: WatchRoomSocket | null;
|
||||
@@ -39,6 +40,7 @@ interface WatchRoomContextType {
|
||||
roomId: string;
|
||||
password?: string;
|
||||
userName: string;
|
||||
ownerToken?: string;
|
||||
}) => Promise<{ room: Room; members: Member[] }>;
|
||||
leaveRoom: () => void;
|
||||
getRoomList: () => Promise<Room[]>;
|
||||
@@ -81,12 +83,12 @@ interface WatchRoomProviderProps {
|
||||
}
|
||||
|
||||
export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const [config, setConfig] = useState<WatchRoomConfig | null>(null);
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
const [toast, setToast] = useState<ToastProps | null>(null);
|
||||
const [reconnectFailed, setReconnectFailed] = useState(false);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [shouldDisableWatchRoomConnection, setShouldDisableWatchRoomConnection] = useState<boolean | null>(null);
|
||||
|
||||
// 处理房间删除的回调
|
||||
const handleRoomDeleted = useCallback((data?: { reason?: string }) => {
|
||||
@@ -123,7 +125,15 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
||||
}, []);
|
||||
|
||||
const watchRoom = useWatchRoom(handleRoomDeleted, handleStateCleared);
|
||||
const shouldDisableWatchRoomConnection = searchParams.get('watchRoomNoConnect') === '1';
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
setShouldDisableWatchRoomConnection(
|
||||
window.location.pathname !== WATCH_ROOM_SCREEN_PATH
|
||||
&& window.localStorage.getItem(WATCH_ROOM_NO_CONNECT_KEY) === '1'
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 检查登录状态
|
||||
useEffect(() => {
|
||||
@@ -162,6 +172,7 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
||||
roomId: info.roomId,
|
||||
password: info.password,
|
||||
userName: info.userName,
|
||||
ownerToken: info.ownerToken,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[WatchRoomProvider] Failed to rejoin room after reconnect:', error);
|
||||
@@ -175,6 +186,10 @@ export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
||||
|
||||
// 加载配置
|
||||
useEffect(() => {
|
||||
if (shouldDisableWatchRoomConnection === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldDisableWatchRoomConnection) {
|
||||
setConfig({
|
||||
enabled: false,
|
||||
|
||||
@@ -12,7 +12,51 @@ const iceServers = [
|
||||
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||
];
|
||||
|
||||
export function useScreenShare() {
|
||||
export type ScreenShareQualityPreset = 'smooth' | 'hd' | 'ultra';
|
||||
|
||||
const SCREEN_SHARE_CONSTRAINTS: Record<
|
||||
ScreenShareQualityPreset,
|
||||
{
|
||||
label: string;
|
||||
frameRate: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
> = {
|
||||
smooth: {
|
||||
label: '流畅 720p / 15fps',
|
||||
frameRate: 15,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
hd: {
|
||||
label: '高清 1080p / 30fps',
|
||||
frameRate: 30,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
ultra: {
|
||||
label: '超清 1440p / 30fps',
|
||||
frameRate: 30,
|
||||
width: 2560,
|
||||
height: 1440,
|
||||
},
|
||||
};
|
||||
|
||||
export const screenShareQualityOptions = Object.entries(SCREEN_SHARE_CONSTRAINTS).map(
|
||||
([value, preset]) => ({
|
||||
value: value as ScreenShareQualityPreset,
|
||||
label: preset.label,
|
||||
})
|
||||
);
|
||||
|
||||
export interface ScreenShareCaptureSettings {
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
frameRate: number | null;
|
||||
}
|
||||
|
||||
export function useScreenShare(qualityPreset: ScreenShareQualityPreset = 'smooth') {
|
||||
const watchRoom = useWatchRoomContextSafe();
|
||||
const localVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const remoteVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||
@@ -23,9 +67,11 @@ export function useScreenShare() {
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
const [captureSettings, setCaptureSettings] = useState<ScreenShareCaptureSettings | null>(null);
|
||||
|
||||
const currentRoom = watchRoom?.currentRoom || null;
|
||||
const socket = watchRoom?.socket || null;
|
||||
const isConnected = watchRoom?.isConnected || false;
|
||||
const isOwner = watchRoom?.isOwner || false;
|
||||
const members = watchRoom?.members || [];
|
||||
const currentState = currentRoom?.currentState;
|
||||
@@ -67,6 +113,7 @@ export function useScreenShare() {
|
||||
localVideoRef.current.srcObject = null;
|
||||
}
|
||||
|
||||
setCaptureSettings(null);
|
||||
clearRemoteVideo();
|
||||
stoppingRef.current = false;
|
||||
}, [clearRemoteVideo, closePeerConnection]);
|
||||
@@ -136,11 +183,12 @@ export function useScreenShare() {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const constraints = SCREEN_SHARE_CONSTRAINTS[qualityPreset];
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: {
|
||||
frameRate: 15,
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
frameRate: constraints.frameRate,
|
||||
width: { ideal: constraints.width },
|
||||
height: { ideal: constraints.height },
|
||||
},
|
||||
audio: true,
|
||||
});
|
||||
@@ -152,6 +200,12 @@ export function useScreenShare() {
|
||||
|
||||
const videoTrack = stream.getVideoTracks()[0];
|
||||
if (videoTrack) {
|
||||
const settings = videoTrack.getSettings();
|
||||
setCaptureSettings({
|
||||
width: typeof settings.width === 'number' ? settings.width : null,
|
||||
height: typeof settings.height === 'number' ? settings.height : null,
|
||||
frameRate: typeof settings.frameRate === 'number' ? settings.frameRate : null,
|
||||
});
|
||||
videoTrack.onended = () => {
|
||||
stopSharing(true);
|
||||
};
|
||||
@@ -176,7 +230,7 @@ export function useScreenShare() {
|
||||
} finally {
|
||||
setIsStarting(false);
|
||||
}
|
||||
}, [currentRoom, isOwner, members, sendOfferToMember, stopSharing, watchRoom]);
|
||||
}, [currentRoom, isOwner, members, qualityPreset, sendOfferToMember, stopSharing, watchRoom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket || !currentRoom) return;
|
||||
@@ -231,6 +285,14 @@ export function useScreenShare() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSocketDisconnect = () => {
|
||||
if (!isOwner) {
|
||||
peerConnectionsRef.current.forEach((_pc, userId) => closePeerConnection(userId));
|
||||
peerConnectionsRef.current.clear();
|
||||
clearRemoteVideo();
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewerReady = (data: { userId: string }) => {
|
||||
if (!isOwner || !displayStreamRef.current) return;
|
||||
sendOfferToMember(data.userId);
|
||||
@@ -241,6 +303,7 @@ export function useScreenShare() {
|
||||
socket.on('screen:ice', handleIce);
|
||||
socket.on('screen:stop', handleScreenStop);
|
||||
socket.on('screen:viewer-ready', handleViewerReady);
|
||||
socket.on('disconnect', handleSocketDisconnect);
|
||||
|
||||
return () => {
|
||||
socket.off('screen:offer', handleOffer);
|
||||
@@ -248,6 +311,7 @@ export function useScreenShare() {
|
||||
socket.off('screen:ice', handleIce);
|
||||
socket.off('screen:stop', handleScreenStop);
|
||||
socket.off('screen:viewer-ready', handleViewerReady);
|
||||
socket.off('disconnect', handleSocketDisconnect);
|
||||
};
|
||||
}, [clearRemoteVideo, closePeerConnection, createPeerConnection, currentRoom, isOwner, sendOfferToMember, socket]);
|
||||
|
||||
@@ -277,11 +341,11 @@ export function useScreenShare() {
|
||||
}, [cleanupSharingResources]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket || !currentRoom || isOwner) return;
|
||||
if (!socket || !currentRoom || isOwner || !isConnected) return;
|
||||
if (currentState?.type !== 'screen' || currentState.status !== 'sharing') return;
|
||||
|
||||
socket.emit('screen:viewer-ready');
|
||||
}, [currentRoom, currentState, isOwner, socket]);
|
||||
}, [currentRoom, currentState, isConnected, isOwner, socket]);
|
||||
|
||||
return {
|
||||
currentRoom,
|
||||
@@ -289,6 +353,7 @@ export function useScreenShare() {
|
||||
isSharing,
|
||||
isStarting,
|
||||
error,
|
||||
captureSettings,
|
||||
localVideoRef,
|
||||
remoteVideoRef,
|
||||
startSharing,
|
||||
|
||||
@@ -30,9 +30,15 @@ export function useWatchRoom(
|
||||
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
|
||||
const [isOwner, setIsOwner] = useState(false);
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const rejoinInFlightRef = useRef(false);
|
||||
|
||||
// 重新加入房间(自动重连)
|
||||
const rejoinRoom = useCallback(async (info: StoredRoomInfo) => {
|
||||
if (rejoinInFlightRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
rejoinInFlightRef.current = true;
|
||||
console.log('[WatchRoom] Auto-rejoining room:', info);
|
||||
try {
|
||||
const sock = watchRoomSocketManager.getSocket();
|
||||
@@ -64,9 +70,21 @@ export function useWatchRoom(
|
||||
} catch (error) {
|
||||
console.error('[WatchRoom] Failed to rejoin room:', error);
|
||||
clearStoredRoomInfo();
|
||||
} finally {
|
||||
rejoinInFlightRef.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const scheduleRejoin = useCallback((info: StoredRoomInfo, delay = 300) => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
}
|
||||
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
rejoinRoom(info);
|
||||
}, delay);
|
||||
}, [rejoinRoom]);
|
||||
|
||||
// 连接到服务器
|
||||
const connect = useCallback(async (config: WatchRoomConfig) => {
|
||||
try {
|
||||
@@ -78,15 +96,13 @@ export function useWatchRoom(
|
||||
const storedInfo = getStoredRoomInfo();
|
||||
if (storedInfo) {
|
||||
console.log('[WatchRoom] Attempting to reconnect to room:', storedInfo.roomId);
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
rejoinRoom(storedInfo);
|
||||
}, 1000);
|
||||
scheduleRejoin(storedInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WatchRoom] Failed to connect:', error);
|
||||
setIsConnected(false);
|
||||
}
|
||||
}, [rejoinRoom]);
|
||||
}, [scheduleRejoin]);
|
||||
|
||||
// 断开连接
|
||||
const disconnect = useCallback(() => {
|
||||
@@ -99,6 +115,7 @@ export function useWatchRoom(
|
||||
setCurrentRoom(null);
|
||||
setMembers([]);
|
||||
setChatMessages([]);
|
||||
setIsOwner(false);
|
||||
}, []);
|
||||
|
||||
// 创建房间
|
||||
@@ -142,7 +159,7 @@ export function useWatchRoom(
|
||||
|
||||
// 加入房间
|
||||
const joinRoom = useCallback(
|
||||
async (data: { roomId: string; password?: string; userName: string }) => {
|
||||
async (data: { roomId: string; password?: string; userName: string; ownerToken?: string }) => {
|
||||
const sock = watchRoomSocketManager.getSocket();
|
||||
if (!sock || !watchRoomSocketManager.isConnected()) {
|
||||
throw new Error('Not connected');
|
||||
@@ -162,7 +179,7 @@ export function useWatchRoom(
|
||||
isOwner: isRoomOwner,
|
||||
userName: data.userName,
|
||||
password: data.password,
|
||||
ownerToken: isRoomOwner ? response.room.ownerToken : undefined,
|
||||
ownerToken: isRoomOwner ? (response.room.ownerToken || data.ownerToken) : undefined,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
resolve({ room: response.room, members: response.members });
|
||||
@@ -343,7 +360,11 @@ export function useWatchRoom(
|
||||
});
|
||||
|
||||
socket.on('room:member-joined', (member) => {
|
||||
setMembers((prev) => [...prev, member]);
|
||||
setMembers((prev) => {
|
||||
const next = prev.filter((existing) => existing.id !== member.id);
|
||||
next.push(member);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('room:member-left', (userId) => {
|
||||
@@ -415,6 +436,10 @@ export function useWatchRoom(
|
||||
// 连接状态
|
||||
socket.on('connect', () => {
|
||||
setIsConnected(true);
|
||||
const storedInfo = getStoredRoomInfo();
|
||||
if (storedInfo) {
|
||||
scheduleRejoin(storedInfo);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
@@ -436,7 +461,7 @@ export function useWatchRoom(
|
||||
socket.off('connect');
|
||||
socket.off('disconnect');
|
||||
};
|
||||
}, [socket, currentRoom, onRoomDeleted, onStateCleared]);
|
||||
}, [socket, currentRoom, onRoomDeleted, onStateCleared, scheduleRejoin]);
|
||||
|
||||
// 清理
|
||||
useEffect(() => {
|
||||
|
||||
@@ -92,15 +92,33 @@ export class WatchRoomServer {
|
||||
}
|
||||
|
||||
const userId = socket.id;
|
||||
let isOwner = false;
|
||||
|
||||
if (data.ownerToken && data.ownerToken === room.ownerToken) {
|
||||
isOwner = true;
|
||||
room.ownerId = userId;
|
||||
room.lastOwnerHeartbeat = Date.now();
|
||||
this.rooms.set(data.roomId, room);
|
||||
console.log(`[WatchRoom] Owner ${data.userName} reconnected to room ${data.roomId}`);
|
||||
}
|
||||
|
||||
const member: Member = {
|
||||
id: userId,
|
||||
name: data.userName,
|
||||
isOwner: false,
|
||||
isOwner,
|
||||
lastHeartbeat: Date.now(),
|
||||
};
|
||||
|
||||
const roomMembers = this.members.get(data.roomId);
|
||||
if (roomMembers) {
|
||||
if (isOwner) {
|
||||
Array.from(roomMembers.entries()).forEach(([memberId, existingMember]) => {
|
||||
if (existingMember.isOwner && memberId !== userId) {
|
||||
roomMembers.delete(memberId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
roomMembers.set(userId, member);
|
||||
room.memberCount = roomMembers.size;
|
||||
this.rooms.set(data.roomId, room);
|
||||
@@ -110,7 +128,7 @@ export class WatchRoomServer {
|
||||
roomId: data.roomId,
|
||||
userId,
|
||||
userName: data.userName,
|
||||
isOwner: false,
|
||||
isOwner,
|
||||
});
|
||||
|
||||
socket.join(data.roomId);
|
||||
@@ -118,7 +136,7 @@ export class WatchRoomServer {
|
||||
// 通知房间内其他成员
|
||||
socket.to(data.roomId).emit('room:member-joined', member);
|
||||
|
||||
console.log(`[WatchRoom] User ${data.userName} joined room ${data.roomId}`);
|
||||
console.log(`[WatchRoom] User ${data.userName} joined room ${data.roomId}${isOwner ? ' (as owner)' : ''}`);
|
||||
|
||||
const members = Array.from(roomMembers?.values() || []);
|
||||
callback({ success: true, room, members });
|
||||
|
||||
@@ -13,6 +13,7 @@ export type WatchRoomSocket = Socket<ServerToClientEvents, ClientToServerEvents>
|
||||
class WatchRoomSocketManager {
|
||||
private socket: WatchRoomSocket | null = null;
|
||||
private config: WatchRoomConfig | null = null;
|
||||
private connectionPromise: Promise<WatchRoomSocket> | null = null;
|
||||
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||
private heartbeatTimeoutCheck: NodeJS.Timeout | null = null;
|
||||
private lastHeartbeatResponse: number = Date.now();
|
||||
@@ -25,6 +26,37 @@ class WatchRoomSocketManager {
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
if (this.socket) {
|
||||
this.connectionPromise = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.connectionPromise = null;
|
||||
reject(new Error('Socket connection timeout'));
|
||||
}, 10000);
|
||||
|
||||
this.socket!.once('connect', () => {
|
||||
clearTimeout(timeout);
|
||||
this.connectionPromise = null;
|
||||
resolve(this.socket!);
|
||||
});
|
||||
|
||||
this.socket!.once('connect_error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
this.connectionPromise = null;
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (!this.socket!.connected) {
|
||||
this.socket!.connect();
|
||||
}
|
||||
});
|
||||
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
|
||||
const socketOptions = {
|
||||
@@ -72,8 +104,9 @@ class WatchRoomSocketManager {
|
||||
// 设置浏览器可见性监听
|
||||
this.setupVisibilityListener();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connectionPromise = new Promise((resolve, reject) => {
|
||||
if (!this.socket) {
|
||||
this.connectionPromise = null;
|
||||
reject(new Error('Socket not initialized'));
|
||||
return;
|
||||
}
|
||||
@@ -82,6 +115,7 @@ class WatchRoomSocketManager {
|
||||
this.socket.once('connect', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[WatchRoom] Connected to server');
|
||||
this.connectionPromise = null;
|
||||
if (this.socket) {
|
||||
resolve(this.socket);
|
||||
}
|
||||
@@ -90,9 +124,12 @@ class WatchRoomSocketManager {
|
||||
this.socket.once('connect_error', (error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[WatchRoom] Connection error:', error);
|
||||
this.connectionPromise = null;
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
@@ -122,6 +159,8 @@ class WatchRoomSocketManager {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
}
|
||||
|
||||
this.connectionPromise = null;
|
||||
}
|
||||
|
||||
getSocket(): WatchRoomSocket | null {
|
||||
|
||||
Reference in New Issue
Block a user