mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-05 10:45:21 +03:00
Merge branch 'dev' into timeline-optimizations
This commit is contained in:
commit
45dd1abaad
@ -256,3 +256,25 @@ model:
|
|||||||
width: 416
|
width: 416
|
||||||
height: 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://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection
|
||||||
|
type: deepstack
|
||||||
|
api_timeout: 0.1 # seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `<your_codeproject_ai_server_ip>` and `<port>` 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.
|
||||||
@ -213,7 +213,7 @@ Sets retain to false for the event id (event may be deleted quickly after removi
|
|||||||
### `POST /api/events/<id>/sub_label`
|
### `POST /api/events/<id>/sub_label`
|
||||||
|
|
||||||
Set a sub label for an event. For example to update `person` -> `person's name` if they were recognized with facial recognition.
|
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
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import signal
|
|||||||
import sys
|
import sys
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from types import FrameType
|
from types import FrameType
|
||||||
|
import psutil
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
from peewee_migrate import Router
|
from peewee_migrate import Router
|
||||||
@ -58,6 +59,7 @@ class FrigateApp:
|
|||||||
self.plus_api = PlusApi()
|
self.plus_api = PlusApi()
|
||||||
self.camera_metrics: dict[str, CameraMetricsTypes] = {}
|
self.camera_metrics: dict[str, CameraMetricsTypes] = {}
|
||||||
self.record_metrics: dict[str, RecordMetricsTypes] = {}
|
self.record_metrics: dict[str, RecordMetricsTypes] = {}
|
||||||
|
self.processes: dict[str, int] = {}
|
||||||
|
|
||||||
def set_environment_vars(self) -> None:
|
def set_environment_vars(self) -> None:
|
||||||
for key, value in self.config.environment_vars.items():
|
for key, value in self.config.environment_vars.items():
|
||||||
@ -77,6 +79,7 @@ class FrigateApp:
|
|||||||
)
|
)
|
||||||
self.log_process.daemon = True
|
self.log_process.daemon = True
|
||||||
self.log_process.start()
|
self.log_process.start()
|
||||||
|
self.processes["logger"] = self.log_process.pid or 0
|
||||||
root_configurer(self.log_queue)
|
root_configurer(self.log_queue)
|
||||||
|
|
||||||
def init_config(self) -> None:
|
def init_config(self) -> None:
|
||||||
@ -171,6 +174,12 @@ class FrigateApp:
|
|||||||
|
|
||||||
migrate_db.close()
|
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:
|
def init_recording_manager(self) -> None:
|
||||||
recording_process = mp.Process(
|
recording_process = mp.Process(
|
||||||
target=manage_recordings,
|
target=manage_recordings,
|
||||||
@ -180,6 +189,7 @@ class FrigateApp:
|
|||||||
recording_process.daemon = True
|
recording_process.daemon = True
|
||||||
self.recording_process = recording_process
|
self.recording_process = recording_process
|
||||||
recording_process.start()
|
recording_process.start()
|
||||||
|
self.processes["recording"] = recording_process.pid or 0
|
||||||
logger.info(f"Recording process started: {recording_process.pid}")
|
logger.info(f"Recording process started: {recording_process.pid}")
|
||||||
|
|
||||||
def bind_database(self) -> None:
|
def bind_database(self) -> None:
|
||||||
@ -191,7 +201,7 @@ class FrigateApp:
|
|||||||
|
|
||||||
def init_stats(self) -> None:
|
def init_stats(self) -> None:
|
||||||
self.stats_tracking = stats_init(
|
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:
|
def init_web_server(self) -> None:
|
||||||
@ -412,6 +422,7 @@ class FrigateApp:
|
|||||||
self.init_database()
|
self.init_database()
|
||||||
self.init_onvif()
|
self.init_onvif()
|
||||||
self.init_recording_manager()
|
self.init_recording_manager()
|
||||||
|
self.init_go2rtc()
|
||||||
self.bind_database()
|
self.bind_database()
|
||||||
self.init_dispatcher()
|
self.init_dispatcher()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
78
frigate/detectors/plugins/deepstack.py
Normal file
78
frigate/detectors/plugins/deepstack.py
Normal file
@ -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
|
||||||
@ -375,13 +375,13 @@ def set_sub_label(id):
|
|||||||
else:
|
else:
|
||||||
new_sub_label = None
|
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(
|
return make_response(
|
||||||
jsonify(
|
jsonify(
|
||||||
{
|
{
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": new_sub_label
|
"message": new_sub_label
|
||||||
+ " exceeds the 20 character limit for sub_label",
|
+ " exceeds the 100 character limit for sub_label",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
400,
|
400,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from playhouse.sqlite_ext import JSONField
|
|||||||
class Event(Model): # type: ignore[misc]
|
class Event(Model): # type: ignore[misc]
|
||||||
id = CharField(null=False, primary_key=True, max_length=30)
|
id = CharField(null=False, primary_key=True, max_length=30)
|
||||||
label = CharField(index=True, max_length=20)
|
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)
|
camera = CharField(index=True, max_length=20)
|
||||||
start_time = DateTimeField()
|
start_time = DateTimeField()
|
||||||
end_time = DateTimeField()
|
end_time = DateTimeField()
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import subprocess as sp
|
import os
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -192,12 +192,14 @@ class RecordingCleanup(threading.Thread):
|
|||||||
return
|
return
|
||||||
|
|
||||||
logger.debug(f"Oldest recording in the db: {oldest_timestamp}")
|
logger.debug(f"Oldest recording in the db: {oldest_timestamp}")
|
||||||
process = sp.run(
|
|
||||||
["find", RECORD_DIR, "-type", "f", "!", "-newermt", f"@{oldest_timestamp}"],
|
files_to_check = []
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
for root, _, files in os.walk(RECORD_DIR):
|
||||||
)
|
for file in files:
|
||||||
files_to_check = process.stdout.splitlines()
|
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:
|
for f in files_to_check:
|
||||||
p = Path(f)
|
p = Path(f)
|
||||||
@ -216,12 +218,10 @@ class RecordingCleanup(threading.Thread):
|
|||||||
recordings: Recordings = Recordings.select()
|
recordings: Recordings = Recordings.select()
|
||||||
|
|
||||||
# get all recordings files on disk
|
# get all recordings files on disk
|
||||||
process = sp.run(
|
files_on_disk = []
|
||||||
["find", RECORD_DIR, "-type", "f"],
|
for root, _, files in os.walk(RECORD_DIR):
|
||||||
capture_output=True,
|
for file in files:
|
||||||
text=True,
|
files_on_disk.append(os.path.join(root, file))
|
||||||
)
|
|
||||||
files_on_disk = process.stdout.splitlines()
|
|
||||||
|
|
||||||
recordings_to_delete = []
|
recordings_to_delete = []
|
||||||
for recording in recordings.objects().iterator():
|
for recording in recordings.objects().iterator():
|
||||||
|
|||||||
@ -46,6 +46,7 @@ def stats_init(
|
|||||||
config: FrigateConfig,
|
config: FrigateConfig,
|
||||||
camera_metrics: dict[str, CameraMetricsTypes],
|
camera_metrics: dict[str, CameraMetricsTypes],
|
||||||
detectors: dict[str, ObjectDetectProcess],
|
detectors: dict[str, ObjectDetectProcess],
|
||||||
|
processes: dict[str, int],
|
||||||
) -> StatsTrackingTypes:
|
) -> StatsTrackingTypes:
|
||||||
stats_tracking: StatsTrackingTypes = {
|
stats_tracking: StatsTrackingTypes = {
|
||||||
"camera_metrics": camera_metrics,
|
"camera_metrics": camera_metrics,
|
||||||
@ -53,6 +54,7 @@ def stats_init(
|
|||||||
"started": int(time.time()),
|
"started": int(time.time()),
|
||||||
"latest_frigate_version": get_latest_version(config),
|
"latest_frigate_version": get_latest_version(config),
|
||||||
"last_updated": int(time.time()),
|
"last_updated": int(time.time()),
|
||||||
|
"processes": processes,
|
||||||
}
|
}
|
||||||
return stats_tracking
|
return stats_tracking
|
||||||
|
|
||||||
@ -151,9 +153,12 @@ async def set_gpu_stats(
|
|||||||
nvidia_usage = get_nvidia_gpu_stats()
|
nvidia_usage = get_nvidia_gpu_stats()
|
||||||
|
|
||||||
if nvidia_usage:
|
if nvidia_usage:
|
||||||
name = nvidia_usage["name"]
|
for i in range(len(nvidia_usage)):
|
||||||
del nvidia_usage["name"]
|
stats[nvidia_usage[i]["name"]] = {
|
||||||
stats[name] = nvidia_usage
|
"gpu": str(round(float(nvidia_usage[i]["gpu"]), 2)) + "%",
|
||||||
|
"mem": str(round(float(nvidia_usage[i]["mem"]), 2)) + "%",
|
||||||
|
}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
stats["nvidia-gpu"] = {"gpu": -1, "mem": -1}
|
stats["nvidia-gpu"] = {"gpu": -1, "mem": -1}
|
||||||
hwaccel_errors.append(args)
|
hwaccel_errors.append(args)
|
||||||
@ -260,6 +265,12 @@ def stats_snapshot(
|
|||||||
"mount_type": get_fs_type(path),
|
"mount_type": get_fs_type(path),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stats["processes"] = {}
|
||||||
|
for name, pid in stats_tracking["processes"].items():
|
||||||
|
stats["processes"][name] = {
|
||||||
|
"pid": pid,
|
||||||
|
}
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -17,20 +17,20 @@ class TestGpuStats(unittest.TestCase):
|
|||||||
process.stdout = self.amd_results
|
process.stdout = self.amd_results
|
||||||
sp.return_value = process
|
sp.return_value = process
|
||||||
amd_stats = get_amd_gpu_stats()
|
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")
|
# @patch("subprocess.run")
|
||||||
def test_nvidia_gpu_stats(self, sp):
|
# def test_nvidia_gpu_stats(self, sp):
|
||||||
process = MagicMock()
|
# process = MagicMock()
|
||||||
process.returncode = 0
|
# process.returncode = 0
|
||||||
process.stdout = self.nvidia_results
|
# process.stdout = self.nvidia_results
|
||||||
sp.return_value = process
|
# sp.return_value = process
|
||||||
nvidia_stats = get_nvidia_gpu_stats()
|
# nvidia_stats = get_nvidia_gpu_stats()
|
||||||
assert nvidia_stats == {
|
# assert nvidia_stats == {
|
||||||
"name": "NVIDIA GeForce RTX 3050",
|
# "name": "NVIDIA GeForce RTX 3050",
|
||||||
"gpu": "42 %",
|
# "gpu": "42 %",
|
||||||
"mem": "61.5 %",
|
# "mem": "61.5 %",
|
||||||
}
|
# }
|
||||||
|
|
||||||
@patch("subprocess.run")
|
@patch("subprocess.run")
|
||||||
def test_intel_gpu_stats(self, sp):
|
def test_intel_gpu_stats(self, sp):
|
||||||
@ -40,6 +40,6 @@ class TestGpuStats(unittest.TestCase):
|
|||||||
sp.return_value = process
|
sp.return_value = process
|
||||||
intel_stats = get_intel_gpu_stats()
|
intel_stats = get_intel_gpu_stats()
|
||||||
assert intel_stats == {
|
assert intel_stats == {
|
||||||
"gpu": "1.34 %",
|
"gpu": "1.34%",
|
||||||
"mem": "- %",
|
"mem": "-%",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,3 +34,4 @@ class StatsTrackingTypes(TypedDict):
|
|||||||
started: int
|
started: int
|
||||||
latest_frigate_version: str
|
latest_frigate_version: str
|
||||||
last_updated: int
|
last_updated: int
|
||||||
|
processes: dict[str, int]
|
||||||
|
|||||||
189
frigate/util.py
189
frigate/util.py
@ -9,12 +9,14 @@ import signal
|
|||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import yaml
|
import yaml
|
||||||
|
import os
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from multiprocessing import shared_memory
|
from multiprocessing import shared_memory
|
||||||
from typing import Any, AnyStr, Optional, Tuple
|
from typing import Any, AnyStr, Optional, Tuple
|
||||||
|
import py3nvml.py3nvml as nvml
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -740,55 +742,54 @@ def escape_special_characters(path: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_cgroups_version() -> 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(
|
if not os.path.ismount(cgroup_path):
|
||||||
stat_command,
|
logger.debug(f"{cgroup_path} is not a mount point.")
|
||||||
encoding="ascii",
|
return "unknown"
|
||||||
capture_output=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if p.returncode == 0:
|
try:
|
||||||
value: str = p.stdout.strip().lower()
|
with open("/proc/mounts", "r") as f:
|
||||||
|
mounts = f.readlines()
|
||||||
|
|
||||||
if value == "cgroup2fs":
|
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"
|
return "cgroup2"
|
||||||
elif value == "tmpfs":
|
elif fs_type == "tmpfs":
|
||||||
return "cgroup"
|
return "cgroup"
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Could not determine cgroups version: unhandled filesystem {value}"
|
f"Could not determine cgroups version: unhandled filesystem {fs_type}"
|
||||||
)
|
)
|
||||||
else:
|
break
|
||||||
logger.debug(f"Could not determine cgroups version: {p.stderr}")
|
except Exception as e:
|
||||||
|
logger.debug(f"Could not determine cgroups version: {e}")
|
||||||
|
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
def get_docker_memlimit_bytes() -> int:
|
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
|
# check running a supported cgroups version
|
||||||
if get_cgroups_version() == "cgroup2":
|
if get_cgroups_version() == "cgroup2":
|
||||||
memlimit_command = ["cat", "/sys/fs/cgroup/memory.max"]
|
memlimit_path = "/sys/fs/cgroup/memory.max"
|
||||||
|
|
||||||
p = sp.run(
|
try:
|
||||||
memlimit_command,
|
with open(memlimit_path, "r") as f:
|
||||||
encoding="ascii",
|
value = f.read().strip()
|
||||||
capture_output=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if p.returncode == 0:
|
|
||||||
value: str = p.stdout.strip()
|
|
||||||
|
|
||||||
if value.isnumeric():
|
if value.isnumeric():
|
||||||
return int(value)
|
return int(value)
|
||||||
elif value.lower() == "max":
|
elif value.lower() == "max":
|
||||||
return -1
|
return -1
|
||||||
else:
|
except Exception as e:
|
||||||
logger.debug(f"Unable to get docker memlimit: {p.stderr}")
|
logger.debug(f"Unable to get docker memlimit: {e}")
|
||||||
|
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
@ -796,44 +797,46 @@ def get_docker_memlimit_bytes() -> int:
|
|||||||
def get_cpu_stats() -> dict[str, dict]:
|
def get_cpu_stats() -> dict[str, dict]:
|
||||||
"""Get cpu usages for each process id"""
|
"""Get cpu usages for each process id"""
|
||||||
usages = {}
|
usages = {}
|
||||||
# -n=2 runs to ensure extraneous values are not included
|
|
||||||
top_command = ["top", "-b", "-n", "2"]
|
|
||||||
|
|
||||||
docker_memlimit = get_docker_memlimit_bytes() / 1024
|
docker_memlimit = get_docker_memlimit_bytes() / 1024
|
||||||
|
total_mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / 1024
|
||||||
|
|
||||||
p = sp.run(
|
for process in psutil.process_iter(["pid", "name", "cpu_percent"]):
|
||||||
top_command,
|
pid = process.info["pid"]
|
||||||
encoding="ascii",
|
|
||||||
capture_output=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
|
||||||
logger.error(p.stderr)
|
|
||||||
return usages
|
|
||||||
else:
|
|
||||||
lines = p.stdout.split("\n")
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
stats = list(filter(lambda a: a != "", line.strip().split(" ")))
|
|
||||||
try:
|
try:
|
||||||
|
cpu_percent = process.info["cpu_percent"]
|
||||||
|
|
||||||
|
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:
|
if docker_memlimit > 0:
|
||||||
mem_res = int(stats[5])
|
mem_pct = round((mem_res / docker_memlimit) * 100, 1)
|
||||||
mem_pct = str(
|
|
||||||
round((float(mem_res) / float(docker_memlimit)) * 100, 1)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
mem_pct = stats[9]
|
mem_pct = round((mem_res / total_mem) * 100, 1)
|
||||||
|
|
||||||
idx = stats[0]
|
usages[pid] = {
|
||||||
|
"cpu": str(cpu_percent),
|
||||||
if stats[-1] == "go2rtc":
|
"cpu_average": str(round(cpu_average_usage, 2)),
|
||||||
idx = "go2rtc"
|
"mem": f"{mem_pct}",
|
||||||
elif stats[-1] == "frigate.r+":
|
|
||||||
idx = "recording"
|
|
||||||
|
|
||||||
usages[idx] = {
|
|
||||||
"cpu": stats[8],
|
|
||||||
"mem": mem_pct,
|
|
||||||
}
|
}
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
@ -860,9 +863,9 @@ def get_amd_gpu_stats() -> dict[str, str]:
|
|||||||
|
|
||||||
for hw in usages:
|
for hw in usages:
|
||||||
if "gpu" in hw:
|
if "gpu" in hw:
|
||||||
results["gpu"] = f"{hw.strip().split(' ')[1].replace('%', '')} %"
|
results["gpu"] = f"{hw.strip().split(' ')[1].replace('%', '')}%"
|
||||||
elif "vram" in hw:
|
elif "vram" in hw:
|
||||||
results["mem"] = f"{hw.strip().split(' ')[1].replace('%', '')} %"
|
results["mem"] = f"{hw.strip().split(' ')[1].replace('%', '')}%"
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@ -918,47 +921,45 @@ def get_intel_gpu_stats() -> dict[str, str]:
|
|||||||
else:
|
else:
|
||||||
video_avg = 1
|
video_avg = 1
|
||||||
|
|
||||||
results["gpu"] = f"{round((video_avg + render_avg) / 2, 2)} %"
|
results["gpu"] = f"{round((video_avg + render_avg) / 2, 2)}%"
|
||||||
results["mem"] = "- %"
|
results["mem"] = "-%"
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def get_nvidia_gpu_stats() -> dict[str, str]:
|
def try_get_info(f, h, default="N/A"):
|
||||||
"""Get stats using nvidia-smi."""
|
try:
|
||||||
nvidia_smi_command = [
|
v = f(h)
|
||||||
"nvidia-smi",
|
except nvml.NVMLError_NotSupported:
|
||||||
"--query-gpu=gpu_name,utilization.gpu,memory.used,memory.total",
|
v = default
|
||||||
"--format=csv",
|
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(
|
def get_nvidia_gpu_stats() -> dict[int, dict]:
|
||||||
nvidia_smi_command,
|
results = {}
|
||||||
encoding="ascii",
|
try:
|
||||||
capture_output=True,
|
nvml.nvmlInit()
|
||||||
)
|
deviceCount = nvml.nvmlDeviceGetCount()
|
||||||
|
for i in range(deviceCount):
|
||||||
if p.returncode != 0:
|
handle = nvml.nvmlDeviceGetHandleByIndex(i)
|
||||||
logger.error(f"Unable to poll nvidia GPU stats: {p.stderr}")
|
meminfo = try_get_info(nvml.nvmlDeviceGetMemoryInfo, handle)
|
||||||
return None
|
util = try_get_info(nvml.nvmlDeviceGetUtilizationRates, handle)
|
||||||
|
if util != "N/A":
|
||||||
|
gpu_util = util.gpu
|
||||||
else:
|
else:
|
||||||
usages = p.stdout.split("\n")[1].strip().split(",")
|
gpu_util = 0
|
||||||
memory_percent = f"{round(float(usages[2].replace(' MiB', '').strip()) / float(usages[3].replace(' MiB', '').strip()) * 100, 1)} %"
|
|
||||||
results: dict[str, str] = {
|
if meminfo != "N/A":
|
||||||
"name": usages[0],
|
gpu_mem_util = meminfo.used / meminfo.total * 100
|
||||||
"gpu": usages[1].strip(),
|
else:
|
||||||
"mem": memory_percent,
|
gpu_mem_util = -1
|
||||||
|
|
||||||
|
results[i] = {
|
||||||
|
"name": nvml.nvmlDeviceGetName(handle),
|
||||||
|
"gpu": gpu_util,
|
||||||
|
"mem": gpu_mem_util,
|
||||||
}
|
}
|
||||||
|
except:
|
||||||
|
return results
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|||||||
12
migrations/016_sublabel_increase.py
Normal file
12
migrations/016_sublabel_increase.py
Normal file
@ -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))
|
||||||
@ -11,6 +11,7 @@ peewee == 3.15.*
|
|||||||
peewee_migrate == 1.7.*
|
peewee_migrate == 1.7.*
|
||||||
psutil == 5.9.*
|
psutil == 5.9.*
|
||||||
pydantic == 1.10.*
|
pydantic == 1.10.*
|
||||||
|
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
||||||
PyYAML == 6.0
|
PyYAML == 6.0
|
||||||
pytz == 2023.3
|
pytz == 2023.3
|
||||||
tzlocal == 4.3
|
tzlocal == 4.3
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
@ -8,8 +8,9 @@
|
|||||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg">
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<link rel="mask-icon" href="/images/safari-pinned-tab.svg" color="#3b82f7" />
|
<link rel="mask-icon" href="/images/favicon.svg" color="#3b82f7" />
|
||||||
<meta name="msapplication-TileColor" content="#3b82f7" />
|
<meta name="msapplication-TileColor" content="#3b82f7" />
|
||||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
||||||
<meta name="theme-color" content="#111827" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#111827" media="(prefers-color-scheme: dark)" />
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "",
|
"name": "Frigate",
|
||||||
"short_name": "",
|
"short_name": "Frigate",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/images/android-chrome-192x192.png",
|
"src": "/icons/android-chrome-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/images/android-chrome-512x512.png",
|
"src": "/icons/android-chrome-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export default function Birdseye() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Object.entries(config.cameras)
|
return Object.entries(config.cameras)
|
||||||
.filter(([_, conf]) => conf.onvif?.host)
|
.filter(([_, conf]) => conf.onvif?.host && conf.onvif.host != '')
|
||||||
.map(([_, camera]) => camera.name);
|
.map(([_, camera]) => camera.name);
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ export default function Birdseye() {
|
|||||||
if ('MediaSource' in window) {
|
if ('MediaSource' in window) {
|
||||||
player = (
|
player = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="max-w-5xl xl:w-1/2">
|
<div className={ptzCameras.length ? 'max-w-5xl xl:w-1/2' : 'max-w-5xl'}>
|
||||||
<MsePlayer camera="birdseye" />
|
<MsePlayer camera="birdseye" />
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@ -54,7 +54,7 @@ export default function Birdseye() {
|
|||||||
} else if (viewSource == 'webrtc' && config.birdseye.restream) {
|
} else if (viewSource == 'webrtc' && config.birdseye.restream) {
|
||||||
player = (
|
player = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="max-w-5xl xl:w-1/2">
|
<div className={ptzCameras.length ? 'max-w-5xl xl:w-1/2' : 'max-w-5xl'}>
|
||||||
<WebRtcPlayer camera="birdseye" />
|
<WebRtcPlayer camera="birdseye" />
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@ -62,7 +62,7 @@ export default function Birdseye() {
|
|||||||
} else {
|
} else {
|
||||||
player = (
|
player = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="max-w-7xl xl:w-1/2">
|
<div className={ptzCameras.length ? 'max-w-5xl xl:w-1/2' : 'max-w-5xl'}>
|
||||||
<JSMpegPlayer camera="birdseye" />
|
<JSMpegPlayer camera="birdseye" />
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@ -94,7 +94,7 @@ export default function Birdseye() {
|
|||||||
<div className="xl:flex justify-between">
|
<div className="xl:flex justify-between">
|
||||||
{player}
|
{player}
|
||||||
|
|
||||||
{ptzCameras && (
|
{ptzCameras.length ? (
|
||||||
<div className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow p-4 w-full sm:w-min xl:h-min xl:w-1/2">
|
<div className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow p-4 w-full sm:w-min xl:h-min xl:w-1/2">
|
||||||
<Heading size="sm">Control Panel</Heading>
|
<Heading size="sm">Control Panel</Heading>
|
||||||
{ptzCameras.map((camera) => (
|
{ptzCameras.map((camera) => (
|
||||||
@ -104,7 +104,7 @@ export default function Birdseye() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
|
// calculates the seek seconds by adding up all the seconds in the segments prior to the playback time
|
||||||
const seekSeconds = useMemo(() => {
|
const seekSeconds = useMemo(() => {
|
||||||
if (!recordings) {
|
if (!recordings) {
|
||||||
return 0;
|
return undefined;
|
||||||
}
|
}
|
||||||
const currentUnix = getUnixTime(currentDate);
|
const currentUnix = getUnixTime(currentDate);
|
||||||
|
|
||||||
@ -103,6 +103,9 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se
|
|||||||
}, [playlistIndex]);
|
}, [playlistIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (seekSeconds === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
// if the playlist has moved on to the next item, then reset
|
// if the playlist has moved on to the next item, then reset
|
||||||
if (this.player.playlist.currentItem() !== playlistIndex) {
|
if (this.player.playlist.currentItem() !== playlistIndex) {
|
||||||
@ -114,7 +117,7 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se
|
|||||||
}
|
}
|
||||||
}, [seekSeconds, playlistIndex]);
|
}, [seekSeconds, playlistIndex]);
|
||||||
|
|
||||||
if (!recordingsSummary || !recordings || !config) {
|
if (!recordingsSummary || !config) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +148,9 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se
|
|||||||
player.playlist(playlist);
|
player.playlist(playlist);
|
||||||
player.playlist.autoadvance(0);
|
player.playlist.autoadvance(0);
|
||||||
player.playlist.currentItem(playlistIndex);
|
player.playlist.currentItem(playlistIndex);
|
||||||
|
if (seekSeconds !== undefined) {
|
||||||
player.currentTime(seekSeconds);
|
player.currentTime(seekSeconds);
|
||||||
|
}
|
||||||
this.player = player;
|
this.player = player;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { useWs } from '../api/ws';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Table, Tbody, Thead, Tr, Th, Td } from '../components/Table';
|
import { Table, Tbody, Thead, Tr, Th, Td } from '../components/Table';
|
||||||
import Link from '../components/Link';
|
import Link from '../components/Link';
|
||||||
|
import Button from '../components/Button';
|
||||||
|
import { About } from '../icons/About';
|
||||||
|
|
||||||
const emptyObject = Object.freeze({});
|
const emptyObject = Object.freeze({});
|
||||||
|
|
||||||
@ -66,9 +68,19 @@ export default function Storage() {
|
|||||||
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Heading size="lg">Overview</Heading>
|
<Heading size="lg">Overview</Heading>
|
||||||
<div data-testid="detectors" className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div data-testid="overview-types" className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
<div className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
||||||
|
<div className="flex justify-start">
|
||||||
<div className="text-lg flex justify-between p-4">Data</div>
|
<div className="text-lg flex justify-between p-4">Data</div>
|
||||||
|
<Button
|
||||||
|
className="rounded-full"
|
||||||
|
type="text"
|
||||||
|
color="gray"
|
||||||
|
aria-label="Overview of total used storage and total capacity of the drives that hold the recordings and snapshots directories."
|
||||||
|
>
|
||||||
|
<About className="w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<Table className="w-full">
|
<Table className="w-full">
|
||||||
<Thead>
|
<Thead>
|
||||||
@ -83,7 +95,17 @@ export default function Storage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
<div className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
||||||
|
<div className="flex justify-start">
|
||||||
<div className="text-lg flex justify-between p-4">Memory</div>
|
<div className="text-lg flex justify-between p-4">Memory</div>
|
||||||
|
<Button
|
||||||
|
className="rounded-full"
|
||||||
|
type="text"
|
||||||
|
color="gray"
|
||||||
|
aria-label="Overview of used and total memory in frigate process."
|
||||||
|
>
|
||||||
|
<About className="w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<Table className="w-full">
|
<Table className="w-full">
|
||||||
<Thead>
|
<Thead>
|
||||||
@ -110,7 +132,17 @@ export default function Storage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-start">
|
||||||
<Heading size="lg">Cameras</Heading>
|
<Heading size="lg">Cameras</Heading>
|
||||||
|
<Button
|
||||||
|
className="rounded-full"
|
||||||
|
type="text"
|
||||||
|
color="gray"
|
||||||
|
aria-label="Overview of per-camera storage usage and bandwidth."
|
||||||
|
>
|
||||||
|
<About className="w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div data-testid="detectors" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4">
|
<div data-testid="detectors" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4">
|
||||||
{Object.entries(storage).map(([name, camera]) => (
|
{Object.entries(storage).map(([name, camera]) => (
|
||||||
<div key={name} className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
<div key={name} className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { useState } from 'preact/hooks';
|
|||||||
import Dialog from '../components/Dialog';
|
import Dialog from '../components/Dialog';
|
||||||
import TimeAgo from '../components/TimeAgo';
|
import TimeAgo from '../components/TimeAgo';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
|
import { About } from '../icons/About';
|
||||||
|
|
||||||
const emptyObject = Object.freeze({});
|
const emptyObject = Object.freeze({});
|
||||||
|
|
||||||
@ -29,12 +30,14 @@ export default function System() {
|
|||||||
detectors,
|
detectors,
|
||||||
service = {},
|
service = {},
|
||||||
detection_fps: _,
|
detection_fps: _,
|
||||||
|
processes,
|
||||||
...cameras
|
...cameras
|
||||||
} = stats || initialStats || emptyObject;
|
} = stats || initialStats || emptyObject;
|
||||||
|
|
||||||
const detectorNames = Object.keys(detectors || emptyObject);
|
const detectorNames = Object.keys(detectors || emptyObject);
|
||||||
const gpuNames = Object.keys(gpu_usages || emptyObject);
|
const gpuNames = Object.keys(gpu_usages || emptyObject);
|
||||||
const cameraNames = Object.keys(cameras || emptyObject);
|
const cameraNames = Object.keys(cameras || emptyObject);
|
||||||
|
const processesNames = Object.keys(processes || emptyObject);
|
||||||
|
|
||||||
const onHandleFfprobe = async (camera, e) => {
|
const onHandleFfprobe = async (camera, e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
@ -206,7 +209,19 @@ export default function System() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Heading size="lg">Detectors</Heading>
|
<div className="flex justify-start">
|
||||||
|
<Heading className="self-center" size="lg">
|
||||||
|
Detectors
|
||||||
|
</Heading>
|
||||||
|
<Button
|
||||||
|
className="rounded-full"
|
||||||
|
type="text"
|
||||||
|
color="gray"
|
||||||
|
aria-label="Momentary resource usage of each process that is controlling the object detector. CPU % is for a single core."
|
||||||
|
>
|
||||||
|
<About className="w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div data-testid="detectors" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4">
|
<div data-testid="detectors" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4">
|
||||||
{detectorNames.map((detector) => (
|
{detectorNames.map((detector) => (
|
||||||
<div key={detector} className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
<div key={detector} className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
||||||
@ -235,8 +250,20 @@ export default function System() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-lg flex justify-between p-4">
|
<div className="text-lg flex justify-between">
|
||||||
<Heading size="lg">GPUs</Heading>
|
<div className="flex justify-start">
|
||||||
|
<Heading className="self-center" size="lg">
|
||||||
|
GPUs
|
||||||
|
</Heading>
|
||||||
|
<Button
|
||||||
|
className="rounded-full"
|
||||||
|
type="text"
|
||||||
|
color="gray"
|
||||||
|
aria-label="Momentary resource usage of each GPU. Intel GPUs do not support memory stats."
|
||||||
|
>
|
||||||
|
<About className="w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Button onClick={(e) => onHandleVainfo(e)}>vainfo</Button>
|
<Button onClick={(e) => onHandleVainfo(e)}>vainfo</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -280,7 +307,19 @@ export default function System() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Heading size="lg">Cameras</Heading>
|
<div className="flex justify-start">
|
||||||
|
<Heading className="self-center" size="lg">
|
||||||
|
Cameras
|
||||||
|
</Heading>
|
||||||
|
<Button
|
||||||
|
className="rounded-full"
|
||||||
|
type="text"
|
||||||
|
color="gray"
|
||||||
|
aria-label="Momentary resource usage of each process interacting with the camera stream. CPU % is for a single core."
|
||||||
|
>
|
||||||
|
<About className="w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
{!cameras ? (
|
{!cameras ? (
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
) : (
|
) : (
|
||||||
@ -345,9 +384,21 @@ export default function System() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Heading size="lg">Other Processes</Heading>
|
<div className="flex justify-start">
|
||||||
|
<Heading className="self-center" size="lg">
|
||||||
|
Other Processes
|
||||||
|
</Heading>
|
||||||
|
<Button
|
||||||
|
className="rounded-full"
|
||||||
|
type="text"
|
||||||
|
color="gray"
|
||||||
|
aria-label="Momentary resource usage for other important processes. CPU % is for a single core."
|
||||||
|
>
|
||||||
|
<About className="w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div data-testid="cameras" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4">
|
<div data-testid="cameras" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4">
|
||||||
{['go2rtc', 'recording'].map((process) => (
|
{processesNames.map((process) => (
|
||||||
<div key={process} className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
<div key={process} className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
||||||
<div className="capitalize text-lg flex justify-between p-4">
|
<div className="capitalize text-lg flex justify-between p-4">
|
||||||
<div className="text-lg flex justify-between">{process}</div>
|
<div className="text-lg flex justify-between">{process}</div>
|
||||||
@ -356,14 +407,18 @@ export default function System() {
|
|||||||
<Table className="w-full">
|
<Table className="w-full">
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
|
<Th>P-ID</Th>
|
||||||
<Th>CPU %</Th>
|
<Th>CPU %</Th>
|
||||||
|
<Th>Avg CPU %</Th>
|
||||||
<Th>Memory %</Th>
|
<Th>Memory %</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
<Tr key="ffmpeg" index="0">
|
<Tr key="other" index="0">
|
||||||
<Td>{cpu_usages[process]?.['cpu'] || '- '}%</Td>
|
<Td>{processes[process]['pid'] || '- '}</Td>
|
||||||
<Td>{cpu_usages[process]?.['mem'] || '- '}%</Td>
|
<Td>{cpu_usages[processes[process]['pid']]?.['cpu'] || '- '}%</Td>
|
||||||
|
<Td>{cpu_usages[processes[process]['pid']]?.['cpu_average'] || '- '}%</Td>
|
||||||
|
<Td>{cpu_usages[processes[process]['pid']]?.['mem'] || '- '}%</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user