frigate/web/src/pages/Live.tsx
Josh Hawkins c687aa5119
Some checks failed
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
Birdseye fixes (#22166)
* permit birdseye access if user has viewer role or a custom viewer role that has access to all cameras

* bump version
2026-02-27 20:02:46 -07:00

187 lines
5.5 KiB
TypeScript

import { useFullscreen } from "@/hooks/use-fullscreen";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
import { useHashState, useSearchEffect } from "@/hooks/use-overlay-state";
import { useUserPersistedOverlayState } 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 { useTranslation } from "react-i18next";
import { useEffect, useMemo, useRef } from "react";
import useSWR from "swr";
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import { useHasFullCameraAccess } from "@/hooks/use-has-full-camera-access";
function Live() {
const { t } = useTranslation(["views/live"]);
const { data: config } = useSWR<FrigateConfig>("config");
const hasFullCameraAccess = useHasFullCameraAccess();
// selection
const [selectedCameraName, setSelectedCameraName] = useHashState();
const [cameraGroup, setCameraGroup, loaded] = useUserPersistedOverlayState(
"cameraGroup",
"default" as string,
);
useSearchEffect("group", (cameraGroup) => {
if (config && cameraGroup && loaded) {
const group = config.camera_groups[cameraGroup];
if (group) {
setCameraGroup(cameraGroup);
// 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;
}
return true;
}
return false;
});
// fullscreen
const mainRef = useRef<HTMLDivElement | null>(null);
const { fullscreen, toggleFullscreen, supportsFullScreen } =
useFullscreen(mainRef);
useKeyboardListener(["f"], (key, modifiers) => {
if (!modifiers.down) {
return true;
}
switch (key) {
case "f":
toggleFullscreen();
return true;
}
return false;
});
// document title
useEffect(() => {
if (selectedCameraName) {
const capitalized = selectedCameraName
.split("_")
.filter((text) => text)
.map((text) => text[0].toUpperCase() + text.substring(1));
document.title = t("documentTitle.withCamera", {
camera: capitalized.join(" "),
});
} else if (cameraGroup && cameraGroup != "default") {
document.title = t("documentTitle.withCamera", {
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
});
} else {
document.title = t("documentTitle", { ns: "views/live" });
}
}, [cameraGroup, selectedCameraName, t]);
// settings
const allowedCameras = useAllowedCameras();
const includesBirdseye = useMemo(() => {
// Users without access to all cameras should not have access to birdseye
if (!hasFullCameraAccess) {
return false;
}
if (
config &&
Object.keys(config.camera_groups).length &&
cameraGroup &&
config.camera_groups[cameraGroup] &&
cameraGroup != "default"
) {
return config.camera_groups[cameraGroup].cameras.includes("birdseye");
} else {
return false;
}
}, [config, cameraGroup, hasFullCameraAccess]);
const cameras = useMemo(() => {
if (!config) {
return [];
}
if (
Object.keys(config.camera_groups).length &&
cameraGroup &&
config.camera_groups[cameraGroup] &&
cameraGroup != "default"
) {
const group = config.camera_groups[cameraGroup];
return Object.values(config.cameras)
.filter(
(conf) => conf.enabled_in_config && group.cameras.includes(conf.name),
)
.filter((cam) => allowedCameras.includes(cam.name))
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}
return Object.values(config.cameras)
.filter((conf) => conf.ui.dashboard && conf.enabled_in_config)
.filter((cam) => allowedCameras.includes(cam.name))
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config, cameraGroup, allowedCameras]);
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]);
return (
<div className="size-full" ref={mainRef}>
{selectedCameraName === "birdseye" &&
hasFullCameraAccess &&
config?.birdseye?.enabled ? (
<LiveBirdseyeView
supportsFullscreen={supportsFullScreen}
fullscreen={fullscreen}
toggleFullscreen={toggleFullscreen}
onSelectCamera={setSelectedCameraName}
/>
) : selectedCamera ? (
<LiveCameraView
key={selectedCameraName}
config={config}
camera={selectedCamera}
supportsFullscreen={supportsFullScreen}
fullscreen={fullscreen}
toggleFullscreen={toggleFullscreen}
/>
) : (
<LiveDashboardView
cameras={cameras}
cameraGroup={cameraGroup ?? "default"}
includeBirdseye={includesBirdseye}
onSelectCamera={setSelectedCameraName}
fullscreen={fullscreen}
toggleFullscreen={toggleFullscreen}
/>
)}
</div>
);
}
export default Live;