import Heading from "@/components/ui/heading"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "../ui/alert-dialog"; import { FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; import axios from "axios"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Slider } from "@/components/ui/slider"; import { Label } from "@/components/ui/label"; import { useImproveContrast, useMotionContourArea, useMotionThreshold, } from "@/api/ws"; import { Skeleton } from "../ui/skeleton"; import { Button } from "../ui/button"; import { Switch } from "../ui/switch"; import { Toaster } from "@/components/ui/sonner"; import { toast } from "sonner"; import { Separator } from "../ui/separator"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; type MotionTunerProps = { selectedCamera: string; }; type MotionSettings = { threshold?: number; contour_area?: number; improve_contrast?: boolean; }; export default function MotionTuner({ selectedCamera }: MotionTunerProps) { const { data: config, mutate: updateConfig } = useSWR("config"); const [changedValue, setChangedValue] = useState(false); const [isLoading, setIsLoading] = useState(false); const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera); const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera); const { send: sendImproveContrast } = useImproveContrast(selectedCamera); const [motionSettings, setMotionSettings] = useState({ threshold: undefined, contour_area: undefined, improve_contrast: undefined, }); const [origMotionSettings, setOrigMotionSettings] = useState({ threshold: undefined, contour_area: undefined, improve_contrast: undefined, }); const cameraConfig = useMemo(() => { if (config && selectedCamera) { return config.cameras[selectedCamera]; } }, [config, selectedCamera]); useEffect(() => { if (cameraConfig) { setMotionSettings({ threshold: cameraConfig.motion.threshold, contour_area: cameraConfig.motion.contour_area, improve_contrast: cameraConfig.motion.improve_contrast, }); setOrigMotionSettings({ threshold: cameraConfig.motion.threshold, contour_area: cameraConfig.motion.contour_area, improve_contrast: cameraConfig.motion.improve_contrast, }); } }, [cameraConfig]); useEffect(() => { if (cameraConfig) { const { threshold, contour_area, improve_contrast } = motionSettings; if ( threshold !== undefined && cameraConfig.motion.threshold !== threshold ) { sendMotionThreshold(threshold); } 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) => { 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) { toast.success("Motion settings have been saved.", { position: "top-center", }); 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, ]); const onCancel = useCallback(() => { setMotionSettings(origMotionSettings); setChangedValue(false); }, [origMotionSettings]); const handleDialog = useCallback( (save: boolean) => { if (save) { saveToConfig(); } setConfirmationDialogOpen(false); setChangedValue(false); }, [saveToConfig], ); if (!cameraConfig && !selectedCamera) { return ; } return (
Motion Detection Tuner

Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection.

Read the Motion Tuning Guide{" "}

The threshold value dictates how much of a change in a pixel's luminance is required to be considered motion.{" "} Default: 30

{ handleMotionConfigChange({ threshold: value[0] }); }} />
{motionSettings.threshold}

The contour area value is used to decide which groups of changed pixels qualify as motion. Default: 10

{ handleMotionConfigChange({ contour_area: value[0] }); }} />
{motionSettings.contour_area}
Improve contrast for darker scenes. Default: ON
{ handleMotionConfigChange({ improve_contrast: isChecked }); }} />
{confirmationDialogOpen && ( setConfirmationDialogOpen(false)} > You have unsaved changes on this camera. Do you want to save your changes before continuing? handleDialog(false)}> Cancel handleDialog(true)}> Save )}
{cameraConfig ? (
) : ( )}
); }