mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-29 19:40:19 +03:00
Merge pull request #45 from ibs0d/claude/analyze-video-storage-OeT89
Claude/analyze video storage oe t89
This commit is contained in:
commit
682d816f19
@ -89,6 +89,11 @@ python3 /usr/local/nginx/get_nginx_settings.py | \
|
|||||||
tempio -template /usr/local/nginx/templates/listen.gotmpl \
|
tempio -template /usr/local/nginx/templates/listen.gotmpl \
|
||||||
-out /usr/local/nginx/conf/listen.conf
|
-out /usr/local/nginx/conf/listen.conf
|
||||||
|
|
||||||
|
# build location blocks for recording roots outside /media/frigate
|
||||||
|
python3 /usr/local/nginx/get_nginx_settings.py | \
|
||||||
|
tempio -template /usr/local/nginx/templates/extra_recordings.gotmpl \
|
||||||
|
-out /usr/local/nginx/conf/extra_recordings.conf
|
||||||
|
|
||||||
# Replace the bash process with the NGINX process, redirecting stderr to stdout
|
# Replace the bash process with the NGINX process, redirecting stderr to stdout
|
||||||
exec 2>&1
|
exec 2>&1
|
||||||
exec \
|
exec \
|
||||||
|
|||||||
@ -101,6 +101,7 @@ http {
|
|||||||
|
|
||||||
include auth_location.conf;
|
include auth_location.conf;
|
||||||
include base_path.conf;
|
include base_path.conf;
|
||||||
|
include extra_recordings.conf;
|
||||||
|
|
||||||
location /vod/ {
|
location /vod/ {
|
||||||
include auth_request.conf;
|
include auth_request.conf;
|
||||||
|
|||||||
@ -52,11 +52,22 @@ listen_config["external_port"] = external_port
|
|||||||
|
|
||||||
base_path = os.environ.get("FRIGATE_BASE_PATH", "")
|
base_path = os.environ.get("FRIGATE_BASE_PATH", "")
|
||||||
|
|
||||||
|
# Collect recording roots that are outside the default /media/frigate tree.
|
||||||
|
# Nginx needs an explicit location block for each such root to serve preview files.
|
||||||
|
_default_recordings = "/media/frigate"
|
||||||
|
_extra_roots: set[str] = set()
|
||||||
|
for _cam_cfg in config.get("cameras", {}).values():
|
||||||
|
if isinstance(_cam_cfg, dict):
|
||||||
|
_path = _cam_cfg.get("path", "")
|
||||||
|
if _path and not _path.startswith(_default_recordings):
|
||||||
|
_extra_roots.add(_path.rstrip("/"))
|
||||||
|
|
||||||
result: dict[str, Any] = {
|
result: dict[str, Any] = {
|
||||||
"tls": tls_config,
|
"tls": tls_config,
|
||||||
"ipv6": ipv6_config,
|
"ipv6": ipv6_config,
|
||||||
"listen": listen_config,
|
"listen": listen_config,
|
||||||
"base_path": base_path,
|
"base_path": base_path,
|
||||||
|
"extra_recording_roots": sorted(_extra_roots),
|
||||||
}
|
}
|
||||||
|
|
||||||
print(json.dumps(result))
|
print(json.dumps(result))
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
{{ range .extra_recording_roots }}
|
||||||
|
location {{ . }}/preview/ {
|
||||||
|
include auth_request.conf;
|
||||||
|
types {
|
||||||
|
video/mp4 mp4;
|
||||||
|
}
|
||||||
|
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
alias {{ . }}/preview/;
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
@ -201,6 +201,7 @@ class FFMpegConverter(threading.Thread):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Error saving preview for {self.config.name} :: {p.stderr}")
|
logger.error(f"Error saving preview for {self.config.name} :: {p.stderr}")
|
||||||
|
Path(self.path).unlink(missing_ok=True)
|
||||||
|
|
||||||
# unlink files from cache
|
# unlink files from cache
|
||||||
# don't delete last frame as it will be used as first frame in next segment
|
# don't delete last frame as it will be used as first frame in next segment
|
||||||
@ -345,7 +346,7 @@ class PreviewRecorder:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def write_frame_to_cache(self, frame_time: float, frame: np.ndarray) -> None:
|
def write_frame_to_cache(self, frame_time: float, frame: np.ndarray) -> bool:
|
||||||
# resize yuv frame
|
# resize yuv frame
|
||||||
small_frame = np.zeros((self.out_height * 3 // 2, self.out_width), np.uint8)
|
small_frame = np.zeros((self.out_height * 3 // 2, self.out_width), np.uint8)
|
||||||
copy_yuv_to_position(
|
copy_yuv_to_position(
|
||||||
@ -360,7 +361,7 @@ class PreviewRecorder:
|
|||||||
small_frame,
|
small_frame,
|
||||||
cv2.COLOR_YUV2BGR_I420,
|
cv2.COLOR_YUV2BGR_I420,
|
||||||
)
|
)
|
||||||
cv2.imwrite(
|
result = cv2.imwrite(
|
||||||
get_cache_image_name(self.config.name, frame_time),
|
get_cache_image_name(self.config.name, frame_time),
|
||||||
small_frame,
|
small_frame,
|
||||||
[
|
[
|
||||||
@ -368,6 +369,11 @@ class PreviewRecorder:
|
|||||||
PREVIEW_QUALITY_WEBP[self.config.record.preview.quality],
|
PREVIEW_QUALITY_WEBP[self.config.record.preview.quality],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
if not result:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to write preview frame for {self.config.name} at {frame_time}, likely no space in cache"
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
def write_data(
|
def write_data(
|
||||||
self,
|
self,
|
||||||
@ -381,8 +387,8 @@ class PreviewRecorder:
|
|||||||
# always write the first frame
|
# always write the first frame
|
||||||
if self.start_time == 0:
|
if self.start_time == 0:
|
||||||
self.start_time = frame_time
|
self.start_time = frame_time
|
||||||
self.output_frames.append(frame_time)
|
if self.write_frame_to_cache(frame_time, frame):
|
||||||
self.write_frame_to_cache(frame_time, frame)
|
self.output_frames.append(frame_time)
|
||||||
return
|
return
|
||||||
|
|
||||||
# check if PREVIEW clip should be generated and cached frames reset
|
# check if PREVIEW clip should be generated and cached frames reset
|
||||||
@ -390,8 +396,8 @@ class PreviewRecorder:
|
|||||||
if len(self.output_frames) > 0:
|
if len(self.output_frames) > 0:
|
||||||
# save last frame to ensure consistent duration
|
# save last frame to ensure consistent duration
|
||||||
if self.config.record:
|
if self.config.record:
|
||||||
self.output_frames.append(frame_time)
|
if self.write_frame_to_cache(frame_time, frame):
|
||||||
self.write_frame_to_cache(frame_time, frame)
|
self.output_frames.append(frame_time)
|
||||||
|
|
||||||
# write the preview if any frames exist for this hour
|
# write the preview if any frames exist for this hour
|
||||||
FFMpegConverter(
|
FFMpegConverter(
|
||||||
@ -409,13 +415,13 @@ class PreviewRecorder:
|
|||||||
|
|
||||||
# include first frame to ensure consistent duration
|
# include first frame to ensure consistent duration
|
||||||
if self.config.record.enabled:
|
if self.config.record.enabled:
|
||||||
self.output_frames.append(frame_time)
|
if self.write_frame_to_cache(frame_time, frame):
|
||||||
self.write_frame_to_cache(frame_time, frame)
|
self.output_frames.append(frame_time)
|
||||||
|
|
||||||
return
|
return
|
||||||
elif self.should_write_frame(current_tracked_objects, motion_boxes, frame_time):
|
elif self.should_write_frame(current_tracked_objects, motion_boxes, frame_time):
|
||||||
self.output_frames.append(frame_time)
|
if self.write_frame_to_cache(frame_time, frame):
|
||||||
self.write_frame_to_cache(frame_time, frame)
|
self.output_frames.append(frame_time)
|
||||||
return
|
return
|
||||||
|
|
||||||
def flag_offline(self, frame_time: float) -> None:
|
def flag_offline(self, frame_time: float) -> None:
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from peewee import SQL, fn
|
|||||||
|
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.const import RECORD_DIR, REPLAY_CAMERA_PREFIX
|
from frigate.const import RECORD_DIR, REPLAY_CAMERA_PREFIX
|
||||||
from frigate.models import Event, Recordings
|
from frigate.models import Event, Previews, Recordings
|
||||||
from frigate.util.builtin import clear_and_unlink
|
from frigate.util.builtin import clear_and_unlink
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -390,6 +390,32 @@ class StorageMaintainer(threading.Thread):
|
|||||||
f"Updated has_clip to False for {len(events_to_update)} events"
|
f"Updated has_clip to False for {len(events_to_update)} events"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Also delete preview files that overlap with deleted recordings so they
|
||||||
|
# don't continue to consume space on the same disk after the recordings
|
||||||
|
# are gone (especially important for multi-path setups where preview and
|
||||||
|
# recordings share the same disk).
|
||||||
|
if deleted_recordings:
|
||||||
|
deleted_previews = []
|
||||||
|
for camera, time_range in camera_recordings.items():
|
||||||
|
overlapping_previews = (
|
||||||
|
Previews.select(Previews.id, Previews.path)
|
||||||
|
.where(
|
||||||
|
Previews.camera == camera,
|
||||||
|
Previews.start_time < time_range["max_end"],
|
||||||
|
Previews.end_time > time_range["min_start"],
|
||||||
|
)
|
||||||
|
.namedtuples()
|
||||||
|
)
|
||||||
|
for preview in overlapping_previews:
|
||||||
|
clear_and_unlink(Path(preview.path), missing_ok=True)
|
||||||
|
deleted_previews.append(preview.id)
|
||||||
|
|
||||||
|
logger.debug(f"Expiring {len(deleted_previews)} previews")
|
||||||
|
for i in range(0, len(deleted_previews), max_deletes):
|
||||||
|
Previews.delete().where(
|
||||||
|
Previews.id << deleted_previews[i : i + max_deletes]
|
||||||
|
).execute()
|
||||||
|
|
||||||
deleted_recordings_list = [r.id for r in deleted_recordings]
|
deleted_recordings_list = [r.id for r in deleted_recordings]
|
||||||
for i in range(0, len(deleted_recordings_list), max_deletes):
|
for i in range(0, len(deleted_recordings_list), max_deletes):
|
||||||
Recordings.delete().where(
|
Recordings.delete().where(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user