From bb50b2b6f4b0c9d23c3635ec5b604bd467e1dfb5 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 23 Mar 2024 13:49:31 -0600 Subject: [PATCH] Respect motion only when playing back (#10632) * Respect motion only when playing back motion * Increase efficiency * Fix import --- .../overlay/TimelineDataOverlay.tsx | 1 + web/src/components/player/LivePlayer.tsx | 2 +- .../player/dynamic/DynamicVideoController.ts | 1 + .../player/dynamic/DynamicVideoPlayer.tsx | 1 + web/src/hooks/use-camera-activity.ts | 58 ++++++++++++++++++- web/src/types/timeline.ts | 11 +--- web/src/utils/timelineUtil.tsx | 1 + web/src/views/events/EventView.tsx | 23 ++++++-- 8 files changed, 82 insertions(+), 16 deletions(-) diff --git a/web/src/components/overlay/TimelineDataOverlay.tsx b/web/src/components/overlay/TimelineDataOverlay.tsx index 3fa9781e6..499cc0745 100644 --- a/web/src/components/overlay/TimelineDataOverlay.tsx +++ b/web/src/components/overlay/TimelineDataOverlay.tsx @@ -1,3 +1,4 @@ +import { Timeline } from "@/types/timeline"; import { useState } from "react"; type TimelineEventOverlayProps = { diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index 69ad60bd3..a511bcb5d 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -6,7 +6,7 @@ import { useEffect, useMemo, useState } from "react"; import MSEPlayer from "./MsePlayer"; import JSMpegPlayer from "./JSMpegPlayer"; import { MdCircle } from "react-icons/md"; -import useCameraActivity from "@/hooks/use-camera-activity"; +import { useCameraActivity } from "@/hooks/use-camera-activity"; import { useRecordingsState } from "@/api/ws"; import { LivePlayerMode } from "@/types/live"; import useCameraLiveMode from "@/hooks/use-camera-live-mode"; diff --git a/web/src/components/player/dynamic/DynamicVideoController.ts b/web/src/components/player/dynamic/DynamicVideoController.ts index ce40fa719..7258ddebe 100644 --- a/web/src/components/player/dynamic/DynamicVideoController.ts +++ b/web/src/components/player/dynamic/DynamicVideoController.ts @@ -1,6 +1,7 @@ import { Recording } from "@/types/record"; import { DynamicPlayback } from "@/types/playback"; import { PreviewController } from "../PreviewPlayer"; +import { Timeline } from "@/types/timeline"; type PlayerMode = "playback" | "scrubbing"; diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index 62f087cfb..1421954cd 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -8,6 +8,7 @@ import { Preview } from "@/types/preview"; import PreviewPlayer, { PreviewController } from "../PreviewPlayer"; import { DynamicVideoController } from "./DynamicVideoController"; import HlsVideoPlayer from "../HlsVideoPlayer"; +import { Timeline } from "@/types/timeline"; /** * Dynamically switches between video playback and scrubbing preview player. diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index b15697c78..01406d29a 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -4,6 +4,8 @@ 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 = { @@ -12,7 +14,7 @@ type useCameraActivityReturn = { activeAudio: boolean; }; -export default function useCameraActivity( +export function useCameraActivity( camera: CameraConfig, ): useCameraActivityReturn { const [activeObjects, setActiveObjects] = useState([]); @@ -66,3 +68,57 @@ export default 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/types/timeline.ts b/web/src/types/timeline.ts index 86b364686..b5e746206 100644 --- a/web/src/types/timeline.ts +++ b/web/src/types/timeline.ts @@ -1,4 +1,4 @@ -type Timeline = { +export type Timeline = { camera: string; timestamp: number; data: { @@ -23,11 +23,4 @@ type Timeline = { source: string; }; -// may be used in the future, keep for now for reference -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type HourlyTimeline = { - start: number; - end: number; - count: number; - hours: { [key: string]: Timeline[] }; -}; +export type TimeRange = { before: number; after: number }; diff --git a/web/src/utils/timelineUtil.tsx b/web/src/utils/timelineUtil.tsx index 0e0b1d104..5bc19f03d 100644 --- a/web/src/utils/timelineUtil.tsx +++ b/web/src/utils/timelineUtil.tsx @@ -21,6 +21,7 @@ import { } from "react-icons/md"; import { FaBicycle } from "react-icons/fa"; import { endOfHourOrCurrentTime } from "./dateUtil"; +import { Timeline } from "@/types/timeline"; export function getTimelineIcon(timelineItem: Timeline) { switch (timelineItem.class_type) { diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index f9b8e2d43..353760ff7 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -39,6 +39,8 @@ import PreviewPlayer, { 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[]; @@ -606,7 +608,7 @@ type MotionReviewProps = { significant_motion: ReviewSegment[]; }; relevantPreviews?: Preview[]; - timeRange: { before: number; after: number }; + timeRange: TimeRange; startTime?: number; filter?: ReviewFilter; motionOnly?: boolean; @@ -718,6 +720,12 @@ function MotionReview({ const [playbackRate, setPlaybackRate] = useState(8); const [controlsOpen, setControlsOpen] = useState(false); + const seekTimestamps = useCameraMotionTimestamps( + timeRange, + motionOnly, + reviewItems?.all ?? [], + motionData ?? [], + ); useEffect(() => { if (!playing) { @@ -725,17 +733,22 @@ function MotionReview({ } const interval = 500 / playbackRate; - const startTime = currentTime; + const startIdx = seekTimestamps.findIndex((time) => time > currentTime); + + if (!startIdx) { + return; + } + let counter = 0; const intervalId = setInterval(() => { - counter += 0.5; + counter += 1; - if (startTime + counter >= timeRange.before) { + if (startIdx + counter >= seekTimestamps.length) { setPlaying(false); return; } - setCurrentTime(startTime + counter); + setCurrentTime(seekTimestamps[startIdx + counter]); }, interval); return () => {