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}
+