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

Merge branch 'master' into jassub

This commit is contained in:
Cas 2023-02-22 19:19:56 +01:00 committed by GitHub
commit cd52499849
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 658 additions and 257 deletions

View file

@ -422,7 +422,8 @@ function getPlaybackInfo(player,
enableDirectPlay,
enableDirectStream,
allowVideoStreamCopy,
allowAudioStreamCopy) {
allowAudioStreamCopy,
secondarySubtitleStreamIndex) {
if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio' && !player.useServerPlaybackInfoForAudio) {
return Promise.resolve({
MediaSources: [
@ -462,6 +463,9 @@ function getPlaybackInfo(player,
if (subtitleStreamIndex != null) {
query.SubtitleStreamIndex = subtitleStreamIndex;
}
if (secondarySubtitleStreamIndex != null) {
query.SecondarySubtitleStreamIndex = secondarySubtitleStreamIndex;
}
if (enableDirectPlay != null) {
query.EnableDirectPlay = enableDirectPlay;
}
@ -876,25 +880,49 @@ class PlaybackManager {
});
};
function getCurrentSubtitleStream(player) {
self.playerHasSecondarySubtitleSupport = function (player = self._currentPlayer) {
if (!player) return false;
return Boolean(player.supports('SecondarySubtitles'));
};
/**
* Checks if:
* - the track can be used directly as a secondary subtitle
* - or if it can be paired with a secondary subtitle when used as a primary subtitle
*/
self.trackHasSecondarySubtitleSupport = function (track, player = self._currentPlayer) {
if (!player || !track) return false;
const format = (track.Codec || '').toLowerCase();
// Currently, only non-SSA/non-ASS external subtitles are supported.
// Showing secondary subtitles does not work with any SSA/ASS subtitle combinations because
// of the complexity of how they are rendered and the risk of the subtitles overlapping
return format !== 'ssa' && format !== 'ass' && getDeliveryMethod(track) === 'External';
};
self.secondarySubtitleTracks = function (player = self._currentPlayer) {
const streams = self.subtitleTracks(player);
return streams.filter((stream) => self.trackHasSecondarySubtitleSupport(stream, player));
};
function getCurrentSubtitleStream(player, isSecondaryStream = false) {
if (!player) {
throw new Error('player cannot be null');
}
const index = getPlayerData(player).subtitleStreamIndex;
const index = isSecondaryStream ? getPlayerData(player).secondarySubtitleStreamIndex : getPlayerData(player).subtitleStreamIndex;
if (index == null || index === -1) {
return null;
}
return getSubtitleStream(player, index);
return self.getSubtitleStream(player, index);
}
function getSubtitleStream(player, index) {
self.getSubtitleStream = function (player, index) {
return self.subtitleTracks(player).filter(function (s) {
return s.Type === 'Subtitle' && s.Index === index;
})[0];
}
};
self.getPlaylist = function (player) {
player = player || self._currentPlayer;
@ -1463,6 +1491,24 @@ class PlaybackManager {
return getPlayerData(player).subtitleStreamIndex;
};
self.getSecondarySubtitleStreamIndex = function (player) {
player = player || self._currentPlayer;
if (!player) {
throw new Error('player cannot be null');
}
try {
if (!enableLocalPlaylistManagement(player)) {
return player.getSecondarySubtitleStreamIndex();
}
} catch (e) {
console.error('[playbackmanager] Failed to get secondary stream index:', e);
}
return getPlayerData(player).secondarySubtitleStreamIndex;
};
function getDeliveryMethod(subtitleStream) {
// This will be null for internal subs for local items
if (subtitleStream.DeliveryMethod) {
@ -1480,7 +1526,7 @@ class PlaybackManager {
const currentStream = getCurrentSubtitleStream(player);
const newStream = getSubtitleStream(player, index);
const newStream = self.getSubtitleStream(player, index);
if (!currentStream && !newStream) {
return;
@ -1522,9 +1568,48 @@ class PlaybackManager {
player.setSubtitleStreamIndex(selectedTrackElementIndex);
// Also disable secondary subtitles when disabling the primary
// subtitles, or if it doesn't support a secondary pair
if (selectedTrackElementIndex === -1 || !self.trackHasSecondarySubtitleSupport(newStream)) {
self.setSecondarySubtitleStreamIndex(-1);
}
getPlayerData(player).subtitleStreamIndex = index;
};
self.setSecondarySubtitleStreamIndex = function (index, player) {
player = player || self._currentPlayer;
if (!self.playerHasSecondarySubtitleSupport(player)) return;
if (player && !enableLocalPlaylistManagement(player)) {
try {
return player.setSecondarySubtitleStreamIndex(index);
} catch (e) {
console.error('[playbackmanager] AutoSet - Failed to set secondary track:', e);
}
}
const currentStream = getCurrentSubtitleStream(player, true);
const newStream = self.getSubtitleStream(player, index);
if (!currentStream && !newStream) {
return;
}
// Secondary subtitles are currently only handled client side
// Changes to the server code are required before we can handle other delivery methods
if (newStream && !self.trackHasSecondarySubtitleSupport(newStream, player)) {
return;
}
try {
player.setSecondarySubtitleStreamIndex(index);
getPlayerData(player).secondarySubtitleStreamIndex = index;
} catch (e) {
console.error('[playbackmanager] AutoSet - Failed to set secondary track:', e);
}
};
self.supportSubtitleOffset = function (player) {
player = player || self._currentPlayer;
return player && 'setSubtitleOffset' in player;
@ -1548,7 +1633,7 @@ class PlaybackManager {
};
self.isSubtitleStreamExternal = function (index, player) {
const stream = getSubtitleStream(player, index);
const stream = self.getSubtitleStream(player, index);
return stream ? getDeliveryMethod(stream) === 'External' : false;
};
@ -1639,6 +1724,7 @@ class PlaybackManager {
}).then(function (deviceProfile) {
const audioStreamIndex = params.AudioStreamIndex == null ? getPlayerData(player).audioStreamIndex : params.AudioStreamIndex;
const subtitleStreamIndex = params.SubtitleStreamIndex == null ? getPlayerData(player).subtitleStreamIndex : params.SubtitleStreamIndex;
const secondarySubtitleStreamIndex = params.SecondarySubtitleStreamIndex == null ? getPlayerData(player).secondarySubtitleStreamIndex : params.SecondarySubtitleStreamIndex;
let currentMediaSource = self.currentMediaSource(player);
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
@ -1665,6 +1751,7 @@ class PlaybackManager {
}
getPlayerData(player).subtitleStreamIndex = subtitleStreamIndex;
getPlayerData(player).secondarySubtitleStreamIndex = secondarySubtitleStreamIndex;
getPlayerData(player).audioStreamIndex = audioStreamIndex;
getPlayerData(player).maxStreamingBitrate = maxBitrate;
@ -1950,6 +2037,7 @@ class PlaybackManager {
state.PlayState.PlaybackRate = self.getPlaybackRate(player);
state.PlayState.SubtitleStreamIndex = self.getSubtitleStreamIndex(player);
state.PlayState.SecondarySubtitleStreamIndex = self.getSecondarySubtitleStreamIndex(player);
state.PlayState.AudioStreamIndex = self.getAudioStreamIndex(player);
state.PlayState.BufferedRanges = self.getBufferedRanges(player);
@ -2230,11 +2318,16 @@ class PlaybackManager {
});
}
function rankStreamType(prevIndex, prevSource, mediaSource, streamType) {
function rankStreamType(prevIndex, prevSource, mediaSource, streamType, isSecondarySubtitle) {
if (prevIndex == -1) {
console.debug(`AutoSet ${streamType} - No Stream Set`);
if (streamType == 'Subtitle')
mediaSource.DefaultSubtitleStreamIndex = -1;
if (streamType == 'Subtitle') {
if (isSecondarySubtitle) {
mediaSource.DefaultSecondarySubtitleStreamIndex = -1;
} else {
mediaSource.DefaultSubtitleStreamIndex = -1;
}
}
return;
}
@ -2292,8 +2385,13 @@ class PlaybackManager {
if (bestStreamIndex != null) {
console.debug(`AutoSet ${streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.`);
if (streamType == 'Subtitle')
mediaSource.DefaultSubtitleStreamIndex = bestStreamIndex;
if (streamType == 'Subtitle') {
if (isSecondarySubtitle) {
mediaSource.DefaultSecondarySubtitleStreamIndex = bestStreamIndex;
} else {
mediaSource.DefaultSubtitleStreamIndex = bestStreamIndex;
}
}
if (streamType == 'Audio')
mediaSource.DefaultAudioStreamIndex = bestStreamIndex;
} else {
@ -2317,6 +2415,10 @@ class PlaybackManager {
if (subtitle && typeof prevSource.DefaultSubtitleStreamIndex == 'number') {
rankStreamType(prevSource.DefaultSubtitleStreamIndex, prevSource, mediaSource, 'Subtitle');
}
if (subtitle && typeof prevSource.DefaultSecondarySubtitleStreamIndex == 'number') {
rankStreamType(prevSource.DefaultSecondarySubtitleStreamIndex, prevSource, mediaSource, 'Subtitle', true);
}
} catch (e) {
console.error(`AutoSet - Caught unexpected error: ${e}`);
}
@ -2384,6 +2486,19 @@ class PlaybackManager {
const user = await apiClient.getCurrentUser();
autoSetNextTracks(prevSource, mediaSource, user.Configuration.RememberAudioSelections, user.Configuration.RememberSubtitleSelections);
if (mediaSource.DefaultSubtitleStreamIndex == null || mediaSource.DefaultSubtitleStreamIndex < 0) {
mediaSource.DefaultSubtitleStreamIndex = mediaSource.DefaultSecondarySubtitleStreamIndex;
mediaSource.DefaultSecondarySubtitleStreamIndex = -1;
}
const subtitleTrack1 = mediaSource.MediaStreams[mediaSource.DefaultSubtitleStreamIndex];
const subtitleTrack2 = mediaSource.MediaStreams[mediaSource.DefaultSecondarySubtitleStreamIndex];
if (!self.trackHasSecondarySubtitleSupport(subtitleTrack1, player)
|| !self.trackHasSecondarySubtitleSupport(subtitleTrack2, player)) {
mediaSource.DefaultSecondarySubtitleStreamIndex = -1;
}
const streamInfo = createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition, player);
streamInfo.fullscreen = playOptions.fullscreen;
@ -2751,7 +2866,8 @@ class PlaybackManager {
return {
...prevSource,
DefaultAudioStreamIndex: prevPlayerData.audioStreamIndex,
DefaultSubtitleStreamIndex: prevPlayerData.subtitleStreamIndex
DefaultSubtitleStreamIndex: prevPlayerData.subtitleStreamIndex,
DefaultSecondarySubtitleStreamIndex: prevPlayerData.secondarySubtitleStreamIndex
};
}
@ -2910,9 +3026,11 @@ class PlaybackManager {
if (mediaSource) {
playerData.audioStreamIndex = mediaSource.DefaultAudioStreamIndex;
playerData.subtitleStreamIndex = mediaSource.DefaultSubtitleStreamIndex;
playerData.secondarySubtitleStreamIndex = mediaSource.DefaultSecondarySubtitleStreamIndex;
} else {
playerData.audioStreamIndex = null;
playerData.subtitleStreamIndex = null;
playerData.secondarySubtitleStreamIndex = null;
}
self._playNextAfterEnded = true;

View file

@ -988,9 +988,57 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
});
}
function showSecondarySubtitlesMenu(actionsheet, positionTo) {
const player = currentPlayer;
if (!playbackManager.playerHasSecondarySubtitleSupport(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,6 +1061,29 @@ 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
* - primary subtitle has support
*/
const currentTrackCanAddSecondarySubtitle = playbackManager.playerHasSecondarySubtitleSupport(player)
&& streams.length > 1
&& secondaryStreams.length > 0
&& currentIndex !== -1
&& playbackManager.trackHasSecondarySubtitleSupport(playbackManager.getSubtitleStream(player, currentIndex), player);
if (currentTrackCanAddSecondarySubtitle) {
const secondarySubtitleMenuItem = {
name: globalize.translate('SecondarySubtitles'),
id: 'secondarysubtitle'
};
menuItems.unshift(secondarySubtitleMenuItem);
}
const positionTo = this;
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
@ -1021,10 +1092,18 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
items: menuItems,
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();

View file

@ -155,6 +155,9 @@ function tryRemoveElement(elem) {
return profileBuilder({});
}
const PRIMARY_TEXT_TRACK_INDEX = 0;
const SECONDARY_TEXT_TRACK_INDEX = 1;
export class HtmlVideoPlayer {
/**
* @type {string}
@ -178,7 +181,6 @@ function tryRemoveElement(elem) {
* @type {boolean}
*/
isFetching = false;
/**
* @type {HTMLDivElement | null | undefined}
*/
@ -187,6 +189,10 @@ function tryRemoveElement(elem) {
* @type {number | undefined}
*/
#subtitleTrackIndexToSetOnPlaying;
/**
* @type {number | undefined}
*/
#secondarySubtitleTrackIndexToSetOnPlaying;
/**
* @type {number | null}
*/
@ -207,6 +213,10 @@ function tryRemoveElement(elem) {
* @type {number | undefined}
*/
#customTrackIndex;
/**
* @type {number | undefined}
*/
#customSecondaryTrackIndex;
/**
* @type {boolean | undefined}
*/
@ -215,14 +225,26 @@ function tryRemoveElement(elem) {
* @type {number | undefined}
*/
#currentTrackOffset;
/**
* @type {HTMLElement | null | undefined}
*/
#secondaryTrackOffset;
/**
* @type {HTMLElement | null | undefined}
*/
#videoSubtitlesElem;
/**
* @type {HTMLElement | null | undefined}
*/
#videoSecondarySubtitlesElem;
/**
* @type {any | null | undefined}
*/
#currentTrackEvents;
/**
* @type {any | null | undefined}
*/
#currentSecondaryTrackEvents;
/**
* @type {string[] | undefined}
*/
@ -448,18 +470,39 @@ function tryRemoveElement(elem) {
destroyFlvPlayer(this);
destroyCastPlayer(this);
let secondaryTrackValid = true;
this.#subtitleTrackIndexToSetOnPlaying = options.mediaSource.DefaultSubtitleStreamIndex == null ? -1 : options.mediaSource.DefaultSubtitleStreamIndex;
if (this.#subtitleTrackIndexToSetOnPlaying != null && this.#subtitleTrackIndexToSetOnPlaying >= 0) {
const initialSubtitleStream = options.mediaSource.MediaStreams[this.#subtitleTrackIndexToSetOnPlaying];
if (!initialSubtitleStream || initialSubtitleStream.DeliveryMethod === 'Encode') {
this.#subtitleTrackIndexToSetOnPlaying = -1;
secondaryTrackValid = false;
}
// secondary track should not be shown if primary track is no longer a valid pair
if (initialSubtitleStream && !playbackManager.trackHasSecondarySubtitleSupport(initialSubtitleStream, this)) {
secondaryTrackValid = false;
}
} else {
secondaryTrackValid = false;
}
this.#audioTrackIndexToSetOnPlaying = options.playMethod === 'Transcode' ? null : options.mediaSource.DefaultAudioStreamIndex;
this._currentPlayOptions = options;
if (secondaryTrackValid) {
this.#secondarySubtitleTrackIndexToSetOnPlaying = options.mediaSource.DefaultSecondarySubtitleStreamIndex == null ? -1 : options.mediaSource.DefaultSecondarySubtitleStreamIndex;
if (this.#secondarySubtitleTrackIndexToSetOnPlaying != null && this.#secondarySubtitleTrackIndexToSetOnPlaying >= 0) {
const initialSecondarySubtitleStream = options.mediaSource.MediaStreams[this.#secondarySubtitleTrackIndexToSetOnPlaying];
if (!initialSecondarySubtitleStream || !playbackManager.trackHasSecondarySubtitleSupport(initialSecondarySubtitleStream, this)) {
this.#secondarySubtitleTrackIndexToSetOnPlaying = -1;
}
}
} else {
this.#secondarySubtitleTrackIndexToSetOnPlaying = -1;
}
const crossOrigin = getCrossOriginValue(options.mediaSource);
if (crossOrigin) {
elem.crossOrigin = crossOrigin;
@ -490,8 +533,13 @@ function tryRemoveElement(elem) {
this.setCurrentTrackElement(index);
}
setSecondarySubtitleStreamIndex(index) {
this.setCurrentTrackElement(index, SECONDARY_TEXT_TRACK_INDEX);
}
resetSubtitleOffset() {
this.#currentTrackOffset = 0;
this.#secondaryTrackOffset = 0;
this.#showTrackOffset = false;
}
@ -510,11 +558,11 @@ function tryRemoveElement(elem) {
/**
* @private
*/
getTextTrack() {
getTextTracks() {
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';
});
@ -523,9 +571,6 @@ function tryRemoveElement(elem) {
}
}
/**
* @private
*/
setSubtitleOffset(offset) {
const offsetValue = parseFloat(offset);
@ -534,12 +579,15 @@ function tryRemoveElement(elem) {
this.updateCurrentTrackOffset(offsetValue);
this.#currentJASSUB.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + offsetValue;
} else {
const trackElement = this.getTextTrack();
const trackElements = this.getTextTracks();
// if .vtt currently rendering
if (trackElement) {
this.setTextTrackSubtitleOffset(trackElement, offsetValue);
} else if (this.#currentTrackEvents) {
this.setTrackEventsSubtitleOffset(this.#currentTrackEvents, offsetValue);
if (trackElements?.length > 0) {
trackElements.forEach((trackElement, index) => {
this.setTextTrackSubtitleOffset(trackElement, offsetValue, index);
});
} else if (this.#currentTrackEvents || this.#currentSecondaryTrackEvents) {
this.#currentTrackEvents && this.setTrackEventsSubtitleOffset(this.#currentTrackEvents, offsetValue, PRIMARY_TEXT_TRACK_INDEX);
this.#currentSecondaryTrackEvents && this.setTrackEventsSubtitleOffset(this.#currentSecondaryTrackEvents, offsetValue, SECONDARY_TEXT_TRACK_INDEX);
} else {
console.debug('No available track, cannot apply offset: ', offsetValue);
}
@ -549,13 +597,25 @@ function tryRemoveElement(elem) {
/**
* @private
*/
updateCurrentTrackOffset(offsetValue) {
updateCurrentTrackOffset(offsetValue, currentTrackIndex = PRIMARY_TEXT_TRACK_INDEX) {
let offsetToCompare = this.#currentTrackOffset;
if (this.isSecondaryTrack(currentTrackIndex)) {
offsetToCompare = this.#secondaryTrackOffset;
}
let relativeOffset = offsetValue;
const newTrackOffset = offsetValue;
if (this.#currentTrackOffset) {
relativeOffset -= this.#currentTrackOffset;
if (offsetToCompare) {
relativeOffset -= offsetToCompare;
}
this.#currentTrackOffset = newTrackOffset;
if (this.isSecondaryTrack(currentTrackIndex)) {
this.#secondaryTrackOffset = newTrackOffset;
} else {
this.#currentTrackOffset = newTrackOffset;
}
// relative to currentTrackOffset
return relativeOffset;
}
@ -563,9 +623,12 @@ function tryRemoveElement(elem) {
/**
* @private
*/
setTextTrackSubtitleOffset(currentTrack, offsetValue) {
setTextTrackSubtitleOffset(currentTrack, offsetValue, currentTrackIndex) {
if (currentTrack.cues) {
offsetValue = this.updateCurrentTrackOffset(offsetValue);
offsetValue = this.updateCurrentTrackOffset(offsetValue, currentTrackIndex);
if (offsetValue === 0) {
return;
}
Array.from(currentTrack.cues)
.forEach(function (cue) {
cue.startTime -= offsetValue;
@ -577,9 +640,12 @@ function tryRemoveElement(elem) {
/**
* @private
*/
setTrackEventsSubtitleOffset(trackEvents, offsetValue) {
setTrackEventsSubtitleOffset(trackEvents, offsetValue, currentTrackIndex) {
if (Array.isArray(trackEvents)) {
offsetValue = this.updateCurrentTrackOffset(offsetValue) * 1e7; // ticks
offsetValue = this.updateCurrentTrackOffset(offsetValue, currentTrackIndex) * 1e7; // ticks
if (offsetValue === 0) {
return;
}
trackEvents.forEach(function (trackEvent) {
trackEvent.StartPositionTicks -= offsetValue;
trackEvent.EndPositionTicks -= offsetValue;
@ -591,6 +657,14 @@ function tryRemoveElement(elem) {
return this.#currentTrackOffset;
}
isPrimaryTrack(textTrackIndex) {
return textTrackIndex === PRIMARY_TEXT_TRACK_INDEX;
}
isSecondaryTrack(textTrackIndex) {
return textTrackIndex === SECONDARY_TEXT_TRACK_INDEX;
}
/**
* @private
*/
@ -819,6 +893,16 @@ function tryRemoveElement(elem) {
if (this.#audioTrackIndexToSetOnPlaying != null && this.canSetAudioStreamIndex()) {
this.setAudioStreamIndex(this.#audioTrackIndexToSetOnPlaying);
}
if (this.#secondarySubtitleTrackIndexToSetOnPlaying != null && this.#secondarySubtitleTrackIndexToSetOnPlaying >= 0) {
/**
* Using a 0ms timeout to set the secondary subtitles because of some weird race condition when
* setting both primary and secondary tracks at the same time.
* The `TextTrack` content and cues will somehow get mixed up and each track will play a mix of both languages.
* Putting this in a timeout fixes it completely.
*/
setTimeout(() => this.setSecondarySubtitleStreamIndex(this.#secondarySubtitleTrackIndexToSetOnPlaying), 0);
}
}
/**
@ -954,27 +1038,75 @@ function tryRemoveElement(elem) {
/**
* @private
*/
destroyCustomTrack(videoElement) {
if (this.#videoSubtitlesElem) {
const subtitlesContainer = this.#videoSubtitlesElem.parentNode;
if (subtitlesContainer) {
tryRemoveElement(subtitlesContainer);
destroyCustomRenderedTrackElements(targetTrackIndex) {
if (this.isPrimaryTrack(targetTrackIndex)) {
if (this.#videoSubtitlesElem) {
tryRemoveElement(this.#videoSubtitlesElem);
this.#videoSubtitlesElem = null;
}
} else if (this.isSecondaryTrack(targetTrackIndex)) {
if (this.#videoSecondarySubtitlesElem) {
tryRemoveElement(this.#videoSecondarySubtitlesElem);
this.#videoSecondarySubtitlesElem = null;
}
} else { // destroy all
if (this.#videoSubtitlesElem) {
const subtitlesContainer = this.#videoSubtitlesElem.parentNode;
if (subtitlesContainer) {
tryRemoveElement(subtitlesContainer);
}
this.#videoSubtitlesElem = null;
this.#videoSecondarySubtitlesElem = null;
}
this.#videoSubtitlesElem = null;
}
}
this.#currentTrackEvents = null;
/**
* @private
*/
destroyNativeTracks(videoElement, targetTrackIndex) {
if (videoElement) {
const destroySingleTrack = typeof targetTrackIndex === 'number';
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];
// Skip all other tracks if we are targeting just one
if (destroySingleTrack && targetTrackIndex !== index) {
continue;
}
if (track.label.includes('manualTrack')) {
track.mode = 'disabled';
}
}
}
}
/**
* @private
*/
destroyStoredTrackInfo(targetTrackIndex) {
if (this.isPrimaryTrack(targetTrackIndex)) {
this.#customTrackIndex = -1;
this.#currentTrackEvents = null;
} else if (this.isSecondaryTrack(targetTrackIndex)) {
this.#customSecondaryTrackIndex = -1;
this.#currentSecondaryTrackEvents = null;
} else { // destroy all
this.#customTrackIndex = -1;
this.#customSecondaryTrackIndex = -1;
this.#currentTrackEvents = null;
this.#currentSecondaryTrackEvents = null;
}
}
/**
* @private
*/
destroyCustomTrack(videoElement, targetTrackIndex) {
this.destroyCustomRenderedTrackElements(targetTrackIndex);
this.destroyNativeTracks(videoElement, targetTrackIndex);
this.destroyStoredTrackInfo(targetTrackIndex);
this.#customTrackIndex = -1;
this.#currentClock = null;
this._currentAspectRatio = null;
@ -1027,23 +1159,34 @@ function tryRemoveElement(elem) {
/**
* @private
*/
setTrackForDisplay(videoElement, track) {
setTrackForDisplay(videoElement, track, targetTextTrackIndex = 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);
}
/**
@ -1152,16 +1295,39 @@ function tryRemoveElement(elem) {
/**
* @private
*/
renderSubtitlesWithCustomElement(videoElement, track, item) {
this.fetchSubtitles(track, item).then((data) => {
if (!this.#videoSubtitlesElem) {
const subtitlesContainer = document.createElement('div');
subtitlesContainer.classList.add('videoSubtitles');
subtitlesContainer.innerHTML = '<div class="videoSubtitlesInner"></div>';
this.#videoSubtitlesElem = subtitlesContainer.querySelector('.videoSubtitlesInner');
renderSubtitlesWithCustomElement(videoElement, track, item, targetTextTrackIndex) {
Promise.all([import('../../scripts/settings/userSettings'), this.fetchSubtitles(track, item)]).then((results) => {
const [userSettings, subtitleData] = results;
const subtitleAppearance = userSettings.getSubtitleAppearanceSettings();
const subtitleVerticalPosition = parseInt(subtitleAppearance.verticalPosition, 10);
if (!this.#videoSubtitlesElem && !this.isSecondaryTrack(targetTextTrackIndex)) {
let subtitlesContainer = document.querySelector('.videoSubtitles');
if (!subtitlesContainer) {
subtitlesContainer = document.createElement('div');
subtitlesContainer.classList.add('videoSubtitles');
}
const subtitlesElement = document.createElement('div');
subtitlesElement.classList.add('videoSubtitlesInner');
subtitlesContainer.appendChild(subtitlesElement);
this.#videoSubtitlesElem = subtitlesElement;
this.setSubtitleAppearance(subtitlesContainer, this.#videoSubtitlesElem);
videoElement.parentNode.appendChild(subtitlesContainer);
this.#currentTrackEvents = data.TrackEvents;
this.#currentTrackEvents = subtitleData.TrackEvents;
} else if (!this.#videoSecondarySubtitlesElem && this.isSecondaryTrack(targetTextTrackIndex)) {
const subtitlesContainer = document.querySelector('.videoSubtitles');
if (!subtitlesContainer) return;
const secondarySubtitlesElement = document.createElement('div');
secondarySubtitlesElement.classList.add('videoSecondarySubtitlesInner');
// determine the order of the subtitles
if (subtitleVerticalPosition < 0) {
subtitlesContainer.insertBefore(secondarySubtitlesElement, subtitlesContainer.firstChild);
} else {
subtitlesContainer.appendChild(secondarySubtitlesElement);
}
this.#videoSecondarySubtitlesElem = secondarySubtitlesElement;
this.setSubtitleAppearance(subtitlesContainer, this.#videoSecondarySubtitlesElem);
this.#currentSecondaryTrackEvents = subtitleData.TrackEvents;
}
});
}
@ -1208,7 +1374,7 @@ function tryRemoveElement(elem) {
/**
* @private
*/
renderTracksEvents(videoElement, track, item) {
renderTracksEvents(videoElement, track, item, targetTextTrackIndex = PRIMARY_TEXT_TRACK_INDEX) {
if (!itemHelper.isLocalItem(item) || track.IsExternal) {
const format = (track.Codec || '').toLowerCase();
if (format === 'ssa' || format === 'ass') {
@ -1217,15 +1383,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 {
@ -1285,24 +1451,29 @@ function tryRemoveElement(elem) {
return;
}
const trackEvents = this.#currentTrackEvents;
const subtitleTextElement = this.#videoSubtitlesElem;
const allTrackEvents = [this.#currentTrackEvents, this.#currentSecondaryTrackEvents];
const subtitleTextElements = [this.#videoSubtitlesElem, this.#videoSecondarySubtitlesElem];
if (trackEvents && subtitleTextElement) {
const ticks = timeMs * 10000;
let selectedTrackEvent;
for (const trackEvent of trackEvents) {
if (trackEvent.StartPositionTicks <= ticks && trackEvent.EndPositionTicks >= ticks) {
selectedTrackEvent = trackEvent;
break;
for (let i = 0; i < allTrackEvents.length; i++) {
const trackEvents = allTrackEvents[i];
const subtitleTextElement = subtitleTextElements[i];
if (trackEvents && subtitleTextElement) {
const ticks = timeMs * 10000;
let selectedTrackEvent;
for (const trackEvent of trackEvents) {
if (trackEvent.StartPositionTicks <= ticks && trackEvent.EndPositionTicks >= ticks) {
selectedTrackEvent = trackEvent;
break;
}
}
}
if (selectedTrackEvent && selectedTrackEvent.Text) {
subtitleTextElement.innerHTML = normalizeTrackEventText(selectedTrackEvent.Text, true);
subtitleTextElement.classList.remove('hide');
} else {
subtitleTextElement.classList.add('hide');
if (selectedTrackEvent && selectedTrackEvent.Text) {
subtitleTextElement.innerHTML = normalizeTrackEventText(selectedTrackEvent.Text, true);
subtitleTextElement.classList.remove('hide');
} else {
subtitleTextElement.classList.add('hide');
}
}
}
}
@ -1310,7 +1481,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);
@ -1319,7 +1490,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();
@ -1497,6 +1668,7 @@ function tryRemoveElement(elem) {
list.push('SetBrightness');
list.push('SetAspectRatio');
list.push('SecondarySubtitles');
return list;
}

View file

@ -65,13 +65,22 @@ video[controls]::-webkit-media-controls {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
display: flex;
flex-direction: column;
align-items: center;
}
.videoSubtitlesInner {
max-width: 70%;
background-color: rgba(0, 0, 0, 0.8);
margin: auto;
display: inline-block;
}
.videoSecondarySubtitlesInner {
max-width: 70%;
background-color: rgba(0, 0, 0, 0.8);
min-height: 0 !important;
margin-top: 0.5em !important;
margin-bottom: 0.5em !important;
}
@keyframes htmlvideoplayer-zoomin {

View file

@ -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",

View file

@ -1235,7 +1235,7 @@
"TitleHostingSettings": "Configuraciones de alojamiento",
"TitleHardwareAcceleration": "Aceleración por hardware",
"Thursday": "Jueves",
"Thumb": "Pulgar",
"Thumb": "Miniatura",
"TheseSettingsAffectSubtitlesOnThisDevice": "Esta configuración afecta los subtítulos en este dispositivo",
"ThemeVideos": "Videos temáticos",
"ThemeSongs": "Canciones temáticas",
@ -1353,7 +1353,7 @@
"NextTrack": "Pasar al siguiente",
"LabelUnstable": "Inestable",
"Video": "Video",
"ThumbCard": "Tarjeta de pulgar",
"ThumbCard": "Tarjeta de miniatura",
"Subtitle": "Subtítulo",
"SpecialFeatures": "Características especiales",
"SelectServer": "Seleccionar servidor",
@ -1517,7 +1517,7 @@
"MessageSent": "Mensaje enviado.",
"LabelSlowResponseTime": "Tiempo en ms después de lo cual una respuesta es considerada lenta:",
"LabelSlowResponseEnabled": "Log de alarma si la respuesta del servidor fue lenta",
"UseEpisodeImagesInNextUpHelp": "Las secciones Siguiente y Continuar viendo utilizaran imagenes del episodio como miniaturas en lugar de miniaturas del show.",
"UseEpisodeImagesInNextUpHelp": "Las secciones 'Siguiente' y 'Continuar viendo' utilizarán imágenes del episodio como miniaturas en lugar de miniaturas del show.",
"UseEpisodeImagesInNextUp": "Usar imágenes de los episodios en \"Siguiente\" y \"Continuar Viendo\"",
"LabelAutomaticallyAddToCollection": "Agregar automáticamente a la colección",
"HeaderSyncPlayTimeSyncSettings": "Sincronización de tiempo",

View file

@ -1525,7 +1525,7 @@
"VideoFramerateNotSupported": "Videoens bildefrekvens støttes ikke",
"VideoBitDepthNotSupported": "Videoens bitdybde støttes ikke",
"RefFramesNotSupported": "Referanse-bilder støttes ikke",
"EnableGamepadHelp": "Lytt til inndata fra tilkoblet kontroller.",
"EnableGamepadHelp": "Lytt til inndata fra tilkoblet kontroller. (Krever: \"TV\"-visningsmodus)",
"LabelEnableGamepad": "Aktiver spillkontroller",
"AudioBitDepthNotSupported": "Lydens bitdybde støttes ikke",
"ThemeSong": "Tema-låt",
@ -1685,5 +1685,12 @@
"MessageNoItemsAvailable": "Ingen filer er tilgjengelige for øyeblikket.",
"OptionDateShowAdded": "Dato serien ble lagt til",
"Experimental": "Eksperimentell",
"DownloadAll": "Laste ned alt"
"DownloadAll": "Laste ned alt",
"LabelDummyChapterCountHelp": "Maksimalt antall kapittelbilder som vil bli ekstrahert for hver mediefil.",
"LabelStereoDownmixAlgorithm": "Stereo nedmiksingsalgoritme",
"HeaderDummyChapter": "Kapittel Bilder",
"LabelDummyChapterCount": "Grense:",
"LabelChapterImageResolution": "Oppløsning:",
"LabelDummyChapterDuration": "Intervall:",
"HeaderRecordingMetadataSaving": "Opptak metadata"
}

View file

@ -1693,5 +1693,5 @@
"LabelChapterImageResolution": "Разрешение изображения:",
"HeaderDummyChapter": "Изображения глав",
"LabelDummyChapterCount": "Лимит:",
"HeaderRecordingMetadataSaving": "Метадата записей"
"HeaderRecordingMetadataSaving": "Метаданные записей"
}

View file

@ -8,10 +8,10 @@
"DeathDateValue": "Помер: {0}",
"Favorite": "Обране",
"HeaderDeleteDevice": "Видаліть пристрій",
"HeaderLatestEpisodes": "Нещодавно переглянуті епізоди",
"HeaderLatestMedia": "Нещодавно переглянуті",
"HeaderLatestMovies": "Нещодавні фільми",
"HeaderLatestMusic": "Остання музика",
"HeaderLatestEpisodes": "Нещодавно додані серії",
"HeaderLatestMedia": "Нещодавно додані медіа",
"HeaderLatestMovies": "Нещодавно додані фільми",
"HeaderLatestMusic": "Нещодавно додана музика",
"HeaderSeasons": "Сезони",
"HeaderTracks": "Доріжки",
"HeaderUsers": "Користувачі",
@ -274,7 +274,7 @@
"DrmChannelsNotImported": "Канали з DRM не імпортуватимуться.",
"DisplayModeHelp": "Виберіть бажаний стиль макету інтерфейсу.",
"DisplayMissingEpisodesWithinSeasonsHelp": "Також, це має бути включено для ТВ-медіатек у конфігурації сервера.",
"DisplayInOtherHomeScreenSections": "Показувати на головному екрані такі розділи як \"Останні медіа\" і \"Продовження перегляду\"",
"DisplayInOtherHomeScreenSections": "Відображення в розділах головного екрана, таких як «Нещодавно додані медіа» та «Продовжити перегляд»",
"DeleteDevicesConfirmation": "Ви впевнені, що хочете видалити всі пристрої? Усі інші сеанси будуть завершені. Пристрої знову з’являться, після того як користувач увійде в систему.",
"DeleteAll": "Видалити все",
"Data": "Дані",
@ -583,7 +583,7 @@
"Identify": "Ідентифікувати",
"Horizontal": "Горизонтально",
"Home": "Головна",
"HideWatchedContentFromLatestMedia": "Приховати переглянуте з останніх медіа",
"HideWatchedContentFromLatestMedia": "Приховати переглянутий вміст із «Нещодавно доданих медіа»",
"Hide": "Приховати",
"Help": "Допомога",
"HeaderYears": "Роки",
@ -723,7 +723,7 @@
"HeaderLibraryAccess": "Доступ до медіатеки",
"HeaderLibraryFolders": "Папки медіатеки",
"HeaderLibraryOrder": "Порядок медіатек",
"HeaderLatestRecordings": "Останні записи",
"HeaderLatestRecordings": "Нещодавно додані записи",
"HeaderKodiMetadataHelp": "Щоб увімкнути або вимкнути метадані NFO, відкрийте меню редагування медіатеки та знайдіть розділ \"Збереження метаданих\".",
"HeaderKeepSeries": "Зберегти серіал",
"HeaderKeepRecording": "Продовжуйте записувати",
@ -909,7 +909,7 @@
"LibraryAccessHelp": "Виберіть медіатеки, якими хочете поділитися з цим користувачем. Адміністратори зможуть редагувати всі папки за допомогою менеджера метаданих.",
"LeaveBlankToNotSetAPassword": "Ви можете залишити це поле порожнім, щоб не встановлювати пароль.",
"LearnHowYouCanContribute": "Дізнайтеся, як ви можете зробити свій внесок.",
"LatestFromLibrary": "Нове в {0}",
"LatestFromLibrary": "Нещодавно додано в {0}",
"LastSeen": "Востаннє був {0}",
"Larger": "Більший",
"LabelZipCode": "Індекс:",
@ -1049,7 +1049,7 @@
"LabelSelectFolderGroupsHelp": "Папки, для яких не встановлено прапорець, відображатимуться самі по собі у власному поданні.",
"LabelSeasonNumber": "Номер сезону:",
"LabelScreensaver": "Заставка:",
"LabelScheduledTaskLastRan": "Останній раз запускалося: {0}, час виконання: {1}.",
"LabelScheduledTaskLastRan": "Останній запуск: {0}, час виконання: {1}.",
"LabelSaveLocalMetadata": "Збережіть ілюстрацію в медіа-папках",
"LabelRuntimeMinutes": "Час виконання:",
"LabelRequireHttps": "Вимагати HTTPS",
@ -1284,7 +1284,7 @@
"PackageInstallFailed": "Помилка встановлення {0} (версія {1}).",
"PackageInstallCompleted": "Установлення {0} (версія {1}) завершено.",
"PackageInstallCancelled": "Установлення {0} (версія {1}) скасовано.",
"OtherArtist": "Інший художник",
"OtherArtist": "Інший виконавець",
"OriginalAirDateValue": "Початкова дата ефіру: {0}",
"OptionWakeFromSleep": "Пробудити",
"OptionUnairedEpisode": "Невипущені епізоди",
@ -1329,7 +1329,7 @@
"OptionEnableM2tsMode": "Увімкнути режим M2TS",
"OptionEnableForAllTuners": "Увімкнути для всіх пристроїв тюнера",
"OptionEnableExternalContentInSuggestionsHelp": "Дозволити включати інтернет-трейлери та телепрограми в прямому ефірі до запропонованого вмісту.",
"OptionCaptionInfoExSamsung": "CaptionInfoEx (Samsung)",
"OptionCaptionInfoExSamsung": "Функція CaptionInfoEx (Samsung)",
"OptionEnableExternalContentInSuggestions": "Увімкнути зовнішній вміст у пропозиціях",
"OptionEnableAccessToAllLibraries": "Увімкнути доступ до всіх медіатек",
"OptionEnableAccessToAllChannels": "Увімкнути доступ до всіх каналів",
@ -1422,7 +1422,7 @@
"TabMyPlugins": "Мої плагіни",
"TabMusic": "Музика",
"TabLogs": "Журнали",
"TabLatest": "Останні",
"TabLatest": "Нещодавно додані",
"TabDashboard": "Панель",
"TabContainers": "Контейнери",
"TabCodecs": "Кодеки",
@ -1518,7 +1518,7 @@
"SubtitleCodecNotSupported": "Кодек субтитрів не підтримується",
"ContainerNotSupported": "Контейнер не підтримується",
"AudioCodecNotSupported": "Аудіокодек не підтримується",
"EnableGamepadHelp": "Слухайте вхід з будь-яких підключених контролерів.",
"EnableGamepadHelp": "Очікуйте вхідних даних від будь-яких підключених контролерів. (Вимагається: режим відображення «TV»)",
"LabelEnableGamepad": "Увімкнути геймпад",
"Controls": "Елементи керування",
"AllowVppTonemappingHelp": "Повне відображення тонів на основі драйверів Intel. Наразі працює лише на певному обладнанні з відео HDR10. Це має вищий пріоритет порівняно з іншою реалізацією OpenCL.",
@ -1559,8 +1559,8 @@
"XmlTvMovieCategoriesHelp": "Програми з цими категоріями відображатимуться як фільми. Розділіть множинні символом \"|\".",
"XmlTvKidsCategoriesHelp": "Програми з цими категоріями відображатимуться як програми для дітей. Розділіть множинні символом \"|\".",
"XmlDocumentAttributeListHelp": "Ці атрибути застосовуються до кореневого елемента кожної відповіді XML.",
"Writers": "Письменники",
"Writer": "Письменник",
"Writers": "Сценаристи",
"Writer": "Сценарист",
"WizardCompleted": "Це все, що нам зараз потрібно. Jellyfin почав збирати інформацію про вашу медіатеку. Перегляньте деякі з наших програм, а потім натисніть <b>Готово</b>, щоб переглянути <b>інформаційну панель</b>.",
"Whitelist": "Білий список",
"WelcomeToProject": "Ласкаво просимо до Jellyfin!",
@ -1685,5 +1685,20 @@
"MessageNoFavoritesAvailable": "Зараз немає доступних улюблених.",
"Experimental": "Експериментальний",
"LabelStereoDownmixAlgorithm": "Stereo Downmix алгоритм",
"StereoDownmixAlgorithmHelp": "Алгоритм мікшування багатоканального аудіо у стерео."
"StereoDownmixAlgorithmHelp": "Алгоритм мікшування багатоканального аудіо у стерео.",
"LabelChapterImageResolutionHelp": "Роздільна здатність витягнутих зображень розділу.",
"PreferEmbeddedExtrasTitlesOverFileNames": "Віддавайте перевагу вбудованим назвам, а не назвам файлів для додаткових функцій",
"PreferEmbeddedExtrasTitlesOverFileNamesHelp": "Додатки часто мають таке ж вбудоване ім’я, що й батьківське, позначте це, щоб усе одно використовувати для них вбудовані заголовки.",
"ResolutionMatchSource": "Джерело відповідності",
"SaveRecordingNFO": "Збережіть метадані EPG запису в NFO",
"SaveRecordingNFOHelp": "Зберігайте метадані від постачальника списків EPG разом із бічними носіями.",
"SaveRecordingImages": "Зберегти записані зображення EPG",
"SaveRecordingImagesHelp": "Зберігайте зображення з постачальника списків EPG разом із носієм.",
"HeaderDummyChapter": "Зображення розділів",
"LabelDummyChapterDuration": "Інтервал:",
"LabelDummyChapterDurationHelp": "Інтервал вилучення зображення розділу в секундах.",
"LabelDummyChapterCount": "Ліміт:",
"LabelDummyChapterCountHelp": "Максимальна кількість зображень розділів, які буде видобуто для кожного медіафайлу.",
"LabelChapterImageResolution": "Роздільна здатність:",
"HeaderRecordingMetadataSaving": "Метадані запису"
}