frigate/web/src/components/settings/MotionTuner.tsx

359 lines
12 KiB
TypeScript
Raw Normal View History

2024-04-05 15:25:23 +03:00
import Heading from "@/components/ui/heading";
2024-04-10 17:16:46 +03:00
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
2024-04-05 15:25:23 +03:00
import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
2024-04-10 17:16:46 +03:00
import axios from "axios";
2024-04-05 15:25:23 +03:00
import ActivityIndicator from "@/components/indicators/activity-indicator";
2024-04-08 04:57:15 +03:00
import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage";
2024-04-10 17:16:46 +03:00
import { useCallback, useEffect, useMemo, useState } from "react";
2024-04-05 15:25:23 +03:00
import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label";
2024-04-10 17:16:46 +03:00
import {
useImproveContrast,
useMotionContourArea,
useMotionThreshold,
} from "@/api/ws";
2024-04-09 17:27:51 +03:00
import { Skeleton } from "../ui/skeleton";
2024-04-10 17:16:46 +03:00
import { Button } from "../ui/button";
import { Switch } from "../ui/switch";
import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner";
2024-04-18 05:27:04 +03:00
import { Separator } from "../ui/separator";
import { Link } from "react-router-dom";
import { LuExternalLink } from "react-icons/lu";
2024-04-10 17:16:46 +03:00
2024-04-15 02:36:39 +03:00
type MotionTunerProps = {
selectedCamera: string;
};
2024-04-10 17:16:46 +03:00
type MotionSettings = {
threshold?: number;
contour_area?: number;
improve_contrast?: boolean;
};
2024-04-05 15:25:23 +03:00
2024-04-15 02:36:39 +03:00
export default function MotionTuner({ selectedCamera }: MotionTunerProps) {
2024-04-10 17:16:46 +03:00
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
const [changedValue, setChangedValue] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
2024-04-05 15:25:23 +03:00
2024-04-10 17:16:46 +03:00
const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera);
const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera);
const { send: sendImproveContrast } = useImproveContrast(selectedCamera);
const [motionSettings, setMotionSettings] = useState<MotionSettings>({
threshold: undefined,
contour_area: undefined,
improve_contrast: undefined,
});
2024-04-05 15:25:23 +03:00
2024-04-18 05:27:04 +03:00
const [origMotionSettings, setOrigMotionSettings] = useState<MotionSettings>({
threshold: undefined,
contour_area: undefined,
improve_contrast: undefined,
});
2024-04-05 15:25:23 +03:00
const cameraConfig = useMemo(() => {
if (config && selectedCamera) {
return config.cameras[selectedCamera];
}
}, [config, selectedCamera]);
2024-04-10 17:16:46 +03:00
useEffect(() => {
if (cameraConfig) {
setMotionSettings({
threshold: cameraConfig.motion.threshold,
contour_area: cameraConfig.motion.contour_area,
improve_contrast: cameraConfig.motion.improve_contrast,
});
2024-04-18 05:27:04 +03:00
setOrigMotionSettings({
threshold: cameraConfig.motion.threshold,
contour_area: cameraConfig.motion.contour_area,
improve_contrast: cameraConfig.motion.improve_contrast,
});
2024-04-10 17:16:46 +03:00
}
}, [cameraConfig]);
2024-04-05 15:25:23 +03:00
2024-04-10 17:16:46 +03:00
useEffect(() => {
if (cameraConfig) {
const { threshold, contour_area, improve_contrast } = motionSettings;
2024-04-05 15:25:23 +03:00
2024-04-10 17:16:46 +03:00
if (
threshold !== undefined &&
cameraConfig.motion.threshold !== threshold
) {
2024-04-05 15:25:23 +03:00
sendMotionThreshold(threshold);
}
2024-04-10 17:16:46 +03:00
if (
contour_area !== undefined &&
cameraConfig.motion.contour_area !== contour_area
) {
sendMotionContourArea(contour_area);
}
if (
improve_contrast !== undefined &&
cameraConfig.motion.improve_contrast !== improve_contrast
) {
sendImproveContrast(improve_contrast ? "ON" : "OFF");
}
}
}, [
cameraConfig,
motionSettings,
sendMotionThreshold,
sendMotionContourArea,
sendImproveContrast,
]);
const handleMotionConfigChange = (newConfig: Partial<MotionSettings>) => {
setMotionSettings((prevConfig) => ({ ...prevConfig, ...newConfig }));
setChangedValue(true);
};
const saveToConfig = useCallback(async () => {
setIsLoading(true);
axios
.put(
`config/set?cameras.${selectedCamera}.motion.threshold=${motionSettings.threshold}&cameras.${selectedCamera}.motion.contour_area=${motionSettings.contour_area}&cameras.${selectedCamera}.motion.improve_contrast=${motionSettings.improve_contrast}`,
{ requires_restart: 0 },
)
.then((res) => {
if (res.status === 200) {
2024-04-18 03:59:00 +03:00
toast.success("Motion settings have been saved.", {
position: "top-center",
});
2024-04-10 17:16:46 +03:00
setChangedValue(false);
updateConfig();
} else {
toast.error(`Failed to save config changes: ${res.statusText}`, {
position: "top-center",
});
}
})
.catch((error) => {
toast.error(
`Failed to save config changes: ${error.response.data.message}`,
{ position: "top-center" },
);
})
.finally(() => {
setIsLoading(false);
});
}, [
updateConfig,
motionSettings.threshold,
motionSettings.contour_area,
motionSettings.improve_contrast,
selectedCamera,
]);
2024-04-18 05:27:04 +03:00
const onCancel = useCallback(() => {
setMotionSettings(origMotionSettings);
setChangedValue(false);
}, [origMotionSettings]);
2024-04-05 15:25:23 +03:00
2024-04-10 17:16:46 +03:00
const handleDialog = useCallback(
(save: boolean) => {
if (save) {
saveToConfig();
2024-04-05 15:25:23 +03:00
}
2024-04-10 17:16:46 +03:00
setConfirmationDialogOpen(false);
setChangedValue(false);
2024-04-05 15:25:23 +03:00
},
2024-04-15 02:36:39 +03:00
[saveToConfig],
2024-04-05 15:25:23 +03:00
);
if (!cameraConfig && !selectedCamera) {
return <ActivityIndicator />;
}
return (
2024-04-15 02:36:39 +03:00
<div className="flex flex-col md:flex-row size-full">
<Toaster position="top-center" />
<div className="flex flex-col w-full overflow-y-auto mt-2 md:mt-0 md:w-3/12 order-last md:order-none md:mr-2 rounded-lg border-secondary-foreground border-[1px] p-2 bg-background_alt">
<Heading as="h3" className="my-2">
Motion Detection Tuner
</Heading>
2024-04-18 05:27:04 +03:00
<div className="text-sm text-muted-foreground my-3 space-y-3">
<p>
Frigate uses motion detection as a first line check to see if there
is anything happening in the frame worth checking with object
detection.
</p>
2024-04-10 17:16:46 +03:00
2024-04-18 05:27:04 +03:00
<div className="flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/motion_detection"
target="_blank"
rel="noopener noreferrer"
className="inline"
>
Read the Motion Tuning Guide{" "}
<LuExternalLink className="size-3 ml-2 inline-flex" />
</Link>
2024-04-15 02:36:39 +03:00
</div>
2024-04-18 05:27:04 +03:00
</div>
<Separator className="flex my-2 bg-secondary" />
<div className="flex flex-col w-full space-y-6">
<div className="mt-2 space-y-6">
<div className="space-y-0.5">
<Label htmlFor="motion-threshold" className="text-md">
Threshold
</Label>
<div className="text-sm text-muted-foreground my-2">
<p>
The threshold value dictates how much of a change in a pixel's
luminance is required to be considered motion.{" "}
<em>Default: 30</em>
</p>
</div>
</div>
<div className="flex flex-row justify-between">
<Slider
id="motion-threshold"
className="w-full"
disabled={motionSettings.threshold === undefined}
value={[motionSettings.threshold ?? 0]}
min={5}
max={80}
step={1}
onValueChange={(value) => {
handleMotionConfigChange({ threshold: value[0] });
}}
/>
<div className="text-lg ml-6 mr-2 flex align-center">
{motionSettings.threshold}
</div>
</div>
</div>
<div className="mt-2 space-y-6">
<div className="space-y-0.5">
<Label htmlFor="motion-threshold" className="text-md">
Contour Area
</Label>
<div className="text-sm text-muted-foreground my-2">
<p>
The contour area value is used to decide which groups of
changed pixels qualify as motion. <em>Default: 10</em>
</p>
</div>
</div>
<div className="flex flex-row justify-between">
<Slider
id="motion-contour-area"
className="w-full"
disabled={motionSettings.contour_area === undefined}
value={[motionSettings.contour_area ?? 0]}
min={5}
max={100}
step={1}
onValueChange={(value) => {
handleMotionConfigChange({ contour_area: value[0] });
}}
/>
<div className="text-lg ml-6 mr-2 flex align-center">
{motionSettings.contour_area}
</div>
</div>
2024-04-15 02:36:39 +03:00
</div>
2024-04-18 05:27:04 +03:00
<Separator className="flex my-2 bg-secondary" />
<div className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="improve-contrast">Improve Contrast</Label>
<div className="text-sm text-muted-foreground">
Improve contrast for darker scenes. <em>Default: ON</em>
</div>
</div>
2024-04-15 02:36:39 +03:00
<Switch
id="improve-contrast"
2024-04-18 05:27:04 +03:00
className="ml-3"
2024-04-15 02:36:39 +03:00
disabled={motionSettings.improve_contrast === undefined}
checked={motionSettings.improve_contrast === true}
onCheckedChange={(isChecked) => {
handleMotionConfigChange({ improve_contrast: isChecked });
}}
/>
2024-04-09 17:27:51 +03:00
</div>
2024-04-18 05:27:04 +03:00
</div>
<div className="flex flex-col flex-1 justify-end">
2024-04-18 03:59:00 +03:00
<div className="flex flex-row gap-2 pt-5">
<Button className="flex flex-1" onClick={onCancel}>
2024-04-18 05:27:04 +03:00
Reset
2024-04-18 03:59:00 +03:00
</Button>
2024-04-15 02:36:39 +03:00
<Button
2024-04-18 03:59:00 +03:00
variant="select"
2024-04-18 05:27:04 +03:00
disabled={!changedValue || isLoading}
2024-04-18 03:59:00 +03:00
className="flex flex-1"
2024-04-18 05:27:04 +03:00
onClick={saveToConfig}
2024-04-10 17:16:46 +03:00
>
2024-04-18 03:59:00 +03:00
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>Saving...</span>
</div>
) : (
"Save"
)}
2024-04-15 02:36:39 +03:00
</Button>
</div>
</div>
{confirmationDialogOpen && (
<AlertDialog
open={confirmationDialogOpen}
onOpenChange={() => setConfirmationDialogOpen(false)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
You have unsaved changes on this camera.
</AlertDialogTitle>
<AlertDialogDescription>
Do you want to save your changes before continuing?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => handleDialog(false)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={() => handleDialog(true)}>
Save
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
{cameraConfig ? (
<div className="flex md:w-7/12 md:grow md:h-dvh md:max-h-full">
<div className="size-full min-h-10">
<AutoUpdatingCameraImage
camera={cameraConfig.name}
searchParams={new URLSearchParams([["motion", "1"]])}
showFps={false}
className="size-full"
cameraClasses="relative w-full h-full flex flex-col justify-start"
/>
</div>
2024-04-05 15:25:23 +03:00
</div>
2024-04-09 17:27:51 +03:00
) : (
<Skeleton className="size-full rounded-2xl" />
)}
2024-04-15 02:36:39 +03:00
</div>
2024-04-05 15:25:23 +03:00
);
}