From e640981cc4fca80405376d4453325fa0622f64d1 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Wed, 28 Jun 2023 13:39:39 +0300 Subject: [PATCH 01/33] Performance: multiprocessing improvement (#6936) * Add faster-fifo dependency for improved performance * isort --- Dockerfile | 4 +++- frigate/app.py | 2 +- frigate/events/external.py | 2 +- frigate/events/maintainer.py | 3 ++- frigate/log.py | 2 +- frigate/timeline.py | 3 ++- frigate/types.py | 3 ++- requirements-wheels.txt | 1 + 8 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0c244f901..058bb6ad7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -127,7 +127,9 @@ RUN apt-get -qq update \ libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \ libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \ # scipy dependencies - gcc gfortran libopenblas-dev liblapack-dev && \ + gcc gfortran libopenblas-dev liblapack-dev \ + # faster-fifo dependencies + g++ cython3 && \ rm -rf /var/lib/apt/lists/* RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ diff --git a/frigate/app.py b/frigate/app.py index 9d85f461e..8d3972343 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -6,12 +6,12 @@ import shutil import signal import sys import traceback -from multiprocessing.queues import Queue from multiprocessing.synchronize import Event as MpEvent from types import FrameType from typing import Optional import psutil +from faster_fifo import Queue from peewee_migrate import Router from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqliteq import SqliteQueueDatabase diff --git a/frigate/events/external.py b/frigate/events/external.py index 5422de260..910aee35f 100644 --- a/frigate/events/external.py +++ b/frigate/events/external.py @@ -6,10 +6,10 @@ import logging import os import random import string -from multiprocessing.queues import Queue from typing import Optional import cv2 +from faster_fifo import Queue from frigate.config import CameraConfig, FrigateConfig from frigate.const import CLIPS_DIR diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index 264ab1142..28fb4646b 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -3,10 +3,11 @@ import logging import queue import threading from enum import Enum -from multiprocessing.queues import Queue from multiprocessing.synchronize import Event as MpEvent from typing import Dict +from faster_fifo import Queue + from frigate.config import EventsConfig, FrigateConfig from frigate.models import Event from frigate.types import CameraMetricsTypes diff --git a/frigate/log.py b/frigate/log.py index 5dbf4eed0..ac51fc3da 100644 --- a/frigate/log.py +++ b/frigate/log.py @@ -7,10 +7,10 @@ import signal import threading from collections import deque from logging import handlers -from multiprocessing.queues import Queue from types import FrameType from typing import Deque, Optional +from faster_fifo import Queue from setproctitle import setproctitle from frigate.util import clean_camera_user_pass diff --git a/frigate/timeline.py b/frigate/timeline.py index 9ca617ba9..6cfcbe928 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -3,9 +3,10 @@ import logging import queue import threading -from multiprocessing.queues import Queue from multiprocessing.synchronize import Event as MpEvent +from faster_fifo import Queue + from frigate.config import FrigateConfig from frigate.events.maintainer import EventTypeEnum from frigate.models import Timeline diff --git a/frigate/types.py b/frigate/types.py index 8c3e54654..23751d499 100644 --- a/frigate/types.py +++ b/frigate/types.py @@ -1,8 +1,9 @@ from multiprocessing.context import Process -from multiprocessing.queues import Queue from multiprocessing.sharedctypes import Synchronized from typing import Optional, TypedDict +from faster_fifo import Queue + from frigate.object_detection import ObjectDetectProcess diff --git a/requirements-wheels.txt b/requirements-wheels.txt index f02317e41..19bd1077c 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -1,5 +1,6 @@ click == 8.1.* Flask == 2.3.* +faster-fifo == 1.4.* imutils == 0.5.* matplotlib == 3.7.* mypy == 0.942 From 3d40ed5d474305ea66c515e619900d2a22e472bc Mon Sep 17 00:00:00 2001 From: spacebares <57186372+spacebares@users.noreply.github.com> Date: Wed, 28 Jun 2023 06:45:54 -0400 Subject: [PATCH 02/33] fix tooltip not showing if too far left (#6909) --- web/src/components/Tooltip.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/Tooltip.jsx b/web/src/components/Tooltip.jsx index ba56f20b8..58f723ac5 100644 --- a/web/src/components/Tooltip.jsx +++ b/web/src/components/Tooltip.jsx @@ -29,7 +29,7 @@ export default function Tooltip({ relativeTo, text }) { let newLeft = left - Math.round(tipWidth / 2); // too far right if (newLeft + tipWidth + TIP_SPACE > windowWidth - window.scrollX) { - newLeft = left - tipWidth - TIP_SPACE; + newLeft = Math.max(0, left - tipWidth - TIP_SPACE); newTop = top - Math.round(tipHeight / 2); } // too far left From ece070fee1359ea00cbe52a4b39a2308d34801e8 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 28 Jun 2023 04:51:53 -0600 Subject: [PATCH 03/33] Load labels dynamically for event filters (#6896) * Load labels dynamically to include custom events and audio, do not include attribute labels * Formatting * Fix sorting * Also filter tracked object list on camera page * isort * Don't fail before load --- frigate/const.py | 10 ++++++++++ frigate/http.py | 18 ++++++++++++++++++ frigate/video.py | 14 +++----------- web/src/routes/Camera.jsx | 7 +++++-- web/src/routes/Events.jsx | 10 +++------- 5 files changed, 39 insertions(+), 20 deletions(-) diff --git a/frigate/const.py b/frigate/const.py index 9b7e177f2..28ce766e4 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -12,6 +12,16 @@ PLUS_ENV_VAR = "PLUS_API_KEY" PLUS_API_HOST = "https://api.frigate.video" BTBN_PATH = "/usr/lib/btbn-ffmpeg" +# Attributes + +ATTRIBUTE_LABEL_MAP = { + "person": ["face", "amazon"], + "car": ["ups", "fedex", "amazon", "license_plate"], +} +ALL_ATTRIBUTE_LABELS = [ + item for sublist in ATTRIBUTE_LABEL_MAP.values() for item in sublist +] + # Regex Consts REGEX_CAMERA_NAME = r"^[a-zA-Z0-9_-]+$" diff --git a/frigate/http.py b/frigate/http.py index b4813c1f2..44706f2c2 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -410,6 +410,24 @@ def set_sub_label(id): ) +@bp.route("/labels") +def get_labels(): + camera = request.args.get("camera", type=str, default="") + + try: + if camera: + events = Event.select(Event.label).where(Event.camera == camera).distinct() + else: + events = Event.select(Event.label).distinct() + except Exception as e: + return jsonify( + {"success": False, "message": f"Failed to get labels: {e}"}, "404" + ) + + labels = sorted([e.label for e in events]) + return jsonify(labels) + + @bp.route("/sub_labels") def get_sub_labels(): split_joined = request.args.get("split_joined", type=int) diff --git a/frigate/video.py b/frigate/video.py index c02ad15c4..f60bce1d9 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -15,7 +15,7 @@ import numpy as np from setproctitle import setproctitle from frigate.config import CameraConfig, DetectConfig -from frigate.const import CACHE_DIR +from frigate.const import ALL_ATTRIBUTE_LABELS, ATTRIBUTE_LABEL_MAP, CACHE_DIR from frigate.detectors.detector_config import PixelFormatEnum from frigate.log import LogPipe from frigate.motion import MotionDetector @@ -723,14 +723,6 @@ def process_frames( stop_event, exit_on_empty: bool = False, ): - # attribute labels are not tracked and are not assigned regions - attribute_label_map = { - "person": ["face", "amazon"], - "car": ["ups", "fedex", "amazon", "license_plate"], - } - all_attribute_labels = [ - item for sublist in attribute_label_map.values() for item in sublist - ] fps = process_info["process_fps"] detection_fps = process_info["detection_fps"] current_frame_time = process_info["detection_frame"] @@ -906,7 +898,7 @@ def process_frames( tracked_detections = [ d for d in consolidated_detections - if d[0] not in all_attribute_labels + if d[0] not in ALL_ATTRIBUTE_LABELS ] # now that we have refined our detections, we need to track objects object_tracker.match_and_update(frame_time, tracked_detections) @@ -916,7 +908,7 @@ def process_frames( # group the attribute detections based on what label they apply to attribute_detections = {} - for label, attribute_labels in attribute_label_map.items(): + for label, attribute_labels in ATTRIBUTE_LABEL_MAP.items(): attribute_detections[label] = [ d for d in consolidated_detections if d[0] in attribute_labels ] diff --git a/web/src/routes/Camera.jsx b/web/src/routes/Camera.jsx index 7f5e3ca77..11ae2cdea 100644 --- a/web/src/routes/Camera.jsx +++ b/web/src/routes/Camera.jsx @@ -22,6 +22,7 @@ const emptyObject = Object.freeze({}); export default function Camera({ camera }) { const { data: config } = useSWR('config'); + const { data: trackedLabels } = useSWR(['labels', { camera }]); const apiHost = useApiHost(); const [showSettings, setShowSettings] = useState(false); const [viewMode, setViewMode] = useState('live'); @@ -121,7 +122,9 @@ export default function Camera({ camera }) {
@@ -203,7 +206,7 @@ export default function Camera({ camera }) {
Tracked objects
- {cameraConfig.objects.track.map((objectType) => ( + {(trackedLabels || []).map((objectType) => ( self.indexOf(value) === i), 'None', ], - labels: Object.values(config?.cameras || {}) - .reduce((memo, camera) => { - memo = memo.concat(camera?.objects?.track || []); - return memo; - }, config?.objects?.track || []) - .filter((value, i, self) => self.indexOf(value) === i), + labels: Object.values(allLabels || {}), sub_labels: (allSubLabels || []).length > 0 ? [...Object.values(allSubLabels), 'None'] : [], }), - [config, allSubLabels] + [config, allLabels, allSubLabels] ); const onSave = async (e, eventId, save) => { From ee4a1336558eb3711ee42158a138d54f5fe8c712 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Wed, 28 Jun 2023 04:53:28 -0600 Subject: [PATCH 04/33] actually keep track of skipped frames (#6889) * actually keep track of skipped frames fixes #6863 * fix black errors --- frigate/video.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/frigate/video.py b/frigate/video.py index f60bce1d9..4f315885c 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -172,7 +172,7 @@ def capture_frames( skipped_eps.start() while True: fps.value = frame_rate.eps() - skipped_eps.eps() + skipped_fps.value = skipped_eps.eps() current_frame.value = datetime.datetime.now().timestamp() frame_name = f"{camera_name}{current_frame.value}" @@ -215,6 +215,7 @@ class CameraWatchdog(threading.Thread): config: CameraConfig, frame_queue, camera_fps, + skipped_fps, ffmpeg_pid, stop_event, ): @@ -227,6 +228,7 @@ class CameraWatchdog(threading.Thread): self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect") self.ffmpeg_other_processes: list[dict[str, any]] = [] self.camera_fps = camera_fps + self.skipped_fps = skipped_fps self.ffmpeg_pid = ffmpeg_pid self.frame_queue = frame_queue self.frame_shape = self.config.frame_shape_yuv @@ -346,6 +348,7 @@ class CameraWatchdog(threading.Thread): self.frame_shape, self.frame_queue, self.camera_fps, + self.skipped_fps, self.stop_event, ) self.capture_thread.start() @@ -376,7 +379,14 @@ class CameraWatchdog(threading.Thread): class CameraCapture(threading.Thread): def __init__( - self, camera_name, ffmpeg_process, frame_shape, frame_queue, fps, stop_event + self, + camera_name, + ffmpeg_process, + frame_shape, + frame_queue, + fps, + skipped_fps, + stop_event, ): threading.Thread.__init__(self) self.name = f"capture:{camera_name}" @@ -385,14 +395,13 @@ class CameraCapture(threading.Thread): self.frame_queue = frame_queue self.fps = fps self.stop_event = stop_event - self.skipped_fps = EventsPerSecond() + self.skipped_fps = skipped_fps self.frame_manager = SharedMemoryFrameManager() self.ffmpeg_process = ffmpeg_process self.current_frame = mp.Value("d", 0.0) self.last_frame = 0 def run(self): - self.skipped_fps.start() capture_frames( self.ffmpeg_process, self.camera_name, @@ -424,6 +433,7 @@ def capture_camera(name, config: CameraConfig, process_info): config, frame_queue, process_info["camera_fps"], + process_info["skipped_fps"], process_info["ffmpeg_pid"], stop_event, ) From ef14a43930636ec9d87efda9a6a5b527f96a513c Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Wed, 28 Jun 2023 04:55:53 -0600 Subject: [PATCH 05/33] optimize frame-per-second calculations (#6887) * optimize frame-per-second calculations FPS is never calculated over differing periods for even given counter. By only storing timestamps within the period that is used, we avoid having to iterate 1000 entries every time it's re-calculated (multiple times per second). 1000 entries would normally only be needed if something is running at 100fps. A more common speed - anywhere from 5 to 30fps, only needs 50 to 300 entries. This isn't a huge improvement in the grand scheme, but when motion detection is disabled, it takes nearly as much time in a flamegraph as actually transferring the video frame. * fix python checks --- frigate/util.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/frigate/util.py b/frigate/util.py index aa19e99fa..f535a9572 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -650,34 +650,42 @@ def restart_frigate(): class EventsPerSecond: - def __init__(self, max_events=1000): + def __init__(self, max_events=1000, last_n_seconds=10): self._start = None self._max_events = max_events + self._last_n_seconds = last_n_seconds self._timestamps = [] def start(self): self._start = datetime.datetime.now().timestamp() def update(self): + now = datetime.datetime.now().timestamp() if self._start is None: - self.start() - self._timestamps.append(datetime.datetime.now().timestamp()) + self._start = now + self._timestamps.append(now) # truncate the list when it goes 100 over the max_size if len(self._timestamps) > self._max_events + 100: self._timestamps = self._timestamps[(1 - self._max_events) :] + self.expire_timestamps(now) - def eps(self, last_n_seconds=10): - if self._start is None: - self.start() - # compute the (approximate) events in the last n seconds + def eps(self): now = datetime.datetime.now().timestamp() - seconds = min(now - self._start, last_n_seconds) + if self._start is None: + self._start = now + # compute the (approximate) events in the last n seconds + self.expire_timestamps(now) + seconds = min(now - self._start, self._last_n_seconds) # avoid divide by zero if seconds == 0: seconds = 1 - return ( - len([t for t in self._timestamps if t > (now - last_n_seconds)]) / seconds - ) + return len(self._timestamps) / seconds + + # remove aged out timestamps + def expire_timestamps(self, now): + threshold = now - self._last_n_seconds + while self._timestamps and self._timestamps[0] < threshold: + del self._timestamps[0] def print_stack(sig, frame): From 9137f1594b11a60f9696cc52318862b2c3b08507 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 30 Jun 2023 06:13:00 -0600 Subject: [PATCH 06/33] Remove legacy recordings file cleanup (#6947) * Remove legacy recordings file cleanup * Remove unused --- frigate/const.py | 1 - frigate/record/cleanup.py | 64 +++++---------------------------------- 2 files changed, 7 insertions(+), 58 deletions(-) diff --git a/frigate/const.py b/frigate/const.py index 28ce766e4..c1524a6a8 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -38,5 +38,4 @@ DRIVER_INTEL_iHD = "iHD" # Record Values MAX_SEGMENT_DURATION = 600 -SECONDS_IN_DAY = 60 * 60 * 24 MAX_PLAYLIST_SECONDS = 7200 # support 2 hour segments for a single playlist to account for cameras with inconsistent segment times diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py index bb5022c93..a48b3fef7 100644 --- a/frigate/record/cleanup.py +++ b/frigate/record/cleanup.py @@ -8,10 +8,10 @@ import threading from multiprocessing.synchronize import Event as MpEvent from pathlib import Path -from peewee import DatabaseError, DoesNotExist, chunked +from peewee import DatabaseError, chunked from frigate.config import FrigateConfig, RetainModeEnum -from frigate.const import RECORD_DIR, SECONDS_IN_DAY +from frigate.const import RECORD_DIR from frigate.models import Event, Recordings, RecordingsToDelete, Timeline from frigate.record.util import remove_empty_directories @@ -28,7 +28,7 @@ class RecordingCleanup(threading.Thread): self.stop_event = stop_event def clean_tmp_clips(self) -> None: - # delete any clips more than 5 minutes old + """delete any clips in the cache that are more than 5 minutes old.""" for p in Path("/tmp/cache").rglob("clip_*.mp4"): logger.debug(f"Checking tmp clip {p}.") if p.stat().st_mtime < (datetime.datetime.now().timestamp() - 60 * 1): @@ -40,8 +40,8 @@ class RecordingCleanup(threading.Thread): p.unlink(missing_ok=True) def expire_recordings(self) -> None: - logger.debug("Start expire recordings (new).") - + """Delete recordings based on retention config.""" + logger.debug("Start expire recordings.") logger.debug("Start deleted cameras.") # Handle deleted cameras expire_days = self.config.record.retain.days @@ -161,59 +161,10 @@ class RecordingCleanup(threading.Thread): logger.debug(f"End camera: {camera}.") logger.debug("End all cameras.") - logger.debug("End expire recordings (new).") - - def expire_files(self) -> None: - logger.debug("Start expire files (legacy).") - - default_expire = ( - datetime.datetime.now().timestamp() - - SECONDS_IN_DAY * self.config.record.retain.days - ) - delete_before = {} - - for name, camera in self.config.cameras.items(): - delete_before[name] = ( - datetime.datetime.now().timestamp() - - SECONDS_IN_DAY * camera.record.retain.days - ) - - # find all the recordings older than the oldest recording in the db - try: - oldest_recording = ( - Recordings.select().order_by(Recordings.start_time).limit(1).get() - ) - - p = Path(oldest_recording.path) - oldest_timestamp = p.stat().st_mtime - 1 - except DoesNotExist: - oldest_timestamp = datetime.datetime.now().timestamp() - except FileNotFoundError: - logger.warning(f"Unable to find file from recordings database: {p}") - Recordings.delete().where(Recordings.id == oldest_recording.id).execute() - return - - logger.debug(f"Oldest recording in the db: {oldest_timestamp}") - - files_to_check = [] - - for root, _, files in os.walk(RECORD_DIR): - for file in files: - file_path = os.path.join(root, file) - if os.path.getmtime(file_path) < oldest_timestamp: - files_to_check.append(file_path) - - for f in files_to_check: - p = Path(f) - try: - if p.stat().st_mtime < delete_before.get(p.parent.name, default_expire): - p.unlink(missing_ok=True) - except FileNotFoundError: - logger.warning(f"Attempted to expire missing file: {f}") - - logger.debug("End expire files (legacy).") + logger.debug("End expire recordings.") def sync_recordings(self) -> None: + """Check the db for stale recordings entries that don't exist in the filesystem.""" logger.debug("Start sync recordings.") # get all recordings in the db @@ -283,5 +234,4 @@ class RecordingCleanup(threading.Thread): if counter == 0: self.expire_recordings() - self.expire_files() remove_empty_directories(RECORD_DIR) From ed0d2be321b75e0d8fb51b40e4296159893ad33d Mon Sep 17 00:00:00 2001 From: spacebares <57186372+spacebares@users.noreply.github.com> Date: Fri, 30 Jun 2023 08:14:39 -0400 Subject: [PATCH 07/33] configurable ffmpeg timeout (#6897) * configurable ffmpeg timeout * configurable ffmpeg healthcheck interval rename timeout to healthcheck_interval only grab config value once * configurable ffmpeg retry interval rename healthcheck_interval to retry_interval * add retry_interval to docs - update retry_interval text in config.py --- docs/docs/configuration/index.md | 5 +++++ frigate/config.py | 4 ++++ frigate/video.py | 5 +++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index ac65a1018..496247f37 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -189,6 +189,11 @@ ffmpeg: record: preset-record-generic # Optional: output args for rtmp streams (default: shown below) rtmp: preset-rtmp-generic + # Optional: Time in seconds to wait before ffmpeg retries connecting to the camera. (default: shown below) + # If set too low, frigate will retry a connection to the camera's stream too frequently, using up the limited streams some cameras can allow at once + # If set too high, then if a ffmpeg crash or camera stream timeout occurs, you could potentially lose up to a maximum of retry_interval second(s) of footage + # NOTE: this can be a useful setting for Wireless / Battery cameras to reduce how much footage is potentially lost during a connection timeout. + retry_interval: 10 # Optional: Detect configuration # NOTE: Can be overridden at the camera level diff --git a/frigate/config.py b/frigate/config.py index b71ba1907..4bc7e1a01 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -463,6 +463,10 @@ class FfmpegConfig(FrigateBaseModel): default_factory=FfmpegOutputArgsConfig, title="FFmpeg output arguments per role.", ) + retry_interval: float = Field( + default=10.0, + title="Time in seconds to wait before FFmpeg retries connecting to the camera.", + ) class CameraRoleEnum(str, Enum): diff --git a/frigate/video.py b/frigate/video.py index 4f315885c..8980fcde0 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -234,6 +234,7 @@ class CameraWatchdog(threading.Thread): self.frame_shape = self.config.frame_shape_yuv self.frame_size = self.frame_shape[0] * self.frame_shape[1] self.stop_event = stop_event + self.sleeptime = self.config.ffmpeg.retry_interval def run(self): self.start_ffmpeg_detect() @@ -253,8 +254,8 @@ class CameraWatchdog(threading.Thread): } ) - time.sleep(10) - while not self.stop_event.wait(10): + time.sleep(self.sleeptime) + while not self.stop_event.wait(self.sleeptime): now = datetime.datetime.now().timestamp() if not self.capture_thread.is_alive(): From bd17e7d824af9fa8a26f3098dbb063f0a75294cb Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 30 Jun 2023 06:15:19 -0600 Subject: [PATCH 08/33] Show object attributes when hovering or tapping timeline bounding box (#6879) * Show overlay when hovering on timeline box * Make it work with mobile too * Fix lint * Fix ratio * Update web/src/components/TimelineEventOverlay.jsx Co-authored-by: Blake Blackshear * Show overlay when hovering on timeline box * Make it work with mobile too * Fix lint * Fix ratio * Remove label and make overlay respect selected theme --------- Co-authored-by: Blake Blackshear --- web/src/components/TimelineEventOverlay.jsx | 65 +++++++++++++++++++++ web/src/routes/Events.jsx | 22 ++----- 2 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 web/src/components/TimelineEventOverlay.jsx diff --git a/web/src/components/TimelineEventOverlay.jsx b/web/src/components/TimelineEventOverlay.jsx new file mode 100644 index 000000000..717b3809a --- /dev/null +++ b/web/src/components/TimelineEventOverlay.jsx @@ -0,0 +1,65 @@ +import { Fragment, h } from 'preact'; +import { useState } from 'preact/hooks'; + +export default function TimelineEventOverlay({ eventOverlay, cameraConfig }) { + const boxLeftEdge = Math.round(eventOverlay.data.box[0] * 100); + const boxTopEdge = Math.round(eventOverlay.data.box[1] * 100); + const boxRightEdge = Math.round((1 - eventOverlay.data.box[2] - eventOverlay.data.box[0]) * 100); + const boxBottomEdge = Math.round((1 - eventOverlay.data.box[3] - eventOverlay.data.box[1]) * 100); + + const [isHovering, setIsHovering] = useState(false); + const getHoverStyle = () => { + if (boxLeftEdge < 15) { + // show object stats on right side + return { + left: `${boxLeftEdge + eventOverlay.data.box[2] * 100 + 1}%`, + top: `${boxTopEdge}%`, + }; + } + + return { + right: `${boxRightEdge + eventOverlay.data.box[2] * 100 + 1}%`, + top: `${boxTopEdge}%`, + }; + }; + + const getObjectArea = () => { + const width = eventOverlay.data.box[2] * cameraConfig.detect.width; + const height = eventOverlay.data.box[3] * cameraConfig.detect.height; + return Math.round(width * height); + }; + + const getObjectRatio = () => { + const width = eventOverlay.data.box[2] * cameraConfig.detect.width; + const height = eventOverlay.data.box[3] * cameraConfig.detect.height; + return Math.round(100 * (width / height)) / 100; + }; + + return ( + +
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + onTouchStart={() => setIsHovering(true)} + onTouchEnd={() => setIsHovering(false)} + style={{ + left: `${boxLeftEdge}%`, + top: `${boxTopEdge}%`, + right: `${boxRightEdge}%`, + bottom: `${boxBottomEdge}%`, + }} + > + {eventOverlay.class_type == 'entered_zone' ? ( +
+ ) : null} +
+ {isHovering && ( +
+
{`Area: ${getObjectArea()} px`}
+
{`Ratio: ${getObjectRatio()}`}
+
+ )} + + ); +} diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index 8de35413b..9d503cb8b 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -29,6 +29,7 @@ import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../uti import TimeAgo from '../components/TimeAgo'; import Timepicker from '../components/TimePicker'; import TimelineSummary from '../components/TimelineSummary'; +import TimelineEventOverlay from '../components/TimelineEventOverlay'; const API_LIMIT = 25; @@ -717,23 +718,10 @@ export default function Events({ path, ...props }) { }} > {eventOverlay ? ( -
- {eventOverlay.class_type == 'entered_zone' ? ( -
- ) : null} -
+ ) : null}
From d2a2643cd6bd5c576313a97ebb50c1a0c1098ce0 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 30 Jun 2023 06:15:55 -0600 Subject: [PATCH 09/33] Apply zone filter before inertia (#6854) * Apply zone filter before intertia * Formatting --- frigate/object_processing.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frigate/object_processing.py b/frigate/object_processing.py index 6d31c3cdd..e69210cce 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -188,15 +188,14 @@ class TrackedObject: zone_score = self.zone_presence.get(name, 0) # check if the object is in the zone if cv2.pointPolygonTest(contour, bottom_center, False) >= 0: - self.zone_presence[name] = zone_score + 1 + # if the object passed the filters once, dont apply again + if name in self.current_zones or not zone_filtered(self, zone.filters): + self.zone_presence[name] = zone_score + 1 - # an object is only considered present in a zone if it has a zone inertia of 3+ - if zone_score >= zone.inertia: - # if the object passed the filters once, dont apply again - if name in self.current_zones or not zone_filtered( - self, zone.filters - ): + # an object is only considered present in a zone if it has a zone inertia of 3+ + if zone_score >= zone.inertia: current_zones.append(name) + if name not in self.entered_zones: self.entered_zones.append(name) else: From d51197eaa2088bcc63cc2fb1383e3272ac1ecbfa Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Fri, 30 Jun 2023 07:27:31 -0500 Subject: [PATCH 10/33] use a different method for blur and contrast to reduce CPU (#6940) * use a different method for blur and contrast to reduce CPU * blur with radius instead * use faster interpolation for motion * improve contrast based on averages * increase default threshold to 30 * ensure mask is applied after contrast improvement * update opencv * update benchmark script --- benchmark_motion.py | 29 ++++++++++++++----- docs/docs/configuration/index.md | 2 +- frigate/config.py | 2 +- frigate/motion/improved_motion.py | 48 ++++++++++++++++++++++--------- requirements-wheels.txt | 2 +- 5 files changed, 60 insertions(+), 23 deletions(-) diff --git a/benchmark_motion.py b/benchmark_motion.py index 167770280..431398f98 100644 --- a/benchmark_motion.py +++ b/benchmark_motion.py @@ -12,16 +12,32 @@ from frigate.util import create_mask # get info on the video # cap = cv2.VideoCapture("debug/front_cam_2023_05_23_08_41__2023_05_23_08_43.mp4") # cap = cv2.VideoCapture("debug/motion_test_clips/rain_1.mp4") -cap = cv2.VideoCapture("debug/motion_test_clips/ir_off.mp4") +cap = cv2.VideoCapture("debug/motion_test_clips/lawn_mower_night_1.mp4") # cap = cv2.VideoCapture("airport.mp4") width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = cap.get(cv2.CAP_PROP_FPS) frame_shape = (height, width, 3) +# Nick back: +# "1280,0,1280,316,1170,216,1146,126,1016,127,979,82,839,0", +# "310,350,300,402,224,405,241,354", +# "378,0,375,26,0,23,0,0", +# Front door: +# "1080,0,1080,339,1010,280,1020,169,777,163,452,170,318,299,191,365,186,417,139,470,108,516,40,530,0,514,0,0", +# "336,833,438,1024,346,1093,103,1052,24,814", +# Back +# "1855,0,1851,100,1289,96,1105,161,1045,119,890,121,890,0", +# "505,95,506,138,388,153,384,114", +# "689,72,689,122,549,134,547,89", +# "261,134,264,176,169,195,167,158", +# "145,159,146,202,70,220,65,183", mask = create_mask( (height, width), - [], + [ + "1080,0,1080,339,1010,280,1020,169,777,163,452,170,318,299,191,365,186,417,139,470,108,516,40,530,0,514,0,0", + "336,833,438,1024,346,1093,103,1052,24,814", + ], ) # create the motion config @@ -29,7 +45,7 @@ motion_config_1 = MotionConfig() motion_config_1.mask = np.zeros((height, width), np.uint8) motion_config_1.mask[:] = mask # motion_config_1.improve_contrast = 1 -# motion_config_1.frame_height = 150 +motion_config_1.frame_height = 150 # motion_config_1.frame_alpha = 0.02 # motion_config_1.threshold = 30 # motion_config_1.contour_area = 10 @@ -38,10 +54,11 @@ motion_config_2 = MotionConfig() motion_config_2.mask = np.zeros((height, width), np.uint8) motion_config_2.mask[:] = mask # motion_config_2.improve_contrast = 1 -# motion_config_2.frame_height = 150 +motion_config_2.frame_height = 150 # motion_config_2.frame_alpha = 0.01 -# motion_config_2.threshold = 20 +motion_config_2.threshold = 20 # motion_config.contour_area = 10 + save_images = True improved_motion_detector_1 = ImprovedMotionDetector( @@ -52,8 +69,6 @@ improved_motion_detector_1 = ImprovedMotionDetector( threshold=mp.Value("i", motion_config_1.threshold), contour_area=mp.Value("i", motion_config_1.contour_area), name="default", - clipLimit=2.0, - tileGridSize=(8, 8), ) improved_motion_detector_1.save_images = save_images diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 496247f37..7279a7652 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -280,7 +280,7 @@ motion: # Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below) # Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive. # The value should be between 1 and 255. - threshold: 20 + threshold: 30 # Optional: The percentage of the image used to detect lightning or other substantial changes where motion detection # needs to recalibrate. (default: shown below) # Increasing this value will make motion detection more likely to consider lightning or ir mode changes as valid motion. diff --git a/frigate/config.py b/frigate/config.py index 4bc7e1a01..662b7b8bd 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -187,7 +187,7 @@ class RecordConfig(FrigateBaseModel): class MotionConfig(FrigateBaseModel): threshold: int = Field( - default=20, + default=30, title="Motion detection threshold (1-255).", ge=1, le=255, diff --git a/frigate/motion/improved_motion.py b/frigate/motion/improved_motion.py index 525854b82..b281cbbeb 100644 --- a/frigate/motion/improved_motion.py +++ b/frigate/motion/improved_motion.py @@ -1,6 +1,7 @@ import cv2 import imutils import numpy as np +from scipy.ndimage import gaussian_filter from frigate.config import MotionConfig from frigate.motion import MotionDetector @@ -15,9 +16,10 @@ class ImprovedMotionDetector(MotionDetector): improve_contrast, threshold, contour_area, - clipLimit=2.0, - tileGridSize=(2, 2), name="improved", + blur_radius=1, + interpolation=cv2.INTER_NEAREST, + contrast_frame_history=50, ): self.name = name self.config = config @@ -28,13 +30,12 @@ class ImprovedMotionDetector(MotionDetector): config.frame_height * frame_shape[1] // frame_shape[0], ) self.avg_frame = np.zeros(self.motion_frame_size, np.float32) - self.avg_delta = np.zeros(self.motion_frame_size, np.float32) self.motion_frame_count = 0 self.frame_counter = 0 resized_mask = cv2.resize( config.mask, dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), - interpolation=cv2.INTER_LINEAR, + interpolation=cv2.INTER_AREA, ) self.mask = np.where(resized_mask == [0]) self.save_images = False @@ -42,7 +43,11 @@ class ImprovedMotionDetector(MotionDetector): self.improve_contrast = improve_contrast self.threshold = threshold self.contour_area = contour_area - self.clahe = cv2.createCLAHE(clipLimit=clipLimit, tileGridSize=tileGridSize) + self.blur_radius = blur_radius + self.interpolation = interpolation + self.contrast_values = np.zeros((contrast_frame_history, 2), np.uint8) + self.contrast_values[:, 1:2] = 255 + self.contrast_values_index = 0 def detect(self, frame): motion_boxes = [] @@ -53,27 +58,44 @@ class ImprovedMotionDetector(MotionDetector): resized_frame = cv2.resize( gray, dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), - interpolation=cv2.INTER_LINEAR, + interpolation=self.interpolation, ) if self.save_images: resized_saved = resized_frame.copy() - resized_frame = cv2.GaussianBlur(resized_frame, (3, 3), cv2.BORDER_DEFAULT) - - if self.save_images: - blurred_saved = resized_frame.copy() - # Improve contrast if self.improve_contrast.value: - resized_frame = self.clahe.apply(resized_frame) + # TODO tracking moving average of min/max to avoid sudden contrast changes + minval = np.percentile(resized_frame, 4).astype(np.uint8) + maxval = np.percentile(resized_frame, 96).astype(np.uint8) + # skip contrast calcs if the image is a single color + if minval < maxval: + # keep track of the last 50 contrast values + self.contrast_values[self.contrast_values_index] = [minval, maxval] + self.contrast_values_index += 1 + if self.contrast_values_index == len(self.contrast_values): + self.contrast_values_index = 0 + + avg_min, avg_max = np.mean(self.contrast_values, axis=0) + + resized_frame = np.clip(resized_frame, avg_min, avg_max) + resized_frame = ( + ((resized_frame - avg_min) / (avg_max - avg_min)) * 255 + ).astype(np.uint8) if self.save_images: contrasted_saved = resized_frame.copy() # mask frame + # this has to come after contrast improvement resized_frame[self.mask] = [255] + resized_frame = gaussian_filter(resized_frame, sigma=1, radius=self.blur_radius) + + if self.save_images: + blurred_saved = resized_frame.copy() + if self.save_images or self.calibrating: self.frame_counter += 1 # compare to average @@ -134,8 +156,8 @@ class ImprovedMotionDetector(MotionDetector): ) frames = [ cv2.cvtColor(resized_saved, cv2.COLOR_GRAY2BGR), - cv2.cvtColor(blurred_saved, cv2.COLOR_GRAY2BGR), cv2.cvtColor(contrasted_saved, cv2.COLOR_GRAY2BGR), + cv2.cvtColor(blurred_saved, cv2.COLOR_GRAY2BGR), cv2.cvtColor(frameDelta, cv2.COLOR_GRAY2BGR), cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR), thresh_dilated, diff --git a/requirements-wheels.txt b/requirements-wheels.txt index 19bd1077c..00cedf9cd 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -6,7 +6,7 @@ matplotlib == 3.7.* mypy == 0.942 numpy == 1.23.* onvif_zeep == 0.2.12 -opencv-python-headless == 4.5.5.* +opencv-python-headless == 4.7.0.* paho-mqtt == 1.6.* peewee == 3.16.* peewee_migrate == 1.10.* From b6fce8f0bb715fb7006a0ea57e1d7afc5e021788 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Fri, 30 Jun 2023 15:28:48 +0300 Subject: [PATCH 11/33] Integrate ccache into libusb and nginx build scripts for improved build speed (#6886) * Add ccache to libusb and nginx build scripts * Add ccache support to Dockerfile for faster builds * Add ccache to PATH and use it for compiling nginx with Makefile in build_nginx.sh script --- Dockerfile | 11 ++++++++--- docker/build_nginx.sh | 6 +++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 058bb6ad7..6db81e46b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,10 +18,13 @@ WORKDIR /rootfs FROM base AS nginx ARG DEBIAN_FRONTEND +ENV CCACHE_DIR /root/.ccache +ENV CCACHE_MAXSIZE 2G # bind /var/cache/apt to tmpfs to speed up nginx build RUN --mount=type=tmpfs,target=/tmp --mount=type=tmpfs,target=/var/cache/apt \ --mount=type=bind,source=docker/build_nginx.sh,target=/deps/build_nginx.sh \ + --mount=type=cache,target=/root/.ccache \ /deps/build_nginx.sh FROM wget AS go2rtc @@ -61,14 +64,16 @@ RUN mkdir /models \ FROM wget as libusb-build ARG TARGETARCH ARG DEBIAN_FRONTEND +ENV CCACHE_DIR /root/.ccache +ENV CCACHE_MAXSIZE 2G # Build libUSB without udev. Needed for Openvino NCS2 support WORKDIR /opt -RUN apt-get update && apt-get install -y unzip build-essential automake libtool -RUN wget -q https://github.com/libusb/libusb/archive/v1.0.25.zip -O v1.0.25.zip && \ +RUN apt-get update && apt-get install -y unzip build-essential automake libtool ccache +RUN --mount=type=cache,target=/root/.ccache wget -q https://github.com/libusb/libusb/archive/v1.0.25.zip -O v1.0.25.zip && \ unzip v1.0.25.zip && cd libusb-1.0.25 && \ ./bootstrap.sh && \ - ./configure --disable-udev --enable-shared && \ + ./configure CC='ccache gcc' CCX='ccache g++' --disable-udev --enable-shared && \ make -j $(nproc --all) RUN apt-get update && \ apt-get install -y --no-install-recommends libusb-1.0-0-dev && \ diff --git a/docker/build_nginx.sh b/docker/build_nginx.sh index fd1432f32..1e7bfad21 100755 --- a/docker/build_nginx.sh +++ b/docker/build_nginx.sh @@ -15,6 +15,10 @@ apt-get -yqq build-dep nginx apt-get -yqq install --no-install-recommends ca-certificates wget update-ca-certificates -f +apt install -y ccache + +export PATH="/usr/lib/ccache:$PATH" + mkdir /tmp/nginx wget -nv https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz tar -zxf nginx-${NGINX_VERSION}.tar.gz -C /tmp/nginx --strip-components=1 @@ -62,5 +66,5 @@ cd /tmp/nginx --add-module=../nginx-rtmp-module \ --with-cc-opt="-O3 -Wno-error=implicit-fallthrough" -make -j$(nproc) && make install +make CC="ccache gcc" -j$(nproc) && make install rm -rf /usr/local/nginx/html /usr/local/nginx/conf/*.default From 0a8249d6fb11e60091eb553a733c10e8339395ae Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 30 Jun 2023 06:34:10 -0600 Subject: [PATCH 12/33] Fix Bad Resize For Camera Snapshot (#6797) * Catch cases where incorrect size is requested * Set a default if calculated height is incorrect --- frigate/http.py | 9 +++++++ web/src/components/CameraImage.jsx | 41 ++++++++++++++++-------------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/frigate/http.py b/frigate/http.py index 44706f2c2..9c55d68c4 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -1118,6 +1118,15 @@ def latest_frame(camera_name): height = int(request.args.get("h", str(frame.shape[0]))) width = int(height * frame.shape[1] / frame.shape[0]) + if not frame: + return "Unable to get valid frame from {}".format(camera_name), 500 + + if height < 1 or width < 1: + return ( + "Invalid height / width requested :: {} / {}".format(height, width), + 400, + ) + frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA) ret, jpg = cv2.imencode( diff --git a/web/src/components/CameraImage.jsx b/web/src/components/CameraImage.jsx index 98754e506..ce4e4bef7 100644 --- a/web/src/components/CameraImage.jsx +++ b/web/src/components/CameraImage.jsx @@ -28,13 +28,18 @@ export default function CameraImage({ camera, onload, searchParams = '', stretch const scaledHeight = useMemo(() => { const scaledHeight = Math.floor(availableWidth / aspectRatio); - return stretch ? scaledHeight : Math.min(scaledHeight, height); + const finalHeight = stretch ? scaledHeight : Math.min(scaledHeight, height); + + if (finalHeight > 0) { + return finalHeight; + } + + return 100; }, [availableWidth, aspectRatio, height, stretch]); - const scaledWidth = useMemo(() => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), [ - scaledHeight, - aspectRatio, - scrollBarWidth, - ]); + const scaledWidth = useMemo( + () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), + [scaledHeight, aspectRatio, scrollBarWidth] + ); const img = useMemo(() => new Image(), []); img.onload = useCallback( @@ -58,18 +63,16 @@ export default function CameraImage({ camera, onload, searchParams = '', stretch return (
- { - (enabled) ? - - :
Camera is disabled in config, no stream or snapshot available!
- } - { - (!hasLoaded && enabled) ? ( -
- -
- ) : null - } -
+ {enabled ? ( + + ) : ( +
Camera is disabled in config, no stream or snapshot available!
+ )} + {!hasLoaded && enabled ? ( +
+ +
+ ) : null} +
); } From f3f9b36e07ba81ffa4744ca5e9a4a22634631a31 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 30 Jun 2023 07:06:38 -0600 Subject: [PATCH 13/33] Fix bad check on np.array (#6968) --- frigate/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frigate/http.py b/frigate/http.py index 9c55d68c4..e47e6efc2 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -1118,7 +1118,7 @@ def latest_frame(camera_name): height = int(request.args.get("h", str(frame.shape[0]))) width = int(height * frame.shape[1] / frame.shape[0]) - if not frame: + if frame is None: return "Unable to get valid frame from {}".format(camera_name), 500 if height < 1 or width < 1: From 2f401bd8da33ce7d8f769a02b7f25b7728e7952d Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sat, 1 Jul 2023 07:47:16 -0500 Subject: [PATCH 14/33] update deps (#6973) * update web deps * update python deps --- frigate/app.py | 67 ++- frigate/stats.py | 8 +- frigate/watchdog.py | 4 +- requirements-wheels.txt | 4 +- web/index.html | 2 +- web/package-lock.json | 955 +++++++++++++++++--------------------- web/package.json | 6 +- web/src/routes/Export.jsx | 43 +- 8 files changed, 530 insertions(+), 559 deletions(-) diff --git a/frigate/app.py b/frigate/app.py index 8d3972343..0b476cd43 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -107,33 +107,64 @@ class FrigateApp: for camera_name in self.config.cameras.keys(): # create camera_metrics self.camera_metrics[camera_name] = { - "camera_fps": mp.Value("d", 0.0), - "skipped_fps": mp.Value("d", 0.0), - "process_fps": mp.Value("d", 0.0), - "detection_enabled": mp.Value( - "i", self.config.cameras[camera_name].detect.enabled + "camera_fps": mp.Value("d", 0.0), # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "skipped_fps": mp.Value("d", 0.0), # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "process_fps": mp.Value("d", 0.0), # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "detection_enabled": mp.Value( # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "i", + self.config.cameras[camera_name].detect.enabled, ), - "motion_enabled": mp.Value("i", True), - "improve_contrast_enabled": mp.Value( - "i", self.config.cameras[camera_name].motion.improve_contrast + "motion_enabled": mp.Value("i", True), # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "improve_contrast_enabled": mp.Value( # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "i", + self.config.cameras[camera_name].motion.improve_contrast, ), - "motion_threshold": mp.Value( - "i", self.config.cameras[camera_name].motion.threshold + "motion_threshold": mp.Value( # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "i", + self.config.cameras[camera_name].motion.threshold, ), - "motion_contour_area": mp.Value( - "i", self.config.cameras[camera_name].motion.contour_area + "motion_contour_area": mp.Value( # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "i", + self.config.cameras[camera_name].motion.contour_area, ), - "detection_fps": mp.Value("d", 0.0), - "detection_frame": mp.Value("d", 0.0), - "read_start": mp.Value("d", 0.0), - "ffmpeg_pid": mp.Value("i", 0), + "detection_fps": mp.Value("d", 0.0), # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "detection_frame": mp.Value("d", 0.0), # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "read_start": mp.Value("d", 0.0), # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "ffmpeg_pid": mp.Value("i", 0), # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards "frame_queue": mp.Queue(maxsize=2), "capture_process": None, "process": None, } self.record_metrics[camera_name] = { - "record_enabled": mp.Value( - "i", self.config.cameras[camera_name].record.enabled + "record_enabled": mp.Value( # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "i", + self.config.cameras[camera_name].record.enabled, ) } diff --git a/frigate/stats.py b/frigate/stats.py index d18642dfe..096ad7913 100644 --- a/frigate/stats.py +++ b/frigate/stats.py @@ -262,8 +262,12 @@ def stats_snapshot( for name, detector in stats_tracking["detectors"].items(): pid = detector.detect_process.pid if detector.detect_process else None stats["detectors"][name] = { - "inference_speed": round(detector.avg_inference_speed.value * 1000, 2), - "detection_start": detector.detection_start.value, + "inference_speed": round(detector.avg_inference_speed.value * 1000, 2), # type: ignore[attr-defined] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "detection_start": detector.detection_start.value, # type: ignore[attr-defined] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards "pid": pid, } stats["detection_fps"] = round(total_detection_fps, 2) diff --git a/frigate/watchdog.py b/frigate/watchdog.py index d1573f0b0..245e6f2cb 100644 --- a/frigate/watchdog.py +++ b/frigate/watchdog.py @@ -24,7 +24,9 @@ class FrigateWatchdog(threading.Thread): # check the detection processes for detector in self.detectors.values(): - detection_start = detector.detection_start.value + detection_start = detector.detection_start.value # type: ignore[attr-defined] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards if detection_start > 0.0 and now - detection_start > 10: logger.info( "Detection appears to be stuck. Restarting detection process..." diff --git a/requirements-wheels.txt b/requirements-wheels.txt index 00cedf9cd..778737a92 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -3,13 +3,13 @@ Flask == 2.3.* faster-fifo == 1.4.* imutils == 0.5.* matplotlib == 3.7.* -mypy == 0.942 +mypy == 1.4.1 numpy == 1.23.* onvif_zeep == 0.2.12 opencv-python-headless == 4.7.0.* paho-mqtt == 1.6.* peewee == 3.16.* -peewee_migrate == 1.10.* +peewee_migrate == 1.11.* psutil == 5.9.* pydantic == 1.10.* git+https://github.com/fbcotter/py3nvml#egg=py3nvml diff --git a/web/index.html b/web/index.html index bd3e829e1..7e40f01ff 100644 --- a/web/index.html +++ b/web/index.html @@ -8,7 +8,7 @@ - + diff --git a/web/package-lock.json b/web/package-lock.json index d1076106f..140183032 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -34,8 +34,8 @@ "@testing-library/user-event": "^14.4.3", "@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/parser": "^5.59.1", - "@vitest/coverage-c8": "^0.31.0", - "@vitest/ui": "^0.31.0", + "@vitest/coverage-v8": "^0.32.2", + "@vitest/ui": "^0.32.2", "autoprefixer": "^10.4.14", "eslint": "^8.39.0", "eslint-config-preact": "^1.3.0", @@ -49,7 +49,16 @@ "tailwindcss": "^3.3.2", "typescript": "^5.0.4", "vite": "^4.3.5", - "vitest": "^0.31.0" + "vitest": "^0.32.2" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/@adobe/css-tools": { @@ -894,14 +903,14 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", + "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.2", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -944,18 +953,18 @@ } }, "node_modules/@eslint/js": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz", - "integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", + "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -1774,15 +1783,15 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.8.tgz", - "integrity": "sha512-JDMOmhXteJ4WVKOiHXGCoB96ADWg9q7efPWHRViT/f09bA8XOMLAVHHju3l0MkZnG1izaWXYmgvQcUjTRcpShQ==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz", + "integrity": "sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.8", - "@typescript-eslint/type-utils": "5.59.8", - "@typescript-eslint/utils": "5.59.8", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/type-utils": "5.60.1", + "@typescript-eslint/utils": "5.60.1", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -1808,13 +1817,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.8.tgz", - "integrity": "sha512-/w08ndCYI8gxGf+9zKf1vtx/16y8MHrZs5/tnjHhMLNSixuNcJavSX4wAiPf4aS5x41Es9YPCn44MIe4cxIlig==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz", + "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8" + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1825,9 +1834,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.8.tgz", - "integrity": "sha512-+uWuOhBTj/L6awoWIg0BlWy0u9TyFpCHrAuQ5bNfxDaZ1Ppb3mx6tUigc74LHcbHpOHuOTOJrBoAnhdHdaea1w==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz", + "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1838,13 +1847,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.8.tgz", - "integrity": "sha512-Jy/lPSDJGNow14vYu6IrW790p7HIf/SOV1Bb6lZ7NUkLc2iB2Z9elESmsaUtLw8kVqogSbtLH9tut5GCX1RLDg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz", + "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1865,17 +1874,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.8.tgz", - "integrity": "sha512-Tr65630KysnNn9f9G7ROF3w1b5/7f6QVCJ+WK9nhIocWmx9F+TmCAcglF26Vm7z8KCTwoKcNEBZrhlklla3CKg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz", + "integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.8", - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/typescript-estree": "5.59.8", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/typescript-estree": "5.60.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -1891,12 +1900,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.8.tgz", - "integrity": "sha512-pJhi2ms0x0xgloT7xYabil3SGGlojNNKjK/q6dB3Ey0uJLMjK2UDGJvHieiyJVW/7C3KI+Z4Q3pEHkm4ejA+xQ==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz", + "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.8", + "@typescript-eslint/types": "5.60.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1930,9 +1939,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1964,14 +1973,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.8.tgz", - "integrity": "sha512-AnR19RjJcpjoeGojmwZtCwBX/RidqDZtzcbG3xHrmz0aHHoOcbWnpDllenRDmDvsV0RQ6+tbb09/kyc+UT9Orw==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz", + "integrity": "sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.8", - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/typescript-estree": "5.59.8", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/typescript-estree": "5.60.1", "debug": "^4.3.4" }, "engines": { @@ -1991,13 +2000,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.8.tgz", - "integrity": "sha512-/w08ndCYI8gxGf+9zKf1vtx/16y8MHrZs5/tnjHhMLNSixuNcJavSX4wAiPf4aS5x41Es9YPCn44MIe4cxIlig==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz", + "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8" + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2008,9 +2017,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.8.tgz", - "integrity": "sha512-+uWuOhBTj/L6awoWIg0BlWy0u9TyFpCHrAuQ5bNfxDaZ1Ppb3mx6tUigc74LHcbHpOHuOTOJrBoAnhdHdaea1w==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz", + "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2021,13 +2030,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.8.tgz", - "integrity": "sha512-Jy/lPSDJGNow14vYu6IrW790p7HIf/SOV1Bb6lZ7NUkLc2iB2Z9elESmsaUtLw8kVqogSbtLH9tut5GCX1RLDg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz", + "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2048,12 +2057,12 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.8.tgz", - "integrity": "sha512-pJhi2ms0x0xgloT7xYabil3SGGlojNNKjK/q6dB3Ey0uJLMjK2UDGJvHieiyJVW/7C3KI+Z4Q3pEHkm4ejA+xQ==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz", + "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.8", + "@typescript-eslint/types": "5.60.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2065,9 +2074,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2097,13 +2106,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.8.tgz", - "integrity": "sha512-+5M518uEIHFBy3FnyqZUF3BMP+AXnYn4oyH8RF012+e7/msMY98FhGL5SrN29NQ9xDgvqCgYnsOiKp1VjZ/fpA==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz", + "integrity": "sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.8", - "@typescript-eslint/utils": "5.59.8", + "@typescript-eslint/typescript-estree": "5.60.1", + "@typescript-eslint/utils": "5.60.1", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -2124,13 +2133,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.8.tgz", - "integrity": "sha512-/w08ndCYI8gxGf+9zKf1vtx/16y8MHrZs5/tnjHhMLNSixuNcJavSX4wAiPf4aS5x41Es9YPCn44MIe4cxIlig==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz", + "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8" + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2141,9 +2150,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.8.tgz", - "integrity": "sha512-+uWuOhBTj/L6awoWIg0BlWy0u9TyFpCHrAuQ5bNfxDaZ1Ppb3mx6tUigc74LHcbHpOHuOTOJrBoAnhdHdaea1w==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz", + "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2154,13 +2163,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.8.tgz", - "integrity": "sha512-Jy/lPSDJGNow14vYu6IrW790p7HIf/SOV1Bb6lZ7NUkLc2iB2Z9elESmsaUtLw8kVqogSbtLH9tut5GCX1RLDg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz", + "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2181,17 +2190,17 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.8.tgz", - "integrity": "sha512-Tr65630KysnNn9f9G7ROF3w1b5/7f6QVCJ+WK9nhIocWmx9F+TmCAcglF26Vm7z8KCTwoKcNEBZrhlklla3CKg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz", + "integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.8", - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/typescript-estree": "5.59.8", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/typescript-estree": "5.60.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -2207,12 +2216,12 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.8.tgz", - "integrity": "sha512-pJhi2ms0x0xgloT7xYabil3SGGlojNNKjK/q6dB3Ey0uJLMjK2UDGJvHieiyJVW/7C3KI+Z4Q3pEHkm4ejA+xQ==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz", + "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.8", + "@typescript-eslint/types": "5.60.1", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2246,9 +2255,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -2441,33 +2450,39 @@ "is-function": "^1.0.1" } }, - "node_modules/@vitest/coverage-c8": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.31.4.tgz", - "integrity": "sha512-VPx368m4DTcpA/P0v3YdVxl4QOSh1DbUcXURLRvDShrIB5KxOgfzw4Bn2R8AhAe/GyiWW/FIsJ/OJdYXCCiC1w==", + "node_modules/@vitest/coverage-v8": { + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.32.2.tgz", + "integrity": "sha512-/+V3nB3fyeuuSeKxCfi6XmWjDIxpky7AWSkGVfaMjAk7di8igBwRsThLjultwIZdTDH1RAxpjmCXEfSqsMFZOA==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.1", - "c8": "^7.13.0", + "@bcoe/v8-coverage": "^0.2.3", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.5", "magic-string": "^0.30.0", "picocolors": "^1.0.0", - "std-env": "^3.3.2" + "std-env": "^3.3.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": ">=0.30.0 <1" + "vitest": ">=0.32.0 <1" } }, "node_modules/@vitest/expect": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.31.4.tgz", - "integrity": "sha512-tibyx8o7GUyGHZGyPgzwiaPaLDQ9MMuCOrc03BYT0nryUuhLbL7NV2r/q98iv5STlwMgaKuFJkgBW/8iPKwlSg==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.2.tgz", + "integrity": "sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q==", "dev": true, "dependencies": { - "@vitest/spy": "0.31.4", - "@vitest/utils": "0.31.4", + "@vitest/spy": "0.32.2", + "@vitest/utils": "0.32.2", "chai": "^4.3.7" }, "funding": { @@ -2475,12 +2490,12 @@ } }, "node_modules/@vitest/runner": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.31.4.tgz", - "integrity": "sha512-Wgm6UER+gwq6zkyrm5/wbpXGF+g+UBB78asJlFkIOwyse0pz8lZoiC6SW5i4gPnls/zUcPLWS7Zog0LVepXnpg==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.32.2.tgz", + "integrity": "sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw==", "dev": true, "dependencies": { - "@vitest/utils": "0.31.4", + "@vitest/utils": "0.32.2", "concordance": "^5.0.4", "p-limit": "^4.0.0", "pathe": "^1.1.0" @@ -2517,9 +2532,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.31.4.tgz", - "integrity": "sha512-LemvNumL3NdWSmfVAMpXILGyaXPkZbG5tyl6+RQSdcHnTj6hvA49UAI8jzez9oQyE/FWLKRSNqTGzsHuk89LRA==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.32.2.tgz", + "integrity": "sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A==", "dev": true, "dependencies": { "magic-string": "^0.30.0", @@ -2531,9 +2546,9 @@ } }, "node_modules/@vitest/spy": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.31.4.tgz", - "integrity": "sha512-3ei5ZH1s3aqbEyftPAzSuunGICRuhE+IXOmpURFdkm5ybUADk+viyQfejNk6q8M5QGX8/EVKw+QWMEP3DTJDag==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.32.2.tgz", + "integrity": "sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug==", "dev": true, "dependencies": { "tinyspy": "^2.1.0" @@ -2543,12 +2558,12 @@ } }, "node_modules/@vitest/ui": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.31.4.tgz", - "integrity": "sha512-sKM16ITX6HrNFF+lNZ2AQAen4/6Bx2i6KlBfIvkUjcTgc5YII/j2ltcX14oCUv4EA0OTWGQuGhO3zDoAsTENGA==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.32.2.tgz", + "integrity": "sha512-N5JKftnB8qzKFtpQC5OcUGxYTLo6wiB/95Lgyk6MF52t74Y7BJOWbf6EFYhXqt9J0MSbhOR2kapq+WKKUGDW0g==", "dev": true, "dependencies": { - "@vitest/utils": "0.31.4", + "@vitest/utils": "0.32.2", "fast-glob": "^3.2.12", "fflate": "^0.7.4", "flatted": "^3.2.7", @@ -2564,12 +2579,12 @@ } }, "node_modules/@vitest/utils": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.31.4.tgz", - "integrity": "sha512-DobZbHacWznoGUfYU8XDPY78UubJxXfMNY1+SUdOp1NsI34eopSA6aZMeaGu10waSOeYwE8lxrd/pLfT0RMxjQ==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.32.2.tgz", + "integrity": "sha512-lnJ0T5i03j0IJaeW73hxe2AuVnZ/y1BhhCOuIcl9LIzXnbpXJT9Lrt6brwKHXLOiA7MZ6N5hSJjt0xE1dGNCzQ==", "dev": true, "dependencies": { - "concordance": "^5.0.4", + "diff-sequences": "^29.4.3", "loupe": "^2.3.6", "pretty-format": "^27.5.1" }, @@ -2599,9 +2614,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3086,70 +3101,6 @@ "optional": true, "peer": true }, - "node_modules/c8": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.13.0.tgz", - "integrity": "sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^2.0.0", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-reports": "^3.1.4", - "rimraf": "^3.0.2", - "test-exclude": "^6.0.0", - "v8-to-istanbul": "^9.0.0", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9" - }, - "bin": { - "c8": "bin/c8.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/c8/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/c8/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/c8/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3444,9 +3395,9 @@ } }, "node_modules/concordance/node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3726,9 +3677,9 @@ "dev": true }, "node_modules/diff-sequences": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.3.1.tgz", - "integrity": "sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -3956,16 +3907,16 @@ } }, "node_modules/eslint": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz", - "integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", + "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.41.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint/eslintrc": "^2.1.0", + "@eslint/js": "8.44.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", @@ -3976,7 +3927,7 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", + "espree": "^9.6.0", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3996,7 +3947,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" @@ -4344,12 +4295,12 @@ } }, "node_modules/espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" }, @@ -4637,19 +4588,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/form-data": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", @@ -5723,6 +5661,20 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-reports": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", @@ -6732,13 +6684,13 @@ } }, "node_modules/mlly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.3.0.tgz", - "integrity": "sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz", + "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==", "dev": true, "dependencies": { - "acorn": "^8.8.2", - "pathe": "^1.1.0", + "acorn": "^8.9.0", + "pathe": "^1.1.1", "pkg-types": "^1.0.3", "ufo": "^1.1.2" } @@ -6835,9 +6787,9 @@ "dev": true }, "node_modules/msw": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.1.tgz", - "integrity": "sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.2.tgz", + "integrity": "sha512-GsW3PE/Es/a1tYThXcM8YHOZ1S1MtivcS3He/LQbbTCx3rbWJYCtWD5XXyJ53KlNPT7O1VI9sCW3xMtgFe8XpQ==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -6872,7 +6824,7 @@ "url": "https://opencollective.com/mswjs" }, "peerDependencies": { - "typescript": ">= 4.4.x <= 5.0.x" + "typescript": ">= 4.4.x <= 5.1.x" }, "peerDependenciesMeta": { "typescript": { @@ -7225,17 +7177,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -7465,9 +7417,9 @@ } }, "node_modules/pathe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", - "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", "dev": true }, "node_modules/pathval": { @@ -8250,6 +8202,15 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -8827,16 +8788,16 @@ "peer": true }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/typeson": { @@ -9120,9 +9081,9 @@ } }, "node_modules/vite-node": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.31.4.tgz", - "integrity": "sha512-uzL377GjJtTbuc5KQxVbDu2xfU/x0wVjUtXQR2ihS21q/NK6ROr4oG0rsSkBBddZUVCwzfx22in76/0ZZHXgkQ==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.32.2.tgz", + "integrity": "sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -9151,19 +9112,19 @@ } }, "node_modules/vitest": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.31.4.tgz", - "integrity": "sha512-GoV0VQPmWrUFOZSg3RpQAPN+LPmHg2/gxlMNJlyxJihkz6qReHDV6b0pPDcqFLNEPya4tWJ1pgwUNP9MLmUfvQ==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.2.tgz", + "integrity": "sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ==", "dev": true, "dependencies": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.31.4", - "@vitest/runner": "0.31.4", - "@vitest/snapshot": "0.31.4", - "@vitest/spy": "0.31.4", - "@vitest/utils": "0.31.4", + "@vitest/expect": "0.32.2", + "@vitest/runner": "0.32.2", + "@vitest/snapshot": "0.32.2", + "@vitest/spy": "0.32.2", + "@vitest/utils": "0.32.2", "acorn": "^8.8.2", "acorn-walk": "^8.2.0", "cac": "^6.7.14", @@ -9179,7 +9140,7 @@ "tinybench": "^2.5.0", "tinypool": "^0.5.0", "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.31.4", + "vite-node": "0.32.2", "why-is-node-running": "^2.2.2" }, "bin": { @@ -9413,15 +9374,6 @@ "node": ">=8" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -9578,6 +9530,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@adobe/css-tools": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", @@ -10087,14 +10045,14 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", + "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.2", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -10121,15 +10079,15 @@ } }, "@eslint/js": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz", - "integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", + "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -10798,15 +10756,15 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.8.tgz", - "integrity": "sha512-JDMOmhXteJ4WVKOiHXGCoB96ADWg9q7efPWHRViT/f09bA8XOMLAVHHju3l0MkZnG1izaWXYmgvQcUjTRcpShQ==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz", + "integrity": "sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.8", - "@typescript-eslint/type-utils": "5.59.8", - "@typescript-eslint/utils": "5.59.8", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/type-utils": "5.60.1", + "@typescript-eslint/utils": "5.60.1", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", @@ -10816,29 +10774,29 @@ }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.8.tgz", - "integrity": "sha512-/w08ndCYI8gxGf+9zKf1vtx/16y8MHrZs5/tnjHhMLNSixuNcJavSX4wAiPf4aS5x41Es9YPCn44MIe4cxIlig==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz", + "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8" + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1" } }, "@typescript-eslint/types": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.8.tgz", - "integrity": "sha512-+uWuOhBTj/L6awoWIg0BlWy0u9TyFpCHrAuQ5bNfxDaZ1Ppb3mx6tUigc74LHcbHpOHuOTOJrBoAnhdHdaea1w==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz", + "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.8.tgz", - "integrity": "sha512-Jy/lPSDJGNow14vYu6IrW790p7HIf/SOV1Bb6lZ7NUkLc2iB2Z9elESmsaUtLw8kVqogSbtLH9tut5GCX1RLDg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz", + "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -10847,28 +10805,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.8.tgz", - "integrity": "sha512-Tr65630KysnNn9f9G7ROF3w1b5/7f6QVCJ+WK9nhIocWmx9F+TmCAcglF26Vm7z8KCTwoKcNEBZrhlklla3CKg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz", + "integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.8", - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/typescript-estree": "5.59.8", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/typescript-estree": "5.60.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.8.tgz", - "integrity": "sha512-pJhi2ms0x0xgloT7xYabil3SGGlojNNKjK/q6dB3Ey0uJLMjK2UDGJvHieiyJVW/7C3KI+Z4Q3pEHkm4ejA+xQ==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz", + "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.8", + "@typescript-eslint/types": "5.60.1", "eslint-visitor-keys": "^3.3.0" } }, @@ -10889,9 +10847,9 @@ "dev": true }, "semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -10909,41 +10867,41 @@ } }, "@typescript-eslint/parser": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.8.tgz", - "integrity": "sha512-AnR19RjJcpjoeGojmwZtCwBX/RidqDZtzcbG3xHrmz0aHHoOcbWnpDllenRDmDvsV0RQ6+tbb09/kyc+UT9Orw==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz", + "integrity": "sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.59.8", - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/typescript-estree": "5.59.8", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/typescript-estree": "5.60.1", "debug": "^4.3.4" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.8.tgz", - "integrity": "sha512-/w08ndCYI8gxGf+9zKf1vtx/16y8MHrZs5/tnjHhMLNSixuNcJavSX4wAiPf4aS5x41Es9YPCn44MIe4cxIlig==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz", + "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8" + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1" } }, "@typescript-eslint/types": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.8.tgz", - "integrity": "sha512-+uWuOhBTj/L6awoWIg0BlWy0u9TyFpCHrAuQ5bNfxDaZ1Ppb3mx6tUigc74LHcbHpOHuOTOJrBoAnhdHdaea1w==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz", + "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.8.tgz", - "integrity": "sha512-Jy/lPSDJGNow14vYu6IrW790p7HIf/SOV1Bb6lZ7NUkLc2iB2Z9elESmsaUtLw8kVqogSbtLH9tut5GCX1RLDg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz", + "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -10952,19 +10910,19 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.8.tgz", - "integrity": "sha512-pJhi2ms0x0xgloT7xYabil3SGGlojNNKjK/q6dB3Ey0uJLMjK2UDGJvHieiyJVW/7C3KI+Z4Q3pEHkm4ejA+xQ==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz", + "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.8", + "@typescript-eslint/types": "5.60.1", "eslint-visitor-keys": "^3.3.0" } }, "semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -10983,41 +10941,41 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.8.tgz", - "integrity": "sha512-+5M518uEIHFBy3FnyqZUF3BMP+AXnYn4oyH8RF012+e7/msMY98FhGL5SrN29NQ9xDgvqCgYnsOiKp1VjZ/fpA==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz", + "integrity": "sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.59.8", - "@typescript-eslint/utils": "5.59.8", + "@typescript-eslint/typescript-estree": "5.60.1", + "@typescript-eslint/utils": "5.60.1", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.8.tgz", - "integrity": "sha512-/w08ndCYI8gxGf+9zKf1vtx/16y8MHrZs5/tnjHhMLNSixuNcJavSX4wAiPf4aS5x41Es9YPCn44MIe4cxIlig==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz", + "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8" + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1" } }, "@typescript-eslint/types": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.8.tgz", - "integrity": "sha512-+uWuOhBTj/L6awoWIg0BlWy0u9TyFpCHrAuQ5bNfxDaZ1Ppb3mx6tUigc74LHcbHpOHuOTOJrBoAnhdHdaea1w==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz", + "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.8.tgz", - "integrity": "sha512-Jy/lPSDJGNow14vYu6IrW790p7HIf/SOV1Bb6lZ7NUkLc2iB2Z9elESmsaUtLw8kVqogSbtLH9tut5GCX1RLDg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz", + "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/visitor-keys": "5.59.8", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -11026,28 +10984,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.8.tgz", - "integrity": "sha512-Tr65630KysnNn9f9G7ROF3w1b5/7f6QVCJ+WK9nhIocWmx9F+TmCAcglF26Vm7z8KCTwoKcNEBZrhlklla3CKg==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz", + "integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.8", - "@typescript-eslint/types": "5.59.8", - "@typescript-eslint/typescript-estree": "5.59.8", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/typescript-estree": "5.60.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.59.8", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.8.tgz", - "integrity": "sha512-pJhi2ms0x0xgloT7xYabil3SGGlojNNKjK/q6dB3Ey0uJLMjK2UDGJvHieiyJVW/7C3KI+Z4Q3pEHkm4ejA+xQ==", + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz", + "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.59.8", + "@typescript-eslint/types": "5.60.1", "eslint-visitor-keys": "^3.3.0" } }, @@ -11068,9 +11026,9 @@ "dev": true }, "semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -11198,37 +11156,43 @@ "is-function": "^1.0.1" } }, - "@vitest/coverage-c8": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.31.4.tgz", - "integrity": "sha512-VPx368m4DTcpA/P0v3YdVxl4QOSh1DbUcXURLRvDShrIB5KxOgfzw4Bn2R8AhAe/GyiWW/FIsJ/OJdYXCCiC1w==", + "@vitest/coverage-v8": { + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.32.2.tgz", + "integrity": "sha512-/+V3nB3fyeuuSeKxCfi6XmWjDIxpky7AWSkGVfaMjAk7di8igBwRsThLjultwIZdTDH1RAxpjmCXEfSqsMFZOA==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.1", - "c8": "^7.13.0", + "@bcoe/v8-coverage": "^0.2.3", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.5", "magic-string": "^0.30.0", "picocolors": "^1.0.0", - "std-env": "^3.3.2" + "std-env": "^3.3.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.1.0" } }, "@vitest/expect": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.31.4.tgz", - "integrity": "sha512-tibyx8o7GUyGHZGyPgzwiaPaLDQ9MMuCOrc03BYT0nryUuhLbL7NV2r/q98iv5STlwMgaKuFJkgBW/8iPKwlSg==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.2.tgz", + "integrity": "sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q==", "dev": true, "requires": { - "@vitest/spy": "0.31.4", - "@vitest/utils": "0.31.4", + "@vitest/spy": "0.32.2", + "@vitest/utils": "0.32.2", "chai": "^4.3.7" } }, "@vitest/runner": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.31.4.tgz", - "integrity": "sha512-Wgm6UER+gwq6zkyrm5/wbpXGF+g+UBB78asJlFkIOwyse0pz8lZoiC6SW5i4gPnls/zUcPLWS7Zog0LVepXnpg==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.32.2.tgz", + "integrity": "sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw==", "dev": true, "requires": { - "@vitest/utils": "0.31.4", + "@vitest/utils": "0.32.2", "concordance": "^5.0.4", "p-limit": "^4.0.0", "pathe": "^1.1.0" @@ -11252,9 +11216,9 @@ } }, "@vitest/snapshot": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.31.4.tgz", - "integrity": "sha512-LemvNumL3NdWSmfVAMpXILGyaXPkZbG5tyl6+RQSdcHnTj6hvA49UAI8jzez9oQyE/FWLKRSNqTGzsHuk89LRA==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.32.2.tgz", + "integrity": "sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A==", "dev": true, "requires": { "magic-string": "^0.30.0", @@ -11263,21 +11227,21 @@ } }, "@vitest/spy": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.31.4.tgz", - "integrity": "sha512-3ei5ZH1s3aqbEyftPAzSuunGICRuhE+IXOmpURFdkm5ybUADk+viyQfejNk6q8M5QGX8/EVKw+QWMEP3DTJDag==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.32.2.tgz", + "integrity": "sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug==", "dev": true, "requires": { "tinyspy": "^2.1.0" } }, "@vitest/ui": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.31.4.tgz", - "integrity": "sha512-sKM16ITX6HrNFF+lNZ2AQAen4/6Bx2i6KlBfIvkUjcTgc5YII/j2ltcX14oCUv4EA0OTWGQuGhO3zDoAsTENGA==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.32.2.tgz", + "integrity": "sha512-N5JKftnB8qzKFtpQC5OcUGxYTLo6wiB/95Lgyk6MF52t74Y7BJOWbf6EFYhXqt9J0MSbhOR2kapq+WKKUGDW0g==", "dev": true, "requires": { - "@vitest/utils": "0.31.4", + "@vitest/utils": "0.32.2", "fast-glob": "^3.2.12", "fflate": "^0.7.4", "flatted": "^3.2.7", @@ -11287,12 +11251,12 @@ } }, "@vitest/utils": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.31.4.tgz", - "integrity": "sha512-DobZbHacWznoGUfYU8XDPY78UubJxXfMNY1+SUdOp1NsI34eopSA6aZMeaGu10waSOeYwE8lxrd/pLfT0RMxjQ==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.32.2.tgz", + "integrity": "sha512-lnJ0T5i03j0IJaeW73hxe2AuVnZ/y1BhhCOuIcl9LIzXnbpXJT9Lrt6brwKHXLOiA7MZ6N5hSJjt0xE1dGNCzQ==", "dev": true, "requires": { - "concordance": "^5.0.4", + "diff-sequences": "^29.4.3", "loupe": "^2.3.6", "pretty-format": "^27.5.1" } @@ -11316,9 +11280,9 @@ "dev": true }, "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", "dev": true }, "acorn-jsx": { @@ -11666,60 +11630,6 @@ "optional": true, "peer": true }, - "c8": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.13.0.tgz", - "integrity": "sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^2.0.0", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-reports": "^3.1.4", - "rimraf": "^3.0.2", - "test-exclude": "^6.0.0", - "v8-to-istanbul": "^9.0.0", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9" - }, - "dependencies": { - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, "cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -11937,9 +11847,9 @@ }, "dependencies": { "semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -12154,9 +12064,9 @@ "dev": true }, "diff-sequences": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.3.1.tgz", - "integrity": "sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", "dev": true }, "dir-glob": { @@ -12340,16 +12250,16 @@ "dev": true }, "eslint": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz", - "integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", + "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.41.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint/eslintrc": "^2.1.0", + "@eslint/js": "8.44.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", @@ -12360,7 +12270,7 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", + "espree": "^9.6.0", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -12380,7 +12290,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" @@ -12609,12 +12519,12 @@ "dev": true }, "espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", + "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", "dev": true, "requires": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } @@ -12835,16 +12745,6 @@ "is-callable": "^1.1.3" } }, - "foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - } - }, "form-data": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", @@ -13622,6 +13522,17 @@ } } }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, "istanbul-reports": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", @@ -14381,13 +14292,13 @@ } }, "mlly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.3.0.tgz", - "integrity": "sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz", + "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==", "dev": true, "requires": { - "acorn": "^8.8.2", - "pathe": "^1.1.0", + "acorn": "^8.9.0", + "pathe": "^1.1.1", "pkg-types": "^1.0.3", "ufo": "^1.1.2" } @@ -14463,9 +14374,9 @@ "dev": true }, "msw": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.1.tgz", - "integrity": "sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.2.tgz", + "integrity": "sha512-GsW3PE/Es/a1tYThXcM8YHOZ1S1MtivcS3He/LQbbTCx3rbWJYCtWD5XXyJ53KlNPT7O1VI9sCW3xMtgFe8XpQ==", "dev": true, "requires": { "@mswjs/cookies": "^0.2.2", @@ -14731,17 +14642,17 @@ } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "ora": { @@ -14910,9 +14821,9 @@ "dev": true }, "pathe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", - "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", "dev": true }, "pathval": { @@ -15472,6 +15383,12 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, "source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -15933,9 +15850,9 @@ "peer": true }, "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true }, "typeson": { @@ -16135,9 +16052,9 @@ } }, "vite-node": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.31.4.tgz", - "integrity": "sha512-uzL377GjJtTbuc5KQxVbDu2xfU/x0wVjUtXQR2ihS21q/NK6ROr4oG0rsSkBBddZUVCwzfx22in76/0ZZHXgkQ==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.32.2.tgz", + "integrity": "sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==", "dev": true, "requires": { "cac": "^6.7.14", @@ -16155,19 +16072,19 @@ "requires": {} }, "vitest": { - "version": "0.31.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.31.4.tgz", - "integrity": "sha512-GoV0VQPmWrUFOZSg3RpQAPN+LPmHg2/gxlMNJlyxJihkz6qReHDV6b0pPDcqFLNEPya4tWJ1pgwUNP9MLmUfvQ==", + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.2.tgz", + "integrity": "sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ==", "dev": true, "requires": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.31.4", - "@vitest/runner": "0.31.4", - "@vitest/snapshot": "0.31.4", - "@vitest/spy": "0.31.4", - "@vitest/utils": "0.31.4", + "@vitest/expect": "0.32.2", + "@vitest/runner": "0.32.2", + "@vitest/snapshot": "0.32.2", + "@vitest/spy": "0.32.2", + "@vitest/utils": "0.32.2", "acorn": "^8.8.2", "acorn-walk": "^8.2.0", "cac": "^6.7.14", @@ -16183,7 +16100,7 @@ "tinybench": "^2.5.0", "tinypool": "^0.5.0", "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.31.4", + "vite-node": "0.32.2", "why-is-node-running": "^2.2.2" } }, @@ -16333,12 +16250,6 @@ "stackback": "0.0.2" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/web/package.json b/web/package.json index 98655fedb..6cebd5d8a 100644 --- a/web/package.json +++ b/web/package.json @@ -38,8 +38,8 @@ "@testing-library/user-event": "^14.4.3", "@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/parser": "^5.59.1", - "@vitest/coverage-c8": "^0.31.0", - "@vitest/ui": "^0.31.0", + "@vitest/coverage-v8": "^0.32.2", + "@vitest/ui": "^0.32.2", "autoprefixer": "^10.4.14", "eslint": "^8.39.0", "eslint-config-preact": "^1.3.0", @@ -53,6 +53,6 @@ "tailwindcss": "^3.3.2", "typescript": "^5.0.4", "vite": "^4.3.5", - "vitest": "^0.31.0" + "vitest": "^0.32.2" } } diff --git a/web/src/routes/Export.jsx b/web/src/routes/Export.jsx index 55f1a8933..5181ae57c 100644 --- a/web/src/routes/Export.jsx +++ b/web/src/routes/Export.jsx @@ -18,9 +18,9 @@ export default function Export() { const localISODate = localDate.toISOString().split('T')[0]; const [startDate, setStartDate] = useState(localISODate); - const [startTime, setStartTime] = useState("00:00"); + const [startTime, setStartTime] = useState('00:00'); const [endDate, setEndDate] = useState(localISODate); - const [endTime, setEndTime] = useState("23:59"); + const [endTime, setEndTime] = useState('23:59'); const onHandleExport = () => { if (camera == 'select') { @@ -33,8 +33,6 @@ export default function Export() { return; } - - if (!startDate || !startTime || !endDate || !endTime) { setMessage({ text: 'A start and end time needs to be selected', error: true }); return; @@ -48,12 +46,13 @@ export default function Export() { return; } - axios.post(`export/${camera}/start/${start}/end/${end}`, { playback }) + axios + .post(`export/${camera}/start/${start}/end/${end}`, { playback }) .then(() => { setMessage({ text: 'Successfully started export. View the file in the /exports folder.', error: false }); }) .catch((error) => { - setMessage({ text: 'Failed to start export: '+error.response.data.message, error: true }); + setMessage({ text: `Failed to start export: ${error.response.data.message}`, error: true }); }); }; @@ -93,13 +92,37 @@ export default function Export() { From: - setStartDate(e.target.value)}/> - setStartTime(e.target.value)}/> + setStartDate(e.target.value)} + /> + setStartTime(e.target.value)} + /> To: - setEndDate(e.target.value)}/> - setEndTime(e.target.value)}/> + setEndDate(e.target.value)} + /> + setEndTime(e.target.value)} + />
From f1dc3a639c044189ada0be50c2129b09e1cb26c7 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Sat, 1 Jul 2023 15:16:19 +0200 Subject: [PATCH 15/33] fixed TimeAgo abbreviation (#6977) * fixed TimeAgo abbreviation * Update web/src/components/TimeAgo.tsx Co-authored-by: Blake Blackshear --------- Co-authored-by: Blake Blackshear --- web/src/components/TimeAgo.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/components/TimeAgo.tsx b/web/src/components/TimeAgo.tsx index 7b5a914e7..b7ed45007 100644 --- a/web/src/components/TimeAgo.tsx +++ b/web/src/components/TimeAgo.tsx @@ -25,9 +25,9 @@ const timeAgo = ({ time, currentTime = new Date(), dense = false }: IProp): stri const elapsedTime: number = currentTime.getTime() - pastTime.getTime(); const timeUnits: TimeUnit[] = [ - { unit: 'ye', full: 'year', value: 31536000 }, + { unit: 'yr', full: 'year', value: 31536000 }, { unit: 'mo', full: 'month', value: 0 }, - { unit: 'day', full: 'day', value: 86400 }, + { unit: 'd', full: 'day', value: 86400 }, { unit: 'h', full: 'hour', value: 3600 }, { unit: 'm', full: 'minute', value: 60 }, { unit: 's', full: 'second', value: 1 }, @@ -58,11 +58,11 @@ const timeAgo = ({ time, currentTime = new Date(), dense = false }: IProp): stri if (monthDiff > 0) { const unitAmount = monthDiff; - return `${unitAmount}${dense ? timeUnits[i].unit[0] : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`; + return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`; } } else if (elapsed >= timeUnits[i].value) { const unitAmount: number = Math.floor(elapsed / timeUnits[i].value); - return `${unitAmount}${dense ? timeUnits[i].unit[0] : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`; + return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`; } } return 'Invalid Time'; From c3b313a70da2a4a345e0dca238e0f70cd1ac5585 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 1 Jul 2023 07:18:33 -0600 Subject: [PATCH 16/33] Audio events (#6848) * Initial audio classification model implementation * fix mypy * Keep audio labelmap local * Cleanup * Start adding config for audio * Add the detector * Add audio detection process keypoints * Build out base config * Load labelmap correctly * Fix config bugs * Start audio process * Fix startup issues * Try to cleanup restarting * Add ffmpeg input args * Get audio detection working * Save event to db * End events if not heard for 30 seconds * Use not heard config * Stop ffmpeg when shutting down * Fixes * End events correctly * Use api instead of event queue to save audio events * Get events working * Close threads when stop event is sent * remove unused * Only start audio process if at least one camera is enabled * Add const for float * Cleanup labelmap * Add audio icon in frontend * Add ability to toggle audio with mqtt * Set initial audio value * Fix audio enabling * Close logpipe * Isort * Formatting * Fix web tests * Fix web tests * Handle cases where args are a string * Remove log * Cleanup process close * Use correct field * Simplify if statement * Use var for localhost * Add audio detectors docs * Add restream docs to mention audio detection * Add full config docs * Fix links to other docs --------- Co-authored-by: Jason Hunter --- Dockerfile | 4 +- audio-labelmap.txt | 521 ++++++++++++++++++ docs/docs/configuration/audio_detectors.md | 63 +++ docs/docs/configuration/index.md | 14 + .../{detectors.md => object_detectors.md} | 6 +- docs/docs/configuration/restream.md | 4 + docs/docs/frigate/hardware.md | 4 +- docs/docs/guides/getting_started.md | 2 +- docs/sidebars.js | 3 +- frigate/app.py | 33 +- frigate/comms/dispatcher.py | 36 +- frigate/comms/mqtt.py | 7 +- frigate/config.py | 34 +- frigate/const.py | 8 + frigate/events/audio.py | 247 +++++++++ frigate/events/external.py | 5 +- frigate/events/maintainer.py | 41 +- frigate/ffmpeg_presets.py | 7 + frigate/http.py | 5 +- frigate/output.py | 13 +- frigate/record/maintainer.py | 4 +- frigate/record/record.py | 4 +- frigate/types.py | 3 +- web/__test__/handlers.js | 2 + web/src/api/__tests__/ws.test.jsx | 4 +- web/src/api/ws.jsx | 12 +- web/src/icons/Audio.jsx | 36 ++ web/src/routes/Cameras.jsx | 37 +- 28 files changed, 1090 insertions(+), 69 deletions(-) create mode 100644 audio-labelmap.txt create mode 100644 docs/docs/configuration/audio_detectors.md rename docs/docs/configuration/{detectors.md => object_detectors.md} (99%) create mode 100644 frigate/events/audio.py create mode 100644 web/src/icons/Audio.jsx diff --git a/Dockerfile b/Dockerfile index 6db81e46b..d1dbd0755 100644 --- a/Dockerfile +++ b/Dockerfile @@ -98,7 +98,9 @@ COPY labelmap.txt . COPY --from=ov-converter /models/public/ssdlite_mobilenet_v2/FP16 openvino-model RUN wget -q https://github.com/openvinotoolkit/open_model_zoo/raw/master/data/dataset_classes/coco_91cl_bkgr.txt -O openvino-model/coco_91cl_bkgr.txt && \ sed -i 's/truck/car/g' openvino-model/coco_91cl_bkgr.txt - +# Get Audio Model and labels +RUN wget -qO cpu_audio_model.tflite https://tfhub.dev/google/lite-model/yamnet/classification/tflite/1?lite-format=tflite +COPY audio-labelmap.txt . FROM wget AS s6-overlay diff --git a/audio-labelmap.txt b/audio-labelmap.txt new file mode 100644 index 000000000..4a38b5f63 --- /dev/null +++ b/audio-labelmap.txt @@ -0,0 +1,521 @@ +speech +speech +speech +speech +babbling +speech +yell +bellow +whoop +yell +yell +yell +whispering +laughter +laughter +laughter +snicker +laughter +laughter +crying +crying +crying +yell +sigh +singing +choir +sodeling +chant +mantra +child_singing +synthetic_singing +rapping +humming +groan +grunt +whistling +breathing +wheeze +snoring +gasp +pant +snort +cough +throat_clearing +sneeze +sniff +run +shuffle +footsteps +chewing +biting +gargling +stomach_rumble +burping +hiccup +fart +hands +finger_snapping +clapping +heartbeat +heart_murmur +cheering +applause +chatter +crowd +speech +children_playing +animal +pets +dog +bark +yip +howl +bow-wow +growling +whimper_dog +cat +purr +meow +hiss +caterwaul +livestock +horse +clip-clop +neigh +cattle +moo +cowbell +pig +oink +goat +bleat +sheep +fowl +chicken +cluck +cock-a-doodle-doo +turkey +gobble +duck +quack +goose +honk +wild_animals +roaring_cats +roar +bird +chird +chirp +squawk +pigeon +coo +crow +caw +owl +hoot +flapping_wings +dogs +rats +mouse +patter +insect +cricket +mosquito +fly +buzz +buzz +frog +croak +snake +rattle +whale_vocalization +music +musical_instrument +plucked_string_instrument +guitar +electric_guitar +bass_guitar +acoustic_guitar +steel_guitar +tapping +strum +banjo +sitar +mandolin +zither +ukulele +keyboard +piano +electric_piano +organ +electronic_organ +hammond_organ +synthesizer +sampler +harpsichord +percussion +drum_kit +drum_machine +drum +snare_drum +rimshot +drum_roll +bass_drum +timpani +tabla +cymbal +hi-hat +wood_block +tambourine +rattle +maraca +gong +tubular_bells +mallet_percussion +marimba +glockenspiel +vibraphone +steelpan +orchestra +brass_instrument +french_horn +trumpet +trombone +bowed_string_instrument +string_section +violin +pizzicato +cello +double_bass +wind_instrument +flute +saxophone +clarinet +harp +bell +church_bell +jingle_bell +bicycle_bell +tuning_fork +chime +wind_chime +change_ringing +harmonica +accordion +bagpipes +didgeridoo +shofar +theremin +singing_bowl +scratching +pop_music +hip_hop_music +beatboxing +rock_music +heavy_metal +punk_rock +grunge +progressive_rock +rock_and_roll +psychedelic_rock +rhythm_and_blues +soul_music +reggae +country +swing_music +bluegrass +funk +folk_music +middle_eastern_music +jazz +disco +classical_music +opera +electronic_music +house_music +techno +dubstep +drum_and_bass +electronica +electronic_dance_music +ambient_music +trance_music +music_of_latin_america +salsa_music +flamenco +blues +music_for_children +new-age_music +vocal_music +a_capella +music_of_africa +afrobeat +christian_music +gospel_music +music_of_asia +carnatic_music +music_of_bollywood +ska +traditional_music +independent_music +song +background_music +theme_music +jingle +soundtrack_music +lullaby +video_game_music +christmas_music +dance_music +wedding_music +happy_music +sad_music +tender_music +exciting_music +angry_music +scary_music +wind +rustling_leaves +wind_noise +thunderstorm +thunder +water +rain +raindrop +rain_on_surface +stream +waterfall +ocean +waves +steam +gurgling +fire +crackle +vehicle +boat +sailboat +rowboat +motorboat +ship +motor_vehicle +car +honk +toot +car_alarm +power_windows +skidding +tire_squeal +car_passing_by +race_car +truck +air_brake +air_horn +reversing_beeps +ice_cream_truck +bus +emergency_vehicle +police_car +ambulance +fire_engine +motorcycle +traffic_noise +rail_transport +train +train_whistle +train_horn +railroad_car +train_wheels_squealing +subway +aircraft +aircraft_engine +jet_engine +propeller +helicopter +fixed-wing_aircraft +bicycle +skateboard +engine +light_engine +dental_drill's_drill +lawn_mower +chainsaw +medium_engine +heavy_engine +engine_knocking +engine_starting +idling +accelerating +door +doorbell +ding-dong +sliding_door +slam +knock +tap +squeak +cupboard_open_or_close +drawer_open_or_close +dishes +cutlery +chopping +frying +microwave_oven +blender +water_tap +sink +bathtub +hair_dryer +toilet_flush +toothbrush +electric_toothbrush +vacuum_cleaner +zipper +keys_jangling +coin +scissors +electric_shaver +shuffling_cards +typing +typewriter +computer_keyboard +writing +alarm +telephone +telephone_bell_ringing +ringtone +telephone_dialing +dial_tone +busy_signal +alarm_clock +siren +civil_defense_siren +buzzer +smoke_detector +fire_alarm +foghorn +whistle +steam_whistle +mechanisms +ratchet +clock +tick +tick-tock +gears +pulleys +sewing_machine +mechanical_fan +air_conditioning +cash_register +printer +camera +single-lens_reflex_camera +tools +hammer +jackhammer +sawing +filing +sanding +power_tool +drill +explosion +gunshot +machine_gun +fusillade +artillery_fire +cap_gun +fireworks +firecracker +burst +eruption +boom +wood +chop +splinter +crack +glass +chink +shatter +liquid +splash +slosh +squish +drip +pour +trickle +gush +fill +spray +pump +stir +boiling +sonar +arrow +whoosh +thump +thunk +electronic_tuner +effects_unit +chorus_effect +basketball_bounce +bang +slap +whack +smash +breaking +bouncing +whip +flap +scratch +scrape +rub +roll +crushing +crumpling +tearing +beep +ping +ding +clang +squeal +creak +rustle +whir +clatter +sizzle +clicking +clickety-clack +rumble +plop +jingle +hum +zing +boing +crunch +silence +sine_wave +harmonic +chirp_tone +sound_effect +pulse +inside +inside +inside +outside +outside +reverberation +echo +noise +environmental_noise +static +mains_hum +distortion +sidetone +cacophony +white_noise +pink_noise +throbbing +vibration +television +radio +field_recording diff --git a/docs/docs/configuration/audio_detectors.md b/docs/docs/configuration/audio_detectors.md new file mode 100644 index 000000000..ef1d8227c --- /dev/null +++ b/docs/docs/configuration/audio_detectors.md @@ -0,0 +1,63 @@ +--- +id: audio_detectors +title: Audio Detectors +--- + +Frigate provides a builtin audio detector which runs on the CPU. Compared to object detection in images, audio detection is a relatively lightweight operation so the only option is to run the detection on a CPU. + +## Configuration + +Audio events work by detecting a type of audio and creating an event, the event will end once the type of audio has not been heard for the configured amount of time. Audio events save a snapshot at the beginning of the event as well as recordings throughout the event. The recordings are retained using the configured recording retention. + +### Enabling Audio Events + +Audio events can be enabled for all cameras or only for specific cameras. + +```yaml + +audio: # <- enable audio events for all camera + enabled: True + +cameras: + front_camera: + ffmpeg: + ... + audio: + enabled: True # <- enable audio events for the front_camera +``` + +If you are using multiple streams then you must set the `audio` role on the stream that is going to be used for audio detection, this can be any stream but the stream must have audio included. + +:::note + +The ffmpeg process for capturing audio will be a separate connection to the camera along with the other roles assigned to the camera, for this reason it is recommended that the go2rtc restream is used for this purpose. See [the restream docs](/configuration/restream.md) for more information. + +::: + +```yaml +cameras: + front_camera: + ffmpeg: + inputs: + - path: rtsp://.../main_stream + roles: + - record + - path: rtsp://.../sub_stream # <- this stream must have audio enabled + roles: + - audio + - detect +``` + +### Configuring Audio Events + +The included audio model has over 500 different types of audio that can be detected, many of which are not practical. By default `bark`, `speech`, `yell`, and `scream` are enabled but these can be customized. + +```yaml +audio: + enabled: True + listen: + - bark + - scream + - speech + - yell +``` diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 7279a7652..8915db6b3 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -138,6 +138,20 @@ model: labelmap: 2: vehicle +# Optional: Audio Events Configuration +# NOTE: Can be overridden at the camera level +audio: + # Optional: Enable audio events (default: shown below) + enabled: False + # Optional: Configure the amount of seconds without detected audio to end the event (default: shown below) + max_not_heard: 30 + # Optional: Types of audio to listen for (default: shown below) + listen: + - bark + - scream + - speech + - yell + # Optional: logger verbosity settings logger: # Optional: Default log verbosity (default: shown below) diff --git a/docs/docs/configuration/detectors.md b/docs/docs/configuration/object_detectors.md similarity index 99% rename from docs/docs/configuration/detectors.md rename to docs/docs/configuration/object_detectors.md index 84507321c..3f48423bc 100644 --- a/docs/docs/configuration/detectors.md +++ b/docs/docs/configuration/object_detectors.md @@ -1,6 +1,6 @@ --- -id: detectors -title: Detectors +id: object_detectors +title: Object Detectors --- Frigate provides the following builtin detector types: `cpu`, `edgetpu`, `openvino`, and `tensorrt`. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras. @@ -275,6 +275,6 @@ detectors: api_timeout: 0.1 # seconds ``` -Replace `` and `` with the IP address and port of your CodeProject.AI server. +Replace `` and `` with the IP address and port of your CodeProject.AI server. To verify that the integration is working correctly, start Frigate and observe the logs for any error messages related to CodeProject.AI. Additionally, you can check the Frigate web interface to see if the objects detected by CodeProject.AI are being displayed and tracked properly. \ No newline at end of file diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md index 2d5c565b2..61393a91c 100644 --- a/docs/docs/configuration/restream.md +++ b/docs/docs/configuration/restream.md @@ -67,6 +67,7 @@ cameras: roles: - record - detect + - audio # <- only necessary if audio detection is enabled http_cam: ffmpeg: output_args: @@ -77,6 +78,7 @@ cameras: roles: - record - detect + - audio # <- only necessary if audio detection is enabled ``` ### With Sub Stream @@ -112,6 +114,7 @@ cameras: - path: rtsp://127.0.0.1:8554/rtsp_cam_sub # <--- the name here must match the name of the camera_sub in restream input_args: preset-rtsp-restream roles: + - audio # <- only necessary if audio detection is enabled - detect http_cam: ffmpeg: @@ -125,6 +128,7 @@ cameras: - path: rtsp://127.0.0.1:8554/http_cam_sub # <--- the name here must match the name of the camera_sub in restream input_args: preset-rtsp-restream roles: + - audio # <- only necessary if audio detection is enabled - detect ``` diff --git a/docs/docs/frigate/hardware.md b/docs/docs/frigate/hardware.md index e5233e218..36233ea68 100644 --- a/docs/docs/frigate/hardware.md +++ b/docs/docs/frigate/hardware.md @@ -50,7 +50,7 @@ The OpenVINO detector type is able to run on: - 6th Gen Intel Platforms and newer that have an iGPU - x86 & Arm64 hosts with VPU Hardware (ex: Intel NCS2) -More information is available [in the detector docs](/configuration/detectors#openvino-detector) +More information is available [in the detector docs](/configuration/object_detectors#openvino-detector) Inference speeds vary greatly depending on the CPU, GPU, or VPU used, some known examples are below: @@ -72,7 +72,7 @@ Inference speeds vary greatly depending on the CPU, GPU, or VPU used, some known ### TensorRT -The TensortRT detector is able to run on x86 hosts that have an Nvidia GPU which supports the 11.x series of CUDA libraries. The minimum driver version on the host system must be `>=450.80.02`. Also the GPU must support a Compute Capability of `5.0` or greater. This generally correlates to a Maxwell-era GPU or newer, check the [TensorRT docs for more info](/configuration/detectors#nvidia-tensorrt-detector). +The TensortRT detector is able to run on x86 hosts that have an Nvidia GPU which supports the 11.x series of CUDA libraries. The minimum driver version on the host system must be `>=450.80.02`. Also the GPU must support a Compute Capability of `5.0` or greater. This generally correlates to a Maxwell-era GPU or newer, check the [TensorRT docs for more info](/configuration/object_detectors#nvidia-tensorrt-detector). Inference speeds will vary greatly depending on the GPU and the model used. `tiny` variants are faster than the equivalent non-tiny model, some known examples are below: diff --git a/docs/docs/guides/getting_started.md b/docs/docs/guides/getting_started.md index adbddb9c2..cb67c59b4 100644 --- a/docs/docs/guides/getting_started.md +++ b/docs/docs/guides/getting_started.md @@ -71,7 +71,7 @@ cameras: ... ``` -More details on available detectors can be found [here](../configuration/detectors.md). +More details on available detectors can be found [here](../configuration/object_detectors.md). Restart Frigate and you should start seeing detections for `person`. If you want to track other objects, they will need to be added according to the [configuration file reference](../configuration/index.md#full-configuration-reference). diff --git a/docs/sidebars.js b/docs/sidebars.js index 41628f2ed..35ce2bee3 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -16,7 +16,8 @@ module.exports = { ], Configuration: [ "configuration/index", - "configuration/detectors", + "configuration/object_detectors", + "configuration/audio_detectors", "configuration/cameras", "configuration/masks", "configuration/record", diff --git a/frigate/app.py b/frigate/app.py index 0b476cd43..ccfbd4696 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -29,6 +29,7 @@ from frigate.const import ( MODEL_CACHE_DIR, RECORD_DIR, ) +from frigate.events.audio import listen_to_audio from frigate.events.cleanup import EventCleanup from frigate.events.external import ExternalEventProcessor from frigate.events.maintainer import EventProcessor @@ -44,7 +45,7 @@ from frigate.record.record import manage_recordings from frigate.stats import StatsEmitter, stats_init from frigate.storage import StorageMaintainer from frigate.timeline import TimelineProcessor -from frigate.types import CameraMetricsTypes, RecordMetricsTypes +from frigate.types import CameraMetricsTypes, FeatureMetricsTypes from frigate.version import VERSION from frigate.video import capture_camera, track_camera from frigate.watchdog import FrigateWatchdog @@ -62,7 +63,7 @@ class FrigateApp: self.log_queue: Queue = mp.Queue() self.plus_api = PlusApi() self.camera_metrics: dict[str, CameraMetricsTypes] = {} - self.record_metrics: dict[str, RecordMetricsTypes] = {} + self.feature_metrics: dict[str, FeatureMetricsTypes] = {} self.processes: dict[str, int] = {} def set_environment_vars(self) -> None: @@ -104,7 +105,7 @@ class FrigateApp: user_config = FrigateConfig.parse_file(config_file) self.config = user_config.runtime_config(self.plus_api) - for camera_name in self.config.cameras.keys(): + for camera_name, camera_config in self.config.cameras.items(): # create camera_metrics self.camera_metrics[camera_name] = { "camera_fps": mp.Value("d", 0.0), # type: ignore[typeddict-item] @@ -159,13 +160,19 @@ class FrigateApp: "capture_process": None, "process": None, } - self.record_metrics[camera_name] = { + self.feature_metrics[camera_name] = { + "audio_enabled": mp.Value( # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "i", + self.config.cameras[camera_name].audio.enabled, + ), "record_enabled": mp.Value( # type: ignore[typeddict-item] # issue https://github.com/python/typeshed/issues/8799 # from mypy 0.981 onwards "i", self.config.cameras[camera_name].record.enabled, - ) + ), } def set_log_levels(self) -> None: @@ -253,7 +260,7 @@ class FrigateApp: recording_process = mp.Process( target=manage_recordings, name="recording_manager", - args=(self.config, self.recordings_info_queue, self.record_metrics), + args=(self.config, self.recordings_info_queue, self.feature_metrics), ) recording_process.daemon = True self.recording_process = recording_process @@ -312,7 +319,7 @@ class FrigateApp: self.config, self.onvif_controller, self.camera_metrics, - self.record_metrics, + self.feature_metrics, comms, ) @@ -421,6 +428,17 @@ class FrigateApp: capture_process.start() logger.info(f"Capture process started for {name}: {capture_process.pid}") + def start_audio_processors(self) -> None: + if len([c for c in self.config.cameras.values() if c.audio.enabled]) > 0: + audio_process = mp.Process( + target=listen_to_audio, + name="audio_capture", + args=(self.config, self.feature_metrics), + ) + audio_process.daemon = True + audio_process.start() + logger.info(f"Audio process started: {audio_process.pid}") + def start_timeline_processor(self) -> None: self.timeline_processor = TimelineProcessor( self.config, self.timeline_queue, self.stop_event @@ -517,6 +535,7 @@ class FrigateApp: self.start_detected_frames_processor() self.start_camera_processors() self.start_camera_capture_processes() + self.start_audio_processors() self.start_storage_maintainer() self.init_stats() self.init_external_event_processor() diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index b7e9e8858..1c9105ce8 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -6,7 +6,7 @@ from typing import Any, Callable from frigate.config import FrigateConfig from frigate.ptz import OnvifCommandEnum, OnvifController -from frigate.types import CameraMetricsTypes, RecordMetricsTypes +from frigate.types import CameraMetricsTypes, FeatureMetricsTypes from frigate.util import restart_frigate logger = logging.getLogger(__name__) @@ -39,19 +39,20 @@ class Dispatcher: config: FrigateConfig, onvif: OnvifController, camera_metrics: dict[str, CameraMetricsTypes], - record_metrics: dict[str, RecordMetricsTypes], + feature_metrics: dict[str, FeatureMetricsTypes], communicators: list[Communicator], ) -> None: self.config = config self.onvif = onvif self.camera_metrics = camera_metrics - self.record_metrics = record_metrics + self.feature_metrics = feature_metrics self.comms = communicators for comm in self.comms: comm.subscribe(self._receive) self._camera_settings_handlers: dict[str, Callable] = { + "audio": self._on_audio_command, "detect": self._on_detect_command, "improve_contrast": self._on_motion_improve_contrast_command, "motion": self._on_motion_command, @@ -186,6 +187,29 @@ class Dispatcher: motion_settings.threshold = payload # type: ignore[union-attr] self.publish(f"{camera_name}/motion_threshold/state", payload, retain=True) + def _on_audio_command(self, camera_name: str, payload: str) -> None: + """Callback for audio topic.""" + audio_settings = self.config.cameras[camera_name].audio + + if payload == "ON": + if not self.config.cameras[camera_name].audio.enabled_in_config: + logger.error( + "Audio detection must be enabled in the config to be turned on via MQTT." + ) + return + + if not audio_settings.enabled: + logger.info(f"Turning on audio detection for {camera_name}") + audio_settings.enabled = True + self.feature_metrics[camera_name]["audio_enabled"].value = True + elif payload == "OFF": + if self.feature_metrics[camera_name]["audio_enabled"].value: + logger.info(f"Turning off audio detection for {camera_name}") + audio_settings.enabled = False + self.feature_metrics[camera_name]["audio_enabled"].value = False + + self.publish(f"{camera_name}/audio/state", payload, retain=True) + def _on_recordings_command(self, camera_name: str, payload: str) -> None: """Callback for recordings topic.""" record_settings = self.config.cameras[camera_name].record @@ -200,12 +224,12 @@ class Dispatcher: if not record_settings.enabled: logger.info(f"Turning on recordings for {camera_name}") record_settings.enabled = True - self.record_metrics[camera_name]["record_enabled"].value = True + self.feature_metrics[camera_name]["record_enabled"].value = True elif payload == "OFF": - if self.record_metrics[camera_name]["record_enabled"].value: + if self.feature_metrics[camera_name]["record_enabled"].value: logger.info(f"Turning off recordings for {camera_name}") record_settings.enabled = False - self.record_metrics[camera_name]["record_enabled"].value = False + self.feature_metrics[camera_name]["record_enabled"].value = False self.publish(f"{camera_name}/recordings/state", payload, retain=True) diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py index 07799f9da..4ddfbe7f1 100644 --- a/frigate/comms/mqtt.py +++ b/frigate/comms/mqtt.py @@ -41,7 +41,7 @@ class MqttClient(Communicator): # type: ignore[misc] for camera_name, camera in self.config.cameras.items(): self.publish( f"{camera_name}/recordings/state", - "ON" if camera.record.enabled else "OFF", + "ON" if camera.record.enabled_in_config else "OFF", retain=True, ) self.publish( @@ -49,6 +49,11 @@ class MqttClient(Communicator): # type: ignore[misc] "ON" if camera.snapshots.enabled else "OFF", retain=True, ) + self.publish( + f"{camera_name}/audio/state", + "ON" if camera.audio.enabled_in_config else "OFF", + retain=True, + ) self.publish( f"{camera_name}/detect/state", "ON" if camera.detect.enabled else "OFF", diff --git a/frigate/config.py b/frigate/config.py index 662b7b8bd..ea7ecdc49 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -40,6 +40,7 @@ DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S" FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")} DEFAULT_TRACKED_OBJECTS = ["person"] +DEFAULT_LISTEN_AUDIO = ["bark", "speech", "yell", "scream"] DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}} @@ -387,6 +388,19 @@ class ObjectConfig(FrigateBaseModel): mask: Union[str, List[str]] = Field(default="", title="Object mask.") +class AudioConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable audio events.") + max_not_heard: int = Field( + default=30, title="Seconds of not hearing the type of audio to end the event." + ) + listen: List[str] = Field( + default=DEFAULT_LISTEN_AUDIO, title="Audio to listen for." + ) + enabled_in_config: Optional[bool] = Field( + title="Keep track of original state of audio detection." + ) + + class BirdseyeModeEnum(str, Enum): objects = "objects" motion = "motion" @@ -470,6 +484,7 @@ class FfmpegConfig(FrigateBaseModel): class CameraRoleEnum(str, Enum): + audio = "audio" record = "record" rtmp = "rtmp" detect = "detect" @@ -631,6 +646,9 @@ class CameraConfig(FrigateBaseModel): objects: ObjectConfig = Field( default_factory=ObjectConfig, title="Object configuration." ) + audio: AudioConfig = Field( + default_factory=AudioConfig, title="Audio events configuration." + ) motion: Optional[MotionConfig] = Field(title="Motion detection configuration.") detect: DetectConfig = Field( default_factory=DetectConfig, title="Object detection configuration." @@ -661,12 +679,16 @@ class CameraConfig(FrigateBaseModel): # add roles to the input if there is only one if len(config["ffmpeg"]["inputs"]) == 1: has_rtmp = "rtmp" in config["ffmpeg"]["inputs"][0].get("roles", []) + has_audio = "audio" in config["ffmpeg"]["inputs"][0].get("roles", []) config["ffmpeg"]["inputs"][0]["roles"] = [ "record", "detect", ] + if has_audio: + config["ffmpeg"]["inputs"][0]["roles"].append("audio") + if has_rtmp: config["ffmpeg"]["inputs"][0]["roles"].append("rtmp") @@ -799,6 +821,11 @@ def verify_config_roles(camera_config: CameraConfig) -> None: f"Camera {camera_config.name} has rtmp enabled, but rtmp is not assigned to an input." ) + if camera_config.audio.enabled and "audio" not in assigned_roles: + raise ValueError( + f"Camera {camera_config.name} has audio events enabled, but audio is not assigned to an input." + ) + def verify_valid_live_stream_name( frigate_config: FrigateConfig, camera_config: CameraConfig @@ -911,6 +938,9 @@ class FrigateConfig(FrigateBaseModel): objects: ObjectConfig = Field( default_factory=ObjectConfig, title="Global object configuration." ) + audio: AudioConfig = Field( + default_factory=AudioConfig, title="Global Audio events configuration." + ) motion: Optional[MotionConfig] = Field( title="Global motion detection configuration." ) @@ -935,6 +965,7 @@ class FrigateConfig(FrigateBaseModel): # Global config to propagate down to camera level global_config = config.dict( include={ + "audio": ..., "birdseye": ..., "record": ..., "snapshots": ..., @@ -980,8 +1011,9 @@ class FrigateConfig(FrigateBaseModel): camera_config.onvif.password = camera_config.onvif.password.format( **FRIGATE_ENV_VARS ) - # set config recording value + # set config pre-value camera_config.record.enabled_in_config = camera_config.record.enabled + camera_config.audio.enabled_in_config = camera_config.audio.enabled # Add default filters object_keys = camera_config.objects.track diff --git a/frigate/const.py b/frigate/const.py index c1524a6a8..20e2b0daa 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -8,6 +8,7 @@ EXPORT_DIR = f"{BASE_DIR}/exports" BIRDSEYE_PIPE = "/tmp/cache/birdseye" CACHE_DIR = "/tmp/cache" YAML_EXT = (".yaml", ".yml") +FRIGATE_LOCALHOST = "http://127.0.0.1:5000" PLUS_ENV_VAR = "PLUS_API_KEY" PLUS_API_HOST = "https://api.frigate.video" BTBN_PATH = "/usr/lib/btbn-ffmpeg" @@ -22,6 +23,13 @@ ALL_ATTRIBUTE_LABELS = [ item for sublist in ATTRIBUTE_LABEL_MAP.values() for item in sublist ] +# Audio Consts + +AUDIO_DURATION = 0.975 +AUDIO_FORMAT = "s16le" +AUDIO_MAX_BIT_RANGE = 32768.0 +AUDIO_SAMPLE_RATE = 16000 + # Regex Consts REGEX_CAMERA_NAME = r"^[a-zA-Z0-9_-]+$" diff --git a/frigate/events/audio.py b/frigate/events/audio.py new file mode 100644 index 000000000..4f40334d4 --- /dev/null +++ b/frigate/events/audio.py @@ -0,0 +1,247 @@ +"""Handle creating audio events.""" + +import datetime +import logging +import multiprocessing as mp +import os +import signal +import threading +from types import FrameType +from typing import Optional + +import numpy as np +import requests +from setproctitle import setproctitle + +from frigate.config import CameraConfig, FrigateConfig +from frigate.const import ( + AUDIO_DURATION, + AUDIO_FORMAT, + AUDIO_MAX_BIT_RANGE, + AUDIO_SAMPLE_RATE, + CACHE_DIR, + FRIGATE_LOCALHOST, +) +from frigate.ffmpeg_presets import parse_preset_input +from frigate.log import LogPipe +from frigate.object_detection import load_labels +from frigate.types import FeatureMetricsTypes +from frigate.util import get_ffmpeg_arg_list, listen +from frigate.video import start_or_restart_ffmpeg, stop_ffmpeg + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from tensorflow.lite.python.interpreter import Interpreter + +logger = logging.getLogger(__name__) + + +def get_ffmpeg_command(input_args: list[str], input_path: str, pipe: str) -> list[str]: + return get_ffmpeg_arg_list( + f"ffmpeg {{}} -i {{}} -f {AUDIO_FORMAT} -ar {AUDIO_SAMPLE_RATE} -ac 1 -y {{}}".format( + " ".join(input_args), + input_path, + pipe, + ) + ) + + +def listen_to_audio( + config: FrigateConfig, + process_info: dict[str, FeatureMetricsTypes], +) -> None: + stop_event = mp.Event() + audio_threads: list[threading.Thread] = [] + + def exit_process() -> None: + for thread in audio_threads: + thread.join() + + logger.info("Exiting audio detector...") + + def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: + stop_event.set() + exit_process() + + signal.signal(signal.SIGTERM, receiveSignal) + signal.signal(signal.SIGINT, receiveSignal) + + threading.current_thread().name = "process:audio_manager" + setproctitle("frigate.audio_manager") + listen() + + for camera in config.cameras.values(): + if camera.enabled and camera.audio.enabled_in_config: + audio = AudioEventMaintainer(camera, process_info, stop_event) + audio_threads.append(audio) + audio.start() + + +class AudioTfl: + def __init__(self, stop_event: mp.Event): + self.stop_event = stop_event + self.labels = load_labels("/audio-labelmap.txt") + self.interpreter = Interpreter( + model_path="/cpu_audio_model.tflite", + num_threads=2, + ) + + self.interpreter.allocate_tensors() + + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + + def _detect_raw(self, tensor_input): + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input) + self.interpreter.invoke() + detections = np.zeros((20, 6), np.float32) + + res = self.interpreter.get_tensor(self.tensor_output_details[0]["index"])[0] + non_zero_indices = res > 0 + class_ids = np.argpartition(-res, 20)[:20] + class_ids = class_ids[np.argsort(-res[class_ids])] + class_ids = class_ids[non_zero_indices[class_ids]] + scores = res[class_ids] + boxes = np.full((scores.shape[0], 4), -1, np.float32) + count = len(scores) + + for i in range(count): + if scores[i] < 0.4 or i == 20: + break + detections[i] = [ + class_ids[i], + float(scores[i]), + boxes[i][0], + boxes[i][1], + boxes[i][2], + boxes[i][3], + ] + + return detections + + def detect(self, tensor_input, threshold=0.8): + detections = [] + + if self.stop_event.is_set(): + return detections + + raw_detections = self._detect_raw(tensor_input) + + for d in raw_detections: + if d[1] < threshold: + break + detections.append( + (self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5])) + ) + return detections + + +class AudioEventMaintainer(threading.Thread): + def __init__( + self, + camera: CameraConfig, + feature_metrics: dict[str, FeatureMetricsTypes], + stop_event: mp.Event, + ) -> None: + threading.Thread.__init__(self) + self.name = f"{camera.name}_audio_event_processor" + self.config = camera + self.feature_metrics = feature_metrics + self.detections: dict[dict[str, any]] = feature_metrics + self.stop_event = stop_event + self.detector = AudioTfl(stop_event) + self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),) + self.chunk_size = int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE * 2)) + self.pipe = f"{CACHE_DIR}/{self.config.name}-audio" + self.ffmpeg_cmd = get_ffmpeg_command( + get_ffmpeg_arg_list(self.config.ffmpeg.global_args) + + parse_preset_input("preset-rtsp-audio-only", 1), + [i.path for i in self.config.ffmpeg.inputs if "audio" in i.roles][0], + self.pipe, + ) + self.pipe_file = None + self.logpipe = LogPipe(f"ffmpeg.{self.config.name}.audio") + self.audio_listener = None + + def detect_audio(self, audio) -> None: + if not self.feature_metrics[self.config.name]["audio_enabled"].value: + return + + waveform = (audio / AUDIO_MAX_BIT_RANGE).astype(np.float32) + model_detections = self.detector.detect(waveform) + + for label, score, _ in model_detections: + if label not in self.config.audio.listen: + continue + + self.handle_detection(label, score) + + self.expire_detections() + + def handle_detection(self, label: str, score: float) -> None: + if self.detections.get(label): + self.detections[label][ + "last_detection" + ] = datetime.datetime.now().timestamp() + else: + resp = requests.post( + f"{FRIGATE_LOCALHOST}/api/events/{self.config.name}/{label}/create", + json={"duration": None}, + ) + + if resp.status_code == 200: + event_id = resp.json()[0]["event_id"] + self.detections[label] = { + "id": event_id, + "label": label, + "last_detection": datetime.datetime.now().timestamp(), + } + + def expire_detections(self) -> None: + now = datetime.datetime.now().timestamp() + + for detection in self.detections.values(): + if ( + now - detection.get("last_detection", now) + > self.config.audio.max_not_heard + ): + self.detections[detection["label"]] = None + requests.put( + f"{FRIGATE_LOCALHOST}/api/events/{detection['id']}/end", + json={ + "end_time": detection["last_detection"] + + self.config.record.events.post_capture + }, + ) + + def restart_audio_pipe(self) -> None: + try: + os.mkfifo(self.pipe) + except FileExistsError: + pass + + self.audio_listener = start_or_restart_ffmpeg( + self.ffmpeg_cmd, logger, self.logpipe, None, self.audio_listener + ) + + def read_audio(self) -> None: + if self.pipe_file is None: + self.pipe_file = open(self.pipe, "rb") + + try: + audio = np.frombuffer(self.pipe_file.read(self.chunk_size), dtype=np.int16) + self.detect_audio(audio) + except BrokenPipeError: + self.logpipe.dump() + self.restart_audio_pipe() + + def run(self) -> None: + self.restart_audio_pipe() + + while not self.stop_event.is_set(): + self.read_audio() + + self.pipe_file.close() + stop_ffmpeg(self.audio_listener, logger) + self.logpipe.close() diff --git a/frigate/events/external.py b/frigate/events/external.py index 910aee35f..25ba289f2 100644 --- a/frigate/events/external.py +++ b/frigate/events/external.py @@ -67,11 +67,10 @@ class ExternalEventProcessor: return event_id - def finish_manual_event(self, event_id: str) -> None: + def finish_manual_event(self, event_id: str, end_time: float) -> None: """Finish external event with indeterminate duration.""" - now = datetime.datetime.now().timestamp() self.queue.put( - (EventTypeEnum.api, "end", None, {"id": event_id, "end_time": now}) + (EventTypeEnum.api, "end", None, {"id": event_id, "end_time": end_time}) ) def _write_images( diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index 28fb4646b..f024f0be6 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -18,7 +18,6 @@ logger = logging.getLogger(__name__) class EventTypeEnum(str, Enum): api = "api" - # audio = "audio" tracked_object = "tracked_object" @@ -73,19 +72,21 @@ class EventProcessor(threading.Thread): except queue.Empty: continue - logger.debug(f"Event received: {event_type} {camera} {event_data['id']}") - - self.timeline_queue.put( - ( - camera, - source_type, - event_type, - self.events_in_process.get(event_data["id"]), - event_data, - ) + logger.debug( + f"Event received: {source_type} {event_type} {camera} {event_data['id']}" ) if source_type == EventTypeEnum.tracked_object: + self.timeline_queue.put( + ( + camera, + source_type, + event_type, + self.events_in_process.get(event_data["id"]), + event_data, + ) + ) + if event_type == "start": self.events_in_process[event_data["id"]] = event_data continue @@ -215,7 +216,7 @@ class EventProcessor(threading.Thread): del self.events_in_process[event_data["id"]] self.event_processed_queue.put((event_data["id"], camera)) - def handle_external_detection(self, type: str, event_data: Event): + def handle_external_detection(self, type: str, event_data: Event) -> None: if type == "new": event = { Event.id: event_data["id"], @@ -230,20 +231,14 @@ class EventProcessor(threading.Thread): Event.zones: [], Event.data: {}, } + Event.insert(event).execute() elif type == "end": event = { Event.id: event_data["id"], Event.end_time: event_data["end_time"], } - try: - ( - Event.insert(event) - .on_conflict( - conflict_target=[Event.id], - update=event, - ) - .execute() - ) - except Exception: - logger.warning(f"Failed to update manual event: {event_data['id']}") + try: + Event.update(event).execute() + except Exception: + logger.warning(f"Failed to update manual event: {event_data['id']}") diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index dde158916..a2785813c 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -282,6 +282,13 @@ PRESETS_INPUT = { "-use_wallclock_as_timestamps", "1", ], + "preset-rtsp-audio-only": [ + "-rtsp_transport", + "tcp", + TIMEOUT_PARAM, + "5000000", + "-vn", + ], "preset-rtsp-restream": _user_agent_args + [ "-rtsp_transport", diff --git a/frigate/http.py b/frigate/http.py index e47e6efc2..f3632a0cf 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -908,8 +908,11 @@ def create_event(camera_name, label): @bp.route("/events//end", methods=["PUT"]) def end_event(event_id): + json: dict[str, any] = request.get_json(silent=True) or {} + try: - current_app.external_processor.finish_manual_event(event_id) + end_time = json.get("end_time", datetime.now().timestamp()) + current_app.external_processor.finish_manual_event(event_id, end_time) except Exception: return jsonify( {"success": False, "message": f"{event_id} must be set and valid."}, 404 diff --git a/frigate/output.py b/frigate/output.py index 942bed12e..038835313 100644 --- a/frigate/output.py +++ b/frigate/output.py @@ -156,7 +156,12 @@ class BroadcastThread(threading.Thread): class BirdsEyeFrameManager: - def __init__(self, config: FrigateConfig, frame_manager: SharedMemoryFrameManager): + def __init__( + self, + config: FrigateConfig, + frame_manager: SharedMemoryFrameManager, + stop_event: mp.Event, + ): self.config = config self.mode = config.birdseye.mode self.frame_manager = frame_manager @@ -165,6 +170,7 @@ class BirdsEyeFrameManager: self.frame_shape = (height, width) self.yuv_shape = (height * 3 // 2, width) self.frame = np.ndarray(self.yuv_shape, dtype=np.uint8) + self.stop_event = stop_event # initialize the frame as black and with the Frigate logo self.blank_frame = np.zeros(self.yuv_shape, np.uint8) @@ -458,6 +464,9 @@ class BirdsEyeFrameManager: # decrease scaling coefficient until height of all cameras can fit into the birdseye canvas while calculating: + if self.stop_event.is_set(): + return + layout_candidate = calculate_layout( (canvas_width, canvas_height), active_cameras_to_add, @@ -580,7 +589,7 @@ def output_frames(config: FrigateConfig, video_output_queue): for t in broadcasters.values(): t.start() - birdseye_manager = BirdsEyeFrameManager(config, frame_manager) + birdseye_manager = BirdsEyeFrameManager(config, frame_manager, stop_event) if config.birdseye.restream: birdseye_buffer = frame_manager.create( diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index 3ed6540d0..8e40fc6e7 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -20,7 +20,7 @@ import psutil from frigate.config import FrigateConfig, RetainModeEnum from frigate.const import CACHE_DIR, MAX_SEGMENT_DURATION, RECORD_DIR from frigate.models import Event, Recordings -from frigate.types import RecordMetricsTypes +from frigate.types import FeatureMetricsTypes from frigate.util import area, get_video_properties logger = logging.getLogger(__name__) @@ -31,7 +31,7 @@ class RecordingMaintainer(threading.Thread): self, config: FrigateConfig, recordings_info_queue: mp.Queue, - process_info: dict[str, RecordMetricsTypes], + process_info: dict[str, FeatureMetricsTypes], stop_event: MpEvent, ): threading.Thread.__init__(self) diff --git a/frigate/record/record.py b/frigate/record/record.py index ab6cd3450..530adc031 100644 --- a/frigate/record/record.py +++ b/frigate/record/record.py @@ -14,7 +14,7 @@ from frigate.config import FrigateConfig from frigate.models import Event, Recordings, RecordingsToDelete, Timeline from frigate.record.cleanup import RecordingCleanup from frigate.record.maintainer import RecordingMaintainer -from frigate.types import RecordMetricsTypes +from frigate.types import FeatureMetricsTypes from frigate.util import listen logger = logging.getLogger(__name__) @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) def manage_recordings( config: FrigateConfig, recordings_info_queue: mp.Queue, - process_info: dict[str, RecordMetricsTypes], + process_info: dict[str, FeatureMetricsTypes], ) -> None: stop_event = mp.Event() diff --git a/frigate/types.py b/frigate/types.py index 23751d499..9083ade33 100644 --- a/frigate/types.py +++ b/frigate/types.py @@ -25,7 +25,8 @@ class CameraMetricsTypes(TypedDict): skipped_fps: Synchronized -class RecordMetricsTypes(TypedDict): +class FeatureMetricsTypes(TypedDict): + audio_enabled: Synchronized record_enabled: Synchronized diff --git a/web/__test__/handlers.js b/web/__test__/handlers.js index d7f2fb5a6..7ea1f90a5 100644 --- a/web/__test__/handlers.js +++ b/web/__test__/handlers.js @@ -16,6 +16,7 @@ export const handlers = [ front: { name: 'front', objects: { track: ['taco', 'cat', 'dog'] }, + audio: { enabled: false, enabled_in_config: false }, record: { enabled: true, enabled_in_config: true }, detect: { width: 1280, height: 720 }, snapshots: {}, @@ -25,6 +26,7 @@ export const handlers = [ side: { name: 'side', objects: { track: ['taco', 'cat', 'dog'] }, + audio: { enabled: false, enabled_in_config: false }, record: { enabled: false, enabled_in_config: true }, detect: { width: 1280, height: 720 }, snapshots: {}, diff --git a/web/src/api/__tests__/ws.test.jsx b/web/src/api/__tests__/ws.test.jsx index 3b0e3420a..373f6abea 100644 --- a/web/src/api/__tests__/ws.test.jsx +++ b/web/src/api/__tests__/ws.test.jsx @@ -113,8 +113,8 @@ describe('WsProvider', () => { vi.spyOn(Date, 'now').mockReturnValue(123456); const config = { cameras: { - front: { name: 'front', detect: { enabled: true }, record: { enabled: false }, snapshots: { enabled: true } }, - side: { name: 'side', detect: { enabled: false }, record: { enabled: false }, snapshots: { enabled: false } }, + front: { name: 'front', detect: { enabled: true }, record: { enabled: false }, snapshots: { enabled: true }, audio: { enabled: false } }, + side: { name: 'side', detect: { enabled: false }, record: { enabled: false }, snapshots: { enabled: false }, audio: { enabled: false } }, }, }; render( diff --git a/web/src/api/ws.jsx b/web/src/api/ws.jsx index 0867ed0a4..8324632be 100644 --- a/web/src/api/ws.jsx +++ b/web/src/api/ws.jsx @@ -41,10 +41,11 @@ export function WsProvider({ useEffect(() => { Object.keys(config.cameras).forEach((camera) => { - const { name, record, detect, snapshots } = config.cameras[camera]; + const { name, record, detect, snapshots, audio } = config.cameras[camera]; dispatch({ topic: `${name}/recordings/state`, payload: record.enabled ? 'ON' : 'OFF', retain: false }); dispatch({ topic: `${name}/detect/state`, payload: detect.enabled ? 'ON' : 'OFF', retain: false }); dispatch({ topic: `${name}/snapshots/state`, payload: snapshots.enabled ? 'ON' : 'OFF', retain: false }); + dispatch({ topic: `${name}/audio/state`, payload: audio.enabled ? 'ON' : 'OFF', retain: false }); }); }, [config]); @@ -120,6 +121,15 @@ export function useSnapshotsState(camera) { return { payload, send, connected }; } +export function useAudioState(camera) { + const { + value: { payload }, + send, + connected, + } = useWs(`${camera}/audio/state`, `${camera}/audio/set`); + return { payload, send, connected }; +} + export function usePtzCommand(camera) { const { value: { payload }, diff --git a/web/src/icons/Audio.jsx b/web/src/icons/Audio.jsx new file mode 100644 index 000000000..cec783854 --- /dev/null +++ b/web/src/icons/Audio.jsx @@ -0,0 +1,36 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function Snapshot({ className = 'h-6 w-6', stroke = 'currentColor', onClick = () => {} }) { + return ( + + + + + + ); +} + +export default memo(Snapshot); diff --git a/web/src/routes/Cameras.jsx b/web/src/routes/Cameras.jsx index 1e2bbf903..2298b992e 100644 --- a/web/src/routes/Cameras.jsx +++ b/web/src/routes/Cameras.jsx @@ -2,10 +2,11 @@ import { h, Fragment } from 'preact'; import ActivityIndicator from '../components/ActivityIndicator'; import Card from '../components/Card'; import CameraImage from '../components/CameraImage'; +import AudioIcon from '../icons/Audio'; import ClipIcon from '../icons/Clip'; import MotionIcon from '../icons/Motion'; import SnapshotIcon from '../icons/Snapshot'; -import { useDetectState, useRecordingsState, useSnapshotsState } from '../api/ws'; +import { useAudioState, useDetectState, useRecordingsState, useSnapshotsState } from '../api/ws'; import { useMemo } from 'preact/hooks'; import useSWR from 'swr'; @@ -43,6 +44,7 @@ function Camera({ name, config }) { const { payload: detectValue, send: sendDetect } = useDetectState(name); const { payload: recordValue, send: sendRecordings } = useRecordingsState(name); const { payload: snapshotValue, send: sendSnapshots } = useSnapshotsState(name); + const { payload: audioValue, send: sendAudio } = useAudioState(name); const href = `/cameras/${name}`; const buttons = useMemo(() => { return [ @@ -50,10 +52,9 @@ function Camera({ name, config }) { { name: 'Recordings', href: `/recording/${name}` }, ]; }, [name]); - const cleanName = useMemo( - () => { return `${name.replaceAll('_', ' ')}` }, - [name] - ); + const cleanName = useMemo(() => { + return `${name.replaceAll('_', ' ')}`; + }, [name]); const icons = useMemo( () => [ { @@ -65,7 +66,9 @@ function Camera({ name, config }) { }, }, { - name: config.record.enabled_in_config ? `Toggle recordings ${recordValue === 'ON' ? 'off' : 'on'}` : 'Recordings must be enabled in the config to be turned on in the UI.', + name: config.record.enabled_in_config + ? `Toggle recordings ${recordValue === 'ON' ? 'off' : 'on'}` + : 'Recordings must be enabled in the config to be turned on in the UI.', icon: ClipIcon, color: config.record.enabled_in_config ? (recordValue === 'ON' ? 'blue' : 'gray') : 'red', onClick: () => { @@ -82,11 +85,27 @@ function Camera({ name, config }) { sendSnapshots(snapshotValue === 'ON' ? 'OFF' : 'ON', true); }, }, - ], - [config, detectValue, sendDetect, recordValue, sendRecordings, snapshotValue, sendSnapshots] + config.audio.enabled_in_config + ? { + name: `Toggle audio detection ${audioValue === 'ON' ? 'off' : 'on'}`, + icon: AudioIcon, + color: audioValue === 'ON' ? 'blue' : 'gray', + onClick: () => { + sendAudio(audioValue === 'ON' ? 'OFF' : 'ON', true); + }, + } + : null, + ].filter((button) => button != null), + [config, audioValue, sendAudio, detectValue, sendDetect, recordValue, sendRecordings, snapshotValue, sendSnapshots] ); return ( - } /> + } + /> ); } From 7ee17c7af8715ab28f8da42a030ebdcb60acd154 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 1 Jul 2023 10:19:14 -0600 Subject: [PATCH 17/33] Fix small audio events details (#6978) * Fix missed audio details * Add mqtt docs * Delete instead of setting to None --- docs/docs/configuration/index.md | 5 +++-- docs/docs/integrations/mqtt.md | 12 ++++++++++-- frigate/comms/mqtt.py | 1 + frigate/events/audio.py | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 8915db6b3..f23a32270 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -467,10 +467,11 @@ cameras: # Required: the path to the stream # NOTE: path may include environment variables, which must begin with 'FRIGATE_' and be referenced in {} - path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 - # Required: list of roles for this stream. valid values are: detect,record,rtmp - # NOTICE: In addition to assigning the record and rtmp roles, + # Required: list of roles for this stream. valid values are: audio,detect,record,rtmp + # NOTICE: In addition to assigning the audio, record, and rtmp roles, # they must also be enabled in the camera config. roles: + - audio - detect - record - rtmp diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index cde760559..229285676 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -109,11 +109,19 @@ Same data available at `/api/stats` published at a configurable interval. ### `frigate//detect/set` -Topic to turn detection for a camera on and off. Expected values are `ON` and `OFF`. +Topic to turn object detection for a camera on and off. Expected values are `ON` and `OFF`. ### `frigate//detect/state` -Topic with current state of detection for a camera. Published values are `ON` and `OFF`. +Topic with current state of object detection for a camera. Published values are `ON` and `OFF`. + +### `frigate//audio/set` + +Topic to turn audio detection for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//audio/state` + +Topic with current state of audio detection for a camera. Published values are `ON` and `OFF`. ### `frigate//recordings/set` diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py index 4ddfbe7f1..2859a04a2 100644 --- a/frigate/comms/mqtt.py +++ b/frigate/comms/mqtt.py @@ -149,6 +149,7 @@ class MqttClient(Communicator): # type: ignore[misc] "recordings", "snapshots", "detect", + "audio", "motion", "improve_contrast", "motion_threshold", diff --git a/frigate/events/audio.py b/frigate/events/audio.py index 4f40334d4..e1d25e018 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -206,7 +206,7 @@ class AudioEventMaintainer(threading.Thread): now - detection.get("last_detection", now) > self.config.audio.max_not_heard ): - self.detections[detection["label"]] = None + del self.detections[detection["label"]] requests.put( f"{FRIGATE_LOCALHOST}/api/events/{detection['id']}/end", json={ From c25367221e6702677b575320a3ef7b9345346be7 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 1 Jul 2023 15:33:47 -0600 Subject: [PATCH 18/33] Fix audio events not being ended (#6981) * Fix audio events not being ended * Fix audio events not being ended correctly * Clean up debug code --- frigate/events/audio.py | 5 ++++- frigate/events/maintainer.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frigate/events/audio.py b/frigate/events/audio.py index e1d25e018..164c9c62d 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -202,11 +202,13 @@ class AudioEventMaintainer(threading.Thread): now = datetime.datetime.now().timestamp() for detection in self.detections.values(): + if not detection: + continue + if ( now - detection.get("last_detection", now) > self.config.audio.max_not_heard ): - del self.detections[detection["label"]] requests.put( f"{FRIGATE_LOCALHOST}/api/events/{detection['id']}/end", json={ @@ -214,6 +216,7 @@ class AudioEventMaintainer(threading.Thread): + self.config.record.events.post_capture }, ) + self.detections[detection["label"]] = None def restart_audio_pipe(self) -> None: try: diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index f024f0be6..d688f7dfe 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -239,6 +239,6 @@ class EventProcessor(threading.Thread): } try: - Event.update(event).execute() + Event.update(event).where(Event.id == event_data["id"]).execute() except Exception: logger.warning(f"Failed to update manual event: {event_data['id']}") From 83edf9574ec8fac0335d4100bfa70fad6693bd41 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 2 Jul 2023 06:45:45 -0600 Subject: [PATCH 19/33] Scale birdseye layout up to max size after it has been calculated (#6825) * Scale layout up to max size after it has been calculated * Limit portrait cameras to taking up 2 rows * Fix bug * Fix birdsye not removing cameras once objects are no longer visible * Fix lint --- frigate/output.py | 260 +++++++++++++++++++++++++--------------------- 1 file changed, 142 insertions(+), 118 deletions(-) diff --git a/frigate/output.py b/frigate/output.py index 038835313..ab928efb5 100644 --- a/frigate/output.py +++ b/frigate/output.py @@ -276,119 +276,6 @@ class BirdsEyeFrameManager: def update_frame(self): """Update to a new frame for birdseye.""" - def calculate_layout( - canvas, cameras_to_add: list[str], coefficient - ) -> tuple[any]: - """Calculate the optimal layout for 2+ cameras.""" - camera_layout: list[list[any]] = [] - camera_layout.append([]) - canvas_gcd = math.gcd(canvas[0], canvas[1]) - canvas_aspect_x = (canvas[0] / canvas_gcd) * coefficient - canvas_aspect_y = (canvas[0] / canvas_gcd) * coefficient - starting_x = 0 - x = starting_x - y = 0 - y_i = 0 - max_y = 0 - for camera in cameras_to_add: - camera_dims = self.cameras[camera]["dimensions"].copy() - camera_gcd = math.gcd(camera_dims[0], camera_dims[1]) - camera_aspect_x = camera_dims[0] / camera_gcd - camera_aspect_y = camera_dims[1] / camera_gcd - - if round(camera_aspect_x / camera_aspect_y, 1) == 1.8: - # account for slightly off 16:9 cameras - camera_aspect_x = 16 - camera_aspect_y = 9 - elif round(camera_aspect_x / camera_aspect_y, 1) == 1.3: - # make 4:3 cameras the same relative size as 16:9 - camera_aspect_x = 12 - camera_aspect_y = 9 - - if camera_dims[1] > camera_dims[0]: - portrait = True - else: - portrait = False - - if (x + camera_aspect_x) <= canvas_aspect_x: - # insert if camera can fit on current row - camera_layout[y_i].append( - ( - camera, - ( - camera_aspect_x, - camera_aspect_y, - ), - ) - ) - - if portrait: - starting_x = camera_aspect_x - else: - max_y = max( - max_y, - camera_aspect_y, - ) - - x += camera_aspect_x - else: - # move on to the next row and insert - y += max_y - y_i += 1 - camera_layout.append([]) - x = starting_x - - if x + camera_aspect_x > canvas_aspect_x: - return None - - camera_layout[y_i].append( - ( - camera, - (camera_aspect_x, camera_aspect_y), - ) - ) - x += camera_aspect_x - - if y + max_y > canvas_aspect_y: - return None - - row_height = int(canvas_height / coefficient) - - final_camera_layout = [] - starting_x = 0 - y = 0 - - for row in camera_layout: - final_row = [] - x = starting_x - for cameras in row: - camera_dims = self.cameras[cameras[0]]["dimensions"].copy() - - if camera_dims[1] > camera_dims[0]: - scaled_height = int(row_height * coefficient) - scaled_width = int( - scaled_height * camera_dims[0] / camera_dims[1] - ) - starting_x = scaled_width - else: - scaled_height = row_height - scaled_width = int( - scaled_height * camera_dims[0] / camera_dims[1] - ) - - if ( - x + scaled_width > canvas_width - or y + scaled_height > canvas_height - ): - return None - - final_row.append((cameras[0], (x, y, scaled_width, scaled_height))) - x += scaled_width - y += row_height - final_camera_layout.append(final_row) - - return final_camera_layout - # determine how many cameras are tracking objects within the last 30 seconds active_cameras = set( [ @@ -411,10 +298,8 @@ class BirdsEyeFrameManager: self.clear_frame() return True - # check if we need to reset the layout because there are new cameras to add - reset_layout = ( - True if len(active_cameras.difference(self.active_cameras)) > 0 else False - ) + # check if we need to reset the layout because there is a different number of cameras + reset_layout = len(self.active_cameras) - len(active_cameras) != 0 # reset the layout if it needs to be different if reset_layout: @@ -467,7 +352,7 @@ class BirdsEyeFrameManager: if self.stop_event.is_set(): return - layout_candidate = calculate_layout( + layout_candidate = self.calculate_layout( (canvas_width, canvas_height), active_cameras_to_add, coefficient, @@ -493,6 +378,145 @@ class BirdsEyeFrameManager: return True + def calculate_layout( + self, canvas, cameras_to_add: list[str], coefficient + ) -> tuple[any]: + """Calculate the optimal layout for 2+ cameras.""" + + def map_layout(row_height: int): + """Map the calculated layout.""" + candidate_layout = [] + starting_x = 0 + x = 0 + max_width = 0 + y = 0 + + for row in camera_layout: + final_row = [] + max_width = max(max_width, x) + x = starting_x + for cameras in row: + camera_dims = self.cameras[cameras[0]]["dimensions"].copy() + + if camera_dims[1] > camera_dims[0]: + scaled_height = int(row_height * 2) + scaled_width = int( + scaled_height * camera_dims[0] / camera_dims[1] + ) + starting_x = scaled_width + else: + scaled_height = row_height + scaled_width = int( + scaled_height * camera_dims[0] / camera_dims[1] + ) + + # layout is too large + if ( + x + scaled_width > canvas_width + or y + scaled_height > canvas_height + ): + return 0, 0, None + + final_row.append((cameras[0], (x, y, scaled_width, scaled_height))) + x += scaled_width + + y += row_height + candidate_layout.append(final_row) + + return max_width, y, candidate_layout + + canvas_width = canvas[0] + canvas_height = canvas[1] + camera_layout: list[list[any]] = [] + camera_layout.append([]) + canvas_gcd = math.gcd(canvas[0], canvas[1]) + canvas_aspect_x = (canvas[0] / canvas_gcd) * coefficient + canvas_aspect_y = (canvas[0] / canvas_gcd) * coefficient + starting_x = 0 + x = starting_x + y = 0 + y_i = 0 + max_y = 0 + for camera in cameras_to_add: + camera_dims = self.cameras[camera]["dimensions"].copy() + camera_gcd = math.gcd(camera_dims[0], camera_dims[1]) + camera_aspect_x = camera_dims[0] / camera_gcd + camera_aspect_y = camera_dims[1] / camera_gcd + + if round(camera_aspect_x / camera_aspect_y, 1) == 1.8: + # account for slightly off 16:9 cameras + camera_aspect_x = 16 + camera_aspect_y = 9 + elif round(camera_aspect_x / camera_aspect_y, 1) == 1.3: + # make 4:3 cameras the same relative size as 16:9 + camera_aspect_x = 12 + camera_aspect_y = 9 + + if camera_dims[1] > camera_dims[0]: + portrait = True + else: + portrait = False + + if (x + camera_aspect_x) <= canvas_aspect_x: + # insert if camera can fit on current row + camera_layout[y_i].append( + ( + camera, + ( + camera_aspect_x, + camera_aspect_y, + ), + ) + ) + + if portrait: + starting_x = camera_aspect_x + else: + max_y = max( + max_y, + camera_aspect_y, + ) + + x += camera_aspect_x + else: + # move on to the next row and insert + y += max_y + y_i += 1 + camera_layout.append([]) + x = starting_x + + if x + camera_aspect_x > canvas_aspect_x: + return None + + camera_layout[y_i].append( + ( + camera, + (camera_aspect_x, camera_aspect_y), + ) + ) + x += camera_aspect_x + + if y + max_y > canvas_aspect_y: + return None + + row_height = int(canvas_height / coefficient) + total_width, total_height, standard_candidate_layout = map_layout(row_height) + + # layout can't be optimized more + if total_width / canvas_width >= 0.99: + return standard_candidate_layout + + scale_up_percent = min( + 1 - (total_width / canvas_width), 1 - (total_height / canvas_height) + ) + row_height = int(row_height * (1 + round(scale_up_percent, 1))) + _, _, scaled_layout = map_layout(row_height) + + if scaled_layout: + return scaled_layout + else: + return standard_candidate_layout + def update(self, camera, object_count, motion_count, frame_time, frame) -> bool: # don't process if birdseye is disabled for this camera camera_config = self.config.cameras[camera].birdseye From 58c6ef1e1244c6b9f6969d5f594090167a6f25bf Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 3 Jul 2023 08:48:00 -0600 Subject: [PATCH 20/33] Add designator when events are from the api (#6997) * Add designator when events are custom * Add type field and set via API --- frigate/events/audio.py | 2 +- frigate/events/external.py | 2 ++ frigate/events/maintainer.py | 9 +++++---- frigate/http.py | 1 + 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frigate/events/audio.py b/frigate/events/audio.py index 164c9c62d..b34deb315 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -187,7 +187,7 @@ class AudioEventMaintainer(threading.Thread): else: resp = requests.post( f"{FRIGATE_LOCALHOST}/api/events/{self.config.name}/{label}/create", - json={"duration": None}, + json={"duration": None, "source_type": "audio"}, ) if resp.status_code == 200: diff --git a/frigate/events/external.py b/frigate/events/external.py index 25ba289f2..20456b9cb 100644 --- a/frigate/events/external.py +++ b/frigate/events/external.py @@ -29,6 +29,7 @@ class ExternalEventProcessor: self, camera: str, label: str, + source_type: str, sub_label: Optional[str], duration: Optional[int], include_recording: bool, @@ -61,6 +62,7 @@ class ExternalEventProcessor: "thumbnail": thumbnail, "has_clip": camera_config.record.enabled and include_recording, "has_snapshot": True, + "type": source_type, }, ) ) diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index d688f7dfe..34cb01261 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -193,6 +193,7 @@ class EventProcessor(threading.Thread): "score": score, "top_score": event_data["top_score"], "attributes": attributes, + "type": "object", }, } @@ -216,8 +217,8 @@ class EventProcessor(threading.Thread): del self.events_in_process[event_data["id"]] self.event_processed_queue.put((event_data["id"], camera)) - def handle_external_detection(self, type: str, event_data: Event) -> None: - if type == "new": + def handle_external_detection(self, event_type: str, event_data: Event) -> None: + if event_type == "new": event = { Event.id: event_data["id"], Event.label: event_data["label"], @@ -229,10 +230,10 @@ class EventProcessor(threading.Thread): Event.has_clip: event_data["has_clip"], Event.has_snapshot: event_data["has_snapshot"], Event.zones: [], - Event.data: {}, + Event.data: {"type": event_data["type"]}, } Event.insert(event).execute() - elif type == "end": + elif event_type == "end": event = { Event.id: event_data["id"], Event.end_time: event_data["end_time"], diff --git a/frigate/http.py b/frigate/http.py index f3632a0cf..57b2103e7 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -884,6 +884,7 @@ def create_event(camera_name, label): event_id = current_app.external_processor.create_manual_event( camera_name, label, + json.get("source_type", "api"), json.get("sub_label", None), json.get("duration", 30), json.get("include_recording", True), From f9057044796d524e5f3aa34cf61e95a2d43173f4 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Mon, 3 Jul 2023 17:48:29 +0300 Subject: [PATCH 21/33] UI: add audio process stats to System page (#6993) * Add audio process PID to the list of processes and log the start of the audio process * Update audio process PID key in processes dictionary to "audioDetector" instead of "audio". --- frigate/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frigate/app.py b/frigate/app.py index ccfbd4696..4d4aa5dd4 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -437,6 +437,7 @@ class FrigateApp: ) audio_process.daemon = True audio_process.start() + self.processes["audioDetector"] = audio_process.pid or 0 logger.info(f"Audio process started: {audio_process.pid}") def start_timeline_processor(self) -> None: From 1171f01683ac72fc57474c6e5368eb62e0d64257 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Mon, 3 Jul 2023 17:49:14 +0300 Subject: [PATCH 22/33] Refactor camera rendering logic in System component to only render enabled cameras (#6992) --- web/src/routes/System.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/routes/System.jsx b/web/src/routes/System.jsx index 82c8d619d..8f4f1436f 100644 --- a/web/src/routes/System.jsx +++ b/web/src/routes/System.jsx @@ -334,7 +334,7 @@ export default function System() { ) : (
- {cameraNames.map((camera) => ( + {cameraNames.map((camera) => ( config.cameras[camera]["enabled"] && (
{camera.replaceAll('_', ' ')} @@ -406,7 +406,7 @@ export default function System() {
-
+
) ))} )} From 12d4a47e3d97248f961858bbf5d83a5fdf8386a8 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Mon, 3 Jul 2023 17:50:25 +0300 Subject: [PATCH 23/33] End audio event and update detections if successful, otherwise log a warning if ending audio event fails with a specific status code (#6984) --- frigate/events/audio.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frigate/events/audio.py b/frigate/events/audio.py index b34deb315..488c94fcc 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -209,14 +209,19 @@ class AudioEventMaintainer(threading.Thread): now - detection.get("last_detection", now) > self.config.audio.max_not_heard ): - requests.put( + resp = requests.put( f"{FRIGATE_LOCALHOST}/api/events/{detection['id']}/end", json={ "end_time": detection["last_detection"] + self.config.record.events.post_capture }, ) - self.detections[detection["label"]] = None + if resp.status_code == 200: + self.detections[detection["label"]] = None + else: + logger.warn( + f"Failed to end audio event {detection['id']} with status code {resp.status_code}" + ) def restart_audio_pipe(self) -> None: try: From 998bffe70654e89bac993ccb34f3ef8980896fcc Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 6 Jul 2023 06:25:37 -0600 Subject: [PATCH 24/33] Fix min region size not being divisible by 4 (#7040) * Fix min region size not being divisible by 4 * Simplify half calculation --- frigate/video.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/frigate/video.py b/frigate/video.py index 8980fcde0..2ba82a577 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -14,7 +14,7 @@ import cv2 import numpy as np from setproctitle import setproctitle -from frigate.config import CameraConfig, DetectConfig +from frigate.config import CameraConfig, DetectConfig, ModelConfig from frigate.const import ALL_ATTRIBUTE_LABELS, ATTRIBUTE_LABEL_MAP, CACHE_DIR from frigate.detectors.detector_config import PixelFormatEnum from frigate.log import LogPipe @@ -95,7 +95,17 @@ def filtered(obj, objects_to_track, object_filters): return False -def create_tensor_input(frame, model_config, region): +def get_min_region_size(model_config: ModelConfig) -> int: + """Get the min region size and ensure it is divisible by 4.""" + half = int(max(model_config.height, model_config.width) / 2) + + if half % 4 == 0: + return half + + return int((half + 3) / 4) * 4 + + +def create_tensor_input(frame, model_config: ModelConfig, region): if model_config.input_pixel_format == PixelFormatEnum.rgb: cropped_frame = yuv_region_2_rgb(frame, region) elif model_config.input_pixel_format == PixelFormatEnum.bgr: @@ -719,7 +729,7 @@ def process_frames( camera_name: str, frame_queue: mp.Queue, frame_shape, - model_config, + model_config: ModelConfig, detect_config: DetectConfig, frame_manager: FrameManager, motion_detector: MotionDetector, @@ -743,7 +753,7 @@ def process_frames( startup_scan_counter = 0 - region_min_size = int(max(model_config.height, model_config.width) / 2) + region_min_size = get_min_region_size(model_config) while not stop_event.is_set(): if exit_on_empty and frame_queue.empty(): From 0f68fbc8db9158c0d8b7523e003712ed7c35607d Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 6 Jul 2023 06:26:53 -0600 Subject: [PATCH 25/33] Use pre capture for custom events (#7038) * Use pre capture for custom events * Formatting --- frigate/events/external.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frigate/events/external.py b/frigate/events/external.py index 20456b9cb..23439f2bd 100644 --- a/frigate/events/external.py +++ b/frigate/events/external.py @@ -57,8 +57,12 @@ class ExternalEventProcessor: "label": label, "sub_label": sub_label, "camera": camera, - "start_time": now, - "end_time": now + duration if duration is not None else None, + "start_time": now - camera_config.record.events.pre_capture, + "end_time": now + + duration + + camera_config.record.events.post_capture + if duration is not None + else None, "thumbnail": thumbnail, "has_clip": camera_config.record.enabled and include_recording, "has_snapshot": True, From 339b6944f13ba7b3ae3c924472b821c0a7ce66a8 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 6 Jul 2023 06:30:05 -0600 Subject: [PATCH 26/33] Force birdseye cameras into standard aspect ratios (#7026) * Force birdseye cameras into standard aspect ratios * Clarify comment * Formatting * Actually use the calculated aspect ratio when building the layout * Fix Y aspect * Force canvas into known aspect ratio as well * Save canvas size and don't recalculate * Cache coefficients that are used for different size layouts * Further optimize calculations to not be done multiple times --- frigate/output.py | 123 +++++++++++++++++++++++++++++----------------- 1 file changed, 79 insertions(+), 44 deletions(-) diff --git a/frigate/output.py b/frigate/output.py index ab928efb5..24f4c45cc 100644 --- a/frigate/output.py +++ b/frigate/output.py @@ -29,6 +29,61 @@ from frigate.util import SharedMemoryFrameManager, copy_yuv_to_position, get_yuv logger = logging.getLogger(__name__) +def get_standard_aspect_ratio(width, height) -> tuple[int, int]: + """Ensure that only standard aspect ratios are used.""" + known_aspects = [ + (16, 9), + (9, 16), + (32, 9), + (12, 9), + (9, 12), + ] # aspects are scaled to have common relative size + known_aspects_ratios = list( + map(lambda aspect: aspect[0] / aspect[1], known_aspects) + ) + closest = min( + known_aspects_ratios, + key=lambda x: abs(x - (width / height)), + ) + return known_aspects[known_aspects_ratios.index(closest)] + + +class Canvas: + def __init__(self, canvas_width: int, canvas_height: int) -> None: + gcd = math.gcd(canvas_width, canvas_height) + self.aspect = get_standard_aspect_ratio( + (canvas_width / gcd), (canvas_height / gcd) + ) + self.width = canvas_width + self.height = (self.width * self.aspect[1]) / self.aspect[0] + self.coefficient_cache: dict[int, int] = {} + self.aspect_cache: dict[str, tuple[int, int]] = {} + + def get_aspect(self, coefficient: int) -> tuple[int, int]: + return (self.aspect[0] * coefficient, self.aspect[1] * coefficient) + + def get_coefficient(self, camera_count: int) -> int: + return self.coefficient_cache.get(camera_count, 2) + + def set_coefficient(self, camera_count: int, coefficient: int) -> None: + self.coefficient_cache[camera_count] = coefficient + + def get_camera_aspect( + self, cam_name: str, camera_width: int, camera_height: int + ) -> tuple[int, int]: + cached = self.aspect_cache.get(cam_name) + + if cached: + return cached + + gcd = math.gcd(camera_width, camera_height) + camera_aspect = get_standard_aspect_ratio( + camera_width / gcd, camera_height / gcd + ) + self.aspect_cache[cam_name] = camera_aspect + return camera_aspect + + class FFMpegConverter: def __init__( self, @@ -170,6 +225,7 @@ class BirdsEyeFrameManager: self.frame_shape = (height, width) self.yuv_shape = (height * 3 // 2, width) self.frame = np.ndarray(self.yuv_shape, dtype=np.uint8) + self.canvas = Canvas(width, height) self.stop_event = stop_event # initialize the frame as black and with the Frigate logo @@ -318,16 +374,15 @@ class BirdsEyeFrameManager: ), ) - canvas_width = self.config.birdseye.width - canvas_height = self.config.birdseye.height - if len(active_cameras) == 1: # show single camera as fullscreen camera = active_cameras_to_add[0] camera_dims = self.cameras[camera]["dimensions"].copy() - scaled_width = int(canvas_height * camera_dims[0] / camera_dims[1]) + scaled_width = int(self.canvas.height * camera_dims[0] / camera_dims[1]) coefficient = ( - 1 if scaled_width <= canvas_width else canvas_width / scaled_width + 1 + if scaled_width <= self.canvas.width + else self.canvas.width / scaled_width ) self.camera_layout = [ [ @@ -337,14 +392,14 @@ class BirdsEyeFrameManager: 0, 0, int(scaled_width * coefficient), - int(canvas_height * coefficient), + int(self.canvas.height * coefficient), ), ) ] ] else: # calculate optimal layout - coefficient = 2 + coefficient = self.canvas.get_coefficient(len(active_cameras)) calculating = True # decrease scaling coefficient until height of all cameras can fit into the birdseye canvas @@ -353,7 +408,6 @@ class BirdsEyeFrameManager: return layout_candidate = self.calculate_layout( - (canvas_width, canvas_height), active_cameras_to_add, coefficient, ) @@ -367,6 +421,7 @@ class BirdsEyeFrameManager: return calculating = False + self.canvas.set_coefficient(len(active_cameras), coefficient) self.camera_layout = layout_candidate @@ -378,9 +433,7 @@ class BirdsEyeFrameManager: return True - def calculate_layout( - self, canvas, cameras_to_add: list[str], coefficient - ) -> tuple[any]: + def calculate_layout(self, cameras_to_add: list[str], coefficient) -> tuple[any]: """Calculate the optimal layout for 2+ cameras.""" def map_layout(row_height: int): @@ -397,23 +450,20 @@ class BirdsEyeFrameManager: x = starting_x for cameras in row: camera_dims = self.cameras[cameras[0]]["dimensions"].copy() + camera_aspect = cameras[1] if camera_dims[1] > camera_dims[0]: scaled_height = int(row_height * 2) - scaled_width = int( - scaled_height * camera_dims[0] / camera_dims[1] - ) + scaled_width = int(scaled_height * camera_aspect) starting_x = scaled_width else: scaled_height = row_height - scaled_width = int( - scaled_height * camera_dims[0] / camera_dims[1] - ) + scaled_width = int(scaled_height * camera_aspect) # layout is too large if ( - x + scaled_width > canvas_width - or y + scaled_height > canvas_height + x + scaled_width > self.canvas.width + or y + scaled_height > self.canvas.height ): return 0, 0, None @@ -425,13 +475,9 @@ class BirdsEyeFrameManager: return max_width, y, candidate_layout - canvas_width = canvas[0] - canvas_height = canvas[1] + canvas_aspect_x, canvas_aspect_y = self.canvas.get_aspect(coefficient) camera_layout: list[list[any]] = [] camera_layout.append([]) - canvas_gcd = math.gcd(canvas[0], canvas[1]) - canvas_aspect_x = (canvas[0] / canvas_gcd) * coefficient - canvas_aspect_y = (canvas[0] / canvas_gcd) * coefficient starting_x = 0 x = starting_x y = 0 @@ -439,18 +485,9 @@ class BirdsEyeFrameManager: max_y = 0 for camera in cameras_to_add: camera_dims = self.cameras[camera]["dimensions"].copy() - camera_gcd = math.gcd(camera_dims[0], camera_dims[1]) - camera_aspect_x = camera_dims[0] / camera_gcd - camera_aspect_y = camera_dims[1] / camera_gcd - - if round(camera_aspect_x / camera_aspect_y, 1) == 1.8: - # account for slightly off 16:9 cameras - camera_aspect_x = 16 - camera_aspect_y = 9 - elif round(camera_aspect_x / camera_aspect_y, 1) == 1.3: - # make 4:3 cameras the same relative size as 16:9 - camera_aspect_x = 12 - camera_aspect_y = 9 + camera_aspect_x, camera_aspect_y = self.canvas.get_camera_aspect( + camera, camera_dims[0], camera_dims[1] + ) if camera_dims[1] > camera_dims[0]: portrait = True @@ -462,10 +499,7 @@ class BirdsEyeFrameManager: camera_layout[y_i].append( ( camera, - ( - camera_aspect_x, - camera_aspect_y, - ), + camera_aspect_x / camera_aspect_y, ) ) @@ -491,7 +525,7 @@ class BirdsEyeFrameManager: camera_layout[y_i].append( ( camera, - (camera_aspect_x, camera_aspect_y), + camera_aspect_x / camera_aspect_y, ) ) x += camera_aspect_x @@ -499,15 +533,16 @@ class BirdsEyeFrameManager: if y + max_y > canvas_aspect_y: return None - row_height = int(canvas_height / coefficient) + row_height = int(self.canvas.height / coefficient) total_width, total_height, standard_candidate_layout = map_layout(row_height) # layout can't be optimized more - if total_width / canvas_width >= 0.99: + if total_width / self.canvas.width >= 0.99: return standard_candidate_layout scale_up_percent = min( - 1 - (total_width / canvas_width), 1 - (total_height / canvas_height) + 1 - (total_width / self.canvas.width), + 1 - (total_height / self.canvas.height), ) row_height = int(row_height * (1 + round(scale_up_percent, 1))) _, _, scaled_layout = map_layout(row_height) From f30ba254445221d1f9fb016457774c11e704048a Mon Sep 17 00:00:00 2001 From: Andrew Reiter Date: Thu, 6 Jul 2023 08:31:17 -0400 Subject: [PATCH 27/33] Reduce framerate before downscaling (#7022) * Reduce framerate before downscaling It is cheaper to drop frames and downscale those that remain than it is to downscale all frames and then drop some of them. This is achieved with the filter chain `-cv fps=FPS,scale=W:H`, and perhaps was the original intention. The plain `-r` and `-s` flags do not execute in order though - they each put themselves at the *end* of the filterchain, so `-r FPS -s WxH` actually applies the scale filter first, and then the rate filter. This fix can halve the CPU used by the detect ffmpeg process. * Bring back hard rate limits --- frigate/ffmpeg_presets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index a2785813c..3bbc36868 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -107,14 +107,14 @@ PRESETS_HW_ACCEL_DECODE = { } PRESETS_HW_ACCEL_SCALE = { - "preset-rpi-32-h264": "-r {0} -s {1}x{2}", - "preset-rpi-64-h264": "-r {0} -s {1}x{2}", + "preset-rpi-32-h264": "-r {0} -vf fps={0},scale={1}:{2}", + "preset-rpi-64-h264": "-r {0} -vf fps={0},scale={1}:{2}", "preset-vaapi": "-r {0} -vf fps={0},scale_vaapi=w={1}:h={2},hwdownload,format=yuv420p", "preset-intel-qsv-h264": "-r {0} -vf vpp_qsv=framerate={0}:w={1}:h={2}:format=nv12,hwdownload,format=nv12,format=yuv420p", "preset-intel-qsv-h265": "-r {0} -vf vpp_qsv=framerate={0}:w={1}:h={2}:format=nv12,hwdownload,format=nv12,format=yuv420p", "preset-nvidia-h264": "-r {0} -vf fps={0},scale_cuda=w={1}:h={2}:format=nv12,hwdownload,format=nv12,format=yuv420p", "preset-nvidia-h265": "-r {0} -vf fps={0},scale_cuda=w={1}:h={2}:format=nv12,hwdownload,format=nv12,format=yuv420p", - "default": "-r {0} -s {1}x{2}", + "default": "-r {0} -vf fps={0},scale={1}:{2}", } PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = { From 325205740ba4d7c8b206e2cf159193f9009d12b5 Mon Sep 17 00:00:00 2001 From: Andrew Reiter Date: Thu, 6 Jul 2023 08:35:26 -0400 Subject: [PATCH 28/33] Check ffmpeg version instead of checking for presence of BTBN_PATH (#7023) * Check ffmpeg version instead of checking for presence of BTBN_PATH * Query ffmpeg version in s6 run script instead of subprocessing in every import * Define LIBAVFORMAT_VERSION_MAJOR in devcontainer too * Formatting * Default ffmpeg version to current btbn version so unit tests pass --- .devcontainer/post_create.sh | 5 +++++ docker/rootfs/etc/s6-overlay/s6-rc.d/frigate/run | 1 + docker/rootfs/usr/local/go2rtc/create_config.py | 4 ++-- frigate/const.py | 1 - frigate/ffmpeg_presets.py | 7 +++++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh index 9c5dec5bb..1a1832f3b 100755 --- a/.devcontainer/post_create.sh +++ b/.devcontainer/post_create.sh @@ -14,6 +14,11 @@ curl -L https://api.github.com/meta | jq -r '.ssh_keys | .[]' | \ sudo mkdir -p /media/frigate sudo chown -R "$(id -u):$(id -g)" /media/frigate +# When started as a service, LIBAVFORMAT_VERSION_MAJOR is defined in the +# s6 service file. For dev, where frigate is started from an interactive +# shell, we define it in .bashrc instead. +echo 'export LIBAVFORMAT_VERSION_MAJOR=$(ffmpeg -version | grep -Po "libavformat\W+\K\d+")' >> $HOME/.bashrc + make version cd web diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/frigate/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/frigate/run index 0a835550e..f2cc40fcf 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/frigate/run +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/frigate/run @@ -44,6 +44,7 @@ function migrate_db_path() { echo "[INFO] Preparing Frigate..." migrate_db_path +export LIBAVFORMAT_VERSION_MAJOR=$(ffmpeg -version | grep -Po 'libavformat\W+\K\d+') echo "[INFO] Starting Frigate..." diff --git a/docker/rootfs/usr/local/go2rtc/create_config.py b/docker/rootfs/usr/local/go2rtc/create_config.py index 1397adee8..0531b173d 100644 --- a/docker/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/rootfs/usr/local/go2rtc/create_config.py @@ -7,7 +7,7 @@ import sys import yaml sys.path.insert(0, "/opt/frigate") -from frigate.const import BIRDSEYE_PIPE, BTBN_PATH # noqa: E402 +from frigate.const import BIRDSEYE_PIPE # noqa: E402 from frigate.ffmpeg_presets import ( # noqa: E402 parse_preset_hardware_acceleration_encode, ) @@ -71,7 +71,7 @@ elif go2rtc_config["rtsp"].get("default_query") is None: go2rtc_config["rtsp"]["default_query"] = "mp4" # need to replace ffmpeg command when using ffmpeg4 -if not os.path.exists(BTBN_PATH): +if int(os.environ["LIBAVFORMAT_VERSION_MAJOR"]) < 59: if go2rtc_config.get("ffmpeg") is None: go2rtc_config["ffmpeg"] = { "rtsp": "-fflags nobuffer -flags low_delay -stimeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}" diff --git a/frigate/const.py b/frigate/const.py index 20e2b0daa..b6b0e44bd 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -11,7 +11,6 @@ YAML_EXT = (".yaml", ".yml") FRIGATE_LOCALHOST = "http://127.0.0.1:5000" PLUS_ENV_VAR = "PLUS_API_KEY" PLUS_API_HOST = "https://api.frigate.video" -BTBN_PATH = "/usr/lib/btbn-ffmpeg" # Attributes diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 3bbc36868..4c2e16dbb 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -5,7 +5,6 @@ import os from enum import Enum from typing import Any -from frigate.const import BTBN_PATH from frigate.util import vainfo_hwaccel from frigate.version import VERSION @@ -43,7 +42,11 @@ class LibvaGpuSelector: return "" -TIMEOUT_PARAM = "-timeout" if os.path.exists(BTBN_PATH) else "-stimeout" +TIMEOUT_PARAM = ( + "-timeout" + if int(os.getenv("LIBAVFORMAT_VERSION_MAJOR", "59")) >= 59 + else "-stimeout" +) _gpu_selector = LibvaGpuSelector() _user_agent_args = [ From d0891e518338e229bf08e8ca2242349c0346cfa3 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 6 Jul 2023 06:42:17 -0600 Subject: [PATCH 29/33] Ptz cleanup (#6999) * Use _ instead of - for ptz presets * Update mqtt.md * Fix * Formatting --- docs/docs/integrations/mqtt.md | 2 +- frigate/comms/dispatcher.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index 229285676..378eadb3e 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -184,7 +184,7 @@ Topic to send PTZ commands to camera. | Command | Description | | ---------------------- | ----------------------------------------------------------------------------------------- | -| `preset-` | send command to move to preset with name `` | +| `preset_` | send command to move to preset with name `` | | `MOVE_` | send command to continuously move in ``, possible values are [UP, DOWN, LEFT, RIGHT] | | `ZOOM_` | send command to continuously zoom ``, possible values are [IN, OUT] | | `STOP` | send command to stop moving | diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 1c9105ce8..c8f90bad1 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -253,7 +253,7 @@ class Dispatcher: try: if "preset" in payload.lower(): command = OnvifCommandEnum.preset - param = payload.lower().split("-")[1] + param = payload.lower()[payload.index("_") + 1 :] else: command = OnvifCommandEnum[payload.lower()] param = "" From c38c981cd09a97f8e1ab0efea9da0980ae8b75e8 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Thu, 6 Jul 2023 15:56:38 +0300 Subject: [PATCH 30/33] Performance: multiprocessing improvement: step 2 (#6986) * Refactored queues to use faster_fifo instead of mp.Queue * Refactored LimitedQueue to include a counter for the number of items in the queue and updated put and get methods to use the counter * Refactor app.py and util.py to use a custom Queue implementation called LQueue instead of the existing Queue * Refactor put and get methods in LimitedQueue to handle queue size and blocking behavior more efficiently * code format * remove code from other branch (merging fuckup) --- frigate/app.py | 20 ++++++++------- frigate/object_detection.py | 3 ++- frigate/record/maintainer.py | 4 +-- frigate/record/record.py | 3 ++- frigate/util.py | 50 ++++++++++++++++++++++++++++++++++++ frigate/video.py | 5 ++-- 6 files changed, 70 insertions(+), 15 deletions(-) diff --git a/frigate/app.py b/frigate/app.py index 4d4aa5dd4..ed32dee17 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -10,6 +10,7 @@ from multiprocessing.synchronize import Event as MpEvent from types import FrameType from typing import Optional +import faster_fifo as ff import psutil from faster_fifo import Queue from peewee_migrate import Router @@ -46,6 +47,7 @@ from frigate.stats import StatsEmitter, stats_init from frigate.storage import StorageMaintainer from frigate.timeline import TimelineProcessor from frigate.types import CameraMetricsTypes, FeatureMetricsTypes +from frigate.util import LimitedQueue as LQueue from frigate.version import VERSION from frigate.video import capture_camera, track_camera from frigate.watchdog import FrigateWatchdog @@ -56,11 +58,11 @@ logger = logging.getLogger(__name__) class FrigateApp: def __init__(self) -> None: self.stop_event: MpEvent = mp.Event() - self.detection_queue: Queue = mp.Queue() + self.detection_queue: Queue = ff.Queue() self.detectors: dict[str, ObjectDetectProcess] = {} self.detection_out_events: dict[str, MpEvent] = {} self.detection_shms: list[mp.shared_memory.SharedMemory] = [] - self.log_queue: Queue = mp.Queue() + self.log_queue: Queue = ff.Queue() self.plus_api = PlusApi() self.camera_metrics: dict[str, CameraMetricsTypes] = {} self.feature_metrics: dict[str, FeatureMetricsTypes] = {} @@ -156,7 +158,7 @@ class FrigateApp: "ffmpeg_pid": mp.Value("i", 0), # type: ignore[typeddict-item] # issue https://github.com/python/typeshed/issues/8799 # from mypy 0.981 onwards - "frame_queue": mp.Queue(maxsize=2), + "frame_queue": LQueue(maxsize=2), "capture_process": None, "process": None, } @@ -188,22 +190,22 @@ class FrigateApp: def init_queues(self) -> None: # Queues for clip processing - self.event_queue: Queue = mp.Queue() - self.event_processed_queue: Queue = mp.Queue() - self.video_output_queue: Queue = mp.Queue( + self.event_queue: Queue = ff.Queue() + self.event_processed_queue: Queue = ff.Queue() + self.video_output_queue: Queue = LQueue( maxsize=len(self.config.cameras.keys()) * 2 ) # Queue for cameras to push tracked objects to - self.detected_frames_queue: Queue = mp.Queue( + self.detected_frames_queue: Queue = LQueue( maxsize=len(self.config.cameras.keys()) * 2 ) # Queue for recordings info - self.recordings_info_queue: Queue = mp.Queue() + self.recordings_info_queue: Queue = ff.Queue() # Queue for timeline events - self.timeline_queue: Queue = mp.Queue() + self.timeline_queue: Queue = ff.Queue() def init_database(self) -> None: def vacuum_db(db: SqliteExtDatabase) -> None: diff --git a/frigate/object_detection.py b/frigate/object_detection.py index 0a2a7059c..cebd7ff41 100644 --- a/frigate/object_detection.py +++ b/frigate/object_detection.py @@ -7,6 +7,7 @@ import signal import threading from abc import ABC, abstractmethod +import faster_fifo as ff import numpy as np from setproctitle import setproctitle @@ -72,7 +73,7 @@ class LocalObjectDetector(ObjectDetector): def run_detector( name: str, - detection_queue: mp.Queue, + detection_queue: ff.Queue, out_events: dict[str, mp.Event], avg_speed, start, diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index 8e40fc6e7..b4208f2d2 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -3,7 +3,6 @@ import asyncio import datetime import logging -import multiprocessing as mp import os import queue import random @@ -15,6 +14,7 @@ from multiprocessing.synchronize import Event as MpEvent from pathlib import Path from typing import Any, Tuple +import faster_fifo as ff import psutil from frigate.config import FrigateConfig, RetainModeEnum @@ -30,7 +30,7 @@ class RecordingMaintainer(threading.Thread): def __init__( self, config: FrigateConfig, - recordings_info_queue: mp.Queue, + recordings_info_queue: ff.Queue, process_info: dict[str, FeatureMetricsTypes], stop_event: MpEvent, ): diff --git a/frigate/record/record.py b/frigate/record/record.py index 530adc031..0d22342aa 100644 --- a/frigate/record/record.py +++ b/frigate/record/record.py @@ -7,6 +7,7 @@ import threading from types import FrameType from typing import Optional +import faster_fifo as ff from playhouse.sqliteq import SqliteQueueDatabase from setproctitle import setproctitle @@ -22,7 +23,7 @@ logger = logging.getLogger(__name__) def manage_recordings( config: FrigateConfig, - recordings_info_queue: mp.Queue, + recordings_info_queue: ff.Queue, process_info: dict[str, FeatureMetricsTypes], ) -> None: stop_event = mp.Event() diff --git a/frigate/util.py b/frigate/util.py index f535a9572..cc9bb03c9 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -1,18 +1,22 @@ import copy +import ctypes import datetime import json import logging +import multiprocessing import os import re import shlex import signal import subprocess as sp +import time import traceback import urllib.parse from abc import ABC, abstractmethod from collections import Counter from collections.abc import Mapping from multiprocessing import shared_memory +from queue import Empty, Full from typing import Any, AnyStr, Optional, Tuple import cv2 @@ -21,6 +25,8 @@ import psutil import py3nvml.py3nvml as nvml import pytz import yaml +from faster_fifo import DEFAULT_CIRCULAR_BUFFER_SIZE, DEFAULT_TIMEOUT +from faster_fifo import Queue as FFQueue from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS @@ -1218,3 +1224,47 @@ def get_video_properties(url, get_duration=False): result["height"] = round(height) return result + + +class LimitedQueue(FFQueue): + def __init__( + self, + maxsize=0, + max_size_bytes=DEFAULT_CIRCULAR_BUFFER_SIZE, + loads=None, + dumps=None, + ): + super().__init__(max_size_bytes=max_size_bytes, loads=loads, dumps=dumps) + self.maxsize = maxsize + self.size = multiprocessing.RawValue( + ctypes.c_int, 0 + ) # Add a counter for the number of items in the queue + + def put(self, x, block=True, timeout=DEFAULT_TIMEOUT): + if self.maxsize > 0 and self.size.value >= self.maxsize: + if block: + start_time = time.time() + while self.size.value >= self.maxsize: + remaining = timeout - (time.time() - start_time) + if remaining <= 0.0: + raise Full + time.sleep(min(remaining, 0.1)) + else: + raise Full + self.size.value += 1 + return super().put(x, block=block, timeout=timeout) + + def get(self, block=True, timeout=DEFAULT_TIMEOUT): + if self.size.value <= 0 and not block: + raise Empty + self.size.value -= 1 + return super().get(block=block, timeout=timeout) + + def qsize(self): + return self.size + + def empty(self): + return self.qsize() == 0 + + def full(self): + return self.qsize() == self.maxsize diff --git a/frigate/video.py b/frigate/video.py index 2ba82a577..5928a84d5 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -11,6 +11,7 @@ import time from collections import defaultdict import cv2 +import faster_fifo as ff import numpy as np from setproctitle import setproctitle @@ -727,7 +728,7 @@ def get_consolidated_object_detections(detected_object_groups): def process_frames( camera_name: str, - frame_queue: mp.Queue, + frame_queue: ff.Queue, frame_shape, model_config: ModelConfig, detect_config: DetectConfig, @@ -735,7 +736,7 @@ def process_frames( motion_detector: MotionDetector, object_detector: RemoteObjectDetector, object_tracker: ObjectTracker, - detected_objects_queue: mp.Queue, + detected_objects_queue: ff.Queue, process_info: dict, objects_to_track: list[str], object_filters, From 2fae9dcb93f41936ee1907fc0813719c8559fe1b Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Thu, 6 Jul 2023 07:18:39 -0600 Subject: [PATCH 31/33] reduce contention on frame_queue (#6890) * reduce contention on frame_queue don't check if the queue is full, just attempt to add the frame in a non-blocking manner, and then if it fails, skip it * don't check if the frame queue is empty, just try and get from it * Update frigate/video.py Co-authored-by: Blake Blackshear --------- Co-authored-by: Blake Blackshear --- frigate/video.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/frigate/video.py b/frigate/video.py index 5928a84d5..630859b49 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -206,17 +206,16 @@ def capture_frames( frame_rate.update() - # if the queue is full, skip this frame - if frame_queue.full(): + # don't lock the queue to check, just try since it should rarely be full + try: + # add to the queue + frame_queue.put(current_frame.value, False) + # close the frame + frame_manager.close(frame_name) + except queue.Full: + # if the queue is full, skip this frame skipped_eps.update() frame_manager.delete(frame_name) - continue - - # close the frame - frame_manager.close(frame_name) - - # add to the queue - frame_queue.put(current_frame.value) class CameraWatchdog(threading.Thread): @@ -757,13 +756,15 @@ def process_frames( region_min_size = get_min_region_size(model_config) while not stop_event.is_set(): - if exit_on_empty and frame_queue.empty(): - logger.info("Exiting track_objects...") - break - try: - frame_time = frame_queue.get(True, 1) + if exit_on_empty: + frame_time = frame_queue.get(False) + else: + frame_time = frame_queue.get(True, 1) except queue.Empty: + if exit_on_empty: + logger.info("Exiting track_objects...") + break continue current_frame_time.value = frame_time From 606f00867e73366db984017c81c08790ce30cf19 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 6 Jul 2023 08:11:49 -0600 Subject: [PATCH 32/33] Fix go2rtc getting env var (#7052) --- docker/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run index 85c8f9526..fd5fcb568 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run @@ -43,6 +43,8 @@ function get_ip_and_port_from_supervisor() { export FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL="${ip_address}:${webrtc_port}" } +export LIBAVFORMAT_VERSION_MAJOR=$(ffmpeg -version | grep -Po 'libavformat\W+\K\d+') + if [[ ! -f "/dev/shm/go2rtc.yaml" ]]; then echo "[INFO] Preparing go2rtc config..." From baf671b7649e98068bd3b0f03ed8f03adbf46f03 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 6 Jul 2023 08:28:50 -0600 Subject: [PATCH 33/33] Cleanup and organize utils (#7033) * Force birdseye cameras into standard aspect ratios * Organize utils * Update tests * Formatting * Isort * Fix tests * Cleanup * isort --- frigate/app.py | 2 +- frigate/comms/dispatcher.py | 2 +- frigate/config.py | 4 +- frigate/detectors/detector_config.py | 2 +- frigate/events/audio.py | 3 +- frigate/events/external.py | 2 +- frigate/events/maintainer.py | 2 +- frigate/ffmpeg_presets.py | 2 +- frigate/http.py | 9 +- frigate/log.py | 2 +- frigate/object_detection.py | 4 +- frigate/object_processing.py | 2 +- frigate/output.py | 6 +- frigate/record/maintainer.py | 3 +- frigate/record/record.py | 2 +- frigate/stats.py | 2 +- frigate/test/test_camera_pw.py | 2 +- frigate/test/test_config.py | 2 +- frigate/test/test_copy_yuv_to_position.py | 2 +- frigate/test/test_gpu_stats.py | 2 +- frigate/test/test_video.py | 2 +- frigate/test/test_yuv_region_2_rgb.py | 2 +- frigate/timeline.py | 2 +- frigate/track/norfair_tracker.py | 2 +- frigate/util/builtin.py | 226 ++++++++ frigate/{util.py => util/image.py} | 652 +--------------------- frigate/util/services.py | 403 +++++++++++++ frigate/video.py | 6 +- frigate/watchdog.py | 2 +- 29 files changed, 689 insertions(+), 665 deletions(-) create mode 100644 frigate/util/builtin.py rename frigate/{util.py => util/image.py} (52%) mode change 100755 => 100644 create mode 100644 frigate/util/services.py diff --git a/frigate/app.py b/frigate/app.py index ed32dee17..96c2f1d27 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -47,7 +47,7 @@ from frigate.stats import StatsEmitter, stats_init from frigate.storage import StorageMaintainer from frigate.timeline import TimelineProcessor from frigate.types import CameraMetricsTypes, FeatureMetricsTypes -from frigate.util import LimitedQueue as LQueue +from frigate.util.builtin import LimitedQueue as LQueue from frigate.version import VERSION from frigate.video import capture_camera, track_camera from frigate.watchdog import FrigateWatchdog diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index c8f90bad1..ada7e4cb4 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -7,7 +7,7 @@ from typing import Any, Callable from frigate.config import FrigateConfig from frigate.ptz import OnvifCommandEnum, OnvifController from frigate.types import CameraMetricsTypes, FeatureMetricsTypes -from frigate.util import restart_frigate +from frigate.util.services import restart_frigate logger = logging.getLogger(__name__) diff --git a/frigate/config.py b/frigate/config.py index ea7ecdc49..9399320fe 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -22,13 +22,13 @@ from frigate.ffmpeg_presets import ( parse_preset_output_rtmp, ) from frigate.plus import PlusApi -from frigate.util import ( - create_mask, +from frigate.util.builtin import ( deep_merge, escape_special_characters, get_ffmpeg_arg_list, load_config_with_no_duplicates, ) +from frigate.util.image import create_mask logger = logging.getLogger(__name__) diff --git a/frigate/detectors/detector_config.py b/frigate/detectors/detector_config.py index f65826a57..ca1915449 100644 --- a/frigate/detectors/detector_config.py +++ b/frigate/detectors/detector_config.py @@ -11,7 +11,7 @@ from pydantic import BaseModel, Extra, Field from pydantic.fields import PrivateAttr from frigate.plus import PlusApi -from frigate.util import load_labels +from frigate.util.builtin import load_labels logger = logging.getLogger(__name__) diff --git a/frigate/events/audio.py b/frigate/events/audio.py index 488c94fcc..de0f07e0c 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -26,7 +26,8 @@ from frigate.ffmpeg_presets import parse_preset_input from frigate.log import LogPipe from frigate.object_detection import load_labels from frigate.types import FeatureMetricsTypes -from frigate.util import get_ffmpeg_arg_list, listen +from frigate.util.builtin import get_ffmpeg_arg_list +from frigate.util.services import listen from frigate.video import start_or_restart_ffmpeg, stop_ffmpeg try: diff --git a/frigate/events/external.py b/frigate/events/external.py index 23439f2bd..a801e6d24 100644 --- a/frigate/events/external.py +++ b/frigate/events/external.py @@ -14,7 +14,7 @@ from faster_fifo import Queue from frigate.config import CameraConfig, FrigateConfig from frigate.const import CLIPS_DIR from frigate.events.maintainer import EventTypeEnum -from frigate.util import draw_box_with_label +from frigate.util.image import draw_box_with_label logger = logging.getLogger(__name__) diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index 34cb01261..9640128e1 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -11,7 +11,7 @@ from faster_fifo import Queue from frigate.config import EventsConfig, FrigateConfig from frigate.models import Event from frigate.types import CameraMetricsTypes -from frigate.util import to_relative_box +from frigate.util.builtin import to_relative_box logger = logging.getLogger(__name__) diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 4c2e16dbb..43d2504bd 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -5,7 +5,7 @@ import os from enum import Enum from typing import Any -from frigate.util import vainfo_hwaccel +from frigate.util.services import vainfo_hwaccel from frigate.version import VERSION logger = logging.getLogger(__name__) diff --git a/frigate/http.py b/frigate/http.py index 57b2103e7..fe6dc54ef 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -38,13 +38,8 @@ from frigate.ptz import OnvifController from frigate.record.export import PlaybackFactorEnum, RecordingExporter from frigate.stats import stats_snapshot from frigate.storage import StorageMaintainer -from frigate.util import ( - clean_camera_user_pass, - ffprobe_stream, - get_tz_modifiers, - restart_frigate, - vainfo_hwaccel, -) +from frigate.util.builtin import clean_camera_user_pass, get_tz_modifiers +from frigate.util.services import ffprobe_stream, restart_frigate, vainfo_hwaccel from frigate.version import VERSION logger = logging.getLogger(__name__) diff --git a/frigate/log.py b/frigate/log.py index ac51fc3da..e839de76d 100644 --- a/frigate/log.py +++ b/frigate/log.py @@ -13,7 +13,7 @@ from typing import Deque, Optional from faster_fifo import Queue from setproctitle import setproctitle -from frigate.util import clean_camera_user_pass +from frigate.util.builtin import clean_camera_user_pass def listener_configurer() -> None: diff --git a/frigate/object_detection.py b/frigate/object_detection.py index cebd7ff41..a0c31b755 100644 --- a/frigate/object_detection.py +++ b/frigate/object_detection.py @@ -13,7 +13,9 @@ from setproctitle import setproctitle from frigate.detectors import create_detector from frigate.detectors.detector_config import InputTensorEnum -from frigate.util import EventsPerSecond, SharedMemoryFrameManager, listen, load_labels +from frigate.util.builtin import EventsPerSecond, load_labels +from frigate.util.image import SharedMemoryFrameManager +from frigate.util.services import listen logger = logging.getLogger(__name__) diff --git a/frigate/object_processing.py b/frigate/object_processing.py index e69210cce..d7151a6c8 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -22,7 +22,7 @@ from frigate.config import ( ) from frigate.const import CLIPS_DIR from frigate.events.maintainer import EventTypeEnum -from frigate.util import ( +from frigate.util.image import ( SharedMemoryFrameManager, area, calculate_region, diff --git a/frigate/output.py b/frigate/output.py index 24f4c45cc..80f084edb 100644 --- a/frigate/output.py +++ b/frigate/output.py @@ -24,7 +24,11 @@ from ws4py.websocket import WebSocket from frigate.config import BirdseyeModeEnum, FrigateConfig from frigate.const import BASE_DIR, BIRDSEYE_PIPE -from frigate.util import SharedMemoryFrameManager, copy_yuv_to_position, get_yuv_crop +from frigate.util.image import ( + SharedMemoryFrameManager, + copy_yuv_to_position, + get_yuv_crop, +) logger = logging.getLogger(__name__) diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index b4208f2d2..d21affefa 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -21,7 +21,8 @@ from frigate.config import FrigateConfig, RetainModeEnum from frigate.const import CACHE_DIR, MAX_SEGMENT_DURATION, RECORD_DIR from frigate.models import Event, Recordings from frigate.types import FeatureMetricsTypes -from frigate.util import area, get_video_properties +from frigate.util.image import area +from frigate.util.services import get_video_properties logger = logging.getLogger(__name__) diff --git a/frigate/record/record.py b/frigate/record/record.py index 0d22342aa..3e812a809 100644 --- a/frigate/record/record.py +++ b/frigate/record/record.py @@ -16,7 +16,7 @@ from frigate.models import Event, Recordings, RecordingsToDelete, Timeline from frigate.record.cleanup import RecordingCleanup from frigate.record.maintainer import RecordingMaintainer from frigate.types import FeatureMetricsTypes -from frigate.util import listen +from frigate.util.services import listen logger = logging.getLogger(__name__) diff --git a/frigate/stats.py b/frigate/stats.py index 096ad7913..5fdc671ee 100644 --- a/frigate/stats.py +++ b/frigate/stats.py @@ -17,7 +17,7 @@ from frigate.config import FrigateConfig from frigate.const import CACHE_DIR, CLIPS_DIR, DRIVER_AMD, DRIVER_ENV_VAR, RECORD_DIR from frigate.object_detection import ObjectDetectProcess from frigate.types import CameraMetricsTypes, StatsTrackingTypes -from frigate.util import ( +from frigate.util.services import ( get_amd_gpu_stats, get_bandwidth_stats, get_cpu_stats, diff --git a/frigate/test/test_camera_pw.py b/frigate/test/test_camera_pw.py index 92aec48d8..137d3aad0 100644 --- a/frigate/test/test_camera_pw.py +++ b/frigate/test/test_camera_pw.py @@ -2,7 +2,7 @@ import unittest -from frigate.util import clean_camera_user_pass, escape_special_characters +from frigate.util.builtin import clean_camera_user_pass, escape_special_characters class TestUserPassCleanup(unittest.TestCase): diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index 8e767f77f..f87317817 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -9,7 +9,7 @@ from frigate.config import BirdseyeModeEnum, FrigateConfig from frigate.const import MODEL_CACHE_DIR from frigate.detectors import DetectorTypeEnum from frigate.plus import PlusApi -from frigate.util import deep_merge, load_config_with_no_duplicates +from frigate.util.builtin import deep_merge, load_config_with_no_duplicates class TestConfig(unittest.TestCase): diff --git a/frigate/test/test_copy_yuv_to_position.py b/frigate/test/test_copy_yuv_to_position.py index 33582e2d1..4a31928bd 100644 --- a/frigate/test/test_copy_yuv_to_position.py +++ b/frigate/test/test_copy_yuv_to_position.py @@ -3,7 +3,7 @@ from unittest import TestCase, main import cv2 import numpy as np -from frigate.util import copy_yuv_to_position, get_yuv_crop +from frigate.util.image import copy_yuv_to_position, get_yuv_crop class TestCopyYuvToPosition(TestCase): diff --git a/frigate/test/test_gpu_stats.py b/frigate/test/test_gpu_stats.py index 2cf92def1..4c0a9938e 100644 --- a/frigate/test/test_gpu_stats.py +++ b/frigate/test/test_gpu_stats.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import MagicMock, patch -from frigate.util import get_amd_gpu_stats, get_intel_gpu_stats +from frigate.util.services import get_amd_gpu_stats, get_intel_gpu_stats class TestGpuStats(unittest.TestCase): diff --git a/frigate/test/test_video.py b/frigate/test/test_video.py index e5f7e83fd..99736f658 100644 --- a/frigate/test/test_video.py +++ b/frigate/test/test_video.py @@ -5,7 +5,7 @@ import numpy as np from norfair.drawing.color import Palette from norfair.drawing.drawer import Drawer -from frigate.util import intersection +from frigate.util.image import intersection from frigate.video import ( get_cluster_boundary, get_cluster_candidates, diff --git a/frigate/test/test_yuv_region_2_rgb.py b/frigate/test/test_yuv_region_2_rgb.py index a56a78b1c..10144e674 100644 --- a/frigate/test/test_yuv_region_2_rgb.py +++ b/frigate/test/test_yuv_region_2_rgb.py @@ -3,7 +3,7 @@ from unittest import TestCase, main import cv2 import numpy as np -from frigate.util import yuv_region_2_rgb +from frigate.util.image import yuv_region_2_rgb class TestYuvRegion2RGB(TestCase): diff --git a/frigate/timeline.py b/frigate/timeline.py index 6cfcbe928..48392ad96 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -10,7 +10,7 @@ from faster_fifo import Queue from frigate.config import FrigateConfig from frigate.events.maintainer import EventTypeEnum from frigate.models import Timeline -from frigate.util import to_relative_box +from frigate.util.builtin import to_relative_box logger = logging.getLogger(__name__) diff --git a/frigate/track/norfair_tracker.py b/frigate/track/norfair_tracker.py index b0c4621b4..9389d2973 100644 --- a/frigate/track/norfair_tracker.py +++ b/frigate/track/norfair_tracker.py @@ -7,7 +7,7 @@ from norfair.drawing.drawer import Drawer from frigate.config import DetectConfig from frigate.track import ObjectTracker -from frigate.util import intersection_over_union +from frigate.util.image import intersection_over_union # Normalizes distance from estimate relative to object size diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py new file mode 100644 index 000000000..900764a23 --- /dev/null +++ b/frigate/util/builtin.py @@ -0,0 +1,226 @@ +"""Utilities for builtin types manipulation.""" + +import copy +import ctypes +import datetime +import logging +import multiprocessing +import re +import shlex +import time +import urllib.parse +from collections import Counter +from collections.abc import Mapping +from queue import Empty, Full +from typing import Any, Tuple + +import pytz +import yaml +from faster_fifo import DEFAULT_CIRCULAR_BUFFER_SIZE, DEFAULT_TIMEOUT +from faster_fifo import Queue as FFQueue + +from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS + +logger = logging.getLogger(__name__) + + +class EventsPerSecond: + def __init__(self, max_events=1000, last_n_seconds=10): + self._start = None + self._max_events = max_events + self._last_n_seconds = last_n_seconds + self._timestamps = [] + + def start(self): + self._start = datetime.datetime.now().timestamp() + + def update(self): + now = datetime.datetime.now().timestamp() + if self._start is None: + self._start = now + self._timestamps.append(now) + # truncate the list when it goes 100 over the max_size + if len(self._timestamps) > self._max_events + 100: + self._timestamps = self._timestamps[(1 - self._max_events) :] + self.expire_timestamps(now) + + def eps(self): + now = datetime.datetime.now().timestamp() + if self._start is None: + self._start = now + # compute the (approximate) events in the last n seconds + self.expire_timestamps(now) + seconds = min(now - self._start, self._last_n_seconds) + # avoid divide by zero + if seconds == 0: + seconds = 1 + return len(self._timestamps) / seconds + + # remove aged out timestamps + def expire_timestamps(self, now): + threshold = now - self._last_n_seconds + while self._timestamps and self._timestamps[0] < threshold: + del self._timestamps[0] + + +class LimitedQueue(FFQueue): + def __init__( + self, + maxsize=0, + max_size_bytes=DEFAULT_CIRCULAR_BUFFER_SIZE, + loads=None, + dumps=None, + ): + super().__init__(max_size_bytes=max_size_bytes, loads=loads, dumps=dumps) + self.maxsize = maxsize + self.size = multiprocessing.RawValue( + ctypes.c_int, 0 + ) # Add a counter for the number of items in the queue + + def put(self, x, block=True, timeout=DEFAULT_TIMEOUT): + if self.maxsize > 0 and self.size.value >= self.maxsize: + if block: + start_time = time.time() + while self.size.value >= self.maxsize: + remaining = timeout - (time.time() - start_time) + if remaining <= 0.0: + raise Full + time.sleep(min(remaining, 0.1)) + else: + raise Full + self.size.value += 1 + return super().put(x, block=block, timeout=timeout) + + def get(self, block=True, timeout=DEFAULT_TIMEOUT): + if self.size.value <= 0 and not block: + raise Empty + self.size.value -= 1 + return super().get(block=block, timeout=timeout) + + def qsize(self): + return self.size + + def empty(self): + return self.qsize() == 0 + + def full(self): + return self.qsize() == self.maxsize + + +def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dict: + """ + :param dct1: First dict to merge + :param dct2: Second dict to merge + :param override: if same key exists in both dictionaries, should override? otherwise ignore. (default=True) + :return: The merge dictionary + """ + merged = copy.deepcopy(dct1) + for k, v2 in dct2.items(): + if k in merged: + v1 = merged[k] + if isinstance(v1, dict) and isinstance(v2, Mapping): + merged[k] = deep_merge(v1, v2, override) + elif isinstance(v1, list) and isinstance(v2, list): + if merge_lists: + merged[k] = v1 + v2 + else: + if override: + merged[k] = copy.deepcopy(v2) + else: + merged[k] = copy.deepcopy(v2) + return merged + + +def load_config_with_no_duplicates(raw_config) -> dict: + """Get config ensuring duplicate keys are not allowed.""" + + # https://stackoverflow.com/a/71751051 + class PreserveDuplicatesLoader(yaml.loader.Loader): + pass + + def map_constructor(loader, node, deep=False): + keys = [loader.construct_object(node, deep=deep) for node, _ in node.value] + vals = [loader.construct_object(node, deep=deep) for _, node in node.value] + key_count = Counter(keys) + data = {} + for key, val in zip(keys, vals): + if key_count[key] > 1: + raise ValueError( + f"Config input {key} is defined multiple times for the same field, this is not allowed." + ) + else: + data[key] = val + return data + + PreserveDuplicatesLoader.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, map_constructor + ) + return yaml.load(raw_config, PreserveDuplicatesLoader) + + +def clean_camera_user_pass(line: str) -> str: + """Removes user and password from line.""" + if "rtsp://" in line: + return re.sub(REGEX_RTSP_CAMERA_USER_PASS, "://*:*@", line) + else: + return re.sub(REGEX_HTTP_CAMERA_USER_PASS, "user=*&password=*", line) + + +def escape_special_characters(path: str) -> str: + """Cleans reserved characters to encodings for ffmpeg.""" + try: + found = re.search(REGEX_RTSP_CAMERA_USER_PASS, path).group(0)[3:-1] + pw = found[(found.index(":") + 1) :] + return path.replace(pw, urllib.parse.quote_plus(pw)) + except AttributeError: + # path does not have user:pass + return path + + +def get_ffmpeg_arg_list(arg: Any) -> list: + """Use arg if list or convert to list format.""" + return arg if isinstance(arg, list) else shlex.split(arg) + + +def load_labels(path, encoding="utf-8"): + """Loads labels from file (with or without index numbers). + Args: + path: path to label file. + encoding: label file encoding. + Returns: + Dictionary mapping indices to labels. + """ + with open(path, "r", encoding=encoding) as f: + labels = {index: "unknown" for index in range(91)} + lines = f.readlines() + if not lines: + return {} + + if lines[0].split(" ", maxsplit=1)[0].isdigit(): + pairs = [line.split(" ", maxsplit=1) for line in lines] + labels.update({int(index): label.strip() for index, label in pairs}) + else: + labels.update({index: line.strip() for index, line in enumerate(lines)}) + return labels + + +def get_tz_modifiers(tz_name: str) -> Tuple[str, str]: + seconds_offset = ( + datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds() + ) + hours_offset = int(seconds_offset / 60 / 60) + minutes_offset = int(seconds_offset / 60 - hours_offset * 60) + hour_modifier = f"{hours_offset} hour" + minute_modifier = f"{minutes_offset} minute" + return hour_modifier, minute_modifier + + +def to_relative_box( + width: int, height: int, box: Tuple[int, int, int, int] +) -> Tuple[int, int, int, int]: + return ( + box[0] / width, # x + box[1] / height, # y + (box[2] - box[0]) / width, # w + (box[3] - box[1]) / height, # h + ) diff --git a/frigate/util.py b/frigate/util/image.py old mode 100755 new mode 100644 similarity index 52% rename from frigate/util.py rename to frigate/util/image.py index cc9bb03c9..4af94500d --- a/frigate/util.py +++ b/frigate/util/image.py @@ -1,89 +1,17 @@ -import copy -import ctypes +"""Utilities for creating and manipulating image frames.""" + import datetime -import json import logging -import multiprocessing -import os -import re -import shlex -import signal -import subprocess as sp -import time -import traceback -import urllib.parse from abc import ABC, abstractmethod -from collections import Counter -from collections.abc import Mapping from multiprocessing import shared_memory -from queue import Empty, Full -from typing import Any, AnyStr, Optional, Tuple +from typing import AnyStr, Optional import cv2 import numpy as np -import psutil -import py3nvml.py3nvml as nvml -import pytz -import yaml -from faster_fifo import DEFAULT_CIRCULAR_BUFFER_SIZE, DEFAULT_TIMEOUT -from faster_fifo import Queue as FFQueue - -from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS logger = logging.getLogger(__name__) -def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dict: - """ - :param dct1: First dict to merge - :param dct2: Second dict to merge - :param override: if same key exists in both dictionaries, should override? otherwise ignore. (default=True) - :return: The merge dictionary - """ - merged = copy.deepcopy(dct1) - for k, v2 in dct2.items(): - if k in merged: - v1 = merged[k] - if isinstance(v1, dict) and isinstance(v2, Mapping): - merged[k] = deep_merge(v1, v2, override) - elif isinstance(v1, list) and isinstance(v2, list): - if merge_lists: - merged[k] = v1 + v2 - else: - if override: - merged[k] = copy.deepcopy(v2) - else: - merged[k] = copy.deepcopy(v2) - return merged - - -def load_config_with_no_duplicates(raw_config) -> dict: - """Get config ensuring duplicate keys are not allowed.""" - - # https://stackoverflow.com/a/71751051 - class PreserveDuplicatesLoader(yaml.loader.Loader): - pass - - def map_constructor(loader, node, deep=False): - keys = [loader.construct_object(node, deep=deep) for node, _ in node.value] - vals = [loader.construct_object(node, deep=deep) for _, node in node.value] - key_count = Counter(keys) - data = {} - for key, val in zip(keys, vals): - if key_count[key] > 1: - raise ValueError( - f"Config input {key} is defined multiple times for the same field, this is not allowed." - ) - else: - data[key] = val - return data - - PreserveDuplicatesLoader.add_constructor( - yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, map_constructor - ) - return yaml.load(raw_config, PreserveDuplicatesLoader) - - def draw_timestamp( frame, timestamp, @@ -645,432 +573,6 @@ def clipped(obj, frame_shape): return False -def restart_frigate(): - proc = psutil.Process(1) - # if this is running via s6, sigterm pid 1 - if proc.name() == "s6-svscan": - proc.terminate() - # otherwise, just try and exit frigate - else: - os.kill(os.getpid(), signal.SIGTERM) - - -class EventsPerSecond: - def __init__(self, max_events=1000, last_n_seconds=10): - self._start = None - self._max_events = max_events - self._last_n_seconds = last_n_seconds - self._timestamps = [] - - def start(self): - self._start = datetime.datetime.now().timestamp() - - def update(self): - now = datetime.datetime.now().timestamp() - if self._start is None: - self._start = now - self._timestamps.append(now) - # truncate the list when it goes 100 over the max_size - if len(self._timestamps) > self._max_events + 100: - self._timestamps = self._timestamps[(1 - self._max_events) :] - self.expire_timestamps(now) - - def eps(self): - now = datetime.datetime.now().timestamp() - if self._start is None: - self._start = now - # compute the (approximate) events in the last n seconds - self.expire_timestamps(now) - seconds = min(now - self._start, self._last_n_seconds) - # avoid divide by zero - if seconds == 0: - seconds = 1 - return len(self._timestamps) / seconds - - # remove aged out timestamps - def expire_timestamps(self, now): - threshold = now - self._last_n_seconds - while self._timestamps and self._timestamps[0] < threshold: - del self._timestamps[0] - - -def print_stack(sig, frame): - traceback.print_stack(frame) - - -def listen(): - signal.signal(signal.SIGUSR1, print_stack) - - -def create_mask(frame_shape, mask): - mask_img = np.zeros(frame_shape, np.uint8) - mask_img[:] = 255 - - if isinstance(mask, list): - for m in mask: - add_mask(m, mask_img) - - elif isinstance(mask, str): - add_mask(mask, mask_img) - - return mask_img - - -def add_mask(mask, mask_img): - points = mask.split(",") - contour = np.array( - [[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)] - ) - cv2.fillPoly(mask_img, pts=[contour], color=(0)) - - -def load_labels(path, encoding="utf-8"): - """Loads labels from file (with or without index numbers). - Args: - path: path to label file. - encoding: label file encoding. - Returns: - Dictionary mapping indices to labels. - """ - with open(path, "r", encoding=encoding) as f: - labels = {index: "unknown" for index in range(91)} - lines = f.readlines() - if not lines: - return {} - - if lines[0].split(" ", maxsplit=1)[0].isdigit(): - pairs = [line.split(" ", maxsplit=1) for line in lines] - labels.update({int(index): label.strip() for index, label in pairs}) - else: - labels.update({index: line.strip() for index, line in enumerate(lines)}) - return labels - - -def clean_camera_user_pass(line: str) -> str: - """Removes user and password from line.""" - if "rtsp://" in line: - return re.sub(REGEX_RTSP_CAMERA_USER_PASS, "://*:*@", line) - else: - return re.sub(REGEX_HTTP_CAMERA_USER_PASS, "user=*&password=*", line) - - -def escape_special_characters(path: str) -> str: - """Cleans reserved characters to encodings for ffmpeg.""" - try: - found = re.search(REGEX_RTSP_CAMERA_USER_PASS, path).group(0)[3:-1] - pw = found[(found.index(":") + 1) :] - return path.replace(pw, urllib.parse.quote_plus(pw)) - except AttributeError: - # path does not have user:pass - return path - - -def get_cgroups_version() -> str: - """Determine what version of cgroups is enabled.""" - - cgroup_path = "/sys/fs/cgroup" - - if not os.path.ismount(cgroup_path): - logger.debug(f"{cgroup_path} is not a mount point.") - return "unknown" - - try: - with open("/proc/mounts", "r") as f: - mounts = f.readlines() - - for mount in mounts: - mount_info = mount.split() - if mount_info[1] == cgroup_path: - fs_type = mount_info[2] - if fs_type == "cgroup2fs" or fs_type == "cgroup2": - return "cgroup2" - elif fs_type == "tmpfs": - return "cgroup" - else: - logger.debug( - f"Could not determine cgroups version: unhandled filesystem {fs_type}" - ) - break - except Exception as e: - logger.debug(f"Could not determine cgroups version: {e}") - - return "unknown" - - -def get_docker_memlimit_bytes() -> int: - """Get mem limit in bytes set in docker if present. Returns -1 if no limit detected.""" - - # check running a supported cgroups version - if get_cgroups_version() == "cgroup2": - memlimit_path = "/sys/fs/cgroup/memory.max" - - try: - with open(memlimit_path, "r") as f: - value = f.read().strip() - - if value.isnumeric(): - return int(value) - elif value.lower() == "max": - return -1 - except Exception as e: - logger.debug(f"Unable to get docker memlimit: {e}") - - return -1 - - -def get_cpu_stats() -> dict[str, dict]: - """Get cpu usages for each process id""" - usages = {} - docker_memlimit = get_docker_memlimit_bytes() / 1024 - total_mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / 1024 - - for process in psutil.process_iter(["pid", "name", "cpu_percent", "cmdline"]): - pid = process.info["pid"] - try: - cpu_percent = process.info["cpu_percent"] - cmdline = process.info["cmdline"] - - with open(f"/proc/{pid}/stat", "r") as f: - stats = f.readline().split() - utime = int(stats[13]) - stime = int(stats[14]) - starttime = int(stats[21]) - - with open("/proc/uptime") as f: - system_uptime_sec = int(float(f.read().split()[0])) - - clk_tck = os.sysconf(os.sysconf_names["SC_CLK_TCK"]) - - process_utime_sec = utime // clk_tck - process_stime_sec = stime // clk_tck - process_starttime_sec = starttime // clk_tck - - process_elapsed_sec = system_uptime_sec - process_starttime_sec - process_usage_sec = process_utime_sec + process_stime_sec - cpu_average_usage = process_usage_sec * 100 // process_elapsed_sec - - with open(f"/proc/{pid}/statm", "r") as f: - mem_stats = f.readline().split() - mem_res = int(mem_stats[1]) * os.sysconf("SC_PAGE_SIZE") / 1024 - - if docker_memlimit > 0: - mem_pct = round((mem_res / docker_memlimit) * 100, 1) - else: - mem_pct = round((mem_res / total_mem) * 100, 1) - - usages[pid] = { - "cpu": str(cpu_percent), - "cpu_average": str(round(cpu_average_usage, 2)), - "mem": f"{mem_pct}", - "cmdline": " ".join(cmdline), - } - except Exception: - continue - - return usages - - -def get_physical_interfaces(interfaces) -> list: - with open("/proc/net/dev", "r") as file: - lines = file.readlines() - - physical_interfaces = [] - for line in lines: - if ":" in line: - interface = line.split(":")[0].strip() - for int in interfaces: - if interface.startswith(int): - physical_interfaces.append(interface) - - return physical_interfaces - - -def get_bandwidth_stats(config) -> dict[str, dict]: - """Get bandwidth usages for each ffmpeg process id""" - usages = {} - top_command = ["nethogs", "-t", "-v0", "-c5", "-d1"] + get_physical_interfaces( - config.telemetry.network_interfaces - ) - - p = sp.run( - top_command, - encoding="ascii", - capture_output=True, - ) - - if p.returncode != 0: - return usages - else: - lines = p.stdout.split("\n") - for line in lines: - stats = list(filter(lambda a: a != "", line.strip().split("\t"))) - try: - if re.search( - r"(^ffmpeg|\/go2rtc|frigate\.detector\.[a-z]+)/([0-9]+)/", stats[0] - ): - process = stats[0].split("/") - usages[process[len(process) - 2]] = { - "bandwidth": round(float(stats[1]) + float(stats[2]), 1), - } - except (IndexError, ValueError): - continue - - return usages - - -def get_amd_gpu_stats() -> dict[str, str]: - """Get stats using radeontop.""" - radeontop_command = ["radeontop", "-d", "-", "-l", "1"] - - p = sp.run( - radeontop_command, - encoding="ascii", - capture_output=True, - ) - - if p.returncode != 0: - logger.error(f"Unable to poll radeon GPU stats: {p.stderr}") - return None - else: - usages = p.stdout.split(",") - results: dict[str, str] = {} - - for hw in usages: - if "gpu" in hw: - results["gpu"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" - elif "vram" in hw: - results["mem"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" - - return results - - -def get_intel_gpu_stats() -> dict[str, str]: - """Get stats using intel_gpu_top.""" - intel_gpu_top_command = [ - "timeout", - "0.5s", - "intel_gpu_top", - "-J", - "-o", - "-", - "-s", - "1", - ] - - p = sp.run( - intel_gpu_top_command, - encoding="ascii", - capture_output=True, - ) - - # timeout has a non-zero returncode when timeout is reached - if p.returncode != 124: - logger.error(f"Unable to poll intel GPU stats: {p.stderr}") - return None - else: - reading = "".join(p.stdout.split()) - results: dict[str, str] = {} - - # render is used for qsv - render = [] - for result in re.findall(r'"Render/3D/0":{[a-z":\d.,%]+}', reading): - packet = json.loads(result[14:]) - single = packet.get("busy", 0.0) - render.append(float(single)) - - if render: - render_avg = sum(render) / len(render) - else: - render_avg = 1 - - # video is used for vaapi - video = [] - for result in re.findall('"Video/\d":{[a-z":\d.,%]+}', reading): - packet = json.loads(result[10:]) - single = packet.get("busy", 0.0) - video.append(float(single)) - - if video: - video_avg = sum(video) / len(video) - else: - video_avg = 1 - - results["gpu"] = f"{round((video_avg + render_avg) / 2, 2)}%" - results["mem"] = "-%" - return results - - -def try_get_info(f, h, default="N/A"): - try: - v = f(h) - except nvml.NVMLError_NotSupported: - v = default - return v - - -def get_nvidia_gpu_stats() -> dict[int, dict]: - results = {} - try: - nvml.nvmlInit() - deviceCount = nvml.nvmlDeviceGetCount() - for i in range(deviceCount): - handle = nvml.nvmlDeviceGetHandleByIndex(i) - meminfo = try_get_info(nvml.nvmlDeviceGetMemoryInfo, handle) - util = try_get_info(nvml.nvmlDeviceGetUtilizationRates, handle) - if util != "N/A": - gpu_util = util.gpu - else: - gpu_util = 0 - - if meminfo != "N/A": - gpu_mem_util = meminfo.used / meminfo.total * 100 - else: - gpu_mem_util = -1 - - results[i] = { - "name": nvml.nvmlDeviceGetName(handle), - "gpu": gpu_util, - "mem": gpu_mem_util, - } - except Exception: - pass - finally: - return results - - -def ffprobe_stream(path: str) -> sp.CompletedProcess: - """Run ffprobe on stream.""" - clean_path = escape_special_characters(path) - ffprobe_cmd = [ - "ffprobe", - "-timeout", - "1000000", - "-print_format", - "json", - "-show_entries", - "stream=codec_long_name,width,height,bit_rate,duration,display_aspect_ratio,avg_frame_rate", - "-loglevel", - "quiet", - clean_path, - ] - return sp.run(ffprobe_cmd, capture_output=True) - - -def vainfo_hwaccel(device_name: Optional[str] = None) -> sp.CompletedProcess: - """Run vainfo.""" - ffprobe_cmd = ( - ["vainfo"] - if not device_name - else ["vainfo", "--display", "drm", "--device", f"/dev/dri/{device_name}"] - ) - return sp.run(ffprobe_cmd, capture_output=True) - - -def get_ffmpeg_arg_list(arg: Any) -> list: - """Use arg if list or convert to list format.""" - return arg if isinstance(arg, list) else shlex.split(arg) - - class FrameManager(ABC): @abstractmethod def create(self, name, size) -> AnyStr: @@ -1138,133 +640,23 @@ class SharedMemoryFrameManager(FrameManager): del self.shm_store[name] -def get_tz_modifiers(tz_name: str) -> Tuple[str, str]: - seconds_offset = ( - datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds() +def create_mask(frame_shape, mask): + mask_img = np.zeros(frame_shape, np.uint8) + mask_img[:] = 255 + + if isinstance(mask, list): + for m in mask: + add_mask(m, mask_img) + + elif isinstance(mask, str): + add_mask(mask, mask_img) + + return mask_img + + +def add_mask(mask, mask_img): + points = mask.split(",") + contour = np.array( + [[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)] ) - hours_offset = int(seconds_offset / 60 / 60) - minutes_offset = int(seconds_offset / 60 - hours_offset * 60) - hour_modifier = f"{hours_offset} hour" - minute_modifier = f"{minutes_offset} minute" - return hour_modifier, minute_modifier - - -def to_relative_box( - width: int, height: int, box: Tuple[int, int, int, int] -) -> Tuple[int, int, int, int]: - return ( - box[0] / width, # x - box[1] / height, # y - (box[2] - box[0]) / width, # w - (box[3] - box[1]) / height, # h - ) - - -def get_video_properties(url, get_duration=False): - def calculate_duration(video: Optional[any]) -> float: - duration = None - - if video is not None: - # Get the frames per second (fps) of the video stream - fps = video.get(cv2.CAP_PROP_FPS) - total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) - - if fps and total_frames: - duration = total_frames / fps - - # if cv2 failed need to use ffprobe - if duration is None: - ffprobe_cmd = [ - "ffprobe", - "-v", - "error", - "-show_entries", - "format=duration", - "-of", - "default=noprint_wrappers=1:nokey=1", - f"{url}", - ] - p = sp.run(ffprobe_cmd, capture_output=True) - - if p.returncode == 0 and p.stdout.decode(): - duration = float(p.stdout.decode().strip()) - else: - duration = -1 - - return duration - - width = height = 0 - - try: - # Open the video stream - video = cv2.VideoCapture(url) - - # Check if the video stream was opened successfully - if not video.isOpened(): - video = None - except Exception: - video = None - - result = {} - - if get_duration: - result["duration"] = calculate_duration(video) - - if video is not None: - # Get the width of frames in the video stream - width = video.get(cv2.CAP_PROP_FRAME_WIDTH) - - # Get the height of frames in the video stream - height = video.get(cv2.CAP_PROP_FRAME_HEIGHT) - - # Release the video stream - video.release() - - result["width"] = round(width) - result["height"] = round(height) - - return result - - -class LimitedQueue(FFQueue): - def __init__( - self, - maxsize=0, - max_size_bytes=DEFAULT_CIRCULAR_BUFFER_SIZE, - loads=None, - dumps=None, - ): - super().__init__(max_size_bytes=max_size_bytes, loads=loads, dumps=dumps) - self.maxsize = maxsize - self.size = multiprocessing.RawValue( - ctypes.c_int, 0 - ) # Add a counter for the number of items in the queue - - def put(self, x, block=True, timeout=DEFAULT_TIMEOUT): - if self.maxsize > 0 and self.size.value >= self.maxsize: - if block: - start_time = time.time() - while self.size.value >= self.maxsize: - remaining = timeout - (time.time() - start_time) - if remaining <= 0.0: - raise Full - time.sleep(min(remaining, 0.1)) - else: - raise Full - self.size.value += 1 - return super().put(x, block=block, timeout=timeout) - - def get(self, block=True, timeout=DEFAULT_TIMEOUT): - if self.size.value <= 0 and not block: - raise Empty - self.size.value -= 1 - return super().get(block=block, timeout=timeout) - - def qsize(self): - return self.size - - def empty(self): - return self.qsize() == 0 - - def full(self): - return self.qsize() == self.maxsize + cv2.fillPoly(mask_img, pts=[contour], color=(0)) diff --git a/frigate/util/services.py b/frigate/util/services.py new file mode 100644 index 000000000..507ee76ea --- /dev/null +++ b/frigate/util/services.py @@ -0,0 +1,403 @@ +"""Utilities for services.""" + +import json +import logging +import os +import re +import signal +import subprocess as sp +import traceback +from typing import Optional + +import cv2 +import psutil +import py3nvml.py3nvml as nvml + +from frigate.util.builtin import escape_special_characters + +logger = logging.getLogger(__name__) + + +def restart_frigate(): + proc = psutil.Process(1) + # if this is running via s6, sigterm pid 1 + if proc.name() == "s6-svscan": + proc.terminate() + # otherwise, just try and exit frigate + else: + os.kill(os.getpid(), signal.SIGTERM) + + +def print_stack(sig, frame): + traceback.print_stack(frame) + + +def listen(): + signal.signal(signal.SIGUSR1, print_stack) + + +def get_cgroups_version() -> str: + """Determine what version of cgroups is enabled.""" + + cgroup_path = "/sys/fs/cgroup" + + if not os.path.ismount(cgroup_path): + logger.debug(f"{cgroup_path} is not a mount point.") + return "unknown" + + try: + with open("/proc/mounts", "r") as f: + mounts = f.readlines() + + for mount in mounts: + mount_info = mount.split() + if mount_info[1] == cgroup_path: + fs_type = mount_info[2] + if fs_type == "cgroup2fs" or fs_type == "cgroup2": + return "cgroup2" + elif fs_type == "tmpfs": + return "cgroup" + else: + logger.debug( + f"Could not determine cgroups version: unhandled filesystem {fs_type}" + ) + break + except Exception as e: + logger.debug(f"Could not determine cgroups version: {e}") + + return "unknown" + + +def get_docker_memlimit_bytes() -> int: + """Get mem limit in bytes set in docker if present. Returns -1 if no limit detected.""" + + # check running a supported cgroups version + if get_cgroups_version() == "cgroup2": + memlimit_path = "/sys/fs/cgroup/memory.max" + + try: + with open(memlimit_path, "r") as f: + value = f.read().strip() + + if value.isnumeric(): + return int(value) + elif value.lower() == "max": + return -1 + except Exception as e: + logger.debug(f"Unable to get docker memlimit: {e}") + + return -1 + + +def get_cpu_stats() -> dict[str, dict]: + """Get cpu usages for each process id""" + usages = {} + docker_memlimit = get_docker_memlimit_bytes() / 1024 + total_mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / 1024 + + for process in psutil.process_iter(["pid", "name", "cpu_percent", "cmdline"]): + pid = process.info["pid"] + try: + cpu_percent = process.info["cpu_percent"] + cmdline = process.info["cmdline"] + + with open(f"/proc/{pid}/stat", "r") as f: + stats = f.readline().split() + utime = int(stats[13]) + stime = int(stats[14]) + starttime = int(stats[21]) + + with open("/proc/uptime") as f: + system_uptime_sec = int(float(f.read().split()[0])) + + clk_tck = os.sysconf(os.sysconf_names["SC_CLK_TCK"]) + + process_utime_sec = utime // clk_tck + process_stime_sec = stime // clk_tck + process_starttime_sec = starttime // clk_tck + + process_elapsed_sec = system_uptime_sec - process_starttime_sec + process_usage_sec = process_utime_sec + process_stime_sec + cpu_average_usage = process_usage_sec * 100 // process_elapsed_sec + + with open(f"/proc/{pid}/statm", "r") as f: + mem_stats = f.readline().split() + mem_res = int(mem_stats[1]) * os.sysconf("SC_PAGE_SIZE") / 1024 + + if docker_memlimit > 0: + mem_pct = round((mem_res / docker_memlimit) * 100, 1) + else: + mem_pct = round((mem_res / total_mem) * 100, 1) + + usages[pid] = { + "cpu": str(cpu_percent), + "cpu_average": str(round(cpu_average_usage, 2)), + "mem": f"{mem_pct}", + "cmdline": " ".join(cmdline), + } + except Exception: + continue + + return usages + + +def get_physical_interfaces(interfaces) -> list: + with open("/proc/net/dev", "r") as file: + lines = file.readlines() + + physical_interfaces = [] + for line in lines: + if ":" in line: + interface = line.split(":")[0].strip() + for int in interfaces: + if interface.startswith(int): + physical_interfaces.append(interface) + + return physical_interfaces + + +def get_bandwidth_stats(config) -> dict[str, dict]: + """Get bandwidth usages for each ffmpeg process id""" + usages = {} + top_command = ["nethogs", "-t", "-v0", "-c5", "-d1"] + get_physical_interfaces( + config.telemetry.network_interfaces + ) + + p = sp.run( + top_command, + encoding="ascii", + capture_output=True, + ) + + if p.returncode != 0: + return usages + else: + lines = p.stdout.split("\n") + for line in lines: + stats = list(filter(lambda a: a != "", line.strip().split("\t"))) + try: + if re.search( + r"(^ffmpeg|\/go2rtc|frigate\.detector\.[a-z]+)/([0-9]+)/", stats[0] + ): + process = stats[0].split("/") + usages[process[len(process) - 2]] = { + "bandwidth": round(float(stats[1]) + float(stats[2]), 1), + } + except (IndexError, ValueError): + continue + + return usages + + +def get_amd_gpu_stats() -> dict[str, str]: + """Get stats using radeontop.""" + radeontop_command = ["radeontop", "-d", "-", "-l", "1"] + + p = sp.run( + radeontop_command, + encoding="ascii", + capture_output=True, + ) + + if p.returncode != 0: + logger.error(f"Unable to poll radeon GPU stats: {p.stderr}") + return None + else: + usages = p.stdout.split(",") + results: dict[str, str] = {} + + for hw in usages: + if "gpu" in hw: + results["gpu"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" + elif "vram" in hw: + results["mem"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" + + return results + + +def get_intel_gpu_stats() -> dict[str, str]: + """Get stats using intel_gpu_top.""" + intel_gpu_top_command = [ + "timeout", + "0.5s", + "intel_gpu_top", + "-J", + "-o", + "-", + "-s", + "1", + ] + + p = sp.run( + intel_gpu_top_command, + encoding="ascii", + capture_output=True, + ) + + # timeout has a non-zero returncode when timeout is reached + if p.returncode != 124: + logger.error(f"Unable to poll intel GPU stats: {p.stderr}") + return None + else: + reading = "".join(p.stdout.split()) + results: dict[str, str] = {} + + # render is used for qsv + render = [] + for result in re.findall(r'"Render/3D/0":{[a-z":\d.,%]+}', reading): + packet = json.loads(result[14:]) + single = packet.get("busy", 0.0) + render.append(float(single)) + + if render: + render_avg = sum(render) / len(render) + else: + render_avg = 1 + + # video is used for vaapi + video = [] + for result in re.findall('"Video/\d":{[a-z":\d.,%]+}', reading): + packet = json.loads(result[10:]) + single = packet.get("busy", 0.0) + video.append(float(single)) + + if video: + video_avg = sum(video) / len(video) + else: + video_avg = 1 + + results["gpu"] = f"{round((video_avg + render_avg) / 2, 2)}%" + results["mem"] = "-%" + return results + + +def try_get_info(f, h, default="N/A"): + try: + v = f(h) + except nvml.NVMLError_NotSupported: + v = default + return v + + +def get_nvidia_gpu_stats() -> dict[int, dict]: + results = {} + try: + nvml.nvmlInit() + deviceCount = nvml.nvmlDeviceGetCount() + for i in range(deviceCount): + handle = nvml.nvmlDeviceGetHandleByIndex(i) + meminfo = try_get_info(nvml.nvmlDeviceGetMemoryInfo, handle) + util = try_get_info(nvml.nvmlDeviceGetUtilizationRates, handle) + if util != "N/A": + gpu_util = util.gpu + else: + gpu_util = 0 + + if meminfo != "N/A": + gpu_mem_util = meminfo.used / meminfo.total * 100 + else: + gpu_mem_util = -1 + + results[i] = { + "name": nvml.nvmlDeviceGetName(handle), + "gpu": gpu_util, + "mem": gpu_mem_util, + } + except Exception: + pass + finally: + return results + + +def ffprobe_stream(path: str) -> sp.CompletedProcess: + """Run ffprobe on stream.""" + clean_path = escape_special_characters(path) + ffprobe_cmd = [ + "ffprobe", + "-timeout", + "1000000", + "-print_format", + "json", + "-show_entries", + "stream=codec_long_name,width,height,bit_rate,duration,display_aspect_ratio,avg_frame_rate", + "-loglevel", + "quiet", + clean_path, + ] + return sp.run(ffprobe_cmd, capture_output=True) + + +def vainfo_hwaccel(device_name: Optional[str] = None) -> sp.CompletedProcess: + """Run vainfo.""" + ffprobe_cmd = ( + ["vainfo"] + if not device_name + else ["vainfo", "--display", "drm", "--device", f"/dev/dri/{device_name}"] + ) + return sp.run(ffprobe_cmd, capture_output=True) + + +def get_video_properties(url, get_duration=False): + def calculate_duration(video: Optional[any]) -> float: + duration = None + + if video is not None: + # Get the frames per second (fps) of the video stream + fps = video.get(cv2.CAP_PROP_FPS) + total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + + if fps and total_frames: + duration = total_frames / fps + + # if cv2 failed need to use ffprobe + if duration is None: + ffprobe_cmd = [ + "ffprobe", + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "default=noprint_wrappers=1:nokey=1", + f"{url}", + ] + p = sp.run(ffprobe_cmd, capture_output=True) + + if p.returncode == 0 and p.stdout.decode(): + duration = float(p.stdout.decode().strip()) + else: + duration = -1 + + return duration + + width = height = 0 + + try: + # Open the video stream + video = cv2.VideoCapture(url) + + # Check if the video stream was opened successfully + if not video.isOpened(): + video = None + except Exception: + video = None + + result = {} + + if get_duration: + result["duration"] = calculate_duration(video) + + if video is not None: + # Get the width of frames in the video stream + width = video.get(cv2.CAP_PROP_FRAME_WIDTH) + + # Get the height of frames in the video stream + height = video.get(cv2.CAP_PROP_FRAME_HEIGHT) + + # Release the video stream + video.release() + + result["width"] = round(width) + result["height"] = round(height) + + return result diff --git a/frigate/video.py b/frigate/video.py index 630859b49..0d0b3e5c6 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -24,8 +24,8 @@ from frigate.motion.improved_motion import ImprovedMotionDetector from frigate.object_detection import RemoteObjectDetector from frigate.track import ObjectTracker from frigate.track.norfair_tracker import NorfairTracker -from frigate.util import ( - EventsPerSecond, +from frigate.util.builtin import EventsPerSecond +from frigate.util.image import ( FrameManager, SharedMemoryFrameManager, area, @@ -33,11 +33,11 @@ from frigate.util import ( draw_box_with_label, intersection, intersection_over_union, - listen, yuv_region_2_bgr, yuv_region_2_rgb, yuv_region_2_yuv, ) +from frigate.util.services import listen logger = logging.getLogger(__name__) diff --git a/frigate/watchdog.py b/frigate/watchdog.py index 245e6f2cb..c6d55d18c 100644 --- a/frigate/watchdog.py +++ b/frigate/watchdog.py @@ -5,7 +5,7 @@ import time from multiprocessing.synchronize import Event as MpEvent from frigate.object_detection import ObjectDetectProcess -from frigate.util import restart_frigate +from frigate.util.services import restart_frigate logger = logging.getLogger(__name__)