mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 13:45:25 +03:00
Merge branch 'blakeblackshear:dev' into dev
This commit is contained in:
commit
966025e00b
@ -1351,6 +1351,6 @@ def preview_thumbnail(file_name: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = make_response(jpg_bytes)
|
response = make_response(jpg_bytes)
|
||||||
response.headers["Content-Type"] = "image/jpeg"
|
response.headers["Content-Type"] = "image/webp"
|
||||||
response.headers["Cache-Control"] = "private, max-age=31536000"
|
response.headers["Cache-Control"] = "private, max-age=31536000"
|
||||||
return response
|
return response
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"""Handle communication between Frigate and other applications."""
|
"""Handle communication between Frigate and other applications."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
@ -7,6 +8,7 @@ from typing import Any, Callable, Optional
|
|||||||
from frigate.comms.config_updater import ConfigPublisher
|
from frigate.comms.config_updater import ConfigPublisher
|
||||||
from frigate.config import BirdseyeModeEnum, FrigateConfig
|
from frigate.config import BirdseyeModeEnum, FrigateConfig
|
||||||
from frigate.const import (
|
from frigate.const import (
|
||||||
|
CLEAR_ONGOING_REVIEW_SEGMENTS,
|
||||||
INSERT_MANY_RECORDINGS,
|
INSERT_MANY_RECORDINGS,
|
||||||
INSERT_PREVIEW,
|
INSERT_PREVIEW,
|
||||||
REQUEST_REGION_GRID,
|
REQUEST_REGION_GRID,
|
||||||
@ -116,6 +118,10 @@ class Dispatcher:
|
|||||||
)
|
)
|
||||||
.execute()
|
.execute()
|
||||||
)
|
)
|
||||||
|
elif topic == CLEAR_ONGOING_REVIEW_SEGMENTS:
|
||||||
|
ReviewSegment.update(end_time=datetime.datetime.now().timestamp()).where(
|
||||||
|
ReviewSegment.end_time == None
|
||||||
|
).execute()
|
||||||
else:
|
else:
|
||||||
self.publish(topic, payload, retain=False)
|
self.publish(topic, payload, retain=False)
|
||||||
|
|
||||||
|
|||||||
@ -79,6 +79,7 @@ INSERT_MANY_RECORDINGS = "insert_many_recordings"
|
|||||||
INSERT_PREVIEW = "insert_preview"
|
INSERT_PREVIEW = "insert_preview"
|
||||||
REQUEST_REGION_GRID = "request_region_grid"
|
REQUEST_REGION_GRID = "request_region_grid"
|
||||||
UPSERT_REVIEW_SEGMENT = "upsert_review_segment"
|
UPSERT_REVIEW_SEGMENT = "upsert_review_segment"
|
||||||
|
CLEAR_ONGOING_REVIEW_SEGMENTS = "clear_ongoing_review_segments"
|
||||||
|
|
||||||
# Autotracking
|
# Autotracking
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,12 @@ from frigate.comms.config_updater import ConfigSubscriber
|
|||||||
from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum
|
from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum
|
||||||
from frigate.comms.inter_process import InterProcessRequestor
|
from frigate.comms.inter_process import InterProcessRequestor
|
||||||
from frigate.config import CameraConfig, FrigateConfig
|
from frigate.config import CameraConfig, FrigateConfig
|
||||||
from frigate.const import ALL_ATTRIBUTE_LABELS, CLIPS_DIR, UPSERT_REVIEW_SEGMENT
|
from frigate.const import (
|
||||||
|
ALL_ATTRIBUTE_LABELS,
|
||||||
|
CLEAR_ONGOING_REVIEW_SEGMENTS,
|
||||||
|
CLIPS_DIR,
|
||||||
|
UPSERT_REVIEW_SEGMENT,
|
||||||
|
)
|
||||||
from frigate.events.external import ManualEventState
|
from frigate.events.external import ManualEventState
|
||||||
from frigate.models import ReviewSegment
|
from frigate.models import ReviewSegment
|
||||||
from frigate.object_processing import TrackedObject
|
from frigate.object_processing import TrackedObject
|
||||||
@ -146,25 +151,64 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
|
|
||||||
self.stop_event = stop_event
|
self.stop_event = stop_event
|
||||||
|
|
||||||
def update_segment(self, segment: PendingReviewSegment) -> None:
|
# clear ongoing review segments from last instance
|
||||||
"""Update segment."""
|
self.requestor.send_data(CLEAR_ONGOING_REVIEW_SEGMENTS, "")
|
||||||
seg_data = segment.get_data(ended=False)
|
|
||||||
self.requestor.send_data(UPSERT_REVIEW_SEGMENT, seg_data)
|
def new_segment(
|
||||||
|
self,
|
||||||
|
segment: PendingReviewSegment,
|
||||||
|
) -> None:
|
||||||
|
"""New segment."""
|
||||||
|
new_data = segment.get_data(ended=False)
|
||||||
|
self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data)
|
||||||
|
start_data = {k.name: v for k, v in new_data.items()}
|
||||||
self.requestor.send_data(
|
self.requestor.send_data(
|
||||||
"reviews",
|
"reviews",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{"type": "update", "review": {k.name: v for k, v in seg_data.items()}}
|
{
|
||||||
|
"type": "new",
|
||||||
|
"before": start_data,
|
||||||
|
"after": start_data,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_segment(
|
||||||
|
self,
|
||||||
|
segment: PendingReviewSegment,
|
||||||
|
camera_config: CameraConfig,
|
||||||
|
frame,
|
||||||
|
objects: list[TrackedObject],
|
||||||
|
) -> None:
|
||||||
|
"""Update segment."""
|
||||||
|
prev_data = segment.get_data(ended=False)
|
||||||
|
segment.update_frame(camera_config, frame, objects)
|
||||||
|
new_data = segment.get_data(ended=False)
|
||||||
|
self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data)
|
||||||
|
self.requestor.send_data(
|
||||||
|
"reviews",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"type": "update",
|
||||||
|
"before": {k.name: v for k, v in prev_data.items()},
|
||||||
|
"after": {k.name: v for k, v in new_data.items()},
|
||||||
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def end_segment(self, segment: PendingReviewSegment) -> None:
|
def end_segment(self, segment: PendingReviewSegment) -> None:
|
||||||
"""End segment."""
|
"""End segment."""
|
||||||
seg_data = segment.get_data(ended=True)
|
final_data = segment.get_data(ended=True)
|
||||||
self.requestor.send_data(UPSERT_REVIEW_SEGMENT, seg_data)
|
self.requestor.send_data(UPSERT_REVIEW_SEGMENT, final_data)
|
||||||
|
end_data = {k.name: v for k, v in final_data.items()}
|
||||||
self.requestor.send_data(
|
self.requestor.send_data(
|
||||||
"reviews",
|
"reviews",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{"type": "end", "review": {k.name: v for k, v in seg_data.items()}}
|
{
|
||||||
|
"type": "end",
|
||||||
|
"before": end_data,
|
||||||
|
"after": end_data,
|
||||||
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.active_review_segments[segment.camera] = None
|
self.active_review_segments[segment.camera] = None
|
||||||
@ -219,9 +263,10 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
yuv_frame = self.frame_manager.get(
|
yuv_frame = self.frame_manager.get(
|
||||||
frame_id, camera_config.frame_shape_yuv
|
frame_id, camera_config.frame_shape_yuv
|
||||||
)
|
)
|
||||||
segment.update_frame(camera_config, yuv_frame, active_objects)
|
self.update_segment(
|
||||||
|
segment, camera_config, yuv_frame, active_objects
|
||||||
|
)
|
||||||
self.frame_manager.close(frame_id)
|
self.frame_manager.close(frame_id)
|
||||||
self.update_segment(segment)
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@ -317,7 +362,7 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
camera_config, yuv_frame, active_objects
|
camera_config, yuv_frame, active_objects
|
||||||
)
|
)
|
||||||
self.frame_manager.close(frame_id)
|
self.frame_manager.close(frame_id)
|
||||||
self.update_segment(self.active_review_segments[camera])
|
self.new_segment(self.active_review_segments[camera])
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@ -413,9 +413,7 @@ def track_camera(
|
|||||||
object_filters = config.objects.filters
|
object_filters = config.objects.filters
|
||||||
|
|
||||||
motion_detector = ImprovedMotionDetector(
|
motion_detector = ImprovedMotionDetector(
|
||||||
frame_shape,
|
frame_shape, config.motion, config.detect.fps, name=config.name
|
||||||
config.motion,
|
|
||||||
config.detect.fps,
|
|
||||||
)
|
)
|
||||||
object_detector = RemoteObjectDetector(
|
object_detector = RemoteObjectDetector(
|
||||||
name, labelmap, detection_queue, result_connection, model_config, stop_event
|
name, labelmap, detection_queue, result_connection, model_config, stop_event
|
||||||
|
|||||||
11
web/package-lock.json
generated
11
web/package-lock.json
generated
@ -39,6 +39,7 @@
|
|||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"immer": "^10.0.4",
|
"immer": "^10.0.4",
|
||||||
"konva": "^9.3.6",
|
"konva": "^9.3.6",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.372.0",
|
"lucide-react": "^0.372.0",
|
||||||
"monaco-yaml": "^5.1.1",
|
"monaco-yaml": "^5.1.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
@ -71,6 +72,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@testing-library/jest-dom": "^6.1.5",
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
|
"@types/lodash": "^4.17.0",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"@types/react": "^18.2.79",
|
"@types/react": "^18.2.79",
|
||||||
"@types/react-dom": "^18.2.25",
|
"@types/react-dom": "^18.2.25",
|
||||||
@ -2523,6 +2525,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
|
||||||
|
"integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/mute-stream": {
|
"node_modules/@types/mute-stream": {
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz",
|
||||||
@ -5299,8 +5307,7 @@
|
|||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
|
|||||||
@ -44,6 +44,7 @@
|
|||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"immer": "^10.0.4",
|
"immer": "^10.0.4",
|
||||||
"konva": "^9.3.6",
|
"konva": "^9.3.6",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.372.0",
|
"lucide-react": "^0.372.0",
|
||||||
"monaco-yaml": "^5.1.1",
|
"monaco-yaml": "^5.1.1",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
@ -76,6 +77,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@testing-library/jest-dom": "^6.1.5",
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
|
"@types/lodash": "^4.17.0",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"@types/react": "^18.2.79",
|
"@types/react": "^18.2.79",
|
||||||
"@types/react-dom": "^18.2.25",
|
"@types/react-dom": "^18.2.25",
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { useFrigateStats } from "@/api/ws";
|
import { useFrigateStats } from "@/api/ws";
|
||||||
|
import {
|
||||||
|
StatusBarMessagesContext,
|
||||||
|
StatusMessage,
|
||||||
|
} from "@/context/statusbar-provider";
|
||||||
import useStats from "@/hooks/use-stats";
|
import useStats from "@/hooks/use-stats";
|
||||||
import { FrigateStats } from "@/types/stats";
|
import { FrigateStats } from "@/types/stats";
|
||||||
import { useMemo } from "react";
|
import { useContext, useEffect, useMemo } from "react";
|
||||||
|
import { FaCheck } from "react-icons/fa";
|
||||||
import { IoIosWarning } from "react-icons/io";
|
import { IoIosWarning } from "react-icons/io";
|
||||||
import { MdCircle } from "react-icons/md";
|
import { MdCircle } from "react-icons/md";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -11,6 +16,10 @@ export default function Statusbar() {
|
|||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
});
|
});
|
||||||
const { payload: latestStats } = useFrigateStats();
|
const { payload: latestStats } = useFrigateStats();
|
||||||
|
const { messages, addMessage, clearMessages } = useContext(
|
||||||
|
StatusBarMessagesContext,
|
||||||
|
)!;
|
||||||
|
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
if (latestStats) {
|
if (latestStats) {
|
||||||
return latestStats;
|
return latestStats;
|
||||||
@ -31,6 +40,13 @@ export default function Statusbar() {
|
|||||||
|
|
||||||
const { potentialProblems } = useStats(stats);
|
const { potentialProblems } = useStats(stats);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearMessages("stats");
|
||||||
|
potentialProblems.forEach((problem) => {
|
||||||
|
addMessage("stats", problem.text, problem.color);
|
||||||
|
});
|
||||||
|
}, [potentialProblems, addMessage, clearMessages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute left-0 bottom-0 right-0 w-full h-8 flex justify-between items-center px-4 bg-background_alt z-10 dark:text-secondary-foreground border-t border-secondary-highlight">
|
<div className="absolute left-0 bottom-0 right-0 w-full h-8 flex justify-between items-center px-4 bg-background_alt z-10 dark:text-secondary-foreground border-t border-secondary-highlight">
|
||||||
<div className="h-full flex items-center gap-2">
|
<div className="h-full flex items-center gap-2">
|
||||||
@ -86,16 +102,26 @@ export default function Statusbar() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full flex items-center gap-2">
|
<div className="h-full flex items-center gap-2">
|
||||||
{potentialProblems.map((prob) => (
|
{Object.entries(messages).length === 0 ? (
|
||||||
<div
|
<div className="flex items-center text-sm gap-2">
|
||||||
key={prob.text}
|
<FaCheck className="size-3 text-green-500" />
|
||||||
className="flex items-center text-sm gap-2 capitalize"
|
System is healthy
|
||||||
>
|
</div>
|
||||||
<IoIosWarning className={`size-5 ${prob.color}`} />
|
) : (
|
||||||
{prob.text}
|
Object.entries(messages).map(([key, messageArray]) => (
|
||||||
|
<div key={key} className="h-full flex items-center gap-2">
|
||||||
|
{messageArray.map(({ id, text, color }: StatusMessage) => (
|
||||||
|
<div key={id} className="flex items-center text-sm gap-2">
|
||||||
|
<IoIosWarning
|
||||||
|
className={`size-5 ${color || "text-danger"}`}
|
||||||
|
/>
|
||||||
|
{text}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export default function CameraImage({
|
|||||||
{enabled ? (
|
{enabled ? (
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
className="object-contain rounded-2xl"
|
className="object-contain rounded-lg md:rounded-2xl"
|
||||||
onLoad={() => {
|
onLoad={() => {
|
||||||
setHasLoaded(true);
|
setHasLoaded(true);
|
||||||
|
|
||||||
|
|||||||
@ -100,7 +100,7 @@ export default function CameraImage({
|
|||||||
>
|
>
|
||||||
{enabled ? (
|
{enabled ? (
|
||||||
<canvas
|
<canvas
|
||||||
className="rounded-2xl"
|
className="rounded-lg md:rounded-2xl"
|
||||||
data-testid="cameraimage-canvas"
|
data-testid="cameraimage-canvas"
|
||||||
height={scaledHeight}
|
height={scaledHeight}
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
InProgressPreview,
|
InProgressPreview,
|
||||||
VideoPreview,
|
VideoPreview,
|
||||||
} from "../player/PreviewThumbnailPlayer";
|
} from "../player/PreviewThumbnailPlayer";
|
||||||
|
import { isCurrentHour } from "@/utils/dateUtil";
|
||||||
|
|
||||||
type AnimatedEventCardProps = {
|
type AnimatedEventCardProps = {
|
||||||
event: ReviewSegment;
|
event: ReviewSegment;
|
||||||
@ -19,10 +20,14 @@ type AnimatedEventCardProps = {
|
|||||||
export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
|
export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
|
const currentHour = useMemo(() => isCurrentHour(event.start_time), [event]);
|
||||||
|
|
||||||
// preview
|
// preview
|
||||||
|
|
||||||
const { data: previews } = useSWR<Preview[]>(
|
const { data: previews } = useSWR<Preview[]>(
|
||||||
`/preview/${event.camera}/start/${Math.round(event.start_time)}/end/${Math.round(event.end_time || event.start_time + 20)}`,
|
currentHour
|
||||||
|
? null
|
||||||
|
: `/preview/${event.camera}/start/${Math.round(event.start_time)}/end/${Math.round(event.end_time || event.start_time + 20)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// interaction
|
// interaction
|
||||||
@ -63,7 +68,7 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="size-full rounded cursor-pointer overflow-hidden"
|
className="size-full rounded md:rounded-lg cursor-pointer overflow-hidden"
|
||||||
onClick={onOpenReview}
|
onClick={onOpenReview}
|
||||||
>
|
>
|
||||||
{previews ? (
|
{previews ? (
|
||||||
|
|||||||
@ -104,7 +104,7 @@ export default function ExportCard({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`relative aspect-video bg-black rounded-2xl flex justify-center items-center ${className}`}
|
className={`relative aspect-video bg-black rounded-lg md:rounded-2xl flex justify-center items-center ${className}`}
|
||||||
onMouseEnter={
|
onMouseEnter={
|
||||||
isDesktop && !exportedRecording.in_progress
|
isDesktop && !exportedRecording.in_progress
|
||||||
? () => setHovered(true)
|
? () => setHovered(true)
|
||||||
@ -123,7 +123,7 @@ export default function ExportCard({
|
|||||||
>
|
>
|
||||||
{hovered && (
|
{hovered && (
|
||||||
<>
|
<>
|
||||||
<div className="absolute inset-0 z-10 bg-black bg-opacity-60 rounded-2xl" />
|
<div className="absolute inset-0 z-10 bg-black bg-opacity-60 rounded-lg md:rounded-2xl" />
|
||||||
<div className="absolute top-1 right-1 flex items-center gap-2">
|
<div className="absolute top-1 right-1 flex items-center gap-2">
|
||||||
<a
|
<a
|
||||||
className="z-20"
|
className="z-20"
|
||||||
@ -172,19 +172,19 @@ export default function ExportCard({
|
|||||||
<>
|
<>
|
||||||
{exportedRecording.thumb_path.length > 0 ? (
|
{exportedRecording.thumb_path.length > 0 ? (
|
||||||
<img
|
<img
|
||||||
className="size-full absolute inset-0 object-contain aspect-video rounded-2xl"
|
className="size-full absolute inset-0 object-contain aspect-video rounded-lg md:rounded-2xl"
|
||||||
src={exportedRecording.thumb_path.replace("/media/frigate", "")}
|
src={exportedRecording.thumb_path.replace("/media/frigate", "")}
|
||||||
onLoad={() => setLoading(false)}
|
onLoad={() => setLoading(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-0 bg-secondary rounded-2xl" />
|
<div className="absolute inset-0 bg-secondary rounded-lg md:rounded-2xl" />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{loading && (
|
{loading && (
|
||||||
<Skeleton className="absolute inset-0 aspect-video rounded-2xl" />
|
<Skeleton className="absolute inset-0 aspect-video rounded-lg md:rounded-2xl" />
|
||||||
)}
|
)}
|
||||||
<div className="absolute bottom-0 inset-x-0 rounded-b-l z-10 h-[20%] bg-gradient-to-t from-black/60 to-transparent pointer-events-none rounded-2xl">
|
<div className="absolute bottom-0 inset-x-0 rounded-b-l z-10 h-[20%] bg-gradient-to-t from-black/60 to-transparent pointer-events-none rounded-lg md:rounded-2xl">
|
||||||
<div className="flex h-full justify-between items-end mx-3 pb-1 text-white text-sm capitalize">
|
<div className="flex h-full justify-between items-end mx-3 pb-1 text-white text-sm capitalize">
|
||||||
{exportedRecording.name.replaceAll("_", " ")}
|
{exportedRecording.name.replaceAll("_", " ")}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -210,7 +210,9 @@ export default function GeneralSettings({ className }: GeneralSettings) {
|
|||||||
</SubItemTrigger>
|
</SubItemTrigger>
|
||||||
<Portal>
|
<Portal>
|
||||||
<SubItemContent
|
<SubItemContent
|
||||||
className={isDesktop ? "" : "w-[92%] rounded-2xl"}
|
className={
|
||||||
|
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span tabIndex={0} className="sr-only" />
|
<span tabIndex={0} className="sr-only" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -280,7 +282,9 @@ export default function GeneralSettings({ className }: GeneralSettings) {
|
|||||||
</SubItemTrigger>
|
</SubItemTrigger>
|
||||||
<Portal>
|
<Portal>
|
||||||
<SubItemContent
|
<SubItemContent
|
||||||
className={isDesktop ? "" : "w-[92%] rounded-2xl"}
|
className={
|
||||||
|
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span tabIndex={0} className="sr-only" />
|
<span tabIndex={0} className="sr-only" />
|
||||||
{colorSchemes.map((scheme) => (
|
{colorSchemes.map((scheme) => (
|
||||||
|
|||||||
@ -4,11 +4,15 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateStats } from "@/types/stats";
|
import { FrigateStats } from "@/types/stats";
|
||||||
import { useFrigateStats } from "@/api/ws";
|
import { useFrigateStats } from "@/api/ws";
|
||||||
import { useMemo } from "react";
|
import { useContext, useEffect, useMemo } from "react";
|
||||||
import useStats from "@/hooks/use-stats";
|
import useStats from "@/hooks/use-stats";
|
||||||
import GeneralSettings from "../menu/GeneralSettings";
|
import GeneralSettings from "../menu/GeneralSettings";
|
||||||
import AccountSettings from "../menu/AccountSettings";
|
import AccountSettings from "../menu/AccountSettings";
|
||||||
import useNavigation from "@/hooks/use-navigation";
|
import useNavigation from "@/hooks/use-navigation";
|
||||||
|
import {
|
||||||
|
StatusBarMessagesContext,
|
||||||
|
StatusMessage,
|
||||||
|
} from "@/context/statusbar-provider";
|
||||||
|
|
||||||
function Bottombar() {
|
function Bottombar() {
|
||||||
const navItems = useNavigation("secondary");
|
const navItems = useNavigation("secondary");
|
||||||
@ -30,6 +34,11 @@ function StatusAlertNav() {
|
|||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
});
|
});
|
||||||
const { payload: latestStats } = useFrigateStats();
|
const { payload: latestStats } = useFrigateStats();
|
||||||
|
|
||||||
|
const { messages, addMessage, clearMessages } = useContext(
|
||||||
|
StatusBarMessagesContext,
|
||||||
|
)!;
|
||||||
|
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
if (latestStats) {
|
if (latestStats) {
|
||||||
return latestStats;
|
return latestStats;
|
||||||
@ -39,7 +48,14 @@ function StatusAlertNav() {
|
|||||||
}, [initialStats, latestStats]);
|
}, [initialStats, latestStats]);
|
||||||
const { potentialProblems } = useStats(stats);
|
const { potentialProblems } = useStats(stats);
|
||||||
|
|
||||||
if (!potentialProblems || potentialProblems.length == 0) {
|
useEffect(() => {
|
||||||
|
clearMessages("stats");
|
||||||
|
potentialProblems.forEach((problem) => {
|
||||||
|
addMessage("stats", problem.text, problem.color);
|
||||||
|
});
|
||||||
|
}, [potentialProblems, addMessage, clearMessages]);
|
||||||
|
|
||||||
|
if (!messages || Object.keys(messages).length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,13 +66,16 @@ function StatusAlertNav() {
|
|||||||
</DrawerTrigger>
|
</DrawerTrigger>
|
||||||
<DrawerContent className="max-h-[75dvh] px-2 mx-1 rounded-t-2xl overflow-hidden">
|
<DrawerContent className="max-h-[75dvh] px-2 mx-1 rounded-t-2xl overflow-hidden">
|
||||||
<div className="w-full h-auto py-4 overflow-y-auto overflow-x-hidden flex flex-col items-center gap-2">
|
<div className="w-full h-auto py-4 overflow-y-auto overflow-x-hidden flex flex-col items-center gap-2">
|
||||||
{potentialProblems.map((prob) => (
|
{Object.entries(messages).map(([key, messageArray]) => (
|
||||||
<div
|
<div key={key} className="w-full flex items-center gap-2">
|
||||||
key={prob.text}
|
{messageArray.map(({ id, text, color }: StatusMessage) => (
|
||||||
className="w-full flex items-center text-xs gap-2 capitalize"
|
<div key={id} className="flex items-center text-xs gap-2">
|
||||||
>
|
<IoIosWarning
|
||||||
<IoIosWarning className={`size-5 ${prob.color}`} />
|
className={`size-5 ${color || "text-danger"}`}
|
||||||
{prob.text}
|
/>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -130,7 +130,9 @@ export default function ExportDialog({
|
|||||||
</Trigger>
|
</Trigger>
|
||||||
<Content
|
<Content
|
||||||
className={
|
className={
|
||||||
isDesktop ? "sm:rounded-2xl" : "px-4 pb-4 mx-4 rounded-2xl"
|
isDesktop
|
||||||
|
? "sm:rounded-lg md:rounded-2xl"
|
||||||
|
: "px-4 pb-4 mx-4 rounded-lg md:rounded-2xl"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ExportContent
|
<ExportContent
|
||||||
|
|||||||
@ -21,12 +21,18 @@ export default function BirdseyeLivePlayer({
|
|||||||
let player;
|
let player;
|
||||||
if (liveMode == "webrtc") {
|
if (liveMode == "webrtc") {
|
||||||
player = (
|
player = (
|
||||||
<WebRtcPlayer className={`rounded-2xl size-full`} camera="birdseye" />
|
<WebRtcPlayer
|
||||||
|
className={`rounded-lg md:rounded-2xl size-full`}
|
||||||
|
camera="birdseye"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
} else if (liveMode == "mse") {
|
} else if (liveMode == "mse") {
|
||||||
if ("MediaSource" in window || "ManagedMediaSource" in window) {
|
if ("MediaSource" in window || "ManagedMediaSource" in window) {
|
||||||
player = (
|
player = (
|
||||||
<MSEPlayer className={`rounded-2xl size-full`} camera="birdseye" />
|
<MSEPlayer
|
||||||
|
className={`rounded-lg md:rounded-2xl size-full`}
|
||||||
|
camera="birdseye"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
player = (
|
player = (
|
||||||
@ -39,7 +45,7 @@ export default function BirdseyeLivePlayer({
|
|||||||
} else if (liveMode == "jsmpeg") {
|
} else if (liveMode == "jsmpeg") {
|
||||||
player = (
|
player = (
|
||||||
<JSMpegPlayer
|
<JSMpegPlayer
|
||||||
className="size-full flex justify-center rounded-2xl overflow-hidden"
|
className="size-full flex justify-center rounded-lg md:rounded-2xl overflow-hidden"
|
||||||
camera="birdseye"
|
camera="birdseye"
|
||||||
width={birdseyeConfig.width}
|
width={birdseyeConfig.width}
|
||||||
height={birdseyeConfig.height}
|
height={birdseyeConfig.height}
|
||||||
@ -54,8 +60,8 @@ export default function BirdseyeLivePlayer({
|
|||||||
className={`relative flex justify-center w-full cursor-pointer ${className ?? ""}`}
|
className={`relative flex justify-center w-full cursor-pointer ${className ?? ""}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 inset-x-0 rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none"></div>
|
<div className="absolute top-0 inset-x-0 rounded-lg md:rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none"></div>
|
||||||
<div className="absolute bottom-0 inset-x-0 rounded-2xl z-10 w-full h-[10%] bg-gradient-to-t from-black/20 to-transparent pointer-events-none"></div>
|
<div className="absolute bottom-0 inset-x-0 rounded-lg md:rounded-2xl z-10 w-full h-[10%] bg-gradient-to-t from-black/20 to-transparent pointer-events-none"></div>
|
||||||
<div className="size-full">{player}</div>
|
<div className="size-full">{player}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -164,7 +164,7 @@ export default function HlsVideoPlayer({
|
|||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
className={`size-full bg-black rounded-2xl ${loadedMetadata ? "" : "invisible"}`}
|
className={`size-full bg-black rounded-lg md:rounded-2xl ${loadedMetadata ? "" : "invisible"}`}
|
||||||
preload="auto"
|
preload="auto"
|
||||||
autoPlay
|
autoPlay
|
||||||
controls={false}
|
controls={false}
|
||||||
|
|||||||
@ -95,7 +95,7 @@ export default function LivePlayer({
|
|||||||
if (liveMode == "webrtc") {
|
if (liveMode == "webrtc") {
|
||||||
player = (
|
player = (
|
||||||
<WebRtcPlayer
|
<WebRtcPlayer
|
||||||
className={`rounded-2xl size-full ${liveReady ? "" : "hidden"}`}
|
className={`rounded-lg md:rounded-2xl size-full ${liveReady ? "" : "hidden"}`}
|
||||||
camera={cameraConfig.live.stream_name}
|
camera={cameraConfig.live.stream_name}
|
||||||
playbackEnabled={cameraActive}
|
playbackEnabled={cameraActive}
|
||||||
audioEnabled={playAudio}
|
audioEnabled={playAudio}
|
||||||
@ -109,7 +109,7 @@ export default function LivePlayer({
|
|||||||
if ("MediaSource" in window || "ManagedMediaSource" in window) {
|
if ("MediaSource" in window || "ManagedMediaSource" in window) {
|
||||||
player = (
|
player = (
|
||||||
<MSEPlayer
|
<MSEPlayer
|
||||||
className={`rounded-2xl size-full ${liveReady ? "" : "hidden"}`}
|
className={`rounded-lg md:rounded-2xl size-full ${liveReady ? "" : "hidden"}`}
|
||||||
camera={cameraConfig.name}
|
camera={cameraConfig.name}
|
||||||
playbackEnabled={cameraActive}
|
playbackEnabled={cameraActive}
|
||||||
audioEnabled={playAudio}
|
audioEnabled={playAudio}
|
||||||
@ -128,7 +128,7 @@ export default function LivePlayer({
|
|||||||
} else if (liveMode == "jsmpeg") {
|
} else if (liveMode == "jsmpeg") {
|
||||||
player = (
|
player = (
|
||||||
<JSMpegPlayer
|
<JSMpegPlayer
|
||||||
className="size-full flex justify-center rounded-2xl overflow-hidden"
|
className="size-full flex justify-center rounded-lg md:rounded-2xl overflow-hidden"
|
||||||
camera={cameraConfig.name}
|
camera={cameraConfig.name}
|
||||||
width={cameraConfig.detect.width}
|
width={cameraConfig.detect.width}
|
||||||
height={cameraConfig.detect.height}
|
height={cameraConfig.detect.height}
|
||||||
@ -144,13 +144,13 @@ export default function LivePlayer({
|
|||||||
data-camera={cameraConfig.name}
|
data-camera={cameraConfig.name}
|
||||||
className={`relative flex justify-center ${liveMode == "jsmpeg" ? "size-full" : "w-full"} outline cursor-pointer ${
|
className={`relative flex justify-center ${liveMode == "jsmpeg" ? "size-full" : "w-full"} outline cursor-pointer ${
|
||||||
activeTracking
|
activeTracking
|
||||||
? "outline-severity_alert outline-3 rounded-2xl shadow-severity_alert"
|
? "outline-severity_alert outline-3 rounded-lg md:rounded-2xl shadow-severity_alert"
|
||||||
: "outline-0 outline-background"
|
: "outline-0 outline-background"
|
||||||
} transition-all duration-500 ${className}`}
|
} transition-all duration-500 ${className}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 inset-x-0 rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none"></div>
|
<div className="absolute top-0 inset-x-0 rounded-lg md:rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none"></div>
|
||||||
<div className="absolute bottom-0 inset-x-0 rounded-2xl z-10 w-full h-[10%] bg-gradient-to-t from-black/20 to-transparent pointer-events-none"></div>
|
<div className="absolute bottom-0 inset-x-0 rounded-lg md:rounded-2xl z-10 w-full h-[10%] bg-gradient-to-t from-black/20 to-transparent pointer-events-none"></div>
|
||||||
{player}
|
{player}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -238,7 +238,7 @@ function PreviewVideoPlayer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative rounded-2xl w-full flex justify-center bg-black overflow-hidden ${onClick ? "cursor-pointer" : ""} ${className ?? ""}`}
|
className={`relative rounded-lg md:rounded-2xl w-full flex justify-center bg-black overflow-hidden ${onClick ? "cursor-pointer" : ""} ${className ?? ""}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@ -283,7 +283,7 @@ function PreviewVideoPlayer({
|
|||||||
)}
|
)}
|
||||||
</video>
|
</video>
|
||||||
{cameraPreviews && !currentPreview && (
|
{cameraPreviews && !currentPreview && (
|
||||||
<div className="absolute inset-0 text-white rounded-2xl flex justify-center items-center">
|
<div className="absolute inset-0 text-white rounded-lg md:rounded-2xl flex justify-center items-center">
|
||||||
No Preview Found
|
No Preview Found
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -481,11 +481,11 @@ function PreviewFramesPlayer({
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
className={`size-full object-contain rounded-2xl bg-black`}
|
className={`size-full object-contain rounded-lg md:rounded-2xl bg-black`}
|
||||||
onLoad={onImageLoaded}
|
onLoad={onImageLoaded}
|
||||||
/>
|
/>
|
||||||
{previewFrames?.length === 0 && (
|
{previewFrames?.length === 0 && (
|
||||||
<div className="absolute inset-x-0 top-1/2 -y-translate-1/2 bg-black text-white rounded-2xl align-center text-center">
|
<div className="absolute inset-x-0 top-1/2 -y-translate-1/2 bg-black text-white rounded-lg md:rounded-2xl align-center text-center">
|
||||||
No Preview Found
|
No Preview Found
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { PolygonCanvas } from "./PolygonCanvas";
|
import { PolygonCanvas } from "./PolygonCanvas";
|
||||||
import { Polygon, PolygonType } from "@/types/canvas";
|
import { Polygon, PolygonType } from "@/types/canvas";
|
||||||
import { interpolatePoints, parseCoordinates } from "@/utils/canvasUtil";
|
import { interpolatePoints, parseCoordinates } from "@/utils/canvasUtil";
|
||||||
@ -25,6 +32,7 @@ import ObjectMaskEditPane from "./ObjectMaskEditPane";
|
|||||||
import PolygonItem from "./PolygonItem";
|
import PolygonItem from "./PolygonItem";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop } from "react-device-detect";
|
||||||
|
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
||||||
|
|
||||||
type MasksAndZoneProps = {
|
type MasksAndZoneProps = {
|
||||||
selectedCamera: string;
|
selectedCamera: string;
|
||||||
@ -50,6 +58,8 @@ export default function MasksAndZones({
|
|||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [editPane, setEditPane] = useState<PolygonType | undefined>(undefined);
|
const [editPane, setEditPane] = useState<PolygonType | undefined>(undefined);
|
||||||
|
|
||||||
|
const { addMessage } = useContext(StatusBarMessagesContext)!;
|
||||||
|
|
||||||
const cameraConfig = useMemo(() => {
|
const cameraConfig = useMemo(() => {
|
||||||
if (config && selectedCamera) {
|
if (config && selectedCamera) {
|
||||||
return config.cameras[selectedCamera];
|
return config.cameras[selectedCamera];
|
||||||
@ -167,7 +177,8 @@ export default function MasksAndZones({
|
|||||||
setAllPolygons([...(editingPolygons ?? [])]);
|
setAllPolygons([...(editingPolygons ?? [])]);
|
||||||
setHoveredPolygonIndex(null);
|
setHoveredPolygonIndex(null);
|
||||||
setUnsavedChanges(false);
|
setUnsavedChanges(false);
|
||||||
}, [editingPolygons, setUnsavedChanges]);
|
addMessage("masks_zones", "Restart required (masks/zones changed)");
|
||||||
|
}, [editingPolygons, setUnsavedChanges, addMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import useSWR from "swr";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage";
|
import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
@ -20,6 +20,7 @@ import { toast } from "sonner";
|
|||||||
import { Separator } from "../ui/separator";
|
import { Separator } from "../ui/separator";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { LuExternalLink } from "react-icons/lu";
|
import { LuExternalLink } from "react-icons/lu";
|
||||||
|
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
||||||
|
|
||||||
type MotionTunerProps = {
|
type MotionTunerProps = {
|
||||||
selectedCamera: string;
|
selectedCamera: string;
|
||||||
@ -41,6 +42,8 @@ export default function MotionTuner({
|
|||||||
const [changedValue, setChangedValue] = useState(false);
|
const [changedValue, setChangedValue] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const { addMessage, clearMessages } = useContext(StatusBarMessagesContext)!;
|
||||||
|
|
||||||
const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera);
|
const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera);
|
||||||
const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera);
|
const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera);
|
||||||
const { send: sendImproveContrast } = useImproveContrast(selectedCamera);
|
const { send: sendImproveContrast } = useImproveContrast(selectedCamera);
|
||||||
@ -145,7 +148,16 @@ export default function MotionTuner({
|
|||||||
const onCancel = useCallback(() => {
|
const onCancel = useCallback(() => {
|
||||||
setMotionSettings(origMotionSettings);
|
setMotionSettings(origMotionSettings);
|
||||||
setChangedValue(false);
|
setChangedValue(false);
|
||||||
}, [origMotionSettings]);
|
clearMessages("motion_tuner");
|
||||||
|
}, [origMotionSettings, clearMessages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (changedValue) {
|
||||||
|
addMessage("motion_tuner", "Unsaved motion tuner changes");
|
||||||
|
} else {
|
||||||
|
clearMessages("motion_tuner");
|
||||||
|
}
|
||||||
|
}, [changedValue, addMessage, clearMessages]);
|
||||||
|
|
||||||
if (!cameraConfig && !selectedCamera) {
|
if (!cameraConfig && !selectedCamera) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
@ -296,7 +308,7 @@ export default function MotionTuner({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="size-full rounded-2xl" />
|
<Skeleton className="size-full rounded-lg md:rounded-2xl" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -247,7 +247,7 @@ export function EventSegment({
|
|||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardPortal>
|
<HoverCardPortal>
|
||||||
<HoverCardContent
|
<HoverCardContent
|
||||||
className="rounded-2xl w-[250px] p-2"
|
className="rounded-lg md:rounded-2xl w-[250px] p-2"
|
||||||
side="left"
|
side="left"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { RecoilRoot } from "recoil";
|
|||||||
import { ApiProvider } from "@/api";
|
import { ApiProvider } from "@/api";
|
||||||
import { IconContext } from "react-icons";
|
import { IconContext } from "react-icons";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
import { StatusBarMessagesProvider } from "@/context/statusbar-provider";
|
||||||
|
|
||||||
type TProvidersProps = {
|
type TProvidersProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -16,7 +17,7 @@ function providers({ children }: TProvidersProps) {
|
|||||||
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
|
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<IconContext.Provider value={{ size: "20" }}>
|
<IconContext.Provider value={{ size: "20" }}>
|
||||||
{children}
|
<StatusBarMessagesProvider>{children}</StatusBarMessagesProvider>
|
||||||
</IconContext.Provider>
|
</IconContext.Provider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
83
web/src/context/statusbar-provider.tsx
Normal file
83
web/src/context/statusbar-provider.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useState,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
export type StatusMessage = {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StatusMessagesState = {
|
||||||
|
[key: string]: StatusMessage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type StatusBarMessagesProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StatusBarMessagesContextValue = {
|
||||||
|
messages: StatusMessagesState;
|
||||||
|
addMessage: (
|
||||||
|
key: string,
|
||||||
|
message: string,
|
||||||
|
color?: string,
|
||||||
|
messageId?: string,
|
||||||
|
) => string;
|
||||||
|
removeMessage: (key: string, messageId: string) => void;
|
||||||
|
clearMessages: (key: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StatusBarMessagesContext =
|
||||||
|
createContext<StatusBarMessagesContextValue | null>(null);
|
||||||
|
|
||||||
|
export function StatusBarMessagesProvider({
|
||||||
|
children,
|
||||||
|
}: StatusBarMessagesProviderProps) {
|
||||||
|
const [messagesState, setMessagesState] = useState<StatusMessagesState>({});
|
||||||
|
|
||||||
|
const messages = useMemo(() => messagesState, [messagesState]);
|
||||||
|
|
||||||
|
const addMessage = useCallback(
|
||||||
|
(key: string, message: string, color?: string, messageId?: string) => {
|
||||||
|
const id = messageId || Date.now().toString();
|
||||||
|
const msgColor = color || "text-danger";
|
||||||
|
setMessagesState((prevMessages) => ({
|
||||||
|
...prevMessages,
|
||||||
|
[key]: [
|
||||||
|
...(prevMessages[key] || []),
|
||||||
|
{ id, text: message, color: msgColor },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeMessage = useCallback((key: string, messageId: string) => {
|
||||||
|
setMessagesState((prevMessages) => ({
|
||||||
|
...prevMessages,
|
||||||
|
[key]: prevMessages[key].filter((msg) => msg.id !== messageId),
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearMessages = useCallback((key: string) => {
|
||||||
|
setMessagesState((prevMessages) => {
|
||||||
|
const updatedMessages = { ...prevMessages };
|
||||||
|
delete updatedMessages[key];
|
||||||
|
return updatedMessages;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusBarMessagesContext.Provider
|
||||||
|
value={{ messages, addMessage, removeMessage, clearMessages }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StatusBarMessagesContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
web/src/hooks/use-deep-memo.ts
Normal file
12
web/src/hooks/use-deep-memo.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { isEqual } from "lodash";
|
||||||
|
|
||||||
|
export default function useDeepMemo<T>(value: T) {
|
||||||
|
const ref = useRef<T | undefined>(undefined);
|
||||||
|
|
||||||
|
if (!isEqual(ref.current, value)) {
|
||||||
|
ref.current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
@ -7,78 +7,82 @@ import {
|
|||||||
import { FrigateStats, PotentialProblem } from "@/types/stats";
|
import { FrigateStats, PotentialProblem } from "@/types/stats";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import useDeepMemo from "./use-deep-memo";
|
||||||
|
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||||
|
|
||||||
export default function useStats(stats: FrigateStats | undefined) {
|
export default function useStats(stats: FrigateStats | undefined) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
|
const memoizedStats = useDeepMemo(stats);
|
||||||
|
|
||||||
const potentialProblems = useMemo<PotentialProblem[]>(() => {
|
const potentialProblems = useMemo<PotentialProblem[]>(() => {
|
||||||
const problems: PotentialProblem[] = [];
|
const problems: PotentialProblem[] = [];
|
||||||
|
|
||||||
if (!stats) {
|
if (!memoizedStats) {
|
||||||
return problems;
|
return problems;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if frigate has just started
|
// if frigate has just started
|
||||||
// don't look for issues
|
// don't look for issues
|
||||||
if (stats.service.uptime < 120) {
|
if (memoizedStats.service.uptime < 120) {
|
||||||
return problems;
|
return problems;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check detectors for high inference speeds
|
// check detectors for high inference speeds
|
||||||
Object.entries(stats["detectors"]).forEach(([key, det]) => {
|
Object.entries(memoizedStats["detectors"]).forEach(([key, det]) => {
|
||||||
if (det["inference_speed"] > InferenceThreshold.error) {
|
if (det["inference_speed"] > InferenceThreshold.error) {
|
||||||
problems.push({
|
problems.push({
|
||||||
text: `${key} is very slow (${det["inference_speed"]} ms)`,
|
text: `${capitalizeFirstLetter(key)} is very slow (${det["inference_speed"]} ms)`,
|
||||||
color: "text-danger",
|
color: "text-danger",
|
||||||
});
|
});
|
||||||
} else if (det["inference_speed"] > InferenceThreshold.warning) {
|
} else if (det["inference_speed"] > InferenceThreshold.warning) {
|
||||||
problems.push({
|
problems.push({
|
||||||
text: `${key} is slow (${det["inference_speed"]} ms)`,
|
text: `${capitalizeFirstLetter(key)} is slow (${det["inference_speed"]} ms)`,
|
||||||
color: "text-orange-400",
|
color: "text-orange-400",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// check for offline cameras
|
// check for offline cameras
|
||||||
Object.entries(stats["cameras"]).forEach(([name, cam]) => {
|
Object.entries(memoizedStats["cameras"]).forEach(([name, cam]) => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.cameras[name].enabled && cam["camera_fps"] == 0) {
|
if (config.cameras[name].enabled && cam["camera_fps"] == 0) {
|
||||||
problems.push({
|
problems.push({
|
||||||
text: `${name.replaceAll("_", " ")} is offline`,
|
text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} is offline`,
|
||||||
color: "text-danger",
|
color: "text-danger",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// check camera cpu usages
|
// check camera cpu usages
|
||||||
Object.entries(stats["cameras"]).forEach(([name, cam]) => {
|
Object.entries(memoizedStats["cameras"]).forEach(([name, cam]) => {
|
||||||
const ffmpegAvg = parseFloat(
|
const ffmpegAvg = parseFloat(
|
||||||
stats["cpu_usages"][cam["ffmpeg_pid"]]?.cpu_average,
|
memoizedStats["cpu_usages"][cam["ffmpeg_pid"]]?.cpu_average,
|
||||||
);
|
);
|
||||||
const detectAvg = parseFloat(
|
const detectAvg = parseFloat(
|
||||||
stats["cpu_usages"][cam["pid"]]?.cpu_average,
|
memoizedStats["cpu_usages"][cam["pid"]]?.cpu_average,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isNaN(ffmpegAvg) && ffmpegAvg >= CameraFfmpegThreshold.error) {
|
if (!isNaN(ffmpegAvg) && ffmpegAvg >= CameraFfmpegThreshold.error) {
|
||||||
problems.push({
|
problems.push({
|
||||||
text: `${name.replaceAll("_", " ")} has high FFMPEG CPU usage (${ffmpegAvg}%)`,
|
text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`,
|
||||||
color: "text-danger",
|
color: "text-danger",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNaN(detectAvg) && detectAvg >= CameraDetectThreshold.error) {
|
if (!isNaN(detectAvg) && detectAvg >= CameraDetectThreshold.error) {
|
||||||
problems.push({
|
problems.push({
|
||||||
text: `${name.replaceAll("_", " ")} has high detect CPU usage (${detectAvg}%)`,
|
text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`,
|
||||||
color: "text-danger",
|
color: "text-danger",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return problems;
|
return problems;
|
||||||
}, [config, stats]);
|
}, [config, memoizedStats]);
|
||||||
|
|
||||||
return { potentialProblems };
|
return { potentialProblems };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,7 +113,7 @@ function Exports() {
|
|||||||
<DialogContent className="max-w-7xl">
|
<DialogContent className="max-w-7xl">
|
||||||
<DialogTitle>{selected?.name}</DialogTitle>
|
<DialogTitle>{selected?.name}</DialogTitle>
|
||||||
<video
|
<video
|
||||||
className="size-full rounded-2xl"
|
className="size-full rounded-lg md:rounded-2xl"
|
||||||
playsInline
|
playsInline
|
||||||
preload="auto"
|
preload="auto"
|
||||||
autoPlay
|
autoPlay
|
||||||
|
|||||||
@ -182,11 +182,11 @@ export default function SubmitPlus() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={event.id}
|
key={event.id}
|
||||||
className="w-full rounded-2xl aspect-video flex justify-center items-center bg-black cursor-pointer"
|
className="w-full rounded-lg md:rounded-2xl aspect-video flex justify-center items-center bg-black cursor-pointer"
|
||||||
onClick={() => setUpload(event)}
|
onClick={() => setUpload(event)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="aspect-video h-full object-contain rounded-2xl"
|
className="aspect-video h-full object-contain rounded-lg md:rounded-2xl"
|
||||||
src={`${baseUrl}api/events/${event.id}/snapshot.jpg`}
|
src={`${baseUrl}api/events/${event.id}/snapshot.jpg`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -31,7 +31,8 @@ type FrigateObjectState = {
|
|||||||
|
|
||||||
export interface FrigateReview {
|
export interface FrigateReview {
|
||||||
type: "new" | "update" | "end";
|
type: "new" | "update" | "end";
|
||||||
review: ReviewSegment;
|
before: ReviewSegment;
|
||||||
|
after: ReviewSegment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FrigateEvent {
|
export interface FrigateEvent {
|
||||||
|
|||||||
3
web/src/utils/stringUtil.ts
Normal file
3
web/src/utils/stringUtil.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const capitalizeFirstLetter = (text: string): string => {
|
||||||
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||||
|
};
|
||||||
@ -249,7 +249,7 @@ export default function EventView({
|
|||||||
</div>
|
</div>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className={`px-3 py-4 rounded-2xl ${
|
className={`px-3 py-4 rounded-lg ${
|
||||||
severityToggle == "significant_motion"
|
severityToggle == "significant_motion"
|
||||||
? ""
|
? ""
|
||||||
: "text-muted-foreground"
|
: "text-muted-foreground"
|
||||||
@ -589,7 +589,8 @@ function DetectionReview({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: Array(itemsToReview)
|
: (itemsToReview ?? 0) > 0 &&
|
||||||
|
Array(itemsToReview)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, idx) => (
|
.map((_, idx) => (
|
||||||
<Skeleton key={idx} className="size-full aspect-video" />
|
<Skeleton key={idx} className="size-full aspect-video" />
|
||||||
@ -890,7 +891,7 @@ function MotionReview({
|
|||||||
{motionData ? (
|
{motionData ? (
|
||||||
<>
|
<>
|
||||||
<PreviewPlayer
|
<PreviewPlayer
|
||||||
className={`rounded-2xl ${spans} ${grow}`}
|
className={`rounded-lg md:rounded-2xl ${spans} ${grow}`}
|
||||||
camera={camera.name}
|
camera={camera.name}
|
||||||
timeRange={currentTimeRange}
|
timeRange={currentTimeRange}
|
||||||
startTime={previewStart}
|
startTime={previewStart}
|
||||||
@ -916,7 +917,7 @@ function MotionReview({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
className={`rounded-2xl size-full ${spans} ${grow}`}
|
className={`rounded-lg md:rounded-2xl size-full ${spans} ${grow}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -47,8 +47,19 @@ export default function LiveDashboardView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if event is ended and was saved, update events list
|
// if event is ended and was saved, update events list
|
||||||
if (eventUpdate.review.severity == "alert") {
|
if (eventUpdate.after.severity == "alert") {
|
||||||
setTimeout(() => updateEvents(), eventUpdate.type == "end" ? 1000 : 6000);
|
if (eventUpdate.type == "end" || eventUpdate.type == "new") {
|
||||||
|
setTimeout(
|
||||||
|
() => updateEvents(),
|
||||||
|
eventUpdate.type == "end" ? 1000 : 6000,
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
eventUpdate.before.data.objects.length <
|
||||||
|
eventUpdate.after.data.objects.length
|
||||||
|
) {
|
||||||
|
setTimeout(() => updateEvents(), 5000);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, [eventUpdate, updateEvents]);
|
}, [eventUpdate, updateEvents]);
|
||||||
@ -175,7 +186,7 @@ export default function LiveDashboardView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`mt-2 px-2 grid ${layout == "grid" ? "grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4" : ""} gap-2 md:gap-4 *:rounded-2xl *:bg-black`}
|
className={`mt-2 px-2 grid ${layout == "grid" ? "grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4" : ""} gap-2 md:gap-4`}
|
||||||
>
|
>
|
||||||
{includeBirdseye && birdseyeConfig?.enabled && (
|
{includeBirdseye && birdseyeConfig?.enabled && (
|
||||||
<BirdseyeLivePlayer
|
<BirdseyeLivePlayer
|
||||||
@ -198,7 +209,7 @@ export default function LiveDashboardView({
|
|||||||
<LivePlayer
|
<LivePlayer
|
||||||
cameraRef={cameraRef}
|
cameraRef={cameraRef}
|
||||||
key={camera.name}
|
key={camera.name}
|
||||||
className={grow}
|
className={`${grow} rounded-lg md:rounded-2xl bg-black`}
|
||||||
windowVisible={
|
windowVisible={
|
||||||
windowVisible && visibleCameras.includes(camera.name)
|
windowVisible && visibleCameras.includes(camera.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export default function CameraMetrics({
|
|||||||
[key: string]: { name: string; data: { x: number; y: number }[] };
|
[key: string]: { name: string; data: { x: number; y: number }[] };
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
series["overall_fps"] = { name: "overall frames per second", data: [] };
|
||||||
series["overall_dps"] = { name: "overall detections per second", data: [] };
|
series["overall_dps"] = { name: "overall detections per second", data: [] };
|
||||||
series["overall_skipped_dps"] = {
|
series["overall_skipped_dps"] = {
|
||||||
name: "overall skipped detections per second",
|
name: "overall skipped detections per second",
|
||||||
@ -77,6 +78,16 @@ export default function CameraMetrics({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let frames = 0;
|
||||||
|
Object.values(stats.cameras).forEach(
|
||||||
|
(camStat) => (frames += camStat.camera_fps),
|
||||||
|
);
|
||||||
|
|
||||||
|
series["overall_fps"].data.push({
|
||||||
|
x: statsIdx,
|
||||||
|
y: Math.round(frames),
|
||||||
|
});
|
||||||
|
|
||||||
series["overall_dps"].data.push({
|
series["overall_dps"].data.push({
|
||||||
x: statsIdx,
|
x: statsIdx,
|
||||||
y: stats.detection_fps,
|
y: stats.detection_fps,
|
||||||
@ -161,6 +172,10 @@ export default function CameraMetrics({
|
|||||||
if (!(key in series)) {
|
if (!(key in series)) {
|
||||||
const camName = key.replaceAll("_", " ");
|
const camName = key.replaceAll("_", " ");
|
||||||
series[key] = {};
|
series[key] = {};
|
||||||
|
series[key]["fps"] = {
|
||||||
|
name: `${camName} frames per second`,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
series[key]["det"] = {
|
series[key]["det"] = {
|
||||||
name: `${camName} detections per second`,
|
name: `${camName} detections per second`,
|
||||||
data: [],
|
data: [],
|
||||||
@ -171,6 +186,10 @@ export default function CameraMetrics({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
series[key]["fps"].data.push({
|
||||||
|
x: statsIdx,
|
||||||
|
y: camStats.camera_fps,
|
||||||
|
});
|
||||||
series[key]["det"].data.push({
|
series[key]["det"].data.push({
|
||||||
x: statsIdx,
|
x: statsIdx,
|
||||||
y: camStats.detection_fps,
|
y: camStats.detection_fps,
|
||||||
@ -189,18 +208,18 @@ export default function CameraMetrics({
|
|||||||
<div className="text-muted-foreground text-sm font-medium">Overview</div>
|
<div className="text-muted-foreground text-sm font-medium">Overview</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3">
|
<div className="grid grid-cols-1 md:grid-cols-3">
|
||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl">
|
||||||
<div className="mb-5">DPS</div>
|
<div className="mb-5">Frames / Detections</div>
|
||||||
<CameraLineGraph
|
<CameraLineGraph
|
||||||
graphId="overall-stats"
|
graphId="overall-stats"
|
||||||
unit=" DPS"
|
unit=""
|
||||||
dataLabels={["detect", "skipped"]}
|
dataLabels={["camera", "detect", "skipped"]}
|
||||||
updateTimes={updateTimes}
|
updateTimes={updateTimes}
|
||||||
data={overallFpsSeries}
|
data={overallFpsSeries}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="w-full h-32" />
|
<Skeleton className="w-full rounded-lg md:rounded-2xl h-32" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
@ -214,7 +233,7 @@ export default function CameraMetrics({
|
|||||||
</div>
|
</div>
|
||||||
<div key={camera.name} className="grid sm:grid-cols-2 gap-2">
|
<div key={camera.name} className="grid sm:grid-cols-2 gap-2">
|
||||||
{Object.keys(cameraCpuSeries).includes(camera.name) ? (
|
{Object.keys(cameraCpuSeries).includes(camera.name) ? (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl">
|
||||||
<div className="mb-5">CPU</div>
|
<div className="mb-5">CPU</div>
|
||||||
<CameraLineGraph
|
<CameraLineGraph
|
||||||
graphId={`${camera.name}-cpu`}
|
graphId={`${camera.name}-cpu`}
|
||||||
@ -230,12 +249,12 @@ export default function CameraMetrics({
|
|||||||
<Skeleton className="size-full aspect-video" />
|
<Skeleton className="size-full aspect-video" />
|
||||||
)}
|
)}
|
||||||
{Object.keys(cameraFpsSeries).includes(camera.name) ? (
|
{Object.keys(cameraFpsSeries).includes(camera.name) ? (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl">
|
||||||
<div className="mb-5">DPS</div>
|
<div className="mb-5">Frames / Detections</div>
|
||||||
<CameraLineGraph
|
<CameraLineGraph
|
||||||
graphId={`${camera.name}-dps`}
|
graphId={`${camera.name}-dps`}
|
||||||
unit=" DPS"
|
unit=""
|
||||||
dataLabels={["detect", "skipped"]}
|
dataLabels={["camera", "detect", "skipped"]}
|
||||||
updateTimes={updateTimes}
|
updateTimes={updateTimes}
|
||||||
data={Object.values(
|
data={Object.values(
|
||||||
cameraFpsSeries[camera.name] || {},
|
cameraFpsSeries[camera.name] || {},
|
||||||
|
|||||||
@ -334,7 +334,7 @@ export default function GeneralMetrics({
|
|||||||
className={`w-full mt-4 grid grid-cols-1 gap-2 ${detTempSeries == undefined ? "sm:grid-cols-3" : "sm:grid-cols-4"}`}
|
className={`w-full mt-4 grid grid-cols-1 gap-2 ${detTempSeries == undefined ? "sm:grid-cols-3" : "sm:grid-cols-4"}`}
|
||||||
>
|
>
|
||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl">
|
||||||
<div className="mb-5">Detector Inference Speed</div>
|
<div className="mb-5">Detector Inference Speed</div>
|
||||||
{detInferenceTimeSeries.map((series) => (
|
{detInferenceTimeSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -349,12 +349,12 @@ export default function GeneralMetrics({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton className="w-full aspect-video" />
|
<Skeleton className="w-full rounded-lg md:rounded-2xl aspect-video" />
|
||||||
)}
|
)}
|
||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<>
|
<>
|
||||||
{detTempSeries && (
|
{detTempSeries && (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl">
|
||||||
<div className="mb-5">Detector Temperature</div>
|
<div className="mb-5">Detector Temperature</div>
|
||||||
{detTempSeries.map((series) => (
|
{detTempSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -374,7 +374,7 @@ export default function GeneralMetrics({
|
|||||||
<Skeleton className="w-full aspect-video" />
|
<Skeleton className="w-full aspect-video" />
|
||||||
)}
|
)}
|
||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl">
|
||||||
<div className="mb-5">Detector CPU Usage</div>
|
<div className="mb-5">Detector CPU Usage</div>
|
||||||
{detCpuSeries.map((series) => (
|
{detCpuSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -392,7 +392,7 @@ export default function GeneralMetrics({
|
|||||||
<Skeleton className="w-full aspect-video" />
|
<Skeleton className="w-full aspect-video" />
|
||||||
)}
|
)}
|
||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl">
|
||||||
<div className="mb-5">Detector Memory Usage</div>
|
<div className="mb-5">Detector Memory Usage</div>
|
||||||
{detMemSeries.map((series) => (
|
{detMemSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -429,7 +429,7 @@ export default function GeneralMetrics({
|
|||||||
</div>
|
</div>
|
||||||
<div className=" mt-4 grid grid-cols-1 sm:grid-cols-2 gap-2">
|
<div className=" mt-4 grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl">
|
||||||
<div className="mb-5">GPU Usage</div>
|
<div className="mb-5">GPU Usage</div>
|
||||||
{gpuSeries.map((series) => (
|
{gpuSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -449,7 +449,7 @@ export default function GeneralMetrics({
|
|||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<>
|
<>
|
||||||
{gpuMemSeries && (
|
{gpuMemSeries && (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl">
|
||||||
<div className="mb-5">GPU Memory</div>
|
<div className="mb-5">GPU Memory</div>
|
||||||
{gpuMemSeries.map((series) => (
|
{gpuMemSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -477,7 +477,7 @@ export default function GeneralMetrics({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-2">
|
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl">
|
||||||
<div className="mb-5">Process CPU Usage</div>
|
<div className="mb-5">Process CPU Usage</div>
|
||||||
{otherProcessCpuSeries.map((series) => (
|
{otherProcessCpuSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -495,7 +495,7 @@ export default function GeneralMetrics({
|
|||||||
<Skeleton className="w-full aspect-tall" />
|
<Skeleton className="w-full aspect-tall" />
|
||||||
)}
|
)}
|
||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl">
|
||||||
<div className="mb-5">Process Memory Usage</div>
|
<div className="mb-5">Process Memory Usage</div>
|
||||||
{otherProcessMemSeries.map((series) => (
|
{otherProcessMemSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export default function StorageMetrics({
|
|||||||
<div className="size-full mt-4 flex flex-col overflow-y-auto">
|
<div className="size-full mt-4 flex flex-col overflow-y-auto">
|
||||||
<div className="text-muted-foreground text-sm font-medium">Overview</div>
|
<div className="text-muted-foreground text-sm font-medium">Overview</div>
|
||||||
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl flex-col">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl flex-col">
|
||||||
<div className="mb-5">Recordings</div>
|
<div className="mb-5">Recordings</div>
|
||||||
<StorageGraph
|
<StorageGraph
|
||||||
graphId="general-recordings"
|
graphId="general-recordings"
|
||||||
@ -53,7 +53,7 @@ export default function StorageMetrics({
|
|||||||
total={totalStorage.total}
|
total={totalStorage.total}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl flex-col">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl flex-col">
|
||||||
<div className="mb-5">/tmp/cache</div>
|
<div className="mb-5">/tmp/cache</div>
|
||||||
<StorageGraph
|
<StorageGraph
|
||||||
graphId="general-cache"
|
graphId="general-cache"
|
||||||
@ -61,7 +61,7 @@ export default function StorageMetrics({
|
|||||||
total={stats.service.storage["/tmp/cache"]["total"]}
|
total={stats.service.storage["/tmp/cache"]["total"]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl flex-col">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl flex-col">
|
||||||
<div className="mb-5">/dev/shm</div>
|
<div className="mb-5">/dev/shm</div>
|
||||||
<StorageGraph
|
<StorageGraph
|
||||||
graphId="general-shared-memory"
|
graphId="general-shared-memory"
|
||||||
@ -75,7 +75,7 @@ export default function StorageMetrics({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
{Object.keys(cameraStorage).map((camera) => (
|
{Object.keys(cameraStorage).map((camera) => (
|
||||||
<div className="p-2.5 bg-background_alt rounded-2xl flex-col">
|
<div className="p-2.5 bg-background_alt rounded-lg md:rounded-2xl flex-col">
|
||||||
<div className="mb-5 capitalize">{camera.replaceAll("_", " ")}</div>
|
<div className="mb-5 capitalize">{camera.replaceAll("_", " ")}</div>
|
||||||
<StorageGraph
|
<StorageGraph
|
||||||
graphId={`${camera}-storage`}
|
graphId={`${camera}-storage`}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user