Motion review changes (#10667)

* Add outlines in motion only mode

* fix playground
This commit is contained in:
Josh Hawkins 2024-03-25 11:19:55 -05:00 committed by GitHub
parent 258cd5b6d7
commit 51db63e42b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 170 additions and 97 deletions

View File

@ -4,7 +4,9 @@ import {
useMotionActivity, useMotionActivity,
} from "@/api/ws"; } from "@/api/ws";
import { CameraConfig } from "@/types/frigateConfig"; import { CameraConfig } from "@/types/frigateConfig";
import { MotionData, ReviewSegment } from "@/types/review";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTimelineUtils } from "./use-timeline-utils";
type useCameraActivityReturn = { type useCameraActivityReturn = {
activeTracking: boolean; activeTracking: boolean;
@ -66,3 +68,124 @@ export function useCameraActivity(
: false, : 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;
}

View File

@ -235,7 +235,7 @@ function useDraggableElement({
const elementEarliest = draggableElementEarliestTime const elementEarliest = draggableElementEarliestTime
? timestampToPixels(draggableElementEarliestTime) ? timestampToPixels(draggableElementEarliestTime)
: segmentHeight * (timelineDuration / segmentDuration) - : segmentHeight * (timelineDuration / segmentDuration) -
segmentHeight * 3; segmentHeight * 3.5;
// top of timeline - default 2 segments added for draggableElement visibility // top of timeline - default 2 segments added for draggableElement visibility
const elementLatest = draggableElementLatestTime const elementLatest = draggableElementLatestTime

View File

@ -77,10 +77,12 @@ function generateRandomMotionAudioData(): MotionData[] {
) { ) {
const motion = Math.floor(Math.random() * 101); // Random number between 0 and 100 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 audio = Math.random() * -100; // Random negative value between -100 and 0
const camera = "test_camera";
data.push({ data.push({
start_time: startTimestamp, start_time: startTimestamp,
motion, motion,
audio, audio,
camera,
}); });
} }

View File

@ -46,4 +46,5 @@ export type MotionData = {
start_time: number; start_time: number;
motion?: number; motion?: number;
audio?: number; audio?: number;
camera: string;
}; };

View File

@ -40,6 +40,7 @@ import SummaryTimeline from "@/components/timeline/SummaryTimeline";
import { RecordingStartingPoint } from "@/types/record"; import { RecordingStartingPoint } from "@/types/record";
import VideoControls from "@/components/player/VideoControls"; import VideoControls from "@/components/player/VideoControls";
import { TimeRange } from "@/types/timeline"; import { TimeRange } from "@/types/timeline";
import { useCameraMotionNextTimestamp } from "@/hooks/use-camera-activity";
type EventViewProps = { type EventViewProps = {
reviews?: ReviewSegment[]; reviews?: ReviewSegment[];
@ -720,87 +721,15 @@ function MotionReview({
const [playbackRate, setPlaybackRate] = useState(8); const [playbackRate, setPlaybackRate] = useState(8);
const [controlsOpen, setControlsOpen] = useState(false); const [controlsOpen, setControlsOpen] = useState(false);
const noMotionRanges = useMemo(() => { const nextTimestamp = useCameraMotionNextTimestamp(
if (!motionData || !reviewItems) { timeRangeSegments.end,
return; segmentDuration,
} motionOnly,
reviewItems?.all ?? [],
if (!motionOnly) { motionData ?? [],
return []; currentTime,
}
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 timeoutIdRef = useRef<NodeJS.Timeout | null>(null); const timeoutIdRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
@ -832,8 +761,19 @@ function MotionReview({
const getDetectionType = useCallback( const getDetectionType = useCallback(
(cameraName: string) => { (cameraName: string) => {
if (motionOnly) { if (motionOnly) {
return null; 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 ? "significant_motion" : null;
} else {
const segmentStartTime = alignStartDateToTimeline(currentTime); const segmentStartTime = alignStartDateToTimeline(currentTime);
const segmentEndTime = segmentStartTime + segmentDuration; const segmentEndTime = segmentStartTime + segmentDuration;
const matchingItem = reviewItems?.all.find( const matchingItem = reviewItems?.all.find(
@ -848,8 +788,15 @@ function MotionReview({
); );
return matchingItem ? matchingItem.severity : null; return matchingItem ? matchingItem.severity : null;
}
}, },
[reviewItems, currentTime, motionOnly, alignStartDateToTimeline], [
reviewItems,
motionData,
currentTime,
motionOnly,
alignStartDateToTimeline,
],
); );
if (!relevantPreviews) { if (!relevantPreviews) {