diff --git a/frontend/src/store/player-element.ts b/frontend/src/store/player-element.ts index 4937d8e329f..5093a386b1f 100644 --- a/frontend/src/store/player-element.ts +++ b/frontend/src/store/player-element.ts @@ -7,6 +7,8 @@ import JASSUB from 'jassub'; import jassubWorker from 'jassub/dist/jassub-worker.js?url'; import jassubWasmUrl from 'jassub/dist/jassub-worker.wasm?url'; +import { PgsRenderer } from 'libpgs'; +import pgssubWorker from 'libpgs/dist/libpgs.worker.js?url'; import { computed, nextTick, shallowRef, watch } from 'vue'; import { SubtitleDeliveryMethod } from '@jellyfin/sdk/lib/generated-client/models/subtitle-delivery-method'; import { useFullscreen } from '@vueuse/core'; @@ -43,6 +45,7 @@ class PlayerElementStore extends CommonStore { */ private readonly _fullscreenVideoRoute = '/playback/video'; private _jassub: JASSUB | undefined; + private _pgssub: PgsRenderer | undefined; protected _storeKey = 'playerElement'; public readonly isStretched = computed({ @@ -101,6 +104,22 @@ class PlayerElementStore extends CommonStore { } }; + private readonly _setPgsTrack = (trackSrc: string): void => { + if ( + !this._pgssub + && mediaElementRef.value + && mediaElementRef.value instanceof HTMLVideoElement + ) { + this._pgssub = new PgsRenderer({ + video: mediaElementRef.value, + subUrl: trackSrc, + workerUrl: pgssubWorker + }); + } else if (this._pgssub) { + this._pgssub.loadFromUrl(trackSrc); + } + }; + private readonly _freeSsaTrack = (): void => { if (this._jassub) { try { @@ -111,6 +130,16 @@ class PlayerElementStore extends CommonStore { } }; + private readonly _freePgsTrack = (): void => { + if (this._pgssub) { + try { + this._pgssub.dispose(); + } catch {} + + this._pgssub = undefined; + } + }; + private readonly _isSupportedFont = (mimeType: string | undefined | null): boolean => { return ( !isNil(mimeType) @@ -121,19 +150,6 @@ class PlayerElementStore extends CommonStore { ); }; - private get usingExternalVttSubtitles(): boolean { - return !isNil(playbackManager.currentSubtitleTrack) - && playbackManager.currentSubtitleTrack.DeliveryMethod === SubtitleDeliveryMethod.External - && playbackManager.currentSubtitleTrack.Codec !== 'ass' - && playbackManager.currentSubtitleTrack.Codec !== 'ssa'; - } - - private get usingExternalSsaSubtitles(): boolean { - return !isNil(playbackManager.currentSubtitleTrack) - && playbackManager.currentSubtitleTrack.DeliveryMethod === SubtitleDeliveryMethod.External - && (playbackManager.currentSubtitleTrack.Codec === 'ssa' || playbackManager.currentSubtitleTrack.Codec === 'ass'); - } - /** * Logic for applying custom subtitle track. * @@ -151,6 +167,58 @@ class PlayerElementStore extends CommonStore { && useFullscreen().isSupported.value; } + private get usingExternalVttSubtitles(): boolean { + return !isNil(playbackManager.currentSubtitleTrack) + && playbackManager.currentSubtitleTrack.DeliveryMethod === SubtitleDeliveryMethod.External + && ( + playbackManager.currentSubtitleTrack.Codec === 'vtt' + || playbackManager.currentSubtitleTrack.Codec === 'srt' + || playbackManager.currentSubtitleTrack.Codec === 'subrip' + ); + } + + private get usingExternalSsaSubtitles(): boolean { + return !isNil(playbackManager.currentSubtitleTrack) + && playbackManager.currentSubtitleTrack.DeliveryMethod === SubtitleDeliveryMethod.External + && ( + playbackManager.currentSubtitleTrack.Codec === 'ssa' + || playbackManager.currentSubtitleTrack.Codec === 'ass' + ); + } + + private get usingExternalPgsSubtitles(): boolean { + return !isNil(playbackManager.currentSubtitleTrack) + && playbackManager.currentSubtitleTrack.DeliveryMethod === SubtitleDeliveryMethod.External + && (playbackManager.currentSubtitleTrack.Codec === 'pgs'); + } + + /** + * Applies PGS subtitles to the media element. + */ + private readonly _applyPgsSubtitles = (): void => { + if ( + !this._pgssub + && mediaElementRef.value + && mediaElementRef.value instanceof HTMLVideoElement + ) { + /** + * Finding (if it exists) the pgs track associated to the newly picked subtitle + */ + const pgs = playbackManager.currentItemPgsParsedSubtitleTracks.find( + sub => sub.srcIndex === playbackManager.currentSubtitleStreamIndex + ); + + /** + * If PGS found, applying it + */ + if (pgs?.src && pgs.srcIndex !== -1) { + this.currentExternalSubtitleTrack = pgs; + + this._setPgsTrack(pgs.src); + } + } + }; + /** * Applies VTT (WebVTT) subtitles to the media element. * @@ -181,6 +249,7 @@ class PlayerElementStore extends CommonStore { */ if (this.useCustomSubtitleTrack) { const data = await parseVttFile(vtt.src); + this.currentExternalSubtitleTrack.parsed = data; } else { mediaElementRef.value.textTracks[vtt.srcIndex].mode = 'showing'; @@ -198,6 +267,7 @@ class PlayerElementStore extends CommonStore { if (!mediaElementRef.value) { return; } + const serverAddress = remote.sdk.api?.basePath; /** @@ -270,7 +340,9 @@ class PlayerElementStore extends CommonStore { textTrack.mode = 'disabled'; } } + this._freeSsaTrack(); + this._freePgsTrack(); this.currentExternalSubtitleTrack = undefined; await nextTick(); @@ -282,6 +354,8 @@ class PlayerElementStore extends CommonStore { void this._applyVttSubtitles(); } else if (this.usingExternalSsaSubtitles) { void this._applySsaSubtitles(); + } else if (this.usingExternalPgsSubtitles) { + this._applyPgsSubtitles(); } }; @@ -313,6 +387,7 @@ class PlayerElementStore extends CommonStore { watch(videoContainerRef, () => { if (!videoContainerRef.value) { this._freeSsaTrack(); + this._freePgsTrack(); } }, { flush: 'sync' });