forked from Github/frigate
Improve graph using pandas (#9234)
* Ensure viewport is always full screen * Protect against hour with no cards and ensure data is consistent * Reduce grouped up image refreshes * Include current hour and fix scrubbing bugginess * Scroll initially selected timeline in to view * Expand timelne class type * Use poster image for preview on video player instead of using separate image view * Fix available streaming modes * Incrase timing for grouping timline items * Fix audio activity listener * Fix player not switching views correctly * Use player time to convert to timeline time * Update sub labels for previous timeline items * Show mini timeline bar for non selected items * Rewrite desktop timeline to use separate dynamic video player component * Extend improvements to mobile as well * Improve time formatting * Fix scroll * Fix no preview case * Mobile fixes * Audio toggle fixes * More fixes for mobile * Improve scaling of graph motion activity * Add keyboard shortcut hook and support shortcuts for playback page * Fix sizing of dialog * Improve height scaling of dialog * simplify and fix layout system for timeline * Fix timeilne items not working * Implement basic Frigate+ submitting from timeline
This commit is contained in:
committed by
Blake Blackshear
parent
9c4b69191b
commit
af3f6dadcb
@@ -282,6 +282,14 @@ export function getRangeForTimestamp(timestamp: number) {
|
||||
date.setMinutes(0, 0, 0);
|
||||
const start = date.getTime() / 1000;
|
||||
date.setHours(date.getHours() + 1);
|
||||
const end = date.getTime() / 1000;
|
||||
|
||||
// ensure not to go past current time
|
||||
const end = Math.min(new Date().getTime() / 1000, date.getTime() / 1000);
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
export function isCurrentHour(timestamp: number) {
|
||||
const now = new Date();
|
||||
now.setMinutes(0, 0, 0);
|
||||
return timestamp > now.getTime() / 1000;
|
||||
}
|
||||
|
||||
@@ -1,158 +1,178 @@
|
||||
// group history cards by 60 seconds of activity
|
||||
const GROUP_SECONDS = 60;
|
||||
// group history cards by 120 seconds of activity
|
||||
const GROUP_SECONDS = 120;
|
||||
|
||||
export function getHourlyTimelineData(
|
||||
timelinePages: HourlyTimeline[],
|
||||
detailLevel: string
|
||||
): CardsData {
|
||||
const cards: CardsData = {};
|
||||
const allHours: { [key: string]: Timeline[] } = {};
|
||||
|
||||
timelinePages.forEach((hourlyTimeline) => {
|
||||
Object.keys(hourlyTimeline["hours"])
|
||||
.reverse()
|
||||
.forEach((hour) => {
|
||||
const day = new Date(parseInt(hour) * 1000);
|
||||
day.setHours(0, 0, 0, 0);
|
||||
const dayKey = (day.getTime() / 1000).toString();
|
||||
|
||||
// build a map of course to the types that are included in this hour
|
||||
// which allows us to know what items to keep depending on detail level
|
||||
const source_to_types: { [key: string]: string[] } = {};
|
||||
let cardTypeStart: { [camera: string]: number } = {};
|
||||
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
|
||||
if (i.timestamp > (cardTypeStart[i.camera] ?? 0) + GROUP_SECONDS) {
|
||||
cardTypeStart[i.camera] = i.timestamp;
|
||||
}
|
||||
|
||||
const groupKey = `${i.source_id}-${cardTypeStart[i.camera]}`;
|
||||
|
||||
if (groupKey in source_to_types) {
|
||||
source_to_types[groupKey].push(i.class_type);
|
||||
} else {
|
||||
source_to_types[groupKey] = [i.class_type];
|
||||
}
|
||||
});
|
||||
|
||||
if (!(dayKey in cards)) {
|
||||
cards[dayKey] = {};
|
||||
}
|
||||
|
||||
if (!(hour in cards[dayKey])) {
|
||||
cards[dayKey][hour] = {};
|
||||
}
|
||||
|
||||
let cardStart: { [camera: string]: number } = {};
|
||||
Object.values(hourlyTimeline["hours"][hour]).forEach((i) => {
|
||||
if (i.timestamp > (cardStart[i.camera] ?? 0) + GROUP_SECONDS) {
|
||||
cardStart[i.camera] = i.timestamp;
|
||||
}
|
||||
|
||||
const time = new Date(i.timestamp * 1000);
|
||||
const groupKey = `${i.camera}-${cardStart[i.camera]}`;
|
||||
const sourceKey = `${i.source_id}-${cardStart[i.camera]}`;
|
||||
const uniqueKey = `${i.source_id}-${i.class_type}`;
|
||||
|
||||
// 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[sourceKey].length > 1 &&
|
||||
["active", "attribute", "gone", "stationary", "visible"].includes(
|
||||
i.class_type
|
||||
)
|
||||
) {
|
||||
add = false;
|
||||
}
|
||||
} else if (detailLevel == "extra") {
|
||||
if (
|
||||
source_to_types[sourceKey].length > 1 &&
|
||||
i.class_type in ["attribute", "gone", "visible"]
|
||||
) {
|
||||
add = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (add) {
|
||||
if (groupKey in cards[dayKey][hour]) {
|
||||
if (
|
||||
!cards[dayKey][hour][groupKey].uniqueKeys.includes(uniqueKey) ||
|
||||
detailLevel == "full"
|
||||
) {
|
||||
cards[dayKey][hour][groupKey].entries.push(i);
|
||||
cards[dayKey][hour][groupKey].uniqueKeys.push(uniqueKey);
|
||||
}
|
||||
} else {
|
||||
cards[dayKey][hour][groupKey] = {
|
||||
camera: i.camera,
|
||||
time: time.getTime() / 1000,
|
||||
entries: [i],
|
||||
uniqueKeys: [uniqueKey],
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Object.entries(hourlyTimeline.hours).forEach(([key, values]) => {
|
||||
if (key in allHours) {
|
||||
// only occurs when multiple pages contain elements in the same hour
|
||||
allHours[key] = allHours[key]
|
||||
.concat(values)
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
} else {
|
||||
allHours[key] = values;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(allHours)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.reverse()
|
||||
.forEach((hour) => {
|
||||
const day = new Date(parseInt(hour) * 1000);
|
||||
day.setHours(0, 0, 0, 0);
|
||||
const dayKey = (day.getTime() / 1000).toString();
|
||||
|
||||
// build a map of course to the types that are included in this hour
|
||||
// which allows us to know what items to keep depending on detail level
|
||||
const sourceToTypes: { [key: string]: string[] } = {};
|
||||
let cardTypeStart: { [camera: string]: number } = {};
|
||||
Object.values(allHours[hour]).forEach((i) => {
|
||||
if (i.timestamp > (cardTypeStart[i.camera] ?? 0) + GROUP_SECONDS) {
|
||||
cardTypeStart[i.camera] = i.timestamp;
|
||||
}
|
||||
|
||||
const groupKey = `${i.source_id}-${cardTypeStart[i.camera]}`;
|
||||
|
||||
if (groupKey in sourceToTypes) {
|
||||
sourceToTypes[groupKey].push(i.class_type);
|
||||
} else {
|
||||
sourceToTypes[groupKey] = [i.class_type];
|
||||
}
|
||||
});
|
||||
|
||||
if (!(dayKey in cards)) {
|
||||
cards[dayKey] = {};
|
||||
}
|
||||
|
||||
if (!(hour in cards[dayKey])) {
|
||||
cards[dayKey][hour] = {};
|
||||
}
|
||||
|
||||
let cardStart: { [camera: string]: number } = {};
|
||||
Object.values(allHours[hour]).forEach((i) => {
|
||||
if (i.timestamp > (cardStart[i.camera] ?? 0) + GROUP_SECONDS) {
|
||||
cardStart[i.camera] = i.timestamp;
|
||||
}
|
||||
|
||||
const time = new Date(i.timestamp * 1000);
|
||||
const groupKey = `${i.camera}-${cardStart[i.camera]}`;
|
||||
const sourceKey = `${i.source_id}-${cardStart[i.camera]}`;
|
||||
const uniqueKey = `${i.source_id}-${i.class_type}`;
|
||||
|
||||
// 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;
|
||||
const sourceType = sourceToTypes[sourceKey];
|
||||
let hiddenItems: string[] = [];
|
||||
if (detailLevel == "normal") {
|
||||
hiddenItems = [
|
||||
"active",
|
||||
"attribute",
|
||||
"gone",
|
||||
"stationary",
|
||||
"visible",
|
||||
];
|
||||
} else if (detailLevel == "extra") {
|
||||
hiddenItems = ["attribute", "gone", "visible"];
|
||||
}
|
||||
|
||||
if (sourceType.length > 1) {
|
||||
// we have multiple timeline items for this card
|
||||
|
||||
if (
|
||||
sourceType.find((type) => hiddenItems.includes(type) == false) ==
|
||||
undefined
|
||||
) {
|
||||
// all of the attribute items for this card make it hidden, but we need to show one
|
||||
if (sourceType.indexOf(i.class_type) != 0) {
|
||||
add = false;
|
||||
}
|
||||
} else if (hiddenItems.includes(i.class_type)) {
|
||||
add = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (add) {
|
||||
if (groupKey in cards[dayKey][hour]) {
|
||||
if (
|
||||
!cards[dayKey][hour][groupKey].uniqueKeys.includes(uniqueKey) ||
|
||||
detailLevel == "full"
|
||||
) {
|
||||
cards[dayKey][hour][groupKey].entries.push(i);
|
||||
cards[dayKey][hour][groupKey].uniqueKeys.push(uniqueKey);
|
||||
}
|
||||
} else {
|
||||
cards[dayKey][hour][groupKey] = {
|
||||
camera: i.camera,
|
||||
time: time.getTime() / 1000,
|
||||
entries: [i],
|
||||
uniqueKeys: [uniqueKey],
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
export function getTimelineHoursForDay(
|
||||
camera: string,
|
||||
cards: CardsData,
|
||||
allPreviews: Preview[],
|
||||
cameraPreviews: Preview[],
|
||||
timestamp: number
|
||||
): HistoryTimeline {
|
||||
const now = new Date();
|
||||
const endOfThisHour = new Date();
|
||||
endOfThisHour.setHours(endOfThisHour.getHours() + 1, 0, 0, 0);
|
||||
const data: TimelinePlayback[] = [];
|
||||
const startDay = new Date(timestamp * 1000);
|
||||
startDay.setHours(23, 59, 59, 999);
|
||||
const dayEnd = startDay.getTime() / 1000;
|
||||
startDay.setHours(0, 0, 0, 0);
|
||||
const startTimestamp = startDay.getTime() / 1000;
|
||||
let start = startDay.getTime() / 1000;
|
||||
let end = 0;
|
||||
|
||||
const relevantPreviews = allPreviews.filter((preview) => {
|
||||
return (
|
||||
preview.camera == camera &&
|
||||
preview.start >= start &&
|
||||
Math.floor(preview.end - 1) <= dayEnd
|
||||
);
|
||||
});
|
||||
|
||||
const dayIdx = Object.keys(cards).find((day) => {
|
||||
if (parseInt(day) > start) {
|
||||
if (parseInt(day) < start) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (dayIdx == undefined) {
|
||||
return { start: 0, end: 0, playbackItems: [] };
|
||||
}
|
||||
let day: {
|
||||
[hour: string]: {
|
||||
[groupKey: string]: Card;
|
||||
};
|
||||
} = {};
|
||||
|
||||
const day = cards[dayIdx];
|
||||
if (dayIdx != undefined) {
|
||||
day = cards[dayIdx];
|
||||
}
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
startDay.setHours(startDay.getHours() + 1);
|
||||
|
||||
if (startDay > now) {
|
||||
if (startDay > endOfThisHour) {
|
||||
break;
|
||||
}
|
||||
|
||||
end = startDay.getTime() / 1000;
|
||||
const hour = Object.values(day).find((cards) => {
|
||||
if (
|
||||
Object.values(cards)[0].time < start ||
|
||||
Object.values(cards)[0].time > end
|
||||
) {
|
||||
const card = Object.values(cards)[0];
|
||||
if (card == undefined || card.time < start || card.time > end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -167,7 +187,7 @@ export function getTimelineHoursForDay(
|
||||
return [];
|
||||
})
|
||||
: [];
|
||||
const relevantPreview = relevantPreviews.find(
|
||||
const relevantPreview = cameraPreviews.find(
|
||||
(preview) =>
|
||||
Math.round(preview.start) >= start && Math.floor(preview.end) <= end
|
||||
);
|
||||
|
||||
@@ -42,15 +42,6 @@ export function getTimelineIcon(timelineItem: Timeline) {
|
||||
default:
|
||||
return <LuTruck className="w-4 mr-1" />;
|
||||
}
|
||||
case "sub_label":
|
||||
switch (timelineItem.data.label) {
|
||||
case "person":
|
||||
return <MdFaceUnlock className="w-4 mr-1" />;
|
||||
case "car":
|
||||
return <MdOutlinePictureInPictureAlt className="w-4 mr-1" />;
|
||||
default:
|
||||
return <LuCircleDot className="w-4 mr-1" />;
|
||||
}
|
||||
case "heard":
|
||||
return <LuEar className="w-4 mr-1" />;
|
||||
case "external":
|
||||
@@ -119,8 +110,6 @@ export function getTimelineItemDescription(timelineItem: Timeline) {
|
||||
}
|
||||
return title;
|
||||
}
|
||||
case "sub_label":
|
||||
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
|
||||
case "gone":
|
||||
return `${label} left`;
|
||||
case "heard":
|
||||
|
||||
Reference in New Issue
Block a user