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:
commit
6d0f0e85a6
8 changed files with 89 additions and 2 deletions
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 = [];
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue