From 23832929e9595f8b4eb37b6e02248cd9bef200e7 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 21 Oct 2025 17:25:36 +0200 Subject: [PATCH] Add libass support --- front/.dockerignore | 1 + front/Dockerfile | 4 +- front/Dockerfile.dev | 1 + front/bun.lock | 14 + front/package.json | 3 + front/public/jassub/.gitignore | 2 + front/scripts/postinstall.ts | 12 + front/src/ui/player/controls/tracks-menu.tsx | 18 +- front/src/ui/player/index.tsx | 7 + front/src/ui/player/old/state.tsx | 265 ----------- front/src/ui/player/old/video.web.tsx | 452 ------------------- front/src/ui/player/subtitles.ts | 3 + front/src/ui/player/subtitles.web.ts | 62 +++ front/tsconfig.json | 22 +- 14 files changed, 130 insertions(+), 736 deletions(-) create mode 100644 front/public/jassub/.gitignore create mode 100644 front/scripts/postinstall.ts delete mode 100644 front/src/ui/player/old/state.tsx delete mode 100644 front/src/ui/player/old/video.web.tsx create mode 100644 front/src/ui/player/subtitles.ts create mode 100644 front/src/ui/player/subtitles.web.ts diff --git a/front/.dockerignore b/front/.dockerignore index 295337bb..067c2d52 100644 --- a/front/.dockerignore +++ b/front/.dockerignore @@ -6,3 +6,4 @@ !/app.config.ts !/src !/public +!/scripts diff --git a/front/Dockerfile b/front/Dockerfile index c3ba6fea..180a0f84 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,8 +1,8 @@ FROM oven/bun AS builder WORKDIR /app -COPY package.json bun.lock . -RUN bun install --production +COPY package.json bun.lock scripts . +RUN bun install --production --frozen-lockfile COPY . . diff --git a/front/Dockerfile.dev b/front/Dockerfile.dev index 266a8d97..2bfbec5a 100644 --- a/front/Dockerfile.dev +++ b/front/Dockerfile.dev @@ -2,6 +2,7 @@ FROM oven/bun WORKDIR /app COPY package.json bun.lock . +COPY scripts scripts RUN bun install --frozen-lockfile COPY . . diff --git a/front/bun.lock b/front/bun.lock index 7d439862..124fe50b 100644 --- a/front/bun.lock +++ b/front/bun.lock @@ -27,6 +27,7 @@ "expo-status-bar": "~3.0.8", "expo-updates": "~29.0.11", "i18next-http-backend": "^3.0.2", + "jassub": "^1.8.6", "langmap": "^0.0.16", "react": "19.1.0", "react-dom": "19.1.0", @@ -51,6 +52,7 @@ "devDependencies": { "@biomejs/biome": "2.2.6", "@tanstack/react-query-devtools": "^5.90.2", + "@types/bun": "^1.3.0", "@types/react": "~19.1.10", "@types/react-dom": "~19.1.7", "react-native-svg-transformer": "^1.5.1", @@ -519,6 +521,8 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], + "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], "@types/inline-style-prefixer": ["@types/inline-style-prefixer@5.0.3", "", {}, "sha512-GOiSoBwH2U8LmbCnOLU6ZRPtm+qycO9sNXCvP+ahG0abpHrYTd1rm6ZPX4qYTFf1mTB6tqTQ9fYaJPcQWGFMSQ=="], @@ -645,6 +649,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "caller-callsite": ["caller-callsite@2.0.0", "", { "dependencies": { "callsites": "^2.0.0" } }, "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ=="], @@ -965,6 +971,8 @@ "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "jassub": ["jassub@1.8.6", "", { "dependencies": { "rvfc-polyfill": "^1.0.7" } }, "sha512-56ZTtjM7LfdKsi7boUN/seNOQSOclLuDWEXxnHO55xNakj95SlBrv36hLyNDw0NmoOtLVeqzbpBU1VxT+ubFpg=="], + "jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="], "jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], @@ -1315,6 +1323,8 @@ "rtl-detect": ["rtl-detect@1.1.2", "", {}, "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ=="], + "rvfc-polyfill": ["rvfc-polyfill@1.0.7", "", {}, "sha512-seBl7J1J3/k0LuzW2T9fG6JIOpni5AbU+/87LA+zTYKgTVhsfShmS8K/yOo1eeEjGJHnAdkVAUUM+PEjN9Mpkw=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], @@ -1621,6 +1631,8 @@ "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + "bun-types/@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], + "caller-callsite/callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="], "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -1831,6 +1843,8 @@ "@types/graceful-fs/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + "bun-types/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + "chrome-launcher/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], "chromium-edge-launcher/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], diff --git a/front/package.json b/front/package.json index 0cbe257e..b75b05f7 100644 --- a/front/package.json +++ b/front/package.json @@ -4,6 +4,7 @@ "main": "expo-router/entry", "version": "1.0.0", "scripts": { + "postinstall": "bun ./scripts/postinstall.ts", "dev": "expo start", "apk": "eas build --profile preview --platform android --non-interactive --json", "apk:dev": "eas build --profile development --platform android --non-interactive", @@ -36,6 +37,7 @@ "expo-status-bar": "~3.0.8", "expo-updates": "~29.0.11", "i18next-http-backend": "^3.0.2", + "jassub": "^1.8.6", "langmap": "^0.0.16", "react": "19.1.0", "react-dom": "19.1.0", @@ -60,6 +62,7 @@ "devDependencies": { "@biomejs/biome": "2.2.6", "@tanstack/react-query-devtools": "^5.90.2", + "@types/bun": "^1.3.0", "@types/react": "~19.1.10", "@types/react-dom": "~19.1.7", "react-native-svg-transformer": "^1.5.1", diff --git a/front/public/jassub/.gitignore b/front/public/jassub/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/front/public/jassub/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/front/scripts/postinstall.ts b/front/scripts/postinstall.ts new file mode 100644 index 00000000..2ec18601 --- /dev/null +++ b/front/scripts/postinstall.ts @@ -0,0 +1,12 @@ +import { readdir , mkdir } from 'node:fs/promises'; + +const srcDir = new URL("../node_modules/jassub/dist/", import.meta.url); +const destDir = new URL("../public/jassub/", import.meta.url); + +await mkdir(destDir, { recursive: true }); + +const files = await readdir(srcDir); +for (const file of files) { + const src = await Bun.file(new URL(file, srcDir)).arrayBuffer(); + await Bun.write(new URL(file, destDir), src); +} diff --git a/front/src/ui/player/controls/tracks-menu.tsx b/front/src/ui/player/controls/tracks-menu.tsx index 4a5c865a..3b24b4d4 100644 --- a/front/src/ui/player/controls/tracks-menu.tsx +++ b/front/src/ui/player/controls/tracks-menu.tsx @@ -12,6 +12,7 @@ import { useFetch } from "~/query"; import { useDisplayName, useSubtitleName } from "~/track-utils"; import { useQueryState } from "~/utils"; import { Player } from ".."; +import { Platform } from "react-native"; type MenuProps = ComponentProps>>; @@ -36,17 +37,6 @@ export const SubtitleMenu = ({ .getAvailableTextTracks() .findIndex((x) => x.selected); - const select = (track: Subtitle | null, idx: number) => { - if (!track) { - player.selectTextTrack(null); - return; - } - - // TODO: filter by codec here - const sub = player.getAvailableTextTracks()[idx]; - player.selectTextTrack(sub); - }; - return ( select(null, -1)} + onSelect={() => player.selectTextTrack(null)} /> {data?.subtitles.map((x, i) => ( select(x, i)} + onSelect={() => + player.selectTextTrack(player.getAvailableTextTracks()[i]) + } /> ))} diff --git a/front/src/ui/player/index.tsx b/front/src/ui/player/index.tsx index 7d5f43d8..2fdbc9c4 100644 --- a/front/src/ui/player/index.tsx +++ b/front/src/ui/player/index.tsx @@ -17,6 +17,7 @@ import { Back } from "./controls/back"; import { toggleFullscreen } from "./controls/misc"; import { PlayModeContext } from "./controls/tracks-menu"; import { useKeyboard } from "./keyboard"; +import { enhanceSubtitles } from "./subtitles"; const clientId = uuidv4(); @@ -81,6 +82,7 @@ export const Player = () => { p.playWhenInactive = true; p.playInBackground = true; p.showNotificationControls = true; + enhanceSubtitles(p); const seek = start ?? data?.progress.time; // TODO: fix console.error bellow if (seek) p.seekTo(seek); @@ -89,6 +91,11 @@ export const Player = () => { }, ); + // we'll also want to replace source here once https://github.com/TheWidlarzGroup/react-native-video/issues/4722 is ready + useEffect(() => { + player.__ass.fonts = info?.fonts ?? []; + }, [player, info?.fonts]); + const router = useRouter(); const playPrev = useCallback(() => { if (!data?.previous) return false; diff --git a/front/src/ui/player/old/state.tsx b/front/src/ui/player/old/state.tsx deleted file mode 100644 index 24d97cdb..00000000 --- a/front/src/ui/player/old/state.tsx +++ /dev/null @@ -1,265 +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 { type Audio, type Episode, type Subtitle, getLocalSetting, useAccount } from "@kyoo/models"; -import { useSnackbar } from "@kyoo/primitives"; -import { atom, getDefaultStore, useAtom, useAtomValue, useSetAtom } from "jotai"; -import { useAtomCallback } from "jotai/utils"; -import { - type ElementRef, - memo, - useCallback, - useEffect, - useLayoutEffect, - useRef, - useState, -} from "react"; -import { useTranslation } from "react-i18next"; -import { Platform } from "react-native"; -import NativeVideo, { canPlay, type VideoMetadata, type VideoProps } from "../videoideo"; - -export const playAtom = atom(true); -export const loadAtom = atom(false); - -export enum PlayMode { - Direct, - Hls, -} -export const playModeAtom = atom( - getLocalSetting("playmode", "direct") !== "auto" ? PlayMode.Direct : PlayMode.Hls, -); - -export const bufferedAtom = atom(0); -export const durationAtom = atom(undefined); - -export const progressAtom = atom( - (get) => get(privateProgressAtom), - (get, set, update: number | ((value: number) => number)) => { - const run = (value: number) => { - set(privateProgressAtom, value); - set(publicProgressAtom, value); - }; - if (typeof update === "function") run(update(get(privateProgressAtom))); - else run(update); - }, -); -const privateProgressAtom = atom(0); -const publicProgressAtom = atom(0); - -export const volumeAtom = atom(100); -export const mutedAtom = atom(false); - -export const fullscreenAtom = atom( - (get) => get(privateFullscreen), - (get, set, update: boolean | ((value: boolean) => boolean)) => { - const run = async (value: boolean) => { - try { - if (value) { - await document.body.requestFullscreen({ - navigationUI: "hide", - }); - set(privateFullscreen, true); - // @ts-expect-error Firefox does not support this so ts complains - await screen.orientation.lock("landscape"); - } else { - if (document.fullscreenElement) await document.exitFullscreen(); - set(privateFullscreen, false); - screen.orientation.unlock(); - } - } catch (e) { - console.error(e); - } - }; - if (typeof update === "function") run(update(get(privateFullscreen))); - else run(update); - }, -); -const privateFullscreen = atom(false); - -export const subtitleAtom = atom(null); -export const audioAtom = atom