2024-05-31 15:58:33 +03:00
|
|
|
import { useFullscreen } from "@/hooks/use-fullscreen";
|
2024-10-01 00:55:44 +03:00
|
|
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
2024-03-15 21:46:17 +03:00
|
|
|
import {
|
|
|
|
|
useHashState,
|
2024-03-14 17:27:27 +03:00
|
|
|
usePersistedOverlayState,
|
2024-06-19 15:09:49 +03:00
|
|
|
useSearchEffect,
|
2024-03-14 17:27:27 +03:00
|
|
|
} from "@/hooks/use-overlay-state";
|
2023-12-16 02:24:50 +03:00
|
|
|
import { FrigateConfig } from "@/types/frigateConfig";
|
2024-03-16 02:28:32 +03:00
|
|
|
import LiveBirdseyeView from "@/views/live/LiveBirdseyeView";
|
2024-03-02 03:43:02 +03:00
|
|
|
import LiveCameraView from "@/views/live/LiveCameraView";
|
|
|
|
|
import LiveDashboardView from "@/views/live/LiveDashboardView";
|
2025-03-16 18:36:20 +03:00
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
|
2024-05-31 15:58:33 +03:00
|
|
|
import { useEffect, useMemo, useRef } from "react";
|
2023-12-16 02:24:50 +03:00
|
|
|
import useSWR from "swr";
|
2025-09-12 14:19:29 +03:00
|
|
|
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
|
|
|
|
import { useIsCustomRole } from "@/hooks/use-is-custom-role";
|
2023-12-08 16:33:22 +03:00
|
|
|
|
|
|
|
|
function Live() {
|
2025-03-16 18:36:20 +03:00
|
|
|
const { t } = useTranslation(["views/live"]);
|
2023-12-16 02:24:50 +03:00
|
|
|
const { data: config } = useSWR<FrigateConfig>("config");
|
2025-09-12 14:19:29 +03:00
|
|
|
const isCustomRole = useIsCustomRole();
|
2024-03-05 02:18:30 +03:00
|
|
|
|
2024-04-12 15:31:30 +03:00
|
|
|
// selection
|
|
|
|
|
|
2024-03-15 21:46:17 +03:00
|
|
|
const [selectedCameraName, setSelectedCameraName] = useHashState();
|
2025-10-01 01:53:48 +03:00
|
|
|
const [cameraGroup, setCameraGroup, loaded, ,] = usePersistedOverlayState(
|
2024-03-15 17:59:41 +03:00
|
|
|
"cameraGroup",
|
|
|
|
|
"default" as string,
|
|
|
|
|
);
|
2023-12-16 02:24:50 +03:00
|
|
|
|
2024-06-19 15:09:49 +03:00
|
|
|
useSearchEffect("group", (cameraGroup) => {
|
2025-10-01 01:53:48 +03:00
|
|
|
if (config && cameraGroup && loaded) {
|
2024-06-19 15:09:49 +03:00
|
|
|
const group = config.camera_groups[cameraGroup];
|
|
|
|
|
|
|
|
|
|
if (group) {
|
|
|
|
|
setCameraGroup(cameraGroup);
|
2025-10-01 01:53:48 +03:00
|
|
|
// return false so that url cleanup doesn't occur here.
|
|
|
|
|
// will be cleaned up by usePersistedOverlayState in the
|
|
|
|
|
// camera group selector so that the icon switches correctly
|
|
|
|
|
return false;
|
2024-06-19 15:09:49 +03:00
|
|
|
}
|
2024-09-12 17:46:29 +03:00
|
|
|
|
|
|
|
|
return true;
|
2024-06-19 15:09:49 +03:00
|
|
|
}
|
2024-09-12 17:46:29 +03:00
|
|
|
|
|
|
|
|
return false;
|
2024-06-19 15:09:49 +03:00
|
|
|
});
|
|
|
|
|
|
2024-05-31 15:58:33 +03:00
|
|
|
// fullscreen
|
|
|
|
|
|
|
|
|
|
const mainRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
|
2024-08-20 00:01:21 +03:00
|
|
|
const { fullscreen, toggleFullscreen, supportsFullScreen } =
|
|
|
|
|
useFullscreen(mainRef);
|
2024-05-31 15:58:33 +03:00
|
|
|
|
2024-10-01 00:55:44 +03:00
|
|
|
useKeyboardListener(["f"], (key, modifiers) => {
|
|
|
|
|
if (!modifiers.down) {
|
2025-10-02 16:21:37 +03:00
|
|
|
return true;
|
2024-10-01 00:55:44 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (key) {
|
|
|
|
|
case "f":
|
|
|
|
|
toggleFullscreen();
|
2025-10-02 16:21:37 +03:00
|
|
|
return true;
|
2024-10-01 00:55:44 +03:00
|
|
|
}
|
2025-10-02 16:21:37 +03:00
|
|
|
|
|
|
|
|
return false;
|
2024-10-01 00:55:44 +03:00
|
|
|
});
|
|
|
|
|
|
2024-04-12 15:31:30 +03:00
|
|
|
// document title
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (selectedCameraName) {
|
|
|
|
|
const capitalized = selectedCameraName
|
|
|
|
|
.split("_")
|
2024-11-30 05:44:42 +03:00
|
|
|
.filter((text) => text)
|
2024-04-12 15:31:30 +03:00
|
|
|
.map((text) => text[0].toUpperCase() + text.substring(1));
|
2025-03-16 18:36:20 +03:00
|
|
|
document.title = t("documentTitle.withCamera", {
|
|
|
|
|
camera: capitalized.join(" "),
|
|
|
|
|
});
|
2024-04-12 15:31:30 +03:00
|
|
|
} else if (cameraGroup && cameraGroup != "default") {
|
2025-03-16 18:36:20 +03:00
|
|
|
document.title = t("documentTitle.withCamera", {
|
|
|
|
|
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
|
|
|
|
|
});
|
2024-04-12 15:31:30 +03:00
|
|
|
} else {
|
2025-03-16 18:36:20 +03:00
|
|
|
document.title = t("documentTitle", { ns: "views/live" });
|
2024-04-12 15:31:30 +03:00
|
|
|
}
|
2025-03-16 18:36:20 +03:00
|
|
|
}, [cameraGroup, selectedCameraName, t]);
|
2024-04-12 15:31:30 +03:00
|
|
|
|
|
|
|
|
// settings
|
|
|
|
|
|
2025-09-12 14:19:29 +03:00
|
|
|
const allowedCameras = useAllowedCameras();
|
|
|
|
|
|
2024-03-12 22:53:01 +03:00
|
|
|
const includesBirdseye = useMemo(() => {
|
2024-04-26 02:19:31 +03:00
|
|
|
if (
|
|
|
|
|
config &&
|
|
|
|
|
Object.keys(config.camera_groups).length &&
|
|
|
|
|
cameraGroup &&
|
|
|
|
|
config.camera_groups[cameraGroup] &&
|
2025-09-12 14:19:29 +03:00
|
|
|
cameraGroup != "default" &&
|
|
|
|
|
(!isCustomRole || "birdseye" in allowedCameras)
|
2024-04-26 02:19:31 +03:00
|
|
|
) {
|
2024-03-12 22:53:01 +03:00
|
|
|
return config.camera_groups[cameraGroup].cameras.includes("birdseye");
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-09-12 14:19:29 +03:00
|
|
|
}, [config, cameraGroup, allowedCameras, isCustomRole]);
|
2024-03-12 22:53:01 +03:00
|
|
|
|
2024-02-10 15:30:53 +03:00
|
|
|
const cameras = useMemo(() => {
|
|
|
|
|
if (!config) {
|
|
|
|
|
return [];
|
2023-12-16 02:24:50 +03:00
|
|
|
}
|
|
|
|
|
|
2024-04-26 02:19:31 +03:00
|
|
|
if (
|
|
|
|
|
Object.keys(config.camera_groups).length &&
|
|
|
|
|
cameraGroup &&
|
|
|
|
|
config.camera_groups[cameraGroup] &&
|
|
|
|
|
cameraGroup != "default"
|
|
|
|
|
) {
|
2024-03-05 02:18:30 +03:00
|
|
|
const group = config.camera_groups[cameraGroup];
|
|
|
|
|
return Object.values(config.cameras)
|
2025-03-03 18:30:52 +03:00
|
|
|
.filter(
|
|
|
|
|
(conf) => conf.enabled_in_config && group.cameras.includes(conf.name),
|
|
|
|
|
)
|
2025-09-12 14:19:29 +03:00
|
|
|
.filter((cam) => allowedCameras.includes(cam.name))
|
2024-03-05 02:18:30 +03:00
|
|
|
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-10 15:30:53 +03:00
|
|
|
return Object.values(config.cameras)
|
2025-03-03 18:30:52 +03:00
|
|
|
.filter((conf) => conf.ui.dashboard && conf.enabled_in_config)
|
2025-09-12 14:19:29 +03:00
|
|
|
.filter((cam) => allowedCameras.includes(cam.name))
|
2024-02-10 15:30:53 +03:00
|
|
|
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
2025-09-12 14:19:29 +03:00
|
|
|
}, [config, cameraGroup, allowedCameras]);
|
2023-12-16 02:24:50 +03:00
|
|
|
|
2024-03-02 03:43:02 +03:00
|
|
|
const selectedCamera = useMemo(
|
|
|
|
|
() => cameras.find((cam) => cam.name == selectedCameraName),
|
|
|
|
|
[cameras, selectedCameraName],
|
|
|
|
|
);
|
2024-02-13 04:28:36 +03:00
|
|
|
|
2023-12-08 16:33:22 +03:00
|
|
|
return (
|
2024-05-31 15:58:33 +03:00
|
|
|
<div className="size-full" ref={mainRef}>
|
|
|
|
|
{selectedCameraName === "birdseye" ? (
|
|
|
|
|
<LiveBirdseyeView
|
2024-08-20 00:01:21 +03:00
|
|
|
supportsFullscreen={supportsFullScreen}
|
2024-05-31 15:58:33 +03:00
|
|
|
fullscreen={fullscreen}
|
|
|
|
|
toggleFullscreen={toggleFullscreen}
|
|
|
|
|
/>
|
|
|
|
|
) : selectedCamera ? (
|
|
|
|
|
<LiveCameraView
|
2025-09-30 23:51:47 +03:00
|
|
|
key={selectedCameraName}
|
2024-05-31 15:58:33 +03:00
|
|
|
config={config}
|
|
|
|
|
camera={selectedCamera}
|
2024-08-20 00:01:21 +03:00
|
|
|
supportsFullscreen={supportsFullScreen}
|
2024-05-31 15:58:33 +03:00
|
|
|
fullscreen={fullscreen}
|
|
|
|
|
toggleFullscreen={toggleFullscreen}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<LiveDashboardView
|
|
|
|
|
cameras={cameras}
|
2024-07-15 18:34:41 +03:00
|
|
|
cameraGroup={cameraGroup ?? "default"}
|
2024-05-31 15:58:33 +03:00
|
|
|
includeBirdseye={includesBirdseye}
|
|
|
|
|
onSelectCamera={setSelectedCameraName}
|
|
|
|
|
fullscreen={fullscreen}
|
|
|
|
|
toggleFullscreen={toggleFullscreen}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2023-12-08 16:33:22 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Live;
|