diff --git a/server.js b/server.js
index ebf7e94..1ce336e 100644
--- a/server.js
+++ b/server.js
@@ -32,6 +32,8 @@ class WatchRoomServer {
this.rooms = new Map();
this.members = new Map();
this.socketToRoom = new Map();
+ this.screenHelpers = new Map();
+ this.helperToRoom = new Map();
this.roomDeletionTimers = new Map(); // 房间延迟删除定时器
this.cleanupInterval = null;
this.setupEventHandlers();
@@ -55,6 +57,7 @@ class WatchRoomServer {
description: data.description,
password: data.password,
isPublic: data.isPublic,
+ roomType: data.roomType || 'sync',
ownerId: userId,
ownerName: data.userName,
ownerToken: ownerToken, // 保存房主令牌
@@ -260,6 +263,114 @@ class WatchRoomServer {
}
});
+ socket.on('screen:helper-register', (data, callback) => {
+ try {
+ const room = this.rooms.get(data.roomId);
+ if (!room) {
+ callback({ success: false, error: '房间不存在' });
+ return;
+ }
+
+ if (room.ownerToken !== data.ownerToken) {
+ callback({ success: false, error: '房主身份验证失败' });
+ return;
+ }
+
+ const oldHelperSocketId = this.screenHelpers.get(data.roomId);
+ if (oldHelperSocketId && oldHelperSocketId !== socket.id) {
+ this.helperToRoom.delete(oldHelperSocketId);
+ }
+
+ this.screenHelpers.set(data.roomId, socket.id);
+ this.helperToRoom.set(socket.id, data.roomId);
+ callback({ success: true });
+ } catch (error) {
+ console.error('[WatchRoom] Error registering screen helper:', error);
+ callback({ success: false, error: '注册共享控制窗口失败' });
+ }
+ });
+
+ // 开始屏幕共享
+ socket.on('screen:start', (state) => {
+ const roomInfo = this.socketToRoom.get(socket.id);
+ const helperRoomId = this.helperToRoom.get(socket.id);
+ const roomId = roomInfo?.roomId || helperRoomId;
+ if (!roomId) return;
+ if (helperRoomId && this.screenHelpers.get(helperRoomId) !== socket.id) return;
+ if (roomInfo && !roomInfo.isOwner) return;
+
+ const room = this.rooms.get(roomId);
+ if (room) {
+ room.currentState = state;
+ this.rooms.set(roomId, room);
+ this.io.to(roomId).emit('screen:start', state);
+ }
+ });
+
+ // 停止屏幕共享
+ socket.on('screen:stop', () => {
+ const roomInfo = this.socketToRoom.get(socket.id);
+ const helperRoomId = this.helperToRoom.get(socket.id);
+ const roomId = roomInfo?.roomId || helperRoomId;
+ if (!roomId) return;
+ if (helperRoomId && this.screenHelpers.get(helperRoomId) !== socket.id) return;
+ if (roomInfo && !roomInfo.isOwner) return;
+
+ const room = this.rooms.get(roomId);
+ if (room) {
+ room.currentState = null;
+ this.rooms.set(roomId, room);
+ this.io.to(roomId).emit('screen:stop');
+ }
+ });
+
+ socket.on('screen:viewer-ready', () => {
+ const roomInfo = this.socketToRoom.get(socket.id);
+ if (!roomInfo) return;
+
+ const room = this.rooms.get(roomInfo.roomId);
+ if (!room || roomInfo.isOwner || room.currentState?.type !== 'screen') return;
+
+ const targetSocketId = this.screenHelpers.get(roomInfo.roomId) || room.ownerId;
+ this.io.to(targetSocketId).emit('screen:viewer-ready', {
+ userId: socket.id,
+ });
+ });
+
+ // 屏幕共享 WebRTC 信令
+ socket.on('screen:offer', (data) => {
+ const roomInfo = this.socketToRoom.get(socket.id);
+ const helperRoomId = this.helperToRoom.get(socket.id);
+ if (!roomInfo && !helperRoomId) return;
+
+ this.io.to(data.targetUserId).emit('screen:offer', {
+ userId: socket.id,
+ offer: data.offer,
+ });
+ });
+
+ socket.on('screen:answer', (data) => {
+ const roomInfo = this.socketToRoom.get(socket.id);
+ const helperRoomId = this.helperToRoom.get(socket.id);
+ if (!roomInfo && !helperRoomId) return;
+
+ this.io.to(data.targetUserId).emit('screen:answer', {
+ userId: socket.id,
+ answer: data.answer,
+ });
+ });
+
+ socket.on('screen:ice', (data) => {
+ const roomInfo = this.socketToRoom.get(socket.id);
+ const helperRoomId = this.helperToRoom.get(socket.id);
+ if (!roomInfo && !helperRoomId) return;
+
+ this.io.to(data.targetUserId).emit('screen:ice', {
+ userId: socket.id,
+ candidate: data.candidate,
+ });
+ });
+
// 聊天消息
socket.on('chat:message', (data) => {
const roomInfo = this.socketToRoom.get(socket.id);
@@ -347,6 +458,19 @@ class WatchRoomServer {
// 断开连接
socket.on('disconnect', () => {
console.log(`[WatchRoom] Client disconnected: ${socket.id}`);
+ const helperRoomId = this.helperToRoom.get(socket.id);
+ if (helperRoomId) {
+ this.helperToRoom.delete(socket.id);
+ if (this.screenHelpers.get(helperRoomId) === socket.id) {
+ this.screenHelpers.delete(helperRoomId);
+ const room = this.rooms.get(helperRoomId);
+ if (room && room.currentState?.type === 'screen') {
+ room.currentState = null;
+ this.rooms.set(helperRoomId, room);
+ this.io.to(helperRoomId).emit('screen:stop');
+ }
+ }
+ }
this.handleLeaveRoom(socket);
});
});
@@ -425,6 +549,11 @@ class WatchRoomServer {
this.rooms.delete(roomId);
this.members.delete(roomId);
+ const helperSocketId = this.screenHelpers.get(roomId);
+ if (helperSocketId) {
+ this.helperToRoom.delete(helperSocketId);
+ this.screenHelpers.delete(roomId);
+ }
}
startCleanupTimer() {
diff --git a/src/app/watch-room/page.tsx b/src/app/watch-room/page.tsx
index a824fb6..0c2eaf8 100644
--- a/src/app/watch-room/page.tsx
+++ b/src/app/watch-room/page.tsx
@@ -10,7 +10,7 @@ import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
import PageLayout from '@/components/PageLayout';
import { useWatchRoomContext } from '@/components/WatchRoomProvider';
-import type { Room } from '@/types/watch-room';
+import type { Room, RoomType } from '@/types/watch-room';
type TabType = 'create' | 'join' | 'list';
@@ -34,6 +34,7 @@ export default function WatchRoomPage() {
description: '',
password: '',
isPublic: true,
+ roomType: 'sync' as RoomType,
});
// 加入房间表单
@@ -49,26 +50,30 @@ export default function WatchRoomPage() {
const [joinLoading, setJoinLoading] = useState(false);
// 加载房间列表
- const loadRooms = async () => {
+ const loadRooms = async (showLoading = false) => {
if (!isConnected) return;
- setLoading(true);
+ if (showLoading) {
+ setLoading(true);
+ }
try {
const roomList = await getRoomList();
setRooms(roomList);
} catch (error) {
console.error('[WatchRoom] Failed to load rooms:', error);
} finally {
- setLoading(false);
+ if (showLoading) {
+ setLoading(false);
+ }
}
};
// 切换到房间列表 tab 时加载房间
useEffect(() => {
if (activeTab === 'list') {
- loadRooms();
+ loadRooms(true);
// 每5秒刷新一次
- const interval = setInterval(loadRooms, 5000);
+ const interval = setInterval(() => loadRooms(false), 5000);
return () => clearInterval(interval);
}
}, [activeTab, isConnected]);
@@ -88,6 +93,7 @@ export default function WatchRoomPage() {
description: createForm.description.trim(),
password: createForm.password.trim() || undefined,
isPublic: createForm.isPublic,
+ roomType: createForm.roomType,
userName: currentUsername,
});
@@ -97,6 +103,7 @@ export default function WatchRoomPage() {
description: '',
password: '',
isPublic: true,
+ roomType: 'sync',
});
} catch (error: any) {
alert(error.message || '创建房间失败');
@@ -141,6 +148,11 @@ export default function WatchRoomPage() {
useEffect(() => {
if (!currentRoom || isOwner) return;
+ if (currentRoom.roomType === 'screen') {
+ router.push('/watch-room/screen');
+ return;
+ }
+
// 房员加入房间后,不立即跳转
// 而是监听 play:change 或 live:change 事件(说明房主正在活跃使用)
// 这样可以避免房主已经离开play页面但状态未清除的情况
@@ -153,6 +165,8 @@ export default function WatchRoomPage() {
useEffect(() => {
if (!currentRoom || isOwner) return;
+ if (currentRoom.roomType === 'screen') return;
+
const handlePlayChange = (state: any) => {
if (state.type === 'play') {
const params = new URLSearchParams({
@@ -196,6 +210,13 @@ export default function WatchRoomPage() {
}
}, [currentRoom, isOwner, router, socket]);
+ // 屏幕共享房间创建/加入后直接进入共享页
+ useEffect(() => {
+ if (currentRoom?.roomType === 'screen') {
+ router.push('/watch-room/screen');
+ }
+ }, [currentRoom?.id, currentRoom?.roomType, router]);
+
// 从房间列表加入房间
const handleJoinFromList = (room: Room) => {
setJoinForm({
@@ -237,7 +258,9 @@ export default function WatchRoomPage() {
- {currentRoom.currentState ? '房主正在播放' : '等待房主开始播放'}
+ {currentRoom.roomType === 'screen'
+ ? currentRoom.currentState?.type === 'screen' ? '房主正在共享屏幕' : '等待房主开始共享'
+ : currentRoom.currentState ? '房主正在播放' : '等待房主开始播放'}
房间: {currentRoom.name} | 房主: {currentRoom.ownerName}
@@ -246,12 +269,14 @@ export default function WatchRoomPage() {
{currentRoom.currentState.type === 'play'
? `${currentRoom.currentState.videoName || '未知视频'}`
- : `${currentRoom.currentState.channelName || '未知频道'}`}
+ : currentRoom.currentState.type === 'live'
+ ? `${currentRoom.currentState.channelName || '未知频道'}`
+ : '屏幕共享进行中'}
)}
{!currentRoom.currentState && (
- 当房主开始播放时,您将自动跟随
+ {currentRoom.roomType === 'screen' ? '当房主开始共享时,您将自动进入共享页' : '当房主开始播放时,您将自动跟随'}
)}
@@ -280,6 +305,8 @@ export default function WatchRoomPage() {
// 普通 live 格式,导航到 live 页面
router.push(`/live?id=${state.channelId}`);
}
+ } else if (state.type === 'screen') {
+ router.push('/watch-room/screen');
}
}}
className="px-6 py-2 bg-white text-blue-600 font-medium rounded-lg hover:bg-white/90 transition-colors whitespace-nowrap"
@@ -303,7 +330,7 @@ export default function WatchRoomPage() {
)}
- 与好友一起看视频,实时同步播放
+ 与好友一起看视频,支持进度同步或屏幕共享
@@ -360,7 +387,7 @@ export default function WatchRoomPage() {
)}
-
+
房间号
{currentRoom.id}
@@ -369,6 +396,10 @@ export default function WatchRoomPage() {
成员数
{members.length} 人
+
+
房间类型
+
{currentRoom.roomType === 'screen' ? '屏幕共享' : '进度同步'}
+
@@ -402,7 +433,9 @@ export default function WatchRoomPage() {
{/* 提示信息 */}
- 💡 前往播放页面或直播页面开始观影,房间成员将自动同步您的操作
+ 💡 {currentRoom.roomType === 'screen'
+ ? '这是屏幕共享房间,创建后将进入共享页,由房主发起屏幕共享'
+ : '前往播放页面或直播页面开始观影,房间成员将自动同步您的操作'}
@@ -471,6 +504,38 @@ export default function WatchRoomPage() {
+
+
+
+
+
+
+
+