观影室增加屏幕共享

This commit is contained in:
mtvpls
2026-04-05 00:15:53 +08:00
parent 0285162e2f
commit 2ab48b678f
10 changed files with 946 additions and 23 deletions
+95 -18
View File
@@ -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() {
</div>
<div className="flex-1">
<h3 className="text-lg font-bold mb-1">
{currentRoom.currentState ? '房主正在播放' : '等待房主开始播放'}
{currentRoom.roomType === 'screen'
? currentRoom.currentState?.type === 'screen' ? '房主正在共享屏幕' : '等待房主开始共享'
: currentRoom.currentState ? '房主正在播放' : '等待房主开始播放'}
</h3>
<p className="text-sm text-white/80">
: {currentRoom.name} | : {currentRoom.ownerName}
@@ -246,12 +269,14 @@ export default function WatchRoomPage() {
<p className="text-xs text-white/90 mt-1">
{currentRoom.currentState.type === 'play'
? `${currentRoom.currentState.videoName || '未知视频'}`
: `${currentRoom.currentState.channelName || '未知频道'}`}
: currentRoom.currentState.type === 'live'
? `${currentRoom.currentState.channelName || '未知频道'}`
: '屏幕共享进行中'}
</p>
)}
{!currentRoom.currentState && (
<p className="text-xs text-white/70 mt-1">
{currentRoom.roomType === 'screen' ? '当房主开始共享时,您将自动进入共享页' : '当房主开始播放时,您将自动跟随'}
</p>
)}
</div>
@@ -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() {
)}
</h1>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
</p>
</div>
@@ -360,7 +387,7 @@ export default function WatchRoomPage() {
)}
</div>
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
<div className="bg-white/10 backdrop-blur rounded-lg p-3">
<p className="text-blue-100 text-xs mb-1"></p>
<p className="text-xl font-mono font-bold">{currentRoom.id}</p>
@@ -369,6 +396,10 @@ export default function WatchRoomPage() {
<p className="text-blue-100 text-xs mb-1"></p>
<p className="text-xl font-bold">{members.length} </p>
</div>
<div className="bg-white/10 backdrop-blur rounded-lg p-3">
<p className="text-blue-100 text-xs mb-1"></p>
<p className="text-base font-bold">{currentRoom.roomType === 'screen' ? '屏幕共享' : '进度同步'}</p>
</div>
</div>
</div>
@@ -402,7 +433,9 @@ export default function WatchRoomPage() {
{/* 提示信息 */}
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
<p className="text-sm text-blue-800 dark:text-blue-200">
💡
💡 {currentRoom.roomType === 'screen'
? '这是屏幕共享房间,创建后将进入共享页,由房主发起屏幕共享'
: '前往播放页面或直播页面开始观影,房间成员将自动同步您的操作'}
</p>
</div>
</div>
@@ -471,6 +504,38 @@ export default function WatchRoomPage() {
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button
type="button"
onClick={() => setCreateForm({ ...createForm, roomType: 'sync' })}
className={`rounded-lg border p-4 text-left transition-colors ${
createForm.roomType === 'sync'
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 dark:border-gray-600'
}`}
>
<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>
</button>
<button
type="button"
onClick={() => setCreateForm({ ...createForm, roomType: 'screen' })}
className={`rounded-lg border p-4 text-left transition-colors ${
createForm.roomType === 'screen'
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 dark:border-gray-600'
}`}
>
<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>
</button>
</div>
</div>
<button
type="submit"
disabled={createLoading || !createForm.roomName.trim()}
@@ -486,7 +551,7 @@ export default function WatchRoomPage() {
{!currentRoom && (
<div className="mt-6 bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong></strong>
<strong></strong>
</p>
</div>
)}
@@ -518,7 +583,7 @@ export default function WatchRoomPage() {
)}
</div>
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
<div className="bg-white/10 backdrop-blur rounded-lg p-3">
<p className="text-green-100 text-xs mb-1"></p>
<p className="text-xl font-mono font-bold">{currentRoom.id}</p>
@@ -527,6 +592,10 @@ export default function WatchRoomPage() {
<p className="text-green-100 text-xs mb-1"></p>
<p className="text-xl font-bold">{members.length} </p>
</div>
<div className="bg-white/10 backdrop-blur rounded-lg p-3">
<p className="text-green-100 text-xs mb-1"></p>
<p className="text-base font-bold">{currentRoom.roomType === 'screen' ? '屏幕共享' : '进度同步'}</p>
</div>
</div>
</div>
@@ -560,7 +629,9 @@ export default function WatchRoomPage() {
{/* 提示信息 */}
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4 border border-green-200 dark:border-green-800">
<p className="text-sm text-green-800 dark:text-green-200">
💡 {isOwner ? '前往播放页面或直播页面开始观影,房间成员将自动同步您的操作' : '等待房主开始播放,您的播放进度将自动跟随房主'}
💡 {currentRoom.roomType === 'screen'
? '这是屏幕共享房间,进入后即可观看房主共享画面'
: isOwner ? '前往播放页面或直播页面开始观影,房间成员将自动同步您的操作' : '等待房主开始播放,您的播放进度将自动跟随房主'}
</p>
</div>
</div>
@@ -617,7 +688,7 @@ export default function WatchRoomPage() {
{!currentRoom && (
<div className="mt-6 bg-green-50 dark:bg-green-900/20 rounded-lg p-4 border border-green-200 dark:border-green-800">
<p className="text-sm text-green-800 dark:text-green-200">
<strong></strong>
<strong></strong>
</p>
</div>
)}
@@ -633,7 +704,7 @@ export default function WatchRoomPage() {
<span className="font-medium text-gray-900 dark:text-gray-100">{rooms.length}</span>
</p>
<button
onClick={loadRooms}
onClick={() => loadRooms(true)}
disabled={loading}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg text-gray-700 dark:text-gray-300 transition-colors disabled:opacity-50"
>
@@ -704,6 +775,10 @@ export default function WatchRoomPage() {
<span></span>
<span className="font-medium">{room.ownerName}</span>
</div>
<div className="flex items-center justify-between text-gray-600 dark:text-gray-400">
<span></span>
<span>{room.roomType === 'screen' ? '屏幕共享' : '进度同步'}</span>
</div>
<div className="flex items-center justify-between text-gray-600 dark:text-gray-400">
<span></span>
<span>{formatTime(room.createdAt)}</span>
@@ -713,7 +788,9 @@ export default function WatchRoomPage() {
<p className="text-xs text-blue-700 dark:text-blue-300 truncate">
{room.currentState.type === 'play'
? `正在播放: ${room.currentState.videoName}`
: `正在观看: ${room.currentState.channelName}`}
: room.currentState.type === 'live'
? `正在观看: ${room.currentState.channelName}`
: '正在共享屏幕'}
</p>
</div>
)}
+201
View File
@@ -0,0 +1,201 @@
'use client';
import { Monitor, MonitorPlay, Users } from 'lucide-react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useWatchRoomContext } from '@/components/WatchRoomProvider';
import { useScreenShare } from '@/hooks/useScreenShare';
const NEW_TAB_KEY_PREFIX = 'watch_room_screen_home_opened_';
export default function WatchRoomScreenPage() {
const router = useRouter();
const watchRoom = useWatchRoomContext();
const { currentRoom, members, leaveRoom } = watchRoom;
const {
currentRoom: screenRoom,
isOwner,
isSharing,
isStarting,
error,
localVideoRef,
remoteVideoRef,
startSharing,
stopSharing,
} = useScreenShare();
useEffect(() => {
if (!currentRoom) {
router.replace('/watch-room');
return;
}
if (currentRoom.roomType !== 'screen') {
router.replace('/watch-room');
}
}, [currentRoom, router]);
useEffect(() => {
if (!screenRoom || !isOwner) return;
const key = `${NEW_TAB_KEY_PREFIX}${screenRoom.id}`;
if (sessionStorage.getItem(key)) return;
sessionStorage.setItem(key, '1');
window.open('/?watchRoomNoConnect=1', '_blank', 'noopener,noreferrer');
}, [isOwner, screenRoom?.id]);
if (!screenRoom || screenRoom.roomType !== 'screen') {
return null;
}
const handleLeave = () => {
if (isOwner && isSharing) {
stopSharing(true);
}
leaveRoom();
router.push('/watch-room');
};
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'>
<div className='flex items-center justify-between gap-4 rounded-2xl border border-gray-200 bg-white/90 px-5 py-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/80'>
<div>
<h1 className='flex items-center gap-2 text-2xl font-semibold'>
<Monitor className='h-6 w-6 text-blue-500' />
</h1>
<p className='mt-1 text-sm text-gray-600 dark:text-gray-400'>
{screenRoom.name} · {screenRoom.ownerName}
</p>
</div>
<div className='flex items-center gap-2'>
{isOwner && (
<Link
href='/?watchRoomNoConnect=1'
target='_blank'
rel='noreferrer'
className='rounded-lg bg-blue-500 px-4 py-2 text-white'
>
</Link>
)}
<button
onClick={handleLeave}
className='rounded-lg bg-gray-200 px-4 py-2 text-gray-900 dark:bg-gray-700 dark:text-gray-100'
>
</button>
</div>
</div>
<div className='grid flex-1 grid-cols-1 gap-4 xl:grid-cols-[1fr_320px]'>
<div className='relative flex min-h-[420px] items-center justify-center overflow-hidden rounded-2xl border border-gray-200 bg-black dark:border-gray-800'>
{isOwner ? (
<video
ref={localVideoRef}
autoPlay
muted
playsInline
className='h-full w-full bg-black object-contain'
/>
) : (
<video
ref={remoteVideoRef}
autoPlay
playsInline
controls
className='h-full w-full bg-black object-contain'
/>
)}
{!isSharing && (
<div className='absolute px-6 text-center text-white'>
<MonitorPlay className='mx-auto mb-3 h-12 w-12 text-white/70' />
<p className='text-lg font-medium'>
{isOwner ? '点击开始共享,向房员推送浏览器画面' : '等待房主开始共享屏幕'}
</p>
{isOwner && (
<p className='mt-2 text-sm text-white/70'>
便
</p>
)}
</div>
)}
</div>
<div className='space-y-4'>
<div className='rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900'>
<h2 className='mb-3 font-semibold'></h2>
<div className='space-y-2 text-sm text-gray-600 dark:text-gray-400'>
<p></p>
<p>{isSharing ? '共享中' : '未开始'}</p>
<p>{members.length} </p>
</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>
)}
<div className='mt-4 flex gap-3'>
{isOwner ? (
<>
<button
onClick={() => startSharing()}
disabled={isStarting || isSharing}
className='flex-1 rounded-lg bg-blue-500 px-4 py-2 text-white disabled:bg-gray-400'
>
{isStarting ? '启动中...' : isSharing ? '共享中' : '开始共享'}
</button>
<button
onClick={() => stopSharing(true)}
disabled={!isSharing}
className='rounded-lg bg-red-500 px-4 py-2 text-white disabled:bg-gray-400'
>
</button>
</>
) : (
<div className='rounded-lg bg-blue-50 px-3 py-2 text-sm text-blue-700 dark:bg-blue-900/20 dark:text-blue-300'>
</div>
)}
</div>
</div>
<div className='rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900'>
<h2 className='mb-3 flex items-center gap-2 font-semibold'>
<Users className='h-4 w-4' />
</h2>
<div className='space-y-2'>
{members.map((member) => (
<div
key={member.id}
className='flex items-center justify-between rounded-lg bg-gray-50 px-3 py-2 dark:bg-gray-800/70'
>
<span className='text-sm'>{member.name}</span>
{member.isOwner && (
<span className='rounded bg-yellow-100 px-2 py-1 text-xs text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300'>
</span>
)}
</div>
))}
</div>
</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
</div>
</div>
</div>
</div>
</div>
);
}