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 blurred-pdf-fixed

This commit is contained in:
Zourlo 2023-03-10 17:40:27 +09:00 committed by GitHub
commit 2848f6295b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
109 changed files with 2259 additions and 1324 deletions

View file

@ -48,6 +48,9 @@ function supportsFade() {
function requireHlsPlayer(callback) {
import('hls.js').then(({ default: hls }) => {
hls.DefaultConfig.lowLatencyMode = false;
hls.DefaultConfig.backBufferLength = Infinity;
hls.DefaultConfig.liveBackBufferLength = 90;
window.Hls = hls;
callback();
});

View file

@ -32,6 +32,7 @@ import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings';
import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../components/backdrop/backdrop';
import Events from '../../utils/events.ts';
import { includesAny } from '../../utils/container.ts';
import debounce from 'lodash-es/debounce';
/**
* Returns resolved URL.
@ -106,6 +107,9 @@ function tryRemoveElement(elem) {
function requireHlsPlayer(callback) {
import('hls.js').then(({default: hls}) => {
hls.DefaultConfig.lowLatencyMode = false;
hls.DefaultConfig.backBufferLength = Infinity;
hls.DefaultConfig.liveBackBufferLength = 90;
window.Hls = hls;
callback();
});
@ -155,6 +159,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 +185,6 @@ function tryRemoveElement(elem) {
* @type {boolean}
*/
isFetching = false;
/**
* @type {HTMLDivElement | null | undefined}
*/
@ -187,6 +193,10 @@ function tryRemoveElement(elem) {
* @type {number | undefined}
*/
#subtitleTrackIndexToSetOnPlaying;
/**
* @type {number | undefined}
*/
#secondarySubtitleTrackIndexToSetOnPlaying;
/**
* @type {number | null}
*/
@ -207,6 +217,10 @@ function tryRemoveElement(elem) {
* @type {number | undefined}
*/
#customTrackIndex;
/**
* @type {number | undefined}
*/
#customSecondaryTrackIndex;
/**
* @type {boolean | undefined}
*/
@ -215,14 +229,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}
*/
@ -356,7 +382,7 @@ function tryRemoveElement(elem) {
this.#currentTime = null;
this.resetSubtitleOffset();
if (options.resetSubtitleOffset !== false) this.resetSubtitleOffset();
return this.createMediaElement(options).then(elem => {
return this.updateVideoUrl(options).then(() => {
@ -448,18 +474,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 +537,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 +562,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,10 +575,12 @@ function tryRemoveElement(elem) {
}
}
setSubtitleOffset = debounce(this._setSubtitleOffset, 100);
/**
* @private
*/
setSubtitleOffset(offset) {
_setSubtitleOffset(offset) {
const offsetValue = parseFloat(offset);
// if .ass currently rendering
@ -534,12 +588,15 @@ function tryRemoveElement(elem) {
this.updateCurrentTrackOffset(offsetValue);
this.#currentSubtitlesOctopus.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,37 +606,100 @@ 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;
}
/**
* @private
* These browsers will not clear the existing active cue when setting an offset
* for native TextTracks.
* Any previous text tracks that are on the screen when the offset changes will remain next
* to the new tracks until they reach the end time of the new offset's instance of the track.
*/
setTextTrackSubtitleOffset(currentTrack, offsetValue) {
if (currentTrack.cues) {
offsetValue = this.updateCurrentTrackOffset(offsetValue);
Array.from(currentTrack.cues)
.forEach(function (cue) {
cue.startTime -= offsetValue;
cue.endTime -= offsetValue;
});
requiresHidingActiveCuesOnOffsetChange() {
return !!browser.firefox;
}
/**
* @private
*/
hideTextTrackWithActiveCues(currentTrack) {
if (currentTrack.activeCues) {
currentTrack.mode = 'hidden';
}
}
/**
* Forces the active cue to clear by disabling then re-enabling the track.
* The track mode is reverted inside of a 0ms timeout to free up the track
* and allow it to disable and clear the active cue.
* @private
*/
forceClearTextTrackActiveCues(currentTrack) {
if (currentTrack.activeCues) {
currentTrack.mode = 'disabled';
setTimeout(() => {
currentTrack.mode = 'showing';
}, 0);
}
}
/**
* @private
*/
setTrackEventsSubtitleOffset(trackEvents, offsetValue) {
setTextTrackSubtitleOffset(currentTrack, offsetValue, currentTrackIndex) {
if (currentTrack.cues) {
offsetValue = this.updateCurrentTrackOffset(offsetValue, currentTrackIndex);
if (offsetValue === 0) {
return;
}
const shouldClearActiveCues = this.requiresHidingActiveCuesOnOffsetChange();
if (shouldClearActiveCues) {
this.hideTextTrackWithActiveCues(currentTrack);
}
Array.from(currentTrack.cues)
.forEach(function (cue) {
cue.startTime -= offsetValue;
cue.endTime -= offsetValue;
});
if (shouldClearActiveCues) {
this.forceClearTextTrackActiveCues(currentTrack);
}
}
}
/**
* @private
*/
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 +711,14 @@ function tryRemoveElement(elem) {
return this.#currentTrackOffset;
}
isPrimaryTrack(textTrackIndex) {
return textTrackIndex === PRIMARY_TEXT_TRACK_INDEX;
}
isSecondaryTrack(textTrackIndex) {
return textTrackIndex === SECONDARY_TEXT_TRACK_INDEX;
}
/**
* @private
*/
@ -697,6 +825,8 @@ function tryRemoveElement(elem) {
}
destroy() {
this.setSubtitleOffset.cancel();
destroyHlsPlayer(this);
destroyFlvPlayer(this);
@ -819,6 +949,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);
}
}
/**
@ -956,27 +1096,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;
@ -1029,23 +1217,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);
}
/**
@ -1155,16 +1354,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;
}
});
}
@ -1211,7 +1433,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') {
@ -1220,15 +1442,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 {
@ -1288,24 +1510,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');
}
}
}
}
@ -1313,7 +1540,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 +1549,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 +1727,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 {