mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
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
This commit is contained in:
parent
021ffb2437
commit
8b344cea81
@ -6,7 +6,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import Hls from "hls.js";
|
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 { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||||
import VideoControls from "./VideoControls";
|
import VideoControls from "./VideoControls";
|
||||||
import { VideoResolutionType } from "@/types/live";
|
import { VideoResolutionType } from "@/types/live";
|
||||||
@ -28,24 +28,28 @@ type HlsVideoPlayerProps = {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
currentSource: string;
|
currentSource: string;
|
||||||
hotKeys: boolean;
|
hotKeys: boolean;
|
||||||
|
fullscreen: boolean;
|
||||||
onClipEnded?: () => void;
|
onClipEnded?: () => void;
|
||||||
onPlayerLoaded?: () => void;
|
onPlayerLoaded?: () => void;
|
||||||
onTimeUpdate?: (time: number) => void;
|
onTimeUpdate?: (time: number) => void;
|
||||||
onPlaying?: () => void;
|
onPlaying?: () => void;
|
||||||
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||||
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
|
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
|
||||||
|
setFullscreen?: (full: boolean) => void;
|
||||||
};
|
};
|
||||||
export default function HlsVideoPlayer({
|
export default function HlsVideoPlayer({
|
||||||
videoRef,
|
videoRef,
|
||||||
visible,
|
visible,
|
||||||
currentSource,
|
currentSource,
|
||||||
hotKeys,
|
hotKeys,
|
||||||
|
fullscreen,
|
||||||
onClipEnded,
|
onClipEnded,
|
||||||
onPlayerLoaded,
|
onPlayerLoaded,
|
||||||
onTimeUpdate,
|
onTimeUpdate,
|
||||||
onPlaying,
|
onPlaying,
|
||||||
setFullResolution,
|
setFullResolution,
|
||||||
onUploadFrame,
|
onUploadFrame,
|
||||||
|
setFullscreen,
|
||||||
}: HlsVideoPlayerProps) {
|
}: HlsVideoPlayerProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -153,6 +157,7 @@ export default function HlsVideoPlayer({
|
|||||||
seek: true,
|
seek: true,
|
||||||
playbackRate: true,
|
playbackRate: true,
|
||||||
plusUpload: config?.plus?.enabled == true,
|
plusUpload: config?.plus?.enabled == true,
|
||||||
|
fullscreen: !isIOS,
|
||||||
}}
|
}}
|
||||||
setControlsOpen={setControlsOpen}
|
setControlsOpen={setControlsOpen}
|
||||||
setMuted={setMuted}
|
setMuted={setMuted}
|
||||||
@ -196,6 +201,8 @@ export default function HlsVideoPlayer({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
fullscreen={fullscreen}
|
||||||
|
setFullscreen={setFullscreen}
|
||||||
/>
|
/>
|
||||||
<TransformComponent
|
<TransformComponent
|
||||||
wrapperStyle={{
|
wrapperStyle={{
|
||||||
|
@ -30,12 +30,14 @@ import {
|
|||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "../ui/alert-dialog";
|
} from "../ui/alert-dialog";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { FaCompress, FaExpand } from "react-icons/fa";
|
||||||
|
|
||||||
type VideoControls = {
|
type VideoControls = {
|
||||||
volume?: boolean;
|
volume?: boolean;
|
||||||
seek?: boolean;
|
seek?: boolean;
|
||||||
playbackRate?: boolean;
|
playbackRate?: boolean;
|
||||||
plusUpload?: boolean;
|
plusUpload?: boolean;
|
||||||
|
fullscreen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONTROLS_DEFAULT: VideoControls = {
|
const CONTROLS_DEFAULT: VideoControls = {
|
||||||
@ -43,6 +45,7 @@ const CONTROLS_DEFAULT: VideoControls = {
|
|||||||
seek: true,
|
seek: true,
|
||||||
playbackRate: true,
|
playbackRate: true,
|
||||||
plusUpload: false,
|
plusUpload: false,
|
||||||
|
fullscreen: false,
|
||||||
};
|
};
|
||||||
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
|
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
|
||||||
|
|
||||||
@ -57,12 +60,14 @@ type VideoControlsProps = {
|
|||||||
playbackRates?: number[];
|
playbackRates?: number[];
|
||||||
playbackRate: number;
|
playbackRate: number;
|
||||||
hotKeys?: boolean;
|
hotKeys?: boolean;
|
||||||
|
fullscreen?: boolean;
|
||||||
setControlsOpen?: (open: boolean) => void;
|
setControlsOpen?: (open: boolean) => void;
|
||||||
setMuted?: (muted: boolean) => void;
|
setMuted?: (muted: boolean) => void;
|
||||||
onPlayPause: (play: boolean) => void;
|
onPlayPause: (play: boolean) => void;
|
||||||
onSeek: (diff: number) => void;
|
onSeek: (diff: number) => void;
|
||||||
onSetPlaybackRate: (rate: number) => void;
|
onSetPlaybackRate: (rate: number) => void;
|
||||||
onUploadFrame?: () => void;
|
onUploadFrame?: () => void;
|
||||||
|
setFullscreen?: (full: boolean) => void;
|
||||||
};
|
};
|
||||||
export default function VideoControls({
|
export default function VideoControls({
|
||||||
className,
|
className,
|
||||||
@ -75,12 +80,14 @@ export default function VideoControls({
|
|||||||
playbackRates = PLAYBACK_RATE_DEFAULT,
|
playbackRates = PLAYBACK_RATE_DEFAULT,
|
||||||
playbackRate,
|
playbackRate,
|
||||||
hotKeys = true,
|
hotKeys = true,
|
||||||
|
fullscreen,
|
||||||
setControlsOpen,
|
setControlsOpen,
|
||||||
setMuted,
|
setMuted,
|
||||||
onPlayPause,
|
onPlayPause,
|
||||||
onSeek,
|
onSeek,
|
||||||
onSetPlaybackRate,
|
onSetPlaybackRate,
|
||||||
onUploadFrame,
|
onUploadFrame,
|
||||||
|
setFullscreen,
|
||||||
}: VideoControlsProps) {
|
}: VideoControlsProps) {
|
||||||
const onReplay = useCallback(
|
const onReplay = useCallback(
|
||||||
(e: React.MouseEvent<SVGElement>) => {
|
(e: React.MouseEvent<SVGElement>) => {
|
||||||
@ -163,7 +170,7 @@ export default function VideoControls({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-4 py-2 flex justify-between items-center gap-8 text-primary z-50 bg-background/60 rounded-lg",
|
"w-[96%] sm:w-auto px-4 py-2 flex flex-wrap sm:flex-nowrap justify-between items-center gap-4 sm:gap-8 text-primary z-50 bg-background/60 rounded-lg",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -248,6 +255,14 @@ export default function VideoControls({
|
|||||||
onUploadFrame={onUploadFrame}
|
onUploadFrame={onUploadFrame}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{features.fullscreen && setFullscreen && (
|
||||||
|
<div
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => setFullscreen(!fullscreen)}
|
||||||
|
>
|
||||||
|
{fullscreen ? <FaCompress /> : <FaExpand />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,12 @@ type DynamicVideoPlayerProps = {
|
|||||||
startTimestamp?: number;
|
startTimestamp?: number;
|
||||||
isScrubbing: boolean;
|
isScrubbing: boolean;
|
||||||
hotKeys: boolean;
|
hotKeys: boolean;
|
||||||
|
fullscreen: boolean;
|
||||||
onControllerReady: (controller: DynamicVideoController) => void;
|
onControllerReady: (controller: DynamicVideoController) => void;
|
||||||
onTimestampUpdate?: (timestamp: number) => void;
|
onTimestampUpdate?: (timestamp: number) => void;
|
||||||
onClipEnded?: () => void;
|
onClipEnded?: () => void;
|
||||||
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||||
|
setFullscreen: (full: boolean) => void;
|
||||||
};
|
};
|
||||||
export default function DynamicVideoPlayer({
|
export default function DynamicVideoPlayer({
|
||||||
className,
|
className,
|
||||||
@ -37,10 +39,12 @@ export default function DynamicVideoPlayer({
|
|||||||
startTimestamp,
|
startTimestamp,
|
||||||
isScrubbing,
|
isScrubbing,
|
||||||
hotKeys,
|
hotKeys,
|
||||||
|
fullscreen,
|
||||||
onControllerReady,
|
onControllerReady,
|
||||||
onTimestampUpdate,
|
onTimestampUpdate,
|
||||||
onClipEnded,
|
onClipEnded,
|
||||||
setFullResolution,
|
setFullResolution,
|
||||||
|
setFullscreen,
|
||||||
}: DynamicVideoPlayerProps) {
|
}: DynamicVideoPlayerProps) {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
@ -184,6 +188,7 @@ export default function DynamicVideoPlayer({
|
|||||||
visible={!(isScrubbing || isLoading)}
|
visible={!(isScrubbing || isLoading)}
|
||||||
currentSource={source}
|
currentSource={source}
|
||||||
hotKeys={hotKeys}
|
hotKeys={hotKeys}
|
||||||
|
fullscreen={fullscreen}
|
||||||
onTimeUpdate={onTimeUpdate}
|
onTimeUpdate={onTimeUpdate}
|
||||||
onPlayerLoaded={onPlayerLoaded}
|
onPlayerLoaded={onPlayerLoaded}
|
||||||
onClipEnded={onClipEnded}
|
onClipEnded={onClipEnded}
|
||||||
@ -201,6 +206,7 @@ export default function DynamicVideoPlayer({
|
|||||||
}}
|
}}
|
||||||
setFullResolution={setFullResolution}
|
setFullResolution={setFullResolution}
|
||||||
onUploadFrame={onUploadFrameToPlus}
|
onUploadFrame={onUploadFrameToPlus}
|
||||||
|
setFullscreen={setFullscreen}
|
||||||
/>
|
/>
|
||||||
<PreviewPlayer
|
<PreviewPlayer
|
||||||
className={cn(
|
className={cn(
|
||||||
|
@ -43,5 +43,19 @@ export function useResizeObserver(...refs: RefType[]) {
|
|||||||
};
|
};
|
||||||
}, [refs, resizeObserver]);
|
}, [refs, resizeObserver]);
|
||||||
|
|
||||||
return dimensions;
|
if (dimensions.length == refs.length) {
|
||||||
|
return dimensions;
|
||||||
|
} else {
|
||||||
|
const items = [...dimensions];
|
||||||
|
for (let i = dimensions.length; i < refs.length; i++) {
|
||||||
|
items.push({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
x: -Infinity,
|
||||||
|
y: -Infinity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -997,6 +997,7 @@ function MotionReview({
|
|||||||
volume: false,
|
volume: false,
|
||||||
seek: true,
|
seek: true,
|
||||||
playbackRate: true,
|
playbackRate: true,
|
||||||
|
fullscreen: false,
|
||||||
}}
|
}}
|
||||||
isPlaying={playing}
|
isPlaying={playing}
|
||||||
show={!scrubbing || controlsOpen}
|
show={!scrubbing || controlsOpen}
|
||||||
|
@ -43,6 +43,8 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { FaVideo } from "react-icons/fa";
|
import { FaVideo } from "react-icons/fa";
|
||||||
import { VideoResolutionType } from "@/types/live";
|
import { VideoResolutionType } from "@/types/live";
|
||||||
import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
||||||
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const SEGMENT_DURATION = 30;
|
const SEGMENT_DURATION = 30;
|
||||||
|
|
||||||
@ -76,6 +78,9 @@ export function RecordingView({
|
|||||||
|
|
||||||
const [mainCamera, setMainCamera] = useState(startCamera);
|
const [mainCamera, setMainCamera] = useState(startCamera);
|
||||||
const mainControllerRef = useRef<DynamicVideoController | null>(null);
|
const mainControllerRef = useRef<DynamicVideoController | null>(null);
|
||||||
|
const mainLayoutRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const cameraLayoutRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const previewRowRef = useRef<HTMLDivElement | null>(null);
|
||||||
const previewRefs = useRef<{ [camera: string]: PreviewController }>({});
|
const previewRefs = useRef<{ [camera: string]: PreviewController }>({});
|
||||||
|
|
||||||
const [playbackStart, setPlaybackStart] = useState(startTime);
|
const [playbackStart, setPlaybackStart] = useState(startTime);
|
||||||
@ -208,7 +213,36 @@ export function RecordingView({
|
|||||||
[currentTime],
|
[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(
|
const getCameraAspect = useCallback(
|
||||||
(cam: string) => {
|
(cam: string) => {
|
||||||
@ -259,20 +293,67 @@ export function RecordingView({
|
|||||||
}
|
}
|
||||||
}, [mainCameraAspect]);
|
}, [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 (
|
return (
|
||||||
<div ref={contentRef} className="size-full pt-2 flex flex-col">
|
<div ref={contentRef} className="size-full pt-2 flex flex-col">
|
||||||
<Toaster closeButton={true} />
|
<Toaster closeButton={true} />
|
||||||
<div
|
<div className="w-full h-11 mb-2 px-2 relative flex items-center justify-between">
|
||||||
className={`w-full h-11 mb-2 px-2 relative flex items-center justify-between`}
|
|
||||||
>
|
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" />
|
<Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" />
|
||||||
)}
|
)}
|
||||||
<div
|
<div className={cn("flex items-center gap-2")}>
|
||||||
className={`flex items-center gap-2 ${isMobile ? "landscape:flex-col" : ""}`}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
className={`flex items-center gap-2.5 rounded-lg`}
|
className="flex items-center gap-2.5 rounded-lg"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
@ -372,20 +453,46 @@ export function RecordingView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`h-full flex justify-center overflow-hidden ${isDesktop ? "" : "flex-col landscape:flex-row gap-2"}`}
|
ref={mainLayoutRef}
|
||||||
|
className={cn(
|
||||||
|
"h-full flex justify-center overflow-hidden",
|
||||||
|
isDesktop ? "" : "flex-col landscape:flex-row gap-2",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className={`${isDesktop ? "w-[80%]" : ""} flex flex-1 flex-wrap`}>
|
<div
|
||||||
|
ref={cameraLayoutRef}
|
||||||
|
className={cn("flex flex-1 flex-wrap", isDesktop ? "w-[80%]" : "")}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`size-full flex items-center ${mainCameraAspect == "tall" ? "flex-row justify-evenly" : "flex-col justify-center gap-2"}`}
|
className={cn(
|
||||||
|
"size-full flex items-center",
|
||||||
|
mainCameraAspect == "tall"
|
||||||
|
? "flex-row justify-evenly"
|
||||||
|
: "flex-col justify-center gap-2",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
key={mainCamera}
|
key={mainCamera}
|
||||||
className={`relative ${
|
className={cn(
|
||||||
|
"relative",
|
||||||
isDesktop
|
isDesktop
|
||||||
? `${mainCameraAspect == "tall" ? "h-[50%] md:h-[60%] lg:h-[75%] xl:h-[90%]" : mainCameraAspect == "wide" ? "w-full" : "w-[78%]"} px-4 flex justify-center`
|
? cn(
|
||||||
: `portrait:w-full pt-2 ${mainCameraAspect == "wide" ? "landscape:w-full aspect-wide" : "landscape:h-[94%] aspect-video"}`
|
"px-4 flex justify-center",
|
||||||
}`}
|
mainCameraAspect == "tall"
|
||||||
|
? "h-[50%] md:h-[60%] lg:h-[75%] xl:h-[90%]"
|
||||||
|
: mainCameraAspect == "wide"
|
||||||
|
? "w-full"
|
||||||
|
: "",
|
||||||
|
)
|
||||||
|
: cn(
|
||||||
|
"portrait:w-full pt-2",
|
||||||
|
mainCameraAspect == "wide"
|
||||||
|
? "landscape:w-full aspect-wide"
|
||||||
|
: "landscape:h-[94%] aspect-video",
|
||||||
|
),
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
width: mainCameraStyle ? mainCameraStyle.width : undefined,
|
||||||
aspectRatio: isDesktop
|
aspectRatio: isDesktop
|
||||||
? mainCameraAspect == "tall"
|
? mainCameraAspect == "tall"
|
||||||
? getCameraAspect(mainCamera)
|
? getCameraAspect(mainCamera)
|
||||||
@ -400,6 +507,7 @@ export function RecordingView({
|
|||||||
cameraPreviews={allPreviews ?? []}
|
cameraPreviews={allPreviews ?? []}
|
||||||
startTimestamp={playbackStart}
|
startTimestamp={playbackStart}
|
||||||
hotKeys={exportMode != "select"}
|
hotKeys={exportMode != "select"}
|
||||||
|
fullscreen={fullscreen}
|
||||||
onTimestampUpdate={(timestamp) => {
|
onTimestampUpdate={(timestamp) => {
|
||||||
setPlayerTime(timestamp);
|
setPlayerTime(timestamp);
|
||||||
setCurrentTime(timestamp);
|
setCurrentTime(timestamp);
|
||||||
@ -413,12 +521,21 @@ export function RecordingView({
|
|||||||
}}
|
}}
|
||||||
isScrubbing={scrubbing || exportMode == "timeline"}
|
isScrubbing={scrubbing || exportMode == "timeline"}
|
||||||
setFullResolution={setFullResolution}
|
setFullResolution={setFullResolution}
|
||||||
|
setFullscreen={onToggleFullscreen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<div
|
<div
|
||||||
className={`flex gap-2 ${mainCameraAspect == "tall" ? "h-full w-[12%] flex-col justify-center overflow-y-auto" : "w-full h-[14%] justify-center items-center overflow-x-auto"} `}
|
ref={previewRowRef}
|
||||||
|
className={cn(
|
||||||
|
"flex gap-2 overflow-auto",
|
||||||
|
mainCameraAspect == "tall"
|
||||||
|
? "h-full w-48 flex-col"
|
||||||
|
: `w-full h-28`,
|
||||||
|
previewRowOverflows ? "" : "justify-center items-center",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
|
<div className="w-2" />
|
||||||
{allCameras.map((cam) => {
|
{allCameras.map((cam) => {
|
||||||
if (cam == mainCamera) {
|
if (cam == mainCamera) {
|
||||||
return;
|
return;
|
||||||
@ -450,6 +567,7 @@ export function RecordingView({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<div className="w-2" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user