屏幕共享优化

This commit is contained in:
mtvpls
2026-04-06 01:30:24 +08:00
parent 14d4c483f3
commit 23b8115f36
8 changed files with 272 additions and 33 deletions
+72 -7
View File
@@ -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,
+33 -8
View File
@@ -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(() => {