Tweaks and fixes (#11541)

* Update config version to be stored inside of the config

* Don't remove items from list when navigating back

* Use video api instead of webps for live current hour filmstrip

* Check that the config file is writable

* Show camera name when camera is offline

* Show camera name when offline

* Cleanup
This commit is contained in:
Nicolas Mowen 2024-05-26 15:49:12 -06:00 committed by GitHub
parent 63d81bef45
commit c2eac10925
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 151 additions and 117 deletions

View File

@ -1355,6 +1355,7 @@ class FrigateConfig(FrigateBaseModel):
default_factory=TimestampStyleConfig,
title="Global timestamp style configuration.",
)
version: Optional[float] = Field(default=None, title="Current config version.")
def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig:
"""Merge camera config with globals."""

View File

@ -17,16 +17,17 @@ CURRENT_CONFIG_VERSION = 0.14
def migrate_frigate_config(config_file: str):
"""handle migrating the frigate config."""
logger.info("Checking if frigate config needs migration...")
version_file = os.path.join(CONFIG_DIR, ".version")
if not os.path.isfile(version_file):
previous_version = 0.13
else:
with open(version_file) as f:
try:
previous_version = float(f.readline())
except Exception:
previous_version = 0.13
if not os.access(config_file, mode=os.W_OK):
logger.error("Config file is read-only, unable to migrate config file.")
return
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
with open(config_file, "r") as f:
config: dict[str, dict[str, any]] = yaml.load(f)
previous_version = config.get("version", 0.13)
if previous_version == CURRENT_CONFIG_VERSION:
logger.info("frigate config does not need migration...")
@ -35,11 +36,6 @@ def migrate_frigate_config(config_file: str):
logger.info("copying config as backup...")
shutil.copy(config_file, os.path.join(CONFIG_DIR, "backup_config.yaml"))
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
with open(config_file, "r") as f:
config: dict[str, dict[str, any]] = yaml.load(f)
if previous_version < 0.14:
logger.info(f"Migrating frigate config from {previous_version} to 0.14...")
new_config = migrate_014(config)
@ -57,9 +53,6 @@ def migrate_frigate_config(config_file: str):
os.path.join(EXPORT_DIR, file), os.path.join(EXPORT_DIR, new_name)
)
with open(version_file, "w") as f:
f.write(str(CURRENT_CONFIG_VERSION))
logger.info("Finished frigate config migration...")
@ -141,6 +134,7 @@ def migrate_014(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
new_config["cameras"][name] = camera_config
new_config["version"] = 0.14
return new_config

View File

@ -1,33 +1,20 @@
import { useFrigateStats } from "@/api/ws";
import {
StatusBarMessagesContext,
StatusMessage,
} from "@/context/statusbar-provider";
import useStats from "@/hooks/use-stats";
import { FrigateStats } from "@/types/stats";
import useStats, { useAutoFrigateStats } from "@/hooks/use-stats";
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 { Link } from "react-router-dom";
import useSWR from "swr";
export default function Statusbar() {
const { data: initialStats } = useSWR<FrigateStats>("stats", {
revalidateOnFocus: false,
});
const { payload: latestStats } = useFrigateStats();
const { messages, addMessage, clearMessages } = useContext(
StatusBarMessagesContext,
)!;
const stats = useMemo(() => {
if (latestStats) {
return latestStats;
}
return initialStats;
}, [initialStats, latestStats]);
const stats = useAutoFrigateStats();
const cpuPercent = useMemo(() => {
const systemCpu = stats?.cpu_usages["frigate.full_system"]?.cpu;

View File

@ -7,12 +7,10 @@ import { REVIEW_PADDING, ReviewSegment } from "@/types/review";
import { useNavigate } from "react-router-dom";
import { RecordingStartingPoint } from "@/types/record";
import axios from "axios";
import {
InProgressPreview,
VideoPreview,
} from "../player/PreviewThumbnailPlayer";
import { VideoPreview } from "../player/PreviewThumbnailPlayer";
import { isCurrentHour } from "@/utils/dateUtil";
import { useCameraPreviews } from "@/hooks/use-camera-previews";
import { baseUrl } from "@/api/baseUrl";
type AnimatedEventCardProps = {
event: ReviewSegment;
@ -105,18 +103,11 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
windowVisible={windowVisible}
/>
) : (
<InProgressPreview
review={event}
timeRange={{
after: event.start_time,
before: event.end_time ?? event.start_time + 20,
}}
<video
src={`${baseUrl}api/review/${event.id}/preview?format=ts`}
muted
autoPlay
loop
showProgress={false}
setReviewed={() => {}}
setIgnoreClick={() => {}}
isPlayingBack={() => {}}
windowVisible={windowVisible}
/>
)}
</div>

View File

@ -46,7 +46,7 @@ export default function LivePlayer({
}: LivePlayerProps) {
// camera activity
const { activeMotion, activeTracking, objects } =
const { activeMotion, activeTracking, objects, offline } =
useCameraActivity(cameraConfig);
const cameraActive = useMemo(
@ -224,9 +224,16 @@ export default function LivePlayer({
/>
</div>
<div className="absolute right-2 top-2 size-4">
{activeMotion && (
<MdCircle className="size-2 animate-pulse text-danger shadow-danger drop-shadow-md" />
<div className="absolute right-2 top-2">
{!offline && activeMotion && (
<MdCircle className="mr-2 size-2 animate-pulse text-danger shadow-danger drop-shadow-md" />
)}
{offline && (
<Chip
className={`z-0 flex items-start justify-between space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`}
>
{cameraConfig.name.replaceAll("_", " ")}
</Chip>
)}
</div>
</div>

View File

@ -10,11 +10,13 @@ import { useTimelineUtils } from "./use-timeline-utils";
import { ObjectType } from "@/types/ws";
import useDeepMemo from "./use-deep-memo";
import { isEqual } from "lodash";
import { useAutoFrigateStats } from "./use-stats";
type useCameraActivityReturn = {
activeTracking: boolean;
activeMotion: boolean;
objects: ObjectType[];
offline: boolean;
};
export function useCameraActivity(
@ -116,12 +118,31 @@ export function useCameraActivity(
handleSetObjects(newObjects);
}, [camera, updatedEvent, objects, handleSetObjects]);
// determine if camera is offline
const stats = useAutoFrigateStats();
const offline = useMemo(() => {
if (!stats) {
return false;
}
const cameras = stats["cameras"];
if (!cameras) {
return false;
}
return cameras[camera.name].camera_fps == 0;
}, [camera, stats]);
return {
activeTracking: hasActiveObjects,
activeMotion: detectingMotion
? detectingMotion === "ON"
: initialCameraState?.motion === true,
objects,
offline,
};
}

View File

@ -9,6 +9,7 @@ import { useMemo } from "react";
import useSWR from "swr";
import useDeepMemo from "./use-deep-memo";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { useFrigateStats } from "@/api/ws";
export default function useStats(stats: FrigateStats | undefined) {
const { data: config } = useSWR<FrigateConfig>("config");
@ -91,3 +92,20 @@ export default function useStats(stats: FrigateStats | undefined) {
return { potentialProblems };
}
export function useAutoFrigateStats() {
const { data: initialStats } = useSWR<FrigateStats>("stats", {
revalidateOnFocus: false,
});
const { payload: latestStats } = useFrigateStats();
const stats = useMemo(() => {
if (latestStats) {
return latestStats;
}
return initialStats;
}, [initialStats, latestStats]);
return stats;
}

View File

@ -10,6 +10,7 @@ import {
ReviewSegment,
ReviewSeverity,
ReviewSummary,
SegmentedReviewData,
} from "@/types/review";
import { getTimestampOffset } from "@/utils/dateUtil";
import EventView from "@/views/events/EventView";
@ -138,6 +139,66 @@ export default function Events() {
},
);
const reviewItems = useMemo<SegmentedReviewData>(() => {
if (!reviews) {
return undefined;
}
const all: ReviewSegment[] = [];
const alerts: ReviewSegment[] = [];
const detections: ReviewSegment[] = [];
const motion: ReviewSegment[] = [];
reviews?.forEach((segment) => {
all.push(segment);
switch (segment.severity) {
case "alert":
alerts.push(segment);
break;
case "detection":
detections.push(segment);
break;
default:
motion.push(segment);
break;
}
});
return {
all: all,
alert: alerts,
detection: detections,
significant_motion: motion,
};
}, [reviews]);
const currentItems = useMemo(() => {
if (!reviewItems || !severity) {
return null;
}
let current;
if (reviewFilter?.showAll) {
current = reviewItems.all;
} else {
current = reviewItems[severity];
}
if (!current || current.length == 0) {
return [];
}
if (reviewFilter?.showReviewed != 1) {
return current.filter((seg) => !seg.has_been_reviewed);
} else {
return current;
}
// only refresh when severity or filter changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [severity, reviewFilter, reviewItems?.all.length]);
// review summary
const { data: reviewSummary, mutate: updateSummary } = useSWR<ReviewSummary>(
@ -353,7 +414,8 @@ export default function Events() {
} else {
return (
<EventView
reviews={reviews}
reviewItems={reviewItems}
currentReviewItems={currentItems}
reviewSummary={reviewSummary}
relevantPreviews={allPreviews}
timeRange={selectedTimeRange}

View File

@ -20,6 +20,15 @@ export type ReviewData = {
zones: string[];
};
export type SegmentedReviewData =
| {
all: ReviewSegment[];
alert: ReviewSegment[];
detection: ReviewSegment[];
significant_motion: ReviewSegment[];
}
| undefined;
export type ReviewFilter = {
cameras?: string[];
labels?: string[];

View File

@ -17,6 +17,7 @@ import {
ReviewSegment,
ReviewSeverity,
ReviewSummary,
SegmentedReviewData,
} from "@/types/review";
import { getChunkedTimeRange } from "@/utils/timelineUtil";
import axios from "axios";
@ -49,7 +50,8 @@ import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner";
type EventViewProps = {
reviews?: ReviewSegment[];
reviewItems?: SegmentedReviewData;
currentReviewItems: ReviewSegment[] | null;
reviewSummary?: ReviewSummary;
relevantPreviews?: Preview[];
timeRange: TimeRange;
@ -64,7 +66,8 @@ type EventViewProps = {
updateFilter: (filter: ReviewFilter) => void;
};
export default function EventView({
reviews,
reviewItems,
currentReviewItems,
reviewSummary,
relevantPreviews,
timeRange,
@ -116,42 +119,6 @@ export default function EventView({
}
}, [filter, reviewSummary]);
// review paging
const reviewItems = useMemo(() => {
if (!reviews) {
return undefined;
}
const all: ReviewSegment[] = [];
const alerts: ReviewSegment[] = [];
const detections: ReviewSegment[] = [];
const motion: ReviewSegment[] = [];
reviews?.forEach((segment) => {
all.push(segment);
switch (segment.severity) {
case "alert":
alerts.push(segment);
break;
case "detection":
detections.push(segment);
break;
default:
motion.push(segment);
break;
}
});
return {
all: all,
alert: alerts,
detection: detections,
significant_motion: motion,
};
}, [reviews]);
// review interaction
const [selectedReviews, setSelectedReviews] = useState<string[]>([]);
@ -182,6 +149,7 @@ export default function EventView({
severity: review.severity,
});
review.has_been_reviewed = true;
markItemAsReviewed(review);
}
},
@ -332,6 +300,7 @@ export default function EventView({
<DetectionReview
contentRef={contentRef}
reviewItems={reviewItems}
currentItems={currentReviewItems}
relevantPreviews={relevantPreviews}
selectedReviews={selectedReviews}
itemsToReview={reviewCounts[severityToggle]}
@ -372,6 +341,7 @@ type DetectionReviewProps = {
detection: ReviewSegment[];
significant_motion: ReviewSegment[];
};
currentItems: ReviewSegment[] | null;
itemsToReview?: number;
relevantPreviews?: Preview[];
selectedReviews: string[];
@ -388,6 +358,7 @@ type DetectionReviewProps = {
function DetectionReview({
contentRef,
reviewItems,
currentItems,
itemsToReview,
relevantPreviews,
selectedReviews,
@ -405,33 +376,6 @@ function DetectionReview({
const segmentDuration = 60;
// review data
const currentItems = useMemo(() => {
if (!reviewItems) {
return null;
}
let current;
if (filter?.showAll) {
current = reviewItems.all;
} else {
current = reviewItems[severity];
}
if (!current || current.length == 0) {
return [];
}
if (filter?.showReviewed != 1) {
return current.filter((seg) => !seg.has_been_reviewed);
} else {
return current;
}
// only refresh when severity or filter changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [severity, filter, reviewItems?.all.length]);
// preview
const [previewTime, setPreviewTime] = useState<number>();