diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx
index 5afea161b..a7a54ae60 100644
--- a/web/src/components/player/PreviewThumbnailPlayer.tsx
+++ b/web/src/components/player/PreviewThumbnailPlayer.tsx
@@ -13,13 +13,14 @@ import { getIconForLabel } from "@/utils/iconUtil";
import TimeAgo from "../dynamic/TimeAgo";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
-import { isFirefox, isMobile, isSafari } from "react-device-detect";
+import { isFirefox, isIOS, isMobile, isSafari } from "react-device-detect";
import Chip from "@/components/indicators/Chip";
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import useImageLoaded from "@/hooks/use-image-loaded";
import { useSwipeable } from "react-swipeable";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
+import useContextMenu from "@/hooks/use-contextmenu";
type PreviewPlayerProps = {
review: ReviewSegment;
@@ -73,6 +74,10 @@ export default function PreviewThumbnailPlayer({
setReviewed(review);
}, [review, setReviewed]);
+ useContextMenu(imgRef, () => {
+ onClick(review, true);
+ });
+
// playback
const relevantPreview = useMemo(() => {
@@ -170,10 +175,6 @@ export default function PreviewThumbnailPlayer({
className="relative size-full cursor-pointer"
onMouseOver={isMobile ? undefined : () => setIsHovered(true)}
onMouseLeave={isMobile ? undefined : () => setIsHovered(false)}
- onContextMenu={(e) => {
- e.preventDefault();
- onClick(review, true);
- }}
onClick={handleOnClick}
{...swipeHandlers}
>
@@ -196,9 +197,18 @@ export default function PreviewThumbnailPlayer({
{
diff --git a/web/src/hooks/use-contextmenu.ts b/web/src/hooks/use-contextmenu.ts
new file mode 100644
index 000000000..f121846ae
--- /dev/null
+++ b/web/src/hooks/use-contextmenu.ts
@@ -0,0 +1,46 @@
+import { MutableRefObject, useEffect } from "react";
+import { isIOS } from "react-device-detect";
+
+export default function useContextMenu(
+ ref: MutableRefObject
,
+ callback: () => void,
+) {
+ useEffect(() => {
+ if (!ref.current) {
+ return;
+ }
+
+ const elem = ref.current;
+
+ if (isIOS) {
+ let timeoutId: NodeJS.Timeout;
+ const touchStart = () => {
+ timeoutId = setTimeout(() => {
+ callback();
+ }, 610);
+ };
+ const touchClear = () => {
+ clearTimeout(timeoutId);
+ };
+ elem.addEventListener("touchstart", touchStart);
+ elem.addEventListener("touchmove", touchClear);
+ elem.addEventListener("touchend", touchClear);
+
+ return () => {
+ elem.removeEventListener("touchstart", touchStart);
+ elem.removeEventListener("touchmove", touchClear);
+ elem.removeEventListener("touchend", touchClear);
+ };
+ } else {
+ const context = (e: MouseEvent) => {
+ e.preventDefault();
+ callback();
+ };
+ elem.addEventListener("contextmenu", context);
+
+ return () => {
+ elem.removeEventListener("contextmenu", context);
+ };
+ }
+ }, [callback, ref]);
+}
diff --git a/web/src/views/system/CameraMetrics.tsx b/web/src/views/system/CameraMetrics.tsx
index fa78ec5a9..54b6debd5 100644
--- a/web/src/views/system/CameraMetrics.tsx
+++ b/web/src/views/system/CameraMetrics.tsx
@@ -19,7 +19,7 @@ export default function CameraMetrics({
// stats
const { data: initialStats } = useSWR(
- ["stats/history", { keys: "cpu_usages,cameras,service" }],
+ ["stats/history", { keys: "cpu_usages,cameras,detection_fps,service" }],
{
revalidateOnFocus: false,
},
@@ -57,6 +57,44 @@ export default function CameraMetrics({
// stats data
+ const overallFpsSeries = useMemo(() => {
+ if (!statsHistory) {
+ return [];
+ }
+
+ const series: {
+ [key: string]: { name: string; data: { x: number; y: number }[] };
+ } = {};
+
+ series["overall_dps"] = { name: "overall detections per second", data: [] };
+ series["overall_skipped_dps"] = {
+ name: "overall skipped detections per second",
+ data: [],
+ };
+
+ statsHistory.forEach((stats, statsIdx) => {
+ if (!stats) {
+ return;
+ }
+
+ series["overall_dps"].data.push({
+ x: statsIdx,
+ y: stats.detection_fps,
+ });
+
+ let skipped = 0;
+ Object.values(stats.cameras).forEach(
+ (camStat) => (skipped += camStat.skipped_fps),
+ );
+
+ series["overall_skipped_dps"].data.push({
+ x: statsIdx,
+ y: skipped,
+ });
+ });
+ return Object.values(series);
+ }, [statsHistory]);
+
const cameraCpuSeries = useMemo(() => {
if (!statsHistory || statsHistory.length == 0) {
return {};
@@ -147,19 +185,36 @@ export default function CameraMetrics({
}, [statsHistory]);
return (
-
+
+
Overview
+
+ {statsHistory.length != 0 ? (
+
+ ) : (
+
+ )}
+
{config &&
Object.values(config.cameras).map((camera) => {
if (camera.enabled) {
return (
-
-
+
+
{camera.name.replaceAll("_", " ")}
{Object.keys(cameraCpuSeries).includes(camera.name) ? (
-
+
CPU
)}
{Object.keys(cameraFpsSeries).includes(camera.name) ? (
-
+
DPS
{statsHistory.length != 0 ? (
-
+
Detector Inference Speed
{detInferenceTimeSeries.map((series) => (
)}
{statsHistory.length != 0 ? (
-
+
Detector CPU Usage
{detCpuSeries.map((series) => (
)}
{statsHistory.length != 0 ? (
-
+
Detector Memory Usage
{detMemSeries.map((series) => (
{statsHistory.length != 0 ? (
-
+
GPU Usage
{gpuSeries.map((series) => (
)}
{statsHistory.length != 0 ? (
-
-
GPU Memory
- {gpuMemSeries.map((series) => (
-
- ))}
-
+ <>
+ {gpuMemSeries && (
+
+
GPU Memory
+ {gpuMemSeries.map((series) => (
+
+ ))}
+
+ )}
+ >
) : (
)}
@@ -402,7 +414,7 @@ export default function GeneralMetrics({
{statsHistory.length != 0 ? (
-
+
Process CPU Usage
{otherProcessCpuSeries.map((series) => (
)}
{statsHistory.length != 0 ? (
-
+
Process Memory Usage
{otherProcessMemSeries.map((series) => (
-
- General Storage
-
+ Overview