From df67a7a2fae6989add15c7dd99a8869138ec5241 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Fri, 13 Jun 2025 05:52:40 +0300 Subject: [PATCH] added videojs --- package-lock.json | 539 +++++++++++++++++- package.json | 3 + src/common/useIdleTimeout.ts | 14 + .../ResourceList/ResourceListDisplay.tsx | 2 +- src/components/VideoPlayer/LoadingVideo.tsx | 1 + src/components/VideoPlayer/VideoControls.tsx | 208 ++++++- .../VideoPlayer/VideoControlsBar.tsx | 42 +- .../VideoPlayer/VideoPlayer-styles.ts | 18 +- src/components/VideoPlayer/VideoPlayer.tsx | 200 ++++++- .../VideoPlayer/useVideoPlayerController.tsx | 15 +- .../VideoPlayer/useVideoPlayerHotKeys.tsx | 28 +- src/hooks/useResourceStatus.tsx | 4 +- src/index.css | 3 +- tsconfig.json | 2 + tsup.config.ts | 1 + 15 files changed, 979 insertions(+), 101 deletions(-) create mode 100644 src/common/useIdleTimeout.ts diff --git a/package-lock.json b/package-lock.json index db937f7..f6143ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,9 +20,11 @@ "idb-keyval": "^6.2.2", "react-dropzone": "^14.3.8", "react-hot-toast": "^2.5.2", + "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.16.0", "short-unique-id": "^5.2.0", "ts-key-enum": "^3.0.13", + "video.js": "^8.23.3", "zustand": "^4.3.2" }, "devDependencies": { @@ -42,6 +44,7 @@ "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.0.1", "@mui/material": "^7.0.1", + "mediainfo.js": "^0.3.5", "react": "^19.0.0" } }, @@ -125,7 +128,6 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1424,6 +1426,75 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "optional": true }, + "node_modules/@videojs/http-streaming": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.17.0.tgz", + "integrity": "sha512-Ch1P3tvvIEezeZXyK11UfWgp4cWKX4vIhZ30baN/lRinqdbakZ5hiAI3pGjRy3d+q/Epyc8Csz5xMdKNNGYpcw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "aes-decrypter": "^4.0.2", + "global": "^4.4.0", + "m3u8-parser": "^7.2.0", + "mpd-parser": "^1.3.1", + "mux.js": "7.1.0", + "video.js": "^7 || ^8" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "video.js": "^8.19.0" + } + }, + "node_modules/@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@videojs/xhr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz", + "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aes-decrypter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz", + "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, "node_modules/aggregate-error": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", @@ -1680,6 +1751,100 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "peer": true + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -1693,7 +1858,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1704,8 +1868,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/commander": { "version": "4.1.1", @@ -1902,6 +2065,11 @@ "csstype": "^3.0.2" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/dompurify": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", @@ -1971,6 +2139,16 @@ "@esbuild/win32-x64": "0.25.2" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2009,10 +2187,11 @@ } }, "node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, + "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -2090,6 +2269,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "peer": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2122,6 +2311,16 @@ "node": ">= 6" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2329,11 +2528,16 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "license": "MIT" + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2480,6 +2684,33 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/m3u8-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz", + "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0" + } + }, + "node_modules/mediainfo.js": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mediainfo.js/-/mediainfo.js-0.3.5.tgz", + "integrity": "sha512-frLJzKOoAUC0sbPzmg9VOR+WFbNj5CarbTuOzXeH9cOl33haU/CGcyXUTWK00HPXCVS2N5eT0o0dirVxaPIOIw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "yargs": "^17.7.2" + }, + "bin": { + "mediainfo.js": "dist/esm/cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/meow": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", @@ -2526,6 +2757,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2550,12 +2789,44 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mpd-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz", + "integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.0.0", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/mux.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz", + "integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + }, + "bin": { + "muxjs-transmux": "bin/transmux.js" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -2567,6 +2838,27 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/nested-error-stacks": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz", @@ -2753,6 +3045,49 @@ "node": ">= 6" } }, + "node_modules/pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.5.5" + }, + "bin": { + "pkcs7": "bin/cli.js" + } + }, + "node_modules/postcss": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postcss-load-config": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", @@ -2795,6 +3130,15 @@ } } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -2891,6 +3235,16 @@ "react-dom": ">=16" } }, + "node_modules/react-idle-timer": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-5.7.2.tgz", + "integrity": "sha512-+BaPfc7XEUU5JFkwZCx6fO1bLVK+RBlFH+iY4X34urvIzZiZINP6v2orePx3E6pAztJGE7t4DzvL7if2SL/0GQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-intersection-observer": { "version": "9.16.0", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", @@ -2948,8 +3302,17 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/resolve": { "version": "1.22.10", @@ -3129,6 +3492,18 @@ "node": ">= 8" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -3293,12 +3668,13 @@ "dev": true }, "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, + "license": "MIT", "dependencies": { - "fdir": "^6.4.3", + "fdir": "^6.4.4", "picomatch": "^4.0.2" }, "engines": { @@ -3427,6 +3803,57 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/video.js": { + "version": "8.23.3", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.23.3.tgz", + "integrity": "sha512-Toe0VLlDZcUhiaWfcePS1OEdT3ATfktm0hk/PELfD7zUoPDHeT+cJf/wZmCy5M5eGVwtGUg25RWPCj1L/1XufA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "^3.17.0", + "@videojs/vhs-utils": "^4.1.1", + "@videojs/xhr": "2.7.0", + "aes-decrypter": "^4.0.2", + "global": "4.4.0", + "m3u8-parser": "^7.2.0", + "mpd-parser": "^1.3.1", + "mux.js": "^7.0.1", + "videojs-contrib-quality-levels": "4.1.0", + "videojs-font": "4.2.0", + "videojs-vtt.js": "0.15.5" + } + }, + "node_modules/videojs-contrib-quality-levels": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz", + "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==", + "license": "Apache-2.0", + "dependencies": { + "global": "^4.4.0" + }, + "engines": { + "node": ">=16", + "npm": ">=8" + }, + "peerDependencies": { + "video.js": "^8" + } + }, + "node_modules/videojs-font": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz", + "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==", + "license": "Apache-2.0" + }, + "node_modules/videojs-vtt.js": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", + "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", + "license": "Apache-2.0", + "dependencies": { + "global": "^4.3.1" + } + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -3558,6 +3985,90 @@ "cuint": "^0.2.2" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "peer": true + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zustand": { "version": "4.5.6", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", diff --git a/package.json b/package.json index f736d6e..7649c95 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,11 @@ "idb-keyval": "^6.2.2", "react-dropzone": "^14.3.8", "react-hot-toast": "^2.5.2", + "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.16.0", "short-unique-id": "^5.2.0", "ts-key-enum": "^3.0.13", + "video.js": "^8.23.3", "zustand": "^4.3.2" }, "peerDependencies": { @@ -44,6 +46,7 @@ "@emotion/styled": "^11.14.0", "@mui/icons-material": "^7.0.1", "@mui/material": "^7.0.1", + "mediainfo.js": "^0.3.5", "react": "^19.0.0" }, "devDependencies": { diff --git a/src/common/useIdleTimeout.ts b/src/common/useIdleTimeout.ts new file mode 100644 index 0000000..1d77527 --- /dev/null +++ b/src/common/useIdleTimeout.ts @@ -0,0 +1,14 @@ +import { useContext, useState } from "react"; +import { useIdleTimer } from "react-idle-timer"; + +const useIdleTimeout = ({ onIdle, onActive, idleTime = 10_000 }: any) => { + const idleTimer = useIdleTimer({ + timeout: idleTime, + onIdle: onIdle, + onActive: onActive, + }); + return { + idleTimer, + }; +}; +export default useIdleTimeout; diff --git a/src/components/ResourceList/ResourceListDisplay.tsx b/src/components/ResourceList/ResourceListDisplay.tsx index cde6eff..499ebe9 100644 --- a/src/components/ResourceList/ResourceListDisplay.tsx +++ b/src/components/ResourceList/ResourceListDisplay.tsx @@ -160,7 +160,7 @@ const addItems = useListStore((s) => s.addItems); - const searchIntervalRef = useRef(null) + const searchIntervalRef = useRef(null) const lastItemTimestampRef = useRef(null) const stringifiedEntityParams = useMemo(()=> { if(!entityParams) return null diff --git a/src/components/VideoPlayer/LoadingVideo.tsx b/src/components/VideoPlayer/LoadingVideo.tsx index de1d3e6..e5eb5a4 100644 --- a/src/components/VideoPlayer/LoadingVideo.tsx +++ b/src/components/VideoPlayer/LoadingVideo.tsx @@ -19,6 +19,7 @@ export const LoadingVideo = ({ const progress = percentLoaded; return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%"; }; + if(status === 'READY') return null return ( <> diff --git a/src/components/VideoPlayer/VideoControls.tsx b/src/components/VideoPlayer/VideoControls.tsx index 601963b..f3c00c8 100644 --- a/src/components/VideoPlayer/VideoControls.tsx +++ b/src/components/VideoPlayer/VideoControls.tsx @@ -1,4 +1,4 @@ -import { Box, IconButton, Slider, Typography } from "@mui/material"; +import { Box, IconButton, Popper, Slider, Typography } from "@mui/material"; export const fontSizeExSmall = "60%"; export const fontSizeSmall = "80%"; import AspectRatioIcon from "@mui/icons-material/AspectRatio"; @@ -13,6 +13,7 @@ import { } from "@mui/icons-material"; import { formatTime } from "../../utils/time.js"; import { CustomFontTooltip } from "./CustomFontTooltip.js"; +import { useCallback, useEffect, useRef, useState } from "react"; const buttonPaddingBig = "6px"; const buttonPaddingSmall = "4px"; @@ -49,36 +50,183 @@ export const ReloadButton = ({reloadVideo, isScreenSmall}: any) => { ); }; -export const ProgressSlider = ({progress, duration, videoRef}: any) => { - const onProgressChange = async (_: any, value: number | number[]) => { - if (!videoRef.current) return; - videoRef.current.currentTime = value as number; - }; - return ( - { + const sliderRef = useRef(null); - "& .MuiSlider-thumb": { - backgroundColor: "#fff", - width: "16px", - height: "16px", - }, - "& .MuiSlider-thumb::after": { width: "20px", height: "20px" }, - "& .MuiSlider-rail": { opacity: 0.5, height: "6px" }, - "& .MuiSlider-track": { height: "6px", border: "0px" }, - }} - /> + const [hoverX, setHoverX] = useState(null); + const [thumbnailUrl, setThumbnailUrl] = useState(null); + const [showDuration, setShowDuration] = useState(0) + const onProgressChange = (_: any, value: number | number[]) => { + if (!playerRef.current) return; + playerRef.current.currentTime(value as number); + }; + + const THUMBNAIL_DEBOUNCE = 500; + const THUMBNAIL_MIN_DIFF = 10; + + const lastRequestedTimeRef = useRef(null); + const debounceTimeoutRef = useRef(null); + const previousBlobUrlRef = useRef(null); + + const debouncedExtract = useCallback( + (time: number, clientX: number) => { + const last = lastRequestedTimeRef.current; + console.log('hello101') + console.log('last', last) + if (last !== null && Math.abs(time - last) < THUMBNAIL_MIN_DIFF) return; + lastRequestedTimeRef.current = time; + console.log('hello102') + + extractFrames(time).then((blobUrl: string | null) => { + console.log('blobUrl', blobUrl) + if (!blobUrl) return; + + // Clean up previous blob URL + if (previousBlobUrlRef.current) { + URL.revokeObjectURL(previousBlobUrlRef.current); + } + + previousBlobUrlRef.current = blobUrl; + setThumbnailUrl(blobUrl); + + }); + }, + [extractFrames] + ); + + const handleMouseMove = (e: React.MouseEvent) => { + const slider = sliderRef.current; + if (!slider) return; + + const rect = slider.getBoundingClientRect(); + const x = e.clientX - rect.left; + const percent = x / rect.width; + const time = Math.min(Math.max(0, percent * duration), duration); + console.log('hello100') + setHoverX(e.clientX); + + setShowDuration(time) + if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current); + + // debounceTimeoutRef.current = setTimeout(() => { + // debouncedExtract(time, e.clientX); + // }, THUMBNAIL_DEBOUNCE); + }; + + const handleMouseLeave = () => { + lastRequestedTimeRef.current = null; + setThumbnailUrl(null); + setHoverX(null); + if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current); + + if (previousBlobUrlRef.current) { + URL.revokeObjectURL(previousBlobUrlRef.current); + previousBlobUrlRef.current = null; + } + }; + + // Clean up on unmount + useEffect(() => { + return () => { + if (previousBlobUrlRef.current) { + URL.revokeObjectURL(previousBlobUrlRef.current); + } + }; + }, []); + + const hoverAnchorRef = useRef(null); + if(hoverX){ +console.log('thumbnailUrl', thumbnailUrl, hoverX) + + } + + return ( + + + + {hoverX !== null && ( + + + {/* + + preview + */} + {formatTime(showDuration)} + + + )} + ); }; diff --git a/src/components/VideoPlayer/VideoControlsBar.tsx b/src/components/VideoPlayer/VideoControlsBar.tsx index 4ce5b38..39a2598 100644 --- a/src/components/VideoPlayer/VideoControlsBar.tsx +++ b/src/components/VideoPlayer/VideoControlsBar.tsx @@ -26,9 +26,15 @@ interface VideoControlsBarProps { reloadVideo: ()=> void; volume: number onVolumeChange: (_: any, val: number)=> void + toggleFullscreen: ()=> void + extractFrames: (time: number)=> void + showControls: boolean; + showControlsFullScreen: boolean; + isFullScreen: boolean; + playerRef: any } -export const VideoControlsBar = ({reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, videoRef, duration, progress, togglePlay}: VideoControlsBarProps) => { +export const VideoControlsBar = ({showControls, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, videoRef, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames}: VideoControlsBarProps) => { const showMobileControls = isScreenSmall && canPlay; @@ -39,23 +45,46 @@ export const VideoControlsBar = ({reloadVideo, onVolumeChange, volume, isPlaying height: controlsHeight, }; + let additionalStyles: React.CSSProperties = {} + if(isFullScreen && showControlsFullScreen){ + additionalStyles = { + opacity: 1, + position: 'fixed', + bottom: 0 + } + } + return ( {showMobileControls ? ( null // ) : canPlay ? ( - <> + + + + - + @@ -65,9 +94,10 @@ export const VideoControlsBar = ({reloadVideo, onVolumeChange, volume, isPlaying - + - + + ) : null} ); diff --git a/src/components/VideoPlayer/VideoPlayer-styles.ts b/src/components/VideoPlayer/VideoPlayer-styles.ts index 5508987..c3a54c4 100644 --- a/src/components/VideoPlayer/VideoPlayer-styles.ts +++ b/src/components/VideoPlayer/VideoPlayer-styles.ts @@ -11,19 +11,33 @@ export const VideoContainer = styled(Box)(({ theme }) => ({ height: "100%", margin: 0, padding: 0, + borderRadius: '12px', + overflow: 'hidden', "&:focus": { outline: "none" }, })); export const VideoElement = styled("video")(({ theme }) => ({ - width: "100%", + position: 'absolute', + top: 0, + bottom: 0, + right: 0, + left: 0, background: "rgb(33, 33, 33)", "&:focus": { outline: "none" }, + "&::-webkit-media-controls": { + display:"none !important" +}, +"&:fullscreen": { + paddingBottom: '50px' +} })); //1075 x 604 export const ControlsContainer = styled(Box)` width: 100%; + position: absolute; + bottom: 0; display: flex; align-items: center; justify-content: space-between; - background-color: rgba(0, 0, 0, 0.6); +background-image: linear-gradient(0deg,#000,#0000); `; diff --git a/src/components/VideoPlayer/VideoPlayer.tsx b/src/components/VideoPlayer/VideoPlayer.tsx index beeb50d..2f0b5c4 100644 --- a/src/components/VideoPlayer/VideoPlayer.tsx +++ b/src/components/VideoPlayer/VideoPlayer.tsx @@ -1,4 +1,4 @@ -import { Ref, RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { ReactEventHandler, Ref, RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { QortalGetMetadata } from "../../types/interfaces/resources"; import { VideoContainer, VideoElement } from "./VideoPlayer-styles"; import { useVideoPlayerHotKeys } from "./useVideoPlayerHotKeys"; @@ -6,14 +6,19 @@ import { useProgressStore, useVideoStore } from "../../state/video"; import { useVideoPlayerController } from "./useVideoPlayerController"; import { LoadingVideo } from "./LoadingVideo"; import { VideoControlsBar } from "./VideoControlsBar"; +import videojs from 'video.js'; +import 'video.js/dist/video-js.css'; + +import Player from "video.js/dist/types/player"; + type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down"; -export interface VideoPlayerProps { + + interface VideoPlayerProps { qortalVideoResource: QortalGetMetadata; videoRef: Ref; retryAttempts?: number; - showControls?: boolean; poster?: string; autoPlay?: boolean; onEnded?: (e: React.SyntheticEvent) => void; @@ -27,7 +32,6 @@ export const VideoPlayer = ({ videoRef, qortalVideoResource, retryAttempts, - showControls, poster, autoPlay, onEnded, @@ -39,14 +43,15 @@ export const VideoPlayer = ({ volume: state.playbackSettings.volume, setVolume: state.setVolume, })); + const playerRef = useRef(null); + const [videoCodec, setVideoCodec] = useState(null) const [isMuted, setIsMuted] = useState(false); const { setProgress } = useProgressStore(); const [localProgress, setLocalProgress] = useState(0) const [duration, setDuration] = useState(0) const [isLoading, setIsLoading] = useState(true); - - + const [showControls, setShowControls] = useState(false) const { reloadVideo, togglePlay, @@ -54,8 +59,6 @@ export const VideoPlayer = ({ increaseSpeed, decreaseSpeed, toggleMute, - showControlsFullScreen, - setShowControlsFullScreen, isFullscreen, toggleObjectFit, controlsHeight, @@ -68,7 +71,8 @@ export const VideoPlayer = ({ startPlay, setProgressAbsolute, setAlwaysShowControls, - status, percentLoaded + status, percentLoaded, + showControlsFullScreen } = useVideoPlayerController({ autoPlay, videoRef, @@ -105,6 +109,13 @@ export const VideoPlayer = ({ ] ); + + + + + + + const videoLocation = useMemo(() => { if (!qortalVideoResource) return null; return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`; @@ -145,29 +156,26 @@ export const VideoPlayer = ({ [setIsMuted, setVolume] ); - const handleMouseEnter = useCallback(() => { - setShowControlsFullScreen(true); - }, [setShowControlsFullScreen]); - const handleMouseLeave = useCallback(() => { - setShowControlsFullScreen(false); - }, [setShowControlsFullScreen]); const videoStylesContainer = useMemo(() => { return { - cursor: !showControlsFullScreen && isFullscreen ? "none" : "auto", + cursor: !showControls && isFullscreen ? "none" : "auto", ...videoStyles?.videoContainer, }; - }, [showControlsFullScreen, isFullscreen]); + }, [showControls, isFullscreen]); + + console.log('isFullscreen', isFullscreen, showControlsFullScreen) const videoStylesVideo = useMemo(() => { return { ...videoStyles?.video, objectFit: videoObjectFit, backgroundColor: "#000000", - height: isFullscreen && showControls ? "calc(100vh - 40px)" : "100%", + height: isFullscreen ? "calc(100vh - 40px)" : "100%", + width: '100%' }; - }, [videoObjectFit, showControls, isFullscreen]); + }, [videoObjectFit, isFullscreen]); const handleEnded = useCallback( (e: React.SyntheticEvent) => { @@ -201,20 +209,156 @@ export const VideoPlayer = ({ }; }, []); + const enterFullscreen = () => { + const ref = containerRef?.current as any; + console.log('refffff', ref) + if (!ref) return; + + if (ref.requestFullscreen && !isFullscreen) { + console.log('requset ') + ref.requestFullscreen(); + } + + + }; + + const exitFullscreen = () => { + if (isFullscreen) document.exitFullscreen(); + }; + + const toggleFullscreen = () => { + isFullscreen ? exitFullscreen() : enterFullscreen(); + }; + +const canvasRef = useRef(null) +const videoRefForCanvas = useRef(null) +const extractFrames = useCallback(async (time: number): Promise => { + const video = videoRefForCanvas?.current; + const canvas: any = canvasRef.current; + + if (!video || !canvas) return null; + + // Avoid unnecessary resize if already correct + if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) { + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + } + + const context = canvas.getContext("2d"); + if (!context) return null; + + // If video is already near the correct time, don't seek again + const threshold = 0.01; // 10ms threshold + if (Math.abs(video.currentTime - time) > threshold) { + await new Promise((resolve) => { + const onSeeked = () => resolve(); + video.addEventListener("seeked", onSeeked, { once: true }); + video.currentTime = time; + }); + } + + context.drawImage(video, 0, 0, canvas.width, canvas.height); + + // Use a faster method for image export (optional tradeoff) + const blob = await new Promise((resolve) => { + canvas.toBlob((blob: any) => resolve(blob), "image/webp", 0.7); + }); + + if (!blob) return null; + + return URL.createObjectURL(blob); +}, []); + + + const hideTimeout = useRef(null); + + +const resetHideTimer = () => { + setShowControls(true); + if (hideTimeout.current) clearTimeout(hideTimeout.current); + hideTimeout.current = setTimeout(() => { + setShowControls(false); + }, 2500); // 3s of inactivity +}; + +const handleMouseMove = () => { + resetHideTimer(); +}; + +useEffect(() => { + resetHideTimer(); // initial show + return () => { + if (hideTimeout.current) clearTimeout(hideTimeout.current); + }; +}, []); + + + + const handleMouseLeave = useCallback(() => { + setShowControls(false); + if (hideTimeout.current) clearTimeout(hideTimeout.current); + }, [setShowControls]); + + const onLoadedMetadata= (e: any)=> { + console.log('eeeeeeeeeee', e) + const ref = videoRef as any; + if (!ref.current) return; + console.log('datataa', ref.current.audioTracks , // List of available audio tracks +ref.current.textTracks , // Subtitles/closed captions +ref.current.videoTracks ) + } + + + useEffect(() => { + if(!resourceUrl || !isReady) return + const options = { + autoplay: true, + controls: false, + responsive: true, + fluid: true, + poster: startPlay ? "" : poster, + sources: [ + { + src: resourceUrl, + type: 'video/mp4' + }, + ], + }; + const ref = videoRef as any; + if (!ref.current) return; + // Only initialize once + if (!playerRef.current && ref.current) { + playerRef.current = videojs(ref.current, options, () => { + playerRef.current?.poster(''); + if (playerRef.current){ + playerRef.current.play() + } + }); + } + + return () => { + if (playerRef.current) { + playerRef.current.dispose(); + playerRef.current = null; + } + }; + }, [isReady, resourceUrl]); + return ( - + + + + {isReady && ( + + )} + + ); }; diff --git a/src/components/VideoPlayer/useVideoPlayerController.tsx b/src/components/VideoPlayer/useVideoPlayerController.tsx index 708afb8..19d6bc5 100644 --- a/src/components/VideoPlayer/useVideoPlayerController.tsx +++ b/src/components/VideoPlayer/useVideoPlayerController.tsx @@ -8,11 +8,10 @@ import { useRef, useImperativeHandle, } from "react"; -import { Key } from "ts-key-enum"; import { useProgressStore, useVideoStore } from "../../state/video"; -import { VideoPlayerProps } from "./VideoPlayer"; import { QortalGetMetadata } from "../../types/interfaces/resources"; import { useResourceStatus } from "../../hooks/useResourceStatus"; +import useIdleTimeout from "../../common/useIdleTimeout"; const controlsHeight = "42px"; const minSpeed = 0.25; @@ -30,10 +29,10 @@ export const useVideoPlayerController = (props: UseVideoControls) => { const { autoPlay, videoRef, qortalVideoResource, retryAttempts } = props; const [isFullscreen, setIsFullscreen] = useState(false); + const [showControlsFullScreen, setShowControlsFullScreen] = useState(false) const [videoObjectFit, setVideoObjectFit] = useState<"contain" | "fill">( "contain" ); - const [showControlsFullScreen, setShowControlsFullScreen] = useState(true); const [alwaysShowControls, setAlwaysShowControls] = useState(false); const [startPlay, setStartPlay] = useState(false); const [startedFetch, setStartedFetch] = useState(false); @@ -47,6 +46,12 @@ export const useVideoPlayerController = (props: UseVideoControls) => { retryAttempts, }); + const idleTime = 5000; // Time in milliseconds + useIdleTimeout({ + onIdle: () => (setShowControlsFullScreen(false)), + onActive: () => (setShowControlsFullScreen(true)), + idleTime, + }); const videoLocation = useMemo(() => { @@ -213,7 +218,6 @@ export const useVideoPlayerController = (props: UseVideoControls) => { increaseSpeed, decreaseSpeed, toggleMute, - showControlsFullScreen, isFullscreen, toggleObjectFit, controlsHeight, @@ -221,12 +225,11 @@ export const useVideoPlayerController = (props: UseVideoControls) => { toggleAlwaysShowControls, changeVolume, setProgressAbsolute, - setShowControlsFullScreen, setAlwaysShowControls, startedFetch, isReady, resourceUrl, startPlay, - status, percentLoaded + status, percentLoaded, showControlsFullScreen }; }; diff --git a/src/components/VideoPlayer/useVideoPlayerHotKeys.tsx b/src/components/VideoPlayer/useVideoPlayerHotKeys.tsx index b06e653..68e4e77 100644 --- a/src/components/VideoPlayer/useVideoPlayerHotKeys.tsx +++ b/src/components/VideoPlayer/useVideoPlayerHotKeys.tsx @@ -1,5 +1,4 @@ import { useEffect, useCallback } from 'react'; -import { Key } from 'ts-key-enum'; interface UseVideoControls { reloadVideo: () => void; @@ -31,15 +30,15 @@ export const useVideoPlayerHotKeys = (props: UseVideoControls) => { const handleKeyDown = useCallback((e: KeyboardEvent) => { const target = e.target as HTMLElement; -const tag = target.tagName.toUpperCase(); -const role = target.getAttribute("role"); -const isTypingOrInteractive = - ["INPUT", "TEXTAREA", "SELECT", "BUTTON"].includes(tag) || - target.isContentEditable || - role === "button"; + const tag = target.tagName.toUpperCase(); + const role = target.getAttribute("role"); + const isTypingOrInteractive = + ["INPUT", "TEXTAREA", "SELECT", "BUTTON"].includes(tag) || + target.isContentEditable || + role === "button"; if (isTypingOrInteractive) return; - e.preventDefault() + e.preventDefault(); const key = e.key; const mod = (s: number) => setProgressRelative(s); @@ -50,32 +49,30 @@ const isTypingOrInteractive = case "c": toggleAlwaysShowControls(); break; - case Key.Add: case "+": case ">": increaseSpeed(false); break; - case Key.Subtract: case "-": case "<": decreaseSpeed(); break; - case Key.ArrowLeft: + case "ArrowLeft": if (e.shiftKey) mod(-300); else if (e.ctrlKey) mod(-60); else if (e.altKey) mod(-10); else mod(-5); break; - case Key.ArrowRight: + case "ArrowRight": if (e.shiftKey) mod(300); else if (e.ctrlKey) mod(60); else if (e.altKey) mod(10); else mod(5); break; - case Key.ArrowDown: + case "ArrowDown": changeVolume(-0.05); break; - case Key.ArrowUp: + case "ArrowUp": changeVolume(0.05); break; case " ": @@ -139,6 +136,5 @@ const isTypingOrInteractive = }; }, [handleKeyDown]); - // Optional: return if you still want manual use - return null + return null; }; diff --git a/src/hooks/useResourceStatus.tsx b/src/hooks/useResourceStatus.tsx index ad9d1f9..a8c861f 100644 --- a/src/hooks/useResourceStatus.tsx +++ b/src/hooks/useResourceStatus.tsx @@ -12,8 +12,8 @@ export const useResourceStatus = ({ }: PropsUseResourceStatus) => { const resourceId = !resource ? null : `${resource.service}-${resource.name}-${resource.identifier}`; const status = usePublishStore((state)=> state.getResourceStatus(resourceId)) || null - const intervalRef = useRef(null) - const timeoutRef = useRef(null) + const intervalRef = useRef(null) + const timeoutRef = useRef(null) const setResourceStatus = usePublishStore((state) => state.setResourceStatus); const statusRef = useRef(null) diff --git a/src/index.css b/src/index.css index e6dc758..99535d7 100644 --- a/src/index.css +++ b/src/index.css @@ -4,4 +4,5 @@ padding: 0px; margin: 0px; box-sizing: border-box; -} \ No newline at end of file +} + diff --git a/tsconfig.json b/tsconfig.json index abbcb06..20b101a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,8 @@ "skipLibCheck": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true + + }, "include": ["src"] } diff --git a/tsup.config.ts b/tsup.config.ts index 6df2718..ca9a14f 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -10,5 +10,6 @@ export default defineConfig({ '@mui/system', '@emotion/react', '@emotion/styled', + 'mediainfo.js' ], });