From 086330a542842206fcd703cfd41fcbf2bb70ecf2 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 24 Nov 2025 06:54:22 -0600 Subject: [PATCH] tracking details tweaks - Add attribute box overlay and area - Add score - Throttle swr revalidation during video component rerendering --- frigate/timeline.py | 1 + .../components/overlay/ObjectTrackOverlay.tsx | 30 ++- .../overlay/detail/TrackingDetails.tsx | 214 ++++++++++++------ web/src/types/timeline.ts | 1 + 4 files changed, 173 insertions(+), 73 deletions(-) diff --git a/frigate/timeline.py b/frigate/timeline.py index 8e6aedc67..a2d59b88e 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -109,6 +109,7 @@ class TimelineProcessor(threading.Thread): event_data["region"], ), "attribute": "", + "score": event_data["score"], }, } diff --git a/web/src/components/overlay/ObjectTrackOverlay.tsx b/web/src/components/overlay/ObjectTrackOverlay.tsx index 7e548af2e..8f78adcd7 100644 --- a/web/src/components/overlay/ObjectTrackOverlay.tsx +++ b/web/src/components/overlay/ObjectTrackOverlay.tsx @@ -42,6 +42,7 @@ type ObjectData = { pathPoints: PathPoint[]; currentZones: string[]; currentBox?: number[]; + currentAttributeBox?: number[]; }; export default function ObjectTrackOverlay({ @@ -105,6 +106,12 @@ export default function ObjectTrackOverlay({ selectedObjectIds.length > 0 ? ["event_ids", { ids: selectedObjectIds.join(",") }] : null, + null, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 30000, + }, ); // Fetch timeline data for each object ID using fixed number of hooks @@ -112,7 +119,12 @@ export default function ObjectTrackOverlay({ selectedObjectIds.length > 0 ? `timeline?source_id=${selectedObjectIds.join(",")}&limit=1000` : null, - { revalidateOnFocus: false }, + null, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 30000, + }, ); const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => { @@ -270,6 +282,7 @@ export default function ObjectTrackOverlay({ ); const currentBox = nearbyTimelineEvent?.data?.box; + const currentAttributeBox = nearbyTimelineEvent?.data?.attribute_box; return { objectId, @@ -278,6 +291,7 @@ export default function ObjectTrackOverlay({ pathPoints: combinedPoints, currentZones, currentBox, + currentAttributeBox, }; }) .filter((obj: ObjectData) => obj.pathPoints.length > 0); // Only include objects with path data @@ -482,6 +496,20 @@ export default function ObjectTrackOverlay({ /> )} + {objData.currentAttributeBox && showBoundingBoxes && ( + + + + )} ); })} diff --git a/web/src/components/overlay/detail/TrackingDetails.tsx b/web/src/components/overlay/detail/TrackingDetails.tsx index 727dd4552..c6e10f8c2 100644 --- a/web/src/components/overlay/detail/TrackingDetails.tsx +++ b/web/src/components/overlay/detail/TrackingDetails.tsx @@ -75,12 +75,15 @@ export function TrackingDetails({ setIsVideoLoading(true); }, [event.id]); - const { data: eventSequence } = useSWR([ - "timeline", + const { data: eventSequence } = useSWR( + ["timeline", { source_id: event.id }], + null, { - source_id: event.id, + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 30000, }, - ]); + ); const { data: config } = useSWR("config"); @@ -104,6 +107,12 @@ export function TrackingDetails({ }, ] : null, + null, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 30000, + }, ); // Convert a timeline timestamp to actual video player time, accounting for @@ -714,53 +723,6 @@ export function TrackingDetails({ )}
{eventSequence.map((item, idx) => { - const isActive = - Math.abs( - (effectiveTime ?? 0) - (item.timestamp ?? 0), - ) <= 0.5; - const formattedEventTimestamp = config - ? formatUnixTimestampToDateTime(item.timestamp ?? 0, { - timezone: config.ui.timezone, - date_format: - config.ui.time_format == "24hour" - ? t( - "time.formattedTimestampHourMinuteSecond.24hour", - { ns: "common" }, - ) - : t( - "time.formattedTimestampHourMinuteSecond.12hour", - { ns: "common" }, - ), - time_style: "medium", - date_style: "medium", - }) - : ""; - - const ratio = - Array.isArray(item.data.box) && - item.data.box.length >= 4 - ? ( - aspectRatio * - (item.data.box[2] / item.data.box[3]) - ).toFixed(2) - : "N/A"; - const areaPx = - Array.isArray(item.data.box) && - item.data.box.length >= 4 - ? Math.round( - (config.cameras[event.camera]?.detect?.width ?? - 0) * - (config.cameras[event.camera]?.detect - ?.height ?? 0) * - (item.data.box[2] * item.data.box[3]), - ) - : undefined; - const areaPct = - Array.isArray(item.data.box) && - item.data.box.length >= 4 - ? (item.data.box[2] * item.data.box[3]).toFixed(4) - : undefined; - return (
handleLifecycleClick(item)} setSelectedZone={setSelectedZone} getZoneColor={getZoneColor} @@ -798,11 +756,7 @@ export function TrackingDetails({ type LifecycleIconRowProps = { item: TrackingDetailsSequence; - isActive?: boolean; - formattedEventTimestamp: string; - ratio: string; - areaPx?: number; - areaPct?: string; + event: Event; onClick: () => void; setSelectedZone: (z: string) => void; getZoneColor: (zoneName: string) => number[] | undefined; @@ -812,11 +766,7 @@ type LifecycleIconRowProps = { function LifecycleIconRow({ item, - isActive, - formattedEventTimestamp, - ratio, - areaPx, - areaPct, + event, onClick, setSelectedZone, getZoneColor, @@ -826,9 +776,101 @@ function LifecycleIconRow({ const { t } = useTranslation(["views/explore", "components/player"]); const { data: config } = useSWR("config"); const [isOpen, setIsOpen] = useState(false); - const navigate = useNavigate(); + const aspectRatio = useMemo(() => { + if (!config) { + return 16 / 9; + } + + return ( + config.cameras[event.camera].detect.width / + config.cameras[event.camera].detect.height + ); + }, [config, event]); + + const isActive = useMemo( + () => Math.abs((effectiveTime ?? 0) - (item.timestamp ?? 0)) <= 0.5, + [effectiveTime, item.timestamp], + ); + + const formattedEventTimestamp = useMemo( + () => + config + ? formatUnixTimestampToDateTime(item.timestamp ?? 0, { + timezone: config.ui.timezone, + date_format: + config.ui.time_format == "24hour" + ? t("time.formattedTimestampHourMinuteSecond.24hour", { + ns: "common", + }) + : t("time.formattedTimestampHourMinuteSecond.12hour", { + ns: "common", + }), + time_style: "medium", + date_style: "medium", + }) + : "", + [config, item.timestamp, t], + ); + + const ratio = useMemo( + () => + Array.isArray(item.data.box) && item.data.box.length >= 4 + ? (aspectRatio * (item.data.box[2] / item.data.box[3])).toFixed(2) + : "N/A", + [aspectRatio, item.data.box], + ); + + const areaPx = useMemo( + () => + Array.isArray(item.data.box) && item.data.box.length >= 4 + ? Math.round( + (config?.cameras[event.camera]?.detect?.width ?? 0) * + (config?.cameras[event.camera]?.detect?.height ?? 0) * + (item.data.box[2] * item.data.box[3]), + ) + : undefined, + [config, event.camera, item.data.box], + ); + + const attributeAreaPx = useMemo( + () => + Array.isArray(item.data.attribute_box) && + item.data.attribute_box.length >= 4 + ? Math.round( + (config?.cameras[event.camera]?.detect?.width ?? 0) * + (config?.cameras[event.camera]?.detect?.height ?? 0) * + (item.data.attribute_box[2] * item.data.attribute_box[3]), + ) + : undefined, + [config, event.camera, item.data.attribute_box], + ); + + const attributeAreaPct = useMemo( + () => + Array.isArray(item.data.attribute_box) && + item.data.attribute_box.length >= 4 + ? (item.data.attribute_box[2] * item.data.attribute_box[3]).toFixed(4) + : undefined, + [item.data.attribute_box], + ); + + const areaPct = useMemo( + () => + Array.isArray(item.data.box) && item.data.box.length >= 4 + ? (item.data.box[2] * item.data.box[3]).toFixed(4) + : undefined, + [item.data.box], + ); + + const score = useMemo(() => { + if (item.data.score !== undefined) { + return (item.data.score * 100).toFixed(0) + "%"; + } + return "N/A"; + }, [item.data.score]); + return (
{getLifecycleItemDescription(item)}
-
-
+
+
+ + {t("trackingDetails.lifecycleItemDesc.header.score")} + + {score} +
+
{t("trackingDetails.lifecycleItemDesc.header.ratio")} {ratio}
-
+
- {t("trackingDetails.lifecycleItemDesc.header.area")} + {t("trackingDetails.lifecycleItemDesc.header.area")}{" "} + {attributeAreaPx !== undefined && + attributeAreaPct !== undefined && ( + + ({getTranslatedLabel(item.data.label)}) + + )} {areaPx !== undefined && areaPct !== undefined ? ( @@ -876,9 +930,25 @@ function LifecycleIconRow({ N/A )}
+ {attributeAreaPx !== undefined && + attributeAreaPct !== undefined && ( +
+ + {t("trackingDetails.lifecycleItemDesc.header.area")} ( + {getTranslatedLabel(item.data.attribute)}) + + + {t("information.pixels", { + ns: "common", + area: attributeAreaPx, + })}{" "} + ยท {attributeAreaPct}% + +
+ )} {item.data?.zones && item.data.zones.length > 0 && ( -
+
{item.data.zones.map((zone, zidx) => { const color = getZoneColor(zone)?.join(",") ?? "0,0,0"; return ( diff --git a/web/src/types/timeline.ts b/web/src/types/timeline.ts index c8e5f7543..0de067406 100644 --- a/web/src/types/timeline.ts +++ b/web/src/types/timeline.ts @@ -16,6 +16,7 @@ export type TrackingDetailsSequence = { data: { camera: string; label: string; + score: number; sub_label: string; box?: [number, number, number, number]; region: [number, number, number, number];