mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2024-11-17 02:48:19 -07:00
feat: add native secondary subtitle support
This commit is contained in:
parent
d69d4b22d9
commit
145aea184f
@ -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) {
|
||||
if (!player) {
|
||||
throw new Error('player cannot be null');
|
||||
@ -890,6 +901,20 @@ class PlaybackManager {
|
||||
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) {
|
||||
return self.subtitleTracks(player).filter(function (s) {
|
||||
return s.Type === 'Subtitle' && s.Index === index;
|
||||
@ -1522,9 +1547,51 @@ class PlaybackManager {
|
||||
|
||||
player.setSubtitleStreamIndex(selectedTrackElementIndex);
|
||||
|
||||
// Also disable secondary subtitles when disabling the primary subtitles
|
||||
if (selectedTrackElementIndex === -1) {
|
||||
self.setSecondarySubtitleStreamIndex(selectedTrackElementIndex);
|
||||
}
|
||||
|
||||
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) {
|
||||
player = player || self._currentPlayer;
|
||||
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() {
|
||||
const player = currentPlayer;
|
||||
const streams = playbackManager.subtitleTracks(player);
|
||||
const secondaryStreams = playbackManager.secondarySubtitleTracks(player);
|
||||
let currentIndex = playbackManager.getSubtitleStreamIndex(player);
|
||||
|
||||
if (currentIndex == null) {
|
||||
@ -1013,18 +1061,37 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
|
||||
|
||||
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;
|
||||
|
||||
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
|
||||
actionsheet.show({
|
||||
title: globalize.translate('Subtitles'),
|
||||
items: menuItems,
|
||||
resolveOnClick: true,
|
||||
positionTo: positionTo
|
||||
}).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) {
|
||||
playbackManager.setSubtitleStreamIndex(index, player);
|
||||
if (index !== currentIndex) {
|
||||
playbackManager.setSubtitleStreamIndex(index, player);
|
||||
}
|
||||
}
|
||||
|
||||
toggleSubtitleSync();
|
||||
|
@ -178,7 +178,6 @@ function tryRemoveElement(elem) {
|
||||
* @type {boolean}
|
||||
*/
|
||||
isFetching = false;
|
||||
|
||||
/**
|
||||
* @type {HTMLDivElement | null | undefined}
|
||||
*/
|
||||
@ -207,6 +206,10 @@ function tryRemoveElement(elem) {
|
||||
* @type {number | undefined}
|
||||
*/
|
||||
#customTrackIndex;
|
||||
/**
|
||||
* @type {number | undefined}
|
||||
*/
|
||||
#customSecondaryTrackIndex;
|
||||
/**
|
||||
* @type {boolean | undefined}
|
||||
*/
|
||||
@ -270,6 +273,14 @@ function tryRemoveElement(elem) {
|
||||
* @type {any | undefined}
|
||||
*/
|
||||
_currentPlayOptions;
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
_PRIMARY_TEXT_TRACK_INDEX = 0;
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
_SECONDARY_TEXT_TRACK_INDEX = 1;
|
||||
/**
|
||||
* @type {any | undefined}
|
||||
*/
|
||||
@ -490,6 +501,10 @@ function tryRemoveElement(elem) {
|
||||
this.setCurrentTrackElement(index);
|
||||
}
|
||||
|
||||
setSecondarySubtitleStreamIndex(index) {
|
||||
this.setCurrentTrackElement(index, this._SECONDARY_TEXT_TRACK_INDEX);
|
||||
}
|
||||
|
||||
resetSubtitleOffset() {
|
||||
this.#currentTrackOffset = 0;
|
||||
this.#showTrackOffset = false;
|
||||
@ -514,7 +529,7 @@ function tryRemoveElement(elem) {
|
||||
const videoElement = this.#mediaElement;
|
||||
if (videoElement) {
|
||||
return Array.from(videoElement.textTracks)
|
||||
.find(function (trackElement) {
|
||||
.filter(function (trackElement) {
|
||||
// get showing .vtt textTack
|
||||
return trackElement.mode === 'showing';
|
||||
});
|
||||
@ -591,6 +606,10 @@ function tryRemoveElement(elem) {
|
||||
return this.#currentTrackOffset;
|
||||
}
|
||||
|
||||
isSecondaryTrack(textTrackIndex) {
|
||||
return textTrackIndex === this._SECONDARY_TEXT_TRACK_INDEX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@ -956,7 +975,9 @@ function tryRemoveElement(elem) {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
destroyCustomTrack(videoElement) {
|
||||
destroyCustomTrack(videoElement, targetTrackIndex) {
|
||||
const destroySingleTrack = typeof targetTrackIndex === 'number';
|
||||
|
||||
if (this.#videoSubtitlesElem) {
|
||||
const subtitlesContainer = this.#videoSubtitlesElem.parentNode;
|
||||
if (subtitlesContainer) {
|
||||
@ -969,7 +990,11 @@ function tryRemoveElement(elem) {
|
||||
|
||||
if (videoElement) {
|
||||
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')) {
|
||||
track.mode = 'disabled';
|
||||
}
|
||||
@ -1029,23 +1054,34 @@ function tryRemoveElement(elem) {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setTrackForDisplay(videoElement, track) {
|
||||
setTrackForDisplay(videoElement, track, targetTextTrackIndex = this._PRIMARY_TEXT_TRACK_INDEX) {
|
||||
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;
|
||||
}
|
||||
|
||||
let targetTrackIndex = this.#customTrackIndex;
|
||||
if (this.isSecondaryTrack(targetTextTrackIndex)) {
|
||||
targetTrackIndex = this.#customSecondaryTrackIndex;
|
||||
}
|
||||
|
||||
// skip if already playing this track
|
||||
if (this.#customTrackIndex === track.Index) {
|
||||
if (targetTrackIndex === track.Index) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetSubtitleOffset();
|
||||
const item = this._currentPlayOptions.item;
|
||||
|
||||
this.destroyCustomTrack(videoElement);
|
||||
this.#customTrackIndex = track.Index;
|
||||
this.renderTracksEvents(videoElement, track, item);
|
||||
this.destroyCustomTrack(videoElement, targetTextTrackIndex);
|
||||
|
||||
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
|
||||
*/
|
||||
renderTracksEvents(videoElement, track, item) {
|
||||
renderTracksEvents(videoElement, track, item, targetTextTrackIndex = this._PRIMARY_TEXT_TRACK_INDEX) {
|
||||
if (!itemHelper.isLocalItem(item) || track.IsExternal) {
|
||||
const format = (track.Codec || '').toLowerCase();
|
||||
if (format === 'ssa' || format === 'ass') {
|
||||
@ -1220,15 +1256,15 @@ function tryRemoveElement(elem) {
|
||||
}
|
||||
|
||||
if (this.requiresCustomSubtitlesElement()) {
|
||||
this.renderSubtitlesWithCustomElement(videoElement, track, item);
|
||||
this.renderSubtitlesWithCustomElement(videoElement, track, item, targetTextTrackIndex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let trackElement = null;
|
||||
if (videoElement.textTracks && videoElement.textTracks.length > 0) {
|
||||
trackElement = videoElement.textTracks[0];
|
||||
|
||||
const updatingTrack = videoElement.textTracks && videoElement.textTracks.length > (this.isSecondaryTrack(targetTextTrackIndex) ? 1 : 0);
|
||||
if (updatingTrack) {
|
||||
trackElement = videoElement.textTracks[targetTextTrackIndex];
|
||||
// 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
|
||||
try {
|
||||
@ -1313,7 +1349,7 @@ function tryRemoveElement(elem) {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setCurrentTrackElement(streamIndex) {
|
||||
setCurrentTrackElement(streamIndex, targetTextTrackIndex) {
|
||||
console.debug(`setting new text track index to: ${streamIndex}`);
|
||||
|
||||
const mediaStreamTextTracks = getMediaStreamTextTracks(this._currentPlayOptions.mediaSource);
|
||||
@ -1322,7 +1358,7 @@ function tryRemoveElement(elem) {
|
||||
return t.Index === streamIndex;
|
||||
})[0];
|
||||
|
||||
this.setTrackForDisplay(this.#mediaElement, track);
|
||||
this.setTrackForDisplay(this.#mediaElement, track, targetTextTrackIndex);
|
||||
if (enableNativeTrackSupport(this.#currentSrc, track)) {
|
||||
if (streamIndex !== -1) {
|
||||
this.setCueAppearance();
|
||||
@ -1500,6 +1536,7 @@ function tryRemoveElement(elem) {
|
||||
|
||||
list.push('SetBrightness');
|
||||
list.push('SetAspectRatio');
|
||||
list.push('SecondarySubtitles');
|
||||
|
||||
return list;
|
||||
}
|
||||
|
@ -1412,6 +1412,7 @@
|
||||
"SearchForSubtitles": "Search for Subtitles",
|
||||
"SearchResults": "Search Results",
|
||||
"Season": "Season",
|
||||
"SecondarySubtitles": "Secondary Subtitles",
|
||||
"SelectAdminUsername": "Please select a username for the admin account.",
|
||||
"SelectServer": "Select Server",
|
||||
"SendMessage": "Send message",
|
||||
|
Loading…
Reference in New Issue
Block a user