diff --git a/apps/ScreenShareWeb/src/App.css b/apps/ScreenShareWeb/src/App.css index 94ca4ee..5763ba7 100644 --- a/apps/ScreenShareWeb/src/App.css +++ b/apps/ScreenShareWeb/src/App.css @@ -8,6 +8,11 @@ width: min(460px, calc(100% - 32px)); } +.app-shell.in-room { + width: min(1440px, calc(100% - 32px)); + padding: 20px 0; +} + .app-header { display: flex; align-items: flex-start; @@ -240,7 +245,7 @@ button:disabled { background: #181a29; } -.screen-frame figcaption { +.screen-caption { padding: 8px 10px; color: #e8e9f4; font-size: 0.82rem; @@ -253,6 +258,84 @@ button:disabled { background: #10111b; } +.room-toolbar { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-bottom: 14px; +} + +.room-toolbar .share-button { + margin-left: 0; +} + +.screen-stage { + display: grid; + min-height: min(72vh, 760px); + overflow: hidden; + border: 1px solid #dedfed; + border-radius: 16px; + background: #10111b; +} + +.screen-stage .screen-frame { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + min-height: 0; + border-radius: 0; +} + +.screen-stage .media-slot, +.screen-stage .media-slot video { + width: 100%; + height: 100%; + max-height: none; + object-fit: contain; +} + +.stage-empty { + margin: auto; + color: #c3c5d5; +} + +.screen-thumbnails { + display: flex; + gap: 12px; + overflow-x: auto; + padding: 14px 2px 2px; +} + +.screen-thumbnail { + flex: 0 0 230px; + min-height: 0; + overflow: hidden; + padding: 0; + border: 2px solid transparent; + border-radius: 10px; + background: #181a29; + text-align: left; +} + +.screen-thumbnail:hover, +.screen-thumbnail:focus-visible { + border-color: #7669ed; + outline: none; +} + +.screen-thumbnail .screen-frame { + border-radius: 8px; +} + +.screen-thumbnail .screen-caption { + padding: 6px 8px; + font-size: 0.73rem; +} + +.screen-thumbnail .media-slot video { + height: 128px; + object-fit: contain; +} + .development-note { margin: 28px auto 0; max-width: 780px; @@ -279,4 +362,12 @@ button:disabled { .share-button { margin-left: 0; } + + .screen-stage { + min-height: 52vh; + } + + .screen-thumbnail { + flex-basis: 170px; + } } diff --git a/apps/ScreenShareWeb/src/App.tsx b/apps/ScreenShareWeb/src/App.tsx index c77ddf9..3a5ff8f 100644 --- a/apps/ScreenShareWeb/src/App.tsx +++ b/apps/ScreenShareWeb/src/App.tsx @@ -22,12 +22,12 @@ type TokenResponse = { role: Role } -type ScreenFeed = { track: Track; label: string } +type ScreenFeed = { id: string; track: Track; label: string; isLocal: boolean } type RoomHistoryItem = { id: string; title: string; visitedAt: string } const ROOM_HISTORY_KEY = 'screen-share-room-history' -function ScreenTrack({ track, label }: ScreenFeed) { +function ScreenTrack({ track, label, variant = 'main' }: ScreenFeed & { variant?: 'main' | 'thumbnail' }) { const mediaContainerRef = useRef(null) useEffect(() => { @@ -43,10 +43,10 @@ function ScreenTrack({ track, label }: ScreenFeed) { }, [track]) return ( -
-
{label}
+
+
{label}
-
+ ) } @@ -90,9 +90,9 @@ function App() { const [connectionState, setConnectionState] = useState<'idle' | 'connecting' | 'connected'>('idle') const [roomRole, setRoomRole] = useState(null) const [isSharing, setIsSharing] = useState(false) - const [remoteCount, setRemoteCount] = useState(0) const [localScreen, setLocalScreen] = useState(null) const [remoteScreens, setRemoteScreens] = useState([]) + const [activeScreenId, setActiveScreenId] = useState(null) const [createdInvite, setCreatedInvite] = useState('') const [error, setError] = useState('') @@ -114,10 +114,6 @@ function App() { setSession(next) } - function refreshRemoteCount(room: Room) { - setRemoteCount(room.remoteParticipants.size) - } - function rememberRoom(room: Pick) { setRoomHistory((history) => { const next = [ @@ -224,27 +220,38 @@ function App() { setLocalScreen(null) setRemoteScreens([]) const room = new Room() - room.on(RoomEvent.ParticipantConnected, () => refreshRemoteCount(room)) - room.on(RoomEvent.ParticipantDisconnected, () => refreshRemoteCount(room)) room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => { - if (track.kind === Track.Kind.Video && publication.source === Track.Source.ScreenShare) { + const trackId = track.sid + if (track.kind === Track.Kind.Video && publication.source === Track.Source.ScreenShare && trackId) { setRemoteScreens((screens) => [ - ...screens.filter((screen) => screen.track.sid !== track.sid), - { track, label: (participant.name || participant.identity) + ' 的屏幕' }, + ...screens.filter((screen) => screen.track.sid !== trackId), + { id: trackId, track, label: (participant.name || participant.identity) + ' 的屏幕', isLocal: false }, ]) + setActiveScreenId((current) => current?.startsWith('local-') || !current ? trackId : current) } }) room.on(RoomEvent.TrackUnsubscribed, (track) => { setRemoteScreens((screens) => screens.filter((screen) => screen.track !== track)) + if (track.sid) { + setActiveScreenId((current) => current === track.sid ? null : current) + } }) room.on(RoomEvent.LocalTrackPublished, (publication) => { if (publication.source === Track.Source.ScreenShare && publication.track) { - setLocalScreen({ track: publication.track, label: '你正在共享的屏幕' }) + const localScreenId = 'local-' + publication.track.sid + setLocalScreen({ + id: localScreenId, + track: publication.track, + label: '你正在共享的屏幕', + isLocal: true, + }) + setActiveScreenId((current) => current ?? localScreenId) setIsSharing(true) } }) room.on(RoomEvent.LocalTrackUnpublished, (publication) => { if (publication.source === Track.Source.ScreenShare) { + setActiveScreenId((current) => current?.startsWith('local-') ? null : current) setLocalScreen(null) setIsSharing(false) } @@ -253,7 +260,6 @@ function App() { setConnectionState('idle') setRoomRole(null) setIsSharing(false) - setRemoteCount(0) setLocalScreen(null) setRemoteScreens([]) }) @@ -262,7 +268,6 @@ function App() { roomRef.current = room setRoomId(targetRoomId) setRoomRole(payload.role) - refreshRemoteCount(room) setConnectionState('connected') } catch (cause) { setConnectionState('idle') @@ -293,6 +298,9 @@ function App() { const connected = connectionState === 'connected' const canShare = roomRole === 'host' || roomRole === 'publisher' + const screens = [...(localScreen ? [localScreen] : []), ...remoteScreens] + const activeScreen = screens.find((screen) => screen.id === activeScreenId) ?? screens[0] + const secondaryScreens = screens.filter((screen) => screen.id !== activeScreen?.id) if (!session) { return ( @@ -323,63 +331,78 @@ function App() { } return ( -
-
-
-

LIVEKIT · 已登录

-

屏幕共享房间

-

当前用户:{session.user.displayName}({session.user.isAdmin ? '管理员' : '普通用户'})

-
- -
+
+ {!connected && ( + <> +
+
+

LIVEKIT · 已登录

+

屏幕共享房间

+

当前用户:{session.user.displayName}({session.user.isAdmin ? '管理员' : '普通用户'})

+
+ +
- {session.user.isAdmin && ( -
- 管理员邀请码 - - - {createdInvite && {createdInvite}} -
- )} + {session.user.isAdmin && ( +
+ 管理员邀请码 + + + {createdInvite && {createdInvite}} +
+ )} -
- - - {!connected - ? - : } - - {!connected && } -
+
+ + + + + +
- {roomHistory.length > 0 && ( -
- 最近房间 - {roomHistory.map((item) => ( - - ))} -
+ {roomHistory.length > 0 && ( +
+ 最近房间 + {roomHistory.map((item) => ( + + ))} +
+ )} + )} {error &&

{error}

} -
- {connected ? '已连接' : '未连接'} - {roomRole ? '房间角色:' + roomRole : '尚未加入房间'} - 其他参与者:{remoteCount} - {connected && } -
+ {connected && ( + <> +
+ {canShare && } + +
-
-

我的屏幕

{localScreen ? :

尚未共享屏幕

}
-

远端屏幕

{remoteScreens.length === 0 ?

等待其他参与者共享屏幕

: remoteScreens.map((screen) => )}
-
+
+ {activeScreen + ? + :

等待参与者共享屏幕

} +
+ + {secondaryScreens.length > 0 && ( +
+ {secondaryScreens.map((screen) => ( + + ))} +
+ )} + + )}
) }