From bbc3d18309694f55087681a3cec43579ed9aa2fc Mon Sep 17 00:00:00 2001 From: Cyril Date: Thu, 23 Apr 2026 15:04:59 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20adaptive=20PiP=20st?= =?UTF-8?q?age=20with=20focus=20and=20grid=20layouts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render all participants with a focus mode and animated adaptive grid. --- .../src/features/pip/components/PipView.tsx | 107 +---------------- .../pip/components/layouts/PipFocusLayout.tsx | 79 +++++++++++++ .../pip/components/layouts/PipGridLayout.tsx | 85 ++++++++++++++ .../pip/components/layouts/PipStage.tsx | 78 +++++++++++++ .../pip/hooks/usePipFlipAnimations.ts | 90 ++++++++++++++ .../src/features/pip/utils/pipGrid.ts | 110 ++++++++++++++++++ .../features/pip/utils/pipTrackSelection.ts | 48 ++++++++ 7 files changed, 495 insertions(+), 102 deletions(-) create mode 100644 src/frontend/src/features/pip/components/layouts/PipFocusLayout.tsx create mode 100644 src/frontend/src/features/pip/components/layouts/PipGridLayout.tsx create mode 100644 src/frontend/src/features/pip/components/layouts/PipStage.tsx create mode 100644 src/frontend/src/features/pip/hooks/usePipFlipAnimations.ts create mode 100644 src/frontend/src/features/pip/utils/pipGrid.ts create mode 100644 src/frontend/src/features/pip/utils/pipTrackSelection.ts diff --git a/src/frontend/src/features/pip/components/PipView.tsx b/src/frontend/src/features/pip/components/PipView.tsx index 585c0a70..b161d919 100644 --- a/src/frontend/src/features/pip/components/PipView.tsx +++ b/src/frontend/src/features/pip/components/PipView.tsx @@ -1,91 +1,20 @@ -import { styled } from '@/styled-system/jsx' import { supportsScreenSharing } from '@livekit/components-core' -import { - isTrackReference, - TrackReferenceOrPlaceholder, -} from '@livekit/components-core' -import { useTracks } from '@livekit/components-react' -import { Track } from 'livekit-client' -import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile' +import { styled } from '@/styled-system/jsx' import { SidePanel } from '@/features/rooms/livekit/components/SidePanel' import { pipLayoutStore } from '../stores/pipLayoutStore' import { PipControlBar } from './PipControlBar' import { PipReactionsToolbar } from './PipReactionsToolbar' +import { PipStage } from './layouts/PipStage' -const pickTrackForPip = ( - tracks: TrackReferenceOrPlaceholder[] -): TrackReferenceOrPlaceholder | undefined => { - // Prefer screen share when present; otherwise fallback to first available track. - const screenShareTrack = tracks - .filter((track) => isTrackReference(track)) - .find((track) => track.publication.source === Track.Source.ScreenShare) - if (screenShareTrack) return screenShareTrack - return tracks[0] -} - -const pickLocalCameraTrack = ( - tracks: TrackReferenceOrPlaceholder[] -): TrackReferenceOrPlaceholder | undefined => - tracks.find( - (track) => - track.source === Track.Source.Camera && track.participant?.isLocal - ) - -const pickRemoteCameraTrack = ( - tracks: TrackReferenceOrPlaceholder[] -): TrackReferenceOrPlaceholder | undefined => - tracks.find( - (track) => - track.source === Track.Source.Camera && !track.participant?.isLocal - ) - -/** - * Main view component for the Picture-in-Picture window. - * Handles track selection (prioritizes screen share), layout switching (grid for multiple participants), - * and renders the control bar and side panel within the PiP window. - */ +// Composition shell for the Picture-in-Picture window. + export const PipView = () => { - const tracks = useTracks( - [ - { source: Track.Source.Camera, withPlaceholder: true }, - { source: Track.Source.ScreenShare, withPlaceholder: false }, - ], - { onlySubscribed: false } - ) - - const trackRef = pickTrackForPip(tracks) const browserSupportsScreenSharing = supportsScreenSharing() - const hasMultipleTiles = tracks.length > 1 - const localCameraTrack = pickLocalCameraTrack(tracks) - const remoteCameraTrack = pickRemoteCameraTrack(tracks) - const mainTrackRef = remoteCameraTrack ?? trackRef - const thumbnailTrackRef = - mainTrackRef && localCameraTrack && localCameraTrack !== mainTrackRef - ? localCameraTrack - : tracks.find((track) => track !== mainTrackRef) - - if (!trackRef && !hasMultipleTiles) return null return ( - {/* Keep stage height stable to avoid layout shifting on track changes. */} - - {hasMultipleTiles ? ( - <> - {mainTrackRef && ( - - )} - {thumbnailTrackRef && ( - - - - )} - - ) : ( - - )} - + {/* Side panel (effects, settings, etc.) opens within PiP window. */} @@ -117,29 +46,3 @@ const PipContainer = styled('div', { }, }, }) - -const PipStage = styled('div', { - base: { - position: 'relative', - minHeight: 0, - margin: '0.5rem', - borderRadius: 'lg', - overflow: 'hidden', - }, -}) - -const PipThumbnail = styled('div', { - base: { - position: 'absolute', - right: '1rem', - bottom: '1rem', - width: '42%', - maxWidth: '220px', - minWidth: '140px', - aspectRatio: '16 / 9', - borderRadius: 'md', - overflow: 'hidden', - boxShadow: 'md', - zIndex: 2, - }, -}) diff --git a/src/frontend/src/features/pip/components/layouts/PipFocusLayout.tsx b/src/frontend/src/features/pip/components/layouts/PipFocusLayout.tsx new file mode 100644 index 00000000..285d557c --- /dev/null +++ b/src/frontend/src/features/pip/components/layouts/PipFocusLayout.tsx @@ -0,0 +1,79 @@ +import { memo } from 'react' +import type { TrackReferenceOrPlaceholder } from '@livekit/components-core' +import { styled } from '@/styled-system/jsx' +import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile' +import { getTrackKey } from '../../utils/pipTrackSelection' + +type PipFocusLayoutProps = { + mainTrack: TrackReferenceOrPlaceholder + thumbnailTrack?: TrackReferenceOrPlaceholder +} + +/** + * Focus layout used when 1-2 tracks are visible in the PiP window. + * + * The main tile is letterboxed (object-fit: contain) so the camera is + * never stretched to a non-video aspect and leaves dark padding + * above/below when the window shape doesn't match the source. + * The thumbnail keeps the usual cover fill. + */ +export const PipFocusLayout = memo(function PipFocusLayout({ + mainTrack, + thumbnailTrack, +}: PipFocusLayoutProps) { + return ( + + + + + {thumbnailTrack && ( + + + + )} + + ) +}) + +const FocusContainer = styled('div', { + base: { + position: 'relative', + width: '100%', + height: '100%', + backgroundColor: 'primaryDark.100', + }, +}) + +const MainSlot = styled('div', { + base: { + width: '100%', + height: '100%', + '& .lk-participant-media-video': { + objectFit: 'contain', + }, + }, +}) + +const Thumbnail = styled('div', { + base: { + position: 'absolute', + right: '1rem', + bottom: '1rem', + width: '42%', + maxWidth: '220px', + minWidth: '140px', + aspectRatio: '16 / 9', + borderRadius: 'md', + overflow: 'hidden', + boxShadow: 'md', + zIndex: 2, + }, +}) diff --git a/src/frontend/src/features/pip/components/layouts/PipGridLayout.tsx b/src/frontend/src/features/pip/components/layouts/PipGridLayout.tsx new file mode 100644 index 00000000..06b9a365 --- /dev/null +++ b/src/frontend/src/features/pip/components/layouts/PipGridLayout.tsx @@ -0,0 +1,85 @@ +import { memo, useMemo, useRef } from 'react' +import type { TrackReferenceOrPlaceholder } from '@livekit/components-core' +import { styled } from '@/styled-system/jsx' +import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile' +import { usePipElementSize } from '../../hooks/usePipElementSize' +import { usePipFlipAnimations } from '../../hooks/usePipFlipAnimations' +import { computePipGridLayout } from '../../utils/pipGrid' +import { getTrackKey } from '../../utils/pipTrackSelection' + +type PipGridLayoutProps = { + tracks: TrackReferenceOrPlaceholder[] +} + +/** + * Adaptive grid used when 3+ tracks are visible in the PiP window. + * + * All grid math (shape choice + partial-row stretching) is delegated to + * `computePipGridLayout`. This component only measures the container, + * applies the returned placements, and plays a FLIP animation when the + * tile set or grid shape changes (participant joins/leaves or shape shift). + * + * Tiles keep a stable key so resizing never remounts