diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index f79263178..ef84eff17 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -6,13 +6,14 @@ import MobileEventView from "@/views/events/MobileEventView"; import axios from "axios"; import { useCallback, useEffect, useMemo, useState } from "react"; import { isMobile } from "react-device-detect"; +import useSWR from "swr"; import useSWRInfinite from "swr/infinite"; const API_LIMIT = 250; export default function Events() { // recordings viewer - const [selectedReview, setSelectedReview] = useOverlayState("review"); + const [selectedReviewId, setSelectedReviewId] = useOverlayState("review"); // review paging @@ -66,6 +67,34 @@ export default function Events() { [reviewPages] ); + // preview videos + + const previewTimes = useMemo(() => { + if ( + !reviewPages || + reviewPages.length == 0 || + reviewPages.at(-1)!!.length == 0 + ) { + return undefined; + } + + const startDate = new Date(); + startDate.setMinutes(0, 0, 0); + + const endDate = new Date(reviewPages.at(-1)!!.at(-1)!!.end_time); + endDate.setHours(0, 0, 0, 0); + return { + start: startDate.getTime() / 1000, + end: endDate.getTime() / 1000, + }; + }, [reviewPages]); + const { data: allPreviews } = useSWR( + previewTimes + ? `preview/all/start/${previewTimes.start}/end/${previewTimes.end}` + : null, + { revalidateOnFocus: false } + ); + // review status const markItemAsReviewed = useCallback( @@ -104,8 +133,42 @@ export default function Events() { [updateSegments] ); - if (selectedReview) { - return ; + // selected items + + const selectedReviews = useMemo(() => { + if (!selectedReviewId) { + return undefined; + } + + if (!reviewPages) { + return undefined; + } + + const allReviews = reviewPages.flat(); + const selectedReview = allReviews.find( + (item) => item.id == selectedReviewId + ); + + if (!selectedReview) { + return undefined; + } + + return { + selected: selectedReview, + cameraReviews: allReviews.filter( + (seg) => seg.camera == selectedReview?.camera + ), + }; + }, [selectedReviewId, reviewPages]); + + if (selectedReviews) { + return ( + + ); } else { if (isMobile) { return ( @@ -122,12 +185,13 @@ export default function Events() { return ( setSize(size + 1)} markItemAsReviewed={markItemAsReviewed} - onSelectReview={setSelectedReview} + onSelectReview={setSelectedReviewId} /> ); } diff --git a/web/src/utils/timelineUtil.tsx b/web/src/utils/timelineUtil.tsx index 483d63231..d380bbb51 100644 --- a/web/src/utils/timelineUtil.tsx +++ b/web/src/utils/timelineUtil.tsx @@ -20,6 +20,7 @@ import { MdOutlinePictureInPictureAlt, } from "react-icons/md"; import { FaBicycle } from "react-icons/fa"; +import { endOfHourOrCurrentTime } from "./dateUtil"; export function getTimelineIcon(timelineItem: Timeline) { switch (timelineItem.class_type) { @@ -118,3 +119,31 @@ export function getTimelineItemDescription(timelineItem: Timeline) { return `${label} detected`; } } + +export function getChunkedTimeRange(timestamp: number) { + const endOfThisHour = new Date(); + endOfThisHour.setHours(endOfThisHour.getHours() + 1, 0, 0, 0); + const data: { start: number; end: number }[] = []; + const startDay = new Date(timestamp * 1000); + startDay.setHours(0, 0, 0, 0); + const startTimestamp = startDay.getTime() / 1000; + let start = startDay.getTime() / 1000; + let end = 0; + + for (let i = 0; i < 24; i++) { + startDay.setHours(startDay.getHours() + 1); + + if (startDay > endOfThisHour) { + break; + } + + end = endOfHourOrCurrentTime(startDay.getTime() / 1000); + data.push({ + start, + end, + }); + start = startDay.getTime() / 1000; + } + + return { start: startTimestamp, end, ranges: data }; +} diff --git a/web/src/views/events/DesktopEventView.tsx b/web/src/views/events/DesktopEventView.tsx index f05855de3..fd6142131 100644 --- a/web/src/views/events/DesktopEventView.tsx +++ b/web/src/views/events/DesktopEventView.tsx @@ -19,6 +19,7 @@ import useSWR from "swr"; type DesktopEventViewProps = { reviewPages?: ReviewSegment[][]; + relevantPreviews?: Preview[]; timeRange: [number, number]; reachedEnd: boolean; isValidating: boolean; @@ -28,6 +29,7 @@ type DesktopEventViewProps = { }; export default function DesktopEventView({ reviewPages, + relevantPreviews, timeRange, reachedEnd, isValidating, @@ -166,34 +168,6 @@ export default function DesktopEventView({ return data; }, [minimap]); - // preview videos - - const previewTimes = useMemo(() => { - if ( - !reviewPages || - reviewPages.length == 0 || - reviewPages.at(-1)!!.length == 0 - ) { - return undefined; - } - - const startDate = new Date(); - startDate.setMinutes(0, 0, 0); - - const endDate = new Date(reviewPages.at(-1)!!.at(-1)!!.end_time); - endDate.setHours(0, 0, 0, 0); - return { - start: startDate.getTime() / 1000, - end: endDate.getTime() / 1000, - }; - }, [reviewPages]); - const { data: allPreviews } = useSWR( - previewTimes - ? `preview/all/start/${previewTimes.start}/end/${previewTimes.end}` - : null, - { revalidateOnFocus: false } - ); - if (!config) { return ; } @@ -258,7 +232,7 @@ export default function DesktopEventView({ {currentItems ? ( currentItems.map((value, segIdx) => { const lastRow = segIdx == reviewItems[severity].length - 1; - const relevantPreview = Object.values(allPreviews || []).find( + const relevantPreview = Object.values(relevantPreviews || []).find( (preview) => preview.camera == value.camera && preview.start < value.start_time && diff --git a/web/src/views/events/DesktopRecordingView.tsx b/web/src/views/events/DesktopRecordingView.tsx index dca373838..22b11c16f 100644 --- a/web/src/views/events/DesktopRecordingView.tsx +++ b/web/src/views/events/DesktopRecordingView.tsx @@ -1,7 +1,89 @@ +import DynamicVideoPlayer, { + DynamicVideoController, +} from "@/components/player/DynamicVideoPlayer"; +import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; +import { Button } from "@/components/ui/button"; +import { ReviewSegment } from "@/types/review"; +import { getChunkedTimeRange } from "@/utils/timelineUtil"; +import { useMemo, useRef, useState } from "react"; +import { IoMdArrowRoundBack } from "react-icons/io"; +import { useNavigate } from "react-router-dom"; +type DesktopRecordingViewProps = { + selectedReview: ReviewSegment; + reviewItems: ReviewSegment[]; + relevantPreviews?: Preview[]; +}; +export default function DesktopRecordingView({ + selectedReview, + reviewItems, + relevantPreviews, +}: DesktopRecordingViewProps) { + const navigate = useNavigate(); + const controllerRef = useRef(undefined); + const contentRef = useRef(null); -export default function DesktopRecordingView ({ }) { - return ( -
Hey this is pretty cool
- ) -} \ No newline at end of file + // timeline time + const timeRange = useMemo( + () => getChunkedTimeRange(selectedReview.start_time), + [] + ); + const [selectedRangeIdx, setSelectedRangeIdx] = useState( + timeRange.ranges.findIndex((chunk) => { + return ( + chunk.start <= selectedReview.start_time && + chunk.end >= selectedReview.start_time + ); + }) + ); + + const [currentTime, setCurrentTime] = useState( + selectedReview?.start_time || Date.now() / 1000 + ); + + return ( +
+ + +
+ { + controllerRef.current = controller; + controllerRef.current.onPlayerTimeUpdate((timestamp: number) => { + setCurrentTime(timestamp); + }); + + controllerRef.current?.seekToTimestamp( + selectedReview.start_time, + true + ); + }} + /> +
+ +
+ +
+
+ ); +}