Skip to content

Fix video wallpaper sync and streamline updater UI#366

Merged
webadderall merged 9 commits intomainfrom
codex/video-wallpaper-sync-update-preview
Apr 27, 2026
Merged

Fix video wallpaper sync and streamline updater UI#366
webadderall merged 9 commits intomainfrom
codex/video-wallpaper-sync-update-preview

Conversation

@webadderall
Copy link
Copy Markdown
Collaborator

@webadderall webadderall commented Apr 27, 2026

What changed

  • fixed custom and bundled video wallpaper resolution so local video backgrounds preview and export through the correct video path
  • kept video wallpaper playback synced to the active clip playhead in preview, including seeks and speed-region playback changes
  • limited the Preview Update UI launcher action to development builds only
  • removed manual updater surfaces for "already checking" and "up to date" so update UI only appears when there is an actionable update state
  • added focused coverage for the new video asset URL resolver

Why

Custom video wallpapers were being routed through image-oriented asset handling, which could break preview/export behavior and let the wallpaper drift away from the main clip timeline. The updater flow also surfaced non-actionable UI during manual checks.

Impact

  • video backgrounds should now stay visually aligned with the clip timestamp during preview and export
  • local uploaded video wallpapers should resolve more reliably
  • production users will not see the dev-only update preview action
  • update UI stays quieter unless a real update is available or being downloaded

Root cause

Video wallpaper sources were not consistently treated as video resources end-to-end, and preview sync only hard-corrected wallpaper time during paused scrubbing instead of continuously following the clip timeline.

Validation

  • ./node_modules/.bin/tsc --noEmit
  • attempted targeted Vitest runs, but this checkout currently hits a local Rollup native-module loading issue (@rollup/rollup-darwin-arm64) before tests execute

Summary by CodeRabbit

  • New Features

    • Added a development-only menu option to preview update notifications
  • Improvements

    • Removed redundant "no updates" / "check in progress" message boxes
    • Better synchronization and playback behavior for video wallpapers with the timeline
    • Improved video wallpaper previews in Settings and more reliable handling during export
    • Adjusted timeline sizing/scroll behavior for a more consistent editor layout

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 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
📝 Walkthrough

Walkthrough

Refactors video wallpaper URL resolution and playback sync, updates exporter to treat video wallpapers as first-class media, removes informational update dialogs, and adds a dev-only update-toast preview option.

Changes

Cohort / File(s) Summary
Updater
electron/updater.ts
Removes the "No Updates Available" dialog and stops showing informational dialogs for checks already in progress; error dialogs and toast updates unchanged.
Dev UI
src/components/launch/LaunchWindow.tsx
Adds a development-only "preview update toast" menu item that calls electronAPI.previewUpdateToast() and logs failures; only rendered when import.meta.env.DEV.
Asset URL Utilities & Tests
src/lib/assetPath.ts, src/lib/assetPath.test.ts
Adds absolute-local-path detection and encoding (toFileUrl, isAbsoluteLocalAssetPath) plus getRenderableVideoUrl and getExportableVideoUrl; tests for local, bundled, and URL-like inputs.
Video Preview Component
src/components/video-editor/SettingsPanel.tsx
Replaces inline video preview with a new WallpaperVideoPreview component and switches previews to use getRenderableVideoUrl.
Playback Sync
src/components/video-editor/VideoPlayback.tsx
Replaces prior wallpaper sync with continuous media-locking: computes targetTime, selects SpeedRegion, derives playbackRate via getMediaSyncPlaybackRate, smooths rate, and snaps time on jumps/drift; centralizes URL resolution via getRenderableVideoUrl.
Exporter: frameRenderer / modernFrameRenderer
src/lib/exporter/frameRenderer.ts, src/lib/exporter/modernFrameRenderer.ts
Treats video wallpapers as special-case: resolves via getExportableVideoUrl, stages background video frames separately, loads <video> elements with readiness waiting, and manages revoke/cleanup callbacks.
Exporter Tests
src/lib/exporter/frameRenderer.test.ts, src/lib/exporter/modernFrameRenderer.test.ts
Updates mocks to include async getExportableVideoUrl that returns the input string.
Timeline Layout & Tests
src/components/video-editor/timeline/timelineLayout.ts, src/components/video-editor/timeline/timelineLayout.test.ts, src/components/video-editor/timeline/TimelineEditor.tsx, src/components/video-editor/timeline/TimelineWrapper.tsx
Adds TIMELINE_VISIBLE_ROW_COUNT and getTimelineViewportStretchFactor, adjusts container sizing to use computed height and h-full min-h-0 to refine scrolling/layout behavior; includes tests for stretch factor.

Sequence Diagram(s)

sequenceDiagram
    participant Timeline
    participant VideoPlayback
    participant SyncRate as getMediaSyncPlaybackRate
    participant BgVideo as BackgroundVideoElement

    Timeline->>VideoPlayback: timeline time / play state update
    VideoPlayback->>VideoPlayback: compute targetTime, select SpeedRegion
    VideoPlayback->>SyncRate: request interpolated playback rate
    SyncRate-->>VideoPlayback: interpolated playbackRate
    VideoPlayback->>BgVideo: smoothly set playbackRate
    alt jump or drift > threshold
        VideoPlayback->>BgVideo: snap currentTime to targetTime
    end
    alt isPlaying toggled
        VideoPlayback->>BgVideo: play() / pause()
    end
    BgVideo-->>VideoPlayback: render frames (synchronized)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

Checked

🐰 I hopped through code with nimble paws,
Resolved video paths and smoothed playback laws.
Dev toasts, exporters, and timelines aligned,
Background frames tidy, no drift left behind. 🎥✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: video wallpaper synchronization improvements and updater UI streamlining.
Description check ✅ Passed The PR description is thorough, covering what changed, motivation, impact, root cause, and validation attempts, though it lacks formal template structure with checkboxes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/video-wallpaper-sync-update-preview

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

@webadderall webadderall marked this pull request as ready for review April 27, 2026 05:52
@webadderall webadderall changed the title [codex] Fix video wallpaper sync and streamline updater UI Fix video wallpaper sync and streamline updater UI Apr 27, 2026
Copy link
Copy Markdown
Contributor

@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 (1)
src/components/launch/LaunchWindow.tsx (1)

1476-1486: Handle possible IPC rejection when triggering preview toast.

At Line 1480, the fire-and-forget call can produce an unhandled rejection if the invoke fails; worth handling explicitly for cleaner renderer logs.

Suggested tweak
  onClick={() => {
-   void window.electronAPI.previewUpdateToast();
+   void window.electronAPI.previewUpdateToast().catch((error) => {
+     console.warn("Failed to preview update toast:", error);
+   });
    setActiveDropdown("none");
  }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/launch/LaunchWindow.tsx` around lines 1476 - 1486, The
fire-and-forget call to window.electronAPI.previewUpdateToast inside the
SHOW_DEV_UPDATE_PREVIEW DropdownItem can produce an unhandled rejection; change
the onClick to call previewUpdateToast() and explicitly handle failures (e.g.
window.electronAPI.previewUpdateToast().catch(err => console.error(...))) so
errors are logged, and ensure setActiveDropdown("none") still runs regardless of
the promise outcome; locate the handler around the DropdownItem using
SHOW_DEV_UPDATE_PREVIEW, window.electronAPI.previewUpdateToast, and
setActiveDropdown to implement the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/video-editor/SettingsPanel.tsx`:
- Around line 158-177: When src changes the effect currently waits for
getRenderableVideoUrl to finish and leaves the previous resolvedSrc leading to
stale preview frames; update the useEffect (the effect that references src,
getRenderableVideoUrl, setResolvedSrc and the cancelled flag) to immediately
reset resolvedSrc (call setResolvedSrc(undefined or empty value appropriate for
your state type) at the start of the effect) before starting the async
resolution so the UI clears the old preview while the new URL is being resolved;
keep the existing try/catch and cancelled logic for updating resolvedSrc with
the resolved value or fallback.

In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 1017-1019: The jump-detection logic uses the wrapped targetTime
(computed with currentTime % videoDuration) which drops from ~videoDuration to
~0 at loop boundaries and causes false positives when compared to
previousTimelineTime; change the detection to use an unwrapped timeline position
for comparison (e.g., track a monotonicTimeline or rawCurrentTime variable
derived from currentTime before modulo) or explicitly detect and ignore
wrap-around by checking if videoDuration is truthy and the difference crosses
the boundary, then only compute the display/seek target using targetTime
(currentTime % videoDuration or clampMediaTimeToDuration) while using the
unwrapped value (rawCurrentTime or monotonicTimeline) for comparing against
previousTimelineTime in the jump-detection block.

---

Nitpick comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 1476-1486: The fire-and-forget call to
window.electronAPI.previewUpdateToast inside the SHOW_DEV_UPDATE_PREVIEW
DropdownItem can produce an unhandled rejection; change the onClick to call
previewUpdateToast() and explicitly handle failures (e.g.
window.electronAPI.previewUpdateToast().catch(err => console.error(...))) so
errors are logged, and ensure setActiveDropdown("none") still runs regardless of
the promise outcome; locate the handler around the DropdownItem using
SHOW_DEV_UPDATE_PREVIEW, window.electronAPI.previewUpdateToast, and
setActiveDropdown to implement the fix.
🪄 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 Plus

Run ID: e858cf40-2f7e-450f-99d8-1a89d8860d33

📥 Commits

Reviewing files that changed from the base of the PR and between f58cd07 and bbd46b4.

📒 Files selected for processing (8)
  • electron/updater.ts
  • src/components/launch/LaunchWindow.tsx
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/lib/assetPath.test.ts
  • src/lib/assetPath.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/modernFrameRenderer.ts
💤 Files with no reviewable changes (1)
  • electron/updater.ts

Comment on lines +158 to +177
useEffect(() => {
let cancelled = false;

void (async () => {
try {
const nextSrc = await getRenderableVideoUrl(src);
if (!cancelled) {
setResolvedSrc(nextSrc);
}
} catch {
if (!cancelled) {
setResolvedSrc(src);
}
}
})();

return () => {
cancelled = true;
};
}, [src]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reset resolvedSrc immediately on src changes to avoid stale preview frames.

Line 156/Line 158 currently keeps the old resolved URL until async resolution finishes, which can briefly show the wrong tile video after list updates/reordering.

Suggested patch
 function WallpaperVideoPreview({ src }: { src: string }) {
 	const [resolvedSrc, setResolvedSrc] = useState(src);

 	useEffect(() => {
 		let cancelled = false;
+		setResolvedSrc(src);

 		void (async () => {
 			try {
 				const nextSrc = await getRenderableVideoUrl(src);
 				if (!cancelled) {
 					setResolvedSrc(nextSrc);
 				}
 			} catch {
 				if (!cancelled) {
 					setResolvedSrc(src);
 				}
 			}
 		})();

 		return () => {
 			cancelled = true;
 		};
 	}, [src]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const nextSrc = await getRenderableVideoUrl(src);
if (!cancelled) {
setResolvedSrc(nextSrc);
}
} catch {
if (!cancelled) {
setResolvedSrc(src);
}
}
})();
return () => {
cancelled = true;
};
}, [src]);
useEffect(() => {
let cancelled = false;
setResolvedSrc(src);
void (async () => {
try {
const nextSrc = await getRenderableVideoUrl(src);
if (!cancelled) {
setResolvedSrc(nextSrc);
}
} catch {
if (!cancelled) {
setResolvedSrc(src);
}
}
})();
return () => {
cancelled = true;
};
}, [src]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/SettingsPanel.tsx` around lines 158 - 177, When
src changes the effect currently waits for getRenderableVideoUrl to finish and
leaves the previous resolvedSrc leading to stale preview frames; update the
useEffect (the effect that references src, getRenderableVideoUrl, setResolvedSrc
and the cancelled flag) to immediately reset resolvedSrc (call
setResolvedSrc(undefined or empty value appropriate for your state type) at the
start of the effect) before starting the async resolution so the UI clears the
old preview while the new URL is being resolved; keep the existing try/catch and
cancelled logic for updating resolvedSrc with the resolved value or fallback.

Comment thread src/components/video-editor/VideoPlayback.tsx Outdated
Copy link
Copy Markdown
Contributor

@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.

Caution

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

⚠️ Outside diff range comments (1)
src/lib/exporter/modernFrameRenderer.ts (1)

339-353: ⚠️ Potential issue | 🟠 Major

Missing destroy-time cleanup for new background resources

Line 339 and Line 352-353 add long-lived background resources, but destroy() does not revoke cleanupBackgroundSource or clear the background staging canvas/context. This can leak blob URLs/memory across exporter instances.

💡 Suggested fix
 destroy(): void {
+		this.cleanupBackgroundSource?.();
+		this.cleanupBackgroundSource = null;
+
 		this.closeBackgroundDecodedFrame();
 		this.backgroundForwardFrameSource?.cancel();
 		void this.backgroundForwardFrameSource?.destroy();
 		this.backgroundForwardFrameSource = null;
@@
 		this.sceneVideoFrameStagingCanvas = null;
 		this.sceneVideoFrameStagingCtx = null;
+		this.backgroundVideoFrameStagingCanvas = null;
+		this.backgroundVideoFrameStagingCtx = null;
 		this.webcamVideoFrameStagingCanvas = null;
 		this.webcamVideoFrameStagingCtx = null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/exporter/modernFrameRenderer.ts` around lines 339 - 353, The new
background resources cleanupBackgroundSource, backgroundVideoFrameStagingCanvas
and backgroundVideoFrameStagingCtx must be released in destroy(): if
cleanupBackgroundSource is set, call it and then set cleanupBackgroundSource =
null; if backgroundVideoFrameStagingCanvas exists, clear it (e.g., set
width/height = 0 or clearRect) and set backgroundVideoFrameStagingCtx = null and
backgroundVideoFrameStagingCanvas = null; ensure any related blob URLs or
video-related sources are revoked in that same block to prevent memory/URL
leaks; add these steps to the existing destroy() method next to other resource
teardown so the exporter fully releases background resources.
🧹 Nitpick comments (1)
src/lib/exporter/modernFrameRenderer.ts (1)

1477-1485: Consider handling background startup-staging mode transitions like scene/webcam

Line 1477-1485 now stages background frames, but background texture updates do not track staging mode transitions (canvas ↔ decoded frame) the way scene/webcam do. This asymmetry risks stale/incorrect texture updates when startup staging turns off.

♻️ Suggested direction
+	private backgroundTextureUsesStartupStaging = false;
@@
-				const resolvedBackgroundSource = this.stageVideoFrameForTexture(
+				const resolvedBackgroundSource = this.stageVideoFrameForTexture(
 					decodedFrame,
 					"background",
 					decodedFrame.displayWidth,
 					decodedFrame.displayHeight,
 				);
+				const usesStartupStaging = resolvedBackgroundSource !== decodedFrame;
 				this.ensureBackgroundSprite(
 					resolvedBackgroundSource,
 					decodedFrame.displayWidth,
 					decodedFrame.displayHeight,
+					usesStartupStaging,
 				);

And mirror the scene/webcam swap logic inside ensureBackgroundSprite(...) (replace texture when mode flips).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/exporter/modernFrameRenderer.ts` around lines 1477 - 1485, The
background path now stages frames via stageVideoFrameForTexture and calls
ensureBackgroundSprite, but ensureBackgroundSprite does not detect or handle
startup staging mode transitions (canvas ↔ decoded frame) the way the
scene/webcam code does, which can leave an old texture in place; update
ensureBackgroundSprite to mirror the scene/webcam swap logic: detect when the
resolvedBackgroundSource type/mode has changed compared to the current
background sprite's texture source and replace the sprite's texture (dispose old
texture if needed) when a flip between canvas and decoded frame occurs, ensuring
stageVideoFrameForTexture+ensureBackgroundSprite together perform the same
mode-aware texture swap behavior as scene/webcam.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/lib/exporter/modernFrameRenderer.ts`:
- Around line 339-353: The new background resources cleanupBackgroundSource,
backgroundVideoFrameStagingCanvas and backgroundVideoFrameStagingCtx must be
released in destroy(): if cleanupBackgroundSource is set, call it and then set
cleanupBackgroundSource = null; if backgroundVideoFrameStagingCanvas exists,
clear it (e.g., set width/height = 0 or clearRect) and set
backgroundVideoFrameStagingCtx = null and backgroundVideoFrameStagingCanvas =
null; ensure any related blob URLs or video-related sources are revoked in that
same block to prevent memory/URL leaks; add these steps to the existing
destroy() method next to other resource teardown so the exporter fully releases
background resources.

---

Nitpick comments:
In `@src/lib/exporter/modernFrameRenderer.ts`:
- Around line 1477-1485: The background path now stages frames via
stageVideoFrameForTexture and calls ensureBackgroundSprite, but
ensureBackgroundSprite does not detect or handle startup staging mode
transitions (canvas ↔ decoded frame) the way the scene/webcam code does, which
can leave an old texture in place; update ensureBackgroundSprite to mirror the
scene/webcam swap logic: detect when the resolvedBackgroundSource type/mode has
changed compared to the current background sprite's texture source and replace
the sprite's texture (dispose old texture if needed) when a flip between canvas
and decoded frame occurs, ensuring
stageVideoFrameForTexture+ensureBackgroundSprite together perform the same
mode-aware texture swap behavior as scene/webcam.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e7f5c1eb-53c8-46c6-b15d-bf46b1cad21a

📥 Commits

Reviewing files that changed from the base of the PR and between b4b009f and f144907.

📒 Files selected for processing (6)
  • src/lib/assetPath.test.ts
  • src/lib/assetPath.ts
  • src/lib/exporter/frameRenderer.test.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/modernFrameRenderer.test.ts
  • src/lib/exporter/modernFrameRenderer.ts
✅ Files skipped from review due to trivial changes (2)
  • src/lib/exporter/frameRenderer.test.ts
  • src/lib/exporter/modernFrameRenderer.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/lib/assetPath.test.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/assetPath.ts

@webadderall
Copy link
Copy Markdown
Collaborator Author

@CodeRabbit Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@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.

Caution

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

⚠️ Outside diff range comments (2)
src/lib/exporter/modernFrameRenderer.ts (2)

339-353: ⚠️ Potential issue | 🟠 Major

Revoke the new background resources on every teardown path.

Line 865 stores a revoker for local wallpaper blob URLs, but today that callback is only consumed from resetBackgroundLayer(). If the initial load falls into the fallback path, or the renderer goes straight to destroy(), the blob URL stays alive. The new background staging canvas/context at Lines 352-353 also never get cleared in destroy(), unlike the scene/webcam staging buffers. This will accumulate memory across exports with local video wallpapers.

♻️ Suggested cleanup
 		} catch (error) {
+			this.cleanupBackgroundSource?.();
+			this.cleanupBackgroundSource = null;
 			console.error("[FrameRenderer] Error setting up background, using fallback:", error);
 			const fallback = document.createElement("canvas");
 			fallback.width = this.config.width;
@@
 		if (this.backgroundVideoElement) {
 			this.backgroundVideoElement.pause();
 			this.backgroundVideoElement.src = "";
 			this.backgroundVideoElement.load();
 			this.backgroundVideoElement = null;
 		}
+		this.cleanupBackgroundSource?.();
+		this.cleanupBackgroundSource = null;
@@
 		this.sceneVideoFrameStagingCanvas = null;
 		this.sceneVideoFrameStagingCtx = null;
+		this.backgroundVideoFrameStagingCanvas = null;
+		this.backgroundVideoFrameStagingCtx = null;
 		this.webcamVideoFrameStagingCanvas = null;
 		this.webcamVideoFrameStagingCtx = null;

Also applies to: 833-834, 865-867

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/exporter/modernFrameRenderer.ts` around lines 339 - 353, The
cleanupBackgroundSource revoker and the background staging buffers are not
always torn down — ensure any stored revoker in cleanupBackgroundSource is
invoked and cleared on every teardown path (including destroy() and any
background-load fallback paths), and null out/cleanup
backgroundVideoFrameStagingCanvas and backgroundVideoFrameStagingCtx just like
the scene/webcam staging buffers; update resetBackgroundLayer(), destroy(), and
any fallback branch that sets the local wallpaper to call the revoker (if
present) and then set cleanupBackgroundSource = null, and set
backgroundVideoFrameStagingCanvas = null and backgroundVideoFrameStagingCtx =
null to prevent leaked blob URLs and canvases.

859-898: ⚠️ Potential issue | 🟠 Major

Synchronize background wallpaper frames using the same presented-frame barrier as webcams.

Lines 859-898 create an HTMLVideoElement for video wallpapers, routed to syncBackgroundFrame(). However, this function (lines 1477-1501) waits only for the seeked event before calling ensureBackgroundSprite(). The seeked event does not guarantee the new frame is ready to draw—around seeks and speed transitions, this can export the previous wallpaper frame for one tick.

syncWebcamFrame() (lines 1921-1960+) already addresses this by calling waitForPresentedFrame() after seek completion, which uses requestVideoFrameCallback and requestAnimationFrame to ensure the frame is presented before proceeding. Apply the same pattern to syncBackgroundFrame() for consistency and correctness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/exporter/modernFrameRenderer.ts` around lines 859 - 898,
syncBackgroundFrame currently proceeds after the video `seeked`/loaded event
which can still yield the previous frame; update syncBackgroundFrame to mirror
syncWebcamFrame by calling the existing waitForPresentedFrame(video) (the helper
that uses requestVideoFrameCallback + requestAnimationFrame) after seek
completion and before calling ensureBackgroundSprite(), so the wallpaper frame
is guaranteed presented; locate syncBackgroundFrame and replace the
post-seek/loaded completion path to await waitForPresentedFrame(video) (or
equivalent) rather than relying solely on the seeked event, ensuring
ensureBackgroundSprite() runs only after waitForPresentedFrame resolves.
🧹 Nitpick comments (2)
src/components/video-editor/timeline/timelineLayout.test.ts (1)

34-40: Consider pinning normalization edge-cases in stretch-factor tests.

Since the helper internally normalizes input, adding assertions for negative/NaN/fractional values would lock intended behavior and reduce regressions around Line 34.

Proposed test additions
 	it("stretches content height to keep a two-row viewport", () => {
 		expect(TIMELINE_VISIBLE_ROW_COUNT).toBe(2);
 		expect(getTimelineViewportStretchFactor(2)).toBe(1);
 		expect(getTimelineViewportStretchFactor(4)).toBe(2);
 		expect(getTimelineViewportStretchFactor(5)).toBe(2.5);
 		expect(getTimelineViewportStretchFactor(0)).toBe(1);
+		expect(getTimelineViewportStretchFactor(-1)).toBe(1);
+		expect(getTimelineViewportStretchFactor(Number.NaN)).toBe(1);
+		expect(getTimelineViewportStretchFactor(2.9)).toBe(1);
 	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/timeline/timelineLayout.test.ts` around lines 34
- 40, Add assertions that pin the normalization behavior of
getTimelineViewportStretchFactor for edge inputs: include tests verifying
negative numbers (e.g., -1), NaN, and fractional values (e.g., 2.3 or 0.5)
return the expected normalized stretch factor (consistent with
TIMELINE_VISIBLE_ROW_COUNT behavior and existing cases). Update the test block
that references TIMELINE_VISIBLE_ROW_COUNT and getTimelineViewportStretchFactor
to explicitly assert handling of negative, NaN, and fractional inputs so the
intended normalization logic is locked in and prevents regressions.
src/components/video-editor/timeline/TimelineEditor.tsx (1)

721-721: Extract the height expression to a named memoized value for readability.

Line 721 is doing the right math, but inlining this full CSS expression makes future edits riskier.

Proposed refactor
 	const timelineRowsMinHeightPx = getTimelineRowsMinHeightPx(timelineRowCount);
 	const timelineContentMinHeightPx = getTimelineContentMinHeightPx(timelineRowCount);
 	const timelineViewportStretchFactor = getTimelineViewportStretchFactor(timelineRowCount);
+	const timelineHeight = useMemo(
+		() =>
+			`max(100%, ${timelineContentMinHeightPx}px, calc(${TIMELINE_AXIS_HEIGHT_PX}px + (100% - ${TIMELINE_AXIS_HEIGHT_PX}px) * ${timelineViewportStretchFactor}))`,
+		[timelineContentMinHeightPx, timelineViewportStretchFactor],
+	);
 
 	return (
 		<div
 			ref={setRefs}
 			style={{
 				...style,
-				height: `max(100%, ${timelineContentMinHeightPx}px, calc(${TIMELINE_AXIS_HEIGHT_PX}px + (100% - ${TIMELINE_AXIS_HEIGHT_PX}px) * ${timelineViewportStretchFactor}))`,
+				height: timelineHeight,
 			}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/timeline/TimelineEditor.tsx` at line 721, Extract
the complex CSS height string into a named memoized value to improve readability
and prevent accidental breakage: inside the TimelineEditor component compute a
const (e.g., computedTimelineHeight or timelineHeightStyle) using useMemo that
returns the template string `max(100%, ${timelineContentMinHeightPx}px,
calc(${TIMELINE_AXIS_HEIGHT_PX}px + (100% - ${TIMELINE_AXIS_HEIGHT_PX}px) *
${timelineViewportStretchFactor}))`, then replace the inline expression at the
height prop with that constant; reference the existing symbols
TIMELINE_AXIS_HEIGHT_PX, timelineContentMinHeightPx, and
timelineViewportStretchFactor when building the memo so it only recalculates
when those change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/lib/exporter/modernFrameRenderer.ts`:
- Around line 339-353: The cleanupBackgroundSource revoker and the background
staging buffers are not always torn down — ensure any stored revoker in
cleanupBackgroundSource is invoked and cleared on every teardown path (including
destroy() and any background-load fallback paths), and null out/cleanup
backgroundVideoFrameStagingCanvas and backgroundVideoFrameStagingCtx just like
the scene/webcam staging buffers; update resetBackgroundLayer(), destroy(), and
any fallback branch that sets the local wallpaper to call the revoker (if
present) and then set cleanupBackgroundSource = null, and set
backgroundVideoFrameStagingCanvas = null and backgroundVideoFrameStagingCtx =
null to prevent leaked blob URLs and canvases.
- Around line 859-898: syncBackgroundFrame currently proceeds after the video
`seeked`/loaded event which can still yield the previous frame; update
syncBackgroundFrame to mirror syncWebcamFrame by calling the existing
waitForPresentedFrame(video) (the helper that uses requestVideoFrameCallback +
requestAnimationFrame) after seek completion and before calling
ensureBackgroundSprite(), so the wallpaper frame is guaranteed presented; locate
syncBackgroundFrame and replace the post-seek/loaded completion path to await
waitForPresentedFrame(video) (or equivalent) rather than relying solely on the
seeked event, ensuring ensureBackgroundSprite() runs only after
waitForPresentedFrame resolves.

---

Nitpick comments:
In `@src/components/video-editor/timeline/TimelineEditor.tsx`:
- Line 721: Extract the complex CSS height string into a named memoized value to
improve readability and prevent accidental breakage: inside the TimelineEditor
component compute a const (e.g., computedTimelineHeight or timelineHeightStyle)
using useMemo that returns the template string `max(100%,
${timelineContentMinHeightPx}px, calc(${TIMELINE_AXIS_HEIGHT_PX}px + (100% -
${TIMELINE_AXIS_HEIGHT_PX}px) * ${timelineViewportStretchFactor}))`, then
replace the inline expression at the height prop with that constant; reference
the existing symbols TIMELINE_AXIS_HEIGHT_PX, timelineContentMinHeightPx, and
timelineViewportStretchFactor when building the memo so it only recalculates
when those change.

In `@src/components/video-editor/timeline/timelineLayout.test.ts`:
- Around line 34-40: Add assertions that pin the normalization behavior of
getTimelineViewportStretchFactor for edge inputs: include tests verifying
negative numbers (e.g., -1), NaN, and fractional values (e.g., 2.3 or 0.5)
return the expected normalized stretch factor (consistent with
TIMELINE_VISIBLE_ROW_COUNT behavior and existing cases). Update the test block
that references TIMELINE_VISIBLE_ROW_COUNT and getTimelineViewportStretchFactor
to explicitly assert handling of negative, NaN, and fractional inputs so the
intended normalization logic is locked in and prevents regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e7a924f-70ca-4185-b53e-58ab03e72790

📥 Commits

Reviewing files that changed from the base of the PR and between f144907 and 19a09cf.

📒 Files selected for processing (6)
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/timeline/TimelineEditor.tsx
  • src/components/video-editor/timeline/TimelineWrapper.tsx
  • src/components/video-editor/timeline/timelineLayout.test.ts
  • src/components/video-editor/timeline/timelineLayout.ts
  • src/lib/exporter/modernFrameRenderer.ts
✅ Files skipped from review due to trivial changes (1)
  • src/components/video-editor/timeline/TimelineWrapper.tsx

@webadderall webadderall merged commit 7f45a4b into main Apr 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant