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 ? ( +
+
DPS
+ +
+ ) : ( + + )} +
{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
Recordings