Skip to content

fix(export): unknown durations, stuck-progress watchdog, e2e export tests - #1832

Merged
magic-peach merged 8 commits into
mainfrom
fix/export-duration-e2e
Nov 3, 2026
Merged

magic-peach merged 8 commits into
mainfrom
fix/export-duration-e2e

Conversation

@magic-peach

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

Copy link
Copy Markdown
Collaborator

Stacked on #1831. It targets fix/mt-export-hang, so the diff shows only this work. Merge #1831 first, then change this PR's base to main.

A correction: the earlier diagnosis was wrong

I said earlier that production exports hang because getVideoDuration returns Infinity and the export gets -t Infinity. That was wrong. The worker only adds -t when the speed isn't 1×, and the hangs I saw were at 1×.

The real cause is that the multi-threaded FFmpeg core deadlocks when FFmpeg picks its own thread count. Production is cross-origin isolated, so every visitor loaded that core and every export hung, normal MP4s included. Locally, all four fixtures hang with COOP/COEP and finish without it. #1831 fixes this with a thread cap, plus a watchdog that falls back to the single-threaded core.

This PR fixes a separate bug. Browser-recorded files such as screen recordings have no duration in their header, so <video>.duration is Infinity. The editor turned that into 0:

  • the trim fields had no limit and showed "Duration: 0:00";
  • a sped-up export had no -t;
  • a builder copy without the guard would have passed -t Infinity.

None of this caused the production hang.

What changes

Durations

  • src/lib/mediaDuration.ts: when the duration is Infinity, seek past the end so the browser works out the real length. Used by the editor and the export.
  • src/lib/ffmpegArgs.ts:
    • withSafeTimes repairs a non-finite or negative trim start, a trim end that isn't after the start, and an unusable speed before any filter is built.
    • -t is added only for a finite, positive length.
    • -shortest is added when looped music would otherwise never end.
    • assertFiniteArgs throws if any argument still contains Infinity or NaN.
    • An infinite speed no longer makes the audio filter builder loop forever.
  • I checked every other duration, fps and time value that reaches FFmpeg: trim and atrim, setpts, atempo, GIF -t, and text and overlay positions. They all go through these builders. FPS isn't set anywhere.

Stalls

  • The worker computes progress from encoded time ÷ expected length, instead of FFmpeg's own ratio, which needs a header duration. It also reports the encoded seconds.
  • The watchdog stops an export when:
    • there's no activity for 60 s (the deadlock);
    • encoded time doesn't change for 120 s while FFmpeg keeps logging (stuck at 100%);
    • encoding runs past 1.25 × the expected length + 15 s (an overrun).
  • Each case shows a clear message with a Retry button. On the multi-threaded core, fix(ffmpeg): stop multi-threaded exports from hanging at 100% #1831's automatic single-threaded retry happens first.

Telemetry

  • New failure category unknown_duration: a clip whose length couldn't be found that then stalled or failed.
  • export_failed.stall_reason: no_activity, no_progress or overrun.
  • export_cancelled.stuck: true when the user cancelled after 60 s with no progress. Monitors count these as failures.
  • The dashboard JSON gets a "Stalled exports by reason and engine" widget, and the schema is documented in docs/DATADOG.md.

Monitoring (already live in Datadog, not part of the diff)

  • New monitor 340168191, "Exports start but none succeed": more than 3 export_started and 0 export_succeeded on production over 6 h. Email only.
  • Monitor 340137974 now fires on exports not completing: started − succeeded − ordinary cancellations, over 1 h (more than 10, warning at 5). That covers failures, stuck cancellations and abandoned exports.
  • Why the old monitors stayed OK during the outage: hung exports produced no export_failed, only cancellations or nothing at all. Note that builds before feat(telemetry): export reliability by browser and device #1829 used older event names, so these monitors only see traffic from feat(telemetry): export reliability by browser and device #1829 onward.

Tests and CI

  • scripts/make-fixtures.sh makes the fixtures in test/fixtures/ (gitignored), all with the ffmpeg CLI:
    • normal 10 s H.264/AAC;
    • WebM with no duration (-live 1, the MediaRecorder layout);
    • portrait;
    • no audio;
    • about 300 MB.
  • scripts/serve-isolated.mjs serves out/ with or without COOP/COEP.
  • test/e2e/export.spec.ts (Playwright) runs on both the multi- and single-threaded engines:
    • every fixture: 1:1 preset, trim 1–5 s, rotate 90°, 2×, export, download, then ffprobe checks 1080×1080, about 2 s, and the expected audio stream count;
    • the WebM's real length shows in the editor;
    • the WebM at 2× exports its full 3 s.
  • New Tests workflow: Vitest, plus Playwright in headless Chromium with ffmpeg from apt. main didn't run unit tests in CI before.
  • New Vitest tests: every combination of bad time values, resolveDuration, the watchdog's three rules, no false stall without time reports, unknown_duration / stall_reason / stuck, and the hook passing its known duration.

Checks run locally

  • Vitest: 30 files, 362 tests pass. Typecheck and lint are clean.
  • Playwright: 12 passed (6 per engine). The ~300 MB clip (E2E_LARGE=1, multi-threaded) also passed: trim, rotate, 2× and export in 22.9 s.
  • Build passes with NEXT_PUBLIC_DATADOG_ENV=off.
  • CI for the new workflow will run on this PR. I haven't seen it run yet.

magic-peach and others added 6 commits November 3, 2026 13:00
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>
…ests

- Live-recorded files (MediaRecorder screen recordings) report an Infinity
  duration; seek to the end to find the real one (src/lib/mediaDuration.ts),
  in the editor and the export. Trim works on them again.
- ffmpegArgs: time values are repaired before use (withSafeTimes), -t only
  for a finite positive length, -shortest for looped music when the length
  is unknown, and a final check that no argument carries Infinity or NaN.
  An infinite speed no longer hangs the audio filter builder.
- Watchdog: besides 60 s of silence, stop exports whose encoded time stops
  changing for 120 s while FFmpeg keeps logging (stuck at 100%), or that run
  far past the expected length. The worker reports encoded time and computes
  progress from it. The message says what happened and to press Retry.
- Telemetry: unknown_duration failure category and stall_reason.
- scripts/make-fixtures.sh (test/fixtures/, gitignored), a static server with
  COOP/COEP (scripts/serve-isolated.mjs), Playwright export tests on the
  multi- and single-threaded engines, and a Tests workflow running Vitest and
  Playwright in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
…ion-e2e

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

# Conflicts:
#	docs/DATADOG.md
#	src/lib/ffmpeg.ts
#	src/lib/tests/exportTelemetry.test.ts
export_cancelled carries stuck: true when the progress percentage hadn't
changed for 60 s, so monitors can count a user giving up on a hang as a
failure instead of an ordinary cancellation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added level:advanced Advanced level - 55 pts type:bug Bug fix type:design UI/UX design labels Nov 3, 2026
@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 type:docs Documentation 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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@magic-peach
magic-peach changed the base branch from fix/mt-export-hang to main November 3, 2026 15:57
@magic-peach magic-peach closed this Nov 3, 2026
@magic-peach magic-peach reopened this Nov 3, 2026
No content change: this branch already contains #1831's commits.

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 071fd12
🔍 Latest deploy log https://app.netlify.com/projects/reframe-os/deploys/6aea0495d3b0e90021f5a8ed
😎 Deploy Preview https://deploy-preview-1832--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.

@magic-peach
magic-peach marked this pull request as ready for review November 3, 2026 16:02
@magic-peach
magic-peach merged commit d342b6c into main Nov 3, 2026
20 of 22 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: 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:docs Documentation type:performance Performance type:testing Testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant