mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-27 10:46:47 +00:00
✨(feat) add pip portal and base layout
mount react content into pip window with styles
This commit is contained in:
@@ -0,0 +1,103 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { useDocumentPiP } from '../hooks/useDocumentPiP'
|
||||||
|
|
||||||
|
const ensureBaseStyles = (target: Document) => {
|
||||||
|
if (target.getElementById('pip-base-styles')) return
|
||||||
|
const style = target.createElement('style')
|
||||||
|
style.id = 'pip-base-styles'
|
||||||
|
style.textContent = `
|
||||||
|
html, body { margin: 0; padding: 0; height: 100%; background: #0b0f19; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
`
|
||||||
|
target.head.appendChild(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyStyles = (source: Document, target: Document) => {
|
||||||
|
if (target.getElementById('pip-style-clone')) return
|
||||||
|
const marker = target.createElement('meta')
|
||||||
|
marker.id = 'pip-style-clone'
|
||||||
|
target.head.appendChild(marker)
|
||||||
|
|
||||||
|
source.querySelectorAll('style, link[rel="stylesheet"]').forEach((node) => {
|
||||||
|
const cloned = node.cloneNode(true) as HTMLElement
|
||||||
|
target.head.appendChild(cloned)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocumentPiPPortal = ({
|
||||||
|
isOpen,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
children,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
children: React.ReactNode
|
||||||
|
onClose?: () => void
|
||||||
|
}) => {
|
||||||
|
const { openPiP, closePiP, pipWindow, isSupported } = useDocumentPiP({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
})
|
||||||
|
const [container, setContainer] = useState<HTMLElement | null>(null)
|
||||||
|
const containerRef = useRef<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
closePiP()
|
||||||
|
setContainer(null)
|
||||||
|
containerRef.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSupported) return
|
||||||
|
|
||||||
|
let cancelled = false
|
||||||
|
openPiP().then((win) => {
|
||||||
|
if (!win || cancelled) return
|
||||||
|
const doc = win.document
|
||||||
|
ensureBaseStyles(doc)
|
||||||
|
copyStyles(document, doc)
|
||||||
|
const existingContainer = containerRef.current
|
||||||
|
if (!existingContainer || existingContainer.ownerDocument !== doc) {
|
||||||
|
const nextContainer = doc.createElement('div')
|
||||||
|
nextContainer.id = 'pip-root'
|
||||||
|
doc.body.appendChild(nextContainer)
|
||||||
|
containerRef.current = nextContainer
|
||||||
|
setContainer(nextContainer)
|
||||||
|
} else {
|
||||||
|
setContainer(existingContainer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [closePiP, isOpen, isSupported, openPiP])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pipWindow) return
|
||||||
|
const handleClose = () => {
|
||||||
|
containerRef.current = null
|
||||||
|
setContainer(null)
|
||||||
|
onClose?.()
|
||||||
|
}
|
||||||
|
pipWindow.addEventListener('pagehide', handleClose)
|
||||||
|
pipWindow.addEventListener('beforeunload', handleClose)
|
||||||
|
return () => {
|
||||||
|
pipWindow.removeEventListener('pagehide', handleClose)
|
||||||
|
pipWindow.removeEventListener('beforeunload', handleClose)
|
||||||
|
}
|
||||||
|
}, [onClose, pipWindow])
|
||||||
|
|
||||||
|
const portal = useMemo(() => {
|
||||||
|
if (!container) return null
|
||||||
|
return createPortal(children, container)
|
||||||
|
}, [children, container])
|
||||||
|
|
||||||
|
return portal
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
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 { AudioDevicesControl } from './controls/Device/AudioDevicesControl'
|
||||||
|
import { VideoDeviceControl } from './controls/Device/VideoDeviceControl'
|
||||||
|
import { ScreenShareToggle } from './controls/ScreenShareToggle'
|
||||||
|
import { LeaveButton } from './controls/LeaveButton'
|
||||||
|
import { ParticipantTile } from './ParticipantTile'
|
||||||
|
|
||||||
|
const pickTrackForPip = (
|
||||||
|
tracks: TrackReferenceOrPlaceholder[]
|
||||||
|
): TrackReferenceOrPlaceholder | undefined => {
|
||||||
|
const screenShareTrack = tracks
|
||||||
|
.filter(isTrackReference)
|
||||||
|
.find((track) => track.publication.source === Track.Source.ScreenShare)
|
||||||
|
|
||||||
|
if (screenShareTrack) return screenShareTrack
|
||||||
|
return tracks[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
if (!trackRef) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PipContainer>
|
||||||
|
<PipStage>
|
||||||
|
<ParticipantTile trackRef={trackRef} disableMetadata />
|
||||||
|
</PipStage>
|
||||||
|
<PipControlsBar
|
||||||
|
showScreenShare={browserSupportsScreenSharing}
|
||||||
|
/>
|
||||||
|
</PipContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PipContainer = styled('div', {
|
||||||
|
base: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateRows: 'minmax(0, 1fr) auto',
|
||||||
|
backgroundColor: 'primaryDark.50',
|
||||||
|
'& .lk-participant-tile': {
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
'& .lk-participant-media': {
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
'& .lk-participant-media-video': {
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const PipStage = styled('div', {
|
||||||
|
base: {
|
||||||
|
position: 'relative',
|
||||||
|
minHeight: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const PipControls = styled('div', {
|
||||||
|
base: {
|
||||||
|
flex: '0 0 auto',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
padding: '0.5rem',
|
||||||
|
backgroundColor: 'primaryDark.100',
|
||||||
|
borderTop: '1px solid',
|
||||||
|
borderColor: 'primaryDark.200',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const PipControlsBar = ({
|
||||||
|
showScreenShare,
|
||||||
|
}: {
|
||||||
|
showScreenShare: boolean
|
||||||
|
}) => (
|
||||||
|
<PipControls>
|
||||||
|
<AudioDevicesControl hideMenu />
|
||||||
|
<VideoDeviceControl hideMenu />
|
||||||
|
{showScreenShare && <ScreenShareToggle />}
|
||||||
|
<LeaveButton />
|
||||||
|
</PipControls>
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { DocumentPiPPortal } from './DocumentPiPPortal'
|
||||||
|
import { PipView } from './PipView'
|
||||||
|
import { useRoomPiP } from '../hooks/useRoomPiP'
|
||||||
|
|
||||||
|
export const RoomPiP = () => {
|
||||||
|
const { isOpen, close } = useRoomPiP()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentPiPPortal isOpen={isOpen} onClose={close}>
|
||||||
|
<PipView />
|
||||||
|
</DocumentPiPPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user