Merge pull request #41 from ibs0d/codex/modify-preview-storage-logic-for-multi-path

Write preview mp4s to camera recordings path and add preview dir helper
This commit is contained in:
ibs0d 2026-03-09 20:46:33 +11:00 committed by GitHub
commit 38d6052aa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 36 additions and 8 deletions

View File

@ -100,7 +100,9 @@ class OutputProcess(FrigateProcess):
jsmpeg_cameras[camera] = JsmpegCamera( jsmpeg_cameras[camera] = JsmpegCamera(
camera_config, self.stop_event, websocket_server camera_config, self.stop_event, websocket_server
) )
preview_recorders[camera] = PreviewRecorder(camera_config) preview_recorders[camera] = PreviewRecorder(
camera_config, self.config.get_camera_recordings_path(camera)
)
preview_write_times[camera] = 0 preview_write_times[camera] = 0
if ( if (

View File

@ -15,7 +15,7 @@ import numpy as np
from frigate.comms.inter_process import InterProcessRequestor from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import CameraConfig, RecordQualityEnum from frigate.config import CameraConfig, RecordQualityEnum
from frigate.const import CACHE_DIR, CLIPS_DIR, INSERT_PREVIEW, PREVIEW_FRAME_TYPE from frigate.const import CACHE_DIR, INSERT_PREVIEW, PREVIEW_FRAME_TYPE
from frigate.ffmpeg_presets import ( from frigate.ffmpeg_presets import (
FPS_VFR_PARAM, FPS_VFR_PARAM,
EncodeTypeEnum, EncodeTypeEnum,
@ -58,6 +58,11 @@ PREVIEW_QMAX_PARAM = {
} }
def get_preview_dir(recordings_root: str, camera: str) -> str:
"""Get the output directory for generated preview mp4 files."""
return os.path.join(recordings_root, "preview", camera)
def get_cache_image_name(camera: str, frame_time: float) -> str: def get_cache_image_name(camera: str, frame_time: float) -> str:
"""Get the image name in cache.""" """Get the image name in cache."""
return os.path.join( return os.path.join(
@ -119,14 +124,15 @@ class FFMpegConverter(threading.Thread):
config: CameraConfig, config: CameraConfig,
frame_times: list[float], frame_times: list[float],
requestor: InterProcessRequestor, requestor: InterProcessRequestor,
preview_dir: str,
): ):
super().__init__(name=f"{config.name}_preview_converter") super().__init__(name=f"{config.name}_preview_converter")
self.config = config self.config = config
self.frame_times = frame_times self.frame_times = frame_times
self.requestor = requestor self.requestor = requestor
self.path = os.path.join( self.path = os.path.join(
CLIPS_DIR, preview_dir,
f"previews/{self.config.name}/{self.frame_times[0]}-{self.frame_times[-1]}.mp4", f"{self.frame_times[0]}-{self.frame_times[-1]}.mp4",
) )
# write a PREVIEW at fps and 1 key frame per clip # write a PREVIEW at fps and 1 key frame per clip
@ -203,12 +209,13 @@ class FFMpegConverter(threading.Thread):
class PreviewRecorder: class PreviewRecorder:
def __init__(self, config: CameraConfig) -> None: def __init__(self, config: CameraConfig, recordings_root: str) -> None:
self.config = config self.config = config
self.start_time = 0 self.start_time = 0
self.last_output_time = 0 self.last_output_time = 0
self.offline = False self.offline = False
self.output_frames = [] self.output_frames = []
self.preview_dir = get_preview_dir(recordings_root, config.name)
if config.detect.width > config.detect.height: if config.detect.width > config.detect.height:
self.out_height = PREVIEW_HEIGHT self.out_height = PREVIEW_HEIGHT
@ -254,9 +261,7 @@ class PreviewRecorder:
) )
Path(PREVIEW_CACHE_DIR).mkdir(exist_ok=True) Path(PREVIEW_CACHE_DIR).mkdir(exist_ok=True)
Path(os.path.join(CLIPS_DIR, f"previews/{config.name}")).mkdir( Path(self.preview_dir).mkdir(parents=True, exist_ok=True)
parents=True, exist_ok=True
)
# check for existing items in cache # check for existing items in cache
start_ts = ( start_ts = (
@ -393,6 +398,7 @@ class PreviewRecorder:
self.config, self.config,
self.output_frames, self.output_frames,
self.requestor, self.requestor,
self.preview_dir,
).start() ).start()
else: else:
logger.debug( logger.debug(
@ -442,6 +448,7 @@ class PreviewRecorder:
self.config, self.config,
self.output_frames, self.output_frames,
self.requestor, self.requestor,
self.preview_dir,
).start() ).start()
self.reset_frame_cache(frame_time) self.reset_frame_cache(frame_time)

View File

@ -1,11 +1,18 @@
import os import os
import shutil import shutil
import unittest import unittest
import sys
import types
# Allow importing preview module in environments without opencv installed
sys.modules.setdefault("cv2", types.SimpleNamespace())
sys.modules.setdefault("numpy", types.SimpleNamespace(ndarray=object))
from frigate.output.preview import ( from frigate.output.preview import (
PREVIEW_CACHE_DIR, PREVIEW_CACHE_DIR,
PREVIEW_FRAME_TYPE, PREVIEW_FRAME_TYPE,
get_most_recent_preview_frame, get_most_recent_preview_frame,
get_preview_dir,
) )
@ -78,3 +85,15 @@ class TestPreviewLoader(unittest.TestCase):
def test_get_most_recent_preview_frame_no_directory(self): def test_get_most_recent_preview_frame_no_directory(self):
shutil.rmtree(PREVIEW_CACHE_DIR) shutil.rmtree(PREVIEW_CACHE_DIR)
self.assertIsNone(get_most_recent_preview_frame("test_camera")) self.assertIsNone(get_most_recent_preview_frame("test_camera"))
def test_get_preview_dir_with_explicit_camera_path(self):
self.assertEqual(
get_preview_dir("/video2", "front"),
"/video2/preview/front",
)
def test_get_preview_dir_with_default_recordings_path(self):
self.assertEqual(
get_preview_dir("/media/frigate/recordings", "front"),
"/media/frigate/recordings/preview/front",
)