diff --git a/front/apps/web/next.config.js b/front/apps/web/next.config.js index c4842716..ce1c69fe 100755 --- a/front/apps/web/next.config.js +++ b/front/apps/web/next.config.js @@ -22,7 +22,7 @@ const path = require("path"); const CopyPlugin = require("copy-webpack-plugin"); const DefinePlugin = require("webpack").DefinePlugin; -const suboctopus = path.dirname(require.resolve("libass-wasm")); +const suboctopus = path.resolve(path.dirname(require.resolve("jassub")), "../dist"); /** * @type {import("next").NextConfig} @@ -124,7 +124,9 @@ if (process.env.NODE_ENV !== "production") { nextConfig.rewrites = async () => [ { source: "/api/:path*", - destination: `${process.env.KYOO_URL}/:path*` ?? "http://localhost:5000/:path*", + destination: process.env.KYOO_URL + ? `${process.env.KYOO_URL}/:path*` + : "http://localhost:5000/:path*", }, ]; } diff --git a/front/apps/web/package.json b/front/apps/web/package.json index 6f354989..5a92d653 100644 --- a/front/apps/web/package.json +++ b/front/apps/web/package.json @@ -21,8 +21,8 @@ "expo-modules-core": "^1.5.9", "hls.js": "^1.4.10", "i18next": "^23.4.2", + "jassub": "^1.7.8", "jotai": "^2.3.1", - "libass-wasm": "^4.1.0", "moti": "^0.26.0", "next": "13.4.19", "next-translate": "^2.5.3", diff --git a/front/packages/ui/src/player/subtitle-octopus.d.ts b/front/packages/ui/src/player/subtitle-octopus.d.ts deleted file mode 100644 index c92135c9..00000000 --- a/front/packages/ui/src/player/subtitle-octopus.d.ts +++ /dev/null @@ -1,155 +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 . - */ - -declare module "libass-wasm" { - interface OptionsBase { - /** - * The video element to attach listeners to - */ - video?: HTMLVideoElement; - - /** - * The canvas to render the subtitles to. If none is given it will create a new canvas and - * insert it as a sibling of the video element (only if the video element exists) - */ - canvas?: HTMLCanvasElement; - - /** - * The URL of the worker - * - * @default `subtitles-octopus-worker.js` - */ - workerUrl?: string; - - /** - * The URL of the legacy worker - * - * @default `subtitles-octopus-worker-legacy.js` - */ - legacyWorkerUrl?: string; - - /** - * An array of links to the fonts used in the subtitle - */ - fonts?: string[]; - - /** - * Object with all available fonts - Key is font name in lower case, value is link - * - * @example `{"arial": "/font1.ttf"}` - */ - availableFonts?: Record; - - /** - * The amount of time the subtitles should be offset from the video - * - * @default 0 - */ - timeOffset?: number; - - /** - * Whether performance info is printed in the console - * - * @default false - */ - debug?: boolean; - - /** - * The default font. - */ - fallbackFont?: string; - - /** - * A boolean, whether to load files in a lazy way via FS.createLazyFile(). Requires - * Access-Control-Expose-Headers for Accept-Ranges, Content-Length, and Content-Encoding. If - * encoding is compressed or length is not set, file will be fully fetched instead of just a - * HEAD request. - */ - lazyFileLoading?: boolean; - - /** - * Function that's called when SubtitlesOctopus is ready - */ - onReady?: () => void; - - /** - * Function called in case of critical error meaning the subtitles wouldn't be shown and you - * should use an alternative method (for instance it occurs if browser doesn't support web - * workers) - */ - onError?: () => void; - - /** - * Change the render mode - * - * @default wasm - */ - renderMode?: "js-blend" | "wasm-blend" | "lossy"; - } - - interface OptionsWithSubUrl extends OptionsBase { - subUrl: string; - } - - interface OptionsWithSubContent extends OptionsBase { - subContent: string; - } - - export type Options = OptionsWithSubUrl | OptionsWithSubContent; - - declare class SubtitlesOctopus { - constructor(options: Options); - - /** - * Render subtitles at specified time - * - * @param time - */ - setCurrentTime(time: number): void; - - /** - * Works the same as the {@link subUrl} option. It will set the subtitle to display by its URL. - * - * @param url - */ - setTrackByUrl(url: string): void; - - /** - * Works the same as the {@link subContent} option. It will set the subtitle to display by its - * content. - * - * @param content - */ - setTrack(content: string): void; - - /** - * This simply removes the subtitles. You can use {@link setTrackByUrl} or {@link setTrack} - * methods to set a new subtitle file to be displayed. - */ - freeTrack(): void; - - /** - * Destroy instance - */ - dispose(): void; - } - - export default SubtitlesOctopus; -} diff --git a/front/packages/ui/src/player/video.web.tsx b/front/packages/ui/src/player/video.web.tsx index 4251a4df..e6349543 100644 --- a/front/packages/ui/src/player/video.web.tsx +++ b/front/packages/ui/src/player/video.web.tsx @@ -32,7 +32,7 @@ import { import { VideoProps } from "react-native-video"; import { useAtomValue, useSetAtom, useAtom } from "jotai"; import { useYoshiki } from "yoshiki"; -import SubtitleOctopus from "libass-wasm"; +import Jassub from "jassub"; import { playAtom, PlayMode, playModeAtom, subtitleAtom } from "./state"; import Hls, { Level, LoadPolicy } from "hls.js"; import { useTranslation } from "react-i18next"; @@ -223,27 +223,25 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function export default Video; -let htmlTrack: HTMLTrackElement | null; -let subOcto: SubtitleOctopus | null; const useSubtitle = ( player: RefObject, value: Subtitle | null, fonts?: string[], ) => { + const htmlTrack = useRef(); + const subOcto = useRef(); + useEffect(() => { if (!player.current) return; const removeHtmlSubtitle = () => { - if (htmlTrack) htmlTrack.remove(); - htmlTrack = null; + if (htmlTrack.current) htmlTrack.current.remove(); + htmlTrack.current = null; }; const removeOctoSub = () => { - if (subOcto) { - subOcto.freeTrack(); - subOcto.dispose(); - } - subOcto = null; + if (subOcto.current) subOcto.current.destroy(); + subOcto.current = null; }; if (!value || !value.link) { @@ -253,7 +251,7 @@ const useSubtitle = ( removeOctoSub(); if (player.current.textTracks.length > 0) player.current.textTracks[0].mode = "hidden"; const addSubtitle = async () => { - const track: HTMLTrackElement = htmlTrack ?? document.createElement("track"); + const track: HTMLTrackElement = htmlTrack.current ?? document.createElement("track"); track.kind = "subtitles"; track.label = value.displayName; if (value.language) track.srclang = value.language; @@ -263,27 +261,43 @@ const useSubtitle = ( track.onload = () => { if (player.current) player.current.textTracks[0].mode = "showing"; }; - if (!htmlTrack) { - htmlTrack = track; + if (!htmlTrack.current) { + htmlTrack.current = track; if (player.current) player.current.appendChild(track); } }; addSubtitle(); } else if (value.codec === "ass") { removeHtmlSubtitle(); - removeOctoSub(); - subOcto = new SubtitleOctopus({ - video: player.current, - subUrl: value.link, - workerUrl: "/_next/static/chunks/subtitles-octopus-worker.js", - legacyWorkerUrl: "/_next/static/chunks/subtitles-octopus-worker-legacy.js", - fallbackFont: "/default.woff2", - fonts: fonts, - // lazyFileLoading: true, - renderMode: "wasm-blend", - }); + // Also recreate jassub when the player changes (this is not the most effective but + // since it creates a div/canvas, it needs to be recreated when the UI rerender) + // @ts-expect-error We are accessing the private _video field here. + if (!subOcto.current || subOcto.current._video !== player.current) { + removeOctoSub(); + subOcto.current = new Jassub({ + video: player.current, + workerUrl: "/_next/static/chunks/jassub-worker.js", + wasmUrl: "/_next/static/chunks/jassub-worker.wasm", + legacyWasmUrl: "/_next/static/chunks/jassub-worker.wasm.js", + // Disable offscreen renderer for firefox (see https://github.com/ThaUnknown/jassub/issues/31) + offscreenRender: !/firefox/i.test(navigator.userAgent), + subUrl: value.link, + fonts: fonts, + }); + } else { + subOcto.current.freeTrack(); + subOcto.current.setTrackByUrl(value.link); + } } }, [player, value, fonts]); + useEffect(() => { + return () => { + if (subOcto.current) subOcto.current.destroy(); + subOcto.current = null; + if (htmlTrack.current) htmlTrack.current.remove(); + htmlTrack.current = null; + }; + }, []); }; const toWebVtt = async (srtUrl: string) => { @@ -304,7 +318,6 @@ export const AudiosMenu = ({ ...props }: ComponentProps & { audios?: Audio[] }) => { if (!hls || hls.audioTracks.length < 2) return null; - console.log(audios); return ( {hls.audioTracks.map((x, i) => ( diff --git a/front/yarn.lock b/front/yarn.lock index 736322cf..34d3ca4d 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -8910,6 +8910,15 @@ __metadata: languageName: node linkType: hard +"jassub@npm:^1.7.8": + version: 1.7.9 + resolution: "jassub@npm:1.7.9" + dependencies: + rvfc-polyfill: ^1.0.7 + checksum: 5f2abcb65ce2431a77fb4222446707ecd99c8d5e2182c77b385399b0fb1ce4ed9b6c429a343dcb086d3d3b0fa7b091681721f058d5ffc44751bb774370a6abc6 + languageName: node + linkType: hard + "jest-environment-node@npm:^29.2.1": version: 29.6.2 resolution: "jest-environment-node@npm:29.6.2" @@ -9337,13 +9346,6 @@ __metadata: languageName: node linkType: hard -"libass-wasm@npm:^4.1.0": - version: 4.1.0 - resolution: "libass-wasm@npm:4.1.0" - checksum: a6ecc842594b36455b309aa3bb6ce3b59ada407ffbdc5e62bf657144dfce028c0a7d20dac34a9b3ed0b21d051b0daf14ea656f692e2aad3b1d66347a4ed95ec4 - languageName: node - linkType: hard - "lightningcss-darwin-arm64@npm:1.19.0": version: 1.19.0 resolution: "lightningcss-darwin-arm64@npm:1.19.0" @@ -12537,6 +12539,13 @@ __metadata: languageName: node linkType: hard +"rvfc-polyfill@npm:^1.0.7": + version: 1.0.7 + resolution: "rvfc-polyfill@npm:1.0.7" + checksum: 1b0babe38866e252ce1594e82af3e6cc2e140f7d8c433fd7835c1ae28febc1aa5ad1354e7a84fa3b2611dc591bc9ca78dafc72ad41878d8cd2f2bfcb764c8abe + languageName: node + linkType: hard + "sade@npm:^1.7.3": version: 1.8.1 resolution: "sade@npm:1.8.1" @@ -14256,8 +14265,8 @@ __metadata: expo-modules-core: ^1.5.9 hls.js: ^1.4.10 i18next: ^23.4.2 + jassub: ^1.7.8 jotai: ^2.3.1 - libass-wasm: ^4.1.0 moti: ^0.26.0 next: 13.4.19 next-translate: ^2.5.3