diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx
index 769028788..b890bd05f 100644
--- a/web/src/components/player/LivePlayer.tsx
+++ b/web/src/components/player/LivePlayer.tsx
@@ -20,6 +20,7 @@ type LivePlayerProps = {
preferredLiveMode?: LivePlayerMode;
showStillWithoutActivity?: boolean;
windowVisible?: boolean;
+ onClick?: () => void;
};
export default function LivePlayer({
@@ -28,6 +29,7 @@ export default function LivePlayer({
preferredLiveMode,
showStillWithoutActivity = true,
windowVisible = true,
+ onClick,
}: LivePlayerProps) {
// camera activity
@@ -35,8 +37,10 @@ export default function LivePlayer({
useCameraActivity(cameraConfig);
const cameraActive = useMemo(
- () => windowVisible && (activeMotion || activeTracking),
- [activeMotion, activeTracking, windowVisible],
+ () =>
+ !showStillWithoutActivity ||
+ (windowVisible && (activeMotion || activeTracking)),
+ [activeMotion, activeTracking, showStillWithoutActivity, windowVisible],
);
// camera live state
@@ -127,11 +131,12 @@ export default function LivePlayer({
return (
diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx
index 8e3d58193..ba0811ecc 100644
--- a/web/src/pages/Live.tsx
+++ b/web/src/pages/Live.tsx
@@ -5,9 +5,12 @@ import LivePlayer from "@/components/player/LivePlayer";
import { Button } from "@/components/ui/button";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { TooltipProvider } from "@/components/ui/tooltip";
+import useOverlayState from "@/hooks/use-overlay-state";
import { usePersistence } from "@/hooks/use-persistence";
import { FrigateConfig } from "@/types/frigateConfig";
import { ReviewSegment } from "@/types/review";
+import LiveCameraView from "@/views/live/LiveCameraView";
+import LiveDashboardView from "@/views/live/LiveDashboardView";
import { useCallback, useEffect, useMemo, useState } from "react";
import { isDesktop, isMobile, isSafari } from "react-device-detect";
import { CiGrid2H, CiGrid31 } from "react-icons/ci";
@@ -15,45 +18,7 @@ import useSWR from "swr";
function Live() {
const { data: config } = useSWR
("config");
-
- // layout
-
- const [layout, setLayout] = usePersistence<"grid" | "list">(
- "live-layout",
- isDesktop ? "grid" : "list",
- );
-
- // recent events
- const { payload: eventUpdate } = useFrigateReviews();
- const { data: allEvents, mutate: updateEvents } = useSWR([
- "review",
- { limit: 10, severity: "alert" },
- ]);
-
- useEffect(() => {
- if (!eventUpdate) {
- return;
- }
-
- // if event is ended and was saved, update events list
- if (eventUpdate.type == "end" && eventUpdate.review.severity == "alert") {
- updateEvents();
- return;
- }
- }, [eventUpdate, updateEvents]);
-
- const events = useMemo(() => {
- if (!allEvents) {
- return [];
- }
-
- const date = new Date();
- date.setHours(date.getHours() - 1);
- const cutoff = date.getTime() / 1000;
- return allEvents.filter((event) => event.start_time > cutoff);
- }, [allEvents]);
-
- // camera live views
+ const [selectedCameraName, setSelectedCameraName] = useOverlayState("camera");
const cameras = useMemo(() => {
if (!config) {
@@ -65,84 +30,20 @@ function Live() {
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config]);
- const [windowVisible, setWindowVisible] = useState(true);
- const visibilityListener = useCallback(() => {
- setWindowVisible(document.visibilityState == "visible");
- }, []);
+ const selectedCamera = useMemo(
+ () => cameras.find((cam) => cam.name == selectedCameraName),
+ [cameras, selectedCameraName],
+ );
- useEffect(() => {
- addEventListener("visibilitychange", visibilityListener);
-
- return () => {
- removeEventListener("visibilitychange", visibilityListener);
- };
- }, [visibilityListener]);
+ if (selectedCamera) {
+ return ;
+ }
return (
-
- {isMobile && (
-
-
-
-
-
-
-
-
- )}
-
- {events && events.length > 0 && (
-
-
-
- {events.map((event) => {
- return
;
- })}
-
-
-
-
- )}
-
-
- {cameras.map((camera) => {
- let grow;
- const aspectRatio = camera.detect.width / camera.detect.height;
- if (aspectRatio > 2) {
- grow = `${layout == "grid" ? "col-span-2" : ""} aspect-wide`;
- } else if (aspectRatio < 1) {
- grow = `${layout == "grid" ? "row-span-2 aspect-tall md:h-full" : ""} aspect-tall`;
- } else {
- grow = "aspect-video";
- }
- return (
-
- );
- })}
-
-
+
);
}
diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx
new file mode 100644
index 000000000..e5055b93d
--- /dev/null
+++ b/web/src/views/live/LiveCameraView.tsx
@@ -0,0 +1,49 @@
+import LivePlayer from "@/components/player/LivePlayer";
+import { Button } from "@/components/ui/button";
+import { CameraConfig } from "@/types/frigateConfig";
+import { useMemo } from "react";
+import { isSafari } from "react-device-detect";
+import { IoMdArrowBack } from "react-icons/io";
+import { useNavigate } from "react-router-dom";
+
+type LiveCameraViewProps = {
+ camera: CameraConfig;
+};
+export default function LiveCameraView({ camera }: LiveCameraViewProps) {
+ const navigate = useNavigate();
+
+ const growClassName = useMemo(() => {
+ if (camera.detect.width / camera.detect.height > 2) {
+ return "absolute left-2 right-2 top-[50%] -translate-y-[50%]";
+ } else {
+ return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]";
+ }
+ }, [camera]);
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx
new file mode 100644
index 000000000..ba60fadcf
--- /dev/null
+++ b/web/src/views/live/LiveDashboardView.tsx
@@ -0,0 +1,143 @@
+import { useFrigateReviews } from "@/api/ws";
+import Logo from "@/components/Logo";
+import { AnimatedEventThumbnail } from "@/components/image/AnimatedEventThumbnail";
+import LivePlayer from "@/components/player/LivePlayer";
+import { Button } from "@/components/ui/button";
+import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { usePersistence } from "@/hooks/use-persistence";
+import { CameraConfig } from "@/types/frigateConfig";
+import { ReviewSegment } from "@/types/review";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { isDesktop, isMobile, isSafari } from "react-device-detect";
+import { CiGrid2H, CiGrid31 } from "react-icons/ci";
+import useSWR from "swr";
+
+type LiveDashboardViewProps = {
+ cameras: CameraConfig[];
+ onSelectCamera: (camera: string) => void;
+};
+export default function LiveDashboardView({
+ cameras,
+ onSelectCamera,
+}: LiveDashboardViewProps) {
+ // layout
+
+ const [layout, setLayout] = usePersistence<"grid" | "list">(
+ "live-layout",
+ isDesktop ? "grid" : "list",
+ );
+
+ // recent events
+ const { payload: eventUpdate } = useFrigateReviews();
+ const { data: allEvents, mutate: updateEvents } = useSWR([
+ "review",
+ { limit: 10, severity: "alert" },
+ ]);
+
+ useEffect(() => {
+ if (!eventUpdate) {
+ return;
+ }
+
+ // if event is ended and was saved, update events list
+ if (eventUpdate.type == "end" && eventUpdate.review.severity == "alert") {
+ updateEvents();
+ return;
+ }
+ }, [eventUpdate, updateEvents]);
+
+ const events = useMemo(() => {
+ if (!allEvents) {
+ return [];
+ }
+
+ const date = new Date();
+ date.setHours(date.getHours() - 1);
+ const cutoff = date.getTime() / 1000;
+ return allEvents.filter((event) => event.start_time > cutoff);
+ }, [allEvents]);
+
+ // camera live views
+
+ const [windowVisible, setWindowVisible] = useState(true);
+ const visibilityListener = useCallback(() => {
+ setWindowVisible(document.visibilityState == "visible");
+ }, []);
+
+ useEffect(() => {
+ addEventListener("visibilitychange", visibilityListener);
+
+ return () => {
+ removeEventListener("visibilitychange", visibilityListener);
+ };
+ }, [visibilityListener]);
+
+ return (
+
+ {isMobile && (
+
+
+
+
+
+
+
+
+ )}
+
+ {events && events.length > 0 && (
+
+
+
+ {events.map((event) => {
+ return
;
+ })}
+
+
+
+
+ )}
+
+
+ {cameras.map((camera) => {
+ let grow;
+ const aspectRatio = camera.detect.width / camera.detect.height;
+ if (aspectRatio > 2) {
+ grow = `${layout == "grid" ? "col-span-2" : ""} aspect-wide`;
+ } else if (aspectRatio < 1) {
+ grow = `${layout == "grid" ? "row-span-2 aspect-tall md:h-full" : ""} aspect-tall`;
+ } else {
+ grow = "aspect-video";
+ }
+ return (
+ onSelectCamera(camera.name)}
+ />
+ );
+ })}
+
+
+ );
+}