diff --git a/src/frontend/src/features/rooms/livekit/components/layout/CarouselLayout.tsx b/src/frontend/src/features/rooms/livekit/components/layout/CarouselLayout.tsx new file mode 100644 index 00000000..08bef1ed --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/layout/CarouselLayout.tsx @@ -0,0 +1,92 @@ +import type { TrackReferenceOrPlaceholder } from '@livekit/components-core' +import { getScrollBarWidth } from '@livekit/components-core' +import * as React from 'react' +import { TrackLoop, useVisualStableUpdate } from '@livekit/components-react' +import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver' + +const MIN_HEIGHT = 130 +const MIN_WIDTH = 140 +const MIN_VISIBLE_TILES = 1 +const ASPECT_RATIO = 16 / 10 +const ASPECT_RATIO_INVERT = (1 - ASPECT_RATIO) * -1 + +/** @public */ +export interface CarouselLayoutProps + extends React.HTMLAttributes { + tracks: TrackReferenceOrPlaceholder[] + children: React.ReactNode + /** Place the tiles vertically or horizontally next to each other. + * If undefined orientation is guessed by the dimensions of the container. */ + orientation?: 'vertical' | 'horizontal' +} + +/** + * The `CarouselLayout` component displays a list of tracks in a scroll container. + * It will display as many tiles as possible and overflow the rest. + * @remarks + * To ensure visual stability when tiles are reordered due to track updates, + * the component uses the `useVisualStableUpdate` hook. + * @example + * ```tsx + * const tracks = useTracks([Track.Source.Camera]); + * + * + * + * ``` + * @public + */ +export function CarouselLayout({ + tracks, + orientation, + ...props +}: CarouselLayoutProps) { + const asideEl = React.useRef(null) + const [prevTiles, setPrevTiles] = React.useState(0) + const { width, height } = useSize(asideEl) + const carouselOrientation = orientation + ? orientation + : height >= width + ? 'vertical' + : 'horizontal' + + const tileSpan = + carouselOrientation === 'vertical' + ? Math.max(width * ASPECT_RATIO_INVERT, MIN_HEIGHT) + : Math.max(height * ASPECT_RATIO, MIN_WIDTH) + const scrollBarWidth = getScrollBarWidth() + + const tilesThatFit = + carouselOrientation === 'vertical' + ? Math.max((height - scrollBarWidth) / tileSpan, MIN_VISIBLE_TILES) + : Math.max((width - scrollBarWidth) / tileSpan, MIN_VISIBLE_TILES) + + let maxVisibleTiles = Math.round(tilesThatFit) + if (Math.abs(tilesThatFit - prevTiles) < 0.5) { + maxVisibleTiles = Math.round(prevTiles) + } else if (prevTiles !== tilesThatFit) { + setPrevTiles(tilesThatFit) + } + + const sortedTiles = useVisualStableUpdate(tracks, maxVisibleTiles) + + React.useLayoutEffect(() => { + if (asideEl.current) { + asideEl.current.dataset.lkOrientation = carouselOrientation + asideEl.current.style.setProperty( + '--lk-max-visible-tiles', + maxVisibleTiles.toString() + ) + } + }, [maxVisibleTiles, carouselOrientation]) + + return ( + + ) +} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index 95efcb6d..d00d8fe3 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -9,7 +9,6 @@ import { RoomEvent, Track } from 'livekit-client' import * as React from 'react' import { useState } from 'react' import { - CarouselLayout, ConnectionStateToast, FocusLayoutContainer, GridLayout, @@ -36,6 +35,7 @@ import { useVideoResolutionSubscription } from '../hooks/useVideoResolutionSubsc import { SettingsDialogProvider } from '@/features/settings/components/SettingsDialogProvider' import { useSubtitles } from '@/features/subtitle/hooks/useSubtitles' import { Subtitles } from '@/features/subtitle/component/Subtitles' +import { CarouselLayout } from '../components/layout/CarouselLayout' const LayoutWrapper = styled( 'div',