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 { ObjectLifecycleSequence, LifecycleClassType } from "@/types/timeline";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr"; import useSWR from "swr";
import { useActivityStream } from "@/contexts/ActivityStreamContext";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -32,6 +33,10 @@ export default function ObjectTrackOverlay({
objectTimeline, objectTimeline,
}: ObjectTrackOverlayProps) { }: ObjectTrackOverlayProps) {
const { data: config } = useSWR<FrigateConfig>("config"); 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 // Fetch the full event data to get saved path points
const { data: eventData } = useSWR(["event_ids", { ids: selectedObjectId }]); const { data: eventData } = useSWR(["event_ids", { ids: selectedObjectId }]);
@ -76,14 +81,14 @@ export default function ObjectTrackOverlay({
const currentObjectZones = useMemo(() => { const currentObjectZones = useMemo(() => {
if (!objectTimeline) return []; 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 const relevantEvents = objectTimeline
.filter((event) => event.timestamp <= currentTime) .filter((event) => event.timestamp <= effectiveCurrentTime)
.sort((a, b) => b.timestamp - a.timestamp); // Most recent first .sort((a, b) => b.timestamp - a.timestamp); // Most recent first
// Get zones from the most recent event // Get zones from the most recent event
return relevantEvents[0]?.data?.zones || []; return relevantEvents[0]?.data?.zones || [];
}, [objectTimeline, currentTime]); }, [objectTimeline, effectiveCurrentTime]);
const zones = useMemo(() => { const zones = useMemo(() => {
if (!config?.cameras?.[camera]?.zones || !currentObjectZones.length) if (!config?.cameras?.[camera]?.zones || !currentObjectZones.length)
@ -320,9 +325,12 @@ export default function ObjectTrackOverlay({
{(() => { {(() => {
if (!objectTimeline) return null; 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 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 .sort((a, b) => b.timestamp - a.timestamp); // Most recent first
const currentEvent = relevantEvents[0]; const currentEvent = relevantEvents[0];

View File

@ -15,9 +15,11 @@ export default function ActivityStream({
currentTime, currentTime,
onSeek, onSeek,
}: ActivityStreamProps) { }: ActivityStreamProps) {
const { selectedObjectId } = useActivityStream(); const { selectedObjectId, annotationOffset } = useActivityStream();
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const effectiveTime = currentTime + annotationOffset;
// group activities by timestamp (within 1 second resolution window) // group activities by timestamp (within 1 second resolution window)
const groupedActivities = useMemo(() => { const groupedActivities = useMemo(() => {
const groups: { [key: number]: ObjectLifecycleSequence[] } = {}; const groups: { [key: number]: ObjectLifecycleSequence[] } = {};
@ -36,12 +38,13 @@ export default function ActivityStream({
(a, b) => a.timestamp - b.timestamp, (a, b) => a.timestamp - b.timestamp,
); );
return { 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, activities: sortedActivities,
}; };
}) })
.sort((a, b) => a.timestamp - b.timestamp); .sort((a, b) => a.timestamp - b.timestamp);
}, [timelineData]); }, [timelineData, annotationOffset]);
// Filter activities if object is selected // Filter activities if object is selected
const filteredGroups = useMemo(() => { const filteredGroups = useMemo(() => {
@ -62,10 +65,10 @@ export default function ActivityStream({
useEffect(() => { useEffect(() => {
if (!scrollRef.current) return; if (!scrollRef.current) return;
// Find the last group where timestamp <= currentTime // Find the last group where effectiveTimestamp <= currentTime + annotationOffset
let currentGroupIndex = -1; let currentGroupIndex = -1;
for (let i = filteredGroups.length - 1; i >= 0; i--) { for (let i = filteredGroups.length - 1; i >= 0; i--) {
if (filteredGroups[i].timestamp <= currentTime) { if (filteredGroups[i].effectiveTimestamp <= effectiveTime) {
currentGroupIndex = i; currentGroupIndex = i;
break; break;
} }
@ -82,7 +85,7 @@ export default function ActivityStream({
}); });
} }
} }
}, [filteredGroups, currentTime]); }, [filteredGroups, effectiveTime, annotationOffset]);
return ( return (
<div <div
@ -99,7 +102,7 @@ export default function ActivityStream({
<ActivityGroup <ActivityGroup
key={group.timestamp} key={group.timestamp}
group={group} group={group}
isCurrent={group.timestamp <= currentTime} isCurrent={group.effectiveTimestamp <= currentTime}
onSeek={onSeek} onSeek={onSeek}
/> />
)) ))
@ -112,6 +115,7 @@ export default function ActivityStream({
type ActivityGroupProps = { type ActivityGroupProps = {
group: { group: {
timestamp: number; timestamp: number;
effectiveTimestamp: number;
activities: ObjectLifecycleSequence[]; activities: ObjectLifecycleSequence[];
}; };
isCurrent: boolean; isCurrent: boolean;

View File

@ -1,11 +1,14 @@
import React, { createContext, useContext, useState, useMemo } from "react"; import React, { createContext, useContext, useState, useMemo } from "react";
import { ObjectLifecycleSequence } from "@/types/timeline"; import { ObjectLifecycleSequence } from "@/types/timeline";
import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
interface ActivityStreamContextType { interface ActivityStreamContextType {
selectedObjectId: string | undefined; selectedObjectId: string | undefined;
selectedObjectTimeline: ObjectLifecycleSequence[] | undefined; selectedObjectTimeline: ObjectLifecycleSequence[] | undefined;
currentTime: number; currentTime: number;
camera: string; camera: string;
annotationOffset: number;
setSelectedObjectId: (id: string | undefined) => void; setSelectedObjectId: (id: string | undefined) => void;
isActivityMode: boolean; isActivityMode: boolean;
} }
@ -33,6 +36,16 @@ export function ActivityStreamProvider({
string | undefined 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(() => { const selectedObjectTimeline = useMemo(() => {
if (!selectedObjectId || !timelineData) return undefined; if (!selectedObjectId || !timelineData) return undefined;
return timelineData.filter((item) => item.source_id === selectedObjectId); return timelineData.filter((item) => item.source_id === selectedObjectId);
@ -43,6 +56,7 @@ export function ActivityStreamProvider({
selectedObjectTimeline, selectedObjectTimeline,
currentTime, currentTime,
camera, camera,
annotationOffset,
setSelectedObjectId, setSelectedObjectId,
isActivityMode, isActivityMode,
}; };