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,