From fcb28cf1c20eb788090ece926003ea717e5d9387 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:42:29 -0500 Subject: [PATCH] use annotation offset --- .../components/overlay/ObjectTrackOverlay.tsx | 18 +++++++++++++----- web/src/components/timeline/ActivityStream.tsx | 18 +++++++++++------- web/src/contexts/ActivityStreamContext.tsx | 14 ++++++++++++++ 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/web/src/components/overlay/ObjectTrackOverlay.tsx b/web/src/components/overlay/ObjectTrackOverlay.tsx index bce733a20..593dc0b1f 100644 --- a/web/src/components/overlay/ObjectTrackOverlay.tsx +++ b/web/src/components/overlay/ObjectTrackOverlay.tsx @@ -2,6 +2,7 @@ import { useMemo, useCallback } from "react"; import { ObjectLifecycleSequence, LifecycleClassType } from "@/types/timeline"; import { FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; +import { useActivityStream } from "@/contexts/ActivityStreamContext"; import { Tooltip, TooltipContent, @@ -32,6 +33,10 @@ export default function ObjectTrackOverlay({ objectTimeline, }: ObjectTrackOverlayProps) { const { data: config } = useSWR("config"); + const { annotationOffset } = useActivityStream(); + + // Offset currentTime by annotation offset for rendering + const effectiveCurrentTime = currentTime - annotationOffset; // Fetch the full event data to get saved path points const { data: eventData } = useSWR(["event_ids", { ids: selectedObjectId }]); @@ -76,14 +81,14 @@ export default function ObjectTrackOverlay({ const currentObjectZones = useMemo(() => { if (!objectTimeline) return []; - // Find the most recent timeline event at or before current time + // Find the most recent timeline event at or before effective current time const relevantEvents = objectTimeline - .filter((event) => event.timestamp <= currentTime) + .filter((event) => event.timestamp <= effectiveCurrentTime) .sort((a, b) => b.timestamp - a.timestamp); // Most recent first // Get zones from the most recent event return relevantEvents[0]?.data?.zones || []; - }, [objectTimeline, currentTime]); + }, [objectTimeline, effectiveCurrentTime]); const zones = useMemo(() => { if (!config?.cameras?.[camera]?.zones || !currentObjectZones.length) @@ -320,9 +325,12 @@ export default function ObjectTrackOverlay({ {(() => { if (!objectTimeline) return null; - // Find the most recent timeline event at or before current time with a bounding box + // Find the most recent timeline event at or before effective current time with a bounding box const relevantEvents = objectTimeline - .filter((event) => event.timestamp <= currentTime && event.data.box) + .filter( + (event) => + event.timestamp <= effectiveCurrentTime && event.data.box, + ) .sort((a, b) => b.timestamp - a.timestamp); // Most recent first const currentEvent = relevantEvents[0]; diff --git a/web/src/components/timeline/ActivityStream.tsx b/web/src/components/timeline/ActivityStream.tsx index 7ff6db8f3..9aceda227 100644 --- a/web/src/components/timeline/ActivityStream.tsx +++ b/web/src/components/timeline/ActivityStream.tsx @@ -15,9 +15,11 @@ export default function ActivityStream({ currentTime, onSeek, }: ActivityStreamProps) { - const { selectedObjectId } = useActivityStream(); + const { selectedObjectId, annotationOffset } = useActivityStream(); const scrollRef = useRef(null); + const effectiveTime = currentTime + annotationOffset; + // group activities by timestamp (within 1 second resolution window) const groupedActivities = useMemo(() => { const groups: { [key: number]: ObjectLifecycleSequence[] } = {}; @@ -36,12 +38,13 @@ export default function ActivityStream({ (a, b) => a.timestamp - b.timestamp, ); return { - timestamp: sortedActivities[0].timestamp, // use the earliest activity timestamp + timestamp: sortedActivities[0].timestamp, // Original timestamp for display + effectiveTimestamp: sortedActivities[0].timestamp + annotationOffset, // Adjusted for sorting/comparison activities: sortedActivities, }; }) .sort((a, b) => a.timestamp - b.timestamp); - }, [timelineData]); + }, [timelineData, annotationOffset]); // Filter activities if object is selected const filteredGroups = useMemo(() => { @@ -62,10 +65,10 @@ export default function ActivityStream({ useEffect(() => { if (!scrollRef.current) return; - // Find the last group where timestamp <= currentTime + // Find the last group where effectiveTimestamp <= currentTime + annotationOffset let currentGroupIndex = -1; for (let i = filteredGroups.length - 1; i >= 0; i--) { - if (filteredGroups[i].timestamp <= currentTime) { + if (filteredGroups[i].effectiveTimestamp <= effectiveTime) { currentGroupIndex = i; break; } @@ -82,7 +85,7 @@ export default function ActivityStream({ }); } } - }, [filteredGroups, currentTime]); + }, [filteredGroups, effectiveTime, annotationOffset]); return (
)) @@ -112,6 +115,7 @@ export default function ActivityStream({ type ActivityGroupProps = { group: { timestamp: number; + effectiveTimestamp: number; activities: ObjectLifecycleSequence[]; }; isCurrent: boolean; diff --git a/web/src/contexts/ActivityStreamContext.tsx b/web/src/contexts/ActivityStreamContext.tsx index 3c95cbffa..b26c52279 100644 --- a/web/src/contexts/ActivityStreamContext.tsx +++ b/web/src/contexts/ActivityStreamContext.tsx @@ -1,11 +1,14 @@ import React, { createContext, useContext, useState, useMemo } from "react"; import { ObjectLifecycleSequence } from "@/types/timeline"; +import { FrigateConfig } from "@/types/frigateConfig"; +import useSWR from "swr"; interface ActivityStreamContextType { selectedObjectId: string | undefined; selectedObjectTimeline: ObjectLifecycleSequence[] | undefined; currentTime: number; camera: string; + annotationOffset: number; setSelectedObjectId: (id: string | undefined) => void; isActivityMode: boolean; } @@ -33,6 +36,16 @@ export function ActivityStreamProvider({ string | undefined >(); + const { data: config } = useSWR("config"); + + const annotationOffset = useMemo(() => { + if (!config) { + return 0; + } + + return (config.cameras[camera]?.detect?.annotation_offset || 0) / 1000; // Convert to seconds + }, [config, camera]); + const selectedObjectTimeline = useMemo(() => { if (!selectedObjectId || !timelineData) return undefined; return timelineData.filter((item) => item.source_id === selectedObjectId); @@ -43,6 +56,7 @@ export function ActivityStreamProvider({ selectedObjectTimeline, currentTime, camera, + annotationOffset, setSelectedObjectId, isActivityMode, };