mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 11:45:24 +03:00
Show timeline bounding boxes
This commit is contained in:
parent
7c7a6a618a
commit
25f37eea34
@ -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;
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user