diff --git a/web/src/components/scrubber/ActivityScrubber.tsx b/web/src/components/scrubber/ActivityScrubber.tsx index c88f3a3f1..a144f8e8b 100644 --- a/web/src/components/scrubber/ActivityScrubber.tsx +++ b/web/src/components/scrubber/ActivityScrubber.tsx @@ -75,8 +75,7 @@ const domEvents: TimelineEventsWithMissing[] = [ type ActivityScrubberProps = { items: TimelineItem[]; - midBar: boolean; - timeBars: { time: DateType; id?: IdType | undefined }[]; + timeBars?: { time: DateType; id?: IdType | undefined }[]; groups?: TimelineGroup[]; options?: TimelineOptions; } & TimelineEventsHandlers; diff --git a/web/src/pages/History.tsx b/web/src/pages/History.tsx index 84680edee..8f76a2744 100644 --- a/web/src/pages/History.tsx +++ b/web/src/pages/History.tsx @@ -22,7 +22,6 @@ import useApiFilter from "@/hooks/use-api-filter"; import HistoryCardView from "@/views/history/HistoryCardView"; import HistoryTimelineView from "@/views/history/HistoryTimelineView"; import { Button } from "@/components/ui/button"; -import { LuStepBack } from "react-icons/lu"; import { IoMdArrowBack } from "react-icons/io"; const API_LIMIT = 200; @@ -145,13 +144,15 @@ function History() { <>
- + {playback != undefined && ( + + )} History
{!playback && ( diff --git a/web/src/views/history/HistoryTimelineView.tsx b/web/src/views/history/HistoryTimelineView.tsx index 2d2fe7a79..674a449e8 100644 --- a/web/src/views/history/HistoryTimelineView.tsx +++ b/web/src/views/history/HistoryTimelineView.tsx @@ -1,9 +1,12 @@ import { useApiHost } from "@/api"; +import TimelineEventOverlay from "@/components/overlay/TimelineDataOverlay"; import VideoPlayer from "@/components/player/VideoPlayer"; import ActivityScrubber from "@/components/scrubber/ActivityScrubber"; -import { Button } from "@/components/ui/button"; +import ActivityIndicator from "@/components/ui/activity-indicator"; +import { FrigateConfig } from "@/types/frigateConfig"; import { getTimelineItemDescription } from "@/utils/timelineUtil"; -import { useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; +import useSWR from "swr"; import Player from "video.js/dist/types/player"; type HistoryTimelineViewProps = { @@ -16,10 +19,30 @@ export default function HistoryTimelineView({ isMobile, }: HistoryTimelineViewProps) { const apiHost = useApiHost(); + const { data: config } = useSWR("config"); + const timezone = useMemo( + () => + config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, + [config] + ); + const playerRef = useRef(undefined); const previewRef = useRef(undefined); const [scrubbing, setScrubbing] = useState(false); + const [focusedItem, setFocusedItem] = useState( + undefined + ); + + const annotationOffset = useMemo(() => { + if (!config) { + return 0; + } + + return ( + (config.cameras[playback.camera]?.detect?.annotation_offset || 0) / 1000 + ); + }, [config, playback]); const timelineTime = useMemo(() => { if (!playback) { @@ -37,13 +60,69 @@ export default function HistoryTimelineView({ return { start: startTime.toFixed(1), end: endTime.toFixed(1) }; }, [timelineTime]); + const recordingParams = useMemo(() => { + return { + before: playbackTimes.end, + after: playbackTimes.start, + }; + }, [playbackTimes]); + const { data: recordings } = useSWR( + playback ? [`${playback.camera}/recordings`, recordingParams] : null, + { revalidateOnFocus: false } + ); + const playbackUri = useMemo(() => { if (!playback) { return ""; } - return `${apiHost}vod/${playback.camera}/start/${playbackTimes.start}/end/${playbackTimes.end}/master.m3u8`; - }, [playback, playbackTimes]); + const date = new Date(parseInt(playbackTimes.start) * 1000); + return `${apiHost}vod/${date.getFullYear()}-${ + date.getMonth() + 1 + }/${date.getDate()}/${date.getHours()}/${ + playback.camera + }/${timezone.replaceAll("/", ",")}/master.m3u8`; + }, [playbackTimes]); + + const onSelectItem = useCallback( + (data: { items: number[] }) => { + if (data.items.length > 0) { + const selected = data.items[0]; + setFocusedItem( + playback.timelineItems.find( + (timeline) => timeline.timestamp == selected + ) + ); + playerRef.current?.pause(); + + let seekSeconds = 0; + console.log("recordings are " + recordings?.length); + (recordings || []).every((segment) => { + // if the next segment is past the desired time, stop calculating + if (segment.start_time > selected) { + return false; + } + + if (segment.end_time < selected) { + seekSeconds += segment.end_time - segment.start_time; + return true; + } + + seekSeconds += + segment.end_time - + segment.start_time - + (segment.end_time - selected); + return true; + }); + playerRef.current?.currentTime(seekSeconds); + } + }, + [annotationOffset, recordings, playerRef] + ); + + if (!config || !recordings) { + return ; + } return ( <> @@ -53,7 +132,7 @@ export default function HistoryTimelineView({ } >
-
+
{ - //setSelectedItem(undefined); + setFocusedItem(undefined); }); }} onDispose={() => { playerRef.current = undefined; }} - /> + > + {config && focusedItem ? ( + + ) : undefined} +