import { useEffect, useRef, useState } from 'react' import { Room, RoomEvent, Track } from 'livekit-client' import './App.css' type Role = 'host' | 'publisher' | 'viewer' type User = { id: string username: string displayName: string isAdmin: boolean } type Session = { token: string; user: User } type TokenResponse = { roomName: string participantIdentity: string participantName: string livekitUrl: string token: string role: Role } type ScreenFeed = { track: Track; label: string } type RoomHistoryItem = { id: string; title: string; visitedAt: string } const ROOM_HISTORY_KEY = 'screen-share-room-history' function ScreenTrack({ track, label }: ScreenFeed) { const mediaContainerRef = useRef(null) useEffect(() => { const mediaElement = track.attach() mediaElement.autoplay = true if (mediaElement instanceof HTMLVideoElement) { mediaElement.muted = true mediaElement.playsInline = true void mediaElement.play().catch(() => undefined) } mediaContainerRef.current?.append(mediaElement) return () => mediaElement.remove() }, [track]) return (
{label}
) } async function apiRequest(path: string, options: RequestInit = {}, token?: string): Promise { const response = await fetch(path, { ...options, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: 'Bearer ' + token } : {}), ...options.headers, }, }) const payload = (await response.json()) as T | { message: string } if (!response.ok) { const message = typeof payload === 'object' && payload !== null && 'message' in payload ? String(payload.message) : '请求失败' throw new Error(message) } return payload as T } function App() { const roomRef = useRef(null) const [session, setSession] = useState(null) const [authMode, setAuthMode] = useState<'login' | 'register'>('login') const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [displayName, setDisplayName] = useState('') const [inviteCode, setInviteCode] = useState('') const [roomId, setRoomId] = useState('') const [roomTitle, setRoomTitle] = useState('') const [roomHistory, setRoomHistory] = useState(() => { try { const stored = localStorage.getItem(ROOM_HISTORY_KEY) return stored ? JSON.parse(stored) as RoomHistoryItem[] : [] } catch { return [] } }) 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 [createdInvite, setCreatedInvite] = useState('') const [error, setError] = useState('') useEffect(() => { const stored = localStorage.getItem('screen-share-session') if (!stored) return const saved = JSON.parse(stored) as Session void apiRequest('/api/auth/me', {}, saved.token) .then((user) => setSession({ token: saved.token, user })) .catch(() => localStorage.removeItem('screen-share-session')) }, []) useEffect(() => () => { void roomRef.current?.disconnect() }, []) function saveSession(next: Session) { localStorage.setItem('screen-share-session', JSON.stringify(next)) setSession(next) } function refreshRemoteCount(room: Room) { setRemoteCount(room.remoteParticipants.size) } function rememberRoom(room: Pick) { setRoomHistory((history) => { const next = [ { ...room, visitedAt: new Date().toISOString() }, ...history.filter((item) => item.id !== room.id), ].slice(0, 12) localStorage.setItem(ROOM_HISTORY_KEY, JSON.stringify(next)) return next }) } async function submitAuth() { setError('') try { const body = authMode === 'login' ? { username, password } : { username, password, displayName, inviteCode } const result = await apiRequest('/api/auth/' + authMode, { method: 'POST', body: JSON.stringify(body), }) saveSession(result) setPassword('') setInviteCode('') } catch (cause) { setError(cause instanceof Error ? cause.message : '认证失败') } } async function createInvite(type: 'user' | 'admin') { if (!session) return setError('') try { const result = await apiRequest<{ code: string }>('/api/admin/invitations', { method: 'POST', body: JSON.stringify({ type, expiresInHours: 168 }), }, session.token) setCreatedInvite(result.code) } catch (cause) { setError(cause instanceof Error ? cause.message : '创建邀请码失败') } } async function createRoom() { if (!session || !roomTitle.trim()) { setError('请输入房间标题。') return } setError('') try { const room = await apiRequest<{ id: string; title: string }>('/api/rooms', { method: 'POST', body: JSON.stringify({ title: roomTitle.trim() }), }, session.token) setRoomId(room.id) setRoomTitle(room.title) rememberRoom(room) await connectRoom(room.id) } catch (cause) { setError(cause instanceof Error ? cause.message : '创建房间失败') } } async function joinRoomByTitle() { if (!session || !roomTitle.trim()) { setError('请输入房间标题。') return } setError('') try { const room = await apiRequest<{ id: string; title: string }>('/api/rooms/join-by-title', { method: 'POST', body: JSON.stringify({ title: roomTitle.trim() }), }, session.token) setRoomId(room.id) setRoomTitle(room.title) rememberRoom(room) await connectRoom(room.id) } catch (cause) { setError(cause instanceof Error ? cause.message : '加入房间失败') } } async function connectRoom(targetRoomId = roomId.trim()) { if (!session) return if (!targetRoomId) { setError('请输入房间 ID。') return } setError('') setConnectionState('connecting') try { await apiRequest('/api/rooms/' + encodeURIComponent(targetRoomId) + '/join', { method: 'POST', body: '{}', }, session.token) const payload = await apiRequest('/api/rooms/' + encodeURIComponent(targetRoomId) + '/token', { method: 'POST', body: '{}', }, session.token) roomRef.current?.disconnect() 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) { setRemoteScreens((screens) => [ ...screens.filter((screen) => screen.track.sid !== track.sid), { track, label: (participant.name || participant.identity) + ' 的屏幕' }, ]) } }) room.on(RoomEvent.TrackUnsubscribed, (track) => { setRemoteScreens((screens) => screens.filter((screen) => screen.track !== track)) }) room.on(RoomEvent.LocalTrackPublished, (publication) => { if (publication.source === Track.Source.ScreenShare && publication.track) { setLocalScreen({ track: publication.track, label: '你正在共享的屏幕' }) setIsSharing(true) } }) room.on(RoomEvent.LocalTrackUnpublished, (publication) => { if (publication.source === Track.Source.ScreenShare) { setLocalScreen(null) setIsSharing(false) } }) room.on(RoomEvent.Disconnected, () => { setConnectionState('idle') setRoomRole(null) setIsSharing(false) setRemoteCount(0) setLocalScreen(null) setRemoteScreens([]) }) await room.connect(payload.livekitUrl, payload.token) roomRef.current = room setRoomId(targetRoomId) setRoomRole(payload.role) refreshRemoteCount(room) setConnectionState('connected') } catch (cause) { setConnectionState('idle') setError(cause instanceof Error ? cause.message : '连接房间失败') } } async function toggleScreenShare() { if (!roomRef.current) return try { await roomRef.current.localParticipant.setScreenShareEnabled(!isSharing) } catch (cause) { setError(cause instanceof Error ? cause.message : '无法切换屏幕共享') } } function leaveRoom() { roomRef.current?.disconnect() roomRef.current = null } function logout() { leaveRoom() localStorage.removeItem('screen-share-session') setSession(null) setCreatedInvite('') } const connected = connectionState === 'connected' const canShare = roomRole === 'host' || roomRole === 'publisher' if (!session) { return (

SCREEN SHARE

{authMode === 'login' ? '登录' : '使用邀请码注册'}

{authMode === 'register' && ( <> )} {error &&

{error}

}
) } return (

LIVEKIT · 已登录

屏幕共享房间

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

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

{error}

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

我的屏幕

{localScreen ? :

尚未共享屏幕

}

远端屏幕

{remoteScreens.length === 0 ?

等待其他参与者共享屏幕

: remoteScreens.map((screen) => )}
) } export default App