mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 11:45:24 +03:00
Add graph to timeline
This commit is contained in:
parent
e110a7d11d
commit
0f38eb69bd
@ -751,28 +751,6 @@ def hourly_timeline_activity(camera_name: str):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# process data to make data counts relative
|
|
||||||
for hour, data in hours.items():
|
|
||||||
motion_values = np.asarray(list(map(lambda m: m["count"], data)))
|
|
||||||
avg = motion_values.mean()
|
|
||||||
std = motion_values.std()
|
|
||||||
|
|
||||||
for idx, motion in enumerate(motion_values):
|
|
||||||
if motion < (avg - (std * 2)):
|
|
||||||
value = 1
|
|
||||||
elif motion < (avg - std):
|
|
||||||
value = 2
|
|
||||||
elif motion < avg:
|
|
||||||
value = 3
|
|
||||||
elif motion < (avg + std):
|
|
||||||
value = 4
|
|
||||||
elif motion < (avg + (std * 2)):
|
|
||||||
value = 5
|
|
||||||
else:
|
|
||||||
value = 6
|
|
||||||
|
|
||||||
hours[hour][idx]["count"] = value
|
|
||||||
|
|
||||||
return jsonify(hours)
|
return jsonify(hours)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,8 @@ export default function TimelineGraph({ id, data }: TimelineGraphProps) {
|
|||||||
labels: {
|
labels: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
|
logarithmic: true,
|
||||||
|
logBase: 10,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
series={data}
|
series={data}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import { Event } from "@/types/event";
|
|||||||
import ActivityIndicator from "@/components/ui/activity-indicator";
|
import ActivityIndicator from "@/components/ui/activity-indicator";
|
||||||
import { useApiHost } from "@/api";
|
import { useApiHost } from "@/api";
|
||||||
import TimelineScrubber from "@/components/playground/TimelineScrubber";
|
import TimelineScrubber from "@/components/playground/TimelineScrubber";
|
||||||
|
import TimelineGraph from "@/components/graph/TimelineGraph";
|
||||||
|
import { GraphDataPoint } from "@/types/graph";
|
||||||
|
|
||||||
// Color data
|
// Color data
|
||||||
const colors = [
|
const colors = [
|
||||||
@ -74,6 +76,36 @@ function UIPlayground() {
|
|||||||
"events",
|
"events",
|
||||||
{ limit: 10, after: recentTimestamp },
|
{ limit: 10, after: recentTimestamp },
|
||||||
]);
|
]);
|
||||||
|
const { data: recordingSegments } = useSWR<RecordingSegment[]>([
|
||||||
|
"front_cam/recordings",
|
||||||
|
{ before: 1704211200, after: 1704207600 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const graphData = useMemo(() => {
|
||||||
|
if (!recordingSegments) {
|
||||||
|
return { motion: [], objects: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const motion: GraphDataPoint[] = [];
|
||||||
|
const objects: GraphDataPoint[] = [];
|
||||||
|
|
||||||
|
recordingSegments
|
||||||
|
.forEach((seg) => {
|
||||||
|
if (seg.objects > 0) {
|
||||||
|
objects.push({
|
||||||
|
x: new Date((seg.start_time + 5) * 1000),
|
||||||
|
y: seg.motion,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
motion.push({
|
||||||
|
x: new Date((seg.start_time + 5) * 1000),
|
||||||
|
y: seg.motion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { motion, objects };
|
||||||
|
}, [recordingSegments]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -91,12 +123,28 @@ function UIPlayground() {
|
|||||||
{config && (
|
{config && (
|
||||||
<div>
|
<div>
|
||||||
{events && events.length > 0 && (
|
{events && events.length > 0 && (
|
||||||
<>
|
<div className="relative">
|
||||||
<ActivityScrubber
|
<ActivityScrubber
|
||||||
items={eventsToScrubberItems(events)}
|
items={[]}
|
||||||
|
options={{
|
||||||
|
start: new Date(1704207600000),
|
||||||
|
end: new Date(1704211200000),
|
||||||
|
}}
|
||||||
selectHandler={onSelect}
|
selectHandler={onSelect}
|
||||||
/>
|
/>
|
||||||
</>
|
<div className="w-full absolute left-0 top-0 h-[84px]">
|
||||||
|
<TimelineGraph
|
||||||
|
id="test"
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
name: "Motion",
|
||||||
|
data: graphData.motion,
|
||||||
|
},
|
||||||
|
{ name: "Active Objects", data: graphData.objects },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -56,6 +56,12 @@ interface HistoryFilter extends FilterType {
|
|||||||
detailLevel: "normal" | "extra" | "full";
|
detailLevel: "normal" | "extra" | "full";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HistoryTimeline = {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
playbackItems: TimelinePlayback[];
|
||||||
|
};
|
||||||
|
|
||||||
type TimelinePlayback = {
|
type TimelinePlayback = {
|
||||||
camera: string;
|
camera: string;
|
||||||
range: { start: number; end: number };
|
range: { start: number; end: number };
|
||||||
|
|||||||
@ -18,3 +18,13 @@ type RecordingSegment = {
|
|||||||
objects: number;
|
objects: number;
|
||||||
segment_size: number;
|
segment_size: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RecordingActivity = {
|
||||||
|
[hour: number]: RecordingSegmentActivity[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type RecordingSegmentActivity = {
|
||||||
|
date: number;
|
||||||
|
count: number;
|
||||||
|
type: "motion" | "objects";
|
||||||
|
};
|
||||||
|
|||||||
@ -107,13 +107,14 @@ export function getTimelineHoursForDay(
|
|||||||
cards: CardsData,
|
cards: CardsData,
|
||||||
allPreviews: Preview[],
|
allPreviews: Preview[],
|
||||||
timestamp: number
|
timestamp: number
|
||||||
): TimelinePlayback[] {
|
): HistoryTimeline {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const data: TimelinePlayback[] = [];
|
const data: TimelinePlayback[] = [];
|
||||||
const startDay = new Date(timestamp * 1000);
|
const startDay = new Date(timestamp * 1000);
|
||||||
startDay.setHours(23, 59, 59, 999);
|
startDay.setHours(23, 59, 59, 999);
|
||||||
const dayEnd = startDay.getTime() / 1000;
|
const dayEnd = startDay.getTime() / 1000;
|
||||||
startDay.setHours(0, 0, 0, 0);
|
startDay.setHours(0, 0, 0, 0);
|
||||||
|
const startTimestamp = startDay.getTime() / 1000;
|
||||||
let start = startDay.getTime() / 1000;
|
let start = startDay.getTime() / 1000;
|
||||||
let end = 0;
|
let end = 0;
|
||||||
|
|
||||||
@ -134,7 +135,7 @@ export function getTimelineHoursForDay(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (dayIdx == undefined) {
|
if (dayIdx == undefined) {
|
||||||
return [];
|
return { start: 0, end: 0, playbackItems: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const day = cards[dayIdx];
|
const day = cards[dayIdx];
|
||||||
@ -179,5 +180,5 @@ export function getTimelineHoursForDay(
|
|||||||
start = startDay.getTime() / 1000;
|
start = startDay.getTime() / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.reverse();
|
return { start: startTimestamp, end, playbackItems: data.reverse() };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import useSWR from "swr";
|
|||||||
import Player from "video.js/dist/types/player";
|
import Player from "video.js/dist/types/player";
|
||||||
import TimelineItemCard from "@/components/card/TimelineItemCard";
|
import TimelineItemCard from "@/components/card/TimelineItemCard";
|
||||||
import { getTimelineHoursForDay } from "@/utils/historyUtil";
|
import { getTimelineHoursForDay } from "@/utils/historyUtil";
|
||||||
|
import { GraphDataPoint } from "@/types/graph";
|
||||||
|
import TimelineGraph from "@/components/graph/TimelineGraph";
|
||||||
|
|
||||||
type DesktopTimelineViewProps = {
|
type DesktopTimelineViewProps = {
|
||||||
timelineData: CardsData;
|
timelineData: CardsData;
|
||||||
@ -166,6 +168,50 @@ export default function DesktopTimelineView({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: activity } = useSWR<RecordingActivity>(
|
||||||
|
[
|
||||||
|
`${initialPlayback.camera}/recording/hourly/activity`,
|
||||||
|
{
|
||||||
|
after: timelineStack.start,
|
||||||
|
before: timelineStack.end,
|
||||||
|
timezone,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ revalidateOnFocus: false }
|
||||||
|
);
|
||||||
|
const timelineGraphData = useMemo(() => {
|
||||||
|
if (!activity) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const graphData: {
|
||||||
|
[hour: string]: { objects: GraphDataPoint[]; motion: GraphDataPoint[] };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
Object.entries(activity).forEach(([hour, data]) => {
|
||||||
|
const objects: GraphDataPoint[] = [];
|
||||||
|
const motion: GraphDataPoint[] = [];
|
||||||
|
|
||||||
|
data.forEach((seg) => {
|
||||||
|
if (seg.type == "objects") {
|
||||||
|
objects.push({
|
||||||
|
x: new Date(seg.date * 1000),
|
||||||
|
y: seg.count,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
motion.push({
|
||||||
|
x: new Date(seg.date * 1000),
|
||||||
|
y: seg.count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
graphData[hour] = { objects, motion };
|
||||||
|
});
|
||||||
|
|
||||||
|
return graphData;
|
||||||
|
}, [activity]);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
@ -271,14 +317,18 @@ export default function DesktopTimelineView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="m-1 max-h-72 2xl:max-h-80 3xl:max-h-96 overflow-auto">
|
<div className="m-1 max-h-72 2xl:max-h-80 3xl:max-h-96 overflow-auto">
|
||||||
{timelineStack.map((timeline) => {
|
{timelineStack.playbackItems.map((timeline) => {
|
||||||
const isSelected =
|
const isSelected =
|
||||||
timeline.range.start == selectedPlayback.range.start;
|
timeline.range.start == selectedPlayback.range.start;
|
||||||
|
const graphData = timelineGraphData[timeline.range.start];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={timeline.range.start}
|
key={timeline.range.start}
|
||||||
className={`p-2 ${isSelected ? "bg-secondary bg-opacity-30 rounded-md" : ""}`}
|
className={`relative p-2 ${
|
||||||
|
isSelected ? "bg-secondary bg-opacity-30 rounded-md" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedPlayback(timeline)}
|
||||||
>
|
>
|
||||||
<ActivityScrubber
|
<ActivityScrubber
|
||||||
items={[]}
|
items={[]}
|
||||||
@ -324,9 +374,6 @@ export default function DesktopTimelineView({
|
|||||||
setScrubbing(false);
|
setScrubbing(false);
|
||||||
playerRef.current?.play();
|
playerRef.current?.play();
|
||||||
}}
|
}}
|
||||||
doubleClickHandler={() => {
|
|
||||||
setSelectedPlayback(timeline);
|
|
||||||
}}
|
|
||||||
selectHandler={(data) => {
|
selectHandler={(data) => {
|
||||||
if (data.items.length > 0) {
|
if (data.items.length > 0) {
|
||||||
const selected = data.items[0];
|
const selected = data.items[0];
|
||||||
@ -338,6 +385,20 @@ export default function DesktopTimelineView({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{graphData && (
|
||||||
|
<div className="w-full absolute left-0 top-0 h-[84px]">
|
||||||
|
<TimelineGraph
|
||||||
|
id={timeline.range.start.toString()}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
name: "Motion",
|
||||||
|
data: graphData.motion,
|
||||||
|
},
|
||||||
|
{ name: "Active Objects", data: graphData.objects },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user