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 (