From 836e0b645e2f09f31dcfa309df8c31a66cd090be Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Thu, 23 Apr 2026 15:28:39 -0400 Subject: [PATCH 01/14] resolve asset transition lockups and video stall freezes --- .../elements/asset-component.svelte | 7 +- .../src/lib/components/elements/asset.svelte | 23 ++++++- .../components/elements/progress-bar.svelte | 33 ++++----- .../lib/components/home-page/home-page.svelte | 68 +++++++++++++------ 4 files changed, 87 insertions(+), 44 deletions(-) diff --git a/immichFrame.Web/src/lib/components/elements/asset-component.svelte b/immichFrame.Web/src/lib/components/elements/asset-component.svelte index 7f02e36b..66e8f92a 100644 --- a/immichFrame.Web/src/lib/components/elements/asset-component.svelte +++ b/immichFrame.Web/src/lib/components/elements/asset-component.svelte @@ -32,6 +32,7 @@ playAudio?: boolean; onVideoWaiting?: () => void; onVideoPlaying?: () => void; + onAssetError?: () => void; } let { @@ -53,7 +54,8 @@ showInfo = $bindable(false), playAudio = false, onVideoWaiting = () => {}, - onVideoPlaying = () => {} + onVideoPlaying = () => {}, + onAssetError = () => {} }: Props = $props(); let instantTransition = slideshowStore.instantTransition; let transitionDuration = $derived( @@ -119,6 +121,7 @@ {playAudio} {onVideoWaiting} {onVideoPlaying} + {onAssetError} bind:this={primaryAssetComponent} bind:showInfo /> @@ -140,6 +143,7 @@ {playAudio} {onVideoWaiting} {onVideoPlaying} + {onAssetError} bind:this={secondaryAssetComponent} bind:showInfo /> @@ -163,6 +167,7 @@ {playAudio} {onVideoWaiting} {onVideoPlaying} + {onAssetError} bind:this={primaryAssetComponent} bind:showInfo /> diff --git a/immichFrame.Web/src/lib/components/elements/asset.svelte b/immichFrame.Web/src/lib/components/elements/asset.svelte index 14ee6465..20431e46 100644 --- a/immichFrame.Web/src/lib/components/elements/asset.svelte +++ b/immichFrame.Web/src/lib/components/elements/asset.svelte @@ -1,4 +1,5 @@ {#if !hidden} @@ -86,6 +79,6 @@ id="progressbar" class="fixed left-0 h-[3px] bg-primary z-[1000] {location == ProgressBarLocation.Top ? 'top-0' : 'bottom-0'}" - style:width={`${$progress * 100}%`} + style:width={`${progress.current * 100}%`} > {/if} diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index fa183550..7dc19481 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -26,11 +26,14 @@ api.init(); - // TODO: make this configurable? const PRELOAD_ASSETS = 5; + const TRANSITION_WATCHDOG_MS = 10000; + const VIDEO_STALL_MS = 15000; + const CURSOR_HIDE_MS = 2000; + const RELOAD_ON_ERROR_MS = 30000; - let assetHistory: api.AssetResponseDto[] = []; - let assetBacklog: api.AssetResponseDto[] = []; + let assetHistory: api.AssetResponseDto[] = $state([]); + let assetBacklog: api.AssetResponseDto[] = $state([]); let displayingAssets: api.AssetResponseDto[] = $state([]); @@ -40,6 +43,11 @@ let progressBar: ProgressBar = $state() as ProgressBar; let assetComponent: AssetComponentInstance = $state() as AssetComponentInstance; let currentDuration: number = $state($configStore.interval ?? 20); + + let watchdogTimer: number | undefined = $state(); + let videoStallTimeout: number | undefined = $state(); + let timeoutId: number | undefined = $state(); + let userPaused: boolean = $state(false); let error: boolean = $state(false); @@ -63,7 +71,6 @@ let refreshInterval: number; let cursorVisible = $state(true); - let timeoutId: number; const clientIdentifier = page.url.searchParams.get('client'); const authsecret = page.url.searchParams.get('authsecret'); @@ -93,7 +100,7 @@ const showCursor = () => { cursorVisible = true; clearTimeout(timeoutId); - timeoutId = setTimeout(hideCursor, 2000); + timeoutId = window.setTimeout(hideCursor, CURSOR_HIDE_MS); }; async function updateAssetPromises() { @@ -116,16 +123,14 @@ !displayingAssets.find((item) => item.id === key) && !assetBacklog.find((item) => item.id === key) ); - for (const key of keysToRemove) { - try { - const [url] = await assetPromisesDict[key]; - revokeObjectUrl(url); - } catch (err) { - console.warn('Failed to resolve asset during cleanup:', err); - } finally { - delete assetPromisesDict[key]; - } - } + + keysToRemove.forEach((key) => { + const promise = assetPromisesDict[key]; + delete assetPromisesDict[key]; + promise + .then(([url]) => revokeObjectUrl(url)) + .catch((err) => console.warn('Failed to resolve asset during cleanup:', err)); + }); } async function loadAssets() { @@ -149,23 +154,36 @@ } } - let isHandlingAssetTransition = false; + let isHandlingAssetTransition = $state(false); const handleDone = async (previous: boolean = false, instant: boolean = false) => { if (isHandlingAssetTransition) { + console.warn('Transition already in progress, ignoring request'); return; } isHandlingAssetTransition = true; + + clearTimeout(watchdogTimer); + // Watchdog: If the transition (fetching/loading assets) takes longer than + // the current interval plus a 10s buffer, force-release the lock. + watchdogTimer = window.setTimeout(() => { + if (isHandlingAssetTransition) { + console.error('Transition watchdog triggered: Force-resetting lock due to hang'); + isHandlingAssetTransition = false; + } + }, (currentDuration * 1000) + TRANSITION_WATCHDOG_MS); + try { userPaused = false; progressBar.restart(false); $instantTransition = instant; if (previous) await getPreviousAssets(); else await getNextAssets(); - await tick(); + await tick(); await assetComponent?.play?.(); progressBar.play(); } finally { isHandlingAssetTransition = false; + clearTimeout(watchdogTimer); } }; @@ -182,7 +200,6 @@ const useSplit = shouldUseSplitView(assetBacklog); const next = assetBacklog.splice(0, useSplit ? 2 : 1); - assetBacklog = [...assetBacklog]; if (displayingAssets.length) { assetHistory.push(...displayingAssets); @@ -204,7 +221,6 @@ const useSplit = shouldUseSplitView(assetHistory.slice(-2)); const next = assetHistory.splice(useSplit ? -2 : -1); - assetHistory = [...assetHistory]; if (displayingAssets.length) { assetBacklog.unshift(...displayingAssets); @@ -392,7 +408,7 @@ // 30 second reload on error refreshInterval = window.setInterval(() => { if (error) window.location.reload(); - }, 30000); + }, RELOAD_ON_ERROR_MS); if ($configStore.primaryColor) { document.documentElement.style.setProperty('--primary-color', $configStore.primaryColor); @@ -426,6 +442,9 @@ window.removeEventListener('mousemove', showCursor); window.removeEventListener('click', showCursor); window.clearInterval(refreshInterval); + window.clearTimeout(timeoutId); + window.clearTimeout(videoStallTimeout); + window.clearTimeout(watchdogTimer); }; }); @@ -473,12 +492,21 @@ playAudio={$configStore.playAudio} onVideoWaiting={async () => { await progressBar.pause(); + clearTimeout(videoStallTimeout); + videoStallTimeout = window.setTimeout(() => { + console.warn('Video stalled, skipping...'); + handleDone(false, true); + }, Math.min(VIDEO_STALL_MS, currentDuration * 1000)); }} onVideoPlaying={async () => { if (!userPaused) { await progressBar.play(); + clearTimeout(videoStallTimeout); } }} + onAssetError={async () => { + await handleDone(false, true); + }} /> From ab366aa0b7e3f841502462c5de374bcc5fe6b7c3 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Thu, 23 Apr 2026 15:32:29 -0400 Subject: [PATCH 02/14] comment cleanup --- immichFrame.Web/src/lib/components/elements/asset.svelte | 3 --- immichFrame.Web/src/lib/components/home-page/home-page.svelte | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/immichFrame.Web/src/lib/components/elements/asset.svelte b/immichFrame.Web/src/lib/components/elements/asset.svelte index 20431e46..fad28e55 100644 --- a/immichFrame.Web/src/lib/components/elements/asset.svelte +++ b/immichFrame.Web/src/lib/components/elements/asset.svelte @@ -54,8 +54,6 @@ let debug = false; const isVideo = $derived(isVideoAsset(asset[1])); - // Snapshot the interval when the asset ID changes to prevent "jumps" - // when the global currentDuration changes for the next asset. const animationDuration = $derived.by(() => { asset[1].id; return untrack(() => interval); @@ -64,7 +62,6 @@ let videoElement = $state(null); $effect(() => { - // Track asset URL to cleanup when it changes asset[0]; return () => { if (videoElement) { diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index 7dc19481..1a80a322 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -26,6 +26,7 @@ api.init(); + // TODO: make this configurable? const PRELOAD_ASSETS = 5; const TRANSITION_WATCHDOG_MS = 10000; const VIDEO_STALL_MS = 15000; @@ -164,7 +165,7 @@ clearTimeout(watchdogTimer); // Watchdog: If the transition (fetching/loading assets) takes longer than - // the current interval plus a 10s buffer, force-release the lock. + // the current interval plus a buffer, force-release the lock. watchdogTimer = window.setTimeout(() => { if (isHandlingAssetTransition) { console.error('Transition watchdog triggered: Force-resetting lock due to hang'); From e891200e775e6684e651fbb0aabde8b560c05064 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Thu, 23 Apr 2026 15:50:29 -0400 Subject: [PATCH 03/14] coderabbit nitpicks --- .../src/lib/components/home-page/home-page.svelte | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index 1a80a322..9b26d2e1 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -164,6 +164,7 @@ isHandlingAssetTransition = true; clearTimeout(watchdogTimer); + clearTimeout(videoStallTimeout); // Watchdog: If the transition (fetching/loading assets) takes longer than // the current interval plus a buffer, force-release the lock. watchdogTimer = window.setTimeout(() => { @@ -495,14 +496,16 @@ await progressBar.pause(); clearTimeout(videoStallTimeout); videoStallTimeout = window.setTimeout(() => { - console.warn('Video stalled, skipping...'); - handleDone(false, true); - }, Math.min(VIDEO_STALL_MS, currentDuration * 1000)); + if (!userPaused) { + console.warn('Video stalled, skipping...'); + handleDone(false, true); + } + }, Math.min(VIDEO_STALL_MS, Math.max(currentDuration * 1000, 5000))); }} onVideoPlaying={async () => { + clearTimeout(videoStallTimeout); if (!userPaused) { await progressBar.play(); - clearTimeout(videoStallTimeout); } }} onAssetError={async () => { From 9e359619a9a3f588a4368d81faa221d6ea364193 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Thu, 23 Apr 2026 16:02:23 -0400 Subject: [PATCH 04/14] coderrabbit nitpicks 2 --- .../src/lib/components/home-page/home-page.svelte | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index 9b26d2e1..6dac3b78 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -156,9 +156,14 @@ } let isHandlingAssetTransition = $state(false); + let pendingAssetError = $state(false); const handleDone = async (previous: boolean = false, instant: boolean = false) => { if (isHandlingAssetTransition) { console.warn('Transition already in progress, ignoring request'); + // If an error skip or manual skip is requested while busy, queue it + if (instant && !previous) { + pendingAssetError = true; + } return; } isHandlingAssetTransition = true; @@ -172,7 +177,7 @@ console.error('Transition watchdog triggered: Force-resetting lock due to hang'); isHandlingAssetTransition = false; } - }, (currentDuration * 1000) + TRANSITION_WATCHDOG_MS); + }, TRANSITION_WATCHDOG_MS); try { userPaused = false; @@ -186,6 +191,12 @@ } finally { isHandlingAssetTransition = false; clearTimeout(watchdogTimer); + + // If an asset error occurred during the transition, trigger the next skip now + if (pendingAssetError) { + pendingAssetError = false; + handleDone(false, true); + } } }; From 66940156c5cd9ed1792027c50208279105ff7a96 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Thu, 23 Apr 2026 18:40:51 -0400 Subject: [PATCH 05/14] coderabbit 3 --- .../lib/components/home-page/home-page.svelte | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index 6dac3b78..7d681379 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -26,7 +26,6 @@ api.init(); - // TODO: make this configurable? const PRELOAD_ASSETS = 5; const TRANSITION_WATCHDOG_MS = 10000; const VIDEO_STALL_MS = 15000; @@ -156,14 +155,9 @@ } let isHandlingAssetTransition = $state(false); - let pendingAssetError = $state(false); const handleDone = async (previous: boolean = false, instant: boolean = false) => { if (isHandlingAssetTransition) { console.warn('Transition already in progress, ignoring request'); - // If an error skip or manual skip is requested while busy, queue it - if (instant && !previous) { - pendingAssetError = true; - } return; } isHandlingAssetTransition = true; @@ -171,13 +165,13 @@ clearTimeout(watchdogTimer); clearTimeout(videoStallTimeout); // Watchdog: If the transition (fetching/loading assets) takes longer than - // the current interval plus a buffer, force-release the lock. + // the current interval plus a 10s buffer, force-release the lock. watchdogTimer = window.setTimeout(() => { if (isHandlingAssetTransition) { console.error('Transition watchdog triggered: Force-resetting lock due to hang'); isHandlingAssetTransition = false; } - }, TRANSITION_WATCHDOG_MS); + }, (currentDuration * 1000) + TRANSITION_WATCHDOG_MS); try { userPaused = false; @@ -191,12 +185,6 @@ } finally { isHandlingAssetTransition = false; clearTimeout(watchdogTimer); - - // If an asset error occurred during the transition, trigger the next skip now - if (pendingAssetError) { - pendingAssetError = false; - handleDone(false, true); - } } }; @@ -507,16 +495,14 @@ await progressBar.pause(); clearTimeout(videoStallTimeout); videoStallTimeout = window.setTimeout(() => { - if (!userPaused) { - console.warn('Video stalled, skipping...'); - handleDone(false, true); - } - }, Math.min(VIDEO_STALL_MS, Math.max(currentDuration * 1000, 5000))); + console.warn('Video stalled, skipping...'); + handleDone(false, true); + }, Math.min(VIDEO_STALL_MS, currentDuration * 1000)); }} onVideoPlaying={async () => { - clearTimeout(videoStallTimeout); if (!userPaused) { await progressBar.play(); + clearTimeout(videoStallTimeout); } }} onAssetError={async () => { From 2397bb96846f2087b9bc136b871f8bfa021bcdcd Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Fri, 24 Apr 2026 07:57:30 -0400 Subject: [PATCH 06/14] handle manual back skip mid-transition --- .../src/lib/components/home-page/home-page.svelte | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index 7d681379..6dbc8008 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -155,9 +155,11 @@ } let isHandlingAssetTransition = $state(false); + let pendingTransition: { previous: boolean; instant: boolean } | null = $state(null); + const handleDone = async (previous: boolean = false, instant: boolean = false) => { if (isHandlingAssetTransition) { - console.warn('Transition already in progress, ignoring request'); + pendingTransition = { previous, instant }; return; } isHandlingAssetTransition = true; @@ -185,6 +187,12 @@ } finally { isHandlingAssetTransition = false; clearTimeout(watchdogTimer); + + if (pendingTransition) { + const next = pendingTransition; + pendingTransition = null; + handleDone(next.previous, next.instant); + } } }; From b89eb7f497029e3ef504c3b52d12f3496d64a0d0 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Fri, 24 Apr 2026 08:00:25 -0400 Subject: [PATCH 07/14] drain pending transition queue --- .../src/lib/components/home-page/home-page.svelte | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index 6dbc8008..25ae3f6c 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -172,6 +172,12 @@ if (isHandlingAssetTransition) { console.error('Transition watchdog triggered: Force-resetting lock due to hang'); isHandlingAssetTransition = false; + + if (pendingTransition) { + const next = pendingTransition; + pendingTransition = null; + handleDone(next.previous, next.instant); + } } }, (currentDuration * 1000) + TRANSITION_WATCHDOG_MS); From d9a63eb6638be3155806a45bdcea85339c789328 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Fri, 24 Apr 2026 08:03:10 -0400 Subject: [PATCH 08/14] clear videoStallTimeout --- immichFrame.Web/src/lib/components/home-page/home-page.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index 25ae3f6c..f1cf7138 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -193,6 +193,7 @@ } finally { isHandlingAssetTransition = false; clearTimeout(watchdogTimer); + clearTimeout(videoStallTimeout); if (pendingTransition) { const next = pendingTransition; From 36bc89a7c971e282f8a881a64ab41e96db121841 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Fri, 24 Apr 2026 08:23:19 -0400 Subject: [PATCH 09/14] add transition epoch --- .../lib/components/home-page/home-page.svelte | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index f1cf7138..ac4598ce 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -155,6 +155,7 @@ } let isHandlingAssetTransition = $state(false); + let transitionEpoch = 0; let pendingTransition: { previous: boolean; instant: boolean } | null = $state(null); const handleDone = async (previous: boolean = false, instant: boolean = false) => { @@ -162,14 +163,15 @@ pendingTransition = { previous, instant }; return; } + + const currentEpoch = ++transitionEpoch; isHandlingAssetTransition = true; clearTimeout(watchdogTimer); clearTimeout(videoStallTimeout); - // Watchdog: If the transition (fetching/loading assets) takes longer than - // the current interval plus a 10s buffer, force-release the lock. + // Watchdog: If the transition (fetching/loading assets) hangs, force-release the lock. watchdogTimer = window.setTimeout(() => { - if (isHandlingAssetTransition) { + if (currentEpoch === transitionEpoch && isHandlingAssetTransition) { console.error('Transition watchdog triggered: Force-resetting lock due to hang'); isHandlingAssetTransition = false; @@ -179,7 +181,7 @@ handleDone(next.previous, next.instant); } } - }, (currentDuration * 1000) + TRANSITION_WATCHDOG_MS); + }, TRANSITION_WATCHDOG_MS); try { userPaused = false; @@ -188,17 +190,22 @@ if (previous) await getPreviousAssets(); else await getNextAssets(); await tick(); + + if (currentEpoch !== transitionEpoch) return; + await assetComponent?.play?.(); progressBar.play(); } finally { - isHandlingAssetTransition = false; - clearTimeout(watchdogTimer); - clearTimeout(videoStallTimeout); - - if (pendingTransition) { - const next = pendingTransition; - pendingTransition = null; - handleDone(next.previous, next.instant); + if (currentEpoch === transitionEpoch) { + isHandlingAssetTransition = false; + clearTimeout(watchdogTimer); + clearTimeout(videoStallTimeout); + + if (pendingTransition) { + const next = pendingTransition; + pendingTransition = null; + handleDone(next.previous, next.instant); + } } } }; From 827e3255d06f1b0ce228813ceeb07802ed23c8ea Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Fri, 24 Apr 2026 08:51:21 -0400 Subject: [PATCH 10/14] nitpick fixes --- .../src/lib/components/home-page/home-page.svelte | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index ac4598ce..528f4dad 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -44,9 +44,9 @@ let assetComponent: AssetComponentInstance = $state() as AssetComponentInstance; let currentDuration: number = $state($configStore.interval ?? 20); - let watchdogTimer: number | undefined = $state(); - let videoStallTimeout: number | undefined = $state(); - let timeoutId: number | undefined = $state(); + let watchdogTimer: number | undefined; + let videoStallTimeout: number | undefined; + let timeoutId: number | undefined; let userPaused: boolean = $state(false); @@ -199,7 +199,6 @@ if (currentEpoch === transitionEpoch) { isHandlingAssetTransition = false; clearTimeout(watchdogTimer); - clearTimeout(videoStallTimeout); if (pendingTransition) { const next = pendingTransition; From df9fce32e349b669bc43be324805975869fc8deb Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Fri, 24 Apr 2026 11:23:47 -0400 Subject: [PATCH 11/14] Stall-timer gating --- .../src/lib/components/home-page/home-page.svelte | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index 528f4dad..79c9b68b 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -515,15 +515,19 @@ onVideoWaiting={async () => { await progressBar.pause(); clearTimeout(videoStallTimeout); + if (userPaused) return; + videoStallTimeout = window.setTimeout(() => { - console.warn('Video stalled, skipping...'); - handleDone(false, true); - }, Math.min(VIDEO_STALL_MS, currentDuration * 1000)); + if (!userPaused) { + console.warn('Video stalled, skipping...'); + handleDone(false, true); + } + }, Math.max(5000, Math.min(VIDEO_STALL_MS, currentDuration * 1000))); }} onVideoPlaying={async () => { + clearTimeout(videoStallTimeout); if (!userPaused) { await progressBar.play(); - clearTimeout(videoStallTimeout); } }} onAssetError={async () => { From 4cc84cf5f91ce1f8e574a5554322df826ee6b7e7 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Fri, 24 Apr 2026 13:41:00 -0400 Subject: [PATCH 12/14] watchdog recovery, comments --- .../src/lib/components/elements/asset.svelte | 4 ++- .../components/elements/progress-bar.svelte | 1 + .../lib/components/home-page/home-page.svelte | 34 +++++++++++-------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/immichFrame.Web/src/lib/components/elements/asset.svelte b/immichFrame.Web/src/lib/components/elements/asset.svelte index fad28e55..09182bc0 100644 --- a/immichFrame.Web/src/lib/components/elements/asset.svelte +++ b/immichFrame.Web/src/lib/components/elements/asset.svelte @@ -54,8 +54,10 @@ let debug = false; const isVideo = $derived(isVideoAsset(asset[1])); + // Re-evaluate only when the asset changes; keep the interval stable for the + // lifetime of the current asset so zoom/pan animations don't restart. const animationDuration = $derived.by(() => { - asset[1].id; + void asset[1].id; return untrack(() => interval); }); diff --git a/immichFrame.Web/src/lib/components/elements/progress-bar.svelte b/immichFrame.Web/src/lib/components/elements/progress-bar.svelte index 24e28f2b..5e4b083f 100644 --- a/immichFrame.Web/src/lib/components/elements/progress-bar.svelte +++ b/immichFrame.Web/src/lib/components/elements/progress-bar.svelte @@ -57,6 +57,7 @@ export const pause = async () => { status = ProgressBarStatus.Paused; onPaused(); + // Freeze in place: targeting the current value with duration(to-from≈0) ≈ 0 halts motion. await progress.set(progress.current); }; diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index 79c9b68b..fbfb5677 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -43,11 +43,11 @@ let progressBar: ProgressBar = $state() as ProgressBar; let assetComponent: AssetComponentInstance = $state() as AssetComponentInstance; let currentDuration: number = $state($configStore.interval ?? 20); - + let watchdogTimer: number | undefined; let videoStallTimeout: number | undefined; let timeoutId: number | undefined; - + let userPaused: boolean = $state(false); let error: boolean = $state(false); @@ -166,7 +166,7 @@ const currentEpoch = ++transitionEpoch; isHandlingAssetTransition = true; - + clearTimeout(watchdogTimer); clearTimeout(videoStallTimeout); // Watchdog: If the transition (fetching/loading assets) hangs, force-release the lock. @@ -175,11 +175,12 @@ console.error('Transition watchdog triggered: Force-resetting lock due to hang'); isHandlingAssetTransition = false; - if (pendingTransition) { - const next = pendingTransition; - pendingTransition = null; - handleDone(next.previous, next.instant); - } + // Bump the epoch so the original (still-awaiting) transition becomes a no-op + // when/if it eventually resolves, and force a fresh advance. + transitionEpoch++; + const next = pendingTransition ?? { previous: false, instant: true }; + pendingTransition = null; + handleDone(next.previous, next.instant); } }, TRANSITION_WATCHDOG_MS); @@ -189,7 +190,7 @@ $instantTransition = instant; if (previous) await getPreviousAssets(); else await getNextAssets(); - await tick(); + await tick(); if (currentEpoch !== transitionEpoch) return; @@ -517,12 +518,15 @@ clearTimeout(videoStallTimeout); if (userPaused) return; - videoStallTimeout = window.setTimeout(() => { - if (!userPaused) { - console.warn('Video stalled, skipping...'); - handleDone(false, true); - } - }, Math.max(5000, Math.min(VIDEO_STALL_MS, currentDuration * 1000))); + videoStallTimeout = window.setTimeout( + () => { + if (!userPaused) { + console.warn('Video stalled, skipping...'); + handleDone(false, true); + } + }, + Math.max(5000, Math.min(VIDEO_STALL_MS, currentDuration * 1000)) + ); }} onVideoPlaying={async () => { clearTimeout(videoStallTimeout); From 6bf516f982710d7bb06e8274a8a05361dd821d57 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Sat, 25 Apr 2026 20:23:30 -0400 Subject: [PATCH 13/14] stop consecutive errors --- .../lib/components/home-page/home-page.svelte | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/immichFrame.Web/src/lib/components/home-page/home-page.svelte b/immichFrame.Web/src/lib/components/home-page/home-page.svelte index fbfb5677..454be1a0 100644 --- a/immichFrame.Web/src/lib/components/home-page/home-page.svelte +++ b/immichFrame.Web/src/lib/components/home-page/home-page.svelte @@ -44,6 +44,8 @@ let assetComponent: AssetComponentInstance = $state() as AssetComponentInstance; let currentDuration: number = $state($configStore.interval ?? 20); + let consecutiveErrorSkips = 0; + let errorSkipScheduled = false; let watchdogTimer: number | undefined; let videoStallTimeout: number | undefined; let timeoutId: number | undefined; @@ -180,7 +182,10 @@ transitionEpoch++; const next = pendingTransition ?? { previous: false, instant: true }; pendingTransition = null; - handleDone(next.previous, next.instant); + handleDone(next.previous, next.instant).catch((err) => { + console.error('handleDone failed:', err); + isHandlingAssetTransition = false; + }); } }, TRANSITION_WATCHDOG_MS); @@ -196,6 +201,7 @@ await assetComponent?.play?.(); progressBar.play(); + consecutiveErrorSkips = 0; } finally { if (currentEpoch === transitionEpoch) { isHandlingAssetTransition = false; @@ -204,7 +210,10 @@ if (pendingTransition) { const next = pendingTransition; pendingTransition = null; - handleDone(next.previous, next.instant); + handleDone(next.previous, next.instant).catch((err) => { + console.error('handleDone failed:', err); + isHandlingAssetTransition = false; + }); } } } @@ -529,13 +538,26 @@ ); }} onVideoPlaying={async () => { + consecutiveErrorSkips = 0; clearTimeout(videoStallTimeout); if (!userPaused) { await progressBar.play(); } }} onAssetError={async () => { + if (errorSkipScheduled) return; + errorSkipScheduled = true; + + consecutiveErrorSkips++; + if (consecutiveErrorSkips > 10) { + error = true; + errorMessage = 'Too many consecutive asset load failures. Please check your network or server connection.'; + errorSkipScheduled = false; + return; + } + await handleDone(false, true); + errorSkipScheduled = false; }} /> From a1a22406fc27696ddc6327e7e7ff60bf76c31907 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Mon, 27 Apr 2026 10:51:47 -0400 Subject: [PATCH 14/14] progress tween fix --- immichFrame.Web/src/lib/components/elements/progress-bar.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/immichFrame.Web/src/lib/components/elements/progress-bar.svelte b/immichFrame.Web/src/lib/components/elements/progress-bar.svelte index 5e4b083f..aba08d02 100644 --- a/immichFrame.Web/src/lib/components/elements/progress-bar.svelte +++ b/immichFrame.Web/src/lib/components/elements/progress-bar.svelte @@ -25,7 +25,7 @@ onPaused = () => {} }: Props = $props(); - const progress = new Tween(0, { + const progress = new Tween(0, { duration: (from: number, to: number) => { if (to === 0) return 0; return duration * 1000 * (to - from);