1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge pull request #5906 from gnattu/burn-subtitle-transcoding

This commit is contained in:
Bill Thornton 2024-10-09 14:15:18 -04:00 committed by GitHub
commit 52d4919df4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 88 additions and 15 deletions

View file

@ -1,4 +1,5 @@
import { PlaybackErrorCode } from '@jellyfin/sdk/lib/generated-client/models/playback-error-code.js'; 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 merge from 'lodash-es/merge';
import Screenfull from 'screenfull'; 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 { MediaError } from 'types/mediaError';
import { getMediaError } from 'utils/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'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind.js';
const UNLIMITED_ITEMS = -1; 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) { if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio' && !player.useServerPlaybackInfoForAudio) {
return Promise.resolve({ return {
MediaSources: [ MediaSources: [
{ {
StreamUrl: getAudioStreamUrlFromDeviceProfile(item, deviceProfile, options.maxBitrate, apiClient, options.startPosition), StreamUrl: getAudioStreamUrlFromDeviceProfile(item, deviceProfile, options.maxBitrate, apiClient, options.startPosition),
@ -412,13 +414,13 @@ function getPlaybackInfo(player, apiClient, item, deviceProfile, mediaSourceId,
MediaStreams: [], MediaStreams: [],
RunTimeTicks: item.RunTimeTicks RunTimeTicks: item.RunTimeTicks
}] }]
}); };
} }
if (item.PresetMediaSource) { if (item.PresetMediaSource) {
return Promise.resolve({ return {
MediaSources: [item.PresetMediaSource] MediaSources: [item.PresetMediaSource]
}); };
} }
const itemId = item.Id; const itemId = item.Id;
@ -428,6 +430,9 @@ function getPlaybackInfo(player, apiClient, item, deviceProfile, mediaSourceId,
StartTimeTicks: options.startPosition || 0 StartTimeTicks: options.startPosition || 0
}; };
const api = toApi(apiClient);
const mediaInfoApi = getMediaInfoApi(api);
if (options.isPlayback) { if (options.isPlayback) {
query.IsPlayback = true; query.IsPlayback = true;
query.AutoOpenLiveStream = true; query.AutoOpenLiveStream = true;
@ -481,7 +486,12 @@ function getPlaybackInfo(player, apiClient, item, deviceProfile, mediaSourceId,
query.DirectPlayProtocols = player.getDirectPlayProtocols(); 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) { function getOptimalMediaSource(apiClient, item, versions) {

View file

@ -64,6 +64,7 @@ function loadForm(context, user, userSettings, appearanceSettings, apiClient) {
context.querySelector('#chkSubtitleRenderPgs').checked = appSettings.get('subtitlerenderpgs') === 'true'; context.querySelector('#chkSubtitleRenderPgs').checked = appSettings.get('subtitlerenderpgs') === 'true';
context.querySelector('#selectSubtitleBurnIn').dispatchEvent(new CustomEvent('change', {})); context.querySelector('#selectSubtitleBurnIn').dispatchEvent(new CustomEvent('change', {}));
context.querySelector('#chkAlwaysBurnInSubtitleWhenTranscoding').checked = appSettings.alwaysBurnInSubtitleWhenTranscoding();
onAppearanceFieldChange({ onAppearanceFieldChange({
target: context.querySelector('#selectTextSize') 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('subtitleburnin', context.querySelector('#selectSubtitleBurnIn').value);
appSettings.set('subtitlerenderpgs', context.querySelector('#chkSubtitleRenderPgs').checked); appSettings.set('subtitlerenderpgs', context.querySelector('#chkSubtitleRenderPgs').checked);
appSettings.alwaysBurnInSubtitleWhenTranscoding(context.querySelector('#chkAlwaysBurnInSubtitleWhenTranscoding').checked);
apiClient.getUser(userId).then(function (user) { apiClient.getUser(userId).then(function (user) {
saveUser(context, user, userSettings, instance.appearanceKey, apiClient).then(function () { saveUser(context, user, userSettings, instance.appearanceKey, apiClient).then(function () {

View file

@ -42,6 +42,14 @@
</div> </div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" type="checkbox" id="chkAlwaysBurnInSubtitleWhenTranscoding" />
<span>${AlwaysBurnInSubtitleWhenTranscoding}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${AlwaysBurnInSubtitleWhenTranscodingHelp}</div>
</div>
<div class="verticalSection subtitleAppearanceSection hide"> <div class="verticalSection subtitleAppearanceSection hide">
<h2 class="sectionTitle"> <h2 class="sectionTitle">
${HeaderSubtitleAppearance} ${HeaderSubtitleAppearance}

View file

@ -1,6 +1,7 @@
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import browser from '../../scripts/browser'; import browser from '../../scripts/browser';
import appSettings from '../../scripts/settings/appSettings';
import { appHost } from '../../components/apphost'; import { appHost } from '../../components/apphost';
import loading from '../../components/loading/loading'; import loading from '../../components/loading/loading';
import dom from '../../scripts/dom'; import dom from '../../scripts/dom';
@ -1566,16 +1567,53 @@ export class HtmlVideoPlayer {
return t.Index === streamIndex; return t.Index === streamIndex;
})[0]; })[0];
this.setTrackForDisplay(this.#mediaElement, track, targetTextTrackIndex); // This play method can only check if it is real direct play, and will mark Remux as Transcode as well
if (enableNativeTrackSupport(this._currentPlayOptions?.mediaSource, track)) { const isDirectPlay = this._currentPlayOptions.playMethod === 'DirectPlay';
if (streamIndex !== -1) { const burnInWhenTranscoding = appSettings.alwaysBurnInSubtitleWhenTranscoding();
this.setCueAppearance();
} 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 { } else {
// null these out to disable the player's native display (handled below) sessionPromise = Promise.resolve({});
streamIndex = -1;
track = null;
} }
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);
}
});
} }
/** /**

View file

@ -156,6 +156,19 @@ class AppSettings {
return this.get('preferredTranscodeVideoAudioCodec') || ''; 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. * Get or set 'Enable DTS' state.
* @param {boolean|undefined} val - Flag to enable 'Enable DTS' or undefined. * @param {boolean|undefined} val - Flag to enable 'Enable DTS' or undefined.

View file

@ -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.", "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", "Alternate" : "Alternate",
"AlternateDVD" : "Alternate DVD", "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", "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.", "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", "LabelSegmentKeepSeconds": "Time to keep segments",
@ -84,7 +86,7 @@
"BoxRear": "Box (rear)", "BoxRear": "Box (rear)",
"BoxSet": "Box Set", "BoxSet": "Box Set",
"Browse": "Browse", "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", "ButtonActivate": "Activate",
"ButtonAddImage": "Add Image", "ButtonAddImage": "Add Image",
"ButtonAddMediaLibrary": "Add Media Library", "ButtonAddMediaLibrary": "Add Media Library",