From 7e9a7ad49ca856c1cce06bc16233fb188dbba1fe Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 5 Sep 2024 09:51:33 -0500 Subject: [PATCH] Add ffprobe button back to camera metrics page (#13572) --- .../components/overlay/CameraInfoDialog.tsx | 178 ++++++++++++++++++ web/src/types/stats.ts | 14 ++ web/src/views/system/CameraMetrics.tsx | 119 ++++++++---- 3 files changed, 274 insertions(+), 37 deletions(-) create mode 100644 web/src/components/overlay/CameraInfoDialog.tsx diff --git a/web/src/components/overlay/CameraInfoDialog.tsx b/web/src/components/overlay/CameraInfoDialog.tsx new file mode 100644 index 000000000..df14c59db --- /dev/null +++ b/web/src/components/overlay/CameraInfoDialog.tsx @@ -0,0 +1,178 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { Ffprobe } from "@/types/stats"; +import { Button } from "../ui/button"; +import copy from "copy-to-clipboard"; +import { CameraConfig } from "@/types/frigateConfig"; +import { useEffect, useState } from "react"; +import axios from "axios"; +import { toast } from "sonner"; +import { Toaster } from "../ui/sonner"; + +type CameraInfoDialogProps = { + camera: CameraConfig; + showCameraInfoDialog: boolean; + setShowCameraInfoDialog: React.Dispatch>; +}; +export default function CameraInfoDialog({ + camera, + showCameraInfoDialog, + setShowCameraInfoDialog, +}: CameraInfoDialogProps) { + const [ffprobeInfo, setFfprobeInfo] = useState(); + + useEffect(() => { + axios + .get("ffprobe", { + params: { + paths: `camera:${camera.name}`, + }, + }) + .then((res) => { + if (res.status === 200) { + setFfprobeInfo(res.data); + } else { + toast.error(`Unable to probe camera: ${res.statusText}`, { + position: "top-center", + }); + } + }) + .catch((error) => { + toast.error(`Unable to probe camera: ${error.response.data.message}`, { + position: "top-center", + }); + }); + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onCopyFfprobe = async () => { + copy(JSON.stringify(ffprobeInfo)); + toast.success("Copied probe data to clipboard."); + }; + + function gcd(a: number, b: number): number { + return b === 0 ? a : gcd(b, a % b); + } + + return ( + <> + + + + + + {camera.name.replaceAll("_", " ")} Camera Probe Info + + + + Stream data is obtained with ffprobe. + + +
+ {ffprobeInfo ? ( +
+ {ffprobeInfo.map((stream, idx) => ( +
+
+ Stream {idx + 1} +
+ {stream.return_code == 0 ? ( +
+ {stream.stdout.streams.map((codec, idx) => ( +
+ {codec.width ? ( +
+
Video:
+
+
+ Codec: + + {" "} + {codec.codec_long_name} + +
+
+ {codec.width && codec.height ? ( + <> + Resolution:{" "} + + {" "} + {codec.width}x{codec.height} ( + {codec.width / + gcd(codec.width, codec.height)} + / + {codec.height / + gcd(codec.width, codec.height)}{" "} + aspect ratio) + + + ) : ( + + Resolution:{" "} + + Unknown + + + )} +
+
+ FPS:{" "} + + {codec.avg_frame_rate == "0/0" + ? "Unknown" + : codec.avg_frame_rate} + +
+
+
+ ) : ( +
+
Audio:
+
+ Codec:{" "} + + {codec.codec_long_name} + +
+
+ )} +
+ ))} +
+ ) : ( +
+
Error: {stream.stderr}
+
+ )} +
+ ))} +
+ ) : ( +
+ +
Fetching Camera Data
+
+ )} +
+ + + + +
+
+ + ); +} diff --git a/web/src/types/stats.ts b/web/src/types/stats.ts index 2cf277f3d..a4f5605e1 100644 --- a/web/src/types/stats.ts +++ b/web/src/types/stats.ts @@ -70,3 +70,17 @@ export type Vainfo = { stdout: string; stderr: string; }; + +export type Ffprobe = { + return_code: number; + stderr: string; + stdout: { + programs: string[]; + streams: { + avg_frame_rate: string; + codec_long_name: string; + height?: number; + width?: number; + }[]; + }; +}; diff --git a/web/src/views/system/CameraMetrics.tsx b/web/src/views/system/CameraMetrics.tsx index 9b53413f2..764f22e96 100644 --- a/web/src/views/system/CameraMetrics.tsx +++ b/web/src/views/system/CameraMetrics.tsx @@ -1,9 +1,16 @@ import { useFrigateStats } from "@/api/ws"; import { CameraLineGraph } from "@/components/graph/CameraGraph"; +import CameraInfoDialog from "@/components/overlay/CameraInfoDialog"; import { Skeleton } from "@/components/ui/skeleton"; import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateStats } from "@/types/stats"; import { useEffect, useMemo, useState } from "react"; +import { MdInfo } from "react-icons/md"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import useSWR from "swr"; type CameraMetricsProps = { @@ -16,6 +23,11 @@ export default function CameraMetrics({ }: CameraMetricsProps) { const { data: config } = useSWR("config"); + // camera info dialog + + const [showCameraInfoDialog, setShowCameraInfoDialog] = useState(false); + const [probeCameraName, setProbeCameraName] = useState(); + // stats const { data: initialStats } = useSWR( @@ -203,6 +215,12 @@ export default function CameraMetrics({ return series; }, [statsHistory]); + useEffect(() => { + if (!showCameraInfoDialog) { + setProbeCameraName(""); + } + }, [showCameraInfoDialog]); + return (
Overview
@@ -227,45 +245,72 @@ export default function CameraMetrics({ Object.values(config.cameras).map((camera) => { if (camera.enabled) { return ( -
-
- {camera.name.replaceAll("_", " ")} -
-
- {Object.keys(cameraCpuSeries).includes(camera.name) ? ( -
-
CPU
- + <> + {probeCameraName == camera.name && ( + + )} +
+
+
+ {camera.name.replaceAll("_", " ")}
- ) : ( - - )} - {Object.keys(cameraFpsSeries).includes(camera.name) ? ( -
-
Frames / Detections
- -
- ) : ( - - )} + + + { + setShowCameraInfoDialog(true); + setProbeCameraName(camera.name); + }} + /> + + Camera Probe Info + +
+
+ {Object.keys(cameraCpuSeries).includes(camera.name) ? ( +
+
CPU
+ +
+ ) : ( + + )} + {Object.keys(cameraFpsSeries).includes(camera.name) ? ( +
+
Frames / Detections
+ +
+ ) : ( + + )} +
-
+ ); }