Skip to content

fix(export): make audio finalization timeouts progress-aware#283

Merged
webadderall merged 5 commits intowebadderallorg:mainfrom
meiiie:fix/export-finalization-timeouts
Apr 20, 2026
Merged

fix(export): make audio finalization timeouts progress-aware#283
webadderall merged 5 commits intowebadderallorg:mainfrom
meiiie:fix/export-finalization-timeouts

Conversation

@meiiie
Copy link
Copy Markdown
Collaborator

@meiiie meiiie commented Apr 19, 2026

Summary

Make audio-heavy export finalization stages progress-aware instead of relying only on a fixed wall-clock timeout.

Root Cause

Local profiling showed that long exports spend most of their finalization time in audio work rather than muxer finalization itself. A larger fixed timeout helps, but it still treats healthy long-running audio work and a genuinely stuck finalization stage the same way.

Changes

  • keep the existing total timeout ceiling, still scaled by output duration for audio workloads
  • add a bounded idle watchdog window for audio stages
  • refresh that watchdog only when the export reports real finalization progress
  • apply the progress-aware watchdog to the audio stages that already emit progress callbacks in both legacy and modern exporters
  • keep non-audio stages on the existing timeout behavior

Why This Is Safer

This narrows the follow-up fix to the bottleneck identified by profiling:

  • healthy long audio finalization can keep running as long as progress continues
  • genuinely stalled audio finalization still fails fast on a no-progress timeout
  • unrelated finalization stages do not get a broader timeout policy

Validation

  • npx vitest --run src/lib/exporter/finalizationTimeout.test.ts
  • npx tsc --noEmit
  • npx @biomejs/biome check src/lib/exporter/finalizationTimeout.ts src/lib/exporter/finalizationTimeout.test.ts src/lib/exporter/videoExporter.ts src/lib/exporter/modernVideoExporter.ts
  • npx tsc && npx vite build
  • local Electron smoke exports on a long recording:
    • modern pipeline: success, produced modern-progress-aware-1776653676126.mp4
    • legacy pipeline: success, produced legacy-progress-aware-1776653829973.mp4

Related

Summary by CodeRabbit

  • New Features

    • Finalization timeouts now adapt by workload and content duration: audio exports receive duration‑scaled, capped extensions while non‑audio or invalid durations fall back to standard limits. Exporters now support progress‑aware idle monitoring and a refreshable watchdog to avoid premature finalization failures.
  • Tests

    • Added tests for timeout/idle calculations, progress handling, watchdog reset semantics, and edge‑case fallbacks.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 19, 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: 4d7d54c8-6e74-4391-aebf-355f0ea71d20

📥 Commits

Reviewing files that changed from the base of the PR and between 04e013c and 19486c7.

📒 Files selected for processing (4)
  • src/lib/exporter/finalizationTimeout.test.ts
  • src/lib/exporter/finalizationTimeout.ts
  • src/lib/exporter/modernVideoExporter.ts
  • src/lib/exporter/videoExporter.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/exporter/finalizationTimeout.test.ts
  • src/lib/exporter/finalizationTimeout.ts

📝 Walkthrough

Walkthrough

Adds a new finalization timeout module with workload- and progress-aware idle watchdogs and tests; replaces fixed finalization timers in both video exporters with duration/workload-aware timeouts and optional progress-aware watchdog wiring.

Changes

Cohort / File(s) Summary
Finalization timeout module & tests
src/lib/exporter/finalizationTimeout.ts, src/lib/exporter/finalizationTimeout.test.ts
New module: exported types and constants, timeout/idle calculations (base 10min; audio = base + 500ms/sec, capped at 45min), idle-window derivation (25% of total, clamped 90s–5min), progress-state helpers (advanceFinalizationProgress), and withFinalizationTimeout(...) (total timeout + optional progress-aware idle watchdog). Adds Vitest coverage for calculations, idle watchdog behavior, and progress advancement semantics.
Modern video exporter updates
src/lib/exporter/modernVideoExporter.ts
Replaces fixed FINALIZATION_TIMEOUT_MS logic with withFinalizationTimeout(...); threads effectiveDurationSec, workload, and progressAware; stores activeFinalizationProgressWatchdog and progress state; reportFinalizingProgress uses advanceFinalizationProgress and refreshes watchdog; resets state in cleanup().
Classic video exporter updates
src/lib/exporter/videoExporter.ts
Same migration as modern exporter: delegates to withFinalizationTimeout(...), passes workload labels (audio vs default), enables progressAware for audio stages, tracks watchdog and last-progress fields, clears state during cleanup.

Sequence Diagram(s)

sequenceDiagram
  participant Exporter
  participant withFinalizer as Finalizer
  participant Muxer
  participant Watchdog

  Exporter->>withFinalizer: call withFinalizationTimeout(promise, stage, workload, effectiveDurationSec, progressAware)
  Note right of Exporter: compute totalTimeout via getExportFinalizationTimeoutMs(...)
  withFinalizer->>Watchdog: start idleTimer = getExportFinalizationIdleTimeoutMs(...) (if progressAware)
  withFinalizer->>Muxer: run finalization step (workload: audio/default)
  Muxer-->>Exporter: report progress
  Exporter->>Watchdog: refreshProgress()
  Watchdog-->>withFinalizer: idle window expired -> reject ("without observable progress")
  Muxer-->>withFinalizer: finalization completes -> resolve, clear timers, onWatchdogChanged(null)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • webadderall/Recordly#262: Overlaps finalization/progress handling in modernVideoExporter/videoExporter and likely touches the same watchdog/timeout logic.
  • webadderall/Recordly#228: Related audio finalization and FFmpeg audio-fallback paths that now interact with the new timeout/progress hooks.

Poem

🐇 I nibbled seconds, counted beats with care,
Idle watchdog snoozed until progress dared.
Audio stretches, timeouts gently bend,
Fresh carrots—timers reset—then render's end.

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(export): make audio finalization timeouts progress-aware' directly describes the main change—introducing progress-aware timeout behavior for audio finalization stages—which aligns with the primary objective of the changeset.
Description check ✅ Passed The pull request description provides a comprehensive explanation covering summary, root cause, changes, safety rationale, and validation steps. Although not formatted exactly as the template, it includes all essential information about the purpose, motivation, and testing approach.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

This was referenced Apr 19, 2026
@meiiie meiiie marked this pull request as ready for review April 19, 2026 19:52
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.

🧹 Nitpick comments (1)
src/lib/exporter/finalizationTimeout.ts (1)

18-29: Nit: redundant null-coalescing and Math.max(0, …) after the finite+positive guard.

Once execution passes the Number.isFinite(effectiveDurationSec) || (effectiveDurationSec ?? 0) <= 0 guard on line 18, effectiveDurationSec is known to be a finite number that is strictly greater than zero. The later effectiveDurationSec ?? 0 (line 24) and Math.max(0, …) can never take their fallback branches.

♻️ Optional simplification
-	if (!Number.isFinite(effectiveDurationSec) || (effectiveDurationSec ?? 0) <= 0) {
+	if (
+		typeof effectiveDurationSec !== "number" ||
+		!Number.isFinite(effectiveDurationSec) ||
+		effectiveDurationSec <= 0
+	) {
 		return BASE_FINALIZATION_TIMEOUT_MS;
 	}
 
 	// Audio finalization work scales with the output timeline, so long exports need
 	// more headroom without making unrelated finalization hangs wait longer.
-	const safeEffectiveDurationSec = Math.max(0, effectiveDurationSec ?? 0);
 	const adaptiveTimeoutMs =
 		BASE_FINALIZATION_TIMEOUT_MS +
-		safeEffectiveDurationSec * AUDIO_TIMEOUT_HEADROOM_PER_OUTPUT_SECOND_MS;
+		effectiveDurationSec * AUDIO_TIMEOUT_HEADROOM_PER_OUTPUT_SECOND_MS;
 
 	return Math.min(adaptiveTimeoutMs, MAX_AUDIO_FINALIZATION_TIMEOUT_MS);

This also lets TS narrow effectiveDurationSec to number inside the branch, which is more type-honest than threading number | null | undefined through Math.max.

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

In `@src/lib/exporter/finalizationTimeout.ts` around lines 18 - 29, Remove the
redundant null-coalescing and Math.max calls: after the guard that checks
Number.isFinite(effectiveDurationSec) and > 0, treat effectiveDurationSec as a
finite number and compute adaptiveTimeoutMs directly as
BASE_FINALIZATION_TIMEOUT_MS + effectiveDurationSec *
AUDIO_TIMEOUT_HEADROOM_PER_OUTPUT_SECOND_MS, then return
Math.min(adaptiveTimeoutMs, MAX_AUDIO_FINALIZATION_TIMEOUT_MS); remove the
safeEffectiveDurationSec and any uses of (effectiveDurationSec ?? 0) or
Math.max(0, …) so TS can narrow effectiveDurationSec to number.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/lib/exporter/finalizationTimeout.ts`:
- Around line 18-29: Remove the redundant null-coalescing and Math.max calls:
after the guard that checks Number.isFinite(effectiveDurationSec) and > 0, treat
effectiveDurationSec as a finite number and compute adaptiveTimeoutMs directly
as BASE_FINALIZATION_TIMEOUT_MS + effectiveDurationSec *
AUDIO_TIMEOUT_HEADROOM_PER_OUTPUT_SECOND_MS, then return
Math.min(adaptiveTimeoutMs, MAX_AUDIO_FINALIZATION_TIMEOUT_MS); remove the
safeEffectiveDurationSec and any uses of (effectiveDurationSec ?? 0) or
Math.max(0, …) so TS can narrow effectiveDurationSec to number.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: abaf1300-82f5-4217-904c-77d05c616cb7

📥 Commits

Reviewing files that changed from the base of the PR and between daf99c0 and dc60a8e.

📒 Files selected for processing (4)
  • src/lib/exporter/finalizationTimeout.test.ts
  • src/lib/exporter/finalizationTimeout.ts
  • src/lib/exporter/modernVideoExporter.ts
  • src/lib/exporter/videoExporter.ts

@meiiie meiiie changed the title fix(export): scale audio finalization timeouts fix(export): make audio finalization timeouts progress-aware Apr 20, 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: 1

🧹 Nitpick comments (1)
src/lib/exporter/finalizationTimeout.test.ts (1)

58-76: Consider expanding idle-timeout coverage.

The idle helper is only asserted for {default}, {audio, 1_200}, and {audio, 2_700}. Two gaps worth covering while you're here:

  1. getExportFinalizationIdleTimeoutMs with invalid audio durations (0, NaN) — symmetric to the invalid-duration case you already added for getExportFinalizationTimeoutMs. This locks down the value that the consumer's progressAware && idleTimeoutMs truthy check in both exporters depends on (see the comment on videoExporter.ts).
  2. An end-to-end test using Vitest fake timers that exercises the watchdog path in awaitWithFinalizationTimeout itself: verify that no-progress causes rejection at the idle window and that calling the refresh hook resets it. That’s the behavior the PR title actually promises and it isn’t covered today.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/exporter/finalizationTimeout.test.ts` around lines 58 - 76, Add two
new tests in finalizationTimeout.test.ts: (1) assert
getExportFinalizationIdleTimeoutMs returns the default idle timeout for invalid
audio durations (e.g., effectiveDurationSec: 0 and effectiveDurationSec: NaN)
similar to the existing invalid-duration tests for
getExportFinalizationTimeoutMs; reference getExportFinalizationIdleTimeoutMs in
these assertions. (2) add an end-to-end Vitest fake-timers test that exercises
awaitWithFinalizationTimeout: use vi.useFakeTimers(), call
awaitWithFinalizationTimeout with an idle window derived for a workload (via
getExportFinalizationIdleTimeoutMs), verify that advancing timers past the idle
window without calling the provided refresh hook causes the promise to reject,
and verify that calling the refresh hook before the window expires resets the
watchdog so advancing timers further does not reject; reference
awaitWithFinalizationTimeout and the refreshProgress callback to locate the
implementation to test. Ensure to restore real timers after the test.
🤖 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/lib/exporter/videoExporter.ts`:
- Around line 379-444: The current awaitWithFinalizationTimeout implementation
uses a truthy check (progressAware && idleTimeoutMs) which breaks when
getExportFinalizationIdleTimeoutMs returns 0; change the guard to an explicit
null/number check so the idle watchdog is enabled for 0 (e.g. progressAware &&
idleTimeoutMs !== null && idleTimeoutMs !== undefined), and ensure idleTimeoutMs
is treated as a number where used. Also consider deduplicating this logic by
extracting the timer/watchdog plumbing into a shared helper (e.g.
finalizationTimeout.withFinalizationTimeout) used by both
VideoExporter.awaitWithFinalizationTimeout and
ModernVideoExporter.awaitWithFinalizationTimeout so callers can receive the
returned watchdog (to set activeFinalizationProgressWatchdog and allow
reportFinalizingProgress to call refreshProgress) and the tests cover the
behavior once.

---

Nitpick comments:
In `@src/lib/exporter/finalizationTimeout.test.ts`:
- Around line 58-76: Add two new tests in finalizationTimeout.test.ts: (1)
assert getExportFinalizationIdleTimeoutMs returns the default idle timeout for
invalid audio durations (e.g., effectiveDurationSec: 0 and effectiveDurationSec:
NaN) similar to the existing invalid-duration tests for
getExportFinalizationTimeoutMs; reference getExportFinalizationIdleTimeoutMs in
these assertions. (2) add an end-to-end Vitest fake-timers test that exercises
awaitWithFinalizationTimeout: use vi.useFakeTimers(), call
awaitWithFinalizationTimeout with an idle window derived for a workload (via
getExportFinalizationIdleTimeoutMs), verify that advancing timers past the idle
window without calling the provided refresh hook causes the promise to reject,
and verify that calling the refresh hook before the window expires resets the
watchdog so advancing timers further does not reject; reference
awaitWithFinalizationTimeout and the refreshProgress callback to locate the
implementation to test. Ensure to restore real timers after the test.
🪄 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: 97428f2e-d8fc-4f75-b3f6-08b21941ab3f

📥 Commits

Reviewing files that changed from the base of the PR and between dc60a8e and 271dddc.

📒 Files selected for processing (4)
  • src/lib/exporter/finalizationTimeout.test.ts
  • src/lib/exporter/finalizationTimeout.ts
  • src/lib/exporter/modernVideoExporter.ts
  • src/lib/exporter/videoExporter.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/exporter/finalizationTimeout.ts

Comment thread src/lib/exporter/videoExporter.ts
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/videoExporter.ts (1)

880-887: ⚠️ Potential issue | 🟠 Major

Only refresh the idle watchdog when progress actually advances.

Line 885 refreshes on every finalization progress event, so repeated callbacks with the same renderProgress/audioProgress can keep a stalled audio finalization alive indefinitely. Track the last observed finalization progress and refresh only when the normalized value increases.

🐛 Suggested direction
 private reportFinalizingProgress(
 	totalFrames: number,
 	renderProgress: number,
 	audioProgress?: number,
 ) {
-	this.activeFinalizationProgressWatchdog?.refreshProgress();
+	const safeRenderProgress = Math.max(0, Math.min(renderProgress, 100));
+	const safeAudioProgress =
+		typeof audioProgress === "number"
+			? Math.max(0, Math.min(audioProgress, 1))
+			: undefined;
+	const progressed =
+		safeRenderProgress > this.lastFinalizationRenderProgress ||
+		(safeAudioProgress !== undefined &&
+			safeAudioProgress > this.lastFinalizationAudioProgress);
+
+	if (progressed) {
+		this.activeFinalizationProgressWatchdog?.refreshProgress();
+		this.lastFinalizationRenderProgress = safeRenderProgress;
+		if (safeAudioProgress !== undefined) {
+			this.lastFinalizationAudioProgress = safeAudioProgress;
+		}
+	}
 	this.reportProgress(totalFrames, totalFrames, "finalizing", renderProgress, audioProgress);
 }

This also needs matching private fields initialized to sentinel values and reset in cleanup().

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

In `@src/lib/exporter/videoExporter.ts` around lines 880 - 887, The routine
reportFinalizingProgress is refreshing this.activeFinalizationProgressWatchdog
on every call even when renderProgress/audioProgress haven't advanced; add
private fields (e.g., lastFinalizingRenderProgress = -1 and
lastFinalizingAudioProgress = -1) to track the last normalized progress, update
them only when the new normalized renderProgress or audioProgress is greater
than the stored value, and call
this.activeFinalizationProgressWatchdog?.refreshProgress() only when at least
one progressed; also reset those sentinel fields in cleanup() so subsequent runs
start fresh.
src/lib/exporter/modernVideoExporter.ts (1)

1154-1160: ⚠️ Potential issue | 🟠 Major

Gate watchdog refreshes on monotonic progress.

Line 1159 refreshes the idle watchdog for every finalization callback. If an audio stage repeatedly emits the same progress value, the watchdog will not detect a no-progress stall. Refresh only when normalized renderProgress or audioProgress advances.

🐛 Suggested direction
 private reportFinalizingProgress(
 	totalFrames: number,
 	renderProgress: number,
 	audioProgress?: number,
 ) {
-	this.activeFinalizationProgressWatchdog?.refreshProgress();
+	const safeRenderProgress = Math.max(0, Math.min(renderProgress, 100));
+	const safeAudioProgress =
+		typeof audioProgress === "number"
+			? Math.max(0, Math.min(audioProgress, 1))
+			: undefined;
+	const progressed =
+		safeRenderProgress > this.lastFinalizationRenderProgress ||
+		(safeAudioProgress !== undefined &&
+			safeAudioProgress > this.lastFinalizationAudioProgress);
+
+	if (progressed) {
+		this.activeFinalizationProgressWatchdog?.refreshProgress();
+		this.lastFinalizationRenderProgress = safeRenderProgress;
+		if (safeAudioProgress !== undefined) {
+			this.lastFinalizationAudioProgress = safeAudioProgress;
+		}
+	}
 	this.reportProgress(totalFrames, totalFrames, "finalizing", renderProgress, audioProgress);
 }

Add/reset the corresponding sentinel fields alongside the existing finalization state.

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

In `@src/lib/exporter/modernVideoExporter.ts` around lines 1154 - 1160, The
watchdog is being refreshed every time reportFinalizingProgress is called
regardless of monotonic progress; change reportFinalizingProgress to track
previous normalized progress values (e.g., add class fields like
lastFinalRenderProgress and lastFinalAudioProgress), compute normalized
renderProgress and audioProgress, and only call
this.activeFinalizationProgressWatchdog?.refreshProgress() when the normalized
renderProgress or audioProgress is strictly greater than its respective stored
sentinel; update the sentinels when you observe advancement, and still call
this.reportProgress(totalFrames, totalFrames, "finalizing", renderProgress,
audioProgress) on every invocation.
🤖 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/modernVideoExporter.ts`:
- Around line 1154-1160: The watchdog is being refreshed every time
reportFinalizingProgress is called regardless of monotonic progress; change
reportFinalizingProgress to track previous normalized progress values (e.g., add
class fields like lastFinalRenderProgress and lastFinalAudioProgress), compute
normalized renderProgress and audioProgress, and only call
this.activeFinalizationProgressWatchdog?.refreshProgress() when the normalized
renderProgress or audioProgress is strictly greater than its respective stored
sentinel; update the sentinels when you observe advancement, and still call
this.reportProgress(totalFrames, totalFrames, "finalizing", renderProgress,
audioProgress) on every invocation.

In `@src/lib/exporter/videoExporter.ts`:
- Around line 880-887: The routine reportFinalizingProgress is refreshing
this.activeFinalizationProgressWatchdog on every call even when
renderProgress/audioProgress haven't advanced; add private fields (e.g.,
lastFinalizingRenderProgress = -1 and lastFinalizingAudioProgress = -1) to track
the last normalized progress, update them only when the new normalized
renderProgress or audioProgress is greater than the stored value, and call
this.activeFinalizationProgressWatchdog?.refreshProgress() only when at least
one progressed; also reset those sentinel fields in cleanup() so subsequent runs
start fresh.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 69741ce9-e569-4b51-a287-276aebc1bd8a

📥 Commits

Reviewing files that changed from the base of the PR and between 271dddc and 6783eeb.

📒 Files selected for processing (4)
  • src/lib/exporter/finalizationTimeout.test.ts
  • src/lib/exporter/finalizationTimeout.ts
  • src/lib/exporter/modernVideoExporter.ts
  • src/lib/exporter/videoExporter.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/exporter/finalizationTimeout.test.ts

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

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/videoExporter.ts (1)

712-729: ⚠️ Potential issue | 🟠 Major

Keep the native session cancellable until finalization resolves.

Line 713 clears this.nativeExportSessionId before the timed nativeVideoExportFinish call. If the new audio-aware timeout rejects, cleanup() can no longer call nativeVideoExportCancel, leaving the native export session/process running.

🛠️ Proposed fix
 		const sessionId = this.nativeExportSessionId;
-		this.nativeExportSessionId = null;
 
 		const result = await this.awaitWithFinalizationTimeout(
 			window.electronAPI.nativeVideoExportFinish(sessionId, {
 				audioMode: audioPlan.audioMode,
@@
 			"native export finalization",
 			audioPlan.audioMode === "none" ? "default" : "audio",
 		);
+		this.nativeExportSessionId = null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/exporter/videoExporter.ts` around lines 712 - 729, The code clears
this.nativeExportSessionId before awaiting
awaitWithFinalizationTimeout(window.electronAPI.nativeVideoExportFinish(...)),
which prevents cleanup() from calling nativeVideoExportCancel with the correct
id if the timed call rejects; keep the native session cancellable by preserving
the session id until the finalization promise settles — e.g., store sessionId in
a local const (sessionId already exists) and only set this.nativeExportSessionId
= null after the await resolves or in the finally block, or modify
cleanup()/nativeVideoExportCancel to accept the stored local sessionId so
nativeVideoExportCancel can be invoked even if this.nativeExportSessionId was
cleared.
🤖 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/lib/exporter/finalizationTimeout.ts`:
- Around line 78-87: The renderProgress clamping at normalizedRenderProgress
lacks a finite-number guard and can become NaN; change its computation to mirror
audioProgress handling by checking typeof renderProgress === "number" &&
Number.isFinite(renderProgress) and only then clamp to Math.max(0,
Math.min(renderProgress, 100)), otherwise treat it as null (or fallback to
state.lastRenderProgress when computing nextRenderProgress); update
normalizedRenderProgress and nextRenderProgress logic (variables:
normalizedRenderProgress, nextRenderProgress, state.lastRenderProgress) so NaN
values don't overwrite the last known progress.

In `@src/lib/exporter/videoExporter.ts`:
- Around line 1154-1156: The cleanup path nulls audioProcessor without
cancelling in-flight work, allowing timed-out audio finalization to continue;
update the cleanup() logic (and the branch that handles withFinalizationTimeout
/ activeFinalizationProgressWatchdog) to, if this.audioProcessor is non-null,
call this.audioProcessor.cancel() before nulling it so any running audio
finalization is signalled to stop; ensure the cancel() call is safe to call
unconditionally (AudioProcessor.cancel()), then clear
activeFinalizationProgressWatchdog and set this.audioProcessor = null.

---

Outside diff comments:
In `@src/lib/exporter/videoExporter.ts`:
- Around line 712-729: The code clears this.nativeExportSessionId before
awaiting
awaitWithFinalizationTimeout(window.electronAPI.nativeVideoExportFinish(...)),
which prevents cleanup() from calling nativeVideoExportCancel with the correct
id if the timed call rejects; keep the native session cancellable by preserving
the session id until the finalization promise settles — e.g., store sessionId in
a local const (sessionId already exists) and only set this.nativeExportSessionId
= null after the await resolves or in the finally block, or modify
cleanup()/nativeVideoExportCancel to accept the stored local sessionId so
nativeVideoExportCancel can be invoked even if this.nativeExportSessionId was
cleared.
🪄 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: 570f32c0-cd54-4b65-9c01-12fbe8636296

📥 Commits

Reviewing files that changed from the base of the PR and between 6783eeb and 04e013c.

📒 Files selected for processing (4)
  • src/lib/exporter/finalizationTimeout.test.ts
  • src/lib/exporter/finalizationTimeout.ts
  • src/lib/exporter/modernVideoExporter.ts
  • src/lib/exporter/videoExporter.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/exporter/finalizationTimeout.test.ts
  • src/lib/exporter/modernVideoExporter.ts

Comment thread src/lib/exporter/finalizationTimeout.ts Outdated
Comment thread src/lib/exporter/videoExporter.ts
@webadderall webadderall merged commit 2c00f18 into webadderallorg:main Apr 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants