diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index a036008033..784c7248f2 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1,4 +1,5 @@ import { PlaybackErrorCode } from '@jellyfin/sdk/lib/generated-client/models/playback-error-code.js'; +import { getMediaInfoApi } from '@jellyfin/sdk/lib/utils/api/media-info-api'; import merge from 'lodash-es/merge'; import Screenfull from 'screenfull'; @@ -22,6 +23,7 @@ import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type' import { MediaError } from 'types/mediaError'; import { getMediaError } from 'utils/mediaError'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind.js'; const UNLIMITED_ITEMS = -1; @@ -402,9 +404,9 @@ function setStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPositio }); } -function getPlaybackInfo(player, apiClient, item, deviceProfile, mediaSourceId, liveStreamId, options) { +async function getPlaybackInfo(player, apiClient, item, deviceProfile, mediaSourceId, liveStreamId, options) { if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio' && !player.useServerPlaybackInfoForAudio) { - return Promise.resolve({ + return { MediaSources: [ { StreamUrl: getAudioStreamUrlFromDeviceProfile(item, deviceProfile, options.maxBitrate, apiClient, options.startPosition), @@ -412,13 +414,13 @@ function getPlaybackInfo(player, apiClient, item, deviceProfile, mediaSourceId, MediaStreams: [], RunTimeTicks: item.RunTimeTicks }] - }); + }; } if (item.PresetMediaSource) { - return Promise.resolve({ + return { MediaSources: [item.PresetMediaSource] - }); + }; } const itemId = item.Id; @@ -428,6 +430,9 @@ function getPlaybackInfo(player, apiClient, item, deviceProfile, mediaSourceId, StartTimeTicks: options.startPosition || 0 }; + const api = toApi(apiClient); + const mediaInfoApi = getMediaInfoApi(api); + if (options.isPlayback) { query.IsPlayback = true; query.AutoOpenLiveStream = true; @@ -481,7 +486,12 @@ function getPlaybackInfo(player, apiClient, item, deviceProfile, mediaSourceId, query.DirectPlayProtocols = player.getDirectPlayProtocols(); } - return apiClient.getPlaybackInfo(itemId, query, deviceProfile); + query.AlwaysBurnInSubtitleWhenTranscoding = appSettings.alwaysBurnInSubtitleWhenTranscoding(); + + query.DeviceProfile = deviceProfile; + + const res = await mediaInfoApi.getPostedPlaybackInfo({ itemId: itemId, playbackInfoDto: query }); + return res.data; } function getOptimalMediaSource(apiClient, item, versions) { diff --git a/src/components/subtitlesettings/subtitlesettings.js b/src/components/subtitlesettings/subtitlesettings.js index da548a747c..b7852a79fb 100644 --- a/src/components/subtitlesettings/subtitlesettings.js +++ b/src/components/subtitlesettings/subtitlesettings.js @@ -64,6 +64,7 @@ function loadForm(context, user, userSettings, appearanceSettings, apiClient) { context.querySelector('#chkSubtitleRenderPgs').checked = appSettings.get('subtitlerenderpgs') === 'true'; context.querySelector('#selectSubtitleBurnIn').dispatchEvent(new CustomEvent('change', {})); + context.querySelector('#chkAlwaysBurnInSubtitleWhenTranscoding').checked = appSettings.alwaysBurnInSubtitleWhenTranscoding(); onAppearanceFieldChange({ target: context.querySelector('#selectTextSize') @@ -90,6 +91,7 @@ function save(instance, context, userId, userSettings, apiClient, enableSaveConf appSettings.set('subtitleburnin', context.querySelector('#selectSubtitleBurnIn').value); appSettings.set('subtitlerenderpgs', context.querySelector('#chkSubtitleRenderPgs').checked); + appSettings.alwaysBurnInSubtitleWhenTranscoding(context.querySelector('#chkAlwaysBurnInSubtitleWhenTranscoding').checked); apiClient.getUser(userId).then(function (user) { saveUser(context, user, userSettings, instance.appearanceKey, apiClient).then(function () { diff --git a/src/components/subtitlesettings/subtitlesettings.template.html b/src/components/subtitlesettings/subtitlesettings.template.html index 003a2a4e22..32584fc6da 100644 --- a/src/components/subtitlesettings/subtitlesettings.template.html +++ b/src/components/subtitlesettings/subtitlesettings.template.html @@ -42,6 +42,14 @@ +
+ +
${AlwaysBurnInSubtitleWhenTranscodingHelp}
+
+

${HeaderSubtitleAppearance} diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 46a3062a11..0697b65e56 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -1,6 +1,7 @@ import DOMPurify from 'dompurify'; import browser from '../../scripts/browser'; +import appSettings from '../../scripts/settings/appSettings'; import { appHost } from '../../components/apphost'; import loading from '../../components/loading/loading'; import dom from '../../scripts/dom'; @@ -1566,16 +1567,53 @@ export class HtmlVideoPlayer { return t.Index === streamIndex; })[0]; - this.setTrackForDisplay(this.#mediaElement, track, targetTextTrackIndex); - if (enableNativeTrackSupport(this._currentPlayOptions?.mediaSource, track)) { - if (streamIndex !== -1) { - this.setCueAppearance(); - } + // This play method can only check if it is real direct play, and will mark Remux as Transcode as well + const isDirectPlay = this._currentPlayOptions.playMethod === 'DirectPlay'; + const burnInWhenTranscoding = appSettings.alwaysBurnInSubtitleWhenTranscoding(); + + let sessionPromise; + if (!isDirectPlay && burnInWhenTranscoding) { + const apiClient = ServerConnections.getApiClient(this._currentPlayOptions.item.ServerId); + sessionPromise = apiClient.getSessions({ + deviceId: apiClient.deviceId() + }).then(function (sessions) { + return sessions[0] || {}; + }, function () { + return Promise.resolve({}); + }); } else { - // null these out to disable the player's native display (handled below) - streamIndex = -1; - track = null; + sessionPromise = Promise.resolve({}); } + + const player = this; + + sessionPromise.then((s) => { + if (!s.TranscodingInfo || s.TranscodingInfo.IsVideoDirect) { + // restore recorded delivery method if any + mediaStreamTextTracks.forEach((t) => { + t.DeliveryMethod = t.realDeliveryMethod ?? t.DeliveryMethod; + }); + player.setTrackForDisplay(player.#mediaElement, track, targetTextTrackIndex); + if (enableNativeTrackSupport(player._currentPlayOptions?.mediaSource, track)) { + if (streamIndex !== -1) { + player.setCueAppearance(); + } + } else { + // null these out to disable the player's native display (handled below) + streamIndex = -1; + track = null; + } + } else { + // record the original delivery method and set all delivery method to encode + // this is needed for subtitle track switching to properly reload the video stream + mediaStreamTextTracks.forEach((t) => { + t.realDeliveryMethod = t.DeliveryMethod; + t.DeliveryMethod = 'Encode'; + }); + // unset stream when switching to transcode + player.setTrackForDisplay(player.#mediaElement, null, -1); + } + }); } /** diff --git a/src/scripts/settings/appSettings.js b/src/scripts/settings/appSettings.js index cf6cbea284..e10d6053d3 100644 --- a/src/scripts/settings/appSettings.js +++ b/src/scripts/settings/appSettings.js @@ -156,6 +156,19 @@ class AppSettings { return this.get('preferredTranscodeVideoAudioCodec') || ''; } + /** + * Get or set 'Always burn in subtitle when transcoding' state. + * @param {boolean|undefined} val - Flag to enable 'Always burn in subtitle when transcoding' or undefined. + * @return {boolean} 'Always burn in subtitle when transcoding' state. + */ + alwaysBurnInSubtitleWhenTranscoding(val) { + if (val !== undefined) { + return this.set('alwaysBurnInSubtitleWhenTranscoding', val.toString()); + } + + return toBoolean(this.get('alwaysBurnInSubtitleWhenTranscoding'), false); + } + /** * Get or set 'Enable DTS' state. * @param {boolean|undefined} val - Flag to enable 'Enable DTS' or undefined. diff --git a/src/strings/en-us.json b/src/strings/en-us.json index a220b2341e..88f0125fbc 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -34,6 +34,8 @@ "AllowStreamSharingHelp": "Allow Jellyfin to duplicate the mpegts stream from tuner and share this duplicated stream to its clients. This is useful when the tuner has total stream count limit but may also cause playback issues.", "Alternate" : "Alternate", "AlternateDVD" : "Alternate DVD", + "AlwaysBurnInSubtitleWhenTranscoding": "Always burn in subtitle when transcoding", + "AlwaysBurnInSubtitleWhenTranscodingHelp": "Burn in all subtitles when transcoding is triggered. This ensures subtitle synchronization after transcoding at the cost of reduced transcoding speed.", "LabelThrottleDelaySeconds": "Throttle after", "LabelThrottleDelaySecondsHelp": "Time in seconds after which the transcoder will be throttled. Must be large enough for the client to maintain a healthy buffer. Only works if throttling is enabled.", "LabelSegmentKeepSeconds": "Time to keep segments", @@ -84,7 +86,7 @@ "BoxRear": "Box (rear)", "BoxSet": "Box Set", "Browse": "Browse", - "BurnSubtitlesHelp": "Determine if the server should burn in subtitles while transcoding videos. Avoiding this will greatly improve performance. Select Auto to burn image based formats (VobSub, PGS, SUB, IDX, etc.) and certain ASS or SSA subtitles.", + "BurnSubtitlesHelp": "Determine if the server should burn in subtitles. Avoiding this will greatly improve performance. Select Auto to burn image based formats (VobSub, PGS, SUB, IDX, etc.) and certain ASS or SSA subtitles.", "ButtonActivate": "Activate", "ButtonAddImage": "Add Image", "ButtonAddMediaLibrary": "Add Media Library",