Skip to content

fix: make ffmpeg loader concurrency-safe for concurrent load calls#1545

Open
smirk-dev wants to merge 1 commit into
magic-peach:mainfrom
smirk-dev:fix/ffmpeg-concurrent-loader-guard-smirk
Open

fix: make ffmpeg loader concurrency-safe for concurrent load calls#1545
smirk-dev wants to merge 1 commit into
magic-peach:mainfrom
smirk-dev:fix/ffmpeg-concurrent-loader-guard-smirk

Conversation

@smirk-dev
Copy link
Copy Markdown

Closes #1014

Summary

Add a Promise-based guard around the FFmpeg loader so concurrent loadFFmpeg() calls share the same initialization and never double-initialize the WASM runtime.

Before this change, the loader relied on an isFirstLoad capture plus a workerReady/workerReadyResolve === null sentinel to decide whether to trigger initialization. That scheme is fragile under concurrency: overlapping callers can each drive part of the init flow, which risks posting the worker load command more than once, registering the runtime's progress/log listeners more than once, and leaving shared state half-initialized if a load fails. (In this project ffmpeg.load() and its listeners live inside the Web Worker — the { type: "load" } message is what triggers them, so "load once" means "post that message once".)

The fix introduces a single shared in-flight promise that all concurrent callers await, plus an explicit ffmpegLoaded flag for the already-initialized fast path. The public loadFFmpeg(signal?, onProgress?) signature and the normal (non-racy) export flow are unchanged.

Technical details

  • Shared in-flight guard. Module state now holds ffmpegLoaded: boolean and ffmpegLoadingPromise: Promise<void> | null (with resolveLoad / rejectLoad settle handles).
    • If ffmpegLoaded is already true, loadFFmpeg() returns immediately and reports 100%.
    • Otherwise the first caller creates the promise via startLoad() (which creates the worker and posts { type: "load" } exactly once); any caller that arrives while the promise is in flight simply awaits the same promise instead of starting a second load.
  • Listeners registered once. Because the worker is created once and the load command is posted at most once per init cycle, the worker-side ffmpeg.on("progress", …) registration in loadCore() runs once per runtime — no duplicate listeners from overlapping callers.
  • Safe failure / retry. On a worker error (or onerror), failLoad() rejects the shared promise and resetWorker() clears ffmpegWorker, ffmpegLoaded, and ffmpegLoadingPromise. Because the in-flight promise reference is cleared, the next loadFFmpeg() call starts a clean new initialization and can succeed.
  • Per-caller abort. An aborted caller now rejects only its own wait (via Promise.race against an abort promise) and leaves the shared load intact for other in-flight callers, rather than rejecting the shared readiness promise for everyone.

Testing

Commands run (all green):

  • bun run test — full Vitest suite, 127 tests passing (14 files), including the new cases.
  • bun run lintnext lint, no warnings or errors.
  • bun run buildnext build static export succeeds.

New regression tests in src/lib/tests/ffmpegLoader.test.ts stub the global Worker and assert the concurrency invariants:

  • Concurrent loadFFmpeg() calls create one worker and post the load command once, and all callers resolve together on ready.
  • Once ready, subsequent calls take the fast path (no new worker, no new load).
  • ready reports 100% to the caller's progress callback exactly once.
  • A failed first load resets state so a retry spins up a fresh worker and succeeds.
  • An aborted caller rejects while a concurrent non-aborted caller still resolves on ready; an already-aborted signal rejects immediately without creating a worker.

Compatibility

No change to the exported loadFFmpeg / exportVideo API or to the normal non-racy export path — the only behavioral change is that overlapping initializations now share one runtime load instead of racing.

Concurrent loadFFmpeg() calls could each trigger the worker initialization
flow, double-loading the same WASM runtime and registering duplicate
listeners. Replace the brittle isFirstLoad/workerReady sentinels with an
explicit shared in-flight promise (ffmpegLoadingPromise) plus an ffmpegLoaded
flag so:

- The worker is created and {type:"load"} is posted at most once per init.
- Concurrent callers join the same promise instead of starting a second load.
- A failed load resets state (resetWorker) so later calls can retry cleanly.
- Aborting one caller rejects only that caller and leaves the shared load
  intact for others.

Adds regression coverage for concurrent init, retry-after-failure, and abort.

Closes magic-peach#1014

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 7, 2026

@smirk-dev is attempting to deploy a commit to the magic-peach1's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 7, 2026

✅ PR Format Check Passed — @smirk-dev

Basic format checks passed. A maintainer will review your code changes.

This does not mean the PR is approved — it just means the format is correct.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 7, 2026

👋 Thanks for your PR, @smirk-dev!

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:testing Testing labels Jun 7, 2026
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:testing Testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Concurrent FFmpeg Loader Calls Can Double-Initialize the Same WASM Runtime and Corrupt Internal State

1 participant