diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index d2fa49671..9ddcfa692 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -4,7 +4,9 @@ import { useMotionActivity, } from "@/api/ws"; import { CameraConfig } from "@/types/frigateConfig"; +import { MotionData, ReviewSegment } from "@/types/review"; import { useEffect, useMemo, useState } from "react"; +import { useTimelineUtils } from "./use-timeline-utils"; type useCameraActivityReturn = { activeTracking: boolean; @@ -66,3 +68,124 @@ export function useCameraActivity( : false, }; } + +export function useCameraMotionNextTimestamp( + timeRangeSegmentEnd: number, + segmentDuration: number, + motionOnly: boolean, + reviewItems: ReviewSegment[], + motionData: MotionData[], + currentTime: number, +) { + const { alignStartDateToTimeline } = useTimelineUtils({ + segmentDuration, + }); + + const noMotionRanges = useMemo(() => { + if (!motionData || !reviewItems) { + return; + } + + if (!motionOnly) { + return []; + } + + const ranges = []; + let currentSegmentStart = null; + let currentSegmentEnd = null; + + // align motion start to timeline start + const offset = + (motionData[0].start_time - + alignStartDateToTimeline(timeRangeSegmentEnd)) % + segmentDuration; + + const startIndex = + offset > 0 ? Math.floor(offset / (segmentDuration / 15)) : 0; + + for ( + let i = startIndex; + 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.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, + alignStartDateToTimeline, + segmentDuration, + timeRangeSegmentEnd, + ]); + + const nextTimestamp = useMemo(() => { + if (!noMotionRanges) { + return; + } + + if (!motionOnly) { + return currentTime + 0.5; + } + + let currentRange = 0; + let nextTimestamp = currentTime + 0.5; + + while (currentRange < noMotionRanges.length) { + const [start, end] = noMotionRanges[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, noMotionRanges, motionOnly]); + + return nextTimestamp; +} diff --git a/web/src/hooks/use-draggable-element.ts b/web/src/hooks/use-draggable-element.ts index 49499900f..1457dd480 100644 --- a/web/src/hooks/use-draggable-element.ts +++ b/web/src/hooks/use-draggable-element.ts @@ -235,7 +235,7 @@ function useDraggableElement({ const elementEarliest = draggableElementEarliestTime ? timestampToPixels(draggableElementEarliestTime) : segmentHeight * (timelineDuration / segmentDuration) - - segmentHeight * 3; + segmentHeight * 3.5; // top of timeline - default 2 segments added for draggableElement visibility const elementLatest = draggableElementLatestTime diff --git a/web/src/pages/UIPlayground.tsx b/web/src/pages/UIPlayground.tsx index bec9828c3..62f47fb57 100644 --- a/web/src/pages/UIPlayground.tsx +++ b/web/src/pages/UIPlayground.tsx @@ -77,10 +77,12 @@ function generateRandomMotionAudioData(): MotionData[] { ) { const motion = Math.floor(Math.random() * 101); // Random number between 0 and 100 const audio = Math.random() * -100; // Random negative value between -100 and 0 + const camera = "test_camera"; data.push({ start_time: startTimestamp, motion, audio, + camera, }); } diff --git a/web/src/types/review.ts b/web/src/types/review.ts index e88174e14..ffd33b620 100644 --- a/web/src/types/review.ts +++ b/web/src/types/review.ts @@ -46,4 +46,5 @@ export type MotionData = { start_time: number; motion?: number; audio?: number; + camera: string; }; diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 2dfb957d7..0f5561b92 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -40,6 +40,7 @@ import SummaryTimeline from "@/components/timeline/SummaryTimeline"; import { RecordingStartingPoint } from "@/types/record"; import VideoControls from "@/components/player/VideoControls"; import { TimeRange } from "@/types/timeline"; +import { useCameraMotionNextTimestamp } from "@/hooks/use-camera-activity"; type EventViewProps = { reviews?: ReviewSegment[]; @@ -720,86 +721,14 @@ function MotionReview({ const [playbackRate, setPlaybackRate] = useState(8); const [controlsOpen, setControlsOpen] = useState(false); - const noMotionRanges = 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 (!noMotionRanges) { - return; - } - let currentRange = 0; - let nextTimestamp = currentTime + 0.5; - - while (currentRange < noMotionRanges.length) { - const [start, end] = noMotionRanges[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, noMotionRanges]); + const nextTimestamp = useCameraMotionNextTimestamp( + timeRangeSegments.end, + segmentDuration, + motionOnly, + reviewItems?.all ?? [], + motionData ?? [], + currentTime, + ); const timeoutIdRef = useRef(null); @@ -832,24 +761,42 @@ function MotionReview({ const getDetectionType = useCallback( (cameraName: string) => { if (motionOnly) { - return null; - } - const segmentStartTime = alignStartDateToTimeline(currentTime); - const segmentEndTime = segmentStartTime + segmentDuration; - const matchingItem = reviewItems?.all.find( - (item) => - ((item.start_time >= segmentStartTime && - item.start_time < segmentEndTime) || - (item.end_time > segmentStartTime && - item.end_time <= segmentEndTime) || - (item.start_time <= segmentStartTime && - item.end_time >= segmentEndTime)) && - item.camera === cameraName, - ); + const segmentStartTime = alignStartDateToTimeline(currentTime); + const segmentEndTime = segmentStartTime + segmentDuration; + const matchingItem = motionData?.find((item) => { + const cameras = item.camera.split(",").map((camera) => camera.trim()); + return ( + item.start_time >= segmentStartTime && + item.start_time < segmentEndTime && + cameras.includes(cameraName) + ); + }); - return matchingItem ? matchingItem.severity : null; + return matchingItem ? "significant_motion" : null; + } else { + const segmentStartTime = alignStartDateToTimeline(currentTime); + const segmentEndTime = segmentStartTime + segmentDuration; + const matchingItem = reviewItems?.all.find( + (item) => + ((item.start_time >= segmentStartTime && + item.start_time < segmentEndTime) || + (item.end_time > segmentStartTime && + item.end_time <= segmentEndTime) || + (item.start_time <= segmentStartTime && + item.end_time >= segmentEndTime)) && + item.camera === cameraName, + ); + + return matchingItem ? matchingItem.severity : null; + } }, - [reviewItems, currentTime, motionOnly, alignStartDateToTimeline], + [ + reviewItems, + motionData, + currentTime, + motionOnly, + alignStartDateToTimeline, + ], ); if (!relevantPreviews) {