Problem
Exporting a video takes far too long — even short clips can sit at single-digit progress for minutes. The screenshot below shows the overlay stuck at 4% with no indication of how long has elapsed or how long remains.
The target is: any export ≤ 5 minutes of video should complete in under 5 minutes on a modern laptop.
Additionally, when export finishes there is no record of how long it took — users have no way to benchmark their settings or know if something went wrong silently.
Root Causes in src/lib/ffmpeg.ts
1. -preset medium (the biggest offender)
Both MP4 and MKV use libx264 with -preset medium:
// current — slow
args.push("-c:v", "libx264", "-crf", String(recipe.quality), "-preset", "medium");
medium is one of the slowest x264 presets. In a single-threaded WebAssembly environment it is extremely costly. Switching to ultrafast or veryfast gives a 3–8× speed improvement with minimal visual quality loss at typical CRF values (18–28).
Fix:
args.push("-c:v", "libx264", "-crf", String(recipe.quality), "-preset", "ultrafast");
Optionally expose veryfast / ultrafast / medium as a user-selectable "Encode speed" toggle (Fast = ultrafast, Balanced = veryfast, Quality = medium).
2. VP9 has no speed flag
WebM exports use libvpx-vp9 with no -cpu-used flag, defaulting to the slowest quality mode:
// current — no speed hint, defaults to -cpu-used 1 (very slow)
args.push("-c:v", "libvpx-vp9", "-b:v", "0", "-crf", String(recipe.quality));
VP9 with -cpu-used 8 is 10× faster than the default, with acceptable quality for web use. -cpu-used 4 is a good balanced default.
Fix:
args.push("-c:v", "libvpx-vp9", "-b:v", "0", "-crf", String(recipe.quality), "-cpu-used", "4", "-deadline", "realtime");
3. eq filter fires even at neutral defaults
The video filter always appends the eq filter:
filters.push(
`eq=brightness=${recipe.brightness}:contrast=${recipe.contrast}:saturation=${recipe.saturation}`
);
When all values are at their defaults (brightness=0, contrast=0, saturation=0) this still adds a full processing pass. Skip it when nothing has changed:
Fix:
const needsEq =
recipe.brightness !== 0 ||
recipe.contrast !== 0 ||
recipe.saturation !== 0;
if (needsEq) {
filters.push(`eq=brightness=${recipe.brightness}:contrast=${recipe.contrast}:saturation=${recipe.saturation}`);
}
Note (separate bug): FFmpeg's eq filter uses contrast=1.0 and saturation=1.0 as neutral, not 0. The current DEFAULT_RECIPE sets both to 0, which produces a black, grayscale image when the filter runs. This should also be fixed: change DEFAULT_RECIPE.contrast and DEFAULT_RECIPE.saturation to 1 in src/lib/types.ts, or remap the slider range so the UI "0" maps to FFmpeg "1".
4. Single-threaded WebAssembly (stretch goal)
FFmpeg.wasm defaults to @ffmpeg/core (single-threaded). The multi-threaded core (@ffmpeg/core-mt) uses SharedArrayBuffer and can use all available CPU cores, giving 2–4× additional speedup on multi-core machines.
This requires COOP/COEP headers, which are already partially set in vercel.json. Check whether Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp are both set, then switch the core URL to @ffmpeg/core-mt.
Reference: https://ffmpegwasm.netlify.app/docs/getting-started/usage/#multi-thread
Feature: Show Export Elapsed Time
During export
Show a live elapsed time counter in the export overlay:
Processing your video locally. 0:42 elapsed
████████░░░░░░░░░░░░ 42%
- Start the timer when
status transitions to "exporting"
- Display as
M:SS (e.g. 1:23 elapsed)
- Use a
setInterval in ExportOverlay.tsx or derive from a startTime ref in useVideoEditor.ts
On the success screen
Show total export duration on the download result card:
✓ Export complete
video-reframed.mp4 · 42 MB · 1920×1080
⏱ Exported in 1 min 23 sec
- Store
exportStartTime in a ref when export begins
- On success, compute
Date.now() - exportStartTime and include it in the result state or display it in DownloadResult.tsx
Files to Modify
src/lib/ffmpeg.ts — change -preset, add -cpu-used for VP9, skip eq at defaults
src/lib/types.ts — fix DEFAULT_RECIPE.contrast and DEFAULT_RECIPE.saturation (see note above)
src/components/ExportOverlay.tsx — add live elapsed time counter
src/components/DownloadResult.tsx — show total export duration
src/hooks/useVideoEditor.ts — track exportStartTime ref, pass duration to result state
vercel.json — verify COOP/COEP headers for potential multi-thread upgrade
Acceptance Criteria
Screen Recording Required
Your PR must include two screen recordings:
- Before: record an export of a 1–2 minute video with the current code, showing how long it takes (the elapsed time on your system clock or a timer app visible in the recording)
- After: record the same export with your fix applied, showing the reduced time
This before/after recording is how maintainers will verify the performance improvement is real.
See CONTRIBUTING.md for how to record on your OS.
Problem
Exporting a video takes far too long — even short clips can sit at single-digit progress for minutes. The screenshot below shows the overlay stuck at 4% with no indication of how long has elapsed or how long remains.
The target is: any export ≤ 5 minutes of video should complete in under 5 minutes on a modern laptop.
Additionally, when export finishes there is no record of how long it took — users have no way to benchmark their settings or know if something went wrong silently.
Root Causes in
src/lib/ffmpeg.ts1.
-preset medium(the biggest offender)Both MP4 and MKV use libx264 with
-preset medium:mediumis one of the slowest x264 presets. In a single-threaded WebAssembly environment it is extremely costly. Switching toultrafastorveryfastgives a 3–8× speed improvement with minimal visual quality loss at typical CRF values (18–28).Fix:
Optionally expose
veryfast/ultrafast/mediumas a user-selectable "Encode speed" toggle (Fast = ultrafast, Balanced = veryfast, Quality = medium).2. VP9 has no speed flag
WebM exports use libvpx-vp9 with no
-cpu-usedflag, defaulting to the slowest quality mode:VP9 with
-cpu-used 8is 10× faster than the default, with acceptable quality for web use.-cpu-used 4is a good balanced default.Fix:
3.
eqfilter fires even at neutral defaultsThe video filter always appends the
eqfilter:When all values are at their defaults (
brightness=0, contrast=0, saturation=0) this still adds a full processing pass. Skip it when nothing has changed:Fix:
4. Single-threaded WebAssembly (stretch goal)
FFmpeg.wasm defaults to
@ffmpeg/core(single-threaded). The multi-threaded core (@ffmpeg/core-mt) usesSharedArrayBufferand can use all available CPU cores, giving 2–4× additional speedup on multi-core machines.This requires COOP/COEP headers, which are already partially set in
vercel.json. Check whetherCross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corpare both set, then switch the core URL to@ffmpeg/core-mt.Reference: https://ffmpegwasm.netlify.app/docs/getting-started/usage/#multi-thread
Feature: Show Export Elapsed Time
During export
Show a live elapsed time counter in the export overlay:
statustransitions to"exporting"M:SS(e.g.1:23 elapsed)setIntervalinExportOverlay.tsxor derive from astartTimeref inuseVideoEditor.tsOn the success screen
Show total export duration on the download result card:
exportStartTimein a ref when export beginsDate.now() - exportStartTimeand include it in the result state or display it inDownloadResult.tsxFiles to Modify
src/lib/ffmpeg.ts— change-preset, add-cpu-usedfor VP9, skipeqat defaultssrc/lib/types.ts— fixDEFAULT_RECIPE.contrastandDEFAULT_RECIPE.saturation(see note above)src/components/ExportOverlay.tsx— add live elapsed time countersrc/components/DownloadResult.tsx— show total export durationsrc/hooks/useVideoEditor.ts— trackexportStartTimeref, pass duration to result statevercel.json— verify COOP/COEP headers for potential multi-thread upgradeAcceptance Criteria
-preset ultrafast(orveryfast) is used for libx264-cpu-used 4 -deadline realtimeis used for libvpx-vp9eqfilter is skipped when brightness/contrast/saturation are all at neutral defaults0:42 elapsed)Exported in 1 min 23 sec)bun run lintandbunx tsc --noEmitpass with no errorsScreen Recording Required
Your PR must include two screen recordings:
This before/after recording is how maintainers will verify the performance improvement is real.