diff --git a/package-lock.json b/package-lock.json index bbf8018388..c567d86eb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "jquery": "3.7.1", "jstree": "3.3.17", "libarchive.js": "2.0.2", - "libpgs": "0.8.1", + "libbitsub": "1.10.2", "lodash-es": "4.18.1", "markdown-it": "14.3.0", "material-design-icons-iconfont": "6.7.0", @@ -7318,6 +7318,12 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webgpu/types": { + "version": "0.1.71", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.71.tgz", + "integrity": "sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==", + "license": "BSD-3-Clause" + }, "node_modules/@xmldom/xmldom": { "version": "0.7.9", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.9.tgz", @@ -14987,11 +14993,17 @@ "comlink": "^4.4.1" } }, - "node_modules/libpgs": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/libpgs/-/libpgs-0.8.1.tgz", - "integrity": "sha512-Z8WCvHRYr37QjU8b2WqiJpCvnoojbYPtoE4FktwdI+m+b1QkNIKqrYQrlPKG1xvYyQWgV1j+16xjFOhun4nBUA==", - "license": "MIT" + "node_modules/libbitsub": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/libbitsub/-/libbitsub-1.10.2.tgz", + "integrity": "sha512-cXD1ueeeXqJIaNvYRrkNZOZ0A5FMMv9+8Yp/lHJZXPvkim5h0yvF2G27lYr0vGN9HwZCaJhkz6GArRerieTtuw==", + "license": "MIT", + "dependencies": { + "@webgpu/types": "^0.1.71" + }, + "engines": { + "node": ">=18" + } }, "node_modules/lie": { "version": "3.1.1", diff --git a/package.json b/package.json index 93ae12c722..0af828261b 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "jquery": "3.7.1", "jstree": "3.3.17", "libarchive.js": "2.0.2", - "libpgs": "0.8.1", + "libbitsub": "1.10.2", "lodash-es": "4.18.1", "markdown-it": "14.3.0", "material-design-icons-iconfont": "6.7.0", diff --git a/src/apps/legacy/controllers/playback/video/index.js b/src/apps/legacy/controllers/playback/video/index.js index 8737a03078..fdde2c5582 100644 --- a/src/apps/legacy/controllers/playback/video/index.js +++ b/src/apps/legacy/controllers/playback/video/index.js @@ -622,6 +622,8 @@ export default function (view) { Events.off(player, 'timeupdate', onTimeUpdate); Events.off(player, 'fullscreenchange', onFullscreenChanged); Events.off(player, 'mediastreamschange', onMediaStreamsChanged); + Events.off(player, 'beginFetch', onBeginFetch); + Events.off(player, 'endFetch', onEndFetch); currentPlayer = null; } } diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index c4dbbdbe98..c715f4da21 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -644,6 +644,30 @@ function showPlaybackInfoErrorMessage(instance, errorCode) { }); } +function isExternalGraphicalSubtitleStream(stream) { + if (!stream || stream.Type !== 'Subtitle' || stream.DeliveryMethod !== 'External') { + return false; + } + + if (typeof stream.IsTextSubtitleStream === 'boolean') { + return !stream.IsTextSubtitleStream; + } + + const codec = (stream.Codec || '').toLowerCase(); + return codec === 'pgssub' || codec === 'dvdsub' || codec === 'vobsub'; +} + +function hasSelectedExternalGraphicalSubtitle(mediaSource) { + const selectedSubtitleIndices = [mediaSource.DefaultSubtitleStreamIndex, mediaSource.DefaultSecondarySubtitleStreamIndex] + .filter(index => typeof index === 'number' && index >= 0); + + if (selectedSubtitleIndices.length === 0) { + return false; + } + + return (mediaSource.MediaStreams || []).some(stream => selectedSubtitleIndices.includes(stream.Index) && isExternalGraphicalSubtitleStream(stream)); +} + function normalizePlayOptions(playOptions) { playOptions.fullscreen = playOptions.fullscreen !== false; } @@ -875,10 +899,15 @@ export class PlaybackManager { self.trackHasSecondarySubtitleSupport = function (track, player = self._currentPlayer) { if (!player || !track) return false; const format = (track.Codec || '').toLowerCase(); - // Currently, only non-SSA/non-ASS external subtitles are supported. - // Showing secondary subtitles does not work with any SSA/ASS subtitle combinations because - // of the complexity of how they are rendered and the risk of the subtitles overlapping - return format !== 'ssa' && format !== 'ass' && getDeliveryMethod(track) === 'External'; + // Secondary subtitle pairing does not work with SSA/ASS combinations because + // of the complexity of how they are rendered and the risk of the subtitles overlapping. + // Graphical subtitle formats are supported generally, but not for secondary pairing here. + return format !== 'ssa' + && format !== 'ass' + && format !== 'pgssub' + && format !== 'dvdsub' + && format !== 'vobsub' + && getDeliveryMethod(track) === 'External'; }; self.secondarySubtitleTracks = function (player = self._currentPlayer) { @@ -2870,10 +2899,10 @@ export class PlaybackManager { contentType = 'application/x-mpegURL'; } else { contentType = getMimeType(type.toLowerCase(), mediaSource.TranscodingContainer); + } - if (mediaUrl.toLowerCase().indexOf('copytimestamps=true') === -1) { - transcodingOffsetTicks = startPosition || 0; - } + if (!(mediaSource.TranscodingSubProtocol === 'hls' && hasSelectedExternalGraphicalSubtitle(mediaSource))) { + transcodingOffsetTicks = startPosition || 0; } } } else { diff --git a/src/components/subtitlesettings/subtitlesettings.js b/src/components/subtitlesettings/subtitlesettings.js index f8d8aea9b1..0876449db0 100644 --- a/src/components/subtitlesettings/subtitlesettings.js +++ b/src/components/subtitlesettings/subtitlesettings.js @@ -28,6 +28,7 @@ import template from './subtitlesettings.template.html'; function getSubtitleAppearanceObject(context) { return { + aspectMode: context.querySelector('#selectBitmapSubtitleAspectMode').value, subtitleStyling: context.querySelector('#selectSubtitleStyling').value, textSize: context.querySelector('#selectTextSize').value, textWeight: context.querySelector('#selectTextWeight').value, @@ -39,6 +40,15 @@ function getSubtitleAppearanceObject(context) { }; } +function toggleBitmapSubtitleAspectModeField(view) { + const fieldBitmapSubtitleAspectMode = view.querySelector('.fldBitmapSubtitleAspectMode'); + const fieldRenderPgs = view.querySelector('.fldRenderPgs'); + const renderPgsEnabled = view.querySelector('#chkSubtitleRenderPgs').checked; + const renderPgsVisible = !fieldRenderPgs.classList.contains('hide'); + + fieldBitmapSubtitleAspectMode.classList.toggle('hide', !renderPgsVisible || !renderPgsEnabled); +} + function loadForm(context, user, userSettings, appearanceSettings, apiClient) { apiClient.getCultures().then(function (allCultures) { if (appHost.supports(AppFeature.SubtitleBurnIn) && user.Policy.EnableVideoPlaybackTranscoding) { @@ -64,11 +74,13 @@ function loadForm(context, user, userSettings, appearanceSettings, apiClient) { context.querySelector('#inputTextColor').value = appearanceSettings.textColor || '#ffffff'; context.querySelector('#selectFont').value = appearanceSettings.font || ''; context.querySelector('#sliderVerticalPosition').value = appearanceSettings.verticalPosition; + context.querySelector('#selectBitmapSubtitleAspectMode').value = appearanceSettings.aspectMode || 'stretch'; context.querySelector('#selectSubtitleBurnIn').value = appSettings.get('subtitleburnin') || ''; context.querySelector('#chkSubtitleRenderPgs').checked = appSettings.get('subtitlerenderpgs') === 'true'; context.querySelector('#selectSubtitleBurnIn').dispatchEvent(new CustomEvent('change', {})); + toggleBitmapSubtitleAspectModeField(context); context.querySelector('#chkAlwaysBurnInSubtitleWhenTranscoding').checked = appSettings.alwaysBurnInSubtitleWhenTranscoding(); onAppearanceFieldChange({ @@ -138,6 +150,12 @@ function onSubtitleBurnInChange(e) { // Pgs option is only available if burn-in mode is set to 'auto' (empty string) fieldRenderPgs.classList.toggle('hide', !!this.value); + toggleBitmapSubtitleAspectModeField(view); +} + +function onSubtitleRenderPgsChange(e) { + const view = dom.parentWithClass(e.target, 'subtitlesettings'); + toggleBitmapSubtitleAspectModeField(view); } function onAppearanceFieldChange(e) { @@ -197,10 +215,12 @@ function embed(options, self) { options.element.querySelector('#selectSubtitlePlaybackMode').addEventListener('change', onSubtitleModeChange); options.element.querySelector('#selectSubtitleStyling').addEventListener('change', onSubtitleStyleChange); options.element.querySelector('#selectSubtitleBurnIn').addEventListener('change', onSubtitleBurnInChange); + options.element.querySelector('#chkSubtitleRenderPgs').addEventListener('change', onSubtitleRenderPgsChange); options.element.querySelector('#selectTextSize').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#selectTextWeight').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#selectDropShadow').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#selectFont').addEventListener('change', onAppearanceFieldChange); + options.element.querySelector('#selectBitmapSubtitleAspectMode').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#selectTextColor').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#inputTextColor').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#inputTextBackground').addEventListener('change', onAppearanceFieldChange); diff --git a/src/components/subtitlesettings/subtitlesettings.template.html b/src/components/subtitlesettings/subtitlesettings.template.html index f2c774f00c..250b1801f1 100644 --- a/src/components/subtitlesettings/subtitlesettings.template.html +++ b/src/components/subtitlesettings/subtitlesettings.template.html @@ -40,6 +40,15 @@

${RenderPgsSubtitleHelp}
+ +
+ +
${BitmapSubtitleAspectModeHelp}
+
diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 7c7b8f2602..8ecaafad3d 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -45,6 +45,11 @@ import Events from '../../utils/events.ts'; import { includesAny } from '../../utils/container.ts'; import { isHls } from '../../utils/mediaSource.ts'; +const NATIVE_UNSUPPORTED_SUBTITLE_CODECS = ['ssa', 'ass', 'pgssub', 'dvdsub', 'vobsub']; +const ASS_SUBTITLE_CODECS = ['ssa', 'ass']; +const VOBSUB_SUBTITLE_CODECS = ['dvdsub', 'vobsub']; +const BITMAP_SUBTITLE_ASPECT_MODES = ['stretch', 'contain', 'cover']; + /** * Returns resolved URL. * @param {string} url - URL. @@ -106,7 +111,7 @@ function enableNativeTrackSupport(mediaSource, track) { if (track) { const format = (track.Codec || '').toLowerCase(); - if (format === 'ssa' || format === 'ass' || format === 'pgssub') { + if (NATIVE_UNSUPPORTED_SUBTITLE_CODECS.includes(format)) { return false; } } @@ -175,11 +180,54 @@ function getTextTrackUrl(track, item, format) { return url; } +function getSubtitleFileNameHint(track) { + const candidates = [track?.Path, track?.DeliveryUrl]; + for (const candidate of candidates) { + if (!candidate) { + continue; + } + + const sanitized = candidate.split(/[?#]/)[0]; + const fileName = sanitized.split(/[\\/]/).pop(); + if (fileName) { + return fileName; + } + } + + const codec = (track?.Codec || '').toLowerCase(); + if (VOBSUB_SUBTITLE_CODECS.includes(codec)) { + return 'subtitle.mks'; + } + + return undefined; +} + +function getBitmapSubtitleDisplaySettings() { + const aspectMode = userSettings.getSubtitleAppearanceSettings()?.aspectMode; + const normalizedAspectMode = typeof aspectMode === 'string' ? aspectMode.toLowerCase() : 'stretch'; + + if (BITMAP_SUBTITLE_ASPECT_MODES.includes(normalizedAspectMode)) { + return { + aspectMode: normalizedAspectMode + }; + } + + return { + aspectMode: 'contain' + }; +} + +function getSubtitleTimeOffset(playOptions, subtitleOffset = 0) { + return ((playOptions?.transcodingOffsetTicks || 0) / 10000000) + subtitleOffset; +} + function getDefaultProfile() { return profileBuilder({}); } const PRIMARY_TEXT_TRACK_INDEX = 0; +const VOBSUB_DEBAND_THRESHOLD = 64; +const VOBSUB_DEBAND_RANGE = 15; const SECONDARY_TEXT_TRACK_INDEX = 1; export class HtmlVideoPlayer { @@ -228,7 +276,7 @@ export class HtmlVideoPlayer { /** * @type {any | null | undefined} */ - #currentPgsRenderer; + #currentBitmapSubRenderer; /** * @type {number | undefined} */ @@ -277,6 +325,10 @@ export class HtmlVideoPlayer { * @type {number} */ #fetchQueue = 0; + /** + * @type {Map} + */ + #pendingSubtitleLoads = new Map(); /** * @type {string | undefined} */ @@ -356,6 +408,57 @@ export class HtmlVideoPlayer { } } + /** + * @private + */ + beginPendingSubtitleLoad(targetTextTrackIndex, loadToken) { + const pendingLoad = this.#pendingSubtitleLoads.get(targetTextTrackIndex); + if (!pendingLoad || pendingLoad.token !== loadToken || pendingLoad.active) { + return; + } + + pendingLoad.active = true; + this.incrementFetchQueue(); + } + + /** + * @private + */ + endPendingSubtitleLoad(targetTextTrackIndex, loadToken) { + const pendingLoad = this.#pendingSubtitleLoads.get(targetTextTrackIndex); + if (!pendingLoad || (loadToken && pendingLoad.token !== loadToken)) { + return; + } + + this.#pendingSubtitleLoads.delete(targetTextTrackIndex); + if (pendingLoad.active) { + this.decrementFetchQueue(); + } + } + + /** + * @private + */ + createBitmapSubtitleRendererOptions(videoElement, track, item, targetTextTrackIndex) { + const loadToken = Symbol(String(targetTextTrackIndex)); + const displaySettings = getBitmapSubtitleDisplaySettings(); + this.endPendingSubtitleLoad(targetTextTrackIndex); + this.#pendingSubtitleLoads.set(targetTextTrackIndex, { + token: loadToken, + active: false + }); + + return { + video: videoElement, + subUrl: getTextTrackUrl(track, item), + timeOffset: getSubtitleTimeOffset(this._currentPlayOptions, this.#currentTrackOffset), + ...(displaySettings ? { displaySettings } : {}), + onLoading: () => this.beginPendingSubtitleLoad(targetTextTrackIndex, loadToken), + onLoaded: () => this.endPendingSubtitleLoad(targetTextTrackIndex, loadToken), + onError: () => this.endPendingSubtitleLoad(targetTextTrackIndex, loadToken) + }; + } + /** * @private */ @@ -606,10 +709,10 @@ export class HtmlVideoPlayer { // if .ass currently rendering if (this.#currentAssRenderer) { this.updateCurrentTrackOffset(offsetValue); - this.#currentAssRenderer.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + offsetValue; - } else if (this.#currentPgsRenderer) { + this.#currentAssRenderer.timeOffset = getSubtitleTimeOffset(this._currentPlayOptions, offsetValue); + } else if (this.#currentBitmapSubRenderer) { this.updateCurrentTrackOffset(offsetValue); - this.#currentPgsRenderer.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + offsetValue; + this.#currentBitmapSubRenderer.timeOffset = getSubtitleTimeOffset(this._currentPlayOptions, offsetValue); } else { const trackElements = this.getTextTracks(); // if .vtt currently rendering @@ -621,6 +724,7 @@ export class HtmlVideoPlayer { this.#currentTrackEvents && this.setTrackEventsSubtitleOffset(this.#currentTrackEvents, offsetValue, PRIMARY_TEXT_TRACK_INDEX); this.#currentSecondaryTrackEvents && this.setTrackEventsSubtitleOffset(this.#currentSecondaryTrackEvents, offsetValue, SECONDARY_TEXT_TRACK_INDEX); } else { + this.updateCurrentTrackOffset(offsetValue); console.debug('No available track, cannot apply offset: ', offsetValue); } } @@ -999,10 +1103,15 @@ export class HtmlVideoPlayer { seekOnPlaybackStart(this, e.target, this._currentPlayOptions.playerStartPositionTicks, () => { if (this.#currentAssRenderer) { - this.#currentAssRenderer.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + this.#currentTrackOffset; + this.#currentAssRenderer.timeOffset = getSubtitleTimeOffset(this._currentPlayOptions, this.#currentTrackOffset); this.#currentAssRenderer.resize(); this.#currentAssRenderer.resetRenderAheadCache(false); } + + if (this.#currentBitmapSubRenderer) { + this.#currentBitmapSubRenderer.timeOffset = getSubtitleTimeOffset(this._currentPlayOptions, this.#currentTrackOffset); + this.#currentBitmapSubRenderer.updateCanvasSize?.(); + } }); if (this._currentPlayOptions.fullscreen) { @@ -1181,6 +1290,13 @@ export class HtmlVideoPlayer { * @private */ destroyCustomTrack(videoElement, targetTrackIndex) { + if (targetTrackIndex === undefined) { + this.endPendingSubtitleLoad(PRIMARY_TEXT_TRACK_INDEX); + this.endPendingSubtitleLoad(SECONDARY_TEXT_TRACK_INDEX); + } else { + this.endPendingSubtitleLoad(targetTrackIndex); + } + this.destroyCustomRenderedTrackElements(targetTrackIndex); this.destroyNativeTracks(videoElement, targetTrackIndex); this.destroyStoredTrackInfo(targetTrackIndex); @@ -1191,11 +1307,11 @@ export class HtmlVideoPlayer { } this.#currentAssRenderer = null; - const pgsRenderer = this.#currentPgsRenderer; - if (pgsRenderer) { - pgsRenderer.dispose(); + const pgsOrVobSubRenderer = this.#currentBitmapSubRenderer; + if (pgsOrVobSubRenderer) { + pgsOrVobSubRenderer.dispose(); } - this.#currentPgsRenderer = null; + this.#currentBitmapSubRenderer = null; } /** @@ -1346,17 +1462,78 @@ export class HtmlVideoPlayer { /** * @private */ - renderPgs(videoElement, track, item) { - import('libpgs').then((libpgs) => { - const aspectRatio = this.getAspectRatio() === 'auto' ? 'contain' : this.getAspectRatio(); - const options = { - video: videoElement, - subUrl: getTextTrackUrl(track, item), - workerUrl: `${appRouter.baseUrl()}/libraries/libpgs.worker.js`, - timeOffset: (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000, - aspectRatio - }; - this.#currentPgsRenderer = new libpgs.PgsRenderer(options); + renderPgs(videoElement, track, item, targetTextTrackIndex = PRIMARY_TEXT_TRACK_INDEX) { + const options = this.createBitmapSubtitleRendererOptions(videoElement, track, item, targetTextTrackIndex); + const onLoaded = options.onLoaded; + const onError = options.onError; + options.onLoaded = () => { + if (this.#currentBitmapSubRenderer) { + this.#currentBitmapSubRenderer.timeOffset = getSubtitleTimeOffset(this._currentPlayOptions, this.#currentTrackOffset); + this.#currentBitmapSubRenderer.updateCanvasSize?.(); + } + onLoaded?.(); + }; + options.onError = (error) => { + console.error('[libbitsub] pgs error', error); + onError?.(error); + }; + options.onEvent = (event) => { + if (event?.type === 'error' || event?.type === 'loaded' || event?.type === 'cue-change' || event?.type === 'renderer-change' || event?.type === 'worker-state') { + console.debug('[libbitsub] pgs', event); + } + }; + import('libbitsub').then((libbitsub) => { + this.#currentBitmapSubRenderer = new libbitsub.PgsRenderer(options); + requestAnimationFrame(() => { + if (this.#currentBitmapSubRenderer) { + this.#currentBitmapSubRenderer.updateCanvasSize?.(); + } + }); + }).catch((error) => { + this.endPendingSubtitleLoad(targetTextTrackIndex); + console.error(error); + }); + } + + /** + * @private + */ + renderVobSub(videoElement, track, item, targetTextTrackIndex = PRIMARY_TEXT_TRACK_INDEX) { + const options = { + ...this.createBitmapSubtitleRendererOptions(videoElement, track, item, targetTextTrackIndex), + fileName: getSubtitleFileNameHint(track) + }; + const onLoaded = options.onLoaded; + const onError = options.onError; + options.onLoaded = () => { + if (this.#currentBitmapSubRenderer) { + this.#currentBitmapSubRenderer.timeOffset = getSubtitleTimeOffset(this._currentPlayOptions, this.#currentTrackOffset); + this.#currentBitmapSubRenderer.setDebandEnabled?.(true); + this.#currentBitmapSubRenderer.setDebandThreshold?.(VOBSUB_DEBAND_THRESHOLD); + this.#currentBitmapSubRenderer.setDebandRange?.(VOBSUB_DEBAND_RANGE); + this.#currentBitmapSubRenderer.updateCanvasSize?.(); + } + onLoaded?.(); + }; + options.onError = (error) => { + console.error('[libbitsub] vobsub error', error); + onError?.(error); + }; + options.onEvent = (event) => { + if (event?.type === 'error' || event?.type === 'loaded' || event?.type === 'cue-change' || event?.type === 'renderer-change' || event?.type === 'worker-state') { + console.debug('[libbitsub] vobsub', event); + } + }; + import('libbitsub').then((libbitsub) => { + this.#currentBitmapSubRenderer = new libbitsub.VobSubRenderer(options); + requestAnimationFrame(() => { + if (this.#currentBitmapSubRenderer) { + this.#currentBitmapSubRenderer.updateCanvasSize?.(); + } + }); + }).catch((error) => { + this.endPendingSubtitleLoad(targetTextTrackIndex); + console.error(error); }); } @@ -1443,12 +1620,16 @@ export class HtmlVideoPlayer { async renderTracksEvents(videoElement, track, item, targetTextTrackIndex = PRIMARY_TEXT_TRACK_INDEX) { if (!itemHelper.isLocalItem(item) || track.IsExternal) { const format = (track.Codec || '').toLowerCase(); - if (format === 'ssa' || format === 'ass') { + if (ASS_SUBTITLE_CODECS.includes(format)) { this.renderSsaAss(videoElement, track, item); return; } if (format === 'pgssub') { - this.renderPgs(videoElement, track, item); + this.renderPgs(videoElement, track, item, targetTextTrackIndex); + return; + } + if (VOBSUB_SUBTITLE_CODECS.includes(format)) { + this.renderVobSub(videoElement, track, item, targetTextTrackIndex); return; } @@ -2077,8 +2258,8 @@ export class HtmlVideoPlayer { } } - if (this.#currentPgsRenderer) { - this.#currentPgsRenderer.aspectRatio = val === 'auto' ? 'contain' : val; + if (this.#currentBitmapSubRenderer) { + this.#currentBitmapSubRenderer.updateCanvasSize?.(); } } diff --git a/src/scripts/browserDeviceProfile.js b/src/scripts/browserDeviceProfile.js index c92757f5bd..3503024f42 100644 --- a/src/scripts/browserDeviceProfile.js +++ b/src/scripts/browserDeviceProfile.js @@ -1617,6 +1617,11 @@ export default function (options) { Format: 'pgssub', Method: 'External' }); + profile.SubtitleProfiles.push({ + Format: 'vobsub', + Container: 'mks', + Method: 'External' + }); } } diff --git a/src/scripts/settings/userSettings.js b/src/scripts/settings/userSettings.js index 814ccbd5df..ec7eb179dd 100644 --- a/src/scripts/settings/userSettings.js +++ b/src/scripts/settings/userSettings.js @@ -49,7 +49,8 @@ function filterQuerySettings(query, allowedItems) { } const defaultSubtitleAppearanceSettings = { - verticalPosition: -3 + verticalPosition: -3, + aspectMode: 'contain' }; const defaultComicsPlayerSettings = { diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 693468f439..12e5ea5ab5 100755 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -83,6 +83,10 @@ "BirthDateValue": "Born: {0}", "BirthLocation": "Birth location", "BirthPlaceValue": "Birth place: {0}", + "BitmapSubtitleAspectModeContain": "Contain", + "BitmapSubtitleAspectModeCover": "Cover", + "BitmapSubtitleAspectModeHelp": "Controls how client-rendered PGS and VobSub subtitles are scaled to the visible video area.", + "BitmapSubtitleAspectModeStretch": "Stretch", "Blacklist": "Blacklist", "BlockContentWithTagsHelp": "Hide media with at least one of the specified tags.", "BookLibraryHelp": "Audio and text books are supported. Review the {0} book naming guide {1}.", @@ -670,6 +674,7 @@ "LabelBlockContentWithTags": "Block items with tags", "LabelBuildVersion": "Build version", "LabelBundled": "Bundled", + "LabelBitmapSubtitleAspectMode": "Bitmap subtitle aspect mode", "LabelBurnSubtitles": "Burn subtitles", "LabelCache": "Cache", "LabelCachePath": "Cache path", @@ -1515,8 +1520,8 @@ "Remixer": "Remixer", "RemoveFromCollection": "Remove from collection", "RemoveFromPlaylist": "Remove from playlist", - "RenderPgsSubtitle": "Experimental PGS subtitle rendering", - "RenderPgsSubtitleHelp": "Determine if the client should render PGS subtitles instead of using burned in subtitles. This can avoid server-side transcoding in exchange of client-side rendering performance.", + "RenderPgsSubtitle": "Experimental PGS and VobSub subtitle rendering", + "RenderPgsSubtitleHelp": "Determine if the client should render PGS and VobSub subtitles instead of using burned in subtitles. This can avoid server-side transcoding in exchange of client-side rendering performance.", "Repeat": "Repeat", "RepeatAll": "Repeat all", "RepeatEpisodes": "Repeat episodes", diff --git a/webpack.common.js b/webpack.common.js index 9ac83db382..acf1971a1e 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -16,8 +16,7 @@ const Assets = [ '@jellyfin/libass-wasm/dist/js/subtitles-octopus-worker.js', '@jellyfin/libass-wasm/dist/js/subtitles-octopus-worker.wasm', '@jellyfin/libass-wasm/dist/js/subtitles-octopus-worker-legacy.js', - 'pdfjs-dist/build/pdf.worker.js', - 'libpgs/dist/libpgs.worker.js' + 'pdfjs-dist/build/pdf.worker.js' ]; const DEV_MODE = process.env.NODE_ENV !== 'production'; @@ -227,6 +226,7 @@ const config = { path.resolve(__dirname, 'node_modules/highlight-words'), path.resolve(__dirname, 'node_modules/idb-keyval'), path.resolve(__dirname, 'node_modules/libarchive.js'), + path.resolve(__dirname, 'node_modules/libbitsub'), path.resolve(__dirname, 'node_modules/linkify-it'), path.resolve(__dirname, 'node_modules/markdown-it'), path.resolve(__dirname, 'node_modules/material-react-table'),