use annotation offset

This commit is contained in:
Josh Hawkins 2025-10-07 10:42:29 -05:00
parent f673398053
commit fcb28cf1c2
3 changed files with 38 additions and 12 deletions

View File

@ -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<FrigateConfig>("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];

View File

@ -15,9 +15,11 @@ export default function ActivityStream({
currentTime,
onSeek,
}: ActivityStreamProps) {
const { selectedObjectId } = useActivityStream();
const { selectedObjectId, annotationOffset } = useActivityStream();
const scrollRef = useRef<HTMLDivElement>(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 (
<div
@ -99,7 +102,7 @@ export default function ActivityStream({
<ActivityGroup
key={group.timestamp}
group={group}
isCurrent={group.timestamp <= currentTime}
isCurrent={group.effectiveTimestamp <= currentTime}
onSeek={onSeek}
/>
))
@ -112,6 +115,7 @@ export default function ActivityStream({
type ActivityGroupProps = {
group: {
timestamp: number;
effectiveTimestamp: number;
activities: ObjectLifecycleSequence[];
};
isCurrent: boolean;

View File

@ -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<FrigateConfig>("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,
};