MP4/GIF export used to importScripts() an 18.5 MB asm.js ffmpeg build from
https://archive.org/download/ffmpeg_asm/ffmpeg_asm.js: no integrity check, no
pinning, executed in the page, and unavailable offline. vendor.mjs now copies
ffmpeg.wasm out of node_modules, where package-lock.json pins it by hash, and
no CDN fallback is left anywhere in the app.
@ffmpeg/core-st is the single-threaded core, chosen deliberately: the default
@ffmpeg/core is built with pthreads and needs SharedArrayBuffer, which requires
COOP/COEP isolation, which would break the Pixabay, Unsplash and Google Fonts
requests. That core also forces two things worth knowing:
- mainName: 'main' is mandatory. The loader defaults to proxy_main, which only
the multi-threaded build exports, so load() compiles all 23 MB and then aborts.
- Its main() calls exit(), so an instance survives exactly one command. Reusing
one dies with "Program terminated with exit(0)", so convertStreams builds and
tears one down per conversion (~110 ms, and the 23 MB heap comes back in
between). The teardown also runs on failure: an interrupted run otherwise
leaves the loader's "running" flag set and wedges every later conversion until
a page reload.
MP4 encodes with libx264 -crf 23 -pix_fmt yuv420p plus AAC rather than
mpeg4 -b:v 6400k. Same core, better quality per byte, and yuv420p is what makes
it play in Safari and QuickTime.
The two @ffmpeg packages are dependencies, not devDependencies, so the Docker
vendor stage can npm ci --omit=dev without pulling in electron; build.files
excludes them from the asar since src/vendor/ffmpeg/ already carries the copies
the app loads. WITH_FFMPEG=0 now means MP4/GIF export is unavailable and says
so, rather than silently fetching an encoder at run time.
Also deletes src/js/libraries/ffmpeg.min.js, an unreferenced ffmpeg.wasm loader
stub that would have fetched its core from unpkg, and prunes the stale
src/vendor/ffmpeg_asm.js from existing checkouts — src/vendor/ is packaged
whole, so it would have shipped 18.5 MB of dead weight in every installer.
Verified in Chromium against a real MediaRecorder WebM: core loads with
crossOriginIsolated false, MP4 24 KB decoding to 320x240 / 2.00 s, GIF 138 KB,
the two back to back, and the missing-core path reporting correctly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
electron/main.js starts the same static server the bare-metal target uses on
127.0.0.1 with a random port and points the window at it, rather than calling
loadFile(). Chromium exposes WebCodecs and IndexedDB only in a secure context:
over file:// the exporter would fall back to real-time MediaRecorder capture
and projects would stop saving, both silently.
The renderer runs sandboxed with no node integration, denies every permission
request, and hands http(s) navigations to the real browser.
PACKAGING.md documents all three distribution targets, including the NSIS
"Access denied" failure caused by an endpoint protection agent locking the
freshly written unsigned exe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>