2024-05-31 07:58:33 -05:00
|
|
|
import { useFullscreen } from "@/hooks/use-fullscreen";
|
2024-09-30 15:55:44 -06:00
|
|
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
2025-12-01 07:59:54 -06:00
|
|
|
import { useHashState, useSearchEffect } from "@/hooks/use-overlay-state";
|
|
|
|
|
import { useUserPersistedOverlayState } from "@/hooks/use-overlay-state";
|
2023-12-15 16:24:50 -07:00
|
|
|
import { FrigateConfig } from "@/types/frigateConfig";
|
2024-03-15 17:28:32 -06:00
|
|
|
import LiveBirdseyeView from "@/views/live/LiveBirdseyeView";
|
2024-03-01 17:43:02 -07:00
|
|
|
import LiveCameraView from "@/views/live/LiveCameraView";
|
|
|
|
|
import LiveDashboardView from "@/views/live/LiveDashboardView";
|
2025-03-16 23:36:20 +08:00
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
|
2024-05-31 07:58:33 -05:00
|
|
|
import { useEffect, useMemo, useRef } from "react";
|
2023-12-15 16:24:50 -07:00
|
|
|
import useSWR from "swr";
|
2025-09-12 06:19:29 -05:00
|
|
|
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
2026-02-27 21:02:46 -06:00
|
|
|
import { useHasFullCameraAccess } from "@/hooks/use-has-full-camera-access";
|
2023-12-08 07:33:22 -06:00
|
|
|
|
|
|
|
|
function Live() {
|
2025-03-16 23:36:20 +08:00
|
|
|
const { t } = useTranslation(["views/live"]);
|
2023-12-15 16:24:50 -07:00
|
|
|
const { data: config } = useSWR<FrigateConfig>("config");
|
2026-02-27 21:02:46 -06:00
|
|
|
const hasFullCameraAccess = useHasFullCameraAccess();
|
2024-03-04 16:18:30 -07:00
|
|
|
|
2024-04-12 06:31:30 -06:00
|
|
|
// selection
|
|
|
|
|
|
2024-03-15 12:46:17 -06:00
|
|
|
const [selectedCameraName, setSelectedCameraName] = useHashState();
|
2025-12-01 07:59:54 -06:00
|
|
|
const [cameraGroup, setCameraGroup, loaded] = useUserPersistedOverlayState(
|
2024-03-15 09:59:41 -05:00
|
|
|
"cameraGroup",
|
|
|
|
|
"default" as string,
|
|
|
|
|
);
|
2024-02-10 05:30:53 -07:00
|
|
|
|
2024-06-19 06:09:49 -06:00
|
|
|
useSearchEffect("group", (cameraGroup) => {
|
2025-09-30 17:53:48 -05:00
|
|
|
if (config && cameraGroup && loaded) {
|
2024-06-19 06:09:49 -06:00
|
|
|
const group = config.camera_groups[cameraGroup];
|
|
|
|
|
|
|
|
|
|
if (group) {
|
|
|
|
|
setCameraGroup(cameraGroup);
|
2025-09-30 17:53:48 -05: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 06:09:49 -06:00
|
|
|
}
|
2024-09-12 08:46:29 -06:00
|
|
|
|
|
|
|
|
return true;
|
2024-06-19 06:09:49 -06:00
|
|
|
}
|
2024-09-12 08:46:29 -06:00
|
|
|
|
|
|
|
|
return false;
|
2024-06-19 06:09:49 -06:00
|
|
|
});
|
|
|
|
|
|
2024-05-31 07:58:33 -05:00
|
|
|
// fullscreen
|
|
|
|
|
|
|
|
|
|
const mainRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
|
2024-08-19 15:01:21 -06:00
|
|
|
const { fullscreen, toggleFullscreen, supportsFullScreen } =
|
|
|
|
|
useFullscreen(mainRef);
|
2024-05-31 07:58:33 -05:00
|
|
|
|
2024-09-30 15:55:44 -06:00
|
|
|
useKeyboardListener(["f"], (key, modifiers) => {
|
|
|
|
|
if (!modifiers.down) {
|
2025-10-02 07:21:37 -06:00
|
|
|
return true;
|
2024-09-30 15:55:44 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (key) {
|
|
|
|
|
case "f":
|
|
|
|
|
toggleFullscreen();
|
2025-10-02 07:21:37 -06:00
|
|
|
return true;
|
2024-09-30 15:55:44 -06:00
|
|
|
}
|
2025-10-02 07:21:37 -06:00
|
|
|
|
|
|
|
|
return false;
|
2024-09-30 15:55:44 -06:00
|
|
|
});
|
|
|
|
|
|
2024-04-12 06:31:30 -06:00
|
|
|
// document title
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (selectedCameraName) {
|
|
|
|
|
const capitalized = selectedCameraName
|
|
|
|
|
.split("_")
|
2024-11-29 20:44:42 -06:00
|
|
|
.filter((text) => text)
|
2024-04-12 06:31:30 -06:00
|
|
|
.map((text) => text[0].toUpperCase() + text.substring(1));
|
2025-03-16 23:36:20 +08:00
|
|
|
document.title = t("documentTitle.withCamera", {
|
|
|
|
|
camera: capitalized.join(" "),
|
|
|
|
|
});
|
2024-04-12 06:31:30 -06:00
|
|
|
} else if (cameraGroup && cameraGroup != "default") {
|
2025-03-16 23:36:20 +08:00
|
|
|
document.title = t("documentTitle.withCamera", {
|
|
|
|
|
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
|
|
|
|
|
});
|
2024-04-12 06:31:30 -06:00
|
|
|
} else {
|
2026-03-23 08:48:02 -05:00
|
|
|
document.title = t("documentTitle.default", { ns: "views/live" });
|
2024-04-12 06:31:30 -06:00
|
|
|
}
|
2025-03-16 23:36:20 +08:00
|
|
|
}, [cameraGroup, selectedCameraName, t]);
|
2024-04-12 06:31:30 -06:00
|
|
|
|
|
|
|
|
// settings
|
|
|
|
|
|
2025-09-12 06:19:29 -05:00
|
|
|
const allowedCameras = useAllowedCameras();
|
|
|
|
|
|
2024-03-12 14:53:01 -05:00
|
|
|
const includesBirdseye = useMemo(() => {
|
2026-02-27 21:02:46 -06:00
|
|
|
// Users without access to all cameras should not have access to birdseye
|
|
|
|
|
if (!hasFullCameraAccess) {
|
2025-11-20 16:50:17 -07:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-25 18:19:31 -05:00
|
|
|
if (
|
|
|
|
|
config &&
|
|
|
|
|
Object.keys(config.camera_groups).length &&
|
|
|
|
|
cameraGroup &&
|
|
|
|
|
config.camera_groups[cameraGroup] &&
|
2025-11-20 16:50:17 -07:00
|
|
|
cameraGroup != "default"
|
2024-04-25 18:19:31 -05:00
|
|
|
) {
|
2024-03-12 14:53:01 -05:00
|
|
|
return config.camera_groups[cameraGroup].cameras.includes("birdseye");
|
|
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-27 21:02:46 -06:00
|
|
|
}, [config, cameraGroup, hasFullCameraAccess]);
|
2024-03-12 14:53:01 -05:00
|
|
|
|
2024-02-10 05:30:53 -07:00
|
|
|
const cameras = useMemo(() => {
|
2023-12-16 07:40:00 -07:00
|
|
|
if (!config) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-25 18:19:31 -05:00
|
|
|
if (
|
|
|
|
|
Object.keys(config.camera_groups).length &&
|
|
|
|
|
cameraGroup &&
|
|
|
|
|
config.camera_groups[cameraGroup] &&
|
|
|
|
|
cameraGroup != "default"
|
|
|
|
|
) {
|
2024-03-04 16:18:30 -07:00
|
|
|
const group = config.camera_groups[cameraGroup];
|
|
|
|
|
return Object.values(config.cameras)
|
2025-03-03 09:30:52 -06:00
|
|
|
.filter(
|
|
|
|
|
(conf) => conf.enabled_in_config && group.cameras.includes(conf.name),
|
|
|
|
|
)
|
2025-09-12 06:19:29 -05:00
|
|
|
.filter((cam) => allowedCameras.includes(cam.name))
|
2024-03-04 16:18:30 -07:00
|
|
|
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-10 05:30:53 -07:00
|
|
|
return Object.values(config.cameras)
|
2025-03-03 09:30:52 -06:00
|
|
|
.filter((conf) => conf.ui.dashboard && conf.enabled_in_config)
|
2025-09-12 06:19:29 -05:00
|
|
|
.filter((cam) => allowedCameras.includes(cam.name))
|
2024-02-10 05:30:53 -07:00
|
|
|
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
2025-09-12 06:19:29 -05:00
|
|
|
}, [config, cameraGroup, allowedCameras]);
|
2023-12-15 16:24:50 -07:00
|
|
|
|
2025-12-09 11:08:44 -07:00
|
|
|
const selectedCamera = useMemo(() => {
|
|
|
|
|
if (!config || !selectedCameraName || selectedCameraName === "birdseye") {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
const camera = config.cameras[selectedCameraName];
|
|
|
|
|
if (
|
|
|
|
|
camera &&
|
|
|
|
|
allowedCameras.includes(selectedCameraName) &&
|
|
|
|
|
camera.enabled_in_config
|
|
|
|
|
) {
|
|
|
|
|
return camera;
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}, [config, selectedCameraName, allowedCameras]);
|
2024-02-12 18:28:36 -07:00
|
|
|
|
2023-12-08 07:33:22 -06:00
|
|
|
return (
|
2024-05-31 07:58:33 -05:00
|
|
|
<div className="size-full" ref={mainRef}>
|
2026-02-27 21:02:46 -06:00
|
|
|
{selectedCameraName === "birdseye" &&
|
|
|
|
|
hasFullCameraAccess &&
|
|
|
|
|
config?.birdseye?.enabled ? (
|
2024-05-31 07:58:33 -05:00
|
|
|
<LiveBirdseyeView
|
2024-08-19 15:01:21 -06:00
|
|
|
supportsFullscreen={supportsFullScreen}
|
2024-05-31 07:58:33 -05:00
|
|
|
fullscreen={fullscreen}
|
|
|
|
|
toggleFullscreen={toggleFullscreen}
|
2025-12-09 11:08:44 -07:00
|
|
|
onSelectCamera={setSelectedCameraName}
|
2024-05-31 07:58:33 -05:00
|
|
|
/>
|
|
|
|
|
) : selectedCamera ? (
|
|
|
|
|
<LiveCameraView
|
2025-09-30 14:51:47 -06:00
|
|
|
key={selectedCameraName}
|
2024-05-31 07:58:33 -05:00
|
|
|
config={config}
|
|
|
|
|
camera={selectedCamera}
|
2024-08-19 15:01:21 -06:00
|
|
|
supportsFullscreen={supportsFullScreen}
|
2024-05-31 07:58:33 -05:00
|
|
|
fullscreen={fullscreen}
|
|
|
|
|
toggleFullscreen={toggleFullscreen}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<LiveDashboardView
|
|
|
|
|
cameras={cameras}
|
2024-07-15 09:34:41 -06:00
|
|
|
cameraGroup={cameraGroup ?? "default"}
|
2024-05-31 07:58:33 -05:00
|
|
|
includeBirdseye={includesBirdseye}
|
|
|
|
|
onSelectCamera={setSelectedCameraName}
|
|
|
|
|
fullscreen={fullscreen}
|
|
|
|
|
toggleFullscreen={toggleFullscreen}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2023-12-08 07:33:22 -06:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Live;
|