From 7f424bb3f88c8504b2a03dba1bbc800485cf732f Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 14 Apr 2024 10:14:10 -0600 Subject: [PATCH] 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 | 172 +++++++++--------- 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, 224 insertions(+), 195 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 01003816cb..262c114214 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,17 +2618,16 @@ } } }, - "node_modules/@typescript-eslint/parser": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz", - "integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==", + "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/scope-manager": "7.5.0", - "@typescript-eslint/types": "7.5.0", "@typescript-eslint/typescript-estree": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0", - "debug": "^4.3.4" + "@typescript-eslint/utils": "7.5.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2646,14 +2645,19 @@ } } }, - "node_modules/@typescript-eslint/scope-manager": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz", - "integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==", + "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/visitor-keys": "7.5.0" + "@typescript-eslint/typescript-estree": "7.5.0", + "semver": "^7.5.4" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2661,18 +2665,22 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/type-utils": { + "node_modules/@typescript-eslint/parser": { "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz", - "integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz", + "integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==", "dev": true, "dependencies": { + "@typescript-eslint/scope-manager": "7.5.0", + "@typescript-eslint/types": "7.5.0", "@typescript-eslint/typescript-estree": "7.5.0", - "@typescript-eslint/utils": "7.5.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "@typescript-eslint/visitor-keys": "7.5.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2690,6 +2698,23 @@ } } }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz", + "integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.5.0", + "@typescript-eslint/visitor-keys": "7.5.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "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 6a5eccbc3a..626b9a4090 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 cb495220b8..cfcd4a4ff1 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 7aa31152d9..edabf1f1bd 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 4c496fcb5d..5f0348d88d 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 11ef78c1b1..18237c829b 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 4ee9374fac..872289aa04 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 f9d208fd0f..42dc817eea 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 57608a6934..decb87ba5c 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({