From 8b344cea81cd869d4899c8201be38e484e7a373e Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 9 May 2024 15:06:29 -0600 Subject: [PATCH] Implement recordings fullscreen and rework recordings layout size calculation (#11318) * Implement fullscreen button * wrap items on mobile * control based on width * refresh * Implement basic fullscreen * Fix scrolling * Add observer to detect of row overflows * Use cn to simplify classnames * dynamically respond to layout sizing * Simplify listener * Simplify layout * Handle tall browser --- web/src/components/player/HlsVideoPlayer.tsx | 9 +- web/src/components/player/VideoControls.tsx | 17 +- .../player/dynamic/DynamicVideoPlayer.tsx | 6 + web/src/hooks/resize-observer.ts | 16 +- web/src/views/events/EventView.tsx | 1 + web/src/views/events/RecordingView.tsx | 150 ++++++++++++++++-- 6 files changed, 180 insertions(+), 19 deletions(-) diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 0ea37fda5..66124631b 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -6,7 +6,7 @@ import { useState, } from "react"; import Hls from "hls.js"; -import { isAndroid, isDesktop, isMobile } from "react-device-detect"; +import { isAndroid, isDesktop, isIOS, isMobile } from "react-device-detect"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; import VideoControls from "./VideoControls"; import { VideoResolutionType } from "@/types/live"; @@ -28,24 +28,28 @@ type HlsVideoPlayerProps = { visible: boolean; currentSource: string; hotKeys: boolean; + fullscreen: boolean; onClipEnded?: () => void; onPlayerLoaded?: () => void; onTimeUpdate?: (time: number) => void; onPlaying?: () => void; setFullResolution?: React.Dispatch>; onUploadFrame?: (playTime: number) => Promise | undefined; + setFullscreen?: (full: boolean) => void; }; export default function HlsVideoPlayer({ videoRef, visible, currentSource, hotKeys, + fullscreen, onClipEnded, onPlayerLoaded, onTimeUpdate, onPlaying, setFullResolution, onUploadFrame, + setFullscreen, }: HlsVideoPlayerProps) { const { data: config } = useSWR("config"); @@ -153,6 +157,7 @@ export default function HlsVideoPlayer({ seek: true, playbackRate: true, plusUpload: config?.plus?.enabled == true, + fullscreen: !isIOS, }} setControlsOpen={setControlsOpen} setMuted={setMuted} @@ -196,6 +201,8 @@ export default function HlsVideoPlayer({ } } }} + fullscreen={fullscreen} + setFullscreen={setFullscreen} /> void; setMuted?: (muted: boolean) => void; onPlayPause: (play: boolean) => void; onSeek: (diff: number) => void; onSetPlaybackRate: (rate: number) => void; onUploadFrame?: () => void; + setFullscreen?: (full: boolean) => void; }; export default function VideoControls({ className, @@ -75,12 +80,14 @@ export default function VideoControls({ playbackRates = PLAYBACK_RATE_DEFAULT, playbackRate, hotKeys = true, + fullscreen, setControlsOpen, setMuted, onPlayPause, onSeek, onSetPlaybackRate, onUploadFrame, + setFullscreen, }: VideoControlsProps) { const onReplay = useCallback( (e: React.MouseEvent) => { @@ -163,7 +170,7 @@ export default function VideoControls({ return (
@@ -248,6 +255,14 @@ export default function VideoControls({ onUploadFrame={onUploadFrame} /> )} + {features.fullscreen && setFullscreen && ( +
setFullscreen(!fullscreen)} + > + {fullscreen ? : } +
+ )}
); } diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index e3e677530..e67468e6c 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -24,10 +24,12 @@ type DynamicVideoPlayerProps = { startTimestamp?: number; isScrubbing: boolean; hotKeys: boolean; + fullscreen: boolean; onControllerReady: (controller: DynamicVideoController) => void; onTimestampUpdate?: (timestamp: number) => void; onClipEnded?: () => void; setFullResolution: React.Dispatch>; + setFullscreen: (full: boolean) => void; }; export default function DynamicVideoPlayer({ className, @@ -37,10 +39,12 @@ export default function DynamicVideoPlayer({ startTimestamp, isScrubbing, hotKeys, + fullscreen, onControllerReady, onTimestampUpdate, onClipEnded, setFullResolution, + setFullscreen, }: DynamicVideoPlayerProps) { const apiHost = useApiHost(); const { data: config } = useSWR("config"); @@ -184,6 +188,7 @@ export default function DynamicVideoPlayer({ visible={!(isScrubbing || isLoading)} currentSource={source} hotKeys={hotKeys} + fullscreen={fullscreen} onTimeUpdate={onTimeUpdate} onPlayerLoaded={onPlayerLoaded} onClipEnded={onClipEnded} @@ -201,6 +206,7 @@ export default function DynamicVideoPlayer({ }} setFullResolution={setFullResolution} onUploadFrame={onUploadFrameToPlus} + setFullscreen={setFullscreen} /> (null); + const mainLayoutRef = useRef(null); + const cameraLayoutRef = useRef(null); + const previewRowRef = useRef(null); const previewRefs = useRef<{ [camera: string]: PreviewController }>({}); const [playbackStart, setPlaybackStart] = useState(startTime); @@ -208,7 +213,36 @@ export function RecordingView({ [currentTime], ); - // motion timeline data + // fullscreen + + const [fullscreen, setFullscreen] = useState(false); + + const onToggleFullscreen = useCallback( + (full: boolean) => { + if (full) { + mainLayoutRef.current?.requestFullscreen(); + } else { + document.exitFullscreen(); + } + }, + [mainLayoutRef], + ); + + useEffect(() => { + if (mainLayoutRef.current == null) { + return; + } + const fsListener = () => { + setFullscreen(document.fullscreenElement != null); + }; + document.addEventListener("fullscreenchange", fsListener); + + return () => { + document.removeEventListener("fullscreenchange", fsListener); + }; + }, [mainLayoutRef]); + + // layout const getCameraAspect = useCallback( (cam: string) => { @@ -259,20 +293,67 @@ export function RecordingView({ } }, [mainCameraAspect]); + const [{ width: mainWidth, height: mainHeight }] = + useResizeObserver(cameraLayoutRef); + + const mainCameraStyle = useMemo(() => { + if (isMobile || mainCameraAspect != "normal" || !config) { + return undefined; + } + + const camera = config.cameras[mainCamera]; + + if (!camera) { + return undefined; + } + + const aspect = camera.detect.width / camera.detect.height; + + if (!aspect) { + return undefined; + } + + const availableHeight = mainHeight - 112; + + let percent; + if (mainWidth / availableHeight < aspect) { + percent = 100; + } else { + const availableWidth = aspect * availableHeight; + percent = + (mainWidth < availableWidth + ? mainWidth / availableWidth + : availableWidth / mainWidth) * 100; + } + + return { + width: `${Math.round(percent)}%`, + }; + }, [config, mainCameraAspect, mainWidth, mainHeight, mainCamera]); + + const previewRowOverflows = useMemo(() => { + if (!previewRowRef.current) { + return false; + } + + return ( + previewRowRef.current.scrollWidth > previewRowRef.current.clientWidth || + previewRowRef.current.scrollHeight > previewRowRef.current.clientHeight + ); + // we only want to update when the scroll size changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [previewRowRef.current?.scrollWidth, previewRowRef.current?.scrollHeight]); + return (
-
+
{isMobile && ( )} -
+
-
+
{ setPlayerTime(timestamp); setCurrentTime(timestamp); @@ -413,12 +521,21 @@ export function RecordingView({ }} isScrubbing={scrubbing || exportMode == "timeline"} setFullResolution={setFullResolution} + setFullscreen={onToggleFullscreen} />
{isDesktop && (
+
{allCameras.map((cam) => { if (cam == mainCamera) { return; @@ -450,6 +567,7 @@ export function RecordingView({
); })} +
)}