diff --git a/eslint.config.mjs b/eslint.config.mjs index e859a96b52..add0ec5ae2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -207,29 +207,22 @@ export default tseslint.config( }, globals: { ...globals.browser, + // Chromecast globals + 'chrome': 'readonly', // Tizen globals - 'tizen': false, - 'webapis': false, + 'tizen': 'readonly', + 'webapis': 'readonly', // WebOS globals - 'webOS': false, - // Dependency globals - '$': false, - 'jQuery': false, + 'webOS': 'readonly', // Jellyfin globals - 'ApiClient': true, - 'Events': true, - 'chrome': true, - 'Emby': false, - 'Hls': true, - 'LibraryMenu': true, - 'Windows': false, + 'ApiClient': 'readonly', // Build time definitions - __COMMIT_SHA__: false, - __JF_BUILD_VERSION__: false, - __PACKAGE_JSON_NAME__: false, - __PACKAGE_JSON_VERSION__: false, - __USE_SYSTEM_FONTS__: false, - __WEBPACK_SERVE__: false + __COMMIT_SHA__: 'readonly', + __JF_BUILD_VERSION__: 'readonly', + __PACKAGE_JSON_NAME__: 'readonly', + __PACKAGE_JSON_VERSION__: 'readonly', + __USE_SYSTEM_FONTS__: 'readonly', + __WEBPACK_SERVE__: 'readonly' } }, settings: { diff --git a/src/components/htmlMediaHelper.js b/src/components/htmlMediaHelper.js index bcdc285bbf..3c6a339f6f 100644 --- a/src/components/htmlMediaHelper.js +++ b/src/components/htmlMediaHelper.js @@ -1,4 +1,6 @@ -import appSettings from '../scripts/settings/appSettings' ; +import { Events as HlsEvents, ErrorTypes as HlsErrorTypes } from 'hls.js/dist/hls.js'; + +import appSettings from '../scripts/settings/appSettings'; import browser from '../scripts/browser'; import Events from '../utils/events.ts'; import { MediaError } from 'types/mediaError'; @@ -167,24 +169,6 @@ export function seekOnPlaybackStart(instance, element, ticks, onMediaReady) { } } -export function applySrc(elem, src, options) { - if (window.Windows && options.mediaSource?.IsLocal) { - return Windows.Storage.StorageFile.getFileFromPathAsync(options.url).then(function (file) { - const playlist = new Windows.Media.Playback.MediaPlaybackList(); - - const source1 = Windows.Media.Core.MediaSource.createFromStorageFile(file); - const startTime = (options.playerStartPositionTicks || 0) / 10000; - playlist.items.append(new Windows.Media.Playback.MediaPlaybackItem(source1, startTime)); - elem.src = URL.createObjectURL(playlist, { oneTimeOnly: true }); - return Promise.resolve(); - }); - } else { - elem.src = src; - } - - return Promise.resolve(); -} - export function resetSrc(elem) { elem.src = ''; elem.innerHTML = ''; @@ -260,7 +244,7 @@ export function destroyFlvPlayer(instance) { } export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, reject) { - hls.on(Hls.Events.MANIFEST_PARSED, function () { + hls.on(HlsEvents.MANIFEST_PARSED, function () { playWithPromise(elem, onErrorFn).then(resolve, function () { if (reject) { reject(); @@ -269,11 +253,11 @@ export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, r }); }); - hls.on(Hls.Events.ERROR, function (event, data) { + hls.on(HlsEvents.ERROR, function (event, data) { console.error('HLS Error: Type: ' + data.type + ' Details: ' + (data.details || '') + ' Fatal: ' + (data.fatal || false)); // try to recover network error - if (data.type === Hls.ErrorTypes.NETWORK_ERROR + if (data.type === HlsErrorTypes.NETWORK_ERROR && data.response?.code && data.response.code >= 400 ) { console.debug('hls.js response error code: ' + data.response.code); @@ -293,7 +277,7 @@ export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, r if (data.fatal) { switch (data.type) { - case Hls.ErrorTypes.NETWORK_ERROR: + case HlsErrorTypes.NETWORK_ERROR: if (data.response && data.response.code === 0) { // This could be a CORS error related to access control response headers @@ -315,7 +299,7 @@ export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, r } break; - case Hls.ErrorTypes.MEDIA_ERROR: + case HlsErrorTypes.MEDIA_ERROR: console.debug('fatal media error encountered, try to recover'); handleHlsJsMediaError(instance, reject); reject = null; diff --git a/src/components/recordingcreator/recordingfields.js b/src/components/recordingcreator/recordingfields.js index 15b2d6d8c4..7d82464859 100644 --- a/src/components/recordingcreator/recordingfields.js +++ b/src/components/recordingcreator/recordingfields.js @@ -1,17 +1,21 @@ -import globalize from '../../lib/globalize'; -import { ServerConnections } from 'lib/jellyfin-apiclient'; import { OutboundWebSocketMessageType } from '@jellyfin/sdk/lib/websocket'; -import loading from '../loading/loading'; -import dom from '../../utils/dom'; + +import loading from 'components/loading/loading'; +import toast from 'components/toast/toast'; +import globalize from 'lib/globalize'; +import { ServerConnections } from 'lib/jellyfin-apiclient'; +import dom from 'utils/dom'; +import Events from 'utils/events'; + import recordingHelper from './recordinghelper'; -import '../../elements/emby-button/emby-button'; -import '../../elements/emby-button/paper-icon-button-light'; -import './recordingfields.scss'; -import '../../styles/flexstyles.scss'; -import toast from '../toast/toast'; import template from './recordingfields.template.html'; +import 'elements/emby-button/emby-button'; +import 'elements/emby-button/paper-icon-button-light'; +import './recordingfields.scss'; +import 'styles/flexstyles.scss'; + function loadData(parent, program) { if (program.IsSeries) { parent.querySelector('.recordSeriesContainer').classList.remove('hide'); diff --git a/src/components/tvproviders/schedulesdirect.js b/src/components/tvproviders/schedulesdirect.js index 8536f93c1f..fa4e7963d2 100644 --- a/src/components/tvproviders/schedulesdirect.js +++ b/src/components/tvproviders/schedulesdirect.js @@ -1,4 +1,5 @@ -import 'jquery'; +import $ from 'jquery'; + import loading from '../loading/loading'; import globalize from '../../lib/globalize'; import '../../elements/emby-checkbox/emby-checkbox'; diff --git a/src/components/tvproviders/xmltv.js b/src/components/tvproviders/xmltv.js index 437905460a..e81e558752 100644 --- a/src/components/tvproviders/xmltv.js +++ b/src/components/tvproviders/xmltv.js @@ -1,4 +1,5 @@ -import 'jquery'; +import $ from 'jquery'; + import loading from '../loading/loading'; import globalize from '../../lib/globalize'; import '../../elements/emby-checkbox/emby-checkbox'; diff --git a/src/components/viewContainer.js b/src/components/viewContainer.js index b5509f8b5a..b14394d431 100644 --- a/src/components/viewContainer.js +++ b/src/components/viewContainer.js @@ -1,6 +1,9 @@ +import $ from 'jquery'; import { importModule } from '@uupaa/dynamic-import-polyfill'; + +import Dashboard from 'utils/dashboard'; + import './viewManager/viewContainer.scss'; -import Dashboard from '../utils/dashboard'; const getMainAnimatedPages = () => { return document.querySelector('.mainAnimatedPages'); @@ -65,13 +68,13 @@ export function loadView(options) { } if (currentPage) { - if (newViewInfo.hasScript && window.$) { + if (newViewInfo.hasScript) { mainAnimatedPages.removeChild(currentPage); view = $(view).appendTo(mainAnimatedPages)[0]; } else { mainAnimatedPages.replaceChild(view, currentPage); } - } else if (newViewInfo.hasScript && window.$) { + } else if (newViewInfo.hasScript) { view = $(view).appendTo(mainAnimatedPages)[0]; } else { mainAnimatedPages.appendChild(view); @@ -109,11 +112,6 @@ export function loadView(options) { afterAnimate(allPages, pageIndex); } - if (window.$) { - $.mobile = $.mobile || {}; - $.mobile.activePage = view; - } - return view; }); } @@ -213,11 +211,6 @@ export function tryRestoreView(options) { afterAnimate(allPages, index); } - if (window.$) { - $.mobile = $.mobile || {}; - $.mobile.activePage = view; - } - return view; }); } diff --git a/src/elements/emby-itemscontainer/emby-itemscontainer.js b/src/elements/emby-itemscontainer/emby-itemscontainer.js index 7ce51f477d..3cac9dcb17 100644 --- a/src/elements/emby-itemscontainer/emby-itemscontainer.js +++ b/src/elements/emby-itemscontainer/emby-itemscontainer.js @@ -1,17 +1,18 @@ +import { OutboundWebSocketMessageType } from '@jellyfin/sdk/lib/websocket'; import 'webcomponents.js/webcomponents-lite'; import Sortable from 'sortablejs'; -import itemShortcuts from '../../components/shortcuts'; -import inputManager from '../../scripts/inputManager'; -import { playbackManager } from '../../components/playback/playbackmanager'; -import imageLoader from '../../components/images/imageLoader'; -import layoutManager from '../../components/layoutManager'; -import browser from '../../scripts/browser'; -import dom from '../../utils/dom'; -import loading from '../../components/loading/loading'; -import focusManager from '../../components/focusManager'; +import focusManager from 'components/focusManager'; +import imageLoader from 'components/images/imageLoader'; +import layoutManager from 'components/layoutManager'; +import loading from 'components/loading/loading'; +import { playbackManager } from 'components/playback/playbackmanager'; +import itemShortcuts from 'components/shortcuts'; import { ServerConnections } from 'lib/jellyfin-apiclient'; -import { OutboundWebSocketMessageType } from '@jellyfin/sdk/lib/websocket'; +import browser from 'scripts/browser'; +import inputManager from 'scripts/inputManager'; +import dom from 'utils/dom'; +import Events from 'utils/events'; const ItemsContainerPrototype = Object.create(HTMLDivElement.prototype); diff --git a/src/plugins/htmlAudioPlayer/plugin.js b/src/plugins/htmlAudioPlayer/plugin.js index 53a80de2b4..e63649e742 100644 --- a/src/plugins/htmlAudioPlayer/plugin.js +++ b/src/plugins/htmlAudioPlayer/plugin.js @@ -50,13 +50,12 @@ function supportsFade() { return !browser.tv; } -function requireHlsPlayer(callback) { - import('hls.js/dist/hls.js').then(({ default: hls }) => { +function requireHlsPlayer() { + return import('hls.js/dist/hls.js').then(({ default: hls }) => { hls.DefaultConfig.lowLatencyMode = false; hls.DefaultConfig.backBufferLength = Infinity; hls.DefaultConfig.liveBackBufferLength = 90; - window.Hls = hls; - callback(); + return hls; }); } @@ -174,26 +173,25 @@ class HtmlAudioPlayer { navigator.audioSession.type = 'playback'; } - return enableHlsPlayer(val, options.item, options.mediaSource, 'Audio').then(function () { + return enableHlsPlayer(val, options.item, options.mediaSource, 'Audio').then(async function () { + const Hls = await requireHlsPlayer(); + const includeCorsCredentials = await getIncludeCorsCredentials(); + return new Promise(function (resolve, reject) { - requireHlsPlayer(async () => { - const includeCorsCredentials = await getIncludeCorsCredentials(); - - const hls = new Hls({ - manifestLoadingTimeOut: 20000, - xhrSetup: function (xhr) { - xhr.withCredentials = includeCorsCredentials; - } - }); - hls.loadSource(val); - hls.attachMedia(elem); - - htmlMediaHelper.bindEventsToHlsPlayer(self, hls, elem, onError, resolve, reject); - - self._hlsPlayer = hls; - - self._currentSrc = val; + const hls = new Hls({ + manifestLoadingTimeOut: 20000, + xhrSetup: function (xhr) { + xhr.withCredentials = includeCorsCredentials; + } }); + hls.loadSource(val); + hls.attachMedia(elem); + + htmlMediaHelper.bindEventsToHlsPlayer(self, hls, elem, onError, resolve, reject); + + self._hlsPlayer = hls; + + self._currentSrc = val; }); }, async () => { elem.autoplay = true; @@ -204,11 +202,9 @@ class HtmlAudioPlayer { elem.crossOrigin = 'use-credentials'; } - return htmlMediaHelper.applySrc(elem, val, options).then(function () { - self._currentSrc = val; - - return htmlMediaHelper.playWithPromise(elem, onError); - }); + elem.src = val; + self._currentSrc = val; + return htmlMediaHelper.playWithPromise(elem, onError); }); } diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 947d7e47af..5bca9c3892 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -24,7 +24,6 @@ import { destroyCastPlayer, getCrossOriginValue, enableHlsJsPlayerForCodecs, - applySrc, resetSrc, playWithPromise, onEndedInternal, @@ -119,13 +118,12 @@ function enableNativeTrackSupport(mediaSource, track) { return true; } -function requireHlsPlayer(callback) { - import('hls.js/dist/hls.js').then(({ default: hls }) => { +function requireHlsPlayer() { + return import('hls.js/dist/hls.js').then(({ default: hls }) => { hls.DefaultConfig.lowLatencyMode = false; hls.DefaultConfig.backBufferLength = Infinity; hls.DefaultConfig.liveBackBufferLength = 90; - window.Hls = hls; - callback(); + return hls; }); } @@ -549,41 +547,40 @@ export class HtmlVideoPlayer { /** * @private */ - setSrcWithHlsJs(elem, options, url) { + async setSrcWithHlsJs(elem, options, url) { + const Hls = await requireHlsPlayer(); + const includeCorsCredentials = await getIncludeCorsCredentials(); + return new Promise((resolve, reject) => { - requireHlsPlayer(async () => { - let maxBufferLength = 30; + let maxBufferLength = 30; - // Some browsers cannot handle huge fragments in high bitrate. - // This issue usually happens when using HWA encoders with a high bitrate setting. - // Limit the BufferLength to 6s, it works fine when playing 4k 120Mbps over HLS on chrome. - // https://github.com/video-dev/hls.js/issues/876 - if ((browser.chrome || browser.edgeChromium || browser.firefox) && playbackManager.getMaxStreamingBitrate(this) >= 25000000) { - maxBufferLength = 6; + // Some browsers cannot handle huge fragments in high bitrate. + // This issue usually happens when using HWA encoders with a high bitrate setting. + // Limit the BufferLength to 6s, it works fine when playing 4k 120Mbps over HLS on chrome. + // https://github.com/video-dev/hls.js/issues/876 + if ((browser.chrome || browser.edgeChromium || browser.firefox) && playbackManager.getMaxStreamingBitrate(this) >= 25000000) { + maxBufferLength = 6; + } + + const hls = new Hls({ + startPosition: options.playerStartPositionTicks / 10000000, + manifestLoadingTimeOut: 20000, + maxBufferLength, + maxMaxBufferLength: maxBufferLength, + videoPreference: { preferHDR: true }, + xhrSetup(xhr) { + xhr.withCredentials = includeCorsCredentials; } - - const includeCorsCredentials = await getIncludeCorsCredentials(); - - const hls = new Hls({ - startPosition: options.playerStartPositionTicks / 10000000, - manifestLoadingTimeOut: 20000, - maxBufferLength: maxBufferLength, - maxMaxBufferLength: maxBufferLength, - videoPreference: { preferHDR: true }, - xhrSetup(xhr) { - xhr.withCredentials = includeCorsCredentials; - } - }); - hls.loadSource(url); - hls.attachMedia(elem); - - bindEventsToHlsPlayer(this, hls, elem, this.onError, resolve, reject); - - this._hlsPlayer = hls; - - // This is needed in setCurrentTrackElement - this.#currentSrc = url; }); + hls.loadSource(url); + hls.attachMedia(elem); + + bindEventsToHlsPlayer(this, hls, elem, this.onError, resolve, reject); + + this._hlsPlayer = hls; + + // This is needed in setCurrentTrackElement + this.#currentSrc = url; }); } @@ -657,11 +654,9 @@ export class HtmlVideoPlayer { elem.crossOrigin = 'use-credentials'; } - return applySrc(elem, val, options).then(() => { - this.#currentSrc = val; - - return playWithPromise(elem, this.onError); - }); + elem.src = val; + this.#currentSrc = val; + return playWithPromise(elem, this.onError); } } @@ -1328,25 +1323,10 @@ export class HtmlVideoPlayer { this.#currentBitmapSubRenderer = null; } - /** - * @private - */ - fetchSubtitlesUwp(track) { - return Windows.Storage.StorageFile.getFileFromPathAsync(track.Path).then(function (storageFile) { - return Windows.Storage.FileIO.readTextAsync(storageFile); - }).then(function (text) { - return JSON.parse(text); - }); - } - /** * @private */ async fetchSubtitles(track, item) { - if (window.Windows && itemHelper.isLocalItem(item)) { - return this.fetchSubtitlesUwp(track, item); - } - this.incrementFetchQueue(); try { const response = await fetch(getTextTrackUrl(track, item, '.js')); @@ -1956,10 +1936,6 @@ export class HtmlVideoPlayer { if ( // Check non-standard Safari PiP support typeof video.webkitSupportsPresentationMode === 'function' && video.webkitSupportsPresentationMode('picture-in-picture') && typeof video.webkitSetPresentationMode === 'function' - // Check non-standard Windows PiP support - || (window.Windows - && Windows.UI.ViewManagement.ApplicationView.getForCurrentView() - .isViewModeSupported(Windows.UI.ViewManagement.ApplicationViewMode.compactOverlay)) // Check standard PiP support || document.pictureInPictureEnabled ) { @@ -2043,13 +2019,6 @@ export class HtmlVideoPlayer { document.exitPictureInPicture().catch(HtmlVideoPlayer.onPictureInPictureError); } } - } else if (window.Windows) { - this.isPip = isEnabled; - if (isEnabled) { - Windows.UI.ViewManagement.ApplicationView.getForCurrentView().tryEnterViewModeAsync(Windows.UI.ViewManagement.ApplicationViewMode.compactOverlay); - } else { - Windows.UI.ViewManagement.ApplicationView.getForCurrentView().tryEnterViewModeAsync(Windows.UI.ViewManagement.ApplicationViewMode.default); - } } else if (video?.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === 'function') { video.webkitSetPresentationMode(isEnabled ? 'picture-in-picture' : 'inline'); } diff --git a/src/scripts/editorsidebar.js b/src/scripts/editorsidebar.js index 0074d34260..078e9c43c0 100644 --- a/src/scripts/editorsidebar.js +++ b/src/scripts/editorsidebar.js @@ -1,5 +1,5 @@ import escapeHtml from 'escape-html'; -import 'jquery'; +import $ from 'jquery'; import 'material-design-icons-iconfont'; import globalize from 'lib/globalize'; @@ -281,7 +281,7 @@ function updateEditorNode(page, item) { $('.editorNode', elem).remove(); $(elem).append(getNodeInnerHtml(item)); if (item.IsFolder) { - const tree = jQuery.jstree._reference('.libraryTree'); + const tree = $.jstree._reference('.libraryTree'); const currentNode = tree._get_node(null, false); tree.refresh(currentNode); } diff --git a/src/scripts/libraryBrowser.js b/src/scripts/libraryBrowser.js index 830311e510..7e0f3a9455 100644 --- a/src/scripts/libraryBrowser.js +++ b/src/scripts/libraryBrowser.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + import globalize from 'lib/globalize'; export function showLayoutMenu (button, currentLayout, views) { @@ -30,7 +32,7 @@ export function showLayoutMenu (button, currentLayout, views) { cancelable: false })); - if (!dispatchEvent && window.$) { + if (!dispatchEvent) { $(button).trigger('layoutchange', [id]); } }