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

Merge pull request #5688 from Arcus92/native-pgs

Add support for native PGS subtitle rendering without transcoding
This commit is contained in:
Bill Thornton 2024-09-20 15:55:30 -04:00 committed by GitHub
commit 6d0f0e85a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 89 additions and 2 deletions

12
package-lock.json generated
View file

@ -45,6 +45,7 @@
"jquery": "3.7.1", "jquery": "3.7.1",
"jstree": "3.3.17", "jstree": "3.3.17",
"libarchive.js": "2.0.2", "libarchive.js": "2.0.2",
"libpgs": "0.6.0",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"markdown-it": "14.1.0", "markdown-it": "14.1.0",
"material-design-icons-iconfont": "6.7.0", "material-design-icons-iconfont": "6.7.0",
@ -14913,6 +14914,12 @@
"comlink": "^4.4.1" "comlink": "^4.4.1"
} }
}, },
"node_modules/libpgs": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/libpgs/-/libpgs-0.6.0.tgz",
"integrity": "sha512-k8ic6VTJTCun8Ump8iAe+tZi3pa1ZhDlq1v4hmZOmYQzSQ44QpZoClMXuSfJ1B91eRWOO6q50rXhyCPuB9dXbg==",
"license": "MIT"
},
"node_modules/lie": { "node_modules/lie": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
@ -36350,6 +36357,11 @@
"comlink": "^4.4.1" "comlink": "^4.4.1"
} }
}, },
"libpgs": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/libpgs/-/libpgs-0.6.0.tgz",
"integrity": "sha512-k8ic6VTJTCun8Ump8iAe+tZi3pa1ZhDlq1v4hmZOmYQzSQ44QpZoClMXuSfJ1B91eRWOO6q50rXhyCPuB9dXbg=="
},
"lie": { "lie": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",

View file

@ -106,6 +106,7 @@
"jquery": "3.7.1", "jquery": "3.7.1",
"jstree": "3.3.17", "jstree": "3.3.17",
"libarchive.js": "2.0.2", "libarchive.js": "2.0.2",
"libpgs": "0.6.0",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"markdown-it": "14.1.0", "markdown-it": "14.1.0",
"material-design-icons-iconfont": "6.7.0", "material-design-icons-iconfont": "6.7.0",

View file

@ -61,6 +61,9 @@ function loadForm(context, user, userSettings, appearanceSettings, apiClient) {
context.querySelector('#sliderVerticalPosition').value = appearanceSettings.verticalPosition; context.querySelector('#sliderVerticalPosition').value = appearanceSettings.verticalPosition;
context.querySelector('#selectSubtitleBurnIn').value = appSettings.get('subtitleburnin') || ''; context.querySelector('#selectSubtitleBurnIn').value = appSettings.get('subtitleburnin') || '';
context.querySelector('#chkSubtitleRenderPgs').checked = appSettings.get('subtitlerenderpgs') === 'true';
context.querySelector('#selectSubtitleBurnIn').dispatchEvent(new CustomEvent('change', {}));
onAppearanceFieldChange({ onAppearanceFieldChange({
target: context.querySelector('#selectTextSize') target: context.querySelector('#selectTextSize')
@ -86,6 +89,7 @@ function save(instance, context, userId, userSettings, apiClient, enableSaveConf
loading.show(); loading.show();
appSettings.set('subtitleburnin', context.querySelector('#selectSubtitleBurnIn').value); appSettings.set('subtitleburnin', context.querySelector('#selectSubtitleBurnIn').value);
appSettings.set('subtitlerenderpgs', context.querySelector('#chkSubtitleRenderPgs').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 () {
@ -111,6 +115,14 @@ function onSubtitleModeChange(e) {
view.querySelector('.subtitles' + this.value + 'Help').classList.remove('hide'); 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) { function onAppearanceFieldChange(e) {
const view = dom.parentWithClass(e.target, 'subtitlesettings'); const view = dom.parentWithClass(e.target, 'subtitlesettings');
@ -166,6 +178,7 @@ function embed(options, self) {
options.element.querySelector('form').addEventListener('submit', self.onSubmit.bind(self)); options.element.querySelector('form').addEventListener('submit', self.onSubmit.bind(self));
options.element.querySelector('#selectSubtitlePlaybackMode').addEventListener('change', onSubtitleModeChange); options.element.querySelector('#selectSubtitlePlaybackMode').addEventListener('change', onSubtitleModeChange);
options.element.querySelector('#selectSubtitleBurnIn').addEventListener('change', onSubtitleBurnInChange);
options.element.querySelector('#selectTextSize').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#selectTextSize').addEventListener('change', onAppearanceFieldChange);
options.element.querySelector('#selectTextWeight').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#selectTextWeight').addEventListener('change', onAppearanceFieldChange);
options.element.querySelector('#selectDropShadow').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#selectDropShadow').addEventListener('change', onAppearanceFieldChange);

View file

@ -32,6 +32,14 @@
</select> </select>
<div class="fieldDescription">${BurnSubtitlesHelp}</div> <div class="fieldDescription">${BurnSubtitlesHelp}</div>
</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>
<div class="verticalSection subtitleAppearanceSection hide"> <div class="verticalSection subtitleAppearanceSection hide">

View file

@ -100,7 +100,7 @@ function enableNativeTrackSupport(mediaSource, track) {
if (track) { if (track) {
const format = (track.Codec || '').toLowerCase(); const format = (track.Codec || '').toLowerCase();
if (format === 'ssa' || format === 'ass') { if (format === 'ssa' || format === 'ass' || format === 'pgssub') {
return false; return false;
} }
} }
@ -213,6 +213,10 @@ export class HtmlVideoPlayer {
* @type {any | null | undefined} * @type {any | null | undefined}
*/ */
#currentAssRenderer; #currentAssRenderer;
/**
* @type {any | null | undefined}
*/
#currentPgsRenderer;
/** /**
* @type {number | undefined} * @type {number | undefined}
*/ */
@ -590,6 +594,9 @@ export class HtmlVideoPlayer {
if (this.#currentAssRenderer) { if (this.#currentAssRenderer) {
this.updateCurrentTrackOffset(offsetValue); this.updateCurrentTrackOffset(offsetValue);
this.#currentAssRenderer.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + 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 { } else {
const trackElements = this.getTextTracks(); const trackElements = this.getTextTracks();
// if .vtt currently rendering // if .vtt currently rendering
@ -1172,6 +1179,12 @@ export class HtmlVideoPlayer {
octopus.dispose(); octopus.dispose();
} }
this.#currentAssRenderer = null; this.#currentAssRenderer = null;
const pgsRenderer = this.#currentPgsRenderer;
if (pgsRenderer) {
pgsRenderer.dispose();
}
this.#currentPgsRenderer = null;
} }
/** /**
@ -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 * @private
*/ */
@ -1434,6 +1462,10 @@ export class HtmlVideoPlayer {
this.renderSsaAss(videoElement, track, item); this.renderSsaAss(videoElement, track, item);
return; return;
} }
if (format === 'pgssub') {
this.renderPgs(videoElement, track, item);
return;
}
if (this.requiresCustomSubtitlesElement()) { if (this.requiresCustomSubtitlesElement()) {
this.renderSubtitlesWithCustomElement(videoElement, track, item, targetTextTrackIndex); this.renderSubtitlesWithCustomElement(videoElement, track, item, targetTextTrackIndex);

View file

@ -48,6 +48,15 @@ function supportsTextTracks() {
return _supportsTextTracks; return _supportsTextTracks;
} }
let _supportsCanvas2D;
function supportsCanvas2D() {
if (_supportsCanvas2D == null) {
_supportsCanvas2D = document.createElement('canvas').getContext('2d') != null;
}
return _supportsCanvas2D;
}
let _canPlayHls; let _canPlayHls;
function canPlayHls() { function canPlayHls() {
if (_canPlayHls == null) { if (_canPlayHls == null) {
@ -1424,6 +1433,7 @@ export default function (options) {
// External vtt or burn in // External vtt or burn in
profile.SubtitleProfiles = []; profile.SubtitleProfiles = [];
const subtitleBurninSetting = appSettings.get('subtitleburnin'); const subtitleBurninSetting = appSettings.get('subtitleburnin');
const subtitleRenderPgsSetting = appSettings.get('subtitlerenderpgs') === 'true';
if (subtitleBurninSetting !== 'all') { if (subtitleBurninSetting !== 'all') {
if (supportsTextTracks()) { if (supportsTextTracks()) {
profile.SubtitleProfiles.push({ profile.SubtitleProfiles.push({
@ -1441,6 +1451,14 @@ export default function (options) {
Method: 'External' Method: 'External'
}); });
} }
if (supportsCanvas2D() && options.enablePgsRender !== false && !options.isRetry && subtitleRenderPgsSetting
&& subtitleBurninSetting !== 'allcomplexformats' && subtitleBurninSetting !== 'onlyimageformats') {
profile.SubtitleProfiles.push({
Format: 'pgssub',
Method: 'External'
});
}
} }
profile.ResponseProfiles = []; profile.ResponseProfiles = [];

View file

@ -1393,6 +1393,8 @@
"Remixer": "Remixer", "Remixer": "Remixer",
"RemoveFromCollection": "Remove from collection", "RemoveFromCollection": "Remove from collection",
"RemoveFromPlaylist": "Remove from playlist", "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", "Repeat": "Repeat",
"RepeatAll": "Repeat all", "RepeatAll": "Repeat all",
"RepeatEpisodes": "Repeat episodes", "RepeatEpisodes": "Repeat episodes",

View file

@ -15,7 +15,8 @@ const Assets = [
'@jellyfin/libass-wasm/dist/js/subtitles-octopus-worker.js', '@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.wasm',
'@jellyfin/libass-wasm/dist/js/subtitles-octopus-worker-legacy.js', '@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'; const DEV_MODE = process.env.NODE_ENV !== 'production';