mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-31 12:47:58 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26cb99b81c | |||
| 5bc04409de | |||
| 78a06ab55f | |||
| 1443ef12eb |
@@ -75,6 +75,7 @@ db.sqlite3
|
||||
# IDEs
|
||||
.idea/
|
||||
.vscode/
|
||||
.cursor/
|
||||
*.iml
|
||||
.devcontainer
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) share OneToOneFocusLayout between PiP and main room
|
||||
|
||||
## [1.29.0] - 2026-08-25
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { memo } from 'react'
|
||||
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { cva } from '@/styled-system/css'
|
||||
import { ParticipantTile } from '@/features/participantTile/components/ParticipantTile'
|
||||
import { getTrackKey } from '@/features/layout/utils/trackSelection'
|
||||
|
||||
type OneToOneFocusLayoutProps = {
|
||||
mainTrack?: TrackReferenceOrPlaceholder
|
||||
thumbnailTrack?: TrackReferenceOrPlaceholder
|
||||
disableTileControls?: boolean
|
||||
/** Controls thumbnail dimensions – 'pip' for small PiP window, 'room' for the main viewport. */
|
||||
context?: 'pip' | 'room'
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus layout for 1-to-1 calls: one main tile filling the area (letterboxed)
|
||||
* with an optional thumbnail overlay at the bottom-right.
|
||||
*
|
||||
* Shared between PiP and the main room – pass `disableTileControls` in PiP
|
||||
* where hover controls should be hidden.
|
||||
*/
|
||||
export const OneToOneFocusLayout = memo(
|
||||
({
|
||||
mainTrack,
|
||||
thumbnailTrack,
|
||||
disableTileControls,
|
||||
context = 'room',
|
||||
}: OneToOneFocusLayoutProps) => {
|
||||
return (
|
||||
<FocusContainer>
|
||||
{mainTrack && (
|
||||
<MainSlot>
|
||||
<ParticipantTile
|
||||
key={getTrackKey(mainTrack)}
|
||||
trackRef={mainTrack}
|
||||
disableTileControls={disableTileControls}
|
||||
/>
|
||||
</MainSlot>
|
||||
)}
|
||||
{thumbnailTrack && (
|
||||
<Thumbnail context={context}>
|
||||
<ParticipantTile
|
||||
key={getTrackKey(thumbnailTrack)}
|
||||
trackRef={thumbnailTrack}
|
||||
disableTileControls={disableTileControls}
|
||||
/>
|
||||
</Thumbnail>
|
||||
)}
|
||||
</FocusContainer>
|
||||
)
|
||||
}
|
||||
)
|
||||
OneToOneFocusLayout.displayName = 'OneToOneFocusLayout'
|
||||
|
||||
const FocusContainer = styled('div', {
|
||||
base: {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'primaryDark.100',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
})
|
||||
|
||||
const MainSlot = styled('div', {
|
||||
base: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
'& .lk-participant-tile': {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
'& .lk-participant-media-video': {
|
||||
objectFit: 'contain',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Thumbnail = styled(
|
||||
'div',
|
||||
cva({
|
||||
base: {
|
||||
position: 'absolute',
|
||||
right: '1.25rem',
|
||||
bottom: '1.25rem',
|
||||
aspectRatio: '16 / 9',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: 'md',
|
||||
zIndex: 2,
|
||||
'& .lk-participant-tile': {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
context: {
|
||||
pip: {
|
||||
width: '42%',
|
||||
maxWidth: '220px',
|
||||
minWidth: '140px',
|
||||
},
|
||||
room: {
|
||||
width: '20%',
|
||||
maxWidth: '320px',
|
||||
minWidth: '180px',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
context: 'room',
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
import { Track } from 'livekit-client'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { clearPinnedTrack, layoutStore, setPinnedTrack } from '@/stores/layout'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { OneToOneFocusLayout } from '@/features/layout/components/OneToOneFocusLayout'
|
||||
|
||||
export const StageLayout = () => {
|
||||
const lastAutoFocusedScreenShareTrack =
|
||||
@@ -36,6 +37,29 @@ export const StageLayout = () => {
|
||||
(track) => !isEqualTrackRef(track, pinnedTrackRef)
|
||||
)
|
||||
|
||||
const cameraTracks = useMemo(
|
||||
() => tracks.filter((t) => t.source === Track.Source.Camera),
|
||||
[tracks]
|
||||
)
|
||||
|
||||
const isOneToOne =
|
||||
!pinnedTrackRef &&
|
||||
screenShareTracks.length === 0 &&
|
||||
cameraTracks.length <= 2
|
||||
|
||||
const oneToOneMainTrack = useMemo(() => {
|
||||
if (!isOneToOne) return undefined
|
||||
const remote = cameraTracks.find((t) => !t.participant?.isLocal)
|
||||
const local = cameraTracks.find((t) => t.participant?.isLocal)
|
||||
return remote ?? local
|
||||
}, [isOneToOne, cameraTracks])
|
||||
|
||||
const oneToOneThumbnailTrack = useMemo(() => {
|
||||
if (!isOneToOne) return undefined
|
||||
const local = cameraTracks.find((t) => t.participant?.isLocal)
|
||||
return oneToOneMainTrack === local ? undefined : local
|
||||
}, [isOneToOne, cameraTracks, oneToOneMainTrack])
|
||||
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
// Code duplicated from LiveKit; this warning will be addressed in the refactoring.
|
||||
useEffect(() => {
|
||||
@@ -87,7 +111,12 @@ export const StageLayout = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{!pinnedTrackRef ? (
|
||||
{isOneToOne ? (
|
||||
<OneToOneFocusLayout
|
||||
mainTrack={oneToOneMainTrack}
|
||||
thumbnailTrack={oneToOneThumbnailTrack}
|
||||
/>
|
||||
) : !pinnedTrackRef ? (
|
||||
<div className="lk-grid-layout-wrapper" style={{ height: 'auto' }}>
|
||||
<GridLayout tracks={tracks} style={{ padding: 0 }}>
|
||||
<ParticipantTile />
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { memo } from 'react'
|
||||
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { ParticipantTile } from '@/features/participantTile/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(
|
||||
({ mainTrack, thumbnailTrack }: PipFocusLayoutProps) => {
|
||||
return (
|
||||
<FocusContainer>
|
||||
{mainTrack && (
|
||||
<MainSlot>
|
||||
<ParticipantTile
|
||||
key={getTrackKey(mainTrack)}
|
||||
trackRef={mainTrack}
|
||||
disableTileControls
|
||||
/>
|
||||
</MainSlot>
|
||||
)}
|
||||
{thumbnailTrack && (
|
||||
<Thumbnail>
|
||||
<ParticipantTile
|
||||
key={getTrackKey(thumbnailTrack)}
|
||||
trackRef={thumbnailTrack}
|
||||
disableTileControls
|
||||
/>
|
||||
</Thumbnail>
|
||||
)}
|
||||
</FocusContainer>
|
||||
)
|
||||
}
|
||||
)
|
||||
PipFocusLayout.displayName = 'PipFocusLayout'
|
||||
|
||||
const FocusContainer = styled('div', {
|
||||
base: {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'primaryDark.100',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
})
|
||||
|
||||
const MainSlot = styled('div', {
|
||||
base: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
'& .lk-participant-media-video': {
|
||||
objectFit: 'contain',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Thumbnail = styled('div', {
|
||||
base: {
|
||||
position: 'absolute',
|
||||
right: '1.25rem',
|
||||
bottom: '1.25rem',
|
||||
width: '42%',
|
||||
maxWidth: '220px',
|
||||
minWidth: '140px',
|
||||
aspectRatio: '16 / 9',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: 'md',
|
||||
zIndex: 2,
|
||||
},
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import { styled } from '@/styled-system/jsx'
|
||||
import { ParticipantTile } from '@/features/participantTile/components/ParticipantTile'
|
||||
import { usePipElementSize } from '../../hooks/usePipElementSize'
|
||||
import { computePipGridLayout } from '../../utils/pipGrid'
|
||||
import { getTrackKey } from '../../utils/pipTrackSelection'
|
||||
import { getTrackKey } from '@/features/layout/utils/trackSelection'
|
||||
|
||||
type PipGridLayoutProps = {
|
||||
tracks: TrackReferenceOrPlaceholder[]
|
||||
|
||||
@@ -2,7 +2,7 @@ import { memo } from 'react'
|
||||
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { ParticipantTile } from '@/features/participantTile/components/ParticipantTile'
|
||||
import { getTrackKey } from '../../utils/pipTrackSelection'
|
||||
import { getTrackKey } from '@/features/layout/utils/trackSelection'
|
||||
|
||||
type PipScreenShareLayoutProps = {
|
||||
screenShareTrack: TrackReferenceOrPlaceholder
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
|
||||
import { usePagination, useTracks } from '@livekit/components-react'
|
||||
import { RoomEvent, Track } from 'livekit-client'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { PipFocusLayout } from './PipFocusLayout'
|
||||
import { OneToOneFocusLayout } from '@/features/layout/components/OneToOneFocusLayout'
|
||||
import { PipGridLayout } from './PipGridLayout'
|
||||
import { PipPagination } from './PipPagination'
|
||||
import { PipScreenShareLayout } from './PipScreenShareLayout'
|
||||
@@ -59,9 +59,11 @@ export const PipStage = () => {
|
||||
if (cameraTracks.length <= 1) {
|
||||
return (
|
||||
<StageFrame>
|
||||
<PipFocusLayout
|
||||
<OneToOneFocusLayout
|
||||
mainTrack={screenShareTrack}
|
||||
thumbnailTrack={cameraTracks[0]}
|
||||
disableTileControls
|
||||
context="pip"
|
||||
/>
|
||||
</StageFrame>
|
||||
)
|
||||
@@ -99,7 +101,12 @@ export const PipStage = () => {
|
||||
|
||||
return (
|
||||
<StageFrame>
|
||||
<PipFocusLayout mainTrack={mainTrack} thumbnailTrack={thumbnailTrack} />
|
||||
<OneToOneFocusLayout
|
||||
mainTrack={mainTrack}
|
||||
thumbnailTrack={thumbnailTrack}
|
||||
disableTileControls
|
||||
context="pip"
|
||||
/>
|
||||
</StageFrame>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user