add motion data to timelines and allow motion cameas to be selectable

This commit is contained in:
Nicolas Mowen 2024-03-05 06:21:08 -07:00
parent 435aab09be
commit 9abeb54657
3 changed files with 145 additions and 57 deletions

View File

@ -240,22 +240,37 @@ export default function Events() {
// selected items // selected items
const selectedData = useMemo(() => { const selectedReviewData = useMemo(() => {
if (!config) { if (!config) {
return undefined; return undefined;
} }
if (!selectedReviewId) {
return undefined;
}
if (!reviewPages) { if (!reviewPages) {
return undefined; return undefined;
} }
if (!selectedReviewId) {
return undefined;
}
const allCameras = reviewFilter?.cameras ?? Object.keys(config.cameras); const allCameras = reviewFilter?.cameras ?? Object.keys(config.cameras);
const allReviews = reviewPages.flat(); const allReviews = reviewPages.flat();
if (selectedReviewId.startsWith("motion")) {
const motionData = selectedReviewId.split(",");
// format is motion,camera,start_time
return {
camera: motionData[1],
severity: "significant_motion" as ReviewSeverity,
start_time: parseFloat(motionData[2]),
allCameras: allCameras,
cameraSegments: allReviews.filter((seg) =>
allCameras.includes(seg.camera),
),
};
}
const selectedReview = allReviews.find( const selectedReview = allReviews.find(
(item) => item.id == selectedReviewId, (item) => item.id == selectedReviewId,
); );
@ -265,7 +280,9 @@ export default function Events() {
} }
return { return {
selected: selectedReview, camera: selectedReview.camera,
severity: selectedReview.severity,
start_time: selectedReview.start_time,
allCameras: allCameras, allCameras: allCameras,
cameraSegments: allReviews.filter((seg) => cameraSegments: allReviews.filter((seg) =>
allCameras.includes(seg.camera), allCameras.includes(seg.camera),
@ -280,12 +297,14 @@ export default function Events() {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
if (selectedData) { if (selectedReviewData) {
if (isMobile) { if (isMobile) {
return ( return (
<MobileRecordingView <MobileRecordingView
reviewItems={selectedData.cameraSegments} reviewItems={selectedReviewData.cameraSegments}
selectedReview={selectedData.selected} startCamera={selectedReviewData.camera}
startTime={selectedReviewData.start_time}
severity={selectedReviewData.severity}
relevantPreviews={allPreviews} relevantPreviews={allPreviews}
/> />
); );
@ -293,11 +312,11 @@ export default function Events() {
return ( return (
<DesktopRecordingView <DesktopRecordingView
startCamera={selectedData.selected.camera} startCamera={selectedReviewData.camera}
startTime={selectedData.selected.start_time} startTime={selectedReviewData.start_time}
allCameras={selectedData.allCameras} allCameras={selectedReviewData.allCameras}
severity={selectedData.selected.severity} severity={selectedReviewData.severity}
reviewItems={selectedData.cameraSegments} reviewItems={selectedReviewData.cameraSegments}
allPreviews={allPreviews} allPreviews={allPreviews}
/> />
); );

View File

@ -286,6 +286,7 @@ export default function EventView({
relevantPreviews={relevantPreviews} relevantPreviews={relevantPreviews}
timeRange={timeRange} timeRange={timeRange}
filter={filter} filter={filter}
onSelectReview={onSelectReview}
/> />
)} )}
</div> </div>
@ -528,6 +529,7 @@ type MotionReviewProps = {
relevantPreviews?: Preview[]; relevantPreviews?: Preview[];
timeRange: { before: number; after: number }; timeRange: { before: number; after: number };
filter?: ReviewFilter; filter?: ReviewFilter;
onSelectReview: (data: string) => void;
}; };
function MotionReview({ function MotionReview({
contentRef, contentRef,
@ -535,6 +537,7 @@ function MotionReview({
relevantPreviews, relevantPreviews,
timeRange, timeRange,
filter, filter,
onSelectReview,
}: MotionReviewProps) { }: MotionReviewProps) {
const segmentDuration = 30; const segmentDuration = 30;
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -652,6 +655,9 @@ function MotionReview({
videoPlayersRef.current[camera.name] = controller; videoPlayersRef.current[camera.name] = controller;
setPlayerReady(true); setPlayerReady(true);
}} }}
onClick={() =>
onSelectReview(`motion,${camera.name},${currentTime}`)
}
/> />
); );
})} })}

View File

@ -2,13 +2,17 @@ import DynamicVideoPlayer, {
DynamicVideoController, DynamicVideoController,
} from "@/components/player/DynamicVideoPlayer"; } from "@/components/player/DynamicVideoPlayer";
import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; import EventReviewTimeline from "@/components/timeline/EventReviewTimeline";
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Preview } from "@/types/preview"; import { Preview } from "@/types/preview";
import { ReviewSegment, ReviewSeverity } from "@/types/review"; import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review";
import { getChunkedTimeDay } from "@/utils/timelineUtil"; import { getChunkedTimeDay } from "@/utils/timelineUtil";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { IoMdArrowRoundBack } from "react-icons/io"; import { IoMdArrowRoundBack } from "react-icons/io";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import useSWR from "swr";
const SEGMENT_DURATION = 30;
type DesktopRecordingViewProps = { type DesktopRecordingViewProps = {
startCamera: string; startCamera: string;
@ -116,6 +120,21 @@ export function DesktopRecordingView({
[allCameras, currentTime, mainCamera], [allCameras, currentTime, mainCamera],
); );
// motion timeline data
const { data: motionData } = useSWR<MotionData[]>(
severity == "significant_motion"
? [
"review/activity",
{
before: timeRange.end,
after: timeRange.start,
scale: SEGMENT_DURATION / 2,
},
]
: null,
);
return ( return (
<div ref={contentRef} className="relative size-full"> <div ref={contentRef} className="relative size-full">
<Button <Button
@ -181,6 +200,7 @@ export function DesktopRecordingView({
</div> </div>
<div className="absolute overflow-hidden w-56 inset-y-0 right-0"> <div className="absolute overflow-hidden w-56 inset-y-0 right-0">
{severity != "significant_motion" ? (
<EventReviewTimeline <EventReviewTimeline
segmentDuration={30} segmentDuration={30}
timestampSpread={15} timestampSpread={15}
@ -194,18 +214,38 @@ export function DesktopRecordingView({
contentRef={contentRef} contentRef={contentRef}
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)} onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
/> />
) : (
<MotionReviewTimeline
segmentDuration={30}
timestampSpread={15}
timelineStart={timeRange.end}
timelineEnd={timeRange.start}
showHandlebar
handlebarTime={currentTime}
setHandlebarTime={setCurrentTime}
events={reviewItems}
motion_events={motionData ?? []}
severityType={severity}
contentRef={contentRef}
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
/>
)}
</div> </div>
</div> </div>
); );
} }
type MobileRecordingViewProps = { type MobileRecordingViewProps = {
selectedReview: ReviewSegment; startCamera: string;
startTime: number;
severity: ReviewSeverity;
reviewItems: ReviewSegment[]; reviewItems: ReviewSegment[];
relevantPreviews?: Preview[]; relevantPreviews?: Preview[];
}; };
export function MobileRecordingView({ export function MobileRecordingView({
selectedReview, startCamera,
startTime,
severity,
reviewItems, reviewItems,
relevantPreviews, relevantPreviews,
}: MobileRecordingViewProps) { }: MobileRecordingViewProps) {
@ -219,16 +259,10 @@ export function MobileRecordingView({
// timeline time // timeline time
const timeRange = useMemo( const timeRange = useMemo(() => getChunkedTimeDay(startTime), [startTime]);
() => getChunkedTimeDay(selectedReview.start_time),
[selectedReview],
);
const [selectedRangeIdx, setSelectedRangeIdx] = useState( const [selectedRangeIdx, setSelectedRangeIdx] = useState(
timeRange.ranges.findIndex((chunk) => { timeRange.ranges.findIndex((chunk) => {
return ( return chunk.start <= startTime && chunk.end >= startTime;
chunk.start <= selectedReview.start_time &&
chunk.end >= selectedReview.start_time
);
}), }),
); );
@ -251,7 +285,7 @@ export function MobileRecordingView({
const [scrubbing, setScrubbing] = useState(false); const [scrubbing, setScrubbing] = useState(false);
const [currentTime, setCurrentTime] = useState<number>( const [currentTime, setCurrentTime] = useState<number>(
selectedReview?.start_time || Date.now() / 1000, startTime || Date.now() / 1000,
); );
useEffect(() => { useEffect(() => {
@ -269,6 +303,21 @@ export function MobileRecordingView({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [scrubbing]); }, [scrubbing]);
// motion timeline data
const { data: motionData } = useSWR<MotionData[]>(
severity == "significant_motion"
? [
"review/activity",
{
before: timeRange.end,
after: timeRange.start,
scale: SEGMENT_DURATION / 2,
},
]
: null,
);
return ( return (
<div ref={contentRef} className="flex flex-col relative w-full h-full"> <div ref={contentRef} className="flex flex-col relative w-full h-full">
<Button className="rounded-lg" onClick={() => navigate(-1)}> <Button className="rounded-lg" onClick={() => navigate(-1)}>
@ -278,7 +327,7 @@ export function MobileRecordingView({
<div> <div>
<DynamicVideoPlayer <DynamicVideoPlayer
camera={selectedReview.camera} camera={startCamera}
timeRange={timeRange.ranges[selectedRangeIdx]} timeRange={timeRange.ranges[selectedRangeIdx]}
cameraPreviews={relevantPreviews || []} cameraPreviews={relevantPreviews || []}
onControllerReady={(controller) => { onControllerReady={(controller) => {
@ -288,15 +337,13 @@ export function MobileRecordingView({
setCurrentTime(timestamp); setCurrentTime(timestamp);
}); });
controllerRef.current?.seekToTimestamp( controllerRef.current?.seekToTimestamp(startTime, true);
selectedReview.start_time,
true,
);
}} }}
/> />
</div> </div>
<div className="flex-grow overflow-hidden"> <div className="flex-grow overflow-hidden">
{severity != "significant_motion" ? (
<EventReviewTimeline <EventReviewTimeline
segmentDuration={30} segmentDuration={30}
timestampSpread={15} timestampSpread={15}
@ -306,10 +353,26 @@ export function MobileRecordingView({
handlebarTime={currentTime} handlebarTime={currentTime}
setHandlebarTime={setCurrentTime} setHandlebarTime={setCurrentTime}
events={reviewItems} events={reviewItems}
severityType={selectedReview.severity} severityType={severity}
contentRef={contentRef} contentRef={contentRef}
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)} onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
/> />
) : (
<MotionReviewTimeline
segmentDuration={30}
timestampSpread={15}
timelineStart={timeRange.end}
timelineEnd={timeRange.start}
showHandlebar
handlebarTime={currentTime}
setHandlebarTime={setCurrentTime}
events={reviewItems}
motion_events={motionData ?? []}
severityType={severity}
contentRef={contentRef}
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
/>
)}
</div> </div>
</div> </div>
); );