mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-31 16:12:19 +03:00
Timeline improvements (#16429)
* virtualize event segments * use virtual segments in event review timeline * add segmentkey to props * virtualize motion segments * use virtual segments in motion review timeline * update draggable element hook to use only math * timeline zooming hook * add zooming to event review timeline * update playground * zoomable timeline on recording view * consolidate divs in summary timeline * only calculate motion data for visible motion segments * use swr loading state * fix motion only * keep handlebar centered when zooming * zoom animations * clean up * ensure motion only checks both halves of segment * prevent handlebar jump when using motion only mode
This commit is contained in:
@@ -53,6 +53,7 @@ import { FilterList, LAST_24_HOURS_KEY } from "@/types/filter";
|
||||
import { GiSoundWaves } from "react-icons/gi";
|
||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||
import ReviewDetailDialog from "@/components/overlay/detail/ReviewDetailDialog";
|
||||
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
|
||||
|
||||
type EventViewProps = {
|
||||
reviewItems?: SegmentedReviewData;
|
||||
@@ -461,8 +462,6 @@ function DetectionReview({
|
||||
}: DetectionReviewProps) {
|
||||
const reviewTimelineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const segmentDuration = 60;
|
||||
|
||||
// detail
|
||||
|
||||
const [reviewDetail, setReviewDetail] = useState<ReviewSegment>();
|
||||
@@ -492,9 +491,38 @@ function DetectionReview({
|
||||
[timeRange],
|
||||
);
|
||||
|
||||
const [zoomSettings, setZoomSettings] = useState({
|
||||
segmentDuration: 60,
|
||||
timestampSpread: 15,
|
||||
});
|
||||
|
||||
const possibleZoomLevels = useMemo(
|
||||
() => [
|
||||
{ segmentDuration: 60, timestampSpread: 15 },
|
||||
{ segmentDuration: 30, timestampSpread: 5 },
|
||||
{ segmentDuration: 10, timestampSpread: 1 },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleZoomChange = useCallback(
|
||||
(newZoomLevel: number) => {
|
||||
setZoomSettings(possibleZoomLevels[newZoomLevel]);
|
||||
},
|
||||
[possibleZoomLevels],
|
||||
);
|
||||
|
||||
const { isZooming, zoomDirection } = useTimelineZoom({
|
||||
zoomSettings,
|
||||
zoomLevels: possibleZoomLevels,
|
||||
onZoomChange: handleZoomChange,
|
||||
timelineRef: reviewTimelineRef,
|
||||
timelineDuration,
|
||||
});
|
||||
|
||||
const { alignStartDateToTimeline, getVisibleTimelineDuration } =
|
||||
useTimelineUtils({
|
||||
segmentDuration,
|
||||
segmentDuration: zoomSettings.segmentDuration,
|
||||
timelineDuration,
|
||||
timelineRef: reviewTimelineRef,
|
||||
});
|
||||
@@ -692,7 +720,7 @@ function DetectionReview({
|
||||
data-start={value.start_time}
|
||||
data-segment-start={
|
||||
alignStartDateToTimeline(value.start_time) -
|
||||
segmentDuration
|
||||
zoomSettings.segmentDuration
|
||||
}
|
||||
className="review-item relative rounded-lg"
|
||||
>
|
||||
@@ -749,13 +777,13 @@ function DetectionReview({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-[65px] flex-row md:w-[110px]">
|
||||
<div className="no-scrollbar w-[55px] overflow-y-auto md:w-[100px]">
|
||||
<div className="no-scrollbar w-[55px] md:w-[100px]">
|
||||
{loading ? (
|
||||
<Skeleton className="size-full" />
|
||||
) : (
|
||||
<EventReviewTimeline
|
||||
segmentDuration={segmentDuration}
|
||||
timestampSpread={15}
|
||||
segmentDuration={zoomSettings.segmentDuration}
|
||||
timestampSpread={zoomSettings.timestampSpread}
|
||||
timelineStart={timeRange.before}
|
||||
timelineEnd={timeRange.after}
|
||||
showMinimap={showMinimap && !previewTime}
|
||||
@@ -769,6 +797,8 @@ function DetectionReview({
|
||||
contentRef={contentRef}
|
||||
timelineRef={reviewTimelineRef}
|
||||
dense={isMobile}
|
||||
isZooming={isZooming}
|
||||
zoomDirection={zoomDirection}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -780,7 +810,7 @@ function DetectionReview({
|
||||
reviewTimelineRef={reviewTimelineRef}
|
||||
timelineStart={timeRange.before}
|
||||
timelineEnd={timeRange.after}
|
||||
segmentDuration={segmentDuration}
|
||||
segmentDuration={zoomSettings.segmentDuration}
|
||||
events={reviewItems?.all ?? []}
|
||||
severityType={severity}
|
||||
/>
|
||||
@@ -1095,7 +1125,6 @@ function MotionReview({
|
||||
setHandlebarTime={setCurrentTime}
|
||||
events={reviewItems?.all ?? []}
|
||||
motion_events={motionData ?? []}
|
||||
severityType="significant_motion"
|
||||
contentRef={contentRef}
|
||||
onHandlebarDraggingChange={(scrubbing) => {
|
||||
if (playing && scrubbing) {
|
||||
@@ -1105,6 +1134,8 @@ function MotionReview({
|
||||
setScrubbing(scrubbing);
|
||||
}}
|
||||
dense={isMobileOnly}
|
||||
isZooming={false}
|
||||
zoomDirection={null}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton className="size-full" />
|
||||
|
||||
@@ -46,8 +46,7 @@ import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useFullscreen } from "@/hooks/use-fullscreen";
|
||||
|
||||
const SEGMENT_DURATION = 30;
|
||||
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
|
||||
|
||||
type RecordingViewProps = {
|
||||
startCamera: string;
|
||||
@@ -633,6 +632,7 @@ export function RecordingView({
|
||||
|
||||
type TimelineProps = {
|
||||
contentRef: MutableRefObject<HTMLDivElement | null>;
|
||||
timelineRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
mainCamera: string;
|
||||
timelineType: TimelineType;
|
||||
timeRange: TimeRange;
|
||||
@@ -646,6 +646,7 @@ type TimelineProps = {
|
||||
};
|
||||
function Timeline({
|
||||
contentRef,
|
||||
timelineRef,
|
||||
mainCamera,
|
||||
timelineType,
|
||||
timeRange,
|
||||
@@ -657,12 +658,48 @@ function Timeline({
|
||||
setScrubbing,
|
||||
setExportRange,
|
||||
}: TimelineProps) {
|
||||
const { data: motionData } = useSWR<MotionData[]>([
|
||||
const internalTimelineRef = useRef<HTMLDivElement>(null);
|
||||
const selectedTimelineRef = timelineRef || internalTimelineRef;
|
||||
|
||||
// timeline interaction
|
||||
|
||||
const [zoomSettings, setZoomSettings] = useState({
|
||||
segmentDuration: 30,
|
||||
timestampSpread: 15,
|
||||
});
|
||||
|
||||
const possibleZoomLevels = useMemo(
|
||||
() => [
|
||||
{ segmentDuration: 30, timestampSpread: 15 },
|
||||
{ segmentDuration: 15, timestampSpread: 5 },
|
||||
{ segmentDuration: 5, timestampSpread: 1 },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleZoomChange = useCallback(
|
||||
(newZoomLevel: number) => {
|
||||
setZoomSettings(possibleZoomLevels[newZoomLevel]);
|
||||
},
|
||||
[possibleZoomLevels],
|
||||
);
|
||||
|
||||
const { isZooming, zoomDirection } = useTimelineZoom({
|
||||
zoomSettings,
|
||||
zoomLevels: possibleZoomLevels,
|
||||
onZoomChange: handleZoomChange,
|
||||
timelineRef: selectedTimelineRef,
|
||||
timelineDuration: timeRange.after - timeRange.before,
|
||||
});
|
||||
|
||||
// motion data
|
||||
|
||||
const { data: motionData, isLoading } = useSWR<MotionData[]>([
|
||||
"review/activity/motion",
|
||||
{
|
||||
before: timeRange.before,
|
||||
after: timeRange.after,
|
||||
scale: SEGMENT_DURATION / 2,
|
||||
scale: Math.round(zoomSettings.segmentDuration / 2),
|
||||
cameras: mainCamera,
|
||||
},
|
||||
]);
|
||||
@@ -695,10 +732,11 @@ function Timeline({
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-20 h-[30px] w-full bg-gradient-to-b from-secondary to-transparent"></div>
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-20 h-[30px] w-full bg-gradient-to-t from-secondary to-transparent"></div>
|
||||
{timelineType == "timeline" ? (
|
||||
motionData ? (
|
||||
!isLoading ? (
|
||||
<MotionReviewTimeline
|
||||
segmentDuration={30}
|
||||
timestampSpread={15}
|
||||
timelineRef={selectedTimelineRef}
|
||||
segmentDuration={zoomSettings.segmentDuration}
|
||||
timestampSpread={zoomSettings.timestampSpread}
|
||||
timelineStart={timeRange.before}
|
||||
timelineEnd={timeRange.after}
|
||||
showHandlebar={exportRange == undefined}
|
||||
@@ -711,9 +749,10 @@ function Timeline({
|
||||
setHandlebarTime={setCurrentTime}
|
||||
events={mainCameraReviewItems}
|
||||
motion_events={motionData ?? []}
|
||||
severityType="significant_motion"
|
||||
contentRef={contentRef}
|
||||
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
|
||||
isZooming={isZooming}
|
||||
zoomDirection={zoomDirection}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton className="size-full" />
|
||||
|
||||
Reference in New Issue
Block a user