import { PlayerUI, PortalHost, Toast, useLivestreamInfo, useOffline, usePlayerDimensions, usePlayerStore, useSegment, View, zero, } from "@streamplace/components"; import { AnimatedGradient } from "components/ui/gradient"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { Platform } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { BottomControlBar, MuteOverlay, TopControlBar, } from "./desktop-ui/index"; import { useResponsiveLayout } from "./useResponsiveLayout"; const { h, layout, position, w, px, py, r, p } = zero; function isRefObject( ref: any, ): ref is | React.RefObject | React.MutableRefObject { return ref && typeof ref === "object" && "current" in ref; } export function DesktopUi({ dropdownPortalContainer, isChatOpen, setIsChatOpen, }: { dropdownPortalContainer?: any; isChatOpen?: boolean; setIsChatOpen?: (open: boolean) => void; }) { const { ingest, title, setTitle, showCountdown, setShowCountdown, recordSubmitted, setRecordSubmitted, toggleGoLive, } = useLivestreamInfo(); const { width, height } = usePlayerDimensions(); const { shouldShowFloatingMetrics } = useResponsiveLayout(); const playerId = usePlayerStore((state) => state.id); const originalSafeAreaInsets = useSafeAreaInsets(); const offline = useOffline(); const showMetrics = usePlayerStore((state) => state.showDebugInfo); const pipAction = usePlayerStore((state) => state.pipAction); const videoRef = usePlayerStore((state) => state.videoRef); const embedded = usePlayerStore((state) => state.embedded); const fullscreen = usePlayerStore((state) => state.fullscreen); const setFullscreen = usePlayerStore((state) => state.setFullscreen); const selectedRendition = usePlayerStore((state) => state.selectedRendition); const safeAreaInsets = embedded ? { ...originalSafeAreaInsets, top: 0 } : originalSafeAreaInsets; const segment = useSegment(); const [isControlsVisible, setIsControlsVisible] = useState(true); const [pipSupported, setPipSupported] = useState(false); const [pipActive, setPipActive] = useState(false); const fadeOpacity = useSharedValue(1); const fadeTimeout = useRef(null); const FADE_OUT_DELAY = 2500; const isSelfAndNotLive = ingest === "new"; const isActivelyLive = ingest !== null && ingest !== "new"; const resetFadeTimer = useCallback(() => { fadeOpacity.value = withTiming(1, { duration: 200 }); if (fadeTimeout.current) clearTimeout(fadeTimeout.current); setIsControlsVisible(true); if (selectedRendition === "audio") return; if (ingest !== null) return; fadeTimeout.current = setTimeout(() => { fadeOpacity.value = withTiming(0, { duration: 400 }); setIsControlsVisible(false); }, FADE_OUT_DELAY); }, [fadeOpacity, selectedRendition, ingest]); const onPlayerHover = useCallback(() => { resetFadeTimer(); }, [resetFadeTimer]); const toggleChat = useCallback(() => { if (setIsChatOpen) setIsChatOpen(!isChatOpen); }, []); const toggleFullscreen = useCallback(() => { setFullscreen(!fullscreen); }, [fullscreen, setFullscreen]); useEffect(() => { resetFadeTimer(); return () => { if (fadeTimeout.current) clearTimeout(fadeTimeout.current); }; }, [resetFadeTimer]); const animatedFadeStyle = useAnimatedStyle(() => ({ opacity: shouldShowFloatingMetrics ? 1 : fadeOpacity.value, })); // Picture-in-Picture support detection useEffect(() => { if (Platform.OS === "web") { setPipSupported( !!document.pictureInPictureEnabled && pipAction !== undefined, ); } }, [pipAction]); // Picture-in-Picture event listeners useEffect(() => { if (Platform.OS !== "web") return; let video: HTMLVideoElement | null = null; if (isRefObject(videoRef)) { video = videoRef.current; } if (!video) return; function onEnter() { setPipActive(true); } function onLeave() { setPipActive(false); } video.addEventListener("enterpictureinpicture", onEnter); video.addEventListener("leavepictureinpicture", onLeave); return () => { if (video) { video.removeEventListener("enterpictureinpicture", onEnter); video.removeEventListener("leavepictureinpicture", onLeave); } }; }, [videoRef]); // Keyboard shortcuts (F for fullscreen) useEffect(() => { if (Platform.OS !== "web") return; function handleKeyDown(e: KeyboardEvent) { if (e.key === "f" || e.key === "F") { // are we in an input/textarea or contenteditable element? const activeEl = document.activeElement; const isInput = activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || (activeEl as HTMLElement).isContentEditable); if (isInput) return; e.preventDefault(); toggleFullscreen(); } } document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("keydown", handleKeyDown); }; }, [toggleFullscreen]); const handlePip = useCallback(() => { if (pipAction) pipAction(); }, [pipAction]); const hover = Gesture.Hover().onChange((_) => runOnJS(onPlayerHover)()); const handleDoubleClick = useCallback(() => { toggleFullscreen(); }, [toggleFullscreen]); const doubleTap = Gesture.Tap() .numberOfTaps(2) .onEnd(() => runOnJS(handleDoubleClick)()); const hoverAndTap = Gesture.Race(hover, doubleTap); const portalContainerID = "desktop-ui-dropdown-portal-" + playerId; return ( <> {isActivelyLive && isControlsVisible && ( )} {isSelfAndNotLive && ( )} { setShowCountdown(false); }} /> {showMetrics && ( )} {fullscreen && } ); }