From b65656fa8733c1c2f3d944f716d2e9493ae7c99f Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sat, 13 Apr 2024 19:25:58 +0200 Subject: [PATCH 1/4] Make wsdl_dir path resolving more robust (#10967) Relying on importlib to resolve the path to the wsdl directory is more robust, since it traverses all site-packages directories, that are part of the PYTHONPATH. --- frigate/ptz/onvif.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index 335e2ee29..2cd903709 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -1,8 +1,9 @@ """Configure and control camera via onvif.""" import logging -import site from enum import Enum +from importlib.util import find_spec +from pathlib import Path import numpy from onvif import ONVIFCamera, ONVIFError @@ -50,10 +51,7 @@ class OnvifController: cam.onvif.port, cam.onvif.user, cam.onvif.password, - wsdl_dir=site.getsitepackages()[0].replace( - "dist-packages", "site-packages" - ) - + "/wsdl", + wsdl_dir=Path(find_spec("onvif").origin).parent / "../wsdl", ), "init": False, "active": False, From a3e2171675ac78163e8be859d877ff1fede2f9e1 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 14 Apr 2024 10:06:11 -0600 Subject: [PATCH 2/4] Implement score filtering on Frigate+ Page (#10968) * Fix portrait layout disappearing * Refactor sliders * Reuse camera filter * Reuse label filter content * Implement score slider including keyboard input * Implement ability to sort frigate plus submissions --- frigate/api/event.py | 16 +- .../components/filter/ReviewFilterGroup.tsx | 4 +- .../player/PreviewThumbnailPlayer.tsx | 6 +- web/src/components/player/VideoControls.tsx | 4 +- web/src/components/ui/slider-no-thumb.tsx | 26 -- web/src/components/ui/slider-volume.tsx | 26 -- web/src/components/ui/slider.tsx | 75 ++- web/src/pages/SubmitPlus.tsx | 430 ++++++++++++------ web/src/views/events/RecordingView.tsx | 2 +- 9 files changed, 384 insertions(+), 205 deletions(-) delete mode 100644 web/src/components/ui/slider-no-thumb.tsx delete mode 100644 web/src/components/ui/slider-volume.tsx diff --git a/frigate/api/event.py b/frigate/api/event.py index 7cdb933c9..17fc5c776 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -77,6 +77,8 @@ def events(): min_length = request.args.get("min_length", type=float) max_length = request.args.get("max_length", type=float) + sort = request.args.get("sort", type=str) + clauses = [] selected_columns = [ @@ -219,10 +221,22 @@ def events(): if len(clauses) == 0: clauses.append((True)) + if sort: + if sort == "score_asc": + order_by = Event.data["score"].asc() + elif sort == "score_desc": + order_by = Event.data["score"].desc() + elif sort == "date_asc": + Event.start_time.asc() + elif sort == "date_desc": + Event.start_time.desc() + else: + order_by = Event.start_time.desc() + events = ( Event.select(*selected_columns) .where(reduce(operator.and_, clauses)) - .order_by(Event.start_time.desc()) + .order_by(order_by) .limit(limit) .dicts() .iterator() diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index 2038aef42..a92399539 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -209,7 +209,7 @@ type CameraFilterButtonProps = { selectedCameras: string[] | undefined; updateCameraFilter: (cameras: string[] | undefined) => void; }; -function CamerasFilterButton({ +export function CamerasFilterButton({ allCameras, groups, selectedCameras, @@ -227,7 +227,7 @@ function CamerasFilterButton({ size="sm" > = 1 ? "text-selected-foreground" : "text-secondary-foreground"}`} />
- - {video.muted == false && ( - , - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - - -)); -Slider.displayName = SliderPrimitive.Root.displayName; - -export { Slider }; diff --git a/web/src/components/ui/slider-volume.tsx b/web/src/components/ui/slider-volume.tsx deleted file mode 100644 index 1493488a3..000000000 --- a/web/src/components/ui/slider-volume.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from "react"; -import * as SliderPrimitive from "@radix-ui/react-slider"; - -import { cn } from "@/lib/utils"; - -const Slider = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - - -)); -Slider.displayName = SliderPrimitive.Root.displayName; - -export { Slider }; diff --git a/web/src/components/ui/slider.tsx b/web/src/components/ui/slider.tsx index e161daec0..0f57209d8 100644 --- a/web/src/components/ui/slider.tsx +++ b/web/src/components/ui/slider.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import * as SliderPrimitive from "@radix-ui/react-slider" +import * as React from "react"; +import * as SliderPrimitive from "@radix-ui/react-slider"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Slider = React.forwardRef< React.ElementRef, @@ -11,7 +11,7 @@ const Slider = React.forwardRef< ref={ref} className={cn( "relative flex w-full touch-none select-none items-center", - className + className, )} {...props} > @@ -20,7 +20,68 @@ const Slider = React.forwardRef< -)) -Slider.displayName = SliderPrimitive.Root.displayName +)); +Slider.displayName = SliderPrimitive.Root.displayName; -export { Slider } +const VolumeSlider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +VolumeSlider.displayName = SliderPrimitive.Root.displayName; + +const NoThumbSlider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +NoThumbSlider.displayName = SliderPrimitive.Root.displayName; + +const DualThumbSlider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + + +)); +DualThumbSlider.displayName = SliderPrimitive.Root.displayName; + +export { DualThumbSlider, Slider, NoThumbSlider, VolumeSlider }; diff --git a/web/src/pages/SubmitPlus.tsx b/web/src/pages/SubmitPlus.tsx index 831b0370c..88c449362 100644 --- a/web/src/pages/SubmitPlus.tsx +++ b/web/src/pages/SubmitPlus.tsx @@ -1,5 +1,8 @@ import { baseUrl } from "@/api/baseUrl"; -import FilterCheckBox from "@/components/filter/FilterCheckBox"; +import { + CamerasFilterButton, + GeneralFilterContent, +} from "@/components/filter/ReviewFilterGroup"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -13,16 +16,25 @@ import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { DropdownMenu, DropdownMenuContent, - DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { DualThumbSlider } from "@/components/ui/slider"; import { Event } from "@/types/event"; import { FrigateConfig } from "@/types/frigateConfig"; import axios from "axios"; import { useCallback, useEffect, useMemo, useState } from "react"; import { isMobile } from "react-device-detect"; -import { FaList, FaVideo } from "react-icons/fa"; +import { + FaList, + FaSort, + FaSortAmountDown, + FaSortAmountUp, +} from "react-icons/fa"; +import { PiSlidersHorizontalFill } from "react-icons/pi"; import useSWR from "swr"; export default function SubmitPlus() { @@ -36,6 +48,11 @@ export default function SubmitPlus() { const [selectedCameras, setSelectedCameras] = useState(); const [selectedLabels, setSelectedLabels] = useState(); + const [scoreRange, setScoreRange] = useState(); + + // sort + + const [sort, setSort] = useState(); // data @@ -47,6 +64,9 @@ export default function SubmitPlus() { is_submitted: 0, cameras: selectedCameras ? selectedCameras.join(",") : null, labels: selectedLabels ? selectedLabels.join(",") : null, + min_score: scoreRange ? scoreRange[0] : null, + max_score: scoreRange ? scoreRange[1] : null, + sort: sort ? sort : null, }, ]); const [upload, setUpload] = useState(); @@ -104,12 +124,17 @@ export default function SubmitPlus() { return (
- +
+ + +
void; selectedLabels: string[] | undefined; + selectedScoreRange: number[] | undefined; + setSelectedCameras: (cameras: string[] | undefined) => void; setSelectedLabels: (cameras: string[] | undefined) => void; + setSelectedScoreRange: (range: number[] | undefined) => void; }; function PlusFilterGroup({ selectedCameras, - setSelectedCameras, selectedLabels, + selectedScoreRange, + setSelectedCameras, setSelectedLabels, + setSelectedScoreRange, }: PlusFilterGroupProps) { const { data: config } = useSWR("config"); @@ -217,97 +246,28 @@ function PlusFilterGroup({ return [...labels].sort(); }, [config, selectedCameras]); - const [open, setOpen] = useState<"none" | "camera" | "label">("none"); - const [currentCameras, setCurrentCameras] = useState( - undefined, + const [open, setOpen] = useState<"none" | "camera" | "label" | "score">( + "none", ); const [currentLabels, setCurrentLabels] = useState( undefined, ); + const [currentScoreRange, setCurrentScoreRange] = useState< + number[] | undefined + >(undefined); const Menu = isMobile ? Drawer : DropdownMenu; const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger; const Content = isMobile ? DrawerContent : DropdownMenuContent; return ( -
- { - if (!open) { - setCurrentCameras(selectedCameras); - } - setOpen(open ? "camera" : "none"); - }} - > - - - - - - Filter Cameras - - - { - if (isChecked) { - setCurrentCameras(undefined); - } - }} - /> - -
- {allCameras.map((item) => ( - { - if (isChecked) { - const updatedCameras = currentCameras - ? [...currentCameras] - : []; - - updatedCameras.push(item); - setCurrentCameras(updatedCameras); - } else { - const updatedCameras = currentCameras - ? [...currentCameras] - : []; - - // can not deselect the last item - if (updatedCameras.length > 1) { - updatedCameras.splice(updatedCameras.indexOf(item), 1); - setCurrentCameras(updatedCameras); - } - } - }} - /> - ))} -
- -
- -
-
-
+
+ { @@ -318,8 +278,14 @@ function PlusFilterGroup({ }} > - - - Filter Labels - - - { - if (isChecked) { - setCurrentLabels(undefined); - } - }} + setOpen("none")} /> - -
- {allLabels.map((item) => ( - { - if (isChecked) { - const updatedLabels = currentLabels - ? [...currentLabels] - : []; - - updatedLabels.push(item); - setCurrentLabels(updatedLabels); - } else { - const updatedLabels = currentLabels - ? [...currentLabels] - : []; - - // can not deselect the last item - if (updatedLabels.length > 1) { - updatedLabels.splice(updatedLabels.indexOf(item), 1); - setCurrentLabels(updatedLabels); - } - } - }} - /> - ))} + +
+ { + setOpen(open ? "score" : "none"); + }} + > + + + + +
+ + setCurrentScoreRange([ + parseInt(e.target.value) / 100.0, + currentScoreRange?.at(1) ?? 1.0, + ]) + } + /> + + + setCurrentScoreRange([ + currentScoreRange?.at(0) ?? 0.5, + parseInt(e.target.value) / 100.0, + ]) + } + />
-
+
+ +
+ +
+
+ ); +} + +type PlusSortSelectorProps = { + selectedSort?: string; + setSelectedSort: (sort: string | undefined) => void; +}; +function PlusSortSelector({ + selectedSort, + setSelectedSort, +}: PlusSortSelectorProps) { + // menu state + + const [open, setOpen] = useState(false); + + // sort + + const [currentSort, setCurrentSort] = useState(); + const [currentDir, setCurrentDir] = useState("desc"); + + // components + + const Sort = selectedSort + ? selectedSort.split("_")[1] == "desc" + ? FaSortAmountDown + : FaSortAmountUp + : FaSort; + const Menu = isMobile ? Drawer : DropdownMenu; + const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger; + const Content = isMobile ? DrawerContent : DropdownMenuContent; + + return ( +
+ { + setOpen(open); + + if (!open) { + const parts = selectedSort?.split("_"); + + if (parts?.length == 2) { + setCurrentSort(parts[0]); + setCurrentDir(parts[1]); + } + } + }} + > + + + + + setCurrentSort(value)} + > +
+ + + {currentSort == "date" ? ( + currentDir == "desc" ? ( + setCurrentDir("asc")} + /> + ) : ( + setCurrentDir("desc")} + /> + ) + ) : ( +
+ )} +
+
+ + + {currentSort == "score" ? ( + currentDir == "desc" ? ( + setCurrentDir("asc")} + /> + ) : ( + setCurrentDir("desc")} + /> + ) + ) : ( +
+ )} +
+ + +
+ +
diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index c4ac49f62..57608a693 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -366,7 +366,7 @@ export function RecordingView({ key={mainCamera} className={ isDesktop - ? `${mainCameraAspect == "tall" ? "xl:h-[90%]" : mainCameraAspect == "wide" ? "w-full" : "w-[78%]"} px-4 flex justify-center` + ? `${mainCameraAspect == "tall" ? "h-[50%] md:h-[60%] lg:h-[75%] xl:h-[90%]" : mainCameraAspect == "wide" ? "w-full" : "w-[78%]"} px-4 flex justify-center` : `portrait:w-full pt-2 ${mainCameraAspect == "wide" ? "landscape:w-full aspect-wide" : "landscape:h-[94%] aspect-video"}` } style={{ From 7f424bb3f88c8504b2a03dba1bbc800485cf732f Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 14 Apr 2024 10:14:10 -0600 Subject: [PATCH 3/4] UI Improvements (#10972) * Update web deps * Fix tooltip on storage page * Always show video controls even when zooming * Get video controls working when video is paused * Fix control hovering * Add loading indicator to logs tab * Show metrics correctly when hovering graph * Show loading indicators for previews on recordings page * Remove vitest update * remove unused * Make volume props optional --- web/package-lock.json | 192 +++++++++--------- web/package.json | 16 +- web/src/components/graph/SystemGraph.tsx | 7 +- .../indicators/activity-indicator.tsx | 4 +- web/src/components/player/HlsVideoPlayer.tsx | 139 +++++++------ web/src/components/player/PreviewPlayer.tsx | 13 ++ web/src/components/player/VideoControls.tsx | 33 ++- .../player/dynamic/DynamicVideoPlayer.tsx | 17 +- web/src/pages/Logs.tsx | 6 +- web/src/views/events/RecordingView.tsx | 4 +- web/src/views/system/GeneralMetrics.tsx | 8 +- 11 files changed, 234 insertions(+), 205 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 01003816c..262c11421 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -34,10 +34,10 @@ "clsx": "^2.1.0", "copy-to-clipboard": "^3.3.3", "date-fns": "^3.6.0", - "hls.js": "^1.5.7", + "hls.js": "^1.5.8", "idb-keyval": "^6.2.1", "immer": "^10.0.4", - "lucide-react": "^0.365.0", + "lucide-react": "^0.368.0", "monaco-yaml": "^5.1.1", "next-themes": "^0.3.0", "react": "^18.2.0", @@ -45,7 +45,7 @@ "react-day-picker": "^8.9.1", "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", - "react-hook-form": "^7.51.2", + "react-hook-form": "^7.51.3", "react-icons": "^5.0.1", "react-router-dom": "^6.22.3", "react-swipeable": "^7.0.1", @@ -68,9 +68,9 @@ "devDependencies": { "@tailwindcss/forms": "^0.5.7", "@testing-library/jest-dom": "^6.1.5", - "@types/node": "^20.12.5", - "@types/react": "^18.2.74", - "@types/react-dom": "^18.2.24", + "@types/node": "^20.12.7", + "@types/react": "^18.2.78", + "@types/react-dom": "^18.2.25", "@types/react-icons": "^3.0.0", "@types/react-transition-group": "^4.4.10", "@types/strftime": "^0.9.8", @@ -93,9 +93,9 @@ "postcss": "^8.4.38", "prettier": "^3.2.5", "tailwindcss": "^3.4.3", - "typescript": "^5.4.4", + "typescript": "^5.4.5", "vite": "^5.2.8", - "vitest": "^1.3.1" + "vitest": "^1.4.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -715,9 +715,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "node_modules/@inquirer/confirm": { @@ -2507,9 +2507,9 @@ } }, "node_modules/@types/node": { - "version": "20.12.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz", - "integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==", + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -2522,9 +2522,9 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "18.2.74", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.74.tgz", - "integrity": "sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==", + "version": "18.2.78", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.78.tgz", + "integrity": "sha512-qOwdPnnitQY4xKlKayt42q5W5UQrSHjgoXNVEtxeqdITJ99k4VXJOP3vt8Rkm9HmgJpH50UNU+rlqfkfWOqp0A==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -2532,9 +2532,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.24", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.24.tgz", - "integrity": "sha512-cN6upcKd8zkGy4HU9F1+/s98Hrp6D4MOcippK4PoE8OZRngohHZpbJn1GsaDLz87MqvHNoT13nHvNqM9ocRHZg==", + "version": "18.2.25", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.25.tgz", + "integrity": "sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA==", "devOptional": true, "dependencies": { "@types/react": "*" @@ -2618,6 +2618,58 @@ } } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz", + "integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.5.0", + "@typescript-eslint/utils": "7.5.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz", + "integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.5.0", + "@typescript-eslint/types": "7.5.0", + "@typescript-eslint/typescript-estree": "7.5.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, "node_modules/@typescript-eslint/parser": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz", @@ -2663,33 +2715,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz", - "integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "7.5.0", - "@typescript-eslint/utils": "7.5.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/types": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz", @@ -2755,31 +2780,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/utils": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz", - "integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.5.0", - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/typescript-estree": "7.5.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz", @@ -2945,9 +2945,9 @@ "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -4476,9 +4476,9 @@ } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/follow-redirects": { @@ -4622,9 +4622,9 @@ } }, "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -4703,9 +4703,9 @@ "dev": true }, "node_modules/hls.js": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.7.tgz", - "integrity": "sha512-Hnyf7ojTBtXHeOW1/t6wCBJSiK1WpoKF9yg7juxldDx8u3iswrkPt2wbOA/1NiwU4j27DSIVoIEJRAhcdMef/A==" + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.8.tgz", + "integrity": "sha512-hJYMPfLhWO7/7+n4f9pn6bOheCGx0WgvVz7k3ouq3Pp1bja48NN+HeCQu3XCGYzqWQF/wo7Sk6dJAyWVJD8ECA==" }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", @@ -5279,9 +5279,9 @@ } }, "node_modules/lucide-react": { - "version": "0.365.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.365.0.tgz", - "integrity": "sha512-sJYpPyyzGHI4B3pys+XSFnE4qtSWc68rFnDLxbNNKjkLST5XSx9DNn5+1Z3eFgFiw39PphNRiVBSVb+AL3oKwA==", + "version": "0.368.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.368.0.tgz", + "integrity": "sha512-soryVrCjheZs8rbXKdINw9B8iPi5OajBJZMJ1HORig89ljcOcEokKKAgGbg3QWxSXel7JwHOfDFUdDHAKyUAMw==", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } @@ -6261,9 +6261,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.51.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.2.tgz", - "integrity": "sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==", + "version": "7.51.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz", + "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==", "engines": { "node": ">=12.22.0" }, @@ -7284,9 +7284,9 @@ "dev": true }, "node_modules/tinypool": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", - "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.3.tgz", + "integrity": "sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==", "dev": true, "engines": { "node": ">=14.0.0" @@ -7421,9 +7421,9 @@ } }, "node_modules/typescript": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz", - "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/web/package.json b/web/package.json index 6a5eccbc3..626b9a409 100644 --- a/web/package.json +++ b/web/package.json @@ -39,10 +39,10 @@ "clsx": "^2.1.0", "copy-to-clipboard": "^3.3.3", "date-fns": "^3.6.0", - "hls.js": "^1.5.7", + "hls.js": "^1.5.8", "idb-keyval": "^6.2.1", "immer": "^10.0.4", - "lucide-react": "^0.365.0", + "lucide-react": "^0.368.0", "monaco-yaml": "^5.1.1", "next-themes": "^0.3.0", "react": "^18.2.0", @@ -50,7 +50,7 @@ "react-day-picker": "^8.9.1", "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", - "react-hook-form": "^7.51.2", + "react-hook-form": "^7.51.3", "react-icons": "^5.0.1", "react-router-dom": "^6.22.3", "react-swipeable": "^7.0.1", @@ -73,9 +73,9 @@ "devDependencies": { "@tailwindcss/forms": "^0.5.7", "@testing-library/jest-dom": "^6.1.5", - "@types/node": "^20.12.5", - "@types/react": "^18.2.74", - "@types/react-dom": "^18.2.24", + "@types/node": "^20.12.7", + "@types/react": "^18.2.78", + "@types/react-dom": "^18.2.25", "@types/react-icons": "^3.0.0", "@types/react-transition-group": "^4.4.10", "@types/strftime": "^0.9.8", @@ -98,8 +98,8 @@ "postcss": "^8.4.38", "prettier": "^3.2.5", "tailwindcss": "^3.4.3", - "typescript": "^5.4.4", + "typescript": "^5.4.5", "vite": "^5.2.8", - "vitest": "^1.3.1" + "vitest": "^1.4.0" } } diff --git a/web/src/components/graph/SystemGraph.tsx b/web/src/components/graph/SystemGraph.tsx index cb495220b..cfcd4a4ff 100644 --- a/web/src/components/graph/SystemGraph.tsx +++ b/web/src/components/graph/SystemGraph.tsx @@ -92,6 +92,9 @@ export function ThresholdBarGraph({ }, tooltip: { theme: systemTheme || theme, + y: { + formatter: (val) => `${val}${unit}`, + }, }, markers: { size: 0, @@ -118,7 +121,7 @@ export function ThresholdBarGraph({ min: 0, }, } as ApexCharts.ApexOptions; - }, [graphId, threshold, systemTheme, theme, formatTime]); + }, [graphId, threshold, unit, systemTheme, theme, formatTime]); useEffect(() => { ApexCharts.exec(graphId, "updateOptions", options, true, true); @@ -190,7 +193,7 @@ export function StorageGraph({ graphId, used, total }: StorageGraphProps) { }, }, tooltip: { - show: false, + enabled: false, }, xaxis: { axisBorder: { diff --git a/web/src/components/indicators/activity-indicator.tsx b/web/src/components/indicators/activity-indicator.tsx index 7aa31152d..edabf1f1b 100644 --- a/web/src/components/indicators/activity-indicator.tsx +++ b/web/src/components/indicators/activity-indicator.tsx @@ -1,9 +1,9 @@ import { LuLoader2 } from "react-icons/lu"; -export default function ActivityIndicator({ size = 30 }) { +export default function ActivityIndicator({ className = "w-full", size = 30 }) { return (
diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 4c496fcb5..5f0348d88 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -1,10 +1,4 @@ -import { - MutableRefObject, - ReactNode, - useEffect, - useRef, - useState, -} from "react"; +import { MutableRefObject, useEffect, useRef, useState } from "react"; import Hls from "hls.js"; import { isAndroid, isDesktop, isMobile } from "react-device-detect"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; @@ -19,7 +13,6 @@ const unsupportedErrorCodes = [ ]; type HlsVideoPlayerProps = { - children?: ReactNode; videoRef: MutableRefObject; visible: boolean; currentSource: string; @@ -30,7 +23,6 @@ type HlsVideoPlayerProps = { onPlaying?: () => void; }; export default function HlsVideoPlayer({ - children, videoRef, visible, currentSource, @@ -83,19 +75,88 @@ export default function HlsVideoPlayer({ // controls const [isPlaying, setIsPlaying] = useState(true); + const [muted, setMuted] = useState(true); + const [volume, setVolume] = useState(1.0); const [mobileCtrlTimeout, setMobileCtrlTimeout] = useState(); const [controls, setControls] = useState(isMobile); const [controlsOpen, setControlsOpen] = useState(false); + useEffect(() => { + if (!isDesktop) { + return; + } + + const callback = (e: MouseEvent) => { + if (!videoRef.current) { + return; + } + + const rect = videoRef.current.getBoundingClientRect(); + + if ( + e.clientX > rect.left && + e.clientX < rect.right && + e.clientY > rect.top && + e.clientY < rect.bottom + ) { + setControls(true); + } else { + setControls(controlsOpen); + } + }; + window.addEventListener("mousemove", callback); + return () => { + window.removeEventListener("mousemove", callback); + }; + }, [videoRef, controlsOpen]); + return ( + { + if (!videoRef.current) { + return; + } + + if (play) { + videoRef.current.play(); + } else { + videoRef.current.pause(); + } + }} + onSeek={(diff) => { + const currentTime = videoRef.current?.currentTime; + + if (!videoRef.current || !currentTime) { + return; + } + + videoRef.current.currentTime = Math.max(0, currentTime + diff); + }} + onSetPlaybackRate={(rate) => + videoRef.current ? (videoRef.current.playbackRate = rate) : null + } + /> setControls(!controls), + }} contentStyle={{ width: "100%", height: isMobile ? "100%" : undefined, @@ -108,7 +169,8 @@ export default function HlsVideoPlayer({ autoPlay controls={false} playsInline - muted + muted={muted} + onVolumeChange={() => setVolume(videoRef.current?.volume ?? 1.0)} onPlay={() => { setIsPlaying(true); @@ -145,61 +207,6 @@ export default function HlsVideoPlayer({ } }} /> -
{ - setControls(true); - } - : undefined - } - onMouseOut={ - isDesktop - ? () => { - setControls(controlsOpen); - } - : undefined - } - onClick={isDesktop ? undefined : () => setControls(!controls)} - > -
- { - if (!videoRef.current) { - return; - } - - if (play) { - videoRef.current.play(); - } else { - videoRef.current.pause(); - } - }} - onSeek={(diff) => { - const currentTime = videoRef.current?.currentTime; - - if (!videoRef.current || !currentTime) { - return; - } - - videoRef.current.currentTime = Math.max(0, currentTime + diff); - }} - onSetPlaybackRate={(rate) => - videoRef.current ? (videoRef.current.playbackRate = rate) : null - } - /> - {children} -
-
); diff --git a/web/src/components/player/PreviewPlayer.tsx b/web/src/components/player/PreviewPlayer.tsx index 11ef78c1b..18237c829 100644 --- a/web/src/components/player/PreviewPlayer.tsx +++ b/web/src/components/player/PreviewPlayer.tsx @@ -14,6 +14,7 @@ import { isCurrentHour } from "@/utils/dateUtil"; import { baseUrl } from "@/api/baseUrl"; import { isAndroid, isChrome, isMobile } from "react-device-detect"; import { TimeRange } from "@/types/timeline"; +import { Skeleton } from "../ui/skeleton"; type PreviewPlayerProps = { className?: string; @@ -143,6 +144,8 @@ function PreviewVideoPlayer({ // initial state + const [firstLoad, setFirstLoad] = useState(true); + const initialPreview = useMemo(() => { return cameraPreviews.find( (preview) => @@ -253,6 +256,10 @@ function PreviewVideoPlayer({ disableRemotePlayback onSeeked={onPreviewSeeked} onLoadedData={() => { + if (firstLoad) { + setFirstLoad(false); + } + if (controller) { controller.previewReady(); } else { @@ -280,6 +287,7 @@ function PreviewVideoPlayer({ No Preview Found
)} + {firstLoad && }
); } @@ -427,6 +435,8 @@ function PreviewFramesPlayer({ // initial state + const [firstLoad, setFirstLoad] = useState(true); + useEffect(() => { if (!controller) { return; @@ -441,6 +451,8 @@ function PreviewFramesPlayer({ }, [controller]); const onImageLoaded = useCallback(() => { + setFirstLoad(false); + if (!controller) { return; } @@ -477,6 +489,7 @@ function PreviewFramesPlayer({ No Preview Found
)} + {firstLoad && }
); } diff --git a/web/src/components/player/VideoControls.tsx b/web/src/components/player/VideoControls.tsx index 4ee9374fa..872289aa0 100644 --- a/web/src/components/player/VideoControls.tsx +++ b/web/src/components/player/VideoControls.tsx @@ -38,11 +38,14 @@ type VideoControlsProps = { features?: VideoControls; isPlaying: boolean; show: boolean; + muted?: boolean; + volume?: number; controlsOpen?: boolean; playbackRates?: number[]; playbackRate: number; hotKeys?: boolean; setControlsOpen?: (open: boolean) => void; + setMuted?: (muted: boolean) => void; onPlayPause: (play: boolean) => void; onSeek: (diff: number) => void; onSetPlaybackRate: (rate: number) => void; @@ -53,11 +56,14 @@ export default function VideoControls({ features = CONTROLS_DEFAULT, isPlaying, show, + muted, + volume, controlsOpen, playbackRates = PLAYBACK_RATE_DEFAULT, playbackRate, hotKeys = true, setControlsOpen, + setMuted, onPlayPause, onSeek, onSetPlaybackRate, @@ -89,18 +95,18 @@ export default function VideoControls({ // volume control const VolumeIcon = useMemo(() => { - if (!video || video?.muted) { + if (!volume || volume == 0.0 || muted) { return MdVolumeOff; - } else if (video.volume <= 0.33) { + } else if (volume <= 0.33) { return MdVolumeMute; - } else if (video.volume <= 0.67) { + } else if (volume <= 0.67) { return MdVolumeDown; } else { return MdVolumeUp; } // only update when specific fields change // eslint-disable-next-line react-hooks/exhaustive-deps - }, [video?.volume, video?.muted]); + }, [volume, muted]); const onKeyboardShortcut = useCallback( (key: string, down: boolean, repeat: boolean) => { @@ -116,8 +122,8 @@ export default function VideoControls({ } break; case "m": - if (down && !repeat && video) { - video.muted = !video.muted; + if (setMuted && down && !repeat && video) { + setMuted(!muted); } break; case " ": @@ -150,13 +156,16 @@ export default function VideoControls({ className="size-5" onClick={(e: React.MouseEvent) => { e.stopPropagation(); - video.muted = !video.muted; + + if (setMuted) { + setMuted(!muted); + } }} /> - {video.muted == false && ( + {muted == false && ( onSetPlaybackRate(parseFloat(rate))} > {playbackRates.map((rate) => ( - + {rate}x ))} diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index f9d208fd0..42dc817ee 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -1,5 +1,4 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import TimelineEventOverlay from "../../overlay/TimelineDataOverlay"; import { useApiHost } from "@/api"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; @@ -8,7 +7,7 @@ import { Preview } from "@/types/preview"; import PreviewPlayer, { PreviewController } from "../PreviewPlayer"; import { DynamicVideoController } from "./DynamicVideoController"; import HlsVideoPlayer from "../HlsVideoPlayer"; -import { TimeRange, Timeline } from "@/types/timeline"; +import { TimeRange } from "@/types/timeline"; /** * Dynamically switches between video playback and scrubbing preview player. @@ -45,9 +44,6 @@ export default function DynamicVideoPlayer({ const playerRef = useRef(null); const [previewController, setPreviewController] = useState(null); - const [focusedItem, setFocusedItem] = useState( - undefined, - ); const controller = useMemo(() => { if (!config || !playerRef.current || !previewController) { return undefined; @@ -59,7 +55,7 @@ export default function DynamicVideoPlayer({ previewController, (config.cameras[camera]?.detect?.annotation_offset || 0) / 1000, isScrubbing ? "scrubbing" : "playback", - setFocusedItem, + () => {}, ); // we only want to fire once when players are ready // eslint-disable-next-line react-hooks/exhaustive-deps @@ -164,14 +160,7 @@ export default function DynamicVideoPlayer({ setIsLoading(false); }} - > - {config && focusedItem && ( - - )} - + /> )} -
+
Type
@@ -443,6 +444,9 @@ function Logs() { })} {logLines.length > 0 &&
}
+ {logLines.length == 0 && ( + + )}
); diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index 57608a693..decb87ba5 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -364,11 +364,11 @@ export function RecordingView({ >
0 ? Object.values(series) : []; @@ -215,7 +215,7 @@ export default function GeneralMetrics({ series[key] = { name: key, data: [] }; } - series[key].data.push({ x: statsIdx + 1, y: stats.mem }); + series[key].data.push({ x: statsIdx + 1, y: stats.mem.slice(0, -1) }); }); }); return Object.values(series); @@ -373,7 +373,7 @@ export default function GeneralMetrics({ key={series.name} graphId={`${series.name}-gpu`} name={series.name} - unit="" + unit="%" threshold={GPUUsageThreshold} updateTimes={updateTimes} data={[series]} @@ -392,7 +392,7 @@ export default function GeneralMetrics({ Date: Sun, 14 Apr 2024 14:43:43 -0600 Subject: [PATCH 4/4] More UI tweaks (#10974) * Show loading indicator when switching between preview and recording if it takes a while * Scroll down to avoid automatic upscroll --- .../player/dynamic/DynamicVideoPlayer.tsx | 15 ++++++++++++--- web/src/pages/Logs.tsx | 3 +++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index 42dc817ee..d3cf4ee66 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -8,6 +8,7 @@ import PreviewPlayer, { PreviewController } from "../PreviewPlayer"; import { DynamicVideoController } from "./DynamicVideoController"; import HlsVideoPlayer from "../HlsVideoPlayer"; import { TimeRange } from "@/types/timeline"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; /** * Dynamically switches between video playback and scrubbing preview player. @@ -77,6 +78,7 @@ export default function DynamicVideoPlayer({ // initial state const [isLoading, setIsLoading] = useState(false); + const [loadingTimeout, setLoadingTimeout] = useState(); const [source, setSource] = useState( `${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`, ); @@ -84,8 +86,8 @@ export default function DynamicVideoPlayer({ // start at correct time useEffect(() => { - if (isScrubbing) { - setIsLoading(true); + if (!isScrubbing) { + setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000)); } }, [isScrubbing]); @@ -133,7 +135,7 @@ export default function DynamicVideoPlayer({ setSource( `${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`, ); - setIsLoading(true); + setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000)); controller.newPlayback({ recordings: recordings ?? [], @@ -158,6 +160,10 @@ export default function DynamicVideoPlayer({ playerRef.current?.pause(); } + if (loadingTimeout) { + clearTimeout(loadingTimeout); + } + setIsLoading(false); }} /> @@ -172,6 +178,9 @@ export default function DynamicVideoPlayer({ setPreviewController(previewController); }} /> + {isLoading && ( + + )} ); } diff --git a/web/src/pages/Logs.tsx b/web/src/pages/Logs.tsx index ec860848f..e735d9a6f 100644 --- a/web/src/pages/Logs.tsx +++ b/web/src/pages/Logs.tsx @@ -279,6 +279,9 @@ function Logs() { } }) .catch(() => {}); + contentRef.current?.scrollBy({ + top: 10, + }); } }); if (node) startObserver.current.observe(node);