import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage"; import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; import { Toaster } from "@/components/ui/sonner"; import { Label } from "@/components/ui/label"; import useSWR from "swr"; import Heading from "@/components/ui/heading"; import { Switch } from "@/components/ui/switch"; import { usePersistence } from "@/hooks/use-persistence"; import { Skeleton } from "@/components/ui/skeleton"; import { useCameraActivity } from "@/hooks/use-camera-activity"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { AudioDetection, ObjectType } from "@/types/ws"; import useDeepMemo from "@/hooks/use-deep-memo"; import { Card } from "@/components/ui/card"; import { getIconForLabel } from "@/utils/iconUtil"; import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { LuExternalLink, LuInfo } from "react-icons/lu"; import { Link } from "react-router-dom"; import DebugDrawingLayer from "@/components/overlay/DebugDrawingLayer"; import { Separator } from "@/components/ui/separator"; import { isDesktop } from "react-device-detect"; import { Trans, useTranslation } from "react-i18next"; import { useDocDomain } from "@/hooks/use-doc-domain"; import { getTranslatedLabel } from "@/utils/i18n"; import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; import { AudioLevelGraph } from "@/components/audio/AudioLevelGraph"; import { useWs } from "@/api/ws"; type ObjectSettingsViewProps = { selectedCamera?: string; }; type Options = { [key: string]: boolean }; const emptyObject = Object.freeze({}); export default function ObjectSettingsView({ selectedCamera, }: ObjectSettingsViewProps) { const { t } = useTranslation(["views/settings"]); const { getLocaleDocUrl } = useDocDomain(); const { data: config } = useSWR("config"); const containerRef = useRef(null); const DEBUG_OPTIONS = [ { param: "bbox", title: t("debug.boundingBoxes.title"), description: t("debug.boundingBoxes.desc"), info: ( <>

{t("debug.boundingBoxes.colors.label")}

), }, { param: "timestamp", title: t("debug.timestamp.title"), description: t("debug.timestamp.desc"), }, { param: "zones", title: t("debug.zones.title"), description: t("debug.zones.desc"), }, { param: "mask", title: t("debug.mask.title"), description: t("debug.mask.desc"), }, { param: "motion", title: t("debug.motion.title"), description: t("debug.motion.desc"), info: debug.motion.tips, }, { param: "regions", title: t("debug.regions.title"), description: t("debug.regions.desc"), info: debug.regions.tips, }, { param: "paths", title: t("debug.paths.title"), description: t("debug.paths.desc"), info: debug.paths.tips, }, ]; const [options, setOptions, optionsLoaded] = usePersistence( `${selectedCamera}-feed`, emptyObject, ); const handleSetOption = useCallback( (id: string, value: boolean) => { const newOptions = { ...options, [id]: value }; setOptions(newOptions); }, [options, setOptions], ); const [debugDraw, setDebugDraw] = useState(false); useEffect(() => { setDebugDraw(false); }, [selectedCamera]); const cameraConfig = useMemo(() => { if (config && selectedCamera) { return config.cameras[selectedCamera]; } }, [config, selectedCamera]); const cameraName = useCameraFriendlyName(cameraConfig); const { objects, audio_detections } = useCameraActivity( cameraConfig ?? ({} as CameraConfig), ); const memoizedObjects = useDeepMemo(objects); const memoizedAudio = useDeepMemo(audio_detections); const searchParams = useMemo(() => { if (!optionsLoaded) { return new URLSearchParams(); } const params = new URLSearchParams( Object.keys(options || {}).reduce((memo, key) => { //@ts-expect-error we know this is correct memo.push([key, options[key] === true ? "1" : "0"]); return memo; }, []), ); return params; }, [options, optionsLoaded]); useEffect(() => { document.title = t("documentTitle.object"); }, [t]); if (!cameraConfig) { return ; } return (
{t("debug.title")}

{t("debug.detectorDesc", { detectors: config ? Object.keys(config?.detectors) .map((detector) => capitalizeFirstLetter(detector)) .join(",") : "", })}

{t("debug.desc")}

{config?.cameras[cameraConfig.name]?.webui_url && (
{t("debug.openCameraWebUI", { camera: cameraName, })}
)} input.roles.includes("audio")) ? "grid-cols-3" : "grid-cols-2"}`} > {t("debug.debugging")} {t("debug.objectList")} {cameraConfig.ffmpeg.inputs.some((input) => input.roles.includes("audio"), ) && ( {t("debug.audio.title")} )}
{DEBUG_OPTIONS.map(({ param, title, description, info }) => (
{info && (
Info
{info}
)}
{description}
{ handleSetOption(param, isChecked); }} />
))}
{isDesktop && ( <>
{t("button.info", { ns: "common" })}
{t("debug.objectShapeFilterDrawing.tips")}
{t("readTheDocumentation", { ns: "common" })}
{t("debug.objectShapeFilterDrawing.desc")}
{ setDebugDraw(isChecked); }} />
)}
{cameraConfig.ffmpeg.inputs.some((input) => input.roles.includes("audio"), ) && ( )}
{cameraConfig ? (
{debugDraw && ( )}
) : ( )}
); } type ObjectListProps = { cameraConfig: CameraConfig; objects?: ObjectType[]; }; function ObjectList({ cameraConfig, objects }: ObjectListProps) { const { t } = useTranslation(["views/settings"]); const { data: config } = useSWR("config"); const colormap = useMemo(() => { if (!config) { return; } return config.model?.colormap; }, [config]); const getColorForObjectName = useCallback( (objectName: string) => { return colormap && colormap[objectName] ? `rgb(${colormap[objectName][2]}, ${colormap[objectName][1]}, ${colormap[objectName][0]})` : "rgb(128, 128, 128)"; }, [colormap], ); return (
{objects && objects.length > 0 ? ( objects.map((obj: ObjectType) => { return (
{getIconForLabel(obj.label, "size-5 text-white")}
{getTranslatedLabel(obj.label)}

{t("debug.objectShapeFilterDrawing.score")}

{obj.score ? (obj.score * 100).toFixed(1).toString() : "-"} %

{t("debug.objectShapeFilterDrawing.ratio")}

{obj.ratio ? obj.ratio.toFixed(2).toString() : "-"}

{t("debug.objectShapeFilterDrawing.area")}

{obj.area ? ( <>
px: {obj.area.toString()}
%:{" "} {( obj.area / (cameraConfig.detect.width * cameraConfig.detect.height) ) .toFixed(4) .toString()}
) : ( "-" )}
); }) ) : (
{t("debug.noObjects")}
)}
); } type AudioListProps = { cameraConfig: CameraConfig; audioDetections?: AudioDetection[]; }; function AudioList({ cameraConfig, audioDetections }: AudioListProps) { const { t } = useTranslation(["views/settings"]); // Get audio levels directly from ws hooks const { value: { payload: audioRms }, } = useWs(`${cameraConfig.name}/audio/rms`, ""); const { value: { payload: audioDBFS }, } = useWs(`${cameraConfig.name}/audio/dBFS`, ""); return (
{audioDetections && Object.keys(audioDetections).length > 0 ? ( Object.entries(audioDetections).map(([key, obj]) => (
{getIconForLabel(key, "size-5 text-white")}
{getTranslatedLabel(key)}

{t("debug.audio.score")}

{obj.score ? (obj.score * 100).toFixed(1).toString() : "-"}%
)) ) : (

{t("debug.audio.noAudioDetections")}

{t("debug.audio.currentRMS")}{" "} {(typeof audioRms === "number" ? audioRms : 0).toFixed(1)} |{" "} {t("debug.audio.currentdbFS")}{" "} {(typeof audioDBFS === "number" ? audioDBFS : 0).toFixed(1)}

)}
); }