mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-01-21 00:06:44 +01:00
Consolidate recordings view into one (#10496)
This commit is contained in:
parent
4e8600a0ef
commit
1983de6528
@ -11,13 +11,9 @@ import {
|
||||
ReviewSummary,
|
||||
} from "@/types/review";
|
||||
import EventView from "@/views/events/EventView";
|
||||
import {
|
||||
DesktopRecordingView,
|
||||
MobileRecordingView,
|
||||
} from "@/views/events/RecordingView";
|
||||
import { RecordingView } from "@/views/events/RecordingView";
|
||||
import axios from "axios";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function Events() {
|
||||
@ -319,21 +315,8 @@ export default function Events() {
|
||||
}
|
||||
|
||||
if (selectedReviewData) {
|
||||
if (isMobile) {
|
||||
return (
|
||||
<MobileRecordingView
|
||||
reviewItems={selectedReviewData.cameraSegments}
|
||||
startCamera={selectedReviewData.camera}
|
||||
startTime={selectedReviewData.start_time}
|
||||
allCameras={selectedReviewData.allCameras}
|
||||
severity={selectedReviewData.severity}
|
||||
relevantPreviews={allPreviews}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DesktopRecordingView
|
||||
<RecordingView
|
||||
startCamera={selectedReviewData.camera}
|
||||
startTime={selectedReviewData.start_time}
|
||||
allCameras={selectedReviewData.allCameras}
|
||||
|
@ -18,13 +18,14 @@ import { Preview } from "@/types/preview";
|
||||
import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||
import { getChunkedTimeDay } from "@/utils/timelineUtil";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { isDesktop, isMobile } from "react-device-detect";
|
||||
import { IoMdArrowRoundBack } from "react-icons/io";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
|
||||
const SEGMENT_DURATION = 30;
|
||||
|
||||
type DesktopRecordingViewProps = {
|
||||
type RecordingViewProps = {
|
||||
startCamera: string;
|
||||
startTime: number;
|
||||
severity: ReviewSeverity;
|
||||
@ -32,14 +33,14 @@ type DesktopRecordingViewProps = {
|
||||
allCameras: string[];
|
||||
allPreviews?: Preview[];
|
||||
};
|
||||
export function DesktopRecordingView({
|
||||
export function RecordingView({
|
||||
startCamera,
|
||||
startTime,
|
||||
severity,
|
||||
reviewItems,
|
||||
allCameras,
|
||||
allPreviews,
|
||||
}: DesktopRecordingViewProps) {
|
||||
}: RecordingViewProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const navigate = useNavigate();
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -207,15 +208,49 @@ export function DesktopRecordingView({
|
||||
<IoMdArrowRoundBack className="size-5 mr-[10px]" />
|
||||
Back
|
||||
</Button>
|
||||
{isMobile && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="absolute top-0 right-0 rounded-lg capitalize">
|
||||
{mainCamera.replaceAll("_", " ")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={mainCamera}
|
||||
onValueChange={(cam) => {
|
||||
setPlaybackStart(currentTime);
|
||||
setMainCamera(cam);
|
||||
}}
|
||||
>
|
||||
{allCameras.map((cam) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={cam}
|
||||
className="capitalize"
|
||||
value={cam}
|
||||
>
|
||||
{cam.replaceAll("_", " ")}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
<div className="flex h-full justify-center overflow-hidden">
|
||||
<div
|
||||
className={`flex h-full justify-center overflow-hidden ${isDesktop ? "" : "flex-col pt-12"}`}
|
||||
>
|
||||
<div className="flex flex-1 flex-wrap">
|
||||
<div
|
||||
className={`size-full flex px-2 items-center ${mainCameraAspect == "tall" ? "flex-row justify-evenly" : "flex-col justify-center"}`}
|
||||
>
|
||||
<div
|
||||
key={mainCamera}
|
||||
className={`flex justify-center items mb-5 ${mainCameraAspect == "tall" ? "h-[96%]" : "w-[82%]"}`}
|
||||
className={
|
||||
isDesktop
|
||||
? `flex justify-center items mb-5 ${mainCameraAspect == "tall" ? "h-[96%]" : "w-[82%]"}`
|
||||
: `w-full ${mainCameraAspect == "wide" ? "" : "aspect-video"}`
|
||||
}
|
||||
>
|
||||
<DynamicVideoPlayer
|
||||
className={grow}
|
||||
@ -237,6 +272,7 @@ export function DesktopRecordingView({
|
||||
isScrubbing={scrubbing}
|
||||
/>
|
||||
</div>
|
||||
{isDesktop && (
|
||||
<div
|
||||
className={`flex justify-center gap-2 ${mainCameraAspect == "tall" ? "h-full flex-col overflow-y-auto items-center" : "w-full overflow-x-auto"}`}
|
||||
>
|
||||
@ -264,10 +300,17 @@ export function DesktopRecordingView({
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[55px] md:w-[100px] mt-2 overflow-y-auto no-scrollbar">
|
||||
<div
|
||||
className={
|
||||
isDesktop
|
||||
? "w-[100px] mt-2 overflow-y-auto no-scrollbar"
|
||||
: "flex-grow overflow-hidden"
|
||||
}
|
||||
>
|
||||
{severity != "significant_motion" ? (
|
||||
<EventReviewTimeline
|
||||
segmentDuration={30}
|
||||
@ -303,240 +346,3 @@ export function DesktopRecordingView({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type MobileRecordingViewProps = {
|
||||
startCamera: string;
|
||||
startTime: number;
|
||||
severity: ReviewSeverity;
|
||||
reviewItems: ReviewSegment[];
|
||||
relevantPreviews?: Preview[];
|
||||
allCameras: string[];
|
||||
};
|
||||
export function MobileRecordingView({
|
||||
startCamera,
|
||||
startTime,
|
||||
severity,
|
||||
reviewItems,
|
||||
relevantPreviews,
|
||||
allCameras,
|
||||
}: MobileRecordingViewProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const navigate = useNavigate();
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// controller state
|
||||
|
||||
const controllerRef = useRef<DynamicVideoController | undefined>(undefined);
|
||||
const [playbackCamera, setPlaybackCamera] = useState(startCamera);
|
||||
const [playbackStart, setPlaybackStart] = useState(startTime);
|
||||
|
||||
const grow = useMemo(() => {
|
||||
if (!config) {
|
||||
return "aspect-video";
|
||||
}
|
||||
|
||||
const aspectRatio =
|
||||
config.cameras[playbackCamera].detect.width /
|
||||
config.cameras[playbackCamera].detect.height;
|
||||
if (aspectRatio > 2) {
|
||||
return "aspect-wide";
|
||||
} else {
|
||||
return "aspect-video";
|
||||
}
|
||||
}, [config, playbackCamera]);
|
||||
|
||||
// timeline time
|
||||
|
||||
const timeRange = useMemo(() => getChunkedTimeDay(startTime), [startTime]);
|
||||
const [selectedRangeIdx, setSelectedRangeIdx] = useState(
|
||||
timeRange.ranges.findIndex((chunk) => {
|
||||
return chunk.start <= startTime && chunk.end >= startTime;
|
||||
}),
|
||||
);
|
||||
const currentTimeRange = useMemo(
|
||||
() => timeRange.ranges[selectedRangeIdx],
|
||||
[selectedRangeIdx, timeRange],
|
||||
);
|
||||
|
||||
const mainCameraReviewItems = useMemo(
|
||||
() => reviewItems.filter((cam) => cam.camera == playbackCamera),
|
||||
[reviewItems, playbackCamera],
|
||||
);
|
||||
|
||||
// handle clip change
|
||||
|
||||
const onClipEnded = useCallback(() => {
|
||||
if (!controllerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedRangeIdx < timeRange.ranges.length - 1) {
|
||||
setSelectedRangeIdx(selectedRangeIdx + 1);
|
||||
}
|
||||
}, [selectedRangeIdx, timeRange]);
|
||||
|
||||
// scrubbing and timeline state
|
||||
|
||||
const [scrubbing, setScrubbing] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState<number>(startTime);
|
||||
const [playerTime, setPlayerTime] = useState(startTime);
|
||||
|
||||
const updateSelectedSegment = useCallback(
|
||||
(currentTime: number, updateStartTime: boolean) => {
|
||||
const index = timeRange.ranges.findIndex(
|
||||
(seg) => seg.start <= currentTime && seg.end >= currentTime,
|
||||
);
|
||||
|
||||
if (index != -1) {
|
||||
if (updateStartTime) {
|
||||
setPlaybackStart(currentTime);
|
||||
}
|
||||
|
||||
setSelectedRangeIdx(index);
|
||||
}
|
||||
},
|
||||
[timeRange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrubbing) {
|
||||
if (
|
||||
currentTime > currentTimeRange.end + 60 ||
|
||||
currentTime < currentTimeRange.start - 60
|
||||
) {
|
||||
updateSelectedSegment(currentTime, false);
|
||||
return;
|
||||
}
|
||||
|
||||
controllerRef.current?.scrubToTimestamp(currentTime);
|
||||
}
|
||||
}, [
|
||||
currentTime,
|
||||
scrubbing,
|
||||
timeRange,
|
||||
currentTimeRange,
|
||||
updateSelectedSegment,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrubbing) {
|
||||
if (Math.abs(currentTime - playerTime) > 10) {
|
||||
if (
|
||||
currentTimeRange.start <= currentTime &&
|
||||
currentTimeRange.end >= currentTime
|
||||
) {
|
||||
controllerRef.current?.seekToTimestamp(currentTime, true);
|
||||
} else {
|
||||
updateSelectedSegment(currentTime, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// we only want to seek when current time doesn't match the player update time
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentTime, scrubbing]);
|
||||
|
||||
// motion timeline data
|
||||
|
||||
const { data: motionData } = useSWR<MotionData[]>(
|
||||
severity == "significant_motion"
|
||||
? [
|
||||
"review/activity/motion",
|
||||
{
|
||||
before: timeRange.end,
|
||||
after: timeRange.start,
|
||||
scale: SEGMENT_DURATION / 2,
|
||||
cameras: playbackCamera,
|
||||
},
|
||||
]
|
||||
: null,
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={contentRef} className="flex flex-col relative w-full h-full">
|
||||
<div className="flex justify-evenly items-center p-2">
|
||||
<Button className="rounded-lg" onClick={() => navigate(-1)}>
|
||||
<IoMdArrowRoundBack className="size-5 mr-[10px]" />
|
||||
Back
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="capitalize">
|
||||
{playbackCamera.replaceAll("_", " ")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={playbackCamera}
|
||||
onValueChange={(cam) => {
|
||||
setPlaybackStart(currentTime);
|
||||
setPlaybackCamera(cam);
|
||||
}}
|
||||
>
|
||||
{allCameras.map((cam) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={cam}
|
||||
className="capitalize"
|
||||
value={cam}
|
||||
>
|
||||
{cam.replaceAll("_", " ")}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DynamicVideoPlayer
|
||||
className={`w-full ${grow}`}
|
||||
camera={playbackCamera}
|
||||
timeRange={currentTimeRange}
|
||||
cameraPreviews={relevantPreviews || []}
|
||||
startTimestamp={playbackStart}
|
||||
onControllerReady={(controller) => {
|
||||
controllerRef.current = controller;
|
||||
}}
|
||||
onTimestampUpdate={(timestamp) => {
|
||||
setPlayerTime(timestamp);
|
||||
setCurrentTime(timestamp);
|
||||
}}
|
||||
onClipEnded={onClipEnded}
|
||||
isScrubbing={scrubbing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow overflow-hidden">
|
||||
{severity != "significant_motion" ? (
|
||||
<EventReviewTimeline
|
||||
segmentDuration={30}
|
||||
timestampSpread={15}
|
||||
timelineStart={timeRange.end}
|
||||
timelineEnd={timeRange.start}
|
||||
showHandlebar
|
||||
handlebarTime={currentTime}
|
||||
setHandlebarTime={setCurrentTime}
|
||||
events={mainCameraReviewItems}
|
||||
severityType={severity}
|
||||
contentRef={contentRef}
|
||||
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
|
||||
/>
|
||||
) : (
|
||||
<MotionReviewTimeline
|
||||
segmentDuration={30}
|
||||
timestampSpread={15}
|
||||
timelineStart={timeRange.end}
|
||||
timelineEnd={timeRange.start}
|
||||
showHandlebar
|
||||
handlebarTime={currentTime}
|
||||
setHandlebarTime={setCurrentTime}
|
||||
events={mainCameraReviewItems}
|
||||
motion_events={motionData ?? []}
|
||||
severityType={severity}
|
||||
contentRef={contentRef}
|
||||
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user