Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for native PGS subtitle rendering without transcoding #5688

Merged
merged 7 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"jquery": "3.7.1",
"jstree": "3.3.16",
"libarchive.js": "2.0.2",
"libpgs": "0.6.0",
"lodash-es": "4.17.21",
"markdown-it": "14.1.0",
"material-design-icons-iconfont": "6.7.0",
Expand Down
13 changes: 13 additions & 0 deletions src/components/subtitlesettings/subtitlesettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ function loadForm(context, user, userSettings, appearanceSettings, apiClient) {
context.querySelector('#sliderVerticalPosition').value = appearanceSettings.verticalPosition;

context.querySelector('#selectSubtitleBurnIn').value = appSettings.get('subtitleburnin') || '';
context.querySelector('#chkSubtitleRenderPgs').checked = appSettings.get('subtitlerenderpgs') === 'true';

context.querySelector('#selectSubtitleBurnIn').dispatchEvent(new CustomEvent('change', {}));

onAppearanceFieldChange({
target: context.querySelector('#selectTextSize')
Expand All @@ -86,6 +89,7 @@ function save(instance, context, userId, userSettings, apiClient, enableSaveConf
loading.show();

appSettings.set('subtitleburnin', context.querySelector('#selectSubtitleBurnIn').value);
appSettings.set('subtitlerenderpgs', context.querySelector('#chkSubtitleRenderPgs').checked);

apiClient.getUser(userId).then(function (user) {
saveUser(context, user, userSettings, instance.appearanceKey, apiClient).then(function () {
Expand All @@ -111,6 +115,14 @@ function onSubtitleModeChange(e) {
view.querySelector('.subtitles' + this.value + 'Help').classList.remove('hide');
}

function onSubtitleBurnInChange(e) {
const view = dom.parentWithClass(e.target, 'subtitlesettings');
const fieldRenderPgs = view.querySelector('.fldRenderPgs');

// Pgs option is only available if burn-in mode is set to 'auto' (empty string)
fieldRenderPgs.classList.toggle('hide', !!this.value);
}

function onAppearanceFieldChange(e) {
const view = dom.parentWithClass(e.target, 'subtitlesettings');

Expand Down Expand Up @@ -166,6 +178,7 @@ function embed(options, self) {
options.element.querySelector('form').addEventListener('submit', self.onSubmit.bind(self));

options.element.querySelector('#selectSubtitlePlaybackMode').addEventListener('change', onSubtitleModeChange);
options.element.querySelector('#selectSubtitleBurnIn').addEventListener('change', onSubtitleBurnInChange);
options.element.querySelector('#selectTextSize').addEventListener('change', onAppearanceFieldChange);
options.element.querySelector('#selectTextWeight').addEventListener('change', onAppearanceFieldChange);
options.element.querySelector('#selectDropShadow').addEventListener('change', onAppearanceFieldChange);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ <h2 class="sectionTitle">
</select>
<div class="fieldDescription">${BurnSubtitlesHelp}</div>
</div>

<div class="checkboxContainer checkboxContainer-withDescription fldRenderPgs hide">
<label>
<input is="emby-checkbox" type="checkbox" id="chkSubtitleRenderPgs" />
<span>${RenderPgsSubtitle}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${RenderPgsSubtitleHelp}</div>
</div>
</div>

<div class="verticalSection subtitleAppearanceSection hide">
Expand Down
34 changes: 33 additions & 1 deletion src/plugins/htmlVideoPlayer/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function enableNativeTrackSupport(mediaSource, track) {

if (track) {
const format = (track.Codec || '').toLowerCase();
if (format === 'ssa' || format === 'ass') {
if (format === 'ssa' || format === 'ass' || format === 'pgssub') {
return false;
}
}
Expand Down Expand Up @@ -213,6 +213,10 @@ export class HtmlVideoPlayer {
* @type {any | null | undefined}
*/
#currentAssRenderer;
/**
* @type {any | null | undefined}
*/
#currentPgsRenderer;
/**
* @type {number | undefined}
*/
Expand Down Expand Up @@ -590,6 +594,9 @@ export class HtmlVideoPlayer {
if (this.#currentAssRenderer) {
this.updateCurrentTrackOffset(offsetValue);
this.#currentAssRenderer.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + offsetValue;
} else if (this.#currentPgsRenderer) {
this.updateCurrentTrackOffset(offsetValue);
this.#currentPgsRenderer.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + offsetValue;
} else {
const trackElements = this.getTextTracks();
// if .vtt currently rendering
Expand Down Expand Up @@ -1172,6 +1179,12 @@ export class HtmlVideoPlayer {
octopus.dispose();
}
this.#currentAssRenderer = null;

const pgsRenderer = this.#currentPgsRenderer;
if (pgsRenderer) {
pgsRenderer.dispose();
}
this.#currentPgsRenderer = null;
}

/**
Expand Down Expand Up @@ -1316,6 +1329,21 @@ export class HtmlVideoPlayer {
});
}

/**
* @private
*/
renderPgs(videoElement, track, item) {
import('libpgs').then((libpgs) => {
const options = {
video: videoElement,
subUrl: getTextTrackUrl(track, item),
workerUrl: `${appRouter.baseUrl()}/libraries/libpgs.worker.js`,
timeOffset: (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000
};
this.#currentPgsRenderer = new libpgs.PgsRenderer(options);
});
}

/**
* @private
*/
Expand Down Expand Up @@ -1434,6 +1462,10 @@ export class HtmlVideoPlayer {
this.renderSsaAss(videoElement, track, item);
return;
}
if (format === 'pgssub') {
this.renderPgs(videoElement, track, item);
return;
}

if (this.requiresCustomSubtitlesElement()) {
this.renderSubtitlesWithCustomElement(videoElement, track, item, targetTextTrackIndex);
Expand Down
18 changes: 18 additions & 0 deletions src/scripts/browserDeviceProfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ function supportsTextTracks() {
return _supportsTextTracks;
}

let _supportsCanvas2D;
function supportsCanvas2D() {
if (_supportsCanvas2D == null) {
_supportsCanvas2D = document.createElement('canvas').getContext('2d') != null;
}

return _supportsCanvas2D;
}

let _canPlayHls;
function canPlayHls() {
if (_canPlayHls == null) {
Expand Down Expand Up @@ -1415,6 +1424,7 @@ export default function (options) {
// External vtt or burn in
profile.SubtitleProfiles = [];
const subtitleBurninSetting = appSettings.get('subtitleburnin');
const subtitleRenderPgsSetting = appSettings.get('subtitlerenderpgs') === 'true';
if (subtitleBurninSetting !== 'all') {
if (supportsTextTracks()) {
profile.SubtitleProfiles.push({
Expand All @@ -1432,6 +1442,14 @@ export default function (options) {
Method: 'External'
});
}

if (supportsCanvas2D() && options.enablePgsRender !== false && !options.isRetry && subtitleRenderPgsSetting
&& subtitleBurninSetting !== 'allcomplexformats' && subtitleBurninSetting !== 'onlyimageformats') {
profile.SubtitleProfiles.push({
Format: 'pgssub',
Method: 'External'
});
}
}

profile.ResponseProfiles = [];
Expand Down
2 changes: 2 additions & 0 deletions src/strings/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,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.",
"Repeat": "Repeat",
"RepeatAll": "Repeat all",
"RepeatEpisodes": "Repeat episodes",
Expand Down
3 changes: 2 additions & 1 deletion webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ 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'
'pdfjs-dist/build/pdf.worker.js',
'libpgs/dist/libpgs.worker.js'
];

const DEV_MODE = process.env.NODE_ENV !== 'production';
Expand Down
Loading