diff --git a/src/frontend/src/features/rooms/livekit/components/DocumentPiPPortal.tsx b/src/frontend/src/features/rooms/livekit/components/DocumentPiPPortal.tsx index 9dfbc1ff..561c62c5 100644 --- a/src/frontend/src/features/rooms/livekit/components/DocumentPiPPortal.tsx +++ b/src/frontend/src/features/rooms/livekit/components/DocumentPiPPortal.tsx @@ -36,25 +36,63 @@ const syncThemeAttribute = (source: Document, target: Document) => { } } +const cssVarNameCacheByElement = new WeakMap() +const cssVarNameCacheByUri = new Map() + const syncCssVariables = (source: Document, target: Document) => { const sourceView = source.defaultView if (!sourceView) return - const applyVarsFrom = (element: HTMLElement | null) => { - if (!element) return - const styles = sourceView.getComputedStyle(element) - for (let i = 0; i < styles.length; i += 1) { - const property = styles[i] - if (!property.startsWith('--')) continue - const value = styles.getPropertyValue(property) - if (value) { - target.documentElement.style.setProperty(property, value) + const getCachedVarNames = () => { + const docEl = source.documentElement + if (!docEl) return [] + + const cachedByElement = cssVarNameCacheByElement.get(docEl) + if (cachedByElement) return cachedByElement + + const cachedByUri = source.baseURI + ? cssVarNameCacheByUri.get(source.baseURI) + : undefined + if (cachedByUri) return cachedByUri + + const varNames = new Set() + const collectVarsFrom = (element: HTMLElement | null) => { + if (!element) return + const styles = sourceView.getComputedStyle(element) + for (let i = 0; i < styles.length; i += 1) { + const property = styles[i] + if (property.startsWith('--')) { + varNames.add(property) + } } } + + collectVarsFrom(source.documentElement) + collectVarsFrom(source.body) + + const result = Array.from(varNames) + cssVarNameCacheByElement.set(docEl, result) + if (source.baseURI) { + cssVarNameCacheByUri.set(source.baseURI, result) + } + return result } - applyVarsFrom(source.documentElement) - applyVarsFrom(source.body) + const varNames = getCachedVarNames() + if (!varNames.length) return + + const rootStyles = sourceView.getComputedStyle(source.documentElement) + const bodyStyles = source.body + ? sourceView.getComputedStyle(source.body) + : null + + varNames.forEach((property) => { + const bodyValue = bodyStyles?.getPropertyValue(property) + const value = bodyValue || rootStyles.getPropertyValue(property) + if (value) { + target.documentElement.style.setProperty(property, value) + } + }) } /** diff --git a/src/frontend/src/features/rooms/livekit/hooks/useDocumentPiP.ts b/src/frontend/src/features/rooms/livekit/hooks/useDocumentPiP.ts index 1b4315de..1632ccd8 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/useDocumentPiP.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/useDocumentPiP.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' type DocumentPictureInPicture = { requestWindow: (options?: { @@ -19,24 +19,44 @@ export const useDocumentPiP = ({ height?: number } = {}) => { const [pipWindow, setPipWindow] = useState(null) + const pipWindowRef = useRef(null) + const pendingPiPRef = useRef | null>(null) - const isSupported = useMemo(() => { + const [isSupported] = useState(() => { if (typeof window === 'undefined') return false return 'documentPictureInPicture' in window - }, []) + }) const openPiP = useCallback(async () => { if (!isSupported) return null - if (pipWindow && !pipWindow.closed) return pipWindow + const existingWindow = pipWindowRef.current + if (existingWindow && !existingWindow.closed) return existingWindow + + if (pendingPiPRef.current) return pendingPiPRef.current // Request a new PiP window from the browser API. const pip = (window as WindowWithDocumentPiP).documentPictureInPicture if (!pip) return null - const win = await pip.requestWindow({ width, height }) - setPipWindow(win) - return win - }, [height, isSupported, pipWindow, width]) + const requestPromise = (async () => { + try { + const win = await pip.requestWindow({ width, height }) + const currentWindow = pipWindowRef.current + if (currentWindow && !currentWindow.closed) return currentWindow + setPipWindow(win) + return win + } catch (error) { + // Avoid unhandled rejections if the user blocks or closes the request. + console.error('Failed to open Picture-in-Picture window', error) + return null + } finally { + pendingPiPRef.current = null + } + })() + + pendingPiPRef.current = requestPromise + return requestPromise + }, [height, isSupported, width]) const closePiP = useCallback(() => { if (!pipWindow) return @@ -46,6 +66,10 @@ export const useDocumentPiP = ({ setPipWindow(null) }, [pipWindow]) + useEffect(() => { + pipWindowRef.current = pipWindow + }, [pipWindow]) + useEffect(() => { if (!pipWindow) return diff --git a/src/frontend/src/primitives/useOverlayPortalContainer.tsx b/src/frontend/src/primitives/useOverlayPortalContainer.tsx index e8e93032..58e5f5d6 100644 --- a/src/frontend/src/primitives/useOverlayPortalContainer.tsx +++ b/src/frontend/src/primitives/useOverlayPortalContainer.tsx @@ -17,8 +17,5 @@ export const useOverlayPortalContainer = () => { */ export const useOverlayBoundaryElement = () => { const portalContainer = useOverlayPortalContainer() - return useMemo(() => { - if (portalContainer) return portalContainer - return undefined - }, [portalContainer]) + return portalContainer }