diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx
index 37e87854..d8bb285b 100644
--- a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx
+++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx
@@ -7,6 +7,7 @@ import { EffectsMenuItem } from './EffectsMenuItem'
import { SupportMenuItem } from './SupportMenuItem'
import { TranscriptMenuItem } from './TranscriptMenuItem'
import { ScreenRecordingMenuItem } from './ScreenRecordingMenuItem'
+import { PictureInPictureMenuItem } from './PictureInPictureMenuItem'
// @todo try refactoring it to use MenuList component
export const OptionsMenuItems = () => {
@@ -21,6 +22,7 @@ export const OptionsMenuItems = () => {
+
diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/PictureInPictureMenuItem.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/PictureInPictureMenuItem.tsx
new file mode 100644
index 00000000..09d4777b
--- /dev/null
+++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/PictureInPictureMenuItem.tsx
@@ -0,0 +1,24 @@
+import { MenuItem } from 'react-aria-components'
+import { useTranslation } from 'react-i18next'
+import { RiPictureInPicture2Line } from '@remixicon/react'
+import { menuRecipe } from '@/primitives/menuRecipe'
+import { useRoomPiP } from '../../../hooks/useRoomPiP'
+
+export const PictureInPictureMenuItem = () => {
+ const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
+ const { isSupported, isOpen, toggle } = useRoomPiP()
+
+ // Hide the entry when the browser doesn't support Document PiP.
+ if (!isSupported) return null
+
+ return (
+
+ )
+}
+
diff --git a/src/frontend/src/features/rooms/livekit/hooks/RoomPiPProvider.tsx b/src/frontend/src/features/rooms/livekit/hooks/RoomPiPProvider.tsx
new file mode 100644
index 00000000..3726daf7
--- /dev/null
+++ b/src/frontend/src/features/rooms/livekit/hooks/RoomPiPProvider.tsx
@@ -0,0 +1,39 @@
+import { useCallback, useMemo, useState } from 'react'
+import { RoomPiPContext } from './roomPiPContext'
+
+export const RoomPiPProvider = ({
+ children,
+}: {
+ children: React.ReactNode
+}) => {
+ const [isOpen, setIsOpen] = useState(false)
+
+ // Feature detection for Document Picture-in-Picture.
+ const isSupported =
+ typeof window !== 'undefined' && 'documentPictureInPicture' in window
+
+ const open = useCallback(() => setIsOpen(true), [])
+ const close = useCallback(() => setIsOpen(false), [])
+ const toggle = useCallback(
+ () => setIsOpen((current) => !current),
+ []
+ )
+
+ const value = useMemo(
+ () => ({
+ isSupported,
+ isOpen,
+ open,
+ close,
+ toggle,
+ }),
+ [close, isOpen, isSupported, open, toggle]
+ )
+
+ return (
+
+ {children}
+
+ )
+}
+
diff --git a/src/frontend/src/features/rooms/livekit/hooks/roomPiPContext.ts b/src/frontend/src/features/rooms/livekit/hooks/roomPiPContext.ts
new file mode 100644
index 00000000..075289b2
--- /dev/null
+++ b/src/frontend/src/features/rooms/livekit/hooks/roomPiPContext.ts
@@ -0,0 +1,12 @@
+import { createContext } from 'react'
+
+export type RoomPiPContextValue = {
+ isSupported: boolean
+ isOpen: boolean
+ open: () => void
+ close: () => void
+ toggle: () => void
+}
+
+export const RoomPiPContext = createContext(null)
+
diff --git a/src/frontend/src/features/rooms/livekit/hooks/useRoomPiP.tsx b/src/frontend/src/features/rooms/livekit/hooks/useRoomPiP.tsx
new file mode 100644
index 00000000..d1bc4bc2
--- /dev/null
+++ b/src/frontend/src/features/rooms/livekit/hooks/useRoomPiP.tsx
@@ -0,0 +1,11 @@
+import { useContext } from 'react'
+import { RoomPiPContext } from './roomPiPContext'
+
+export const useRoomPiP = () => {
+ const context = useContext(RoomPiPContext)
+ if (!context) {
+ throw new Error('useRoomPiP must be used within a RoomPiPProvider')
+ }
+ return context
+}
+
diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
index c81dfc68..466d36e2 100644
--- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
+++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
@@ -34,6 +34,8 @@ import { SettingsDialogExtendedKey } from '@/features/settings/type'
import { useVideoResolutionSubscription } from '../hooks/useVideoResolutionSubscription'
import { SettingsDialogProvider } from '@/features/settings/components/SettingsDialogProvider'
import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal'
+import { RoomPiP } from '../components/RoomPiP'
+import { RoomPiPProvider } from '../hooks/RoomPiPProvider'
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { ReactionPortals } from '@/features/reactions/components/ReactionPortals'
@@ -238,10 +240,11 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
}}
>
{isWeb() && (
-
+
+
setIsShareErrorVisible(false)}
@@ -289,7 +292,9 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
}}
/>
-
+
+
+
)}