diff --git a/.escheckrc b/.escheckrc index 90d6edddb9..195d28b9fc 100644 --- a/.escheckrc +++ b/.escheckrc @@ -6,7 +6,6 @@ "./dist/libraries/pdf.worker.js", "./dist/libraries/worker-bundle.js", "./dist/libraries/wasm-gen/libarchive.js", - "./dist/node_modules.@jellyfin.libass-wasm.*.chunk.js", "./dist/serviceworker.js" ] } diff --git a/babel.config.js b/babel.config.js index fcd9dcb4ac..e31ea23126 100644 --- a/babel.config.js +++ b/babel.config.js @@ -12,14 +12,7 @@ module.exports = { corejs: 3 } ], - '@babel/preset-react', - [ - '@babel/preset-typescript', - { - isTSX: true, - allExtensions: true - } - ] + '@babel/preset-react' ], plugins: [ '@babel/plugin-proposal-class-properties', diff --git a/package-lock.json b/package-lock.json index d9930b5b51..17a1a027d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@fontsource/noto-sans-kr": "4.5.12", "@fontsource/noto-sans-sc": "4.5.12", "@fontsource/noto-sans-tc": "4.5.12", - "@jellyfin/libass-wasm": "4.1.1", "@jellyfin/sdk": "unstable", "@loadable/component": "5.15.3", "blurhash": "2.0.5", @@ -26,12 +25,14 @@ "dompurify": "3.0.1", "epubjs": "0.3.93", "escape-html": "1.0.3", + "event-target-polyfill": "github:ThaUnknown/event-target-polyfill", "fast-text-encoding": "1.0.6", "flv.js": "1.6.2", "headroom.js": "0.12.0", "history": "5.3.0", "hls.js": "1.2.4", "intersection-observer": "0.12.2", + "jassub": "1.5.12", "jellyfin-apiclient": "1.10.0", "jquery": "3.6.4", "jstree": "3.3.15", @@ -2638,11 +2639,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "node_modules/@jellyfin/libass-wasm": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@jellyfin/libass-wasm/-/libass-wasm-4.1.1.tgz", - "integrity": "sha512-xQVJw+lZUg4U1TmLS80reBECfPtpCgRF8hhUSvUUQM9g68OvINyUU3K2yqRH+8tomGpghiRaIcr/bUJ83e0veA==" - }, "node_modules/@jellyfin/sdk": { "version": "0.0.0-unstable.202304122102", "resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202304122102.tgz", @@ -7332,6 +7328,11 @@ "es5-ext": "~0.10.14" } }, + "node_modules/event-target-polyfill": { + "version": "0.0.3", + "resolved": "git+ssh://git@github.com/ThaUnknown/event-target-polyfill.git#5cb9a0ed6774af1b905b525964316911375726a7", + "license": "MIT" + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -9686,6 +9687,14 @@ "node": ">=0.10.0" } }, + "node_modules/jassub": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/jassub/-/jassub-1.5.12.tgz", + "integrity": "sha512-CJiuNCXMMGqfmVVlaDyxqaKfOy3RIHW4HBwVWvbq8pl/d1/y1fgTarfR31whUUupHZCe7Tfq8XB7WDgdu6IHaA==", + "dependencies": { + "rvfc-polyfill": "^1.0.4" + } + }, "node_modules/jellyfin-apiclient": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/jellyfin-apiclient/-/jellyfin-apiclient-1.10.0.tgz", @@ -13720,6 +13729,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rvfc-polyfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/rvfc-polyfill/-/rvfc-polyfill-1.0.4.tgz", + "integrity": "sha512-BemRbBDZiLB8pxoPT+2q6R30ykY1e75XBE/L1A0Ubd/3KdUoCQLqI/z4v4oNFNlN3/Rs93d3b6WoybnXhdebkw==" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -20994,11 +21008,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "@jellyfin/libass-wasm": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@jellyfin/libass-wasm/-/libass-wasm-4.1.1.tgz", - "integrity": "sha512-xQVJw+lZUg4U1TmLS80reBECfPtpCgRF8hhUSvUUQM9g68OvINyUU3K2yqRH+8tomGpghiRaIcr/bUJ83e0veA==" - }, "@jellyfin/sdk": { "version": "0.0.0-unstable.202304122102", "resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202304122102.tgz", @@ -24572,6 +24581,10 @@ "es5-ext": "~0.10.14" } }, + "event-target-polyfill": { + "version": "git+ssh://git@github.com/ThaUnknown/event-target-polyfill.git#5cb9a0ed6774af1b905b525964316911375726a7", + "from": "event-target-polyfill@github:ThaUnknown/event-target-polyfill" + }, "eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -26323,6 +26336,14 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "jassub": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/jassub/-/jassub-1.5.12.tgz", + "integrity": "sha512-CJiuNCXMMGqfmVVlaDyxqaKfOy3RIHW4HBwVWvbq8pl/d1/y1fgTarfR31whUUupHZCe7Tfq8XB7WDgdu6IHaA==", + "requires": { + "rvfc-polyfill": "^1.0.4" + } + }, "jellyfin-apiclient": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/jellyfin-apiclient/-/jellyfin-apiclient-1.10.0.tgz", @@ -29235,6 +29256,11 @@ "queue-microtask": "^1.2.2" } }, + "rvfc-polyfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/rvfc-polyfill/-/rvfc-polyfill-1.0.4.tgz", + "integrity": "sha512-BemRbBDZiLB8pxoPT+2q6R30ykY1e75XBE/L1A0Ubd/3KdUoCQLqI/z4v4oNFNlN3/Rs93d3b6WoybnXhdebkw==" + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", diff --git a/package.json b/package.json index 4b23b79ae7..26e6e9ae42 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "@fontsource/noto-sans-kr": "4.5.12", "@fontsource/noto-sans-sc": "4.5.12", "@fontsource/noto-sans-tc": "4.5.12", - "@jellyfin/libass-wasm": "4.1.1", "@jellyfin/sdk": "unstable", "@loadable/component": "5.15.3", "blurhash": "2.0.5", @@ -85,12 +84,14 @@ "dompurify": "3.0.1", "epubjs": "0.3.93", "escape-html": "1.0.3", + "event-target-polyfill": "github:ThaUnknown/event-target-polyfill", "fast-text-encoding": "1.0.6", "flv.js": "1.6.2", "headroom.js": "0.12.0", "history": "5.3.0", "hls.js": "1.2.4", "intersection-observer": "0.12.2", + "jassub": "1.5.12", "jellyfin-apiclient": "1.10.0", "jquery": "3.6.4", "jstree": "3.3.15", @@ -131,8 +132,8 @@ "scripts": { "start": "npm run serve", "serve": "webpack serve --config webpack.dev.js", - "build:development": "webpack --config webpack.dev.js", - "build:production": "cross-env NODE_ENV=\"production\" webpack --config webpack.prod.js", + "build:development": "cross-env NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.dev.js", + "build:production": "cross-env NODE_ENV=\"production\" NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.prod.js", "build:check": "tsc --noEmit", "escheck": "es-check", "lint": "eslint \"./\"", diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index cabf6cb3dc..ad39f19ea4 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -209,13 +209,9 @@ function tryRemoveElement(elem) { /** * @type {any | null | undefined} */ - #currentSubtitlesOctopus; - /** - * @type {null | undefined} - */ #currentAssRenderer; /** - * @type {number | undefined} + * @type {null | undefined} */ #customTrackIndex; /** @@ -585,9 +581,9 @@ function tryRemoveElement(elem) { const offsetValue = parseFloat(offset); // if .ass currently rendering - if (this.#currentSubtitlesOctopus) { + if (this.#currentAssRenderer) { this.updateCurrentTrackOffset(offsetValue); - this.#currentSubtitlesOctopus.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + offsetValue; + this.#currentAssRenderer.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + offsetValue; } else { const trackElements = this.getTextTracks(); // if .vtt currently rendering @@ -978,10 +974,8 @@ function tryRemoveElement(elem) { loading.hide(); seekOnPlaybackStart(this, e.target, this._currentPlayOptions.playerStartPositionTicks, () => { - if (this.#currentSubtitlesOctopus) { - this.#currentSubtitlesOctopus.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + this.#currentTrackOffset; - this.#currentSubtitlesOctopus.resize(); - this.#currentSubtitlesOctopus.resetRenderAheadCache(false); + if (this.#currentAssRenderer) { + this.#currentAssRenderer.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + this.#currentTrackOffset; } }); @@ -1169,15 +1163,9 @@ function tryRemoveElement(elem) { this.#currentClock = null; this._currentAspectRatio = null; - const octopus = this.#currentSubtitlesOctopus; - if (octopus) { - octopus.dispose(); - } - this.#currentSubtitlesOctopus = null; - - const renderer = this.#currentAssRenderer; - if (renderer) { - renderer.setEnabled(false); + const jassub = this.#currentAssRenderer; + if (jassub) { + jassub.destroy(); } this.#currentAssRenderer = null; } @@ -1266,59 +1254,68 @@ function tryRemoveElement(elem) { const fallbackFontList = apiClient.getUrl('/FallbackFont/Fonts', { api_key: apiClient.accessToken() }); - const htmlVideoPlayer = this; const options = { video: videoElement, subUrl: getTextTrackUrl(track, item), fonts: avaliableFonts, - workerUrl: `${appRouter.baseUrl()}/libraries/subtitles-octopus-worker.js`, - legacyWorkerUrl: `${appRouter.baseUrl()}/libraries/subtitles-octopus-worker-legacy.js`, - onError() { - // HACK: Clear JavascriptSubtitlesOctopus: it gets disposed when an error occurs - htmlVideoPlayer.#currentSubtitlesOctopus = null; - - // HACK: Give JavascriptSubtitlesOctopus time to dispose itself - setTimeout(() => { - onErrorInternal(htmlVideoPlayer, 'mediadecodeerror'); - }, 0); - }, + fallbackFont: 'liberation sans', + availableFonts: { 'liberation sans': `${appRouter.baseUrl()}/default.woff2` }, + // Disabled eslint compat, but is safe as corejs3 polyfills URL + // eslint-disable-next-line compat/compat + workerUrl: new URL('jassub/dist/jassub-worker.js', import.meta.url), + // eslint-disable-next-line compat/compat + legacyWorkerUrl: new URL('jassub/dist/jassub-worker-legacy.js', import.meta.url), timeOffset: (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000, - - // new octopus options; override all, even defaults - renderMode: 'wasm-blend', + // new jassub options; override all, even defaults + blendMode: 'js', + asyncRender: true, + // firefox implements offscreen canvas, but not according to spec which causes errors + offscreenRender: !browser.firefox, + // RVFC is polyfilled everywhere, but webOS 2 reports polyfill API's as functional even tho they aren't + onDemandRender: browser.web0sVersion !== 2, + useLocalFonts: true, dropAllAnimations: false, libassMemoryLimit: 40, libassGlyphLimit: 40, targetFps: 24, prescaleFactor: 0.8, prescaleHeightLimit: 1080, - maxRenderHeight: 2160, - resizeVariation: 0.2, - renderAhead: 90 + maxRenderHeight: 2160 }; - import('@jellyfin/libass-wasm').then(({ default: SubtitlesOctopus }) => { - Promise.all([ - apiClient.getNamedConfiguration('encoding'), - // Worker in Tizen 5 doesn't resolve relative path with async request - resolveUrl(options.workerUrl), - resolveUrl(options.legacyWorkerUrl) - ]).then(([config, workerUrl, legacyWorkerUrl]) => { - options.workerUrl = workerUrl; - options.legacyWorkerUrl = legacyWorkerUrl; + // TODO: replace with `event-target-polyfill` once https://github.com/benlesh/event-target-polyfill/pull/12 or 11 is merged + import('event-target-polyfill').then(() => { + import('jassub').then(({ default: JASSUB }) => { + Promise.all([ + apiClient.getNamedConfiguration('encoding'), + // Worker in Tizen 5 doesn't resolve relative path with async request + resolveUrl(options.workerUrl), + resolveUrl(options.legacyWorkerUrl) + ]).then(([config, workerUrl, legacyWorkerUrl]) => { + options.workerUrl = workerUrl; + options.legacyWorkerUrl = legacyWorkerUrl; - if (config.EnableFallbackFont) { - apiClient.getJSON(fallbackFontList).then((fontFiles = []) => { - fontFiles.forEach(font => { - const fontUrl = apiClient.getUrl(`/FallbackFont/Fonts/${font.Name}`, { - api_key: apiClient.accessToken() + const cleanup = () => { + this.#currentAssRenderer.destroy(); + this.#currentAssRenderer = null; + onErrorInternal(this, 'mediadecodeerror'); + }; + + if (config.EnableFallbackFont) { + apiClient.getJSON(fallbackFontList).then((fontFiles = []) => { + fontFiles.forEach(font => { + const fontUrl = apiClient.getUrl(`/FallbackFont/Fonts/${font.Name}`, { + api_key: apiClient.accessToken() + }); + avaliableFonts.push(fontUrl); }); - avaliableFonts.push(fontUrl); + this.#currentAssRenderer = new JASSUB(options); + this.#currentAssRenderer.addEventListener('error', cleanup, { once: true }); }); - this.#currentSubtitlesOctopus = new SubtitlesOctopus(options); - }); - } else { - this.#currentSubtitlesOctopus = new SubtitlesOctopus(options); - } + } else { + this.#currentAssRenderer = new JASSUB(options); + this.#currentAssRenderer.addEventListener('error', cleanup, { once: true }); + } + }); }); }); } diff --git a/src/plugins/htmlVideoPlayer/style.scss b/src/plugins/htmlVideoPlayer/style.scss index 0026146081..bfcc90d60e 100644 --- a/src/plugins/htmlVideoPlayer/style.scss +++ b/src/plugins/htmlVideoPlayer/style.scss @@ -17,7 +17,7 @@ z-index: 1000; } -.videoPlayerContainer .libassjs-canvas-parent { +.videoPlayerContainer .JASSUB { order: -1; } diff --git a/webpack.common.js b/webpack.common.js index eebd423d98..57d3cc6fdc 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -8,15 +8,14 @@ const { DefinePlugin } = require('webpack'); const Assets = [ 'native-promise-only/npo.js', 'libarchive.js/dist/worker-bundle.js', - '@jellyfin/libass-wasm/dist/js/subtitles-octopus-worker.js', - '@jellyfin/libass-wasm/dist/js/subtitles-octopus-worker.data', - '@jellyfin/libass-wasm/dist/js/subtitles-octopus-worker.wasm', - '@jellyfin/libass-wasm/dist/js/subtitles-octopus-worker-legacy.js', - '@jellyfin/libass-wasm/dist/js/subtitles-octopus-worker-legacy.data', - '@jellyfin/libass-wasm/dist/js/subtitles-octopus-worker-legacy.js.mem', 'pdfjs-dist/build/pdf.worker.js' ]; +const JassubWasm = [ + 'jassub/dist/jassub-worker.wasm', + 'jassub/dist/default.woff2' +]; + const LibarchiveWasm = [ 'libarchive.js/dist/wasm-gen/libarchive.js', 'libarchive.js/dist/wasm-gen/libarchive.wasm' @@ -83,6 +82,14 @@ const config = { to: path.resolve(__dirname, './dist/libraries/wasm-gen') }; }) + }), + new CopyPlugin({ + patterns: JassubWasm.map(asset => { + return { + from: path.resolve(__dirname, `./node_modules/${asset}`), + to: path.resolve(__dirname, './dist') + }; + }) }) ], output: { @@ -147,7 +154,8 @@ const config = { { test: /\.(js|jsx)$/, include: [ - path.resolve(__dirname, 'node_modules/@jellyfin/libass-wasm'), + path.resolve(__dirname, 'node_modules/event-target-polyfill'), + path.resolve(__dirname, 'node_modules/rvfc-polyfill'), path.resolve(__dirname, 'node_modules/@jellyfin/sdk'), path.resolve(__dirname, 'node_modules/@remix-run/router'), path.resolve(__dirname, 'node_modules/@uupaa/dynamic-import-polyfill'), @@ -174,6 +182,20 @@ const config = { } }] }, + { + test: /\.js$/, + include: [ + path.resolve(__dirname, 'node_modules/jassub') + ], + use: [{ + loader: 'babel-loader', + options: { + cacheCompression: false, + cacheDirectory: true, + presets: ['@babel/preset-env'] + } + }] + }, { test: /\.worker\.ts$/, exclude: /node_modules/,