Merge branch 'dev' into timeline-optimizations

This commit is contained in:
Nicolas Mowen 2023-05-04 17:16:47 -06:00 committed by GitHub
commit 45dd1abaad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 396 additions and 166 deletions

View File

@ -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://<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.

View File

@ -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`
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
{

View File

@ -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:

View 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

View File

@ -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,

View File

@ -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()

View File

@ -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():

View File

@ -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

View File

@ -19,18 +19,18 @@ class TestGpuStats(unittest.TestCase):
amd_stats = get_amd_gpu_stats()
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):

View File

@ -34,3 +34,4 @@ class StatsTrackingTypes(TypedDict):
started: int
latest_frigate_version: str
last_updated: int
processes: dict[str, int]

View File

@ -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":
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 value == "tmpfs":
elif fs_type == "tmpfs":
return "cgroup"
else:
logger.debug(
f"Could not determine cgroups version: unhandled filesystem {value}"
f"Could not determine cgroups version: unhandled filesystem {fs_type}"
)
else:
logger.debug(f"Could not determine cgroups version: {p.stderr}")
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,44 +797,46 @@ 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,
)
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(" ")))
for process in psutil.process_iter(["pid", "name", "cpu_percent"]):
pid = process.info["pid"]
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:
mem_res = int(stats[5])
mem_pct = str(
round((float(mem_res) / float(docker_memlimit)) * 100, 1)
)
mem_pct = round((mem_res / docker_memlimit) * 100, 1)
else:
mem_pct = stats[9]
mem_pct = round((mem_res / total_mem) * 100, 1)
idx = stats[0]
if stats[-1] == "go2rtc":
idx = "go2rtc"
elif stats[-1] == "frigate.r+":
idx = "recording"
usages[idx] = {
"cpu": stats[8],
"mem": mem_pct,
usages[pid] = {
"cpu": str(cpu_percent),
"cpu_average": str(round(cpu_average_usage, 2)),
"mem": f"{mem_pct}",
}
except:
continue
@ -923,42 +926,40 @@ def get_intel_gpu_stats() -> dict[str, str]:
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,
)
if p.returncode != 0:
logger.error(f"Unable to poll nvidia GPU stats: {p.stderr}")
return None
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:
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,
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:
return results
return results

View 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))

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -8,8 +8,9 @@
<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="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="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="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#111827" media="(prefers-color-scheme: dark)" />

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -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"
}

View File

@ -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 = (
<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" />
</div>
</Fragment>
@ -54,7 +54,7 @@ export default function Birdseye() {
} else if (viewSource == 'webrtc' && config.birdseye.restream) {
player = (
<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" />
</div>
</Fragment>
@ -62,7 +62,7 @@ export default function Birdseye() {
} else {
player = (
<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" />
</div>
</Fragment>
@ -94,7 +94,7 @@ export default function Birdseye() {
<div className="xl:flex justify-between">
{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">
<Heading size="sm">Control Panel</Heading>
{ptzCameras.map((camera) => (
@ -104,7 +104,7 @@ export default function Birdseye() {
</div>
))}
</div>
)}
) : null}
</div>
</div>
);

View File

@ -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 <ActivityIndicator />;
}
@ -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);
if (seekSeconds !== undefined) {
player.currentTime(seekSeconds);
}
this.player = player;
}
}}

View File

@ -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() {
<Fragment>
<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="flex justify-start">
<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">
<Table className="w-full">
<Thead>
@ -83,7 +95,17 @@ export default function Storage() {
</div>
</div>
<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>
<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">
<Table className="w-full">
<Thead>
@ -110,7 +132,17 @@ export default function Storage() {
</div>
</div>
<div className="flex justify-start">
<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">
{Object.entries(storage).map(([name, camera]) => (
<div key={name} className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">

View File

@ -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() {
</div>
) : (
<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">
{detectorNames.map((detector) => (
<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 className="text-lg flex justify-between p-4">
<Heading size="lg">GPUs</Heading>
<div className="text-lg flex justify-between">
<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>
</div>
@ -280,7 +307,19 @@ export default function System() {
</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 ? (
<ActivityIndicator />
) : (
@ -345,9 +384,21 @@ export default function System() {
</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">
{['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 className="capitalize text-lg flex justify-between p-4">
<div className="text-lg flex justify-between">{process}</div>
@ -356,14 +407,18 @@ export default function System() {
<Table className="w-full">
<Thead>
<Tr>
<Th>P-ID</Th>
<Th>CPU %</Th>
<Th>Avg CPU %</Th>
<Th>Memory %</Th>
</Tr>
</Thead>
<Tbody>
<Tr key="ffmpeg" index="0">
<Td>{cpu_usages[process]?.['cpu'] || '- '}%</Td>
<Td>{cpu_usages[process]?.['mem'] || '- '}%</Td>
<Tr key="other" index="0">
<Td>{processes[process]['pid'] || '- '}</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>
</Tbody>
</Table>