mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-09 23:15:28 +03:00
Compare commits
No commits in common. "88ab136f8ed56ce4667b076c9cd71ab273e2a318" and "9e0e4162f987fb8cf5d4406bf33ec6295246d5f5" have entirely different histories.
88ab136f8e
...
9e0e4162f9
@ -123,76 +123,6 @@ record:
|
||||
</TabItem>
|
||||
</ConfigTabs>
|
||||
|
||||
## Pre-capture and Post-capture
|
||||
|
||||
The `pre_capture` and `post_capture` settings control how many seconds of video are included before and after an alert or detection. These can be configured independently for alerts and detections, and can be set globally or overridden per camera.
|
||||
|
||||
<ConfigTabs>
|
||||
<TabItem value="ui">
|
||||
|
||||
Navigate to <NavPath path="Settings > Global configuration > Recording" /> for global defaults, or <NavPath path="Settings > Camera configuration > (select camera) > Recording" /> to override for a specific camera.
|
||||
|
||||
| Field | Description |
|
||||
| ---------------------------------------------- | ---------------------------------------------------- |
|
||||
| **Alert retention > Pre-capture seconds** | Seconds of video to include before an alert event |
|
||||
| **Alert retention > Post-capture seconds** | Seconds of video to include after an alert event |
|
||||
| **Detection retention > Pre-capture seconds** | Seconds of video to include before a detection event |
|
||||
| **Detection retention > Post-capture seconds** | Seconds of video to include after a detection event |
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yaml">
|
||||
|
||||
```yaml
|
||||
record:
|
||||
enabled: True
|
||||
alerts:
|
||||
pre_capture: 5 # seconds before the alert to include
|
||||
post_capture: 5 # seconds after the alert to include
|
||||
detections:
|
||||
pre_capture: 5 # seconds before the detection to include
|
||||
post_capture: 5 # seconds after the detection to include
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</ConfigTabs>
|
||||
|
||||
- **Default**: 5 seconds for both pre and post capture.
|
||||
- **Pre-capture maximum**: 60 seconds.
|
||||
- These settings apply per review category (alerts and detections), not per object type.
|
||||
|
||||
### How pre/post capture interacts with retention mode
|
||||
|
||||
The `pre_capture` and `post_capture` values define the **time window** around a review item, but only recording segments that also match the configured **retention mode** are actually kept on disk.
|
||||
|
||||
- **`mode: all`** — Retains every segment within the capture window, regardless of whether motion was detected.
|
||||
- **`mode: motion`** (default) — Only retains segments within the capture window that contain motion. This includes segments with active tracked objects, since object motion implies motion. Segments without any motion are discarded even if they fall within the pre/post capture range.
|
||||
- **`mode: active_objects`** — Only retains segments within the capture window where tracked objects were actively moving. Segments with general motion but no active objects are discarded.
|
||||
|
||||
This means that with the default `motion` mode, you may see less footage than the configured pre/post capture duration if parts of the capture window had no motion.
|
||||
|
||||
To guarantee the full pre/post capture duration is always retained:
|
||||
|
||||
```yaml
|
||||
record:
|
||||
enabled: True
|
||||
alerts:
|
||||
pre_capture: 10
|
||||
post_capture: 10
|
||||
retain:
|
||||
days: 30
|
||||
mode: all # retains all segments within the capture window
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
Because recording segments are written in 10 second chunks, pre-capture timing depends on segment boundaries. The actual pre-capture footage may be slightly shorter or longer than the exact configured value.
|
||||
|
||||
:::
|
||||
|
||||
### Where to view pre/post capture footage
|
||||
|
||||
Pre and post capture footage is included in the **recording timeline**, visible in the History view. Note that pre/post capture settings only affect which recording segments are **retained on disk** — they do not change the start and end points shown in the UI. The History view will still center on the review item's actual time range, but you can scrub backward and forward through the retained pre/post capture footage on the timeline. The Explore view shows object-specific clips that are trimmed to when the tracked object was actually visible, so pre/post capture time will not be reflected there.
|
||||
|
||||
## Will Frigate delete old recordings if my storage runs out?
|
||||
|
||||
As of Frigate 0.12 if there is less than an hour left of storage, the oldest 2 hours of recordings will be deleted.
|
||||
|
||||
@ -694,9 +694,6 @@ def config_set(request: Request, body: AppConfigSetBody):
|
||||
if request.app.stats_emitter is not None:
|
||||
request.app.stats_emitter.config = config
|
||||
|
||||
if request.app.dispatcher is not None:
|
||||
request.app.dispatcher.config = config
|
||||
|
||||
if body.update_topic:
|
||||
if body.update_topic.startswith("config/cameras/"):
|
||||
_, _, camera, field = body.update_topic.split("/")
|
||||
|
||||
@ -92,7 +92,6 @@ class EmbeddingMaintainer(threading.Thread):
|
||||
CameraConfigUpdateEnum.add,
|
||||
CameraConfigUpdateEnum.remove,
|
||||
CameraConfigUpdateEnum.object_genai,
|
||||
CameraConfigUpdateEnum.review,
|
||||
CameraConfigUpdateEnum.review_genai,
|
||||
CameraConfigUpdateEnum.semantic_search,
|
||||
],
|
||||
|
||||
@ -1326,10 +1326,6 @@
|
||||
"keyPlaceholder": "New key",
|
||||
"remove": "Remove"
|
||||
},
|
||||
"knownPlates": {
|
||||
"namePlaceholder": "e.g., Wife's Car",
|
||||
"platePlaceholder": "Plate number or regex"
|
||||
},
|
||||
"timezone": {
|
||||
"defaultOption": "Use browser timezone"
|
||||
},
|
||||
|
||||
@ -67,13 +67,6 @@ const lpr: SectionConfigOverrides = {
|
||||
format: {
|
||||
"ui:options": { size: "md" },
|
||||
},
|
||||
known_plates: {
|
||||
"ui:field": "KnownPlatesField",
|
||||
"ui:options": {
|
||||
label: false,
|
||||
suppressDescription: true,
|
||||
},
|
||||
},
|
||||
replace_rules: {
|
||||
"ui:field": "ReplaceRulesField",
|
||||
"ui:options": {
|
||||
|
||||
@ -16,16 +16,6 @@ const record: SectionConfigOverrides = {
|
||||
},
|
||||
},
|
||||
],
|
||||
fieldDocs: {
|
||||
"alerts.pre_capture":
|
||||
"/configuration/record#pre-capture-and-post-capture",
|
||||
"alerts.post_capture":
|
||||
"/configuration/record#pre-capture-and-post-capture",
|
||||
"detections.pre_capture":
|
||||
"/configuration/record#pre-capture-and-post-capture",
|
||||
"detections.post_capture":
|
||||
"/configuration/record#pre-capture-and-post-capture",
|
||||
},
|
||||
restartRequired: [],
|
||||
fieldOrder: [
|
||||
"enabled",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import { Trans } from "react-i18next";
|
||||
import Heading from "@/components/ui/heading";
|
||||
@ -37,21 +37,6 @@ export default function CameraReviewStatusToggles({
|
||||
const { payload: revDescState, send: sendRevDesc } =
|
||||
useReviewDescriptionState(cameraId);
|
||||
|
||||
// Sync WS runtime state when review genai transitions from disabled to enabled in config
|
||||
const prevRevGenaiEnabled = useRef(
|
||||
cameraConfig?.review?.genai?.enabled_in_config,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const wasEnabled = prevRevGenaiEnabled.current;
|
||||
const isEnabled = cameraConfig?.review?.genai?.enabled_in_config;
|
||||
prevRevGenaiEnabled.current = isEnabled;
|
||||
|
||||
if (!wasEnabled && isEnabled) {
|
||||
sendRevDesc("ON");
|
||||
}
|
||||
}, [cameraConfig?.review?.genai?.enabled_in_config, sendRevDesc]);
|
||||
|
||||
if (!selectedCamera || !cameraConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1,277 +0,0 @@
|
||||
import type { FieldPathList, FieldProps, RJSFSchema } from "@rjsf/utils";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
LuChevronDown,
|
||||
LuChevronRight,
|
||||
LuPlus,
|
||||
LuTrash2,
|
||||
} from "react-icons/lu";
|
||||
import type { ConfigFormContext } from "@/types/configForm";
|
||||
import get from "lodash/get";
|
||||
import { isSubtreeModified } from "../utils";
|
||||
|
||||
type KnownPlatesData = Record<string, string[]>;
|
||||
|
||||
export function KnownPlatesField(props: FieldProps) {
|
||||
const { schema, formData, onChange, idSchema, disabled, readonly } = props;
|
||||
const formContext = props.registry?.formContext as
|
||||
| ConfigFormContext
|
||||
| undefined;
|
||||
|
||||
const { t } = useTranslation(["views/settings", "common"]);
|
||||
|
||||
const data: KnownPlatesData = useMemo(() => {
|
||||
if (!formData || typeof formData !== "object" || Array.isArray(formData)) {
|
||||
return {};
|
||||
}
|
||||
return formData as KnownPlatesData;
|
||||
}, [formData]);
|
||||
|
||||
const entries = useMemo(() => Object.entries(data), [data]);
|
||||
|
||||
const title = (schema as RJSFSchema).title;
|
||||
const description = (schema as RJSFSchema).description;
|
||||
|
||||
const hasItems = entries.length > 0;
|
||||
const emptyPath = useMemo(() => [] as FieldPathList, []);
|
||||
const fieldPath =
|
||||
(props as { fieldPathId?: { path?: FieldPathList } }).fieldPathId?.path ??
|
||||
emptyPath;
|
||||
|
||||
const isModified = useMemo(() => {
|
||||
const baselineRoot = formContext?.baselineFormData;
|
||||
const baselineValue = baselineRoot
|
||||
? get(baselineRoot, fieldPath)
|
||||
: undefined;
|
||||
return isSubtreeModified(
|
||||
data,
|
||||
baselineValue,
|
||||
formContext?.overrides,
|
||||
fieldPath,
|
||||
formContext?.formData,
|
||||
);
|
||||
}, [fieldPath, formContext, data]);
|
||||
|
||||
const [open, setOpen] = useState(hasItems || isModified);
|
||||
|
||||
useEffect(() => {
|
||||
if (isModified) {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [isModified]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasItems) {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [hasItems]);
|
||||
|
||||
const handleAddEntry = useCallback(() => {
|
||||
const next = { ...data, "": [""] };
|
||||
onChange(next, fieldPath);
|
||||
}, [data, fieldPath, onChange]);
|
||||
|
||||
const handleRemoveEntry = useCallback(
|
||||
(key: string) => {
|
||||
const next = { ...data };
|
||||
delete next[key];
|
||||
onChange(next, fieldPath);
|
||||
},
|
||||
[data, fieldPath, onChange],
|
||||
);
|
||||
|
||||
const handleRenameKey = useCallback(
|
||||
(oldKey: string, newKey: string) => {
|
||||
if (oldKey === newKey) return;
|
||||
// Preserve order by rebuilding the object
|
||||
const next: KnownPlatesData = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (k === oldKey) {
|
||||
next[newKey] = v;
|
||||
} else {
|
||||
next[k] = v;
|
||||
}
|
||||
}
|
||||
onChange(next, fieldPath);
|
||||
},
|
||||
[data, fieldPath, onChange],
|
||||
);
|
||||
|
||||
const handleAddPlate = useCallback(
|
||||
(key: string) => {
|
||||
const next = { ...data, [key]: [...(data[key] || []), ""] };
|
||||
onChange(next, fieldPath);
|
||||
},
|
||||
[data, fieldPath, onChange],
|
||||
);
|
||||
|
||||
const handleRemovePlate = useCallback(
|
||||
(key: string, plateIndex: number) => {
|
||||
const plates = [...(data[key] || [])];
|
||||
plates.splice(plateIndex, 1);
|
||||
const next = { ...data, [key]: plates };
|
||||
onChange(next, fieldPath);
|
||||
},
|
||||
[data, fieldPath, onChange],
|
||||
);
|
||||
|
||||
const handleUpdatePlate = useCallback(
|
||||
(key: string, plateIndex: number, value: string) => {
|
||||
const plates = [...(data[key] || [])];
|
||||
plates[plateIndex] = value;
|
||||
const next = { ...data, [key]: plates };
|
||||
onChange(next, fieldPath);
|
||||
},
|
||||
[data, fieldPath, onChange],
|
||||
);
|
||||
|
||||
const baseId = idSchema?.$id || "known_plates";
|
||||
const deleteLabel = t("button.delete", {
|
||||
ns: "common",
|
||||
defaultValue: "Delete",
|
||||
});
|
||||
const namePlaceholder = t("configForm.knownPlates.namePlaceholder", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
const platePlaceholder = t("configForm.knownPlates.platePlaceholder", {
|
||||
ns: "views/settings",
|
||||
});
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer p-4 transition-colors hover:bg-muted/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle
|
||||
className={cn("text-sm", isModified && "text-danger")}
|
||||
>
|
||||
{title}
|
||||
</CardTitle>
|
||||
{description && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{open ? (
|
||||
<LuChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<LuChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent>
|
||||
<CardContent className="space-y-3 p-4 pt-0">
|
||||
{entries.map(([key, plates], entryIndex) => {
|
||||
const entryId = `${baseId}-${entryIndex}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={entryIndex}
|
||||
className="space-y-2 rounded-md border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id={`${entryId}-key`}
|
||||
defaultValue={key}
|
||||
placeholder={namePlaceholder}
|
||||
disabled={disabled || readonly}
|
||||
onBlur={(e) => handleRenameKey(key, e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveEntry(key)}
|
||||
disabled={disabled || readonly}
|
||||
aria-label={deleteLabel}
|
||||
title={deleteLabel}
|
||||
className="shrink-0"
|
||||
>
|
||||
<LuTrash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="ml-1 space-y-2 border-l-2 border-muted-foreground/20 pl-3">
|
||||
{plates.map((plate, plateIndex) => (
|
||||
<div key={plateIndex} className="flex items-center gap-2">
|
||||
<Input
|
||||
id={`${entryId}-plate-${plateIndex}`}
|
||||
value={plate}
|
||||
placeholder={platePlaceholder}
|
||||
disabled={disabled || readonly}
|
||||
onChange={(e) =>
|
||||
handleUpdatePlate(key, plateIndex, e.target.value)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
{plates.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemovePlate(key, plateIndex)}
|
||||
disabled={disabled || readonly}
|
||||
aria-label={deleteLabel}
|
||||
title={deleteLabel}
|
||||
className="shrink-0"
|
||||
>
|
||||
<LuTrash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAddPlate(key)}
|
||||
disabled={disabled || readonly}
|
||||
className="gap-2"
|
||||
>
|
||||
<LuPlus className="h-4 w-4" />
|
||||
{t("button.add", {
|
||||
ns: "common",
|
||||
defaultValue: "Add",
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddEntry}
|
||||
disabled={disabled || readonly}
|
||||
className="gap-2"
|
||||
>
|
||||
<LuPlus className="h-4 w-4" />
|
||||
{t("button.add", { ns: "common", defaultValue: "Add" })}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default KnownPlatesField;
|
||||
@ -49,7 +49,6 @@ import { DetectorHardwareField } from "./fields/DetectorHardwareField";
|
||||
import { ReplaceRulesField } from "./fields/ReplaceRulesField";
|
||||
import { CameraInputsField } from "./fields/CameraInputsField";
|
||||
import { DictAsYamlField } from "./fields/DictAsYamlField";
|
||||
import { KnownPlatesField } from "./fields/KnownPlatesField";
|
||||
|
||||
export interface FrigateTheme {
|
||||
widgets: RegistryWidgetsType;
|
||||
@ -106,6 +105,5 @@ export const frigateTheme: FrigateTheme = {
|
||||
ReplaceRulesField: ReplaceRulesField,
|
||||
CameraInputsField: CameraInputsField,
|
||||
DictAsYamlField: DictAsYamlField,
|
||||
KnownPlatesField: KnownPlatesField,
|
||||
},
|
||||
};
|
||||
|
||||
@ -818,27 +818,6 @@ export default function Settings() {
|
||||
[],
|
||||
);
|
||||
|
||||
// Show save/undo all buttons only when changes span multiple sections
|
||||
// or the single changed section is not the one currently being viewed
|
||||
const showSaveAllButtons = useMemo(() => {
|
||||
const pendingKeys = Object.keys(pendingDataBySection);
|
||||
if (pendingKeys.length === 0) return false;
|
||||
if (pendingKeys.length >= 2) return true;
|
||||
|
||||
// Exactly one pending section — check if it matches the current view
|
||||
const key = pendingKeys[0];
|
||||
const menuKey = pendingKeyToMenuKey(key);
|
||||
if (menuKey !== pageToggle) return true;
|
||||
|
||||
// For camera-scoped keys, also check if the camera matches
|
||||
if (key.includes("::")) {
|
||||
const cameraName = key.slice(0, key.indexOf("::"));
|
||||
return cameraName !== selectedCamera;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [pendingDataBySection, pendingKeyToMenuKey, pageToggle, selectedCamera]);
|
||||
|
||||
const handleSaveAll = useCallback(async () => {
|
||||
if (
|
||||
!config ||
|
||||
@ -1512,7 +1491,7 @@ export default function Settings() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{showSaveAllButtons && (
|
||||
{hasPendingChanges && (
|
||||
<div className="sticky bottom-0 z-50 mt-2 bg-background p-4">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
@ -1688,7 +1667,7 @@ export default function Settings() {
|
||||
</Heading>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{showSaveAllButtons && (
|
||||
{hasPendingChanges && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-2",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user