Skip to content

fix(ffmpeg): stop multi-threaded exports from hanging at 100% - #1831

Merged
magic-peach merged 3 commits into
mainfrom
fix/mt-export-hang
Nov 3, 2026
Merged

magic-peach merged 3 commits into
mainfrom
fix/mt-export-hang

Conversation

@magic-peach

@magic-peach magic-peach commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Production exports are hanging

https://reframe-os.netlify.app is cross-origin isolated, so every browser loads the multi-threaded FFmpeg core. On main, FFmpeg chooses its own thread count, core-mt's 32-worker pool deadlocks, and every export sits at "Exporting 100%" forever. Datadog has no successful production export since 2026-10-22.

Reproduced locally with Playwright (headless Chromium, production build served with COOP/COEP):

Fixture main, isolated main, not isolated this PR, isolated
normal.mp4 (720p H.264/AAC, 10 s) hang (>120 s) done 16.9 s done 7.5 s
no-duration.webm (screen-recording style) hang done 12.7 s done 6.4 s
no-audio.mp4 hang — done 6.3 s
portrait.mp4 hang — done 5.9 s
normal.mp4 → WebM / GIF / MKV — — done 12.2 s / 54.6 s / 7.7 s

What changes

Ports the export hardening from the earlier branch (commits 33b2c5a and bf26cf1). These files hadn't changed on main since then except for #1829's telemetry, which this keeps.

  • src/lib/ffmpegArgs.ts: one command builder, used by the worker. The copy in ffmpeg.ts was dead code, and the worker's own copy ignored sharpness and volume.
    • Decoder, filter and encoder threads are capped at 4, on the multi-threaded core only.
  • Stall watchdog. The worker sends heartbeats. 60 s without activity fails the export with ExportStalledError (telemetry category timeout), and later attempts in the page use the single-threaded core.
  • WebM uses VP8 + Vorbis. libvpx-vp9 and libopus trap in @ffmpeg/core 0.12.10.
  • Engine loading. The multi-threaded core files are integrity-checked (SRI). If core-mt fails to load, the worker falls back to the single-threaded core.
  • No more stuck "Exporting" state after an error with no export id, a cancel during engine load, or a string rejection from a wasm trap.

Changes after review (3fe0335, 29db120)

  • Threads: per stage = min(4, navigator.hardwareConcurrency || 2). Before, an unreported core count meant 4.
  • Automatic fallback: when the multi-threaded engine stalls, the same export (same file and edits) restarts once on the single-threaded engine instead of failing.
    • The overlay says it switched engines and started again.
    • RUM gets engine_fallback with stall_reason. The attempt's one outcome then has engine: single and fell_back: true.
    • A single-threaded stall is not retried; its message tells the user to press Retry.
  • Duration: the editor passes the duration it already read to the export.
  • Cleanup: 3fe0335 accidentally committed 31 untracked local duplicate files (names ending in " 2"), and 29db120 removes them. The PR diff no longer contains them. Please squash-merge so they never appear in main's history.

Checks

  • bun run test: 29 files, 338 tests pass. They include the fallback (retried with the same edits, not retried when single-threaded or cancelled) and thread-count tests.
  • Typecheck and lint are clean, and the build passes.
  • Exports verified in headless Chromium as in the table above, re-run after the fallback change: normal 8.6 s, no-duration 6.1 s, portrait 6.7 s. A real engine stall couldn't be reproduced once the thread cap was in, so the fallback is covered by unit tests only. Not yet verified on the Netlify deploy preview: please do one export there before merging.

Follow-up (stacked PR, Phase 1 of the plan)

  • Treat Infinity durations as unknown everywhere. outputDurationArgs still lets Infinity through when speed ≠ 1.
  • Detect progress stuck at 100%.
  • Report an unknown_duration failure category.
  • Fixtures script and a Playwright end-to-end test in CI.

Production (Netlify) is cross-origin isolated, so every browser loads the
multi-threaded core. With FFmpeg choosing its own thread count, core-mt's
32-worker pool deadlocks and every export sits at "Exporting 100%" forever.
Reproduced locally with COOP/COEP for MP4, WebM, portrait and no-audio
inputs; the single-threaded core was unaffected.

Ports the export hardening from the earlier branch (33b2c5a, bf26cf1):
- src/lib/ffmpegArgs.ts: one command builder used by the worker (the old
  copy in ffmpeg.ts was dead code); decoder, filter and encoder threads
  capped at 4 on the multi-threaded core only
- stall watchdog with worker heartbeats: 60 s without activity fails the
  export with ExportStalledError, and later attempts use the
  single-threaded core
- WebM uses VP8 + Vorbis: libvpx-vp9 and libopus trap in @ffmpeg/core
  0.12.10
- SRI hashes for the multi-threaded core files; falls back to the
  single-threaded core if core-mt fails to load
- id-less worker errors, cancel during engine load, and string rejections
  from wasm traps no longer leave the editor stuck

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@netlify

netlify Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for reframe-os ready!

Name Link
🔨 Latest commit 29db120
🔍 Latest deploy log https://app.netlify.com/projects/reframe-os/deploys/6ae9982a30ac023ec0faa5c0
😎 Deploy Preview https://deploy-preview-1831--reframe-os.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

👋 Thanks for your PR, @magic-peach!

Welcome to Reframe — a browser-based video editor built for everyone 🎬

What happens next

  1. 🤖 Automated checks — build & TypeScript typecheck will run automatically
  2. ✅ Vercel preview — a preview deployment will be created (requires maintainer authorization for fork PRs)
  3. 👀 Code review — a maintainer will review your changes
  4. 🚀 Merge — once approved, your PR will be merged!

Quick checklist

  • PR title follows Conventional Commits (e.g. feat: add dark mode)
  • Linked the issue this PR closes (e.g. Closes #123)
  • Tested the changes locally (bun run dev)
  • Build passes (bun run build)

Useful links

Happy coding! 🎉

@github-actions github-actions Bot added level:advanced Advanced level - 55 pts type:bug Bug fix type:design UI/UX design type:performance Performance type:testing Testing labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Format Issues — @magic-peach

Please fix the following before your PR can be reviewed:

  • ⚠️ No linked issue found. Add Closes #<issue-number> to your PR description.

Push new commits after fixing — this comment will update automatically.

📖 CONTRIBUTING.md

magic-peach and others added 2 commits November 3, 2026 13:45
…read default 2

- Threads per stage are min(4, navigator.hardwareConcurrency || 2); the
  fallback for an unreported core count was 4.
- When the multi-threaded engine stalls, exportVideo retries the same export
  (same file and edits) once on the single-threaded engine instead of failing.
  The overlay says so, and RUM gets engine_fallback with the stall reason;
  the attempt's single outcome then carries engine "single" and fell_back.
- A single-threaded stall, or a stall after cancelling, is not retried; the
  error tells the user to press Retry.
- The editor passes the duration it already read to the export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They are untracked local copies (names ending in " 2"), not project files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@magic-peach
magic-peach marked this pull request as ready for review November 3, 2026 15:56
@magic-peach
magic-peach merged commit 55ed34c into main Nov 3, 2026
21 of 25 checks passed
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

magic-peach added a commit that referenced this pull request Nov 3, 2026
No content change: this branch already contains #1831's commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
magic-peach added a commit that referenced this pull request Nov 3, 2026
No content change: main's tree equals 8e24b37, which this branch builds on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

level:advanced Advanced level - 55 pts type:bug Bug fix type:design UI/UX design type:performance Performance type:testing Testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant