mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 19:55:26 +03:00
remove old web implementations
This commit is contained in:
parent
5c86f6d3f6
commit
23fbbd3c12
@ -3,111 +3,142 @@ import PreviewThumbnailPlayer from "../player/PreviewThumbnailPlayer";
|
|||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import ActivityIndicator from "../ui/activity-indicator";
|
import ActivityIndicator from "../ui/activity-indicator";
|
||||||
import { LuCircle, LuClock, LuPlay, LuPlayCircle, LuTruck } from "react-icons/lu";
|
import {
|
||||||
import { IoMdExit } from "react-icons/io"
|
LuCircle,
|
||||||
import { MdFaceUnlock, MdOutlineLocationOn, MdOutlinePictureInPictureAlt } from "react-icons/md";
|
LuClock,
|
||||||
|
LuPlay,
|
||||||
|
LuPlayCircle,
|
||||||
|
LuTruck,
|
||||||
|
} from "react-icons/lu";
|
||||||
|
import { IoMdExit } from "react-icons/io";
|
||||||
|
import {
|
||||||
|
MdFaceUnlock,
|
||||||
|
MdOutlineLocationOn,
|
||||||
|
MdOutlinePictureInPictureAlt,
|
||||||
|
} from "react-icons/md";
|
||||||
import { HiOutlineVideoCamera } from "react-icons/hi";
|
import { HiOutlineVideoCamera } from "react-icons/hi";
|
||||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||||
|
|
||||||
type HistoryCardProps = {
|
type HistoryCardProps = {
|
||||||
timeline: Card,
|
timeline: Card;
|
||||||
allPreviews?: Preview[],
|
allPreviews?: Preview[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function HistoryCard({ allPreviews, timeline }: HistoryCardProps) {
|
export default function HistoryCard({
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
allPreviews,
|
||||||
|
timeline,
|
||||||
|
}: HistoryCardProps) {
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <ActivityIndicator />
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="my-2 mr-2 bg-secondary">
|
<Card className="my-2 mr-2 bg-secondary">
|
||||||
<PreviewThumbnailPlayer
|
<PreviewThumbnailPlayer
|
||||||
camera={timeline.camera}
|
camera={timeline.camera}
|
||||||
allPreviews={allPreviews || []}
|
allPreviews={allPreviews || []}
|
||||||
startTs={Object.values(timeline.entries)[0].timestamp}
|
startTs={Object.values(timeline.entries)[0].timestamp}
|
||||||
/>
|
/>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<div className="text-sm flex">
|
<div className="text-sm flex">
|
||||||
<LuClock className="h-5 w-5 mr-2 inline" />
|
<LuClock className="h-5 w-5 mr-2 inline" />
|
||||||
{formatUnixTimestampToDateTime(timeline.time, { strftime_fmt: config.ui.time_format == '24hour' ? '%H:%M:%S' : '%I:%M:%S' })}
|
{formatUnixTimestampToDateTime(timeline.time, {
|
||||||
</div>
|
strftime_fmt:
|
||||||
<div className="capitalize text-sm flex align-center mt-1">
|
config.ui.time_format == "24hour" ? "%H:%M:%S" : "%I:%M:%S",
|
||||||
<HiOutlineVideoCamera className="h-5 w-5 mr-2 inline" />
|
})}
|
||||||
{timeline.camera.replaceAll('_', ' ')}
|
</div>
|
||||||
</div>
|
<div className="capitalize text-sm flex align-center mt-1">
|
||||||
<div className="my-2 text-sm font-medium">
|
<HiOutlineVideoCamera className="h-5 w-5 mr-2 inline" />
|
||||||
Activity:
|
{timeline.camera.replaceAll("_", " ")}
|
||||||
</div>
|
</div>
|
||||||
{Object.entries(timeline.entries).map(([_, entry]) => {
|
<div className="my-2 text-sm font-medium">Activity:</div>
|
||||||
return (
|
{Object.entries(timeline.entries).map(([_, entry]) => {
|
||||||
<div key={entry.timestamp} className="flex text-xs capitalize my-1 items-center">
|
return (
|
||||||
{getTimelineIcon(entry)}
|
<div
|
||||||
{getTimelineItemDescription(entry)}
|
key={entry.timestamp}
|
||||||
</div>
|
className="flex text-xs capitalize my-1 items-center"
|
||||||
);
|
>
|
||||||
})}
|
{getTimelineIcon(entry)}
|
||||||
|
{getTimelineItemDescription(entry)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
);
|
||||||
);
|
})}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTimelineIcon(timelineItem: Timeline) {
|
function getTimelineIcon(timelineItem: Timeline) {
|
||||||
switch (timelineItem.class_type) {
|
switch (timelineItem.class_type) {
|
||||||
case 'visible':
|
case "visible":
|
||||||
return <LuPlay className="w-4 mr-1" />;
|
return <LuPlay className="w-4 mr-1" />;
|
||||||
case 'gone':
|
case "gone":
|
||||||
return <IoMdExit className="w-4 mr-1" />;
|
return <IoMdExit className="w-4 mr-1" />;
|
||||||
case 'active':
|
case "active":
|
||||||
return <LuPlayCircle className="w-4 mr-1" />;
|
return <LuPlayCircle className="w-4 mr-1" />;
|
||||||
case 'stationary':
|
case "stationary":
|
||||||
return <LuCircle className="w-4 mr-1" />;
|
return <LuCircle className="w-4 mr-1" />;
|
||||||
case 'entered_zone':
|
case "entered_zone":
|
||||||
return <MdOutlineLocationOn className="w-4 mr-1" />;
|
return <MdOutlineLocationOn className="w-4 mr-1" />;
|
||||||
case 'attribute':
|
case "attribute":
|
||||||
switch (timelineItem.data.attribute) {
|
switch (timelineItem.data.attribute) {
|
||||||
case 'face':
|
case "face":
|
||||||
return <MdFaceUnlock className="w-4 mr-1" />;
|
return <MdFaceUnlock className="w-4 mr-1" />;
|
||||||
case 'license_plate':
|
case "license_plate":
|
||||||
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
|
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
|
||||||
default:
|
default:
|
||||||
return <LuTruck className="w-4 mr-1" />;
|
return <LuTruck className="w-4 mr-1" />;
|
||||||
}
|
}
|
||||||
case 'sub_label':
|
case "sub_label":
|
||||||
switch (timelineItem.data.label) {
|
switch (timelineItem.data.label) {
|
||||||
case 'person':
|
case "person":
|
||||||
return <MdFaceUnlock className="w-4 mr-1" />;
|
return <MdFaceUnlock className="w-4 mr-1" />;
|
||||||
case 'car':
|
case "car":
|
||||||
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
|
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getTimelineItemDescription(timelineItem: Timeline) {
|
function getTimelineItemDescription(timelineItem: Timeline) {
|
||||||
const label = ((Array.isArray(timelineItem.data.sub_label) ? timelineItem.data.sub_label[0] : timelineItem.data.sub_label) || timelineItem.data.label).replaceAll('_', ' ');
|
const label = (
|
||||||
|
(Array.isArray(timelineItem.data.sub_label)
|
||||||
|
? timelineItem.data.sub_label[0]
|
||||||
|
: timelineItem.data.sub_label) || timelineItem.data.label
|
||||||
|
).replaceAll("_", " ");
|
||||||
|
|
||||||
switch (timelineItem.class_type) {
|
switch (timelineItem.class_type) {
|
||||||
case 'visible':
|
case "visible":
|
||||||
return `${label} detected`;
|
return `${label} detected`;
|
||||||
case 'entered_zone':
|
case "entered_zone":
|
||||||
return `${label} entered ${timelineItem.data.zones.join(' and ').replaceAll('_', ' ')}`;
|
return `${label} entered ${timelineItem.data.zones
|
||||||
case 'active':
|
.join(" and ")
|
||||||
return `${label} became active`;
|
.replaceAll("_", " ")}`;
|
||||||
case 'stationary':
|
case "active":
|
||||||
return `${label} became stationary`;
|
return `${label} became active`;
|
||||||
case 'attribute': {
|
case "stationary":
|
||||||
let title = '';
|
return `${label} became stationary`;
|
||||||
if (timelineItem.data.attribute == 'face' || timelineItem.data.attribute == 'license_plate') {
|
case "attribute": {
|
||||||
title = `${timelineItem.data.attribute.replaceAll('_', ' ')} detected for ${label}`;
|
let title = "";
|
||||||
} else {
|
if (
|
||||||
title = `${timelineItem.data.sub_label} recognized as ${timelineItem.data.attribute.replaceAll('_', ' ')}`;
|
timelineItem.data.attribute == "face" ||
|
||||||
}
|
timelineItem.data.attribute == "license_plate"
|
||||||
return title;
|
) {
|
||||||
|
title = `${timelineItem.data.attribute.replaceAll(
|
||||||
|
"_",
|
||||||
|
" "
|
||||||
|
)} detected for ${label}`;
|
||||||
|
} else {
|
||||||
|
title = `${
|
||||||
|
timelineItem.data.sub_label
|
||||||
|
} recognized as ${timelineItem.data.attribute.replaceAll("_", " ")}`;
|
||||||
}
|
}
|
||||||
case 'sub_label':
|
return title;
|
||||||
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
|
|
||||||
case 'gone':
|
|
||||||
return `${label} left`;
|
|
||||||
}
|
}
|
||||||
}
|
case "sub_label":
|
||||||
|
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
|
||||||
|
case "gone":
|
||||||
|
return `${label} left`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -5,88 +5,106 @@ import Heading from "@/components/ui/heading";
|
|||||||
import ActivityIndicator from "@/components/ui/activity-indicator";
|
import ActivityIndicator from "@/components/ui/activity-indicator";
|
||||||
import HistoryCard from "@/components/card/HistoryCard";
|
import HistoryCard from "@/components/card/HistoryCard";
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";;
|
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||||
|
|
||||||
function History() {
|
function History() {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const timezone = useMemo(() => config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config]);
|
const timezone = useMemo(
|
||||||
const { data: hourlyTimeline } = useSWR<HourlyTimeline>(['timeline/hourly', { timezone }]);
|
() =>
|
||||||
const { data: allPreviews } = useSWR<Preview[]>(`preview/all/start/${hourlyTimeline?.start || 0}/end/${hourlyTimeline?.end || 0}`, { revalidateOnFocus: false });
|
config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
[config]
|
||||||
|
);
|
||||||
|
const { data: hourlyTimeline } = useSWR<HourlyTimeline>([
|
||||||
|
"timeline/hourly",
|
||||||
|
{ timezone },
|
||||||
|
]);
|
||||||
|
const { data: allPreviews } = useSWR<Preview[]>(
|
||||||
|
`preview/all/start/${hourlyTimeline?.start || 0}/end/${
|
||||||
|
hourlyTimeline?.end || 0
|
||||||
|
}`,
|
||||||
|
{ revalidateOnFocus: false }
|
||||||
|
);
|
||||||
|
|
||||||
const [detailLevel, setDetailLevel] = useState<'normal' | 'extra' | 'full'>('normal');
|
const [detailLevel, setDetailLevel] = useState<"normal" | "extra" | "full">(
|
||||||
|
"normal"
|
||||||
|
);
|
||||||
|
|
||||||
const timelineCards: CardsData | never[] = useMemo(() => {
|
const timelineCards: CardsData | never[] = useMemo(() => {
|
||||||
if (!hourlyTimeline) {
|
if (!hourlyTimeline) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const cards: CardsData = {};
|
const cards: CardsData = {};
|
||||||
Object.keys(hourlyTimeline["hours"])
|
Object.keys(hourlyTimeline["hours"])
|
||||||
.reverse()
|
.reverse()
|
||||||
.forEach((hour) => {
|
.forEach((hour) => {
|
||||||
const day = new Date(parseInt(hour) * 1000);
|
const day = new Date(parseInt(hour) * 1000);
|
||||||
day.setHours(0, 0, 0, 0);
|
day.setHours(0, 0, 0, 0);
|
||||||
const dayKey = (day.getTime() / 1000).toString();
|
const dayKey = (day.getTime() / 1000).toString();
|
||||||
const source_to_types: {[key: string]: string[]} = {};
|
const source_to_types: { [key: string]: string[] } = {};
|
||||||
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
|
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
|
||||||
const time = new Date(i.timestamp * 1000);
|
const time = new Date(i.timestamp * 1000);
|
||||||
time.setSeconds(0);
|
time.setSeconds(0);
|
||||||
time.setMilliseconds(0);
|
time.setMilliseconds(0);
|
||||||
const key = `${i.source_id}-${time.getMinutes()}`;
|
const key = `${i.source_id}-${time.getMinutes()}`;
|
||||||
if (key in source_to_types) {
|
if (key in source_to_types) {
|
||||||
source_to_types[key].push(i.class_type);
|
source_to_types[key].push(i.class_type);
|
||||||
} else {
|
} else {
|
||||||
source_to_types[key] = [i.class_type];
|
source_to_types[key] = [i.class_type];
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!Object.keys(cards).includes(dayKey)) {
|
|
||||||
cards[dayKey] = {};
|
|
||||||
}
|
}
|
||||||
cards[dayKey][hour] = {};
|
|
||||||
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
|
|
||||||
const time = new Date(i.timestamp * 1000);
|
|
||||||
const key = `${i.camera}-${time.getMinutes()}`;
|
|
||||||
|
|
||||||
// detail level for saving items
|
|
||||||
// detail level determines which timeline items for each moment is returned
|
|
||||||
// values can be normal, extra, or full
|
|
||||||
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
|
|
||||||
// extra: return all items except attribute / gone / visible unless that is the only item
|
|
||||||
// full: return all items
|
|
||||||
|
|
||||||
let add = true;
|
|
||||||
if (detailLevel == 'normal') {
|
|
||||||
if (
|
|
||||||
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
|
|
||||||
['active', 'attribute', 'gone', 'stationary', 'visible'].includes(i.class_type)
|
|
||||||
) {
|
|
||||||
add = false;
|
|
||||||
}
|
|
||||||
} else if (detailLevel == 'extra') {
|
|
||||||
if (
|
|
||||||
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
|
|
||||||
i.class_type in ['attribute', 'gone', 'visible']
|
|
||||||
) {
|
|
||||||
add = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (add) {
|
|
||||||
if (key in cards[dayKey][hour]) {
|
|
||||||
cards[dayKey][hour][key].entries.push(i);
|
|
||||||
} else {
|
|
||||||
cards[dayKey][hour][key] = {
|
|
||||||
camera: i.camera,
|
|
||||||
time: time.getTime() / 1000,
|
|
||||||
entries: [i],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return cards;
|
if (!Object.keys(cards).includes(dayKey)) {
|
||||||
|
cards[dayKey] = {};
|
||||||
|
}
|
||||||
|
cards[dayKey][hour] = {};
|
||||||
|
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
|
||||||
|
const time = new Date(i.timestamp * 1000);
|
||||||
|
const key = `${i.camera}-${time.getMinutes()}`;
|
||||||
|
|
||||||
|
// detail level for saving items
|
||||||
|
// detail level determines which timeline items for each moment is returned
|
||||||
|
// values can be normal, extra, or full
|
||||||
|
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
|
||||||
|
// extra: return all items except attribute / gone / visible unless that is the only item
|
||||||
|
// full: return all items
|
||||||
|
|
||||||
|
let add = true;
|
||||||
|
if (detailLevel == "normal") {
|
||||||
|
if (
|
||||||
|
source_to_types[`${i.source_id}-${time.getMinutes()}`].length >
|
||||||
|
1 &&
|
||||||
|
["active", "attribute", "gone", "stationary", "visible"].includes(
|
||||||
|
i.class_type
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
add = false;
|
||||||
|
}
|
||||||
|
} else if (detailLevel == "extra") {
|
||||||
|
if (
|
||||||
|
source_to_types[`${i.source_id}-${time.getMinutes()}`].length >
|
||||||
|
1 &&
|
||||||
|
i.class_type in ["attribute", "gone", "visible"]
|
||||||
|
) {
|
||||||
|
add = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (add) {
|
||||||
|
if (key in cards[dayKey][hour]) {
|
||||||
|
cards[dayKey][hour][key].entries.push(i);
|
||||||
|
} else {
|
||||||
|
cards[dayKey][hour][key] = {
|
||||||
|
camera: i.camera,
|
||||||
|
time: time.getTime() / 1000,
|
||||||
|
entries: [i],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return cards;
|
||||||
}, [detailLevel, hourlyTimeline]);
|
}, [detailLevel, hourlyTimeline]);
|
||||||
|
|
||||||
if (!config || !timelineCards) {
|
if (!config || !timelineCards) {
|
||||||
@ -94,43 +112,59 @@ function History() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading as="h2">Review</Heading>
|
<Heading as="h2">Review</Heading>
|
||||||
<div className="text-xs mb-4">Dates and times are based on the timezone {timezone}</div>
|
<div className="text-xs mb-4">
|
||||||
|
Dates and times are based on the timezone {timezone}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{Object.entries(timelineCards)
|
||||||
|
.reverse()
|
||||||
|
.map(([day, timelineDay]) => {
|
||||||
|
return (
|
||||||
|
<div key={day}>
|
||||||
|
<Heading as="h3">
|
||||||
|
{formatUnixTimestampToDateTime(parseInt(day), {
|
||||||
|
strftime_fmt: "%A %b %d",
|
||||||
|
})}
|
||||||
|
</Heading>
|
||||||
|
{Object.entries(timelineDay).map(([hour, timelineHour]) => {
|
||||||
|
if (Object.values(timelineHour).length == 0) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
<div>
|
|
||||||
{Object.entries(timelineCards).reverse().map(([day, timelineDay]) => {
|
|
||||||
return (
|
return (
|
||||||
<div key={day}>
|
<div key={hour}>
|
||||||
<Heading as="h3">
|
<Heading as="h4">
|
||||||
{formatUnixTimestampToDateTime(parseInt(day), { strftime_fmt: '%A %b %d' })}
|
{formatUnixTimestampToDateTime(parseInt(hour), {
|
||||||
</Heading>
|
strftime_fmt: "%I:00",
|
||||||
{Object.entries(timelineDay).map(([hour, timelineHour]) => {
|
})}
|
||||||
if (Object.values(timelineHour).length == 0) {
|
</Heading>
|
||||||
return <></>;
|
<ScrollArea>
|
||||||
}
|
<div className="flex">
|
||||||
|
{Object.entries(timelineHour).map(
|
||||||
|
([key, timeline]) => {
|
||||||
return (
|
return (
|
||||||
<div key={hour}>
|
<HistoryCard
|
||||||
<Heading as="h4">
|
key={key}
|
||||||
{formatUnixTimestampToDateTime(parseInt(hour), { strftime_fmt: '%I:00' })}
|
timeline={timeline}
|
||||||
</Heading>
|
allPreviews={allPreviews}
|
||||||
<ScrollArea>
|
/>
|
||||||
<div className="flex">
|
|
||||||
{Object.entries(timelineHour).map(([key, timeline]) => {
|
|
||||||
return <HistoryCard key={key} timeline={timeline} allPreviews={allPreviews} />
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<ScrollBar className="m-2" orientation="horizontal" />
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
}
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
<ScrollBar className="m-2" orientation="horizontal" />
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,83 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import ActivityIndicator from './ActivityIndicator';
|
|
||||||
import VideoPlayer from './VideoPlayer';
|
|
||||||
import { useCallback } from 'preact/hooks';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useApiHost } from '../api';
|
|
||||||
|
|
||||||
export default function PreviewPlayer({ camera, allPreviews, startTs, mode }) {
|
|
||||||
const { data: config } = useSWR('config');
|
|
||||||
const apiHost = useApiHost();
|
|
||||||
|
|
||||||
const relevantPreview = useMemo(() => {
|
|
||||||
return Object.values(allPreviews || []).find(
|
|
||||||
(preview) => preview.camera == camera && preview.start < startTs && preview.end > startTs
|
|
||||||
);
|
|
||||||
}, [allPreviews, camera, startTs]);
|
|
||||||
|
|
||||||
const onHover = useCallback(
|
|
||||||
(isHovered) => {
|
|
||||||
if (isHovered) {
|
|
||||||
this.player.play();
|
|
||||||
} else {
|
|
||||||
this.player.pause();
|
|
||||||
this.player.currentTime(startTs - relevantPreview.start);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[relevantPreview, startTs]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!relevantPreview) {
|
|
||||||
return (
|
|
||||||
<img className={getThumbWidth(camera, config)} src={`${apiHost}api/preview/${camera}/${startTs}/thumbnail.jpg`} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={mode == 'thumbnail' ? getThumbWidth(camera, config) : ''}
|
|
||||||
onMouseEnter={() => onHover(true)}
|
|
||||||
onMouseLeave={() => onHover(false)}
|
|
||||||
>
|
|
||||||
<VideoPlayer
|
|
||||||
tag={startTs}
|
|
||||||
options={{
|
|
||||||
preload: 'auto',
|
|
||||||
autoplay: false,
|
|
||||||
controls: false,
|
|
||||||
muted: true,
|
|
||||||
loadingSpinner: false,
|
|
||||||
sources: [
|
|
||||||
{
|
|
||||||
src: `${relevantPreview.src}`,
|
|
||||||
type: 'video/mp4',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
seekOptions={{}}
|
|
||||||
onReady={(player) => {
|
|
||||||
this.player = player;
|
|
||||||
this.player.playbackRate(8);
|
|
||||||
this.player.currentTime(startTs - relevantPreview.start);
|
|
||||||
}}
|
|
||||||
onDispose={() => {
|
|
||||||
this.player = null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThumbWidth(camera, config) {
|
|
||||||
const detect = config.cameras[camera].detect;
|
|
||||||
if (detect.width / detect.height > 2) {
|
|
||||||
return 'w-[320px]';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (detect.width / detect.height < 1.4) {
|
|
||||||
return 'w-[200px]';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'w-[240px]';
|
|
||||||
}
|
|
||||||
@ -1,222 +0,0 @@
|
|||||||
import Heading from '../components/Heading';
|
|
||||||
import { useMemo, useState } from 'preact/hooks';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import ActivityIndicator from '../components/ActivityIndicator';
|
|
||||||
import PreviewPlayer from '../components/PreviewPlayer';
|
|
||||||
import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
|
|
||||||
import { Clock } from '../icons/Clock';
|
|
||||||
import { Camera } from '../icons/Camera';
|
|
||||||
import ActiveObjectIcon from '../icons/ActiveObject';
|
|
||||||
import PlayIcon from '../icons/Play';
|
|
||||||
import ExitIcon from '../icons/Exit';
|
|
||||||
import StationaryObjectIcon from '../icons/StationaryObject';
|
|
||||||
import FaceIcon from '../icons/Face';
|
|
||||||
import LicensePlateIcon from '../icons/LicensePlate';
|
|
||||||
import DeliveryTruckIcon from '../icons/DeliveryTruck';
|
|
||||||
import ZoneIcon from '../icons/Zone';
|
|
||||||
|
|
||||||
export default function Export() {
|
|
||||||
const { data: config } = useSWR('config');
|
|
||||||
const timezone = useMemo(() => config?.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [config]);
|
|
||||||
const { data: hourlyTimeline } = useSWR(['timeline/hourly', { timezone }]);
|
|
||||||
const { data: allPreviews } = useSWR([
|
|
||||||
`preview/all/start/${Object.keys(hourlyTimeline || [0])[0]}/end/${Object.keys(hourlyTimeline || [0]).slice(-1)[0]}`,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// detail levels can be normal, extra, full
|
|
||||||
const [detailLevel, setDetailLevel] = useState('normal');
|
|
||||||
|
|
||||||
const timelineCards = useMemo(() => {
|
|
||||||
if (!hourlyTimeline) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const cards = {};
|
|
||||||
Object.keys(hourlyTimeline)
|
|
||||||
.reverse()
|
|
||||||
.forEach((hour) => {
|
|
||||||
const source_to_types = {};
|
|
||||||
Object.values(hourlyTimeline[hour]).forEach((i) => {
|
|
||||||
const time = new Date(i.timestamp * 1000);
|
|
||||||
time.setSeconds(0);
|
|
||||||
time.setMilliseconds(0);
|
|
||||||
const key = `${i.source_id}-${time.getMinutes()}`;
|
|
||||||
if (key in source_to_types) {
|
|
||||||
source_to_types[key].push(i.class_type);
|
|
||||||
} else {
|
|
||||||
source_to_types[key] = [i.class_type];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cards[hour] = {};
|
|
||||||
Object.values(hourlyTimeline[hour]).forEach((i) => {
|
|
||||||
const time = new Date(i.timestamp * 1000);
|
|
||||||
time.setSeconds(0);
|
|
||||||
time.setMilliseconds(0);
|
|
||||||
const key = `${i.camera}-${time.getMinutes()}`;
|
|
||||||
|
|
||||||
// detail level for saving items
|
|
||||||
// detail level determines which timeline items for each moment is returned
|
|
||||||
// values can be normal, extra, or full
|
|
||||||
// normal: return all items except active / attribute / gone / stationary / visible unless that is the only item.
|
|
||||||
// extra: return all items except attribute / gone / visible unless that is the only item
|
|
||||||
// full: return all items
|
|
||||||
|
|
||||||
let add = true;
|
|
||||||
if (detailLevel == 'normal') {
|
|
||||||
if (
|
|
||||||
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
|
|
||||||
['active', 'attribute', 'gone', 'stationary', 'visible'].includes(i.class_type)
|
|
||||||
) {
|
|
||||||
add = false;
|
|
||||||
}
|
|
||||||
} else if (detailLevel == 'extra') {
|
|
||||||
if (
|
|
||||||
source_to_types[`${i.source_id}-${time.getMinutes()}`].length > 1 &&
|
|
||||||
i.class_type in ['attribute', 'gone', 'visible']
|
|
||||||
) {
|
|
||||||
add = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (add) {
|
|
||||||
if (key in cards[hour]) {
|
|
||||||
cards[hour][key].entries.push(i);
|
|
||||||
} else {
|
|
||||||
cards[hour][key] = {
|
|
||||||
camera: i.camera,
|
|
||||||
time: time.getTime() / 1000,
|
|
||||||
entries: [i],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return cards;
|
|
||||||
}, [detailLevel, hourlyTimeline]);
|
|
||||||
|
|
||||||
if (!timelineCards) {
|
|
||||||
return <ActivityIndicator />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 p-2 px-4">
|
|
||||||
<Heading>Review</Heading>
|
|
||||||
<div className="text-xs">Dates and times are based on the timezone {timezone}</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{Object.entries(timelineCards).map(([hour, timelineHour]) => {
|
|
||||||
return (
|
|
||||||
<div key={hour}>
|
|
||||||
<Heading size="md">
|
|
||||||
{formatUnixTimestampToDateTime(hour, {
|
|
||||||
date_style: 'short',
|
|
||||||
time_style: 'medium',
|
|
||||||
time_format: config.ui.time_format,
|
|
||||||
})}
|
|
||||||
</Heading>
|
|
||||||
<div className="flex overflow-auto">
|
|
||||||
{Object.entries(timelineHour).map(([key, timeline]) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow my-2 mr-2"
|
|
||||||
>
|
|
||||||
<PreviewPlayer
|
|
||||||
camera={timeline.camera}
|
|
||||||
allPreviews={allPreviews}
|
|
||||||
startTs={Object.values(timeline.entries)[0].timestamp}
|
|
||||||
mode="thumbnail"
|
|
||||||
/>
|
|
||||||
<div className="p-2">
|
|
||||||
<div className="text-sm flex">
|
|
||||||
<Clock className="h-5 w-5 mr-2 inline" />
|
|
||||||
{formatUnixTimestampToDateTime(timeline.time, { ...config.ui })}
|
|
||||||
</div>
|
|
||||||
<div className="capitalize text-sm flex align-center mt-1">
|
|
||||||
<Camera className="h-5 w-5 mr-2 inline" />
|
|
||||||
{timeline.camera.replaceAll('_', ' ')}
|
|
||||||
</div>
|
|
||||||
<Heading size="xs" className="my-2">
|
|
||||||
Activity:
|
|
||||||
</Heading>
|
|
||||||
{Object.entries(timeline.entries).map(([_, entry]) => {
|
|
||||||
return (
|
|
||||||
<div key={entry.timestamp} className="flex text-xs my-1">
|
|
||||||
{getTimelineIcon(entry)}
|
|
||||||
{getTimelineItemDescription(config, entry)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTimelineIcon(timelineItem) {
|
|
||||||
switch (timelineItem.class_type) {
|
|
||||||
case 'visible':
|
|
||||||
return <PlayIcon className="w-4 mr-1" />;
|
|
||||||
case 'gone':
|
|
||||||
return <ExitIcon className="w-4 mr-1" />;
|
|
||||||
case 'active':
|
|
||||||
return <ActiveObjectIcon className="w-4 mr-1" />;
|
|
||||||
case 'stationary':
|
|
||||||
return <StationaryObjectIcon className="w-4 mr-1" />;
|
|
||||||
case 'entered_zone':
|
|
||||||
return <ZoneIcon className="w-4 mr-1" />;
|
|
||||||
case 'attribute':
|
|
||||||
switch (timelineItem.data.attribute) {
|
|
||||||
case 'face':
|
|
||||||
return <FaceIcon className="w-4 mr-1" />;
|
|
||||||
case 'license_plate':
|
|
||||||
return <LicensePlateIcon className="w-4 mr-1" />;
|
|
||||||
default:
|
|
||||||
return <DeliveryTruckIcon className="w-4 mr-1" />;
|
|
||||||
}
|
|
||||||
case 'sub_label':
|
|
||||||
switch (timelineItem.data.label) {
|
|
||||||
case 'person':
|
|
||||||
return <FaceIcon className="w-4 mr-1" />;
|
|
||||||
case 'car':
|
|
||||||
return <LicensePlateIcon className="w-4 mr-1" />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTimelineItemDescription(config, timelineItem) {
|
|
||||||
const label = (timelineItem.data.sub_label || timelineItem.data.label).replaceAll('_', ' ');
|
|
||||||
|
|
||||||
switch (timelineItem.class_type) {
|
|
||||||
case 'visible':
|
|
||||||
return `${label} detected`;
|
|
||||||
case 'entered_zone':
|
|
||||||
return `${label} entered ${timelineItem.data.zones.join(' and ').replaceAll('_', ' ')}`;
|
|
||||||
case 'active':
|
|
||||||
return `${label} became active`;
|
|
||||||
case 'stationary':
|
|
||||||
return `${label} became stationary`;
|
|
||||||
case 'attribute': {
|
|
||||||
let title = '';
|
|
||||||
if (timelineItem.data.attribute == 'face' || timelineItem.data.attribute == 'license_plate') {
|
|
||||||
title = `${timelineItem.data.attribute.replaceAll('_', ' ')} detected for ${label}`;
|
|
||||||
} else {
|
|
||||||
title = `${timelineItem.data.sub_label} recognized as ${timelineItem.data.attribute.replaceAll('_', ' ')}`;
|
|
||||||
}
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
case 'sub_label':
|
|
||||||
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
|
|
||||||
case 'gone':
|
|
||||||
return `${label} left`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -19,7 +19,16 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
'/exports': {
|
'/exports': {
|
||||||
target: 'http://localhost:5000'
|
target: 'http://localhost:5000'
|
||||||
}
|
},
|
||||||
|
'/ws': {
|
||||||
|
target: 'ws://localhost:5000',
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
|
'/live': {
|
||||||
|
target: 'ws://localhost:5000',
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user