import Heading from "@/components/ui/heading"; 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, useContext, 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"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; type MotionTunerProps = { selectedCamera: string; setUnsavedChanges: React.Dispatch>; }; type MotionSettings = { threshold?: number; contour_area?: number; improve_contrast?: boolean; }; export default function MotionTuner({ selectedCamera, setUnsavedChanges, }: MotionTunerProps) { const { data: config, mutate: updateConfig } = useSWR("config"); const [changedValue, setChangedValue] = useState(false); const [isLoading, setIsLoading] = useState(false); const { addMessage, clearMessages } = useContext(StatusBarMessagesContext)!; 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, }); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedCamera]); useEffect(() => { if (!motionSettings.threshold) return; sendMotionThreshold(motionSettings.threshold); }, [motionSettings.threshold, sendMotionThreshold]); useEffect(() => { if (!motionSettings.contour_area) return; sendMotionContourArea(motionSettings.contour_area); }, [motionSettings.contour_area, sendMotionContourArea]); useEffect(() => { if (motionSettings.improve_contrast === undefined) return; sendImproveContrast(motionSettings.improve_contrast ? "ON" : "OFF"); }, [motionSettings.improve_contrast, sendImproveContrast]); const handleMotionConfigChange = (newConfig: Partial) => { setMotionSettings((prevConfig) => ({ ...prevConfig, ...newConfig })); setUnsavedChanges(true); 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); clearMessages("motion_tuner"); }, [origMotionSettings, clearMessages]); useEffect(() => { if (changedValue) { addMessage("motion_tuner", "Unsaved motion tuner changes"); } else { clearMessages("motion_tuner"); } }, [changedValue, addMessage, clearMessages]); 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 }); }} />
{cameraConfig ? (
) : ( )}
); }