mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
feat: add native secondary subtitle support
This commit is contained in:
parent
d69d4b22d9
commit
145aea184f
4 changed files with 192 additions and 20 deletions
|
@ -876,6 +876,17 @@ class PlaybackManager {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.hasSecondarySubtitleSupport = function (player = self._currentPlayer) {
|
||||||
|
if (!player) return false;
|
||||||
|
return Boolean(player.supports('SecondarySubtitles'));
|
||||||
|
};
|
||||||
|
|
||||||
|
self.secondarySubtitleTracks = function (player = self._currentPlayer) {
|
||||||
|
const streams = self.subtitleTracks(player);
|
||||||
|
// Currently, only External subtitles are supported
|
||||||
|
return streams.filter((stream) => getDeliveryMethod(stream) === 'External');
|
||||||
|
};
|
||||||
|
|
||||||
function getCurrentSubtitleStream(player) {
|
function getCurrentSubtitleStream(player) {
|
||||||
if (!player) {
|
if (!player) {
|
||||||
throw new Error('player cannot be null');
|
throw new Error('player cannot be null');
|
||||||
|
@ -890,6 +901,20 @@ class PlaybackManager {
|
||||||
return getSubtitleStream(player, index);
|
return getSubtitleStream(player, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentSecondarySubtitleStream(player) {
|
||||||
|
if (!player) {
|
||||||
|
throw new Error('player cannot be null');
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = getPlayerData(player).secondarySubtitleStreamIndex;
|
||||||
|
|
||||||
|
if (index == null || index === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSubtitleStream(player, index);
|
||||||
|
}
|
||||||
|
|
||||||
function getSubtitleStream(player, index) {
|
function getSubtitleStream(player, index) {
|
||||||
return self.subtitleTracks(player).filter(function (s) {
|
return self.subtitleTracks(player).filter(function (s) {
|
||||||
return s.Type === 'Subtitle' && s.Index === index;
|
return s.Type === 'Subtitle' && s.Index === index;
|
||||||
|
@ -1522,9 +1547,51 @@ class PlaybackManager {
|
||||||
|
|
||||||
player.setSubtitleStreamIndex(selectedTrackElementIndex);
|
player.setSubtitleStreamIndex(selectedTrackElementIndex);
|
||||||
|
|
||||||
|
// Also disable secondary subtitles when disabling the primary subtitles
|
||||||
|
if (selectedTrackElementIndex === -1) {
|
||||||
|
self.setSecondarySubtitleStreamIndex(selectedTrackElementIndex);
|
||||||
|
}
|
||||||
|
|
||||||
getPlayerData(player).subtitleStreamIndex = index;
|
getPlayerData(player).subtitleStreamIndex = index;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.setSecondarySubtitleStreamIndex = function (index, player) {
|
||||||
|
player = player || self._currentPlayer;
|
||||||
|
if (!self.hasSecondarySubtitleSupport(player)) return;
|
||||||
|
if (player && !enableLocalPlaylistManagement(player)) {
|
||||||
|
try {
|
||||||
|
return player.setSecondarySubtitleStreamIndex(index);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`AutoSet - Failed to set secondary track: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentStream = getCurrentSecondarySubtitleStream(player);
|
||||||
|
|
||||||
|
const newStream = getSubtitleStream(player, index);
|
||||||
|
|
||||||
|
if (!currentStream && !newStream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearingStream = currentStream && !newStream;
|
||||||
|
const changingStream = currentStream && newStream;
|
||||||
|
const addingStream = !currentStream && newStream;
|
||||||
|
// Secondary subtitles are currently only handled client side
|
||||||
|
// Changes to the server code are required before we can handle other delivery methods
|
||||||
|
if (!clearingStream && (changingStream || addingStream) && getDeliveryMethod(newStream) !== 'External') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerData(player).secondarySubtitleStreamIndex = index;
|
||||||
|
|
||||||
|
try {
|
||||||
|
player.setSecondarySubtitleStreamIndex(index);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`AutoSet - Failed to set secondary track: ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
self.supportSubtitleOffset = function (player) {
|
self.supportSubtitleOffset = function (player) {
|
||||||
player = player || self._currentPlayer;
|
player = player || self._currentPlayer;
|
||||||
return player && 'setSubtitleOffset' in player;
|
return player && 'setSubtitleOffset' in player;
|
||||||
|
|
|
@ -988,9 +988,57 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showSecondarySubtitlesMenu(actionsheet, positionTo) {
|
||||||
|
const player = currentPlayer;
|
||||||
|
if (!playbackManager.hasSecondarySubtitleSupport(player)) return;
|
||||||
|
let currentIndex = playbackManager.getSecondarySubtitleStreamIndex(player);
|
||||||
|
const streams = playbackManager.secondarySubtitleTracks(player);
|
||||||
|
|
||||||
|
if (currentIndex == null) {
|
||||||
|
currentIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.unshift({
|
||||||
|
Index: -1,
|
||||||
|
DisplayTitle: globalize.translate('Off')
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuItems = streams.map(function (stream) {
|
||||||
|
const opt = {
|
||||||
|
name: stream.DisplayTitle,
|
||||||
|
id: stream.Index
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stream.Index === currentIndex) {
|
||||||
|
opt.selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return opt;
|
||||||
|
});
|
||||||
|
|
||||||
|
actionsheet.show({
|
||||||
|
title: globalize.translate('SecondarySubtitles'),
|
||||||
|
items: menuItems,
|
||||||
|
positionTo
|
||||||
|
}).then(function (id) {
|
||||||
|
if (id) {
|
||||||
|
const index = parseInt(id);
|
||||||
|
if (index !== currentIndex) {
|
||||||
|
playbackManager.setSecondarySubtitleStreamIndex(index, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
resetIdle();
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(resetIdle, 0);
|
||||||
|
}
|
||||||
|
|
||||||
function showSubtitleTrackSelection() {
|
function showSubtitleTrackSelection() {
|
||||||
const player = currentPlayer;
|
const player = currentPlayer;
|
||||||
const streams = playbackManager.subtitleTracks(player);
|
const streams = playbackManager.subtitleTracks(player);
|
||||||
|
const secondaryStreams = playbackManager.secondarySubtitleTracks(player);
|
||||||
let currentIndex = playbackManager.getSubtitleStreamIndex(player);
|
let currentIndex = playbackManager.getSubtitleStreamIndex(player);
|
||||||
|
|
||||||
if (currentIndex == null) {
|
if (currentIndex == null) {
|
||||||
|
@ -1013,18 +1061,37 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
|
||||||
|
|
||||||
return opt;
|
return opt;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Only show option if: player has support, has more than 1 subtitle track, has valid secondary tracks, primary subtitle is not off
|
||||||
|
if (playbackManager.hasSecondarySubtitleSupport(player) && streams.length > 1 && secondaryStreams.length > 0 && currentIndex !== -1) {
|
||||||
|
const secondarySubtitleMenuItem = {
|
||||||
|
name: globalize.translate('SecondarySubtitles'),
|
||||||
|
id: 'secondarysubtitle'
|
||||||
|
};
|
||||||
|
menuItems.unshift(secondarySubtitleMenuItem);
|
||||||
|
}
|
||||||
|
|
||||||
const positionTo = this;
|
const positionTo = this;
|
||||||
|
|
||||||
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
|
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
|
||||||
actionsheet.show({
|
actionsheet.show({
|
||||||
title: globalize.translate('Subtitles'),
|
title: globalize.translate('Subtitles'),
|
||||||
items: menuItems,
|
items: menuItems,
|
||||||
|
resolveOnClick: true,
|
||||||
positionTo: positionTo
|
positionTo: positionTo
|
||||||
}).then(function (id) {
|
}).then(function (id) {
|
||||||
const index = parseInt(id);
|
if (id === 'secondarysubtitle') {
|
||||||
|
try {
|
||||||
|
showSecondarySubtitlesMenu(actionsheet, positionTo);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const index = parseInt(id);
|
||||||
|
|
||||||
if (index !== currentIndex) {
|
if (index !== currentIndex) {
|
||||||
playbackManager.setSubtitleStreamIndex(index, player);
|
playbackManager.setSubtitleStreamIndex(index, player);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSubtitleSync();
|
toggleSubtitleSync();
|
||||||
|
|
|
@ -178,7 +178,6 @@ function tryRemoveElement(elem) {
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
isFetching = false;
|
isFetching = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {HTMLDivElement | null | undefined}
|
* @type {HTMLDivElement | null | undefined}
|
||||||
*/
|
*/
|
||||||
|
@ -207,6 +206,10 @@ function tryRemoveElement(elem) {
|
||||||
* @type {number | undefined}
|
* @type {number | undefined}
|
||||||
*/
|
*/
|
||||||
#customTrackIndex;
|
#customTrackIndex;
|
||||||
|
/**
|
||||||
|
* @type {number | undefined}
|
||||||
|
*/
|
||||||
|
#customSecondaryTrackIndex;
|
||||||
/**
|
/**
|
||||||
* @type {boolean | undefined}
|
* @type {boolean | undefined}
|
||||||
*/
|
*/
|
||||||
|
@ -270,6 +273,14 @@ function tryRemoveElement(elem) {
|
||||||
* @type {any | undefined}
|
* @type {any | undefined}
|
||||||
*/
|
*/
|
||||||
_currentPlayOptions;
|
_currentPlayOptions;
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
_PRIMARY_TEXT_TRACK_INDEX = 0;
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
_SECONDARY_TEXT_TRACK_INDEX = 1;
|
||||||
/**
|
/**
|
||||||
* @type {any | undefined}
|
* @type {any | undefined}
|
||||||
*/
|
*/
|
||||||
|
@ -490,6 +501,10 @@ function tryRemoveElement(elem) {
|
||||||
this.setCurrentTrackElement(index);
|
this.setCurrentTrackElement(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSecondarySubtitleStreamIndex(index) {
|
||||||
|
this.setCurrentTrackElement(index, this._SECONDARY_TEXT_TRACK_INDEX);
|
||||||
|
}
|
||||||
|
|
||||||
resetSubtitleOffset() {
|
resetSubtitleOffset() {
|
||||||
this.#currentTrackOffset = 0;
|
this.#currentTrackOffset = 0;
|
||||||
this.#showTrackOffset = false;
|
this.#showTrackOffset = false;
|
||||||
|
@ -514,7 +529,7 @@ function tryRemoveElement(elem) {
|
||||||
const videoElement = this.#mediaElement;
|
const videoElement = this.#mediaElement;
|
||||||
if (videoElement) {
|
if (videoElement) {
|
||||||
return Array.from(videoElement.textTracks)
|
return Array.from(videoElement.textTracks)
|
||||||
.find(function (trackElement) {
|
.filter(function (trackElement) {
|
||||||
// get showing .vtt textTack
|
// get showing .vtt textTack
|
||||||
return trackElement.mode === 'showing';
|
return trackElement.mode === 'showing';
|
||||||
});
|
});
|
||||||
|
@ -591,6 +606,10 @@ function tryRemoveElement(elem) {
|
||||||
return this.#currentTrackOffset;
|
return this.#currentTrackOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSecondaryTrack(textTrackIndex) {
|
||||||
|
return textTrackIndex === this._SECONDARY_TEXT_TRACK_INDEX;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
@ -956,7 +975,9 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
destroyCustomTrack(videoElement) {
|
destroyCustomTrack(videoElement, targetTrackIndex) {
|
||||||
|
const destroySingleTrack = typeof targetTrackIndex === 'number';
|
||||||
|
|
||||||
if (this.#videoSubtitlesElem) {
|
if (this.#videoSubtitlesElem) {
|
||||||
const subtitlesContainer = this.#videoSubtitlesElem.parentNode;
|
const subtitlesContainer = this.#videoSubtitlesElem.parentNode;
|
||||||
if (subtitlesContainer) {
|
if (subtitlesContainer) {
|
||||||
|
@ -969,7 +990,11 @@ function tryRemoveElement(elem) {
|
||||||
|
|
||||||
if (videoElement) {
|
if (videoElement) {
|
||||||
const allTracks = videoElement.textTracks || []; // get list of tracks
|
const allTracks = videoElement.textTracks || []; // get list of tracks
|
||||||
for (const track of allTracks) {
|
for (let index = 0; index < allTracks.length; index++) {
|
||||||
|
const track = allTracks[index];
|
||||||
|
if (destroySingleTrack && targetTrackIndex !== index) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (track.label.includes('manualTrack')) {
|
if (track.label.includes('manualTrack')) {
|
||||||
track.mode = 'disabled';
|
track.mode = 'disabled';
|
||||||
}
|
}
|
||||||
|
@ -1029,23 +1054,34 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
setTrackForDisplay(videoElement, track) {
|
setTrackForDisplay(videoElement, track, targetTextTrackIndex = this._PRIMARY_TEXT_TRACK_INDEX) {
|
||||||
if (!track) {
|
if (!track) {
|
||||||
this.destroyCustomTrack(videoElement);
|
// Destroy all tracks by passing undefined if there is no valid primary track
|
||||||
|
this.destroyCustomTrack(videoElement, this.isSecondaryTrack(targetTextTrackIndex) ? targetTextTrackIndex : undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let targetTrackIndex = this.#customTrackIndex;
|
||||||
|
if (this.isSecondaryTrack(targetTextTrackIndex)) {
|
||||||
|
targetTrackIndex = this.#customSecondaryTrackIndex;
|
||||||
|
}
|
||||||
|
|
||||||
// skip if already playing this track
|
// skip if already playing this track
|
||||||
if (this.#customTrackIndex === track.Index) {
|
if (targetTrackIndex === track.Index) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resetSubtitleOffset();
|
this.resetSubtitleOffset();
|
||||||
const item = this._currentPlayOptions.item;
|
const item = this._currentPlayOptions.item;
|
||||||
|
|
||||||
this.destroyCustomTrack(videoElement);
|
this.destroyCustomTrack(videoElement, targetTextTrackIndex);
|
||||||
this.#customTrackIndex = track.Index;
|
|
||||||
this.renderTracksEvents(videoElement, track, item);
|
if (this.isSecondaryTrack(targetTextTrackIndex)) {
|
||||||
|
this.#customSecondaryTrackIndex = track.Index;
|
||||||
|
} else {
|
||||||
|
this.#customTrackIndex = track.Index;
|
||||||
|
}
|
||||||
|
this.renderTracksEvents(videoElement, track, item, targetTextTrackIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1211,7 +1247,7 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
renderTracksEvents(videoElement, track, item) {
|
renderTracksEvents(videoElement, track, item, targetTextTrackIndex = this._PRIMARY_TEXT_TRACK_INDEX) {
|
||||||
if (!itemHelper.isLocalItem(item) || track.IsExternal) {
|
if (!itemHelper.isLocalItem(item) || track.IsExternal) {
|
||||||
const format = (track.Codec || '').toLowerCase();
|
const format = (track.Codec || '').toLowerCase();
|
||||||
if (format === 'ssa' || format === 'ass') {
|
if (format === 'ssa' || format === 'ass') {
|
||||||
|
@ -1220,15 +1256,15 @@ function tryRemoveElement(elem) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.requiresCustomSubtitlesElement()) {
|
if (this.requiresCustomSubtitlesElement()) {
|
||||||
this.renderSubtitlesWithCustomElement(videoElement, track, item);
|
this.renderSubtitlesWithCustomElement(videoElement, track, item, targetTextTrackIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let trackElement = null;
|
let trackElement = null;
|
||||||
if (videoElement.textTracks && videoElement.textTracks.length > 0) {
|
const updatingTrack = videoElement.textTracks && videoElement.textTracks.length > (this.isSecondaryTrack(targetTextTrackIndex) ? 1 : 0);
|
||||||
trackElement = videoElement.textTracks[0];
|
if (updatingTrack) {
|
||||||
|
trackElement = videoElement.textTracks[targetTextTrackIndex];
|
||||||
// This throws an error in IE, but is fine in chrome
|
// This throws an error in IE, but is fine in chrome
|
||||||
// In IE it's not necessary anyway because changing the src seems to be enough
|
// In IE it's not necessary anyway because changing the src seems to be enough
|
||||||
try {
|
try {
|
||||||
|
@ -1313,7 +1349,7 @@ function tryRemoveElement(elem) {
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
setCurrentTrackElement(streamIndex) {
|
setCurrentTrackElement(streamIndex, targetTextTrackIndex) {
|
||||||
console.debug(`setting new text track index to: ${streamIndex}`);
|
console.debug(`setting new text track index to: ${streamIndex}`);
|
||||||
|
|
||||||
const mediaStreamTextTracks = getMediaStreamTextTracks(this._currentPlayOptions.mediaSource);
|
const mediaStreamTextTracks = getMediaStreamTextTracks(this._currentPlayOptions.mediaSource);
|
||||||
|
@ -1322,7 +1358,7 @@ function tryRemoveElement(elem) {
|
||||||
return t.Index === streamIndex;
|
return t.Index === streamIndex;
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
this.setTrackForDisplay(this.#mediaElement, track);
|
this.setTrackForDisplay(this.#mediaElement, track, targetTextTrackIndex);
|
||||||
if (enableNativeTrackSupport(this.#currentSrc, track)) {
|
if (enableNativeTrackSupport(this.#currentSrc, track)) {
|
||||||
if (streamIndex !== -1) {
|
if (streamIndex !== -1) {
|
||||||
this.setCueAppearance();
|
this.setCueAppearance();
|
||||||
|
@ -1500,6 +1536,7 @@ function tryRemoveElement(elem) {
|
||||||
|
|
||||||
list.push('SetBrightness');
|
list.push('SetBrightness');
|
||||||
list.push('SetAspectRatio');
|
list.push('SetAspectRatio');
|
||||||
|
list.push('SecondarySubtitles');
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1412,6 +1412,7 @@
|
||||||
"SearchForSubtitles": "Search for Subtitles",
|
"SearchForSubtitles": "Search for Subtitles",
|
||||||
"SearchResults": "Search Results",
|
"SearchResults": "Search Results",
|
||||||
"Season": "Season",
|
"Season": "Season",
|
||||||
|
"SecondarySubtitles": "Secondary Subtitles",
|
||||||
"SelectAdminUsername": "Please select a username for the admin account.",
|
"SelectAdminUsername": "Please select a username for the admin account.",
|
||||||
"SelectServer": "Select Server",
|
"SelectServer": "Select Server",
|
||||||
"SendMessage": "Send message",
|
"SendMessage": "Send message",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue