Show timeline bounding boxes

This commit is contained in:
Nick Mowen 2023-12-21 15:13:31 -07:00
parent 7c7a6a618a
commit 25f37eea34
3 changed files with 105 additions and 17 deletions

View File

@ -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;

View File

@ -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() {
<>
<div className="flex justify-between">
<div className="flex">
<Button
size="sm"
variant="ghost"
onClick={() => setPlayback(undefined)}
>
<IoMdArrowBack className="w-6 h-6" />
</Button>
{playback != undefined && (
<Button
size="sm"
variant="ghost"
onClick={() => setPlayback(undefined)}
>
<IoMdArrowBack className="w-6 h-6" />
</Button>
)}
<Heading as="h2">History</Heading>
</div>
{!playback && (

View File

@ -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<FrigateConfig>("config");
const timezone = useMemo(
() =>
config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
[config]
);
const playerRef = useRef<Player | undefined>(undefined);
const previewRef = useRef<Player | undefined>(undefined);
const [scrubbing, setScrubbing] = useState(false);
const [focusedItem, setFocusedItem] = useState<Timeline | undefined>(
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<Recording[]>(
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 <ActivityIndicator />;
}
return (
<>
@ -53,7 +132,7 @@ export default function HistoryTimelineView({
}
>
<div className={`w-screen 2xl:w-[1280px]`}>
<div className={`${scrubbing ? "hidden" : "visible"}`}>
<div className={`relative ${scrubbing ? "hidden" : "visible"}`}>
<VideoPlayer
options={{
preload: "auto",
@ -72,13 +151,20 @@ export default function HistoryTimelineView({
timelineTime - parseInt(playbackTimes.start)
);
player.on("playing", () => {
//setSelectedItem(undefined);
setFocusedItem(undefined);
});
}}
onDispose={() => {
playerRef.current = undefined;
}}
/>
>
{config && focusedItem ? (
<TimelineEventOverlay
timeline={focusedItem}
cameraConfig={config.cameras[playback.camera]}
/>
) : undefined}
</VideoPlayer>
</div>
<div className={`${scrubbing ? "visible" : "hidden"}`}>
<VideoPlayer
@ -105,6 +191,7 @@ export default function HistoryTimelineView({
/>
</div>
<ActivityScrubber
// @ts-ignore
items={timelineItemsToScrubber(playback.timelineItems)}
timeBars={[{ time: new Date(timelineTime * 1000), id: "playback" }]}
options={{
@ -131,6 +218,7 @@ export default function HistoryTimelineView({
setScrubbing(false);
playerRef.current?.play();
}}
selectHandler={onSelectItem}
/>
</div>
</div>