mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-05-30 01:16:42 +02:00
Refactor Recordings (#7275)
* Run ffmpeg sub process & video_properties as async * Run recording cleanup in the main process * More cleanup * Use inter process communication to write recordings into the DB * Formatting
This commit is contained in:
parent
9016a48dc7
commit
761daf46ea
@ -36,13 +36,14 @@ from frigate.events.external import ExternalEventProcessor
|
|||||||
from frigate.events.maintainer import EventProcessor
|
from frigate.events.maintainer import EventProcessor
|
||||||
from frigate.http import create_app
|
from frigate.http import create_app
|
||||||
from frigate.log import log_process, root_configurer
|
from frigate.log import log_process, root_configurer
|
||||||
from frigate.models import Event, Recordings, Timeline
|
from frigate.models import Event, Recordings, RecordingsToDelete, Timeline
|
||||||
from frigate.object_detection import ObjectDetectProcess
|
from frigate.object_detection import ObjectDetectProcess
|
||||||
from frigate.object_processing import TrackedObjectProcessor
|
from frigate.object_processing import TrackedObjectProcessor
|
||||||
from frigate.output import output_frames
|
from frigate.output import output_frames
|
||||||
from frigate.plus import PlusApi
|
from frigate.plus import PlusApi
|
||||||
from frigate.ptz.autotrack import PtzAutoTrackerThread
|
from frigate.ptz.autotrack import PtzAutoTrackerThread
|
||||||
from frigate.ptz.onvif import OnvifController
|
from frigate.ptz.onvif import OnvifController
|
||||||
|
from frigate.record.cleanup import RecordingCleanup
|
||||||
from frigate.record.record import manage_recordings
|
from frigate.record.record import manage_recordings
|
||||||
from frigate.stats import StatsEmitter, stats_init
|
from frigate.stats import StatsEmitter, stats_init
|
||||||
from frigate.storage import StorageMaintainer
|
from frigate.storage import StorageMaintainer
|
||||||
@ -292,6 +293,7 @@ class FrigateApp:
|
|||||||
name="recording_manager",
|
name="recording_manager",
|
||||||
args=(
|
args=(
|
||||||
self.config,
|
self.config,
|
||||||
|
self.inter_process_queue,
|
||||||
self.object_recordings_info_queue,
|
self.object_recordings_info_queue,
|
||||||
self.audio_recordings_info_queue,
|
self.audio_recordings_info_queue,
|
||||||
self.feature_metrics,
|
self.feature_metrics,
|
||||||
@ -317,7 +319,7 @@ class FrigateApp:
|
|||||||
60, 10 * len([c for c in self.config.cameras.values() if c.enabled])
|
60, 10 * len([c for c in self.config.cameras.values() if c.enabled])
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
models = [Event, Recordings, Timeline]
|
models = [Event, Recordings, RecordingsToDelete, Timeline]
|
||||||
self.db.bind(models)
|
self.db.bind(models)
|
||||||
|
|
||||||
def init_stats(self) -> None:
|
def init_stats(self) -> None:
|
||||||
@ -522,6 +524,10 @@ class FrigateApp:
|
|||||||
self.event_cleanup = EventCleanup(self.config, self.stop_event)
|
self.event_cleanup = EventCleanup(self.config, self.stop_event)
|
||||||
self.event_cleanup.start()
|
self.event_cleanup.start()
|
||||||
|
|
||||||
|
def start_record_cleanup(self) -> None:
|
||||||
|
self.record_cleanup = RecordingCleanup(self.config, self.stop_event)
|
||||||
|
self.record_cleanup.start()
|
||||||
|
|
||||||
def start_storage_maintainer(self) -> None:
|
def start_storage_maintainer(self) -> None:
|
||||||
self.storage_maintainer = StorageMaintainer(self.config, self.stop_event)
|
self.storage_maintainer = StorageMaintainer(self.config, self.stop_event)
|
||||||
self.storage_maintainer.start()
|
self.storage_maintainer.start()
|
||||||
@ -607,6 +613,7 @@ class FrigateApp:
|
|||||||
self.start_timeline_processor()
|
self.start_timeline_processor()
|
||||||
self.start_event_processor()
|
self.start_event_processor()
|
||||||
self.start_event_cleanup()
|
self.start_event_cleanup()
|
||||||
|
self.start_record_cleanup()
|
||||||
self.start_stats_emitter()
|
self.start_stats_emitter()
|
||||||
self.start_watchdog()
|
self.start_watchdog()
|
||||||
self.check_shm()
|
self.check_shm()
|
||||||
@ -643,6 +650,7 @@ class FrigateApp:
|
|||||||
self.ptz_autotracker_thread.join()
|
self.ptz_autotracker_thread.join()
|
||||||
self.event_processor.join()
|
self.event_processor.join()
|
||||||
self.event_cleanup.join()
|
self.event_cleanup.join()
|
||||||
|
self.record_cleanup.join()
|
||||||
self.stats_emitter.join()
|
self.stats_emitter.join()
|
||||||
self.frigate_watchdog.join()
|
self.frigate_watchdog.join()
|
||||||
self.db.stop()
|
self.db.stop()
|
||||||
|
@ -5,6 +5,8 @@ from abc import ABC, abstractmethod
|
|||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
|
from frigate.const import INSERT_MANY_RECORDINGS
|
||||||
|
from frigate.models import Recordings
|
||||||
from frigate.ptz.onvif import OnvifCommandEnum, OnvifController
|
from frigate.ptz.onvif import OnvifCommandEnum, OnvifController
|
||||||
from frigate.types import CameraMetricsTypes, FeatureMetricsTypes, PTZMetricsTypes
|
from frigate.types import CameraMetricsTypes, FeatureMetricsTypes, PTZMetricsTypes
|
||||||
from frigate.util.services import restart_frigate
|
from frigate.util.services import restart_frigate
|
||||||
@ -86,6 +88,8 @@ class Dispatcher:
|
|||||||
return
|
return
|
||||||
elif topic == "restart":
|
elif topic == "restart":
|
||||||
restart_frigate()
|
restart_frigate()
|
||||||
|
elif topic == INSERT_MANY_RECORDINGS:
|
||||||
|
Recordings.insert_many(payload).execute()
|
||||||
else:
|
else:
|
||||||
self.publish(topic, payload, retain=False)
|
self.publish(topic, payload, retain=False)
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -1059,7 +1060,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
if "detect" in input.roles:
|
if "detect" in input.roles:
|
||||||
stream_info = {"width": 0, "height": 0}
|
stream_info = {"width": 0, "height": 0}
|
||||||
try:
|
try:
|
||||||
stream_info = get_video_properties(input.path)
|
stream_info = asyncio.run(get_video_properties(input.path))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
f"Error detecting stream resolution automatically for {input.path} Applying default values."
|
f"Error detecting stream resolution automatically for {input.path} Applying default values."
|
||||||
|
@ -47,3 +47,7 @@ DRIVER_INTEL_iHD = "iHD"
|
|||||||
|
|
||||||
MAX_SEGMENT_DURATION = 600
|
MAX_SEGMENT_DURATION = 600
|
||||||
MAX_PLAYLIST_SECONDS = 7200 # support 2 hour segments for a single playlist to account for cameras with inconsistent segment times
|
MAX_PLAYLIST_SECONDS = 7200 # support 2 hour segments for a single playlist to account for cameras with inconsistent segment times
|
||||||
|
|
||||||
|
# Internal Comms Topics
|
||||||
|
|
||||||
|
INSERT_MANY_RECORDINGS = "insert_many_recordings"
|
||||||
|
@ -8,7 +8,6 @@ import os
|
|||||||
import queue
|
import queue
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import subprocess as sp
|
|
||||||
import threading
|
import threading
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from multiprocessing.synchronize import Event as MpEvent
|
from multiprocessing.synchronize import Event as MpEvent
|
||||||
@ -19,7 +18,12 @@ import numpy as np
|
|||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
from frigate.config import FrigateConfig, RetainModeEnum
|
from frigate.config import FrigateConfig, RetainModeEnum
|
||||||
from frigate.const import CACHE_DIR, MAX_SEGMENT_DURATION, RECORD_DIR
|
from frigate.const import (
|
||||||
|
CACHE_DIR,
|
||||||
|
INSERT_MANY_RECORDINGS,
|
||||||
|
MAX_SEGMENT_DURATION,
|
||||||
|
RECORD_DIR,
|
||||||
|
)
|
||||||
from frigate.models import Event, Recordings
|
from frigate.models import Event, Recordings
|
||||||
from frigate.types import FeatureMetricsTypes
|
from frigate.types import FeatureMetricsTypes
|
||||||
from frigate.util.image import area
|
from frigate.util.image import area
|
||||||
@ -51,6 +55,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: FrigateConfig,
|
config: FrigateConfig,
|
||||||
|
inter_process_queue: mp.Queue,
|
||||||
object_recordings_info_queue: mp.Queue,
|
object_recordings_info_queue: mp.Queue,
|
||||||
audio_recordings_info_queue: Optional[mp.Queue],
|
audio_recordings_info_queue: Optional[mp.Queue],
|
||||||
process_info: dict[str, FeatureMetricsTypes],
|
process_info: dict[str, FeatureMetricsTypes],
|
||||||
@ -59,6 +64,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
threading.Thread.__init__(self)
|
threading.Thread.__init__(self)
|
||||||
self.name = "recording_maintainer"
|
self.name = "recording_maintainer"
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.inter_process_queue = inter_process_queue
|
||||||
self.object_recordings_info_queue = object_recordings_info_queue
|
self.object_recordings_info_queue = object_recordings_info_queue
|
||||||
self.audio_recordings_info_queue = audio_recordings_info_queue
|
self.audio_recordings_info_queue = audio_recordings_info_queue
|
||||||
self.process_info = process_info
|
self.process_info = process_info
|
||||||
@ -161,9 +167,11 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
)
|
)
|
||||||
|
|
||||||
recordings_to_insert: list[Optional[Recordings]] = await asyncio.gather(*tasks)
|
recordings_to_insert: list[Optional[Recordings]] = await asyncio.gather(*tasks)
|
||||||
Recordings.insert_many(
|
|
||||||
[r for r in recordings_to_insert if r is not None]
|
# fire and forget recordings entries
|
||||||
).execute()
|
self.inter_process_queue.put(
|
||||||
|
(INSERT_MANY_RECORDINGS, [r for r in recordings_to_insert if r is not None])
|
||||||
|
)
|
||||||
|
|
||||||
async def validate_and_move_segment(
|
async def validate_and_move_segment(
|
||||||
self, camera: str, events: Event, recording: dict[str, any]
|
self, camera: str, events: Event, recording: dict[str, any]
|
||||||
@ -183,7 +191,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
if cache_path in self.end_time_cache:
|
if cache_path in self.end_time_cache:
|
||||||
end_time, duration = self.end_time_cache[cache_path]
|
end_time, duration = self.end_time_cache[cache_path]
|
||||||
else:
|
else:
|
||||||
segment_info = get_video_properties(cache_path, get_duration=True)
|
segment_info = await get_video_properties(cache_path, get_duration=True)
|
||||||
|
|
||||||
if segment_info["duration"]:
|
if segment_info["duration"]:
|
||||||
duration = float(segment_info["duration"])
|
duration = float(segment_info["duration"])
|
||||||
@ -231,7 +239,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
if overlaps:
|
if overlaps:
|
||||||
record_mode = self.config.cameras[camera].record.events.retain.mode
|
record_mode = self.config.cameras[camera].record.events.retain.mode
|
||||||
# move from cache to recordings immediately
|
# move from cache to recordings immediately
|
||||||
return self.move_segment(
|
return await self.move_segment(
|
||||||
camera,
|
camera,
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
@ -253,7 +261,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
# else retain days includes this segment
|
# else retain days includes this segment
|
||||||
else:
|
else:
|
||||||
record_mode = self.config.cameras[camera].record.retain.mode
|
record_mode = self.config.cameras[camera].record.retain.mode
|
||||||
return self.move_segment(
|
return await self.move_segment(
|
||||||
camera, start_time, end_time, duration, cache_path, record_mode
|
camera, start_time, end_time, duration, cache_path, record_mode
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -296,7 +304,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
|
|
||||||
return SegmentInfo(motion_count, active_count, round(average_dBFS))
|
return SegmentInfo(motion_count, active_count, round(average_dBFS))
|
||||||
|
|
||||||
def move_segment(
|
async def move_segment(
|
||||||
self,
|
self,
|
||||||
camera: str,
|
camera: str,
|
||||||
start_time: datetime.datetime,
|
start_time: datetime.datetime,
|
||||||
@ -332,7 +340,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
start_frame = datetime.datetime.now().timestamp()
|
start_frame = datetime.datetime.now().timestamp()
|
||||||
|
|
||||||
# add faststart to kept segments to improve metadata reading
|
# add faststart to kept segments to improve metadata reading
|
||||||
ffmpeg_cmd = [
|
p = await asyncio.create_subprocess_exec(
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
"-y",
|
"-y",
|
||||||
@ -343,17 +351,13 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
"-movflags",
|
"-movflags",
|
||||||
"+faststart",
|
"+faststart",
|
||||||
file_path,
|
file_path,
|
||||||
]
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
|
||||||
p = sp.run(
|
|
||||||
ffmpeg_cmd,
|
|
||||||
encoding="ascii",
|
|
||||||
capture_output=True,
|
|
||||||
)
|
)
|
||||||
|
await p.wait()
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
logger.error(f"Unable to convert {cache_path} to {file_path}")
|
logger.error(f"Unable to convert {cache_path} to {file_path}")
|
||||||
logger.error(p.stderr)
|
logger.error((await p.stderr.read()).decode("ascii"))
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
@ -11,8 +11,7 @@ from playhouse.sqliteq import SqliteQueueDatabase
|
|||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.models import Event, Recordings, RecordingsToDelete, Timeline
|
from frigate.models import Event, Recordings
|
||||||
from frigate.record.cleanup import RecordingCleanup
|
|
||||||
from frigate.record.maintainer import RecordingMaintainer
|
from frigate.record.maintainer import RecordingMaintainer
|
||||||
from frigate.types import FeatureMetricsTypes
|
from frigate.types import FeatureMetricsTypes
|
||||||
from frigate.util.services import listen
|
from frigate.util.services import listen
|
||||||
@ -22,6 +21,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def manage_recordings(
|
def manage_recordings(
|
||||||
config: FrigateConfig,
|
config: FrigateConfig,
|
||||||
|
inter_process_queue: mp.Queue,
|
||||||
object_recordings_info_queue: mp.Queue,
|
object_recordings_info_queue: mp.Queue,
|
||||||
audio_recordings_info_queue: mp.Queue,
|
audio_recordings_info_queue: mp.Queue,
|
||||||
process_info: dict[str, FeatureMetricsTypes],
|
process_info: dict[str, FeatureMetricsTypes],
|
||||||
@ -47,17 +47,15 @@ def manage_recordings(
|
|||||||
},
|
},
|
||||||
timeout=max(60, 10 * len([c for c in config.cameras.values() if c.enabled])),
|
timeout=max(60, 10 * len([c for c in config.cameras.values() if c.enabled])),
|
||||||
)
|
)
|
||||||
models = [Event, Recordings, Timeline, RecordingsToDelete]
|
models = [Event, Recordings]
|
||||||
db.bind(models)
|
db.bind(models)
|
||||||
|
|
||||||
maintainer = RecordingMaintainer(
|
maintainer = RecordingMaintainer(
|
||||||
config,
|
config,
|
||||||
|
inter_process_queue,
|
||||||
object_recordings_info_queue,
|
object_recordings_info_queue,
|
||||||
audio_recordings_info_queue,
|
audio_recordings_info_queue,
|
||||||
process_info,
|
process_info,
|
||||||
stop_event,
|
stop_event,
|
||||||
)
|
)
|
||||||
maintainer.start()
|
maintainer.start()
|
||||||
|
|
||||||
cleanup = RecordingCleanup(config, stop_event)
|
|
||||||
cleanup.start()
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Utilities for services."""
|
"""Utilities for services."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -352,8 +353,8 @@ def vainfo_hwaccel(device_name: Optional[str] = None) -> sp.CompletedProcess:
|
|||||||
return sp.run(ffprobe_cmd, capture_output=True)
|
return sp.run(ffprobe_cmd, capture_output=True)
|
||||||
|
|
||||||
|
|
||||||
def get_video_properties(url, get_duration=False):
|
async def get_video_properties(url, get_duration=False):
|
||||||
def calculate_duration(video: Optional[any]) -> float:
|
async def calculate_duration(video: Optional[any]) -> float:
|
||||||
duration = None
|
duration = None
|
||||||
|
|
||||||
if video is not None:
|
if video is not None:
|
||||||
@ -366,7 +367,7 @@ def get_video_properties(url, get_duration=False):
|
|||||||
|
|
||||||
# if cv2 failed need to use ffprobe
|
# if cv2 failed need to use ffprobe
|
||||||
if duration is None:
|
if duration is None:
|
||||||
ffprobe_cmd = [
|
p = await asyncio.create_subprocess_exec(
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
"-v",
|
"-v",
|
||||||
"error",
|
"error",
|
||||||
@ -375,11 +376,18 @@ def get_video_properties(url, get_duration=False):
|
|||||||
"-of",
|
"-of",
|
||||||
"default=noprint_wrappers=1:nokey=1",
|
"default=noprint_wrappers=1:nokey=1",
|
||||||
f"{url}",
|
f"{url}",
|
||||||
]
|
stdout=asyncio.subprocess.PIPE,
|
||||||
p = sp.run(ffprobe_cmd, capture_output=True)
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
await p.wait()
|
||||||
|
|
||||||
if p.returncode == 0 and p.stdout.decode():
|
if p.returncode == 0:
|
||||||
duration = float(p.stdout.decode().strip())
|
result = (await p.stdout.read()).decode()
|
||||||
|
else:
|
||||||
|
result = None
|
||||||
|
|
||||||
|
if result:
|
||||||
|
duration = float(result.strip())
|
||||||
else:
|
else:
|
||||||
duration = -1
|
duration = -1
|
||||||
|
|
||||||
@ -400,7 +408,7 @@ def get_video_properties(url, get_duration=False):
|
|||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
if get_duration:
|
if get_duration:
|
||||||
result["duration"] = calculate_duration(video)
|
result["duration"] = await calculate_duration(video)
|
||||||
|
|
||||||
if video is not None:
|
if video is not None:
|
||||||
# Get the width of frames in the video stream
|
# Get the width of frames in the video stream
|
||||||
|
Loading…
Reference in New Issue
Block a user