mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-12 10:07:36 +03:00
add annotation offset slider
This commit is contained in:
parent
cc48dc7c4e
commit
ce7898b4bc
@ -37,8 +37,7 @@ export default function ObjectTrackOverlay({
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const { annotationOffset } = useActivityStream();
|
||||
|
||||
// Offset currentTime by annotation offset for rendering
|
||||
const effectiveCurrentTime = currentTime - annotationOffset;
|
||||
const effectiveCurrentTime = currentTime - annotationOffset / 1000;
|
||||
|
||||
// Fetch the full event data to get saved path points
|
||||
const { data: eventData } = useSWR(["event_ids", { ids: selectedObjectId }]);
|
||||
@ -157,8 +156,9 @@ export default function ObjectTrackOverlay({
|
||||
}, [savedPathPoints, eventSequencePoints, config, camera, currentTime]);
|
||||
|
||||
// get absolute positions on the svg canvas for each point
|
||||
const getAbsolutePositions = useCallback(() => {
|
||||
const absolutePositions = useMemo(() => {
|
||||
if (!pathPoints) return [];
|
||||
|
||||
return pathPoints.map((point) => {
|
||||
// Find the corresponding timeline entry for this point
|
||||
const timelineEntry = objectTimeline?.find(
|
||||
@ -272,11 +272,6 @@ export default function ObjectTrackOverlay({
|
||||
: [255, 0, 0];
|
||||
}, [pathPoints, getObjectColor]);
|
||||
|
||||
const absolutePositions = useMemo(
|
||||
() => getAbsolutePositions(),
|
||||
[getAbsolutePositions],
|
||||
);
|
||||
|
||||
// render any zones for object at current time
|
||||
const zonePolygons = useMemo(() => {
|
||||
return zones.map((zone) => {
|
||||
|
||||
95
web/src/components/overlay/detail/AnnotationOffsetSlider.tsx
Normal file
95
web/src/components/overlay/detail/AnnotationOffsetSlider.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useActivityStream } from "@/contexts/ActivityStreamContext";
|
||||
import axios from "axios";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function AnnotationOffsetSlider({ className }: Props) {
|
||||
const { annotationOffset, setAnnotationOffset, camera } = useActivityStream();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(values: number[]) => {
|
||||
if (!values || values.length === 0) return;
|
||||
const valueMs = values[0];
|
||||
setAnnotationOffset(valueMs);
|
||||
},
|
||||
[setAnnotationOffset],
|
||||
);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setAnnotationOffset(0);
|
||||
}, [setAnnotationOffset]);
|
||||
|
||||
const save = useCallback(async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// save value in milliseconds to config
|
||||
await axios.put(
|
||||
`config/set?cameras.${camera}.detect.annotation_offset=${annotationOffset}`,
|
||||
{ requires_restart: 0 },
|
||||
);
|
||||
|
||||
toast.success(
|
||||
t("objectLifecycle.annotationSettings.offset.toast.success", {
|
||||
camera,
|
||||
}),
|
||||
{ position: "top-center" },
|
||||
);
|
||||
|
||||
// refresh config
|
||||
await mutate("config");
|
||||
} catch (e: unknown) {
|
||||
const err = e as {
|
||||
response?: { data?: { message?: string } };
|
||||
message?: string;
|
||||
};
|
||||
const errorMessage =
|
||||
err?.response?.data?.message || err?.message || "Unknown error";
|
||||
toast.error(t("toast.save.error.title", { errorMessage, ns: "common" }), {
|
||||
position: "top-center",
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [annotationOffset, camera, mutate, t]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 z-30 flex items-center gap-3 bg-gradient-to-t from-secondary/90 to-transparent p-3 ${className ?? ""}`}
|
||||
style={{ pointerEvents: "auto" }}
|
||||
>
|
||||
<div className="w-56 text-sm">
|
||||
Annotation offset (ms): {annotationOffset}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Slider
|
||||
value={[annotationOffset]}
|
||||
min={-1500}
|
||||
max={1500}
|
||||
step={50}
|
||||
onValueChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="ghost" onClick={reset}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button size="sm" onClick={save} disabled={isSaving}>
|
||||
{isSaving
|
||||
? t("button.saving", { ns: "common" })
|
||||
: t("button.save", { ns: "common" })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -174,7 +174,7 @@ export function AnnotationSettingsPane({
|
||||
{t("objectLifecycle.annotationSettings.offset.label")}
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-3 md:flex-row-reverse md:gap-8">
|
||||
<div className="flex flex-row items-center gap-3 rounded-lg bg-destructive/50 p-3 text-sm text-primary-variant md:my-0 md:my-5">
|
||||
<div className="flex flex-row items-center gap-3 rounded-lg bg-destructive/50 p-3 text-sm text-primary-variant md:my-5">
|
||||
<PiWarningCircle className="size-24" />
|
||||
<div>
|
||||
<Trans ns="views/explore">
|
||||
|
||||
@ -7,6 +7,7 @@ import scrollIntoView from "scroll-into-view-if-needed";
|
||||
import useUserInteraction from "@/hooks/use-user-interaction";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AnnotationOffsetSlider from "@/components/overlay/detail/AnnotationOffsetSlider";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import useSWR from "swr";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
@ -27,7 +28,7 @@ export default function ActivityStream({
|
||||
const { selectedObjectId, annotationOffset } = useActivityStream();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const effectiveTime = currentTime + annotationOffset;
|
||||
const effectiveTime = currentTime + annotationOffset / 1000;
|
||||
|
||||
// Track user interaction and adjust scrolling behavior
|
||||
const { userInteracting, setProgrammaticScroll } = useUserInteraction({
|
||||
@ -53,7 +54,8 @@ export default function ActivityStream({
|
||||
);
|
||||
return {
|
||||
timestamp: sortedActivities[0].timestamp, // Original timestamp for display
|
||||
effectiveTimestamp: sortedActivities[0].timestamp + annotationOffset, // Adjusted for sorting/comparison
|
||||
effectiveTimestamp:
|
||||
sortedActivities[0].timestamp + annotationOffset / 1000,
|
||||
activities: sortedActivities,
|
||||
};
|
||||
})
|
||||
@ -113,27 +115,31 @@ export default function ActivityStream({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="scrollbar-container h-full overflow-y-auto bg-secondary"
|
||||
>
|
||||
<div className="space-y-2 p-4">
|
||||
{filteredGroups.length === 0 ? (
|
||||
<div className="py-8 text-center text-muted-foreground">
|
||||
{t("activity.noActivitiesFound")}
|
||||
</div>
|
||||
) : (
|
||||
filteredGroups.map((group) => (
|
||||
<ActivityGroup
|
||||
key={group.timestamp}
|
||||
group={group}
|
||||
config={config}
|
||||
isCurrent={group.effectiveTimestamp <= currentTime}
|
||||
onSeek={onSeek}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="scrollbar-container h-[calc(100vh-70px)] overflow-y-auto bg-secondary"
|
||||
>
|
||||
<div className="space-y-2 p-4">
|
||||
{filteredGroups.length === 0 ? (
|
||||
<div className="py-8 text-center text-muted-foreground">
|
||||
{t("activity.noActivitiesFound")}
|
||||
</div>
|
||||
) : (
|
||||
filteredGroups.map((group) => (
|
||||
<ActivityGroup
|
||||
key={group.timestamp}
|
||||
group={group}
|
||||
config={config}
|
||||
isCurrent={group.effectiveTimestamp <= currentTime}
|
||||
onSeek={onSeek}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnnotationOffsetSlider />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import React, { createContext, useContext, useState, useMemo } from "react";
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useMemo,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { ObjectLifecycleSequence } from "@/types/timeline";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import useSWR from "swr";
|
||||
@ -8,7 +14,8 @@ interface ActivityStreamContextType {
|
||||
selectedObjectTimeline: ObjectLifecycleSequence[] | undefined;
|
||||
currentTime: number;
|
||||
camera: string;
|
||||
annotationOffset: number;
|
||||
annotationOffset: number; // milliseconds
|
||||
setAnnotationOffset: (ms: number) => void;
|
||||
setSelectedObjectId: (id: string | undefined) => void;
|
||||
isActivityMode: boolean;
|
||||
}
|
||||
@ -38,12 +45,15 @@ export function ActivityStreamProvider({
|
||||
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const annotationOffset = useMemo(() => {
|
||||
if (!config) {
|
||||
return 0;
|
||||
}
|
||||
const [annotationOffset, setAnnotationOffset] = useState<number>(() => {
|
||||
if (!config) return 0;
|
||||
return config.cameras[camera]?.detect?.annotation_offset || 0;
|
||||
});
|
||||
|
||||
return (config.cameras[camera]?.detect?.annotation_offset || 0) / 1000; // Convert to seconds
|
||||
useEffect(() => {
|
||||
if (!config) return;
|
||||
const cfgOffset = config.cameras[camera]?.detect?.annotation_offset || 0;
|
||||
setAnnotationOffset(cfgOffset);
|
||||
}, [config, camera]);
|
||||
|
||||
const selectedObjectTimeline = useMemo(() => {
|
||||
@ -57,6 +67,7 @@ export function ActivityStreamProvider({
|
||||
currentTime,
|
||||
camera,
|
||||
annotationOffset,
|
||||
setAnnotationOffset,
|
||||
setSelectedObjectId,
|
||||
isActivityMode,
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user