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 = { type ActivityScrubberProps = {
items: TimelineItem[]; items: TimelineItem[];
midBar: boolean; timeBars?: { time: DateType; id?: IdType | undefined }[];
timeBars: { time: DateType; id?: IdType | undefined }[];
groups?: TimelineGroup[]; groups?: TimelineGroup[];
options?: TimelineOptions; options?: TimelineOptions;
} & TimelineEventsHandlers; } & TimelineEventsHandlers;

View File

@ -22,7 +22,6 @@ import useApiFilter from "@/hooks/use-api-filter";
import HistoryCardView from "@/views/history/HistoryCardView"; import HistoryCardView from "@/views/history/HistoryCardView";
import HistoryTimelineView from "@/views/history/HistoryTimelineView"; import HistoryTimelineView from "@/views/history/HistoryTimelineView";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { LuStepBack } from "react-icons/lu";
import { IoMdArrowBack } from "react-icons/io"; import { IoMdArrowBack } from "react-icons/io";
const API_LIMIT = 200; const API_LIMIT = 200;
@ -145,13 +144,15 @@ function History() {
<> <>
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex"> <div className="flex">
<Button {playback != undefined && (
size="sm" <Button
variant="ghost" size="sm"
onClick={() => setPlayback(undefined)} variant="ghost"
> onClick={() => setPlayback(undefined)}
<IoMdArrowBack className="w-6 h-6" /> >
</Button> <IoMdArrowBack className="w-6 h-6" />
</Button>
)}
<Heading as="h2">History</Heading> <Heading as="h2">History</Heading>
</div> </div>
{!playback && ( {!playback && (

View File

@ -1,9 +1,12 @@
import { useApiHost } from "@/api"; import { useApiHost } from "@/api";
import TimelineEventOverlay from "@/components/overlay/TimelineDataOverlay";
import VideoPlayer from "@/components/player/VideoPlayer"; import VideoPlayer from "@/components/player/VideoPlayer";
import ActivityScrubber from "@/components/scrubber/ActivityScrubber"; 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 { 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"; import Player from "video.js/dist/types/player";
type HistoryTimelineViewProps = { type HistoryTimelineViewProps = {
@ -16,10 +19,30 @@ export default function HistoryTimelineView({
isMobile, isMobile,
}: HistoryTimelineViewProps) { }: HistoryTimelineViewProps) {
const apiHost = useApiHost(); 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 playerRef = useRef<Player | undefined>(undefined);
const previewRef = useRef<Player | undefined>(undefined); const previewRef = useRef<Player | undefined>(undefined);
const [scrubbing, setScrubbing] = useState(false); 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(() => { const timelineTime = useMemo(() => {
if (!playback) { if (!playback) {
@ -37,13 +60,69 @@ export default function HistoryTimelineView({
return { start: startTime.toFixed(1), end: endTime.toFixed(1) }; return { start: startTime.toFixed(1), end: endTime.toFixed(1) };
}, [timelineTime]); }, [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(() => { const playbackUri = useMemo(() => {
if (!playback) { if (!playback) {
return ""; return "";
} }
return `${apiHost}vod/${playback.camera}/start/${playbackTimes.start}/end/${playbackTimes.end}/master.m3u8`; const date = new Date(parseInt(playbackTimes.start) * 1000);
}, [playback, playbackTimes]); 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 ( return (
<> <>
@ -53,7 +132,7 @@ export default function HistoryTimelineView({
} }
> >
<div className={`w-screen 2xl:w-[1280px]`}> <div className={`w-screen 2xl:w-[1280px]`}>
<div className={`${scrubbing ? "hidden" : "visible"}`}> <div className={`relative ${scrubbing ? "hidden" : "visible"}`}>
<VideoPlayer <VideoPlayer
options={{ options={{
preload: "auto", preload: "auto",
@ -72,13 +151,20 @@ export default function HistoryTimelineView({
timelineTime - parseInt(playbackTimes.start) timelineTime - parseInt(playbackTimes.start)
); );
player.on("playing", () => { player.on("playing", () => {
//setSelectedItem(undefined); setFocusedItem(undefined);
}); });
}} }}
onDispose={() => { onDispose={() => {
playerRef.current = undefined; playerRef.current = undefined;
}} }}
/> >
{config && focusedItem ? (
<TimelineEventOverlay
timeline={focusedItem}
cameraConfig={config.cameras[playback.camera]}
/>
) : undefined}
</VideoPlayer>
</div> </div>
<div className={`${scrubbing ? "visible" : "hidden"}`}> <div className={`${scrubbing ? "visible" : "hidden"}`}>
<VideoPlayer <VideoPlayer
@ -105,6 +191,7 @@ export default function HistoryTimelineView({
/> />
</div> </div>
<ActivityScrubber <ActivityScrubber
// @ts-ignore
items={timelineItemsToScrubber(playback.timelineItems)} items={timelineItemsToScrubber(playback.timelineItems)}
timeBars={[{ time: new Date(timelineTime * 1000), id: "playback" }]} timeBars={[{ time: new Date(timelineTime * 1000), id: "playback" }]}
options={{ options={{
@ -131,6 +218,7 @@ export default function HistoryTimelineView({
setScrubbing(false); setScrubbing(false);
playerRef.current?.play(); playerRef.current?.play();
}} }}
selectHandler={onSelectItem}
/> />
</div> </div>
</div> </div>