diff --git a/docs/docs/configuration/detectors.md b/docs/docs/configuration/detectors.md index b7f442c31..7fbd03f79 100644 --- a/docs/docs/configuration/detectors.md +++ b/docs/docs/configuration/detectors.md @@ -256,3 +256,25 @@ model: width: 416 height: 416 ``` + +## Deepstack / CodeProject.AI Server Detector + +The Deepstack / CodeProject.AI Server detector for Frigate allows you to integrate Deepstack and CodeProject.AI object detection capabilities into Frigate. CodeProject.AI and DeepStack are open-source AI platforms that can be run on various devices such as the Raspberry Pi, Nvidia Jetson, and other compatible hardware. It is important to note that the integration is performed over the network, so the inference times may not be as fast as native Frigate detectors, but it still provides an efficient and reliable solution for object detection and tracking. + +### Setup + +To get started with CodeProject.AI, visit their [official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to follow the instructions to download and install the AI server on your preferred device. Detailed setup instructions for CodeProject.AI are outside the scope of the Frigate documentation. + +To integrate CodeProject.AI into Frigate, you'll need to make the following changes to your Frigate configuration file: + +```yaml +detectors: + deepstack: + api_url: http://:/v1/vision/detection + type: deepstack + api_timeout: 0.1 # seconds +``` + +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/integrations/api.md b/docs/docs/integrations/api.md index 9aec392a4..b5b80f3ea 100644 --- a/docs/docs/integrations/api.md +++ b/docs/docs/integrations/api.md @@ -213,7 +213,7 @@ Sets retain to false for the event id (event may be deleted quickly after removi ### `POST /api/events//sub_label` Set a sub label for an event. For example to update `person` -> `person's name` if they were recognized with facial recognition. -Sub labels must be 20 characters or shorter. +Sub labels must be 100 characters or shorter. ```json { diff --git a/frigate/app.py b/frigate/app.py index a07a4465f..d23073d77 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -8,6 +8,7 @@ import signal import sys from typing import Optional from types import FrameType +import psutil import traceback from peewee_migrate import Router @@ -58,6 +59,7 @@ class FrigateApp: self.plus_api = PlusApi() self.camera_metrics: dict[str, CameraMetricsTypes] = {} self.record_metrics: dict[str, RecordMetricsTypes] = {} + self.processes: dict[str, int] = {} def set_environment_vars(self) -> None: for key, value in self.config.environment_vars.items(): @@ -77,6 +79,7 @@ class FrigateApp: ) self.log_process.daemon = True self.log_process.start() + self.processes["logger"] = self.log_process.pid or 0 root_configurer(self.log_queue) def init_config(self) -> None: @@ -171,6 +174,12 @@ class FrigateApp: migrate_db.close() + def init_go2rtc(self) -> None: + for proc in psutil.process_iter(["pid", "name"]): + if proc.info["name"] == "go2rtc": + logger.info(f"go2rtc process pid: {proc.info['pid']}") + self.processes["go2rtc"] = proc.info["pid"] + def init_recording_manager(self) -> None: recording_process = mp.Process( target=manage_recordings, @@ -180,6 +189,7 @@ class FrigateApp: recording_process.daemon = True self.recording_process = recording_process recording_process.start() + self.processes["recording"] = recording_process.pid or 0 logger.info(f"Recording process started: {recording_process.pid}") def bind_database(self) -> None: @@ -191,7 +201,7 @@ class FrigateApp: def init_stats(self) -> None: self.stats_tracking = stats_init( - self.config, self.camera_metrics, self.detectors + self.config, self.camera_metrics, self.detectors, self.processes ) def init_web_server(self) -> None: @@ -412,6 +422,7 @@ class FrigateApp: self.init_database() self.init_onvif() self.init_recording_manager() + self.init_go2rtc() self.bind_database() self.init_dispatcher() except Exception as e: diff --git a/frigate/detectors/plugins/deepstack.py b/frigate/detectors/plugins/deepstack.py new file mode 100644 index 000000000..716065903 --- /dev/null +++ b/frigate/detectors/plugins/deepstack.py @@ -0,0 +1,78 @@ +import logging +import numpy as np +import requests +import io + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig +from typing import Literal +from pydantic import Extra, Field +from PIL import Image + + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "deepstack" + + +class DeepstackDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + api_url: str = Field( + default="http://localhost:80/v1/vision/detection", title="DeepStack API URL" + ) + api_timeout: float = Field(default=0.1, title="DeepStack API timeout (in seconds)") + api_key: str = Field(default="", title="DeepStack API key (if required)") + + +class DeepStack(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: DeepstackDetectorConfig): + self.api_url = detector_config.api_url + self.api_timeout = detector_config.api_timeout + self.api_key = detector_config.api_key + self.labels = detector_config.model.merged_labelmap + + self.h = detector_config.model.height + self.w = detector_config.model.width + + def get_label_index(self, label_value): + if label_value.lower() == "truck": + label_value = "car" + for index, value in self.labels.items(): + if value == label_value.lower(): + return index + return -1 + + def detect_raw(self, tensor_input): + image_data = np.squeeze(tensor_input).astype(np.uint8) + image = Image.fromarray(image_data) + with io.BytesIO() as output: + image.save(output, format="JPEG") + image_bytes = output.getvalue() + data = {"api_key": self.api_key} + response = requests.post( + self.api_url, files={"image": image_bytes}, timeout=self.api_timeout + ) + response_json = response.json() + detections = np.zeros((20, 6), np.float32) + + for i, detection in enumerate(response_json["predictions"]): + logger.debug(f"Response: {detection}") + if detection["confidence"] < 0.4: + logger.debug(f"Break due to confidence < 0.4") + break + label = self.get_label_index(detection["label"]) + if label < 0: + logger.debug(f"Break due to unknown label") + break + detections[i] = [ + label, + float(detection["confidence"]), + detection["y_min"] / self.h, + detection["x_min"] / self.w, + detection["y_max"] / self.h, + detection["x_max"] / self.w, + ] + + return detections diff --git a/frigate/http.py b/frigate/http.py index 66aaff368..94aa5c129 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -375,13 +375,13 @@ def set_sub_label(id): else: new_sub_label = None - if new_sub_label and len(new_sub_label) > 20: + if new_sub_label and len(new_sub_label) > 100: return make_response( jsonify( { "success": False, "message": new_sub_label - + " exceeds the 20 character limit for sub_label", + + " exceeds the 100 character limit for sub_label", } ), 400, diff --git a/frigate/models.py b/frigate/models.py index 3770121cc..0d5f1ab4a 100644 --- a/frigate/models.py +++ b/frigate/models.py @@ -14,7 +14,7 @@ from playhouse.sqlite_ext import JSONField class Event(Model): # type: ignore[misc] id = CharField(null=False, primary_key=True, max_length=30) label = CharField(index=True, max_length=20) - sub_label = CharField(max_length=20, null=True) + sub_label = CharField(max_length=100, null=True) camera = CharField(index=True, max_length=20) start_time = DateTimeField() end_time = DateTimeField() diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py index 75784133a..605979ee4 100644 --- a/frigate/record/cleanup.py +++ b/frigate/record/cleanup.py @@ -3,7 +3,7 @@ import datetime import itertools import logging -import subprocess as sp +import os import threading from pathlib import Path @@ -192,12 +192,14 @@ class RecordingCleanup(threading.Thread): return logger.debug(f"Oldest recording in the db: {oldest_timestamp}") - process = sp.run( - ["find", RECORD_DIR, "-type", "f", "!", "-newermt", f"@{oldest_timestamp}"], - capture_output=True, - text=True, - ) - files_to_check = process.stdout.splitlines() + + 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) @@ -216,12 +218,10 @@ class RecordingCleanup(threading.Thread): recordings: Recordings = Recordings.select() # get all recordings files on disk - process = sp.run( - ["find", RECORD_DIR, "-type", "f"], - capture_output=True, - text=True, - ) - files_on_disk = process.stdout.splitlines() + files_on_disk = [] + for root, _, files in os.walk(RECORD_DIR): + for file in files: + files_on_disk.append(os.path.join(root, file)) recordings_to_delete = [] for recording in recordings.objects().iterator(): diff --git a/frigate/stats.py b/frigate/stats.py index 1ceb5f30d..4287dab0c 100644 --- a/frigate/stats.py +++ b/frigate/stats.py @@ -46,6 +46,7 @@ def stats_init( config: FrigateConfig, camera_metrics: dict[str, CameraMetricsTypes], detectors: dict[str, ObjectDetectProcess], + processes: dict[str, int], ) -> StatsTrackingTypes: stats_tracking: StatsTrackingTypes = { "camera_metrics": camera_metrics, @@ -53,6 +54,7 @@ def stats_init( "started": int(time.time()), "latest_frigate_version": get_latest_version(config), "last_updated": int(time.time()), + "processes": processes, } return stats_tracking @@ -151,9 +153,12 @@ async def set_gpu_stats( nvidia_usage = get_nvidia_gpu_stats() if nvidia_usage: - name = nvidia_usage["name"] - del nvidia_usage["name"] - stats[name] = nvidia_usage + for i in range(len(nvidia_usage)): + stats[nvidia_usage[i]["name"]] = { + "gpu": str(round(float(nvidia_usage[i]["gpu"]), 2)) + "%", + "mem": str(round(float(nvidia_usage[i]["mem"]), 2)) + "%", + } + else: stats["nvidia-gpu"] = {"gpu": -1, "mem": -1} hwaccel_errors.append(args) @@ -260,6 +265,12 @@ def stats_snapshot( "mount_type": get_fs_type(path), } + stats["processes"] = {} + for name, pid in stats_tracking["processes"].items(): + stats["processes"][name] = { + "pid": pid, + } + return stats diff --git a/frigate/test/test_gpu_stats.py b/frigate/test/test_gpu_stats.py index d3f00ce77..5742a583d 100644 --- a/frigate/test/test_gpu_stats.py +++ b/frigate/test/test_gpu_stats.py @@ -17,20 +17,20 @@ class TestGpuStats(unittest.TestCase): process.stdout = self.amd_results sp.return_value = process amd_stats = get_amd_gpu_stats() - assert amd_stats == {"gpu": "4.17 %", "mem": "60.37 %"} + assert amd_stats == {"gpu": "4.17%", "mem": "60.37%"} - @patch("subprocess.run") - def test_nvidia_gpu_stats(self, sp): - process = MagicMock() - process.returncode = 0 - process.stdout = self.nvidia_results - sp.return_value = process - nvidia_stats = get_nvidia_gpu_stats() - assert nvidia_stats == { - "name": "NVIDIA GeForce RTX 3050", - "gpu": "42 %", - "mem": "61.5 %", - } + # @patch("subprocess.run") + # def test_nvidia_gpu_stats(self, sp): + # process = MagicMock() + # process.returncode = 0 + # process.stdout = self.nvidia_results + # sp.return_value = process + # nvidia_stats = get_nvidia_gpu_stats() + # assert nvidia_stats == { + # "name": "NVIDIA GeForce RTX 3050", + # "gpu": "42 %", + # "mem": "61.5 %", + # } @patch("subprocess.run") def test_intel_gpu_stats(self, sp): @@ -40,6 +40,6 @@ class TestGpuStats(unittest.TestCase): sp.return_value = process intel_stats = get_intel_gpu_stats() assert intel_stats == { - "gpu": "1.34 %", - "mem": "- %", + "gpu": "1.34%", + "mem": "-%", } diff --git a/frigate/types.py b/frigate/types.py index 9da4027c9..3cc401ebb 100644 --- a/frigate/types.py +++ b/frigate/types.py @@ -34,3 +34,4 @@ class StatsTrackingTypes(TypedDict): started: int latest_frigate_version: str last_updated: int + processes: dict[str, int] diff --git a/frigate/util.py b/frigate/util.py index b01234c1a..d98cee106 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -9,12 +9,14 @@ import signal import traceback import urllib.parse import yaml +import os from abc import ABC, abstractmethod from collections import Counter from collections.abc import Mapping from multiprocessing import shared_memory from typing import Any, AnyStr, Optional, Tuple +import py3nvml.py3nvml as nvml import cv2 import numpy as np @@ -740,55 +742,54 @@ def escape_special_characters(path: str) -> str: def get_cgroups_version() -> str: - """Determine what version of cgroups is enabled""" + """Determine what version of cgroups is enabled.""" - stat_command = ["stat", "-fc", "%T", "/sys/fs/cgroup"] + cgroup_path = "/sys/fs/cgroup" - p = sp.run( - stat_command, - encoding="ascii", - capture_output=True, - ) + if not os.path.ismount(cgroup_path): + logger.debug(f"{cgroup_path} is not a mount point.") + return "unknown" - if p.returncode == 0: - value: str = p.stdout.strip().lower() + try: + with open("/proc/mounts", "r") as f: + mounts = f.readlines() - if value == "cgroup2fs": - return "cgroup2" - elif value == "tmpfs": - return "cgroup" - else: - logger.debug( - f"Could not determine cgroups version: unhandled filesystem {value}" - ) - else: - logger.debug(f"Could not determine cgroups version: {p.stderr}") + 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""" + """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_command = ["cat", "/sys/fs/cgroup/memory.max"] + memlimit_path = "/sys/fs/cgroup/memory.max" - p = sp.run( - memlimit_command, - encoding="ascii", - capture_output=True, - ) - - if p.returncode == 0: - value: str = p.stdout.strip() + try: + with open(memlimit_path, "r") as f: + value = f.read().strip() if value.isnumeric(): return int(value) elif value.lower() == "max": return -1 - else: - logger.debug(f"Unable to get docker memlimit: {p.stderr}") + except Exception as e: + logger.debug(f"Unable to get docker memlimit: {e}") return -1 @@ -796,49 +797,51 @@ def get_docker_memlimit_bytes() -> int: def get_cpu_stats() -> dict[str, dict]: """Get cpu usages for each process id""" usages = {} - # -n=2 runs to ensure extraneous values are not included - top_command = ["top", "-b", "-n", "2"] - docker_memlimit = get_docker_memlimit_bytes() / 1024 + total_mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / 1024 - p = sp.run( - top_command, - encoding="ascii", - capture_output=True, - ) + for process in psutil.process_iter(["pid", "name", "cpu_percent"]): + pid = process.info["pid"] + try: + cpu_percent = process.info["cpu_percent"] - if p.returncode != 0: - logger.error(p.stderr) - return usages - else: - lines = p.stdout.split("\n") + 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]) - for line in lines: - stats = list(filter(lambda a: a != "", line.strip().split(" "))) - try: - if docker_memlimit > 0: - mem_res = int(stats[5]) - mem_pct = str( - round((float(mem_res) / float(docker_memlimit)) * 100, 1) - ) - else: - mem_pct = stats[9] + with open("/proc/uptime") as f: + system_uptime_sec = int(float(f.read().split()[0])) - idx = stats[0] + clk_tck = os.sysconf(os.sysconf_names["SC_CLK_TCK"]) - if stats[-1] == "go2rtc": - idx = "go2rtc" - elif stats[-1] == "frigate.r+": - idx = "recording" + process_utime_sec = utime // clk_tck + process_stime_sec = stime // clk_tck + process_starttime_sec = starttime // clk_tck - usages[idx] = { - "cpu": stats[8], - "mem": mem_pct, - } - except: - continue + 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 - return usages + 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}", + } + except: + continue + + return usages def get_amd_gpu_stats() -> dict[str, str]: @@ -860,9 +863,9 @@ def get_amd_gpu_stats() -> dict[str, str]: for hw in usages: if "gpu" in hw: - results["gpu"] = f"{hw.strip().split(' ')[1].replace('%', '')} %" + results["gpu"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" elif "vram" in hw: - results["mem"] = f"{hw.strip().split(' ')[1].replace('%', '')} %" + results["mem"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" return results @@ -918,50 +921,48 @@ def get_intel_gpu_stats() -> dict[str, str]: else: video_avg = 1 - results["gpu"] = f"{round((video_avg + render_avg) / 2, 2)} %" - results["mem"] = "- %" + results["gpu"] = f"{round((video_avg + render_avg) / 2, 2)}%" + results["mem"] = "-%" return results -def get_nvidia_gpu_stats() -> dict[str, str]: - """Get stats using nvidia-smi.""" - nvidia_smi_command = [ - "nvidia-smi", - "--query-gpu=gpu_name,utilization.gpu,memory.used,memory.total", - "--format=csv", - ] +def try_get_info(f, h, default="N/A"): + try: + v = f(h) + except nvml.NVMLError_NotSupported: + v = default + return v - if ( - "CUDA_VISIBLE_DEVICES" in os.environ - and os.environ["CUDA_VISIBLE_DEVICES"].isdigit() - ): - nvidia_smi_command.extend(["--id", os.environ["CUDA_VISIBLE_DEVICES"]]) - elif ( - "NVIDIA_VISIBLE_DEVICES" in os.environ - and os.environ["NVIDIA_VISIBLE_DEVICES"].isdigit() - ): - nvidia_smi_command.extend(["--id", os.environ["NVIDIA_VISIBLE_DEVICES"]]) - p = sp.run( - nvidia_smi_command, - encoding="ascii", - capture_output=True, - ) +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 p.returncode != 0: - logger.error(f"Unable to poll nvidia GPU stats: {p.stderr}") - return None - else: - usages = p.stdout.split("\n")[1].strip().split(",") - memory_percent = f"{round(float(usages[2].replace(' MiB', '').strip()) / float(usages[3].replace(' MiB', '').strip()) * 100, 1)} %" - results: dict[str, str] = { - "name": usages[0], - "gpu": usages[1].strip(), - "mem": memory_percent, - } + 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: return results + return results + def ffprobe_stream(path: str) -> sp.CompletedProcess: """Run ffprobe on stream.""" diff --git a/migrations/016_sublabel_increase.py b/migrations/016_sublabel_increase.py new file mode 100644 index 000000000..b46b9fc79 --- /dev/null +++ b/migrations/016_sublabel_increase.py @@ -0,0 +1,12 @@ +import peewee as pw +from playhouse.migrate import * +from playhouse.sqlite_ext import * +from frigate.models import Event + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.change_columns(Event, sub_label=pw.CharField(max_length=100, null=True)) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.change_columns(Event, sub_label=pw.CharField(max_length=20, null=True)) diff --git a/requirements-wheels.txt b/requirements-wheels.txt index e8e92408b..95d70077a 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -11,6 +11,7 @@ peewee == 3.15.* peewee_migrate == 1.7.* psutil == 5.9.* pydantic == 1.10.* +git+https://github.com/fbcotter/py3nvml#egg=py3nvml PyYAML == 6.0 pytz == 2023.3 tzlocal == 4.3 diff --git a/web/images/safari-pinned-tab.svg b/web/images/favicon.svg similarity index 100% rename from web/images/safari-pinned-tab.svg rename to web/images/favicon.svg diff --git a/web/index.html b/web/index.html index db87d3536..bd3e829e1 100644 --- a/web/index.html +++ b/web/index.html @@ -8,8 +8,9 @@ + - + diff --git a/web/images/android-chrome-192x192.png b/web/public/icons/android-chrome-192x192.png similarity index 100% rename from web/images/android-chrome-192x192.png rename to web/public/icons/android-chrome-192x192.png diff --git a/web/images/android-chrome-512x512.png b/web/public/icons/android-chrome-512x512.png similarity index 100% rename from web/images/android-chrome-512x512.png rename to web/public/icons/android-chrome-512x512.png diff --git a/web/site.webmanifest b/web/site.webmanifest index 91aedef05..1f44b8456 100644 --- a/web/site.webmanifest +++ b/web/site.webmanifest @@ -1,14 +1,14 @@ { - "name": "", - "short_name": "", + "name": "Frigate", + "short_name": "Frigate", "icons": [ { - "src": "/images/android-chrome-192x192.png", + "src": "/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "/images/android-chrome-512x512.png", + "src": "/icons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } diff --git a/web/src/routes/Birdseye.jsx b/web/src/routes/Birdseye.jsx index c23713bb9..c2742e14b 100644 --- a/web/src/routes/Birdseye.jsx +++ b/web/src/routes/Birdseye.jsx @@ -24,7 +24,7 @@ export default function Birdseye() { } return Object.entries(config.cameras) - .filter(([_, conf]) => conf.onvif?.host) + .filter(([_, conf]) => conf.onvif?.host && conf.onvif.host != '') .map(([_, camera]) => camera.name); }, [config]); @@ -37,7 +37,7 @@ export default function Birdseye() { if ('MediaSource' in window) { player = ( -
+
@@ -54,7 +54,7 @@ export default function Birdseye() { } else if (viewSource == 'webrtc' && config.birdseye.restream) { player = ( -
+
@@ -62,7 +62,7 @@ export default function Birdseye() { } else { player = ( -
+
@@ -94,7 +94,7 @@ export default function Birdseye() {
{player} - {ptzCameras && ( + {ptzCameras.length ? (
Control Panel {ptzCameras.map((camera) => ( @@ -104,7 +104,7 @@ export default function Birdseye() {
))}
- )} + ) : null}
); diff --git a/web/src/routes/Recording.jsx b/web/src/routes/Recording.jsx index 9a40b674c..405549e8b 100644 --- a/web/src/routes/Recording.jsx +++ b/web/src/routes/Recording.jsx @@ -30,7 +30,7 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se // calculates the seek seconds by adding up all the seconds in the segments prior to the playback time const seekSeconds = useMemo(() => { if (!recordings) { - return 0; + return undefined; } const currentUnix = getUnixTime(currentDate); @@ -103,6 +103,9 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se }, [playlistIndex]); useEffect(() => { + if (seekSeconds === undefined) { + return; + } if (this.player) { // if the playlist has moved on to the next item, then reset if (this.player.playlist.currentItem() !== playlistIndex) { @@ -114,7 +117,7 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se } }, [seekSeconds, playlistIndex]); - if (!recordingsSummary || !recordings || !config) { + if (!recordingsSummary || !config) { return ; } @@ -145,7 +148,9 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se player.playlist(playlist); player.playlist.autoadvance(0); player.playlist.currentItem(playlistIndex); - player.currentTime(seekSeconds); + if (seekSeconds !== undefined) { + player.currentTime(seekSeconds); + } this.player = player; } }} diff --git a/web/src/routes/Storage.jsx b/web/src/routes/Storage.jsx index 4539df6f1..dd7f969c7 100644 --- a/web/src/routes/Storage.jsx +++ b/web/src/routes/Storage.jsx @@ -5,6 +5,8 @@ import { useWs } from '../api/ws'; import useSWR from 'swr'; import { Table, Tbody, Thead, Tr, Th, Td } from '../components/Table'; import Link from '../components/Link'; +import Button from '../components/Button'; +import { About } from '../icons/About'; const emptyObject = Object.freeze({}); @@ -66,9 +68,19 @@ export default function Storage() { Overview -
+
-
Data
+
+
Data
+ +
@@ -83,7 +95,17 @@ export default function Storage() {
-
Memory
+
+
Memory
+ +
@@ -110,7 +132,17 @@ export default function Storage() { - Cameras +
+ Cameras + +
{Object.entries(storage).map(([name, camera]) => (
diff --git a/web/src/routes/System.jsx b/web/src/routes/System.jsx index 0320e237b..4eeaa71a7 100644 --- a/web/src/routes/System.jsx +++ b/web/src/routes/System.jsx @@ -11,6 +11,7 @@ import { useState } from 'preact/hooks'; import Dialog from '../components/Dialog'; import TimeAgo from '../components/TimeAgo'; import copy from 'copy-to-clipboard'; +import { About } from '../icons/About'; const emptyObject = Object.freeze({}); @@ -29,12 +30,14 @@ export default function System() { detectors, service = {}, detection_fps: _, + processes, ...cameras } = stats || initialStats || emptyObject; const detectorNames = Object.keys(detectors || emptyObject); const gpuNames = Object.keys(gpu_usages || emptyObject); const cameraNames = Object.keys(cameras || emptyObject); + const processesNames = Object.keys(processes || emptyObject); const onHandleFfprobe = async (camera, e) => { if (e) { @@ -206,7 +209,19 @@ export default function System() {
) : ( - Detectors +
+ + Detectors + + +
{detectorNames.map((detector) => (
@@ -235,8 +250,20 @@ export default function System() { ))}
-
- GPUs +
+
+ + GPUs + + +
@@ -280,7 +307,19 @@ export default function System() {
)} - Cameras +
+ + Cameras + + +
{!cameras ? ( ) : ( @@ -345,9 +384,21 @@ export default function System() {
)} - Other Processes +
+ + Other Processes + + +
- {['go2rtc', 'recording'].map((process) => ( + {processesNames.map((process) => (
{process}
@@ -356,14 +407,18 @@ export default function System() {
+ + - - - + + + + +
P-ID CPU %Avg CPU % Memory %
{cpu_usages[process]?.['cpu'] || '- '}%{cpu_usages[process]?.['mem'] || '- '}%
{processes[process]['pid'] || '- '}{cpu_usages[processes[process]['pid']]?.['cpu'] || '- '}%{cpu_usages[processes[process]['pid']]?.['cpu_average'] || '- '}%{cpu_usages[processes[process]['pid']]?.['mem'] || '- '}%