mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 11:58:53 +00:00
wip
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { FocusLayout } from '@/features/rooms/livekit/components/FocusLayout'
|
||||
import { SecondaryScreenShareStrip } from './SecondaryScreenShareStrip'
|
||||
|
||||
export interface FocusAreaProps {
|
||||
focusTrack: TrackReferenceOrPlaceholder
|
||||
secondaryScreenShareTracks: TrackReferenceOrPlaceholder[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Main focus track with an optional strip of secondary screen shares below.
|
||||
*/
|
||||
export function FocusArea({
|
||||
focusTrack,
|
||||
secondaryScreenShareTracks,
|
||||
}: FocusAreaProps) {
|
||||
return (
|
||||
<FocusColumn>
|
||||
<MainFocusSlot>
|
||||
<FocusLayout trackRef={focusTrack} />
|
||||
</MainFocusSlot>
|
||||
<SecondaryScreenShareStrip tracks={secondaryScreenShareTracks} />
|
||||
</FocusColumn>
|
||||
)
|
||||
}
|
||||
|
||||
const FocusColumn = styled('div', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
})
|
||||
|
||||
const MainFocusSlot = styled('div', {
|
||||
base: {
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflow: 'hidden',
|
||||
'& .lk-participant-tile': {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
'& .lk-participant-media-video': {
|
||||
objectFit: 'contain',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||
import { memo } from 'react'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
|
||||
import { getTrackKey } from '@/features/pip/utils/pipTrackSelection'
|
||||
|
||||
export interface SecondaryScreenShareStripProps {
|
||||
tracks: TrackReferenceOrPlaceholder[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact horizontal strip for non-focused screen shares.
|
||||
* Each tile keeps pin controls so users can switch focus to any share.
|
||||
*/
|
||||
export const SecondaryScreenShareStrip = memo(
|
||||
({ tracks }: SecondaryScreenShareStripProps) => {
|
||||
if (tracks.length === 0) return null
|
||||
|
||||
return (
|
||||
<StripContainer>
|
||||
{tracks.map((track) => (
|
||||
<ShareTile key={getTrackKey(track)}>
|
||||
<ParticipantTile trackRef={track} />
|
||||
</ShareTile>
|
||||
))}
|
||||
</StripContainer>
|
||||
)
|
||||
}
|
||||
)
|
||||
SecondaryScreenShareStrip.displayName = 'SecondaryScreenShareStrip'
|
||||
|
||||
const StripContainer = styled('div', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '0.5rem',
|
||||
flexShrink: 0,
|
||||
height: '120px',
|
||||
minHeight: '80px',
|
||||
maxHeight: '140px',
|
||||
padding: '0.5rem 0 0',
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
})
|
||||
|
||||
const ShareTile = styled('div', {
|
||||
base: {
|
||||
position: 'relative',
|
||||
flex: '0 0 auto',
|
||||
height: '100%',
|
||||
aspectRatio: '16 / 9',
|
||||
minWidth: 0,
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'primaryDark.100',
|
||||
'& .lk-participant-tile': {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
'& .lk-participant-media-video': {
|
||||
objectFit: 'contain',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
isEqualTrackRef,
|
||||
isTrackReference,
|
||||
type TrackReferenceOrPlaceholder,
|
||||
} from '@livekit/components-core'
|
||||
import { Track } from 'livekit-client'
|
||||
|
||||
export function getSubscribedScreenShareTracks(
|
||||
tracks: TrackReferenceOrPlaceholder[]
|
||||
): TrackReferenceOrPlaceholder[] {
|
||||
return tracks
|
||||
.filter(isTrackReference)
|
||||
.filter(
|
||||
(track) =>
|
||||
track.publication.source === Track.Source.ScreenShare &&
|
||||
track.publication.isSubscribed
|
||||
)
|
||||
}
|
||||
|
||||
export function getNewScreenShareTracks(
|
||||
screenShareTracks: TrackReferenceOrPlaceholder[],
|
||||
knownTrackSids: Set<string>
|
||||
): TrackReferenceOrPlaceholder[] {
|
||||
return screenShareTracks.filter(
|
||||
(track) =>
|
||||
track.publication.trackSid &&
|
||||
!knownTrackSids.has(track.publication.trackSid)
|
||||
)
|
||||
}
|
||||
|
||||
export function syncKnownScreenShareSids(
|
||||
screenShareTracks: TrackReferenceOrPlaceholder[],
|
||||
knownTrackSids: Set<string>
|
||||
): void {
|
||||
const currentSids = new Set(
|
||||
screenShareTracks
|
||||
.map((track) => track.publication.trackSid)
|
||||
.filter((sid): sid is string => !!sid)
|
||||
)
|
||||
|
||||
screenShareTracks.forEach((track) => {
|
||||
if (track.publication.trackSid) {
|
||||
knownTrackSids.add(track.publication.trackSid)
|
||||
}
|
||||
})
|
||||
|
||||
knownTrackSids.forEach((sid) => {
|
||||
if (!currentSids.has(sid)) {
|
||||
knownTrackSids.delete(sid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getNewestScreenShareTrack(
|
||||
screenShareTracks: TrackReferenceOrPlaceholder[]
|
||||
): TrackReferenceOrPlaceholder | undefined {
|
||||
if (screenShareTracks.length === 0) return undefined
|
||||
return screenShareTracks[screenShareTracks.length - 1]
|
||||
}
|
||||
|
||||
export function splitTracksForFocusLayout(
|
||||
tracks: TrackReferenceOrPlaceholder[],
|
||||
focusTrack: TrackReferenceOrPlaceholder | undefined,
|
||||
screenShareTracks: TrackReferenceOrPlaceholder[]
|
||||
): {
|
||||
carouselTracks: TrackReferenceOrPlaceholder[]
|
||||
secondaryScreenShareTracks: TrackReferenceOrPlaceholder[]
|
||||
} {
|
||||
const hasActiveScreenShares = screenShareTracks.length > 0
|
||||
|
||||
const secondaryScreenShareTracks = screenShareTracks.filter(
|
||||
(track) => !focusTrack || !isEqualTrackRef(track, focusTrack)
|
||||
)
|
||||
|
||||
const carouselTracks = tracks.filter((track) => {
|
||||
if (focusTrack && isEqualTrackRef(track, focusTrack)) return false
|
||||
if (hasActiveScreenShares && track.source === Track.Source.ScreenShare) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return { carouselTracks, secondaryScreenShareTracks }
|
||||
}
|
||||
@@ -1,21 +1,52 @@
|
||||
import { useFocusToggle } from '@livekit/components-react'
|
||||
import {
|
||||
isTrackReferencePinned,
|
||||
} from '@livekit/components-core'
|
||||
import { useFocusToggle, useMaybeLayoutContext } from '@livekit/components-react'
|
||||
import { type Participant, Track } from 'livekit-client'
|
||||
import { useCallback } from 'react'
|
||||
import type { MouseEvent } from 'react'
|
||||
import Source = Track.Source
|
||||
|
||||
export const useFocusToggleParticipant = (participant: Participant) => {
|
||||
const trackRef = {
|
||||
participant: participant,
|
||||
publication: participant.getTrackPublication(Source.Camera),
|
||||
source: Source.Camera,
|
||||
}
|
||||
const getParticipantPinTrackRef = (participant: Participant) => {
|
||||
const screenSharePublication = participant.getTrackPublication(
|
||||
Source.ScreenShare
|
||||
)
|
||||
const source =
|
||||
participant.isScreenShareEnabled && screenSharePublication
|
||||
? Source.ScreenShare
|
||||
: Source.Camera
|
||||
|
||||
const { mergedProps, inFocus } = useFocusToggle({
|
||||
return {
|
||||
participant,
|
||||
publication: participant.getTrackPublication(source),
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
export const useFocusToggleParticipant = (participant: Participant) => {
|
||||
const layoutContext = useMaybeLayoutContext()
|
||||
const trackRef = getParticipantPinTrackRef(participant)
|
||||
|
||||
const { mergedProps, inFocus: isFocusedTrack } = useFocusToggle({
|
||||
trackRef,
|
||||
props: {},
|
||||
})
|
||||
|
||||
const inFocus =
|
||||
isFocusedTrack ||
|
||||
(!!layoutContext?.pin.state &&
|
||||
[Source.Camera, Source.ScreenShare].some((source) => {
|
||||
const ref = {
|
||||
participant,
|
||||
publication: participant.getTrackPublication(source),
|
||||
source,
|
||||
}
|
||||
return (
|
||||
!!ref.publication &&
|
||||
isTrackReferencePinned(ref, layoutContext.pin.state)
|
||||
)
|
||||
}))
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
const syntheticEvent = {
|
||||
preventDefault: () => {},
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ControlBar } from './ControlBar/ControlBar'
|
||||
import { FocusLayout } from '../components/FocusLayout'
|
||||
import { ParticipantTile } from '../components/ParticipantTile'
|
||||
import { SidePanel } from '../components/SidePanel'
|
||||
import { RecordingProvider } from '@/features/recording'
|
||||
@@ -39,8 +38,16 @@ import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
|
||||
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
||||
import { ReactionPortals } from '@/features/reactions/components/ReactionPortals'
|
||||
import { CarouselLayout } from '@/features/layout/components/CarouselLayout'
|
||||
import { FocusArea } from '@/features/layout/components/FocusArea'
|
||||
import { GridLayout } from '@/features/layout/components/GridLayout'
|
||||
import { RoomContentArea } from '@/features/layout/components/RoomContentArea'
|
||||
import {
|
||||
getNewScreenShareTracks,
|
||||
getNewestScreenShareTrack,
|
||||
getSubscribedScreenShareTracks,
|
||||
splitTracksForFocusLayout,
|
||||
syncKnownScreenShareSids,
|
||||
} from '@/features/layout/utils/screenShareLayout'
|
||||
import { usePictureInPicture } from '@/features/pip/hooks/usePictureInPicture'
|
||||
import { PipRoomPlaceholder } from '@/features/pip/components/PipRoomPlaceholder'
|
||||
|
||||
@@ -73,6 +80,7 @@ export interface VideoConferenceProps extends React.HTMLAttributes<HTMLDivElemen
|
||||
export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
const lastAutoFocusedScreenShareTrack =
|
||||
useRef<TrackReferenceOrPlaceholder | null>(null)
|
||||
const knownScreenShareTrackSids = useRef<Set<string>>(new Set())
|
||||
const lastPinnedParticipantIdentityRef = useRef<string | null>(null)
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'pinAnnouncements' })
|
||||
const { t: tRooms } = useTranslation('rooms')
|
||||
@@ -114,14 +122,11 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
|
||||
useNoiseReduction()
|
||||
|
||||
const screenShareTracks = tracks
|
||||
.filter(isTrackReference)
|
||||
.filter((track) => track.publication.source === Track.Source.ScreenShare)
|
||||
const screenShareTracks = getSubscribedScreenShareTracks(tracks)
|
||||
|
||||
const focusTrack = usePinnedTracks(layoutContext)?.[0]
|
||||
const carouselTracks = tracks.filter(
|
||||
(track) => !isEqualTrackRef(track, focusTrack)
|
||||
)
|
||||
const { carouselTracks, secondaryScreenShareTracks } =
|
||||
splitTracksForFocusLayout(tracks, focusTrack, screenShareTracks)
|
||||
|
||||
const { isOpen: isPictureInPictureOpen } = usePictureInPicture()
|
||||
|
||||
@@ -181,31 +186,48 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
// Code duplicated from LiveKit; this warning will be addressed in the refactoring.
|
||||
useEffect(() => {
|
||||
// If screen share tracks are published, and no pin is set explicitly, auto set the screen share.
|
||||
if (
|
||||
screenShareTracks.some((track) => track.publication.isSubscribed) &&
|
||||
lastAutoFocusedScreenShareTrack.current === null
|
||||
) {
|
||||
log.debug('Auto set screen share focus:', {
|
||||
newScreenShareTrack: screenShareTracks[0],
|
||||
})
|
||||
layoutContext.pin.dispatch?.({
|
||||
msg: 'set_pin',
|
||||
trackReference: screenShareTracks[0],
|
||||
})
|
||||
lastAutoFocusedScreenShareTrack.current = screenShareTracks[0]
|
||||
const newScreenShares = getNewScreenShareTracks(
|
||||
screenShareTracks,
|
||||
knownScreenShareTrackSids.current
|
||||
)
|
||||
syncKnownScreenShareSids(
|
||||
screenShareTracks,
|
||||
knownScreenShareTrackSids.current
|
||||
)
|
||||
|
||||
if (newScreenShares.length > 0) {
|
||||
const newestScreenShare = getNewestScreenShareTrack(newScreenShares)
|
||||
if (newestScreenShare) {
|
||||
log.debug('Auto set screen share focus:', {
|
||||
newScreenShareTrack: newestScreenShare,
|
||||
})
|
||||
layoutContext.pin.dispatch?.({
|
||||
msg: 'set_pin',
|
||||
trackReference: newestScreenShare,
|
||||
})
|
||||
lastAutoFocusedScreenShareTrack.current = newestScreenShare
|
||||
}
|
||||
} else if (
|
||||
lastAutoFocusedScreenShareTrack.current &&
|
||||
!screenShareTracks.some(
|
||||
(track) =>
|
||||
track.publication.trackSid ===
|
||||
lastAutoFocusedScreenShareTrack.current?.publication?.trackSid
|
||||
)
|
||||
focusTrack?.source === Track.Source.ScreenShare &&
|
||||
!screenShareTracks.some((track) => isEqualTrackRef(track, focusTrack))
|
||||
) {
|
||||
log.debug('Auto clearing screen share focus.')
|
||||
layoutContext.pin.dispatch?.({ msg: 'clear_pin' })
|
||||
lastAutoFocusedScreenShareTrack.current = null
|
||||
const newestRemaining = getNewestScreenShareTrack(screenShareTracks)
|
||||
if (newestRemaining) {
|
||||
log.debug('Auto switching to remaining screen share:', {
|
||||
screenShareTrack: newestRemaining,
|
||||
})
|
||||
layoutContext.pin.dispatch?.({
|
||||
msg: 'set_pin',
|
||||
trackReference: newestRemaining,
|
||||
})
|
||||
lastAutoFocusedScreenShareTrack.current = newestRemaining
|
||||
} else {
|
||||
log.debug('Auto clearing screen share focus.')
|
||||
layoutContext.pin.dispatch?.({ msg: 'clear_pin' })
|
||||
lastAutoFocusedScreenShareTrack.current = null
|
||||
}
|
||||
}
|
||||
|
||||
if (focusTrack && !isTrackReference(focusTrack)) {
|
||||
const updatedFocusTrack = tracks.find(
|
||||
(tr) =>
|
||||
@@ -281,7 +303,14 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
>
|
||||
<ParticipantTile />
|
||||
</CarouselLayout>
|
||||
{focusTrack && <FocusLayout trackRef={focusTrack} />}
|
||||
{focusTrack && (
|
||||
<FocusArea
|
||||
focusTrack={focusTrack}
|
||||
secondaryScreenShareTracks={
|
||||
secondaryScreenShareTracks
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</FocusLayoutContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user