diff --git a/.gitignore b/.gitignore
index 4f5dbf29..608168fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,6 +75,7 @@ db.sqlite3
# IDEs
.idea/
.vscode/
+.cursor/
*.iml
.devcontainer
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9eb62c0a..ad5ac938 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to
- ✨(frontend) add participant color gradient when camera is off #1490
- ✨(all) allow forcing SSO display name for authenticated users
- ➕(frontend) install vite-plugin-static-copy for MediaPipe WASM assets
+- ✨(frontend) share OneToOneFocusLayout between PiP and main room
### Changed
diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
index 13d476fb..d7c7a645 100644
--- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
+++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
@@ -6,7 +6,7 @@ import {
log,
} from '@livekit/components-core'
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 {
ConnectionStateToast,
FocusLayoutContainer,
@@ -40,6 +40,7 @@ import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { ReactionPortals } from '@/features/reactions/components/ReactionPortals'
import { CarouselLayout } from '@/features/layout/components/CarouselLayout'
import { GridLayout } from '@/features/layout/components/GridLayout'
+import { OneToOneFocusLayout } from '@/features/layout/components/OneToOneFocusLayout'
import { RoomContentArea } from '@/features/layout/components/RoomContentArea'
import { usePictureInPicture } from '@/features/pip/hooks/usePictureInPicture'
import { PipRoomPlaceholder } from '@/features/pip/components/PipRoomPlaceholder'
@@ -123,6 +124,26 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
(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()
// handle pin announcements
@@ -258,7 +279,12 @@ export function VideoConference({ ...props }: VideoConferenceProps) {