Skip to content

fix: asset transition/video stall freezes#637

Open
3rob3 wants to merge 15 commits into
mainfrom
dev.3rob3.VideoFreeze
Open

fix: asset transition/video stall freezes#637
3rob3 wants to merge 15 commits into
mainfrom
dev.3rob3.VideoFreeze

Conversation

@3rob3
Copy link
Copy Markdown
Collaborator

@3rob3 3rob3 commented Apr 24, 2026

Closes #601

Summary by CodeRabbit

  • New Features

    • Added an asset error callback so apps can react to media load/playback failures.
    • Progress completion callback now accepts async handlers.
  • Bug Fixes

    • Auto-skip handles stalled videos and asset errors with throttling and escalation.
    • Improved timeout, watchdog and teardown behavior to avoid hangs and ensure clean cleanup.
  • Refactor

    • Stabilized progress/animation timing and simplified transition locking and resource cleanup.

Review Change Stack

@3rob3 3rob3 added the bug Something isn't working label Apr 24, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2e838912-e8df-4f7f-8fe9-10729818d851

📥 Commits

Reviewing files that changed from the base of the PR and between a1a2240 and d3e42b0.

📒 Files selected for processing (1)
  • immichFrame.Web/src/lib/components/elements/progress-bar.svelte
🚧 Files skipped from review as they are similar to previous changes (1)
  • immichFrame.Web/src/lib/components/elements/progress-bar.svelte

📝 Walkthrough

Walkthrough

Replaces the reactive tweened store with an imperative Tween<number> in the progress bar, widens onDone to allow Promise returns, adds a guarded effect to call onDone() once at completion, updates pause logic to freeze the tween, and binds the template to progress.current.

Changes

Progress bar

Layer / File(s) Summary
Imports and API surface
immichFrame.Web/src/lib/components/elements/progress-bar.svelte
Switch motion import from tweened to Tween and update onDone prop type to `() => void
Core tween behavior and completion handling
immichFrame.Web/src/lib/components/elements/progress-bar.svelte
Instantiate Tween<number> for progress, add completed-guarded $effect to call onDone() when progress.current === 1, and wrap the callback in Promise.resolve(...).catch(...).
Pause and template bindings
immichFrame.Web/src/lib/components/elements/progress-bar.svelte
Change pause() to retarget the tween to progress.current and update the progress bar width binding to use progress.current.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

fix

Poem

🐰 I nudged the tween to march on time,

Progress now hums, no more frozen rhyme.
When bars once stalled, they now resume the song,
One guarded call keeps completion strong.
Hooray — the slideshow hops along!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: asset transition/video stall freezes' directly matches the PR's primary objective of fixing application hangs during media transitions and video playback.
Linked Issues check ✅ Passed The code changes address all primary objectives from #601: detect/recover from stuck media via onAssetError callback [asset-component, asset], reset/continue timers on media failures [progress-bar, home-page], and improve error handling with throttling and watchdog mechanisms [home-page].
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing media transition/video stall hangs: onAssetError error hook, animationDuration stability, tween-based progress imperative management, and epoch-based transition locking with watchdog timer.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev.3rob3.VideoFreeze

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
immichFrame.Web/src/lib/components/elements/progress-bar.svelte (1)

57-61: pause() via set(progress.current) — works, but worth a brief comment.

Freezing by setting the target to the current animated value relies on the duration callback returning ~0 when to ≈ from. It's subtle enough that a one-line comment would help future maintainers (and prevents someone from "simplifying" the duration function to always return duration * 1000 * (to - from), which would break the reset path).

📝 Optional clarifying comment
 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);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@immichFrame.Web/src/lib/components/elements/progress-bar.svelte` around lines
57 - 61, Add a one-line explanatory comment inside the pause() function above
the call to progress.set(progress.current) clarifying that freezing animation
relies on the duration callback returning ~0 when to ≈ from, and that this
intentional behavior must be preserved (i.e., avoid refactoring the duration
calculation to a form like duration * 1000 * (to - from) which would break the
reset/pause behavior); reference the pause() function and the
progress.set(progress.current) call so future maintainers understand why the
target is explicitly set to the current value.
immichFrame.Web/src/lib/components/elements/asset.svelte (1)

57-60: Add a comment explaining the untrack pattern and use void to avoid unused expression warnings.

The code reads asset[1].id to establish it as a dependency (triggering re-evaluation when the asset changes) while using untrack(() => interval) to exempt interval updates from re-triggering the derived. This ensures the interval value is snapshotted only when the asset changes, preventing CSS animations from restarting mid-transition. Add a comment explaining this intent and prefix asset[1].id with void to prevent ESLint from flagging the unused expression:

Suggested change
+	// 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);
 	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@immichFrame.Web/src/lib/components/elements/asset.svelte` around lines 57 -
60, Add an inline comment above the animationDuration derived explaining the
untrack pattern: that we intentionally read asset[1].id solely to establish a
dependency so the derived recomputes when the asset changes while using
untrack(() => interval) to snapshot the interval and prevent interval updates
from restarting CSS animations mid-transition; also change the bare expression
to use void asset[1].id to avoid ESLint/unused-expression warnings and keep the
untrack usage as-is. Reference: animationDuration, asset, untrack, interval.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@immichFrame.Web/src/lib/components/home-page/home-page.svelte`:
- Around line 533-535: The onAssetError handler is causing unbounded rapid skips
and double-skips in split view; modify the onAssetError logic (the inline
handler that currently calls handleDone(false, true)) to debounce/sketch one
skip per render cycle and count consecutive error skips before triggering
further action: introduce a module-level flag (e.g., assetErrorPending) or a
per-render timestamp to ignore additional onAssetError calls until the current
transition completes, increment a consecutiveErrorSkips counter when a skip is
performed, reset consecutiveErrorSkips to 0 in successful playback/render
handlers such as onVideoPlaying, and when consecutiveErrorSkips exceeds a
threshold surface an error state instead of calling handleDone or back off calls
to loadAssets; ensure pendingTransition and handleDone integrations respect the
new guard so a second onerror in the same tick cannot queue a duplicate skip.
- Around line 170-184: The watchdog currently only resets
isHandlingAssetTransition and hands off to pendingTransition, leaving the
autoplay progress paused when pendingTransition is null; change the watchdog
timeout handler (watchdogTimer callback that checks currentEpoch,
transitionEpoch, isHandlingAssetTransition and uses
pendingTransition/handleDone) so that if pendingTransition is null it also
forces the slideshow to resume/advance (e.g., trigger progressBar.play() or
progressBar.restart(true) / invoke the same code path that advances the slide)
instead of returning immediately; additionally, add timeout
support/AbortController to the underlying api.getAssets and api.getAsset calls
and ensure getNextAssets accepts and propagates an AbortSignal so hung fetches
are actively rejected and the finally blocks (and isHandlingAssetTransition
cleanup) can run.

---

Nitpick comments:
In `@immichFrame.Web/src/lib/components/elements/asset.svelte`:
- Around line 57-60: Add an inline comment above the animationDuration derived
explaining the untrack pattern: that we intentionally read asset[1].id solely to
establish a dependency so the derived recomputes when the asset changes while
using untrack(() => interval) to snapshot the interval and prevent interval
updates from restarting CSS animations mid-transition; also change the bare
expression to use void asset[1].id to avoid ESLint/unused-expression warnings
and keep the untrack usage as-is. Reference: animationDuration, asset, untrack,
interval.

In `@immichFrame.Web/src/lib/components/elements/progress-bar.svelte`:
- Around line 57-61: Add a one-line explanatory comment inside the pause()
function above the call to progress.set(progress.current) clarifying that
freezing animation relies on the duration callback returning ~0 when to ≈ from,
and that this intentional behavior must be preserved (i.e., avoid refactoring
the duration calculation to a form like duration * 1000 * (to - from) which
would break the reset/pause behavior); reference the pause() function and the
progress.set(progress.current) call so future maintainers understand why the
target is explicitly set to the current value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0c820c13-a70a-4cc6-a0cc-3145d543ae9c

📥 Commits

Reviewing files that changed from the base of the PR and between 265a890 and df9fce3.

📒 Files selected for processing (4)
  • immichFrame.Web/src/lib/components/elements/asset-component.svelte
  • immichFrame.Web/src/lib/components/elements/asset.svelte
  • immichFrame.Web/src/lib/components/elements/progress-bar.svelte
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte

Comment thread immichFrame.Web/src/lib/components/home-page/home-page.svelte
Comment thread immichFrame.Web/src/lib/components/home-page/home-page.svelte
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
immichFrame.Web/src/lib/components/elements/progress-bar.svelte (1)

51-62: ⚠️ Potential issue | 🔴 Critical

Interrupted tween promises never resolve — play()/pause() can leave awaiters hung, skipping critical cleanup.

Per the Svelte motion docs, when a Tween.set(...) is interrupted by another set(...), the interrupted call's promise never resolves. This causes a problem:

  • play() awaits progress.set(1). When pause() or restart(...) is later invoked, that tween is interrupted and the play() promise hangs permanently.
  • pause() awaits progress.set(progress.current). When interrupted, the promise hangs.
  • In home-page.svelte, callers like onVideoWaiting have critical cleanup code after the await:
    await progressBar.pause();
    clearTimeout(videoStallTimeout);
    
    If pause() is interrupted (e.g., by restart() or play()), the clearTimeout never executes, leaving the stall timeout active and causing timing bugs.

The suggested fix is correct: remove the await from the tween call and let completion be signaled via the existing onDone effect instead.

Suggested fix
 export const play = async () => {
   status = ProgressBarStatus.Playing;
   onPlaying();
-  await progress.set(1);
+  progress.set(1);
 };

 export const pause = async () => {
   status = ProgressBarStatus.Paused;
   onPaused();
-  await progress.set(progress.current);
+  progress.set(progress.current);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@immichFrame.Web/src/lib/components/elements/progress-bar.svelte` around lines
51 - 62, The play()/pause() functions currently await progress.set(...) which
can hang if that tween is interrupted (e.g., by pause(), play(), or restart()),
so remove the await from the progress.set calls in play and pause (keep status
updates and onPlaying/onPaused calls), and rely on the existing onDone effect to
signal completion instead of awaiting the tween; ensure play uses
progress.set(1) without await and pause uses progress.set(progress.current)
without await so callers do not get permanently stuck.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@immichFrame.Web/src/lib/components/home-page/home-page.svelte`:
- Line 183: The fire-and-forget calls to handleDone(...) in the watchdog
callback and the finally-drain are causing unhandled promise rejections because
handleDone can throw; update both call sites (the watchdog recovery caller and
the finally-drain caller) to either await handleDone(...) or attach a
.catch(...) to consume/log errors so rejections don’t surface as unhandled and
to ensure the transition lock and retry logic behave correctly; locate the
invocations of handleDone(next.previous, next.instant) and the finally-block
invocation (both currently invoked without awaiting) and change them to handle
the returned promise (await or .catch with proper logging using the existing
logger/context).

---

Outside diff comments:
In `@immichFrame.Web/src/lib/components/elements/progress-bar.svelte`:
- Around line 51-62: The play()/pause() functions currently await
progress.set(...) which can hang if that tween is interrupted (e.g., by pause(),
play(), or restart()), so remove the await from the progress.set calls in play
and pause (keep status updates and onPlaying/onPaused calls), and rely on the
existing onDone effect to signal completion instead of awaiting the tween;
ensure play uses progress.set(1) without await and pause uses
progress.set(progress.current) without await so callers do not get permanently
stuck.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cfc61a12-96ea-4db2-8f11-e6e8d1457d7a

📥 Commits

Reviewing files that changed from the base of the PR and between df9fce3 and 4cc84cf.

📒 Files selected for processing (3)
  • immichFrame.Web/src/lib/components/elements/asset.svelte
  • immichFrame.Web/src/lib/components/elements/progress-bar.svelte
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte
🚧 Files skipped from review as they are similar to previous changes (1)
  • immichFrame.Web/src/lib/components/elements/asset.svelte

Comment thread immichFrame.Web/src/lib/components/home-page/home-page.svelte Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
immichFrame.Web/src/lib/components/elements/progress-bar.svelte (1)

28-33: Clamp computed tween duration to avoid negative-time transitions.

At Line 31, duration * 1000 * (to - from) can go negative when to < from (possible around pause/resume frame timing). Clamp to non-negative for stable behavior.

Proposed patch
 const progress = new Tween<number>(0, {
 	duration: (from: number, to: number) => {
 		if (to === 0) return 0;
-		return duration * 1000 * (to - from);
+		const delta = Math.max(0, to - from);
+		return Math.max(0, duration) * 1000 * delta;
 	}
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@immichFrame.Web/src/lib/components/elements/progress-bar.svelte` around lines
28 - 33, The computed duration for the Tween<number> assigned to progress can
become negative when to < from; update the duration callback in the progress
Tween (the anonymous function using parameters from and to) to clamp the
computed value to a non-negative number (e.g., wrap duration * 1000 * (to -
from) with Math.max(0, ...)) so the Tween never receives a negative time.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@immichFrame.Web/src/lib/components/elements/progress-bar.svelte`:
- Around line 37-40: The call to onDone() inside progress-bar.svelte is invoked
fire-and-forget via untrack(() => onDone()) and can produce unhandled promise
rejections when the handler (e.g., handleDone which calls loadAsset() /
assetComponent.play()) rejects; update the invocation to capture the returned
promise and attach a rejection handler so rejections are handled (e.g., call
onDone(), check if it returns a Promise, and call .catch(...) or use void
onDone().catch(...)) while keeping it wrapped in untrack; refer to
progress.current, completed, untrack and onDone to locate the spot to change.

---

Nitpick comments:
In `@immichFrame.Web/src/lib/components/elements/progress-bar.svelte`:
- Around line 28-33: The computed duration for the Tween<number> assigned to
progress can become negative when to < from; update the duration callback in the
progress Tween (the anonymous function using parameters from and to) to clamp
the computed value to a non-negative number (e.g., wrap duration * 1000 * (to -
from) with Math.max(0, ...)) so the Tween never receives a negative time.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 22f3240e-fe7a-41ad-a91c-dc60dbfdc9c0

📥 Commits

Reviewing files that changed from the base of the PR and between 6bf516f and a1a2240.

📒 Files selected for processing (1)
  • immichFrame.Web/src/lib/components/elements/progress-bar.svelte

Comment thread immichFrame.Web/src/lib/components/elements/progress-bar.svelte
@JW-CH JW-CH self-requested a review May 18, 2026 07:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stops Showing Media

1 participant