@@ -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 < HTMLDivElement > ( null )
useEffect ( ( ) = > {
@@ -43,10 +43,10 @@ function ScreenTrack({ track, label }: ScreenFeed) {
} , [ track ] )
return (
< figure className = " screen-frame" >
< figcaption > { label } < / figcaption >
< div className = { ' screen-frame ' + variant } >
< div className = "screen-caption" > { label } < / div >
< div className = "media-slot" ref = { mediaContainerRef } / >
< / figure >
< / div >
)
}
@@ -90,9 +90,9 @@ function App() {
const [ connectionState , setConnectionState ] = useState < 'idle' | 'connecting' | 'connected' > ( 'idle' )
const [ roomRole , setRoomRole ] = useState < Role | null > ( null )
const [ isSharing , setIsSharing ] = useState ( false )
const [ remoteCount , setRemoteCount ] = useState ( 0 )
const [ localScreen , setLocalScreen ] = useState < ScreenFeed | null > ( null )
const [ remoteScreens , setRemoteScreens ] = useState < ScreenFeed [ ] > ( [ ] )
const [ activeScreenId , setActiveScreenId ] = useState < string | null > ( 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 < RoomHistoryItem , 'id' | 'title' > ) {
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,7 +331,9 @@ function App() {
}
return (
< main className = "app-shell" >
< main className = { connected ? 'app-shell in-room' : 'app-shell' } >
{ ! connected && (
< >
< header className = "app-header" >
< div >
< p className = "eyebrow" > LIVEKIT · 已 登 录 < / p >
@@ -343,15 +353,13 @@ function App() {
) }
< section className = "join-panel room-panel" >
< label > 房 间 标 题 < input list = "room-title-history" value = { roomTitle } onChange = { ( event ) = > setRoomTitle ( event . target . value ) } disabled = { connected } placeholder = "输入标题以创建或加入" / >
< label > 房 间 标 题 < input list = "room-title-history" value = { roomTitle } onChange = { ( event ) = > setRoomTitle ( event . target . value ) } placeholder = "输入标题以创建或加入" / >
< datalist id = "room-title-history" > { roomHistory . map ( ( item ) = > < option key = { item . id } value = { item . title } / > ) } < / datalist >
< / label >
< button type = "button" className = "primary" onClick = { ( ) = > void createRoom ( ) } disabled = { connected } > 创 建 房 间 < / button >
{ ! connected
? < button type = "button" className = "secondary" onClick = { ( ) = > void joinRoomByTitle ( ) } disabled = { connectionState === 'connecting' } > { connectionState === 'connecting' ? '正在连接…' : '按标题加入' } < / button >
: < button type = "button" className = "secondary" onClick = { leaveRoom } > 离 开 房 间 < / button > }
< label className = "advanced-field" > 房 间 ID ( 备 用 ) < input value = { roomId } onChange = { ( event ) = > setRoomId ( event . target . value ) } disabled = { connected } placeholder = "UUID" / > < / label >
{ ! connected && < button type = "button" className = "secondary id-join" onClick = { ( ) = > void connectRoom ( ) } disabled = { connectionState === 'connecting' } > 使 用 ID 加 入 < / button > }
< button type = "button" className = "primary" onClick = { ( ) = > void createRoom ( ) } > 创 建 房 间 < / button >
< button type = "button" className = "secondary" onClick = { ( ) = > void joinRoomByTitle ( ) } disabled = { connectionState === 'connecting' } > { connectionState === 'connecting' ? '正在连接…' : '按标题加入' } < / button >
< label className = "advanced-field" > 房 间 ID ( 备 用 ) < input value = { roomId } onChange = { ( event ) = > setRoomId ( event . target . value ) } placeholder = "UUID" / > < / label >
< button type = "button" className = "secondary id-join " onClick = { ( ) = > void connectRoom ( ) } disabled = { connectionState === 'connecting' } > 使 用 ID 加 入 < / button >
< / section >
{ roomHistory . length > 0 && (
@@ -362,24 +370,39 @@ function App() {
setRoomTitle ( item . title )
setRoomId ( item . id )
void connectRoom ( item . id )
} } disabled = { connected } > { item . title } < / button >
} } > { item . title } < / button >
) ) }
< / section >
) }
< / >
) }
{ error && < p className = "error" role = "alert" > { error } < / p > }
< section className = "status-bar" aria-live = "polite" >
< span className = { connected ? 'status online' : 'status' } > { connected ? '已连接' : '未连接' } < / span >
< span > { roomRole ? '房间角色:' + roomRole : '尚未加入房间' } < / span >
< span > 其 他 参 与 者 : { remoteCount } < / span >
{ connected && < button type = "button" className = "share-button" onClick = { ( ) = > void toggleScreenShare ( ) } disabled = { ! canShare } > { isSharing ? '停止共享屏幕' : '共享屏幕' } < / button > }
{ connected && (
< >
< section className = "room-toolbar" aria-label = "房间控制" >
{ canShare && < button type = "button" className = "share-button" onClick = { ( ) = > void toggleScreenShare ( ) } > { isSharing ? '停止共享屏幕' : '共享屏幕' } < / button > }
< button type = "button" className = "secondary" onClick = { leaveRoom } > 离 开 房 间 < / button >
< / section >
< section className = "screens " >
< div className = "screen-section" > < h2 > 我 的 屏 幕 < / h2 > < div className = "video-stack" > { localScreen ? < ScreenTrack { ...localScreen } / > : < p className = "empty" > 尚 未 共 享 屏 幕 < / p > } < / div > < / div >
< div className = "screen-section" > < h2 > 远 端 屏 幕 < / h2 > < div className = "video-stack" > { remoteScreens . length === 0 ? < p className = "empty" > 等 待 其 他 参 与 者 共 享 屏 幕 < / p > : remoteScreens . map ( ( screen ) = > < ScreenTrack key = { screen . track . sid } { ...screen } / > ) } < / div > < / div >
< section className = "screen-stage " >
{ activeScreen
? < ScreenTrack { ...activeScreen } variant = "main" / >
: < p className = "empty stage-empty" > 等 待 参 与 者 共 享 屏 幕 < / p > }
< / section >
{ secondaryScreens . length > 0 && (
< section className = "screen-thumbnails" aria-label = "副屏幕分享" >
{ secondaryScreens . map ( ( screen ) = > (
< button key = { screen . id } type = "button" className = "screen-thumbnail" onClick = { ( ) = > setActiveScreenId ( screen . id ) } >
< ScreenTrack { ...screen } variant = "thumbnail" / >
< / button >
) ) }
< / section >
) }
< / >
) }
< / main >
)
}