mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-22 08:08:22 +03:00
Compare commits
5 Commits
5b8f108622
...
08c2d91309
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08c2d91309 | ||
|
|
352d271fe4 | ||
|
|
a6e11a59d6 | ||
|
|
a7d8d13d9a | ||
|
|
7ec1d5d2c6 |
@ -510,6 +510,12 @@ record:
|
||||
# Optional: Number of minutes to wait between cleanup runs (default: shown below)
|
||||
# This can be used to reduce the frequency of deleting recording segments from disk if you want to minimize i/o
|
||||
expire_interval: 60
|
||||
# Optional: Maximum size of recordings in MB or string format (e.g. 10GB). (default: shown below)
|
||||
# This serves as a hard limit for the size of the recordings for this camera.
|
||||
# If the total size of recordings exceeds this limit, the oldest recordings will be deleted
|
||||
# until the total size is below the limit, regardless of retention settings.
|
||||
# 0 means no limit.
|
||||
max_size: 0
|
||||
# Optional: Two-way sync recordings database with disk on startup and once a day (default: shown below).
|
||||
sync_recordings: False
|
||||
# Optional: Continuous retention settings
|
||||
|
||||
@ -9,4 +9,25 @@ Snapshots are accessible in the UI in the Explore pane. This allows for quick su
|
||||
|
||||
To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones)
|
||||
|
||||
Snapshots sent via MQTT are configured in the [config file](https://docs.frigate.video/configuration/) under `cameras -> your_camera -> mqtt`
|
||||
Snapshots sent via MQTT are configured in the [config file](/configuration) under `cameras -> your_camera -> mqtt`
|
||||
|
||||
## Frame Selection
|
||||
|
||||
Frigate does not save every frame — it picks a single "best" frame for each tracked object and uses it for both the snapshot and clean copy. As the object is tracked across frames, Frigate continuously evaluates whether the current frame is better than the previous best based on detection confidence, object size, and the presence of key attributes like faces or license plates. Frames where the object touches the edge of the frame are deprioritized. The snapshot is written to disk once tracking ends using whichever frame was determined to be the best.
|
||||
|
||||
MQTT snapshots are published more frequently — each time a better thumbnail frame is found during tracking, or when the current best image is older than `best_image_timeout` (default: 60s). These use their own annotation settings configured under `cameras -> your_camera -> mqtt`.
|
||||
|
||||
## Clean Copy
|
||||
|
||||
Frigate can produce up to two snapshot files per event, each used in different places:
|
||||
|
||||
| Version | File | Annotations | Used by |
|
||||
| --- | --- | --- | --- |
|
||||
| **Regular snapshot** | `<camera>-<id>.jpg` | Respects your `timestamp`, `bounding_box`, `crop`, and `height` settings | API (`/api/events/<id>/snapshot.jpg`), MQTT (`<camera>/<label>/snapshot`), Explore pane in the UI |
|
||||
| **Clean copy** | `<camera>-<id>-clean.webp` | Always unannotated — no bounding box, no timestamp, no crop, full resolution | API (`/api/events/<id>/snapshot-clean.webp`), [Frigate+](/plus/first_model) submissions, "Download Clean Snapshot" in the UI |
|
||||
|
||||
MQTT snapshots are configured separately under `cameras -> your_camera -> mqtt` and are unrelated to the clean copy.
|
||||
|
||||
The clean copy is required for submitting events to [Frigate+](/plus/first_model) — if you plan to use Frigate+, keep `clean_copy` enabled regardless of your other snapshot settings.
|
||||
|
||||
If you are not using Frigate+ and `timestamp`, `bounding_box`, and `crop` are all disabled, the regular snapshot is already effectively clean, so `clean_copy` provides no benefit and only uses additional disk space. You can safely set `clean_copy: False` in this case.
|
||||
|
||||
@ -16,7 +16,15 @@ See the [MQTT integration
|
||||
documentation](https://www.home-assistant.io/integrations/mqtt/) for more
|
||||
details.
|
||||
|
||||
In addition, MQTT must be enabled in your Frigate configuration file and Frigate must be connected to the same MQTT server as Home Assistant for many of the entities created by the integration to function.
|
||||
In addition, MQTT must be enabled in your Frigate configuration file and Frigate must be connected to the same MQTT server as Home Assistant for many of the entities created by the integration to function, e.g.:
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
enabled: True
|
||||
host: mqtt.server.com # the address of your HA server that's running the MQTT integration
|
||||
user: your_mqtt_broker_username
|
||||
password: your_mqtt_broker_password
|
||||
```
|
||||
|
||||
### Integration installation
|
||||
|
||||
@ -95,12 +103,12 @@ services:
|
||||
|
||||
If you are using Home Assistant Add-on, the URL should be one of the following depending on which Add-on variant you are using. Note that if you are using the Proxy Add-on, you should NOT point the integration at the proxy URL. Just enter the same URL used to access Frigate directly from your network.
|
||||
|
||||
| Add-on Variant | URL |
|
||||
| -------------------------- | ----------------------------------------- |
|
||||
| Frigate | `http://ccab4aaf-frigate:5000` |
|
||||
| Frigate (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
|
||||
| Frigate Beta | `http://ccab4aaf-frigate-beta:5000` |
|
||||
| Frigate Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` |
|
||||
| Add-on Variant | URL |
|
||||
| -------------------------- | -------------------------------------- |
|
||||
| Frigate | `http://ccab4aaf-frigate:5000` |
|
||||
| Frigate (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
|
||||
| Frigate Beta | `http://ccab4aaf-frigate-beta:5000` |
|
||||
| Frigate Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` |
|
||||
|
||||
### Frigate running on a separate machine
|
||||
|
||||
|
||||
@ -120,7 +120,7 @@ Message published for each changed tracked object. The first message is publishe
|
||||
|
||||
### `frigate/tracked_object_update`
|
||||
|
||||
Message published for updates to tracked object metadata, for example:
|
||||
Message published for updates to tracked object metadata. All messages include an `id` field which is the tracked object's event ID, and can be used to look up the event via the API or match it to items in the UI.
|
||||
|
||||
#### Generative AI Description Update
|
||||
|
||||
@ -134,12 +134,14 @@ Message published for updates to tracked object metadata, for example:
|
||||
|
||||
#### Face Recognition Update
|
||||
|
||||
Published after each recognition attempt, regardless of whether the score meets `recognition_threshold`. See the [Face Recognition](/configuration/face_recognition) documentation for details on how scoring works.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "face",
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"name": "John",
|
||||
"score": 0.95,
|
||||
"name": "John", // best matching person, or null if no match
|
||||
"score": 0.95, // running weighted average across all recognition attempts
|
||||
"camera": "front_door_cam",
|
||||
"timestamp": 1607123958.748393
|
||||
}
|
||||
@ -147,11 +149,13 @@ Message published for updates to tracked object metadata, for example:
|
||||
|
||||
#### License Plate Recognition Update
|
||||
|
||||
Published when a license plate is recognized on a car object. See the [License Plate Recognition](/configuration/license_plate_recognition) documentation for details.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "lpr",
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"name": "John's Car",
|
||||
"name": "John's Car", // known name for the plate, or null
|
||||
"plate": "123ABC",
|
||||
"score": 0.95,
|
||||
"camera": "driveway_cam",
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from frigate.const import MAX_PRE_CAPTURE
|
||||
from frigate.review.types import SeverityEnum
|
||||
from frigate.util.size import parse_size_to_mb
|
||||
|
||||
from ..base import FrigateBaseModel
|
||||
|
||||
@ -81,6 +82,10 @@ class RecordConfig(FrigateBaseModel):
|
||||
default=60,
|
||||
title="Number of minutes to wait between cleanup runs.",
|
||||
)
|
||||
max_size: Union[float, str] = Field(
|
||||
default=0,
|
||||
title="Maximum size of recordings in MB or string format (e.g. 10GB).",
|
||||
)
|
||||
continuous: RecordRetainConfig = Field(
|
||||
default_factory=RecordRetainConfig,
|
||||
title="Continuous recording retention settings.",
|
||||
@ -104,6 +109,16 @@ class RecordConfig(FrigateBaseModel):
|
||||
default=None, title="Keep track of original state of recording."
|
||||
)
|
||||
|
||||
@field_validator("max_size", mode="before")
|
||||
@classmethod
|
||||
def parse_max_size(cls, v: Union[float, str], info: object) -> float:
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
return parse_size_to_mb(v)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid size string: {v}")
|
||||
return v
|
||||
|
||||
@property
|
||||
def event_pre_capture(self) -> int:
|
||||
return max(
|
||||
|
||||
@ -20,6 +20,17 @@ from frigate.util.time import get_tomorrow_at_time
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_directory_size(directory: str) -> float:
|
||||
"""Get the size of a directory in MB."""
|
||||
total_size = 0
|
||||
for dirpath, dirnames, filenames in os.walk(directory):
|
||||
for f in filenames:
|
||||
fp = os.path.join(dirpath, f)
|
||||
if not os.path.islink(fp):
|
||||
total_size += os.path.getsize(fp)
|
||||
return total_size / 1000000
|
||||
|
||||
|
||||
class RecordingCleanup(threading.Thread):
|
||||
"""Cleanup existing recordings based on retention config."""
|
||||
|
||||
@ -120,6 +131,7 @@ class RecordingCleanup(threading.Thread):
|
||||
Recordings.objects,
|
||||
Recordings.motion,
|
||||
Recordings.dBFS,
|
||||
Recordings.segment_size,
|
||||
)
|
||||
.where(
|
||||
(Recordings.camera == config.name)
|
||||
@ -206,6 +218,10 @@ class RecordingCleanup(threading.Thread):
|
||||
Recordings.id << deleted_recordings_list[i : i + max_deletes]
|
||||
).execute()
|
||||
|
||||
# Check if we need to enforce max_size
|
||||
if config.record.max_size > 0:
|
||||
self.enforce_max_size(config, deleted_recordings)
|
||||
|
||||
previews: list[Previews] = (
|
||||
Previews.select(
|
||||
Previews.id,
|
||||
@ -266,6 +282,52 @@ class RecordingCleanup(threading.Thread):
|
||||
Previews.id << deleted_previews_list[i : i + max_deletes]
|
||||
).execute()
|
||||
|
||||
def enforce_max_size(
|
||||
self, config: CameraConfig, deleted_recordings: set[str]
|
||||
) -> None:
|
||||
"""Ensure that the camera recordings do not exceed the max size."""
|
||||
# Get all recordings for this camera
|
||||
recordings: Recordings = (
|
||||
Recordings.select(
|
||||
Recordings.id,
|
||||
Recordings.path,
|
||||
Recordings.segment_size,
|
||||
)
|
||||
.where(
|
||||
(Recordings.camera == config.name)
|
||||
& (Recordings.id.not_in(list(deleted_recordings)))
|
||||
)
|
||||
.order_by(Recordings.start_time)
|
||||
.namedtuples()
|
||||
.iterator()
|
||||
)
|
||||
|
||||
total_size = 0
|
||||
recordings_list = []
|
||||
for recording in recordings:
|
||||
recordings_list.append(recording)
|
||||
total_size += recording.segment_size
|
||||
|
||||
# If the total size is less than the max size, we are good
|
||||
if total_size <= config.record.max_size:
|
||||
return
|
||||
|
||||
# Delete recordings until we are under the max size
|
||||
recordings_to_delete = []
|
||||
for recording in recordings_list:
|
||||
total_size -= recording.segment_size
|
||||
recordings_to_delete.append(recording.id)
|
||||
Path(recording.path).unlink(missing_ok=True)
|
||||
if total_size <= config.record.max_size:
|
||||
break
|
||||
|
||||
# Delete from database
|
||||
max_deletes = 100000
|
||||
for i in range(0, len(recordings_to_delete), max_deletes):
|
||||
Recordings.delete().where(
|
||||
Recordings.id << recordings_to_delete[i : i + max_deletes]
|
||||
).execute()
|
||||
|
||||
def expire_recordings(self) -> None:
|
||||
"""Delete recordings based on retention config."""
|
||||
logger.debug("Start expire recordings.")
|
||||
|
||||
@ -429,6 +429,29 @@ class TestConfig(unittest.TestCase):
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert "-rtsp_transport" in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]
|
||||
|
||||
def test_record_max_size_validation(self):
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"record": {"max_size": "10GB"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert frigate_config.record.max_size == 10000
|
||||
|
||||
def test_ffmpeg_params_global(self):
|
||||
config = {
|
||||
"ffmpeg": {"input_args": "-re"},
|
||||
|
||||
@ -31,3 +31,15 @@ class TestRecordRetention(unittest.TestCase):
|
||||
)
|
||||
assert not segment_info.should_discard_segment(RetainModeEnum.motion)
|
||||
assert segment_info.should_discard_segment(RetainModeEnum.active_objects)
|
||||
|
||||
def test_size_utility(self):
|
||||
from frigate.util.size import parse_size_to_mb
|
||||
|
||||
assert parse_size_to_mb("10GB") == 10240
|
||||
assert parse_size_to_mb("10MB") == 10
|
||||
assert parse_size_to_mb("1024KB") == 1
|
||||
assert parse_size_to_mb("1048576B") == 1
|
||||
assert parse_size_to_mb("10") == 10
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
parse_size_to_mb("invalid")
|
||||
|
||||
21
frigate/util/size.py
Normal file
21
frigate/util/size.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""Utility for parsing size strings."""
|
||||
|
||||
|
||||
def parse_size_to_mb(size_str: str) -> float:
|
||||
"""Parse a size string to megabytes."""
|
||||
size_str = size_str.strip().upper()
|
||||
if size_str.endswith("TB"):
|
||||
return float(size_str[:-2]) * 1024 * 1024
|
||||
elif size_str.endswith("GB"):
|
||||
return float(size_str[:-2]) * 1024
|
||||
elif size_str.endswith("MB"):
|
||||
return float(size_str[:-2])
|
||||
elif size_str.endswith("KB"):
|
||||
return float(size_str[:-2]) / 1024
|
||||
elif size_str.endswith("B"):
|
||||
return float(size_str[:-1]) / (1024 * 1024)
|
||||
else:
|
||||
try:
|
||||
return float(size_str)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid size string: {size_str}")
|
||||
Loading…
Reference in New Issue
Block a user