From 34a0d9b3145ac203fb62946e5b10c28ca4dc4de2 Mon Sep 17 00:00:00 2001 From: Peaches_MLG Date: Tue, 20 Oct 2026 17:18:36 +0100 Subject: [PATCH] Added loading spinner when video is buffering (#7487) * Added loading spinner when video is buffering * Only show if buffering for more than 500ms --- .../controllers/playback/video/index.js | 26 +++++++++++++++++++ src/plugins/htmlVideoPlayer/plugin.js | 6 +++++ 2 files changed, 32 insertions(+) diff --git a/src/apps/legacy/controllers/playback/video/index.js b/src/apps/legacy/controllers/playback/video/index.js index add9be8143..8e71246c49 100644 --- a/src/apps/legacy/controllers/playback/video/index.js +++ b/src/apps/legacy/controllers/playback/video/index.js @@ -28,6 +28,7 @@ import 'elements/emby-ratingbutton/emby-ratingbutton'; import 'styles/videoosd.scss'; import shell from 'scripts/shell'; import SubtitleSync from 'components/subtitlesync/subtitlesync'; +import loading from 'components/loading/loading'; import { appRouter } from 'components/router/appRouter'; import { ServerConnections } from 'lib/jellyfin-apiclient'; import LibraryMenu from 'scripts/libraryMenu'; @@ -578,6 +579,22 @@ export default function (view) { view.querySelector('.osdMediaStatus').classList.add('hide'); } + let loadingTimeout; + + function onWaiting() { + loadingTimeout = setTimeout(() => { + loading.show(); + }, 500); + } + + function onCanPlay() { + if (loadingTimeout) { + clearTimeout(loadingTimeout); + loadingTimeout = null; + } + loading.hide(); + } + function bindToPlayer(player) { if (player !== currentPlayer) { releaseCurrentPlayer(); @@ -599,6 +616,8 @@ export default function (view) { Events.on(player, 'mediastreamschange', onMediaStreamsChanged); Events.on(player, 'beginFetch', onBeginFetch); Events.on(player, 'endFetch', onEndFetch); + Events.on(player, 'waiting', onWaiting); + Events.on(player, 'canplay', onCanPlay); resetUpNextDialog(); if (player.isFetching) { @@ -607,6 +626,11 @@ export default function (view) { } function releaseCurrentPlayer() { + if (loadingTimeout) { + clearTimeout(loadingTimeout); + loadingTimeout = null; + } + destroyStats(); destroySubtitleSync(); resetUpNextDialog(); @@ -624,6 +648,8 @@ export default function (view) { Events.off(player, 'mediastreamschange', onMediaStreamsChanged); Events.off(player, 'beginFetch', onBeginFetch); Events.off(player, 'endFetch', onEndFetch); + Events.off(player, 'waiting', onWaiting); + Events.off(player, 'canplay', onCanPlay); currentPlayer = null; } } diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index eb05840900..947d7e47af 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -984,6 +984,7 @@ export class HtmlVideoPlayer { videoElement.removeEventListener('click', this.onClick); videoElement.removeEventListener('dblclick', this.onDblClick); videoElement.removeEventListener('waiting', this.onWaiting); + videoElement.removeEventListener('canplay', this.onCanPlay); videoElement.removeEventListener('error', this.onError); // bound in htmlMediaHelper resetSrc(videoElement); @@ -1186,6 +1187,10 @@ export class HtmlVideoPlayer { Events.trigger(this, 'waiting'); }; + onCanPlay = () => { + Events.trigger(this, 'canplay'); + }; + /** * @private * @param e {Event} The event received from the `