From 0e0b8268dc1d53325d97a920c9bdfecb3a1d0d09 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 24 Mar 2024 20:03:31 -0500 Subject: [PATCH] handle motion timestamps with ranges --- web/src/components/timeline/MotionSegment.tsx | 14 ++- web/src/hooks/use-camera-activity.ts | 56 ---------- web/src/hooks/use-draggable-element.ts | 50 ++++++--- web/src/hooks/use-motion-segment-utils.ts | 2 +- web/src/views/events/EventView.tsx | 105 ++++++++++++++---- 5 files changed, 125 insertions(+), 102 deletions(-) diff --git a/web/src/components/timeline/MotionSegment.tsx b/web/src/components/timeline/MotionSegment.tsx index 748024239..f816acc25 100644 --- a/web/src/components/timeline/MotionSegment.tsx +++ b/web/src/components/timeline/MotionSegment.tsx @@ -158,9 +158,9 @@ export function MotionSegment({ : "" }`; - const animationClassesSecondHalf = `motion-segment ${secondHalfSegmentWidth > 1 ? "hidden" : ""} + const animationClassesSecondHalf = `motion-segment ${secondHalfSegmentWidth > 0 ? "hidden" : ""} zoom-in-[0.2] ${secondHalfSegmentWidth < 5 ? "duration-200" : "duration-1000"}`; - const animationClassesFirstHalf = `motion-segment ${firstHalfSegmentWidth > 1 ? "hidden" : ""} + const animationClassesFirstHalf = `motion-segment ${firstHalfSegmentWidth > 0 ? "hidden" : ""} zoom-in-[0.2] ${firstHalfSegmentWidth < 5 ? "duration-200" : "duration-1000"}`; const severityColors: { [key: number]: string } = { @@ -183,14 +183,14 @@ export function MotionSegment({ return ( <> - {(((firstHalfSegmentWidth > 1 || secondHalfSegmentWidth > 1) && + {(((firstHalfSegmentWidth > 0 || secondHalfSegmentWidth > 0) && motionOnly && severity[0] < 2) || !motionOnly) && (
1 || secondHalfSegmentWidth > 1 ? "has-data" : ""} ${segmentClasses}`} + className={`segment ${firstHalfSegmentWidth > 0 || secondHalfSegmentWidth > 0 ? "has-data" : ""} ${segmentClasses}`} onClick={segmentClick} onTouchEnd={(event) => handleTouchStart(event, segmentClick)} > @@ -228,9 +228,10 @@ export function MotionSegment({
@@ -240,9 +241,10 @@ export function MotionSegment({
diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index 01406d29a..d2fa49671 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -4,8 +4,6 @@ import { useMotionActivity, } from "@/api/ws"; import { CameraConfig } from "@/types/frigateConfig"; -import { MotionData, ReviewSegment } from "@/types/review"; -import { TimeRange } from "@/types/timeline"; import { useEffect, useMemo, useState } from "react"; type useCameraActivityReturn = { @@ -68,57 +66,3 @@ export function useCameraActivity( : false, }; } - -export function useCameraMotionTimestamps( - timeRange: TimeRange, - motionOnly: boolean, - events: ReviewSegment[], - motion: MotionData[], -) { - const timestamps = useMemo(() => { - const seekableTimestamps = []; - let lastEventIdx = 0; - let lastMotionIdx = 0; - - for (let i = timeRange.after; i <= timeRange.before; i += 0.5) { - if (!motionOnly) { - seekableTimestamps.push(i); - } else { - const relevantEventIdx = events.findIndex((seg, segIdx) => { - if (segIdx < lastEventIdx) { - return false; - } - - return seg.start_time <= i && seg.end_time >= i; - }); - - if (relevantEventIdx != -1) { - lastEventIdx = relevantEventIdx; - continue; - } - - const relevantMotionIdx = motion.findIndex((mot, motIdx) => { - if (motIdx < lastMotionIdx) { - return false; - } - - return mot.start_time <= i && mot.start_time + 15 >= i; - }); - - if (relevantMotionIdx == -1 || motion[relevantMotionIdx].motion == 0) { - if (relevantMotionIdx != -1) { - lastMotionIdx = relevantMotionIdx; - } - - continue; - } - - seekableTimestamps.push(i); - } - } - - return seekableTimestamps; - }, [timeRange, motionOnly, events, motion]); - - return timestamps; -} diff --git a/web/src/hooks/use-draggable-element.ts b/web/src/hooks/use-draggable-element.ts index d4cd3e713..49499900f 100644 --- a/web/src/hooks/use-draggable-element.ts +++ b/web/src/hooks/use-draggable-element.ts @@ -368,27 +368,10 @@ function useDraggableElement({ const alignedSegmentTime = alignStartDateToTimeline(draggableElementTime); - let segmentElement = timelineRef.current.querySelector( + const segmentElement = timelineRef.current.querySelector( `[data-segment-id="${alignedSegmentTime}"]`, ); - if (!segmentElement) { - // segment not found, maybe we collapsed over a collapsible segment - let searchTime = alignedSegmentTime; - while (searchTime >= timelineStartAligned - timelineDuration) { - // Decrement currentTime by segmentDuration - searchTime -= segmentDuration; - segmentElement = timelineRef.current.querySelector( - `[data-segment-id="${searchTime}"]`, - ); - - if (segmentElement) { - // segmentElement found - break; - } - } - } - if (segmentElement) { const timelineRect = timelineRef.current.getBoundingClientRect(); const timelineTopAbsolute = timelineRect.top; @@ -422,6 +405,37 @@ function useDraggableElement({ segments, ]); + useEffect(() => { + if (timelineRef.current && draggableElementTime && timelineCollapsed) { + const alignedSegmentTime = alignStartDateToTimeline(draggableElementTime); + + let segmentElement = timelineRef.current.querySelector( + `[data-segment-id="${alignedSegmentTime}"]`, + ); + + if (!segmentElement) { + // segment not found, maybe we collapsed over a collapsible segment + let searchTime = alignedSegmentTime; + while (searchTime >= timelineStartAligned - timelineDuration) { + searchTime -= segmentDuration; + segmentElement = timelineRef.current.querySelector( + `[data-segment-id="${searchTime}"]`, + ); + + if (segmentElement) { + // found, set time + if (setDraggableElementTime) { + setDraggableElementTime(searchTime); + } + break; + } + } + } + } + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [timelineCollapsed]); + return { handleMouseDown, handleMouseUp, handleMouseMove }; } diff --git a/web/src/hooks/use-motion-segment-utils.ts b/web/src/hooks/use-motion-segment-utils.ts index dfec48358..0482e776e 100644 --- a/web/src/hooks/use-motion-segment-utils.ts +++ b/web/src/hooks/use-motion-segment-utils.ts @@ -33,7 +33,7 @@ export const useMotionSegmentUtils = ( const interpolateMotionAudioData = useCallback( (value: number, newMax: number): number => { - return Math.ceil((Math.abs(value) / 100.0) * newMax) || 1; + return Math.ceil((Math.abs(value) / 100.0) * newMax) || 0; }, [], ); diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 41101807d..d13fc543d 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -40,7 +40,6 @@ import SummaryTimeline from "@/components/timeline/SummaryTimeline"; import { RecordingStartingPoint } from "@/types/record"; import VideoControls from "@/components/player/VideoControls"; import { TimeRange } from "@/types/timeline"; -import { useCameraMotionTimestamps } from "@/hooks/use-camera-activity"; type EventViewProps = { reviews?: ReviewSegment[]; @@ -720,12 +719,87 @@ function MotionReview({ const [playbackRate, setPlaybackRate] = useState(8); const [controlsOpen, setControlsOpen] = useState(false); - const seekTimestamps = useCameraMotionTimestamps( - timeRange, - motionOnly, - reviewItems?.all ?? [], - motionData ?? [], - ); + + const ranges = useMemo(() => { + if (!motionData || !reviewItems) { + return; + } + + if (!motionOnly) { + return []; + } + + const ranges = []; + let currentSegmentStart = null; + let currentSegmentEnd = null; + + for (let i = 0; i < motionData.length; i = i + segmentDuration / 15) { + const motionStart = motionData[i].start_time; + const motionEnd = motionStart + segmentDuration; + + const segmentMotion = motionData + .slice(i, i + segmentDuration / 15) + .some(({ motion }) => motion !== undefined && motion > 0); + const overlappingReviewItems = reviewItems.all.some( + (item) => + (item.start_time >= motionStart && item.start_time < motionEnd) || + (item.end_time > motionStart && item.end_time <= motionEnd) || + (item.start_time <= motionStart && item.end_time >= motionEnd), + ); + + if (!segmentMotion || overlappingReviewItems) { + if (currentSegmentStart === null) { + currentSegmentStart = motionStart; + } + currentSegmentEnd = motionEnd; + } else { + if (currentSegmentStart !== null) { + ranges.push([currentSegmentStart, currentSegmentEnd]); + currentSegmentStart = null; + currentSegmentEnd = null; + } + } + } + + if (currentSegmentStart !== null) { + ranges.push([currentSegmentStart, currentSegmentEnd]); + } + + return ranges; + }, [motionData, reviewItems, motionOnly]); + + const nextTimestamp = useMemo(() => { + if (!ranges) { + return; + } + let currentRange = 0; + let nextTimestamp = currentTime + 0.5; + + while (currentRange < ranges.length) { + const [start, end] = ranges[currentRange]; + + if (start && end) { + // If the current time is before the start of the current range + if (currentTime < start) { + // The next timestamp is either the start of the current range or currentTime + 0.5, whichever is smaller + nextTimestamp = Math.min(start, nextTimestamp); + break; + } + // If the current time is within the current range + else if (currentTime >= start && currentTime < end) { + // The next timestamp is the end of the current range + nextTimestamp = end; + currentRange++; + } + // If the current time is past the end of the current range + else { + currentRange++; + } + } + } + + return nextTimestamp; + }, [currentTime, ranges]); useEffect(() => { if (!playing) { @@ -733,22 +807,11 @@ function MotionReview({ } const interval = 500 / playbackRate; - const startIdx = seekTimestamps.findIndex((time) => time > currentTime); - if (!startIdx) { - return; - } - - let counter = 0; const intervalId = setInterval(() => { - counter += 1; - - if (startIdx + counter >= seekTimestamps.length) { - setPlaying(false); - return; + if (nextTimestamp) { + setCurrentTime(nextTimestamp); } - - setCurrentTime(seekTimestamps[startIdx + counter]); }, interval); return () => { @@ -756,7 +819,7 @@ function MotionReview({ }; // do not render when current time changes // eslint-disable-next-line react-hooks/exhaustive-deps - }, [playing, playbackRate]); + }, [playing, playbackRate, nextTimestamp]); const { alignStartDateToTimeline } = useTimelineUtils({ segmentDuration,