added videojs

This commit is contained in:
PhilReact 2025-06-13 05:52:40 +03:00
parent 1ba3bfca26
commit df67a7a2fa
15 changed files with 979 additions and 101 deletions

539
package-lock.json generated
View File

@ -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",

View File

@ -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": {

View File

@ -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;

View File

@ -160,7 +160,7 @@ const addItems = useListStore((s) => s.addItems);
const searchIntervalRef = useRef<null | number>(null)
const searchIntervalRef = useRef<any>(null)
const lastItemTimestampRef = useRef<null | number>(null)
const stringifiedEntityParams = useMemo(()=> {
if(!entityParams) return null

View File

@ -19,6 +19,7 @@ export const LoadingVideo = ({
const progress = percentLoaded;
return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%";
};
if(status === 'READY') return null
return (
<>

View File

@ -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 (
<Slider
value={progress}
onChange={onProgressChange}
min={0}
max={duration || 100}
step={0.1}
sx={{
position: "absolute",
bottom: "42px",
color: "#00abff",
padding: "0px",
// prevents the slider from jumping up 20px in certain mobile conditions
"@media (pointer: coarse)": { padding: "0px" },
export const ProgressSlider = ({progress, duration, playerRef, extractFrames}: any) => {
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<number | null>(null);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(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<number | null>(null);
const debounceTimeoutRef = useRef<any>(null);
const previousBlobUrlRef = useRef<string | null>(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<HTMLDivElement | null>(null);
if(hoverX){
console.log('thumbnailUrl', thumbnailUrl, hoverX)
}
return (
<Box position="relative" sx={{
width: '100%',
padding: '0px 10px'
}}>
<Box
ref={hoverAnchorRef}
sx={{
position: 'absolute',
left: hoverX ?? -9999,
top: 0,
width: '1px',
height: '1px',
pointerEvents: 'none',
}}
/>
<Slider
ref={sliderRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
value={progress}
onChange={onProgressChange}
min={0}
max={duration || 100}
step={0.1}
sx={{
color: "#00abff",
padding: "0px",
borderRadius: '0px',
height: '0px',
"@media (pointer: coarse)": { padding: "0px" },
"& .MuiSlider-thumb": {
backgroundColor: "red",
width: "14px",
height: "14px",
},
"& .MuiSlider-thumb::after": { width: "14px", height: "14px", backgroundColor: 'red' },
"& .MuiSlider-rail": { opacity: 0.5, height: "6px", backgroundColor: '#73859f80' },
"& .MuiSlider-track": { height: "6px", border: "0px" , backgroundColor: 'red'},
}}
/>
{hoverX !== null && (
<Popper
open
anchorEl={hoverAnchorRef.current} placement="top"
disablePortal
modifiers={[{ name: "offset", options: { offset: [-20, 0] } }]}
>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}>
{/* <Box
sx={{
width: 250,
height: 125,
backgroundColor: "black",
border: "1px solid white",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: '7px',
background: '#444444',
padding: '2px'
}}
>
<img
src={thumbnailUrl}
alt="preview"
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</Box> */}
<Typography sx={{
fontSize: '0.8rom',
textShadow: '0 0 5px rgba(0, 0, 0, 0.7)'
}}>{formatTime(showDuration)}</Typography>
</Box>
</Popper>
)}
</Box>
);
};

View File

@ -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 (
<ControlsContainer
style={{
padding: "0px",
height: controlsHeight,
opacity: showControls ? 1 : 0,
pointerEvents: showControls ? 'auto' : 'none',
transition: 'opacity 0.4s ease-in-out',
// ...additionalStyles
// height: controlsHeight,
}}
>
{showMobileControls ? (
null
// <MobileControlsBar />
) : canPlay ? (
<>
<Box sx={{
display: 'flex',
flexDirection: 'column',
width: '100%'
}}>
<ProgressSlider extractFrames={extractFrames} playerRef={playerRef} progress={progress} duration={duration} />
<Box sx={{
width: '100%',
display: 'flex'
}}>
<Box sx={controlGroupSX}>
<PlayButton isPlaying={isPlaying} togglePlay={togglePlay}/>
<ReloadButton reloadVideo={reloadVideo} />
<ProgressSlider videoRef={videoRef} progress={progress} duration={duration} />
<VolumeControl onVolumeChange={onVolumeChange} volume={volume} sliderWidth={"100px"} />
<VideoTime videoRef={videoRef} progress={progress}/>
@ -65,9 +94,10 @@ export const VideoControlsBar = ({reloadVideo, onVolumeChange, volume, isPlaying
<PlaybackRate />
<ObjectFitButton />
<PictureInPictureButton />
<FullscreenButton />
<FullscreenButton toggleFullscreen={toggleFullscreen} />
</Box>
</>
</Box>
</Box>
) : null}
</ControlsContainer>
);

View File

@ -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);
`;

View File

@ -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<HTMLVideoElement>;
retryAttempts?: number;
showControls?: boolean;
poster?: string;
autoPlay?: boolean;
onEnded?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => 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<Player | null>(null);
const [videoCodec, setVideoCodec] = useState<null | false | string>(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<HTMLVideoElement, Event>) => {
@ -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<any>(null)
const extractFrames = useCallback(async (time: number): Promise<string | null> => {
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<void>((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<Blob | null>((resolve) => {
canvas.toBlob((blob: any) => resolve(blob), "image/webp", 0.7);
});
if (!blob) return null;
return URL.createObjectURL(blob);
}, []);
const hideTimeout = useRef<any>(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 (
<VideoContainer
tabIndex={0}
style={videoStylesContainer}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
ref={containerRef}
>
<LoadingVideo togglePlay={togglePlay} isReady={isReady} status={status} percentLoaded={percentLoaded} isLoading={isLoading} />
<VideoElement
id={qortalVideoResource?.identifier}
ref={videoRef}
tabIndex={0}
src={isReady && startPlay ? resourceUrl || undefined : undefined}
className="video-js"
// src={isReady && startPlay ? resourceUrl || undefined : undefined}
poster={startPlay ? "" : poster}
onTimeUpdate={updateProgress}
autoPlay={autoPlay}
@ -226,8 +370,18 @@ export const VideoPlayer = ({
onPlay={onPlay}
onPause={onPause}
onVolumeChange={onVolumeChangeHandler}
controls={false}
onLoadedMetadata={onLoadedMetadata}
/>
<VideoControlsBar onVolumeChange={onVolumeChange} volume={volume} togglePlay={togglePlay} reloadVideo={hotkeyHandlers.reloadVideo} isPlaying={isPlaying} canPlay={true} isScreenSmall={false} controlsHeight={controlsHeight} videoRef={videoRef} duration={duration} progress={localProgress} />
<canvas ref={canvasRef} style={{ display: "none" }}></canvas>
<video src={isReady && startPlay ? resourceUrl || undefined : undefined} ref={videoRefForCanvas} style={{ display: "none" }}></video>
{isReady && (
<VideoControlsBar playerRef={playerRef} isFullScreen={isFullscreen} showControlsFullScreen={showControlsFullScreen} showControls={showControls} extractFrames={extractFrames} toggleFullscreen={toggleFullscreen} onVolumeChange={onVolumeChange} volume={volume} togglePlay={togglePlay} reloadVideo={hotkeyHandlers.reloadVideo} isPlaying={isPlaying} canPlay={true} isScreenSmall={false} controlsHeight={controlsHeight} videoRef={videoRef} duration={duration} progress={localProgress} />
)}
</VideoContainer>
);
};

View File

@ -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
};
};

View File

@ -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;
};

View File

@ -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 | number>(null)
const timeoutRef = useRef<null | number>(null)
const intervalRef = useRef<any>(null)
const timeoutRef = useRef<any>(null)
const setResourceStatus = usePublishStore((state) => state.setResourceStatus);
const statusRef = useRef<ResourceStatus | null>(null)

View File

@ -4,4 +4,5 @@
padding: 0px;
margin: 0px;
box-sizing: border-box;
}
}

View File

@ -9,6 +9,8 @@
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"]
}

View File

@ -10,5 +10,6 @@ export default defineConfig({
'@mui/system',
'@emotion/react',
'@emotion/styled',
'mediainfo.js'
],
});