From 816ee8de1476badfdb18d1ad44dd76a95fc91091 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 14 Oct 2025 12:08:40 +0200 Subject: [PATCH] Web player fixes & fullscreen --- front/bun.lock | 42 ++++++++- front/package.json | 2 + front/src/ui/player/controls/index.tsx | 16 ++-- front/src/ui/player/controls/misc.tsx | 39 +++++++-- front/src/ui/player/controls/progress.tsx | 4 +- front/src/ui/player/controls/touch.tsx | 3 +- front/src/ui/player/index.tsx | 85 +++++++++++++----- front/src/ui/player/old/media-session.tsx | 102 ---------------------- front/src/utils.ts | 11 ++- 9 files changed, 154 insertions(+), 150 deletions(-) delete mode 100644 front/src/ui/player/old/media-session.tsx diff --git a/front/bun.lock b/front/bun.lock index 0732b929..10da79ae 100644 --- a/front/bun.lock +++ b/front/bun.lock @@ -39,6 +39,8 @@ "react-native-web": "^0.21.1", "react-tooltip": "^5.29.1", "sweetalert2": "^11.23.0", + "uuid": "^13.0.0", + "video.js": "^8.23.4", "yoshiki": "1.2.14", "zod": "^4.1.11", }, @@ -545,6 +547,12 @@ "@urql/exchange-retry": ["@urql/exchange-retry@1.3.2", "", { "dependencies": { "@urql/core": "^5.1.2", "wonka": "^6.3.2" } }, "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg=="], + "@videojs/http-streaming": ["@videojs/http-streaming@3.17.2", "", { "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" } }, "sha512-VBQ3W4wnKnVKb/limLdtSD2rAd5cmHN70xoMf4OmuDd0t2kfJX04G+sfw6u2j8oOm2BXYM9E1f4acHruqKnM1g=="], + + "@videojs/vhs-utils": ["@videojs/vhs-utils@4.1.1", "", { "dependencies": { "@babel/runtime": "^7.12.5", "global": "^4.4.0" } }, "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA=="], + + "@videojs/xhr": ["@videojs/xhr@2.7.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "global": "~4.4.0", "is-function": "^1.0.1" } }, "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.10", "", {}, "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], @@ -553,6 +561,8 @@ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "aes-decrypter": ["aes-decrypter@4.0.2", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "^4.1.1", "global": "^4.4.0", "pkcs7": "^1.0.4" } }, "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -729,6 +739,8 @@ "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + "dom-walk": ["dom-walk@0.1.2", "", {}, "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="], + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], @@ -875,6 +887,8 @@ "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + "global": ["global@4.4.0", "", { "dependencies": { "min-document": "^2.19.0", "process": "^0.11.10" } }, "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w=="], + "global-dirs": ["global-dirs@0.1.1", "", { "dependencies": { "ini": "^1.3.4" } }, "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -931,6 +945,8 @@ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-function": ["is-function@1.0.2", "", {}, "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], @@ -1025,6 +1041,8 @@ "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "m3u8-parser": ["m3u8-parser@7.2.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "^4.1.1", "global": "^4.4.0" } }, "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ=="], + "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], @@ -1073,6 +1091,8 @@ "mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], + "min-document": ["min-document@2.19.0", "", { "dependencies": { "dom-walk": "^0.1.0" } }, "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ=="], + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -1083,8 +1103,12 @@ "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + "mpd-parser": ["mpd-parser@1.3.1", "", { "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" } }, "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mux.js": ["mux.js@7.1.0", "", { "dependencies": { "@babel/runtime": "^7.11.2", "global": "^4.4.0" }, "bin": { "muxjs-transmux": "bin/transmux.js" } }, "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -1163,6 +1187,8 @@ "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + "pkcs7": ["pkcs7@1.0.4", "", { "dependencies": { "@babel/runtime": "^7.5.5" }, "bin": { "pkcs7": "bin/cli.js" } }, "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ=="], + "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], "pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="], @@ -1177,6 +1203,8 @@ "proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="], + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], "promise": ["promise@8.3.0", "", { "dependencies": { "asap": "~2.0.6" } }, "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg=="], @@ -1227,7 +1255,7 @@ "react-native-svg-transformer": ["react-native-svg-transformer@1.5.1", "", { "dependencies": { "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "path-dirname": "^1.0.2" }, "peerDependencies": { "react-native": ">=0.59.0", "react-native-svg": ">=12.0.0" } }, "sha512-dFvBNR8A9VPum9KCfh+LE49YiJEF8zUSnEFciKQroR/bEOhlPoZA0SuQ0qNk7m2iZl2w59FYjdRe0pMHWMDl0Q=="], - "react-native-video": ["react-native-video@github:zoriya/react-native-video#0bb0dd5", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": ">=0.27.2" } }, "zoriya-react-native-video-0bb0dd5"], + "react-native-video": ["react-native-video@github:zoriya/react-native-video#6613ecf", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": ">=0.27.2" } }, "zoriya-react-native-video-6613ecf"], "react-native-web": ["react-native-web@0.21.1", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-BeNsgwwe4AXUFPAoFU+DKjJ+CVQa3h54zYX77p7GVZrXiiNo3vl03WYDYVEy5R2J2HOPInXtQZB5gmj3vuzrKg=="], @@ -1435,7 +1463,7 @@ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], @@ -1443,6 +1471,14 @@ "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], + "video.js": ["video.js@8.23.4", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@videojs/http-streaming": "^3.17.2", "@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" } }, "sha512-qI0VTlYmKzEqRsz1Nppdfcaww4RSxZAq77z2oNSl3cNg2h6do5C8Ffl0KqWQ1OpD8desWXsCrde7tKJ9gGTEyQ=="], + + "videojs-contrib-quality-levels": ["videojs-contrib-quality-levels@4.1.0", "", { "dependencies": { "global": "^4.4.0" }, "peerDependencies": { "video.js": "^8" } }, "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA=="], + + "videojs-font": ["videojs-font@4.2.0", "", {}, "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ=="], + + "videojs-vtt.js": ["videojs-vtt.js@0.15.5", "", { "dependencies": { "global": "^4.3.1" } }, "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ=="], + "vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], @@ -1713,6 +1749,8 @@ "write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "xcode/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="], + "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], "yoshiki/@types/react": ["@types/react@18.3.23", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w=="], diff --git a/front/package.json b/front/package.json index 99f6629b..cd691682 100644 --- a/front/package.json +++ b/front/package.json @@ -48,6 +48,8 @@ "react-native-web": "^0.21.1", "react-tooltip": "^5.29.1", "sweetalert2": "^11.23.0", + "uuid": "^13.0.0", + "video.js": "^8.23.4", "yoshiki": "1.2.14", "zod": "^4.1.11" }, diff --git a/front/src/ui/player/controls/index.tsx b/front/src/ui/player/controls/index.tsx index dea8d039..366acfa7 100644 --- a/front/src/ui/player/controls/index.tsx +++ b/front/src/ui/player/controls/index.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import type { ViewProps } from "react-native"; -import { StyleSheet } from "react-native"; +import { StyleSheet, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { VideoPlayer } from "react-native-video"; import { useYoshiki } from "yoshiki/native"; @@ -45,16 +45,16 @@ export const Controls = ({ } satisfies ViewProps; return ( - + + - + ); }; diff --git a/front/src/ui/player/controls/misc.tsx b/front/src/ui/player/controls/misc.tsx index f3b18cc6..642a44b3 100644 --- a/front/src/ui/player/controls/misc.tsx +++ b/front/src/ui/player/controls/misc.tsx @@ -6,9 +6,9 @@ import VolumeDown from "@material-symbols/svg-400/rounded/volume_down-fill.svg"; import VolumeMute from "@material-symbols/svg-400/rounded/volume_mute-fill.svg"; import VolumeOff from "@material-symbols/svg-400/rounded/volume_off-fill.svg"; import VolumeUp from "@material-symbols/svg-400/rounded/volume_up-fill.svg"; -import { type ComponentProps, useState } from "react"; +import { type ComponentProps, useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { type PressableProps, View } from "react-native"; +import { type PressableProps, View, Platform } from "react-native"; import { useEvent, type VideoPlayer } from "react-native-video"; import { px, useYoshiki } from "yoshiki/native"; import { @@ -46,18 +46,39 @@ export const PlayButton = ({ ); }; +export const toggleFullscreen = async (set?: boolean) => { + set ??= document.fullscreenElement === null; + try { + if (set) { + await document.body.requestFullscreen({ navigationUI: "hide" }); + // @ts-expect-error Firefox does not support this so ts complains + await screen.orientation.lock("landscape"); + } else { + if (document.fullscreenElement) await document.exitFullscreen(); + screen.orientation.unlock(); + } + } catch (e) { + console.log(e); + } +}; + export const FullscreenButton = ( props: Partial>>, ) => { + // this is a web only component const { t } = useTranslation(); - // TODO: actually implement that - const [fullscreen, setFullscreen] = useState(true); + const [fullscreen, setFullscreen] = useState(false); + useEffect(() => { + const update = () => setFullscreen(document.fullscreenElement !== null); + document.addEventListener("fullscreenchange", update); + return () => document.removeEventListener("fullscreenchange", update); + }, []); return ( console.log("lol")} + onPress={() => toggleFullscreen()} {...tooltip(t("player.fullscreen"), true)} {...props} /> @@ -91,9 +112,9 @@ export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => { icon={ muted || volume === 0 ? VolumeOff - : volume < 25 + : volume < 0.25 ? VolumeMute - : volume < 65 + : volume < 0.65 ? VolumeDown : VolumeUp } @@ -103,9 +124,9 @@ export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => { {...tooltip(t("player.mute"), true)} /> { - player.volume = vol; + player.volume = vol / 100; }} size={4} {...css({ width: px(100) })} diff --git a/front/src/ui/player/controls/progress.tsx b/front/src/ui/player/controls/progress.tsx index c63ad53c..e28d2ca6 100644 --- a/front/src/ui/player/controls/progress.tsx +++ b/front/src/ui/player/controls/progress.tsx @@ -80,11 +80,11 @@ export const ProgressText = ({ }: { player: VideoPlayer } & TextProps) => { const { css } = useYoshiki(); - const [progress, setProgress] = useState(player.currentTime || 0); + const [progress, setProgress] = useState(player.currentTime); useEvent(player, "onProgress", (progress) => { setProgress(progress.currentTime); }); - const [duration, setDuration] = useState(player.duration || 100); + const [duration, setDuration] = useState(player.duration); useEvent(player, "onLoad", (info) => { if (info.duration) setDuration(info.duration); }); diff --git a/front/src/ui/player/controls/touch.tsx b/front/src/ui/player/controls/touch.tsx index 579a1def..4bd4c0fa 100644 --- a/front/src/ui/player/controls/touch.tsx +++ b/front/src/ui/player/controls/touch.tsx @@ -8,6 +8,7 @@ import { import { useEvent, type VideoPlayer } from "react-native-video"; import { useYoshiki } from "yoshiki/native"; import { useIsTouch } from "~/primitives"; +import { toggleFullscreen } from "./misc"; export const TouchControls = ({ player, @@ -62,7 +63,7 @@ export const TouchControls = ({ }} onDoublePress={(e) => { if (!isTouch) { - // player.toggleFullscreen(); + toggleFullscreen(); return; } diff --git a/front/src/ui/player/index.tsx b/front/src/ui/player/index.tsx index 00429b1d..f1054520 100644 --- a/front/src/ui/player/index.tsx +++ b/front/src/ui/player/index.tsx @@ -1,5 +1,5 @@ import { Stack, useRouter } from "expo-router"; -import { StyleSheet, View } from "react-native"; +import { Platform, StyleSheet, View } from "react-native"; import { useEvent, useVideoPlayer, VideoView } from "react-native-video"; import { entryDisplayNumber } from "~/components/entries"; import { FullVideo, VideoInfo } from "~/models"; @@ -9,6 +9,11 @@ import { useLocalSetting } from "~/providers/settings"; import { type QueryIdentifier, useFetch } from "~/query"; import { useQueryState } from "~/utils"; import { Controls, LoadingIndicator } from "./controls"; +import { useEffect } from "react"; +import { v4 as uuidv4 } from "uuid"; +import { toggleFullscreen } from "./controls/misc"; + +const clientId = uuidv4(); export const Player = () => { const [slug, setSlug] = useQueryState("slug", undefined!); @@ -19,28 +24,40 @@ export const Player = () => { // TODO: map current entry using entries' duration & the current playtime const currentEntry = 0; const entry = data?.entries[currentEntry] ?? data?.entries[0]; + const title = entry ? `${entry.name} (${entryDisplayNumber(entry)})` : null; const { apiUrl, authToken } = useToken(); const [playMode] = useLocalSetting<"direct" | "hls">("playMode", "direct"); const player = useVideoPlayer( { - uri: `${apiUrl}/api/videos/${slug}/${playMode === "direct" ? "direct" : "master.m3u8"}`, - headers: { - Authorization: `Bearer ${authToken}`, + uri: `${apiUrl}/api/videos/${slug}/${playMode === "direct" ? "direct" : "master.m3u8"}?clientId=${clientId}`, + // chrome based browsers support matroska but they tell they don't + mimeType: info?.mimeCodec?.replace("x-matroska", "mp4"), + headers: authToken + ? { + Authorization: `Bearer ${authToken}`, + } + : {}, + metadata: { + title: title ?? undefined, + description: entry?.description ?? undefined, + artist: data?.show?.name ?? undefined, + imageUri: data?.show?.thumbnail?.high ?? undefined, }, - // externalSubtitles: info?.subtitles - // .filter((x) => x.link) - // .map((x) => ({ - // uri: x.link!, - // // TODO: translate this `Unknown` - // label: x.title ?? "Unknown", - // language: x.language ?? "und", - // type: x.codec, - // })), + externalSubtitles: info?.subtitles + .filter((x) => x.link) + .map((x) => ({ + uri: x.link!, + // TODO: translate this `Unknown` + label: x.title ?? "Unknown", + language: x.language ?? "und", + type: x.codec, + })), }, (p) => { p.playWhenInactive = true; p.playInBackground = true; + p.showNotificationControls = true; const seek = start ?? data?.progress.time; // TODO: fix console.error bellow if (seek) p.seekTo(seek); @@ -60,6 +77,31 @@ export const Player = () => { } }); + // TODO: add the equivalent of this for android + useEffect(() => { + if (typeof window === "undefined") return; + const prev = data?.previous?.video; + window.navigator.mediaSession.setActionHandler( + "previoustrack", + prev + ? () => { + setStart(0); + setSlug(prev); + } + : null, + ); + const next = data?.next?.video; + window.navigator.mediaSession.setActionHandler( + "nexttrack", + next + ? () => { + setStart(0); + setSlug(next); + } + : null, + ); + }, [data?.next?.video, data?.previous?.video, setSlug, setStart]); + // const [playbackError, setPlaybackError] = useState( // undefined, // ); @@ -67,14 +109,13 @@ export const Player = () => { // const startTime = startTimeP ?? data?.watchStatus?.watchedTime; - // const setFullscreen = useSetAtom(fullscreenAtom); - // useEffect(() => { - // if (Platform.OS !== "web") return; - // if (/Mobi/i.test(window.navigator.userAgent)) setFullscreen(true); - // return () => { - // if (!document.location.href.includes("/watch")) setFullscreen(false); - // }; - // }, [setFullscreen]); + useEffect(() => { + if (Platform.OS !== "web") return; + if (/Mobi/i.test(window.navigator.userAgent)) toggleFullscreen(true); + return () => { + if (!document.location.href.includes("/watch")) toggleFullscreen(false); + }; + }, []); // if (error || infoError || playbackError) // return ( @@ -95,7 +136,7 @@ export const Player = () => { }} > diff --git a/front/src/ui/player/old/media-session.tsx b/front/src/ui/player/old/media-session.tsx deleted file mode 100644 index 64b74dfd..00000000 --- a/front/src/ui/player/old/media-session.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Kyoo - A portable and vast media library solution. - * Copyright (c) Kyoo. - * - * See AUTHORS.md and LICENSE file in the project root for full license information. - * - * Kyoo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Kyoo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Kyoo. If not, see . - */ - -import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { useEffect } from "react"; -import { useRouter } from "solito/router"; -import { reducerAtom } from "./old/keyboardd"; -import { durationAtom, playAtom, progressAtom } from "./old/statee"; - -export const MediaSessionManager = ({ - title, - subtitle, - artist, - imageUri, - previous, - next, -}: { - title?: string; - subtitle?: string; - artist?: string; - imageUri?: string | null; - previous?: string; - next?: string; -}) => { - const [isPlaying, setPlay] = useAtom(playAtom); - const progress = useAtomValue(progressAtom); - const duration = useAtomValue(durationAtom); - const reducer = useSetAtom(reducerAtom); - const router = useRouter(); - - useEffect(() => { - if (!("mediaSession" in navigator)) return; - navigator.mediaSession.metadata = new MediaMetadata({ - title: title, - album: subtitle, - artist: artist, - artwork: imageUri ? [{ src: imageUri }] : undefined, - }); - }, [title, subtitle, artist, imageUri]); - - useEffect(() => { - if (!("mediaSession" in navigator)) return; - const actions: [MediaSessionAction, MediaSessionActionHandler | null][] = [ - ["play", () => setPlay(true)], - ["pause", () => setPlay(false)], - ["previoustrack", previous ? () => router.push(previous) : null], - ["nexttrack", next ? () => router.push(next) : null], - [ - "seekbackward", - (evt: MediaSessionActionDetails) => - reducer({ type: "seek", value: evt.seekOffset ? -evt.seekOffset : -10 }), - ], - [ - "seekforward", - (evt: MediaSessionActionDetails) => - reducer({ type: "seek", value: evt.seekOffset ? evt.seekOffset : 10 }), - ], - [ - "seekto", - (evt: MediaSessionActionDetails) => reducer({ type: "seekTo", value: evt.seekTime! }), - ], - ]; - - for (const [action, handler] of actions) { - try { - navigator.mediaSession.setActionHandler(action, handler); - } catch {} - } - }, [setPlay, reducer, router, previous, next]); - - useEffect(() => { - if (!("mediaSession" in navigator)) return; - navigator.mediaSession.playbackState = isPlaying ? "playing" : "paused"; - }, [isPlaying]); - useEffect(() => { - if (!("mediaSession" in navigator) || !duration) return; - navigator.mediaSession.setPositionState({ - position: Math.min(progress, duration), - duration, - playbackRate: 1, - }); - }, [progress, duration]); - - return null; -}; diff --git a/front/src/utils.ts b/front/src/utils.ts index 7315e0ad..2d67999c 100644 --- a/front/src/utils.ts +++ b/front/src/utils.ts @@ -1,5 +1,5 @@ import { NavigationContext, useRoute } from "@react-navigation/native"; -import { useContext } from "react"; +import { useCallback, useContext } from "react"; import type { Movie, Show } from "~/models"; export function setServerData(_key: string, _val: any) {} @@ -12,9 +12,12 @@ export const useQueryState = (key: string, initial: S) => { const nav = useContext(NavigationContext); const state = ((route.params as any)?.[key] as S) ?? initial; - const update = (val: S | ((old: S) => S)) => { - nav!.setParams({ [key]: val }); - }; + const update = useCallback( + (val: S | ((old: S) => S)) => { + nav!.setParams({ [key]: val }); + }, + [nav, key], + ); return [state, update] as const; };