Add birdseye live view (#10485)

* Add birdseye viewer and make it linkable

* Add on click from main dashboard
This commit is contained in:
Nicolas Mowen 2024-03-15 17:28:32 -06:00 committed by GitHub
parent 657fab2787
commit 64763293a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 205 additions and 2 deletions

View File

@ -3,15 +3,20 @@ import { BirdseyeConfig } from "@/types/frigateConfig";
import ActivityIndicator from "../indicators/activity-indicator";
import JSMpegPlayer from "./JSMpegPlayer";
import MSEPlayer from "./MsePlayer";
import { LivePlayerMode } from "@/types/live";
type LivePlayerProps = {
className?: string;
birdseyeConfig: BirdseyeConfig;
liveMode: string;
liveMode: LivePlayerMode;
onClick?: () => void;
};
export default function BirdseyeLivePlayer({
className,
birdseyeConfig,
liveMode,
onClick,
}: LivePlayerProps) {
let player;
if (liveMode == "webrtc") {
@ -45,7 +50,10 @@ export default function BirdseyeLivePlayer({
}
return (
<div className={`relative flex justify-center w-full cursor-pointer`}>
<div
className={`relative flex justify-center w-full cursor-pointer ${className ?? ""}`}
onClick={onClick}
>
<div className="absolute top-0 inset-x-0 rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none"></div>
<div className="absolute bottom-0 inset-x-0 rounded-2xl z-10 w-full h-[10%] bg-gradient-to-t from-black/20 to-transparent pointer-events-none"></div>
<div className="size-full">{player}</div>

View File

@ -3,6 +3,7 @@ import {
usePersistedOverlayState,
} from "@/hooks/use-overlay-state";
import { FrigateConfig } from "@/types/frigateConfig";
import LiveBirdseyeView from "@/views/live/LiveBirdseyeView";
import LiveCameraView from "@/views/live/LiveCameraView";
import LiveDashboardView from "@/views/live/LiveDashboardView";
import { useMemo } from "react";
@ -47,6 +48,10 @@ function Live() {
[cameras, selectedCameraName],
);
if (selectedCameraName == "birdseye") {
return <LiveBirdseyeView />;
}
if (selectedCamera) {
return <LiveCameraView camera={selectedCamera} />;
}

View File

@ -0,0 +1,189 @@
import CameraFeatureToggle from "@/components/dynamic/CameraFeatureToggle";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
import { Button } from "@/components/ui/button";
import { TooltipProvider } from "@/components/ui/tooltip";
import { useResizeObserver } from "@/hooks/resize-observer";
import { FrigateConfig } from "@/types/frigateConfig";
import { useEffect, useMemo, useRef, useState } from "react";
import {
isDesktop,
isMobile,
isSafari,
useMobileOrientation,
} from "react-device-detect";
import { FaCompress, FaExpand } from "react-icons/fa";
import { IoMdArrowBack } from "react-icons/io";
import { useNavigate } from "react-router-dom";
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import useSWR from "swr";
export default function LiveBirdseyeView() {
const { data: config } = useSWR<FrigateConfig>("config");
const navigate = useNavigate();
const { isPortrait } = useMobileOrientation();
const mainRef = useRef<HTMLDivElement | null>(null);
const [{ width: windowWidth, height: windowHeight }] =
useResizeObserver(window);
// fullscreen state
useEffect(() => {
if (mainRef.current == null) {
return;
}
const listener = () => {
setFullscreen(document.fullscreenElement != null);
};
document.addEventListener("fullscreenchange", listener);
return () => {
document.removeEventListener("fullscreenchange", listener);
};
}, [mainRef]);
// playback state
const [fullscreen, setFullscreen] = useState(false);
const cameraAspectRatio = useMemo(() => {
if (!config) {
return 16 / 9;
}
return config.birdseye.width / config.birdseye.height;
}, [config]);
const growClassName = useMemo(() => {
if (isMobile) {
if (isPortrait) {
return "absolute left-2 right-2 top-[50%] -translate-y-[50%]";
} else {
if (cameraAspectRatio > 16 / 9) {
return "absolute left-0 top-[50%] -translate-y-[50%]";
} else {
return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]";
}
}
}
if (fullscreen) {
if (cameraAspectRatio > 16 / 9) {
return "absolute inset-x-2 top-[50%] -translate-y-[50%]";
} else {
return "absolute inset-y-2 left-[50%] -translate-x-[50%]";
}
} else {
return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]";
}
}, [cameraAspectRatio, fullscreen, isPortrait]);
const preferredLiveMode = useMemo(() => {
if (!config || !config.birdseye.restream) {
return "jsmpeg";
}
if (isSafari) {
return "webrtc";
}
return "mse";
}, [config]);
const windowAspectRatio = useMemo(() => {
return windowWidth / windowHeight;
}, [windowWidth, windowHeight]);
const aspectRatio = useMemo<number>(() => {
if (isMobile || fullscreen) {
return cameraAspectRatio;
} else {
return windowAspectRatio < cameraAspectRatio
? windowAspectRatio - 0.05
: cameraAspectRatio - 0.03;
}
}, [cameraAspectRatio, windowAspectRatio, fullscreen]);
if (!config) {
return <ActivityIndicator />;
}
return (
<TransformWrapper minScale={1.0}>
<div
ref={mainRef}
className={
fullscreen
? `fixed inset-0 bg-black z-30`
: `size-full flex flex-col ${isMobile ? "landscape:flex-row" : ""}`
}
>
<div
className={
fullscreen
? `absolute right-32 top-1 z-40 ${isMobile ? "landscape:left-2 landscape:right-auto landscape:bottom-1 landscape:top-auto" : ""}`
: `w-full h-12 flex flex-row items-center justify-between ${isMobile ? "landscape:w-min landscape:h-full landscape:flex-col" : ""}`
}
>
{!fullscreen ? (
<Button
className={`rounded-lg ${isMobile ? "ml-2" : "ml-0"}`}
size={isMobile ? "icon" : "default"}
onClick={() => navigate(-1)}
>
<IoMdArrowBack className="size-5 lg:mr-[10px]" />
{isDesktop && "Back"}
</Button>
) : (
<div />
)}
<TooltipProvider>
<div
className={`flex flex-row items-center gap-2 mr-1 *:rounded-lg ${isMobile ? "landscape:flex-col" : ""}`}
>
<CameraFeatureToggle
className="p-2 md:p-0"
variant={fullscreen ? "overlay" : "primary"}
Icon={fullscreen ? FaCompress : FaExpand}
isActive={fullscreen}
title={fullscreen ? "Close" : "Fullscreen"}
onClick={() => {
if (fullscreen) {
document.exitFullscreen();
} else {
mainRef.current?.requestFullscreen();
}
}}
/>
</div>
</TooltipProvider>
</div>
<TransformComponent
wrapperStyle={{
width: "100%",
height: "100%",
}}
contentStyle={{
position: "relative",
width: "100%",
height: "100%",
}}
>
<div
className={growClassName}
style={{
aspectRatio: aspectRatio,
}}
>
<BirdseyeLivePlayer
className="h-full"
birdseyeConfig={config.birdseye}
liveMode={preferredLiveMode}
/>
</div>
</TransformComponent>
</div>
</TransformWrapper>
);
}

View File

@ -134,6 +134,7 @@ export default function LiveDashboardView({
<BirdseyeLivePlayer
birdseyeConfig={birdseyeConfig}
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
onClick={() => onSelectCamera("birdseye")}
/>
)}
{cameras.map((camera) => {