mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-13 20:27:01 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| baed2d8289 | |||
| 3e6ccbcab9 | |||
| 1a3fd22a14 | |||
| cd81ea7c46 |
@@ -75,6 +75,7 @@ db.sqlite3
|
|||||||
# IDEs
|
# IDEs
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.cursor/
|
||||||
*.iml
|
*.iml
|
||||||
.devcontainer
|
.devcontainer
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ and this project adheres to
|
|||||||
- ✨(frontend) add participant color gradient when camera is off #1490
|
- ✨(frontend) add participant color gradient when camera is off #1490
|
||||||
- ✨(all) allow forcing SSO display name for authenticated users
|
- ✨(all) allow forcing SSO display name for authenticated users
|
||||||
- ➕(frontend) install vite-plugin-static-copy for MediaPipe WASM assets
|
- ➕(frontend) install vite-plugin-static-copy for MediaPipe WASM assets
|
||||||
|
- ✨(frontend) share OneToOneFocusLayout between PiP and main room
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -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/rooms/livekit/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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -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/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(
|
|
||||||
({ 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/rooms/livekit/components/ParticipantTile'
|
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
|
||||||
import { usePipElementSize } from '../../hooks/usePipElementSize'
|
import { usePipElementSize } from '../../hooks/usePipElementSize'
|
||||||
import { computePipGridLayout } from '../../utils/pipGrid'
|
import { computePipGridLayout } from '../../utils/pipGrid'
|
||||||
import { getTrackKey } from '../../utils/pipTrackSelection'
|
import { getTrackKey } from '@/features/layout/utils/trackSelection'
|
||||||
|
|
||||||
type PipGridLayoutProps = {
|
type PipGridLayoutProps = {
|
||||||
tracks: TrackReferenceOrPlaceholder[]
|
tracks: TrackReferenceOrPlaceholder[]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { memo } from 'react'
|
|||||||
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
|
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
|
||||||
import { getTrackKey } from '../../utils/pipTrackSelection'
|
import { getTrackKey } from '@/features/layout/utils/trackSelection'
|
||||||
|
|
||||||
type PipScreenShareLayoutProps = {
|
type PipScreenShareLayoutProps = {
|
||||||
screenShareTrack: TrackReferenceOrPlaceholder
|
screenShareTrack: TrackReferenceOrPlaceholder
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
|
|||||||
import { usePagination, useTracks } from '@livekit/components-react'
|
import { usePagination, useTracks } from '@livekit/components-react'
|
||||||
import { RoomEvent, Track } from 'livekit-client'
|
import { RoomEvent, Track } from 'livekit-client'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { PipFocusLayout } from './PipFocusLayout'
|
import { OneToOneFocusLayout } from '@/features/layout/components/OneToOneFocusLayout'
|
||||||
import { PipGridLayout } from './PipGridLayout'
|
import { PipGridLayout } from './PipGridLayout'
|
||||||
import { PipPagination } from './PipPagination'
|
import { PipPagination } from './PipPagination'
|
||||||
import { PipScreenShareLayout } from './PipScreenShareLayout'
|
import { PipScreenShareLayout } from './PipScreenShareLayout'
|
||||||
@@ -59,9 +59,11 @@ export const PipStage = () => {
|
|||||||
if (cameraTracks.length <= 1) {
|
if (cameraTracks.length <= 1) {
|
||||||
return (
|
return (
|
||||||
<StageFrame>
|
<StageFrame>
|
||||||
<PipFocusLayout
|
<OneToOneFocusLayout
|
||||||
mainTrack={screenShareTrack}
|
mainTrack={screenShareTrack}
|
||||||
thumbnailTrack={cameraTracks[0]}
|
thumbnailTrack={cameraTracks[0]}
|
||||||
|
disableTileControls
|
||||||
|
context="pip"
|
||||||
/>
|
/>
|
||||||
</StageFrame>
|
</StageFrame>
|
||||||
)
|
)
|
||||||
@@ -99,7 +101,12 @@ export const PipStage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StageFrame>
|
<StageFrame>
|
||||||
<PipFocusLayout mainTrack={mainTrack} thumbnailTrack={thumbnailTrack} />
|
<OneToOneFocusLayout
|
||||||
|
mainTrack={mainTrack}
|
||||||
|
thumbnailTrack={thumbnailTrack}
|
||||||
|
disableTileControls
|
||||||
|
context="pip"
|
||||||
|
/>
|
||||||
</StageFrame>
|
</StageFrame>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
log,
|
log,
|
||||||
} from '@livekit/components-core'
|
} from '@livekit/components-core'
|
||||||
import { type Participant, RoomEvent, Track } from 'livekit-client'
|
import { type Participant, RoomEvent, Track } from 'livekit-client'
|
||||||
import React, { useCallback, useRef, useState, useEffect } from 'react'
|
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
ConnectionStateToast,
|
ConnectionStateToast,
|
||||||
FocusLayoutContainer,
|
FocusLayoutContainer,
|
||||||
@@ -40,6 +40,7 @@ import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
|||||||
import { ReactionPortals } from '@/features/reactions/components/ReactionPortals'
|
import { ReactionPortals } from '@/features/reactions/components/ReactionPortals'
|
||||||
import { CarouselLayout } from '@/features/layout/components/CarouselLayout'
|
import { CarouselLayout } from '@/features/layout/components/CarouselLayout'
|
||||||
import { GridLayout } from '@/features/layout/components/GridLayout'
|
import { GridLayout } from '@/features/layout/components/GridLayout'
|
||||||
|
import { OneToOneFocusLayout } from '@/features/layout/components/OneToOneFocusLayout'
|
||||||
import { RoomContentArea } from '@/features/layout/components/RoomContentArea'
|
import { RoomContentArea } from '@/features/layout/components/RoomContentArea'
|
||||||
import { usePictureInPicture } from '@/features/pip/hooks/usePictureInPicture'
|
import { usePictureInPicture } from '@/features/pip/hooks/usePictureInPicture'
|
||||||
import { PipRoomPlaceholder } from '@/features/pip/components/PipRoomPlaceholder'
|
import { PipRoomPlaceholder } from '@/features/pip/components/PipRoomPlaceholder'
|
||||||
@@ -123,6 +124,26 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
|||||||
(track) => !isEqualTrackRef(track, focusTrack)
|
(track) => !isEqualTrackRef(track, focusTrack)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cameraTracks = useMemo(
|
||||||
|
() => tracks.filter((t) => t.source === Track.Source.Camera),
|
||||||
|
[tracks]
|
||||||
|
)
|
||||||
|
|
||||||
|
const isOneToOne = !focusTrack && 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])
|
||||||
|
|
||||||
const { isOpen: isPictureInPictureOpen } = usePictureInPicture()
|
const { isOpen: isPictureInPictureOpen } = usePictureInPicture()
|
||||||
|
|
||||||
// handle pin announcements
|
// handle pin announcements
|
||||||
@@ -258,7 +279,12 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
|||||||
<PipRoomPlaceholder />
|
<PipRoomPlaceholder />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{!focusTrack ? (
|
{isOneToOne ? (
|
||||||
|
<OneToOneFocusLayout
|
||||||
|
mainTrack={oneToOneMainTrack}
|
||||||
|
thumbnailTrack={oneToOneThumbnailTrack}
|
||||||
|
/>
|
||||||
|
) : !focusTrack ? (
|
||||||
<div
|
<div
|
||||||
className="lk-grid-layout-wrapper"
|
className="lk-grid-layout-wrapper"
|
||||||
style={{ height: 'auto' }}
|
style={{ height: 'auto' }}
|
||||||
|
|||||||
Reference in New Issue
Block a user