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