Export
The three export engines, which range gets exported, and the ffmpeg encoding profiles.
Export is the Export button → dialog, or fablecut_export from an agent (below). Three engines:
- Fast - browser renders each frame with the normal compositor (SVG, keys, AI masks), streams JPEGs + an offline WAV mix to the server; a single ffmpeg pass encodes them via an encoding profile into
./exports/. Quality / software path; keeps rendering if you switch tabs. - WebCodecs - same frame-accurate compositor loop, but the browser’s
VideoEncoderproduces Annex-B H.264 (Main 4:2:0) and the server stream-copies (-c:v copy) while muxing the WAV. Faster uploads, less server CPU. Requires Chromium-classVideoEncoderwithavc: { format: "annexb" }plus ffmpeg. Encoding profiles do not apply (the bitstream is already encoded). No ffmpeg-style CRF - quality is bitrate + VBR/CBR (export dialog; remembered in localStorage). OptionalbitrateMode: "quantizer"(fixed QP) exists in the spec but is rarely supported by hardware encoders with Annex-B. Unavailable while anexportFramecrop is set - use Fast for cropped delivery. - Realtime (MediaRecorder) - automatic offline fallback when the server, ffmpeg, or WebCodecs is unavailable. Plays the timeline once and records it; keep the tab focused.
Fast and WebCodecs export decode MP4 / MOV clips with WebCodecs (mp4-demux.js + VideoDecoder), frame-exact. Fragmented, rotated or undecodable files and bgRemove clips fall back to the <video> element.
The exported span is chosen in the Export dialog (Range: Entire timeline / IN–OUT). IN–OUT is the default when inPoint / outPoint are set; pick Entire timeline to keep those markers for split/trim. Effective IN/OUT export bounds are clamped to projDur so an IN past the last clip cannot produce a one-frame black file.
- Entire timeline (or no markers) → the whole timeline
- IN–OUT, IN only → from
inPointto the end - IN–OUT, OUT only → from the start to
outPoint - IN–OUT, both → the range between them
Agents export with fablecut_export (or POST /api/export/request). It is the same Fast export as the button - same compositor, audio mix and encoding profile - so it needs ffmpeg on PATH. The job goes to an open editor tab (the user sees the progress bar), or, with none open, the server starts a headless Chrome / Edge on the editor and closes it when the file is written (FABLECUT_CHROME picks the browser). The tab first loads the revision the agent last wrote, so an export right after a patch includes that patch.
fablecut_export {}- waits, then returns the file path underexports/. Range:inPoint→outPointwhen either is set, else the whole timeline;range:"entire"|"in-out"overrides.preset:"instagram-reels"picks a delivery preset;delivery:{…}changes any of its settings ({preset:"youtube", delivery:{fps:60, video:{rate:"vbr", bitrate:16000}}}).profile:"hq"picks a raw encoding profile instead. With neither,project.deliveryis used when set, elseproject.encodeProfile, else the server's default profile.outputs:trueexports the project's checked batch outputs;outputs:[{preset:"master"}, {preset:"youtube"}, {preset:"instagram-reels"}, …]exports a list. One render per frame rate; the reply lists every file (or why one failed).where:"headless"renders in the background and leaves the user's tab alone;where:"tab"refuses if no tab is open.- Long edits:
wait:falsereturns a job id at once; poll with{job:"x_…"}and stop one with{cancel:"x_…"}. One export runs at a time.
Delivery presets#
The Export dialog's Preset list. Each preset is a structured spec - no ffmpeg arguments to write - that delivery.js turns into one ffmpeg pass:
| Preset | Output |
|---|---|
youtube | 1920×1080, H.264 CRF 18, keyframe every 0.5 s, AAC 384k 48 kHz |
youtube-shorts | 1080×1920, as YouTube; warns past 3 min |
instagram-reels | 1080×1920, H.264 CRF 19 capped at 25 Mb/s, AAC 256k; ≤ 60 fps, warns past 3 min |
tiktok | 1080×1920, H.264 CRF 19 capped at 20 Mb/s, AAC 256k; warns past 10 min |
x | 1920×1080, H.264 CRF 20 capped at 25 Mb/s, AAC 192k; warns past 2:20 and 512 MB |
linkedin | 1920×1080, H.264 CRF 20 capped at 20 Mb/s, AAC 192k; warns past 15 min and 5 GB |
master (default) | the picture's own size, H.264 CRF 16, AAC 320k |
broadcast | 1920×1080 ProRes 422 HQ MOV, PCM 24-bit 48 kHz, Rec. 709, normalized to −23 LUFS / −1 dBTP (EBU R128); warns off 25 / 29.97 / 50 / 59.94 fps |
web | 1280×720 H.264 CRF 23, AAC 128k, fast start |
archive | the picture's own size, lossless FFV1 4:2:2 10-bit MKV, PCM 24-bit |
custom | your own settings |
A spec (every key optional - what is left out comes from its preset):
{
"preset": "youtube",
"width": 1920, "height": 1080, // null = the picture's own size (the export frame, else the canvas)
"fps": null, // null = the project's; 23.976 / 29.97 / 59.94 are exact NTSC rates
"reframe": "fill", // another shape: fill crops the middle (or the export frame), fit pads, stretch
"container": "mp4", // mp4 · mov · mkv · webm · mxf
"video": {
"codec": "h264", // h264 · hevc · av1 · vp9 · prores · dnxhr · mpeg2 (XDCAM HD422) · ffv1
"encoder": "libx264", // or h264_nvenc / h264_qsv / h264_amf … — listed only when they work here
"rate": "crf", // crf · vbr · cbr (ProRes, DNxHR, XDCAM, FFV1 are fixed)
"crf": 18, "bitrate": 12000, "maxrate": null, // kb/s; maxrate caps a CRF or VBR encode
"keyint": 2, // seconds between keyframes, 0 = the encoder's default
"speed": "balanced", // fast · balanced · best
"pixFmt": "yuv420p", // 4:2:0 / 4:2:2 / 4:4:4, 8 or 10 bit, as the codec allows
"profile": null // ProRes: proxy lt standard hq 4444 4444xq · DNxHR: lb sq hq hqx 444
},
"audio": { "codec": "aac", "bitrate": 320, "sampleRate": 48000, "channels": 2,
"loudness": null, "truePeak": -1 },
// aac · opus · mp3 · pcm16 · pcm24 · flac · none
// loudness: a LUFS target (−14 streaming, −16 podcast, −23 EBU R128, −24 ATSC) or null
"color": "bt709", // bt709 · bt709-full · bt601-625 (PAL) · bt601-525 (NTSC)
"filename": "{project}_{preset}_{res}",
// tokens: {project} {preset} {res} (1080p, 1080x1920, 1080sq) {w} {h} {fps} {codec} {date} {time} {range} {n}
"metadata": { "title": "", "artist": "", "description": "", "copyright": "", "date": "", "keywords": "" }
}- Impossible combinations are corrected, and the reply says how - ProRes moves to MOV, WebM takes Opus, Quick Sync gets 4:2:0. The dialog shows the same notes.
- Reframing happens in ffmpeg: the browser sends the whole canvas and ffmpeg crops the export frame (or the middle, for another shape) and scales. The dialog warns when the output is upscaled or cropped, when a platform's length, size or frame-rate limit is passed, and estimates the file size where it can (bitrate and fixed-rate codecs; CRF cannot know).
- Outputs that keep more than 4:2:0 8-bit (ProRes, DNxHR, FFV1, 10-bit, 4:2:2, 4:4:4) get raw RGBA frames instead of JPEG, so no chroma is lost on the way. Slower to send.
- The audio is cut to exactly the frames' length.
- Loudness (
audio.loudness): the mix is measured and turned to the target with ffmpeg's two-passloudnorm- one linear gain, and a true-peak limiter attruePeak. When the peaks leave no room for the gain it compresses instead and may land under the target; the check after the export says so. Off (null) leaves the mix as mixed - except Broadcast, which normalizes to −23 LUFS / −1 dBTP. Mixes shorter than 3 s may not reach the target. - Checked after the export. Every file is read back with ffprobe and measured (BS.1770 integrated loudness and true peak) and compared with what was asked: size, frame rate, frame count, codec, color tags, audio codec / rate / channels, length, loudness against the target (±1 LU) and the true-peak ceiling, and file size against the platform's limit. Without a target the report says where the platform plays it ("YouTube plays it 4.2 dB quieter"). The report shows in the queue, a toast after a single export, and the
fablecut_exportreply. - Saved presets: "Save preset…" in the dialog writes
library/delivery/<id>.json, listed under "Saved" in every project (GET / POST /api/export/presets,POST /api/export/presets/delete {id}). - The settings are checked by a 0.1 s ffmpeg dry run before anything renders.
- The dialog remembers the last settings in this browser.
setProject {delivery:"youtube"}(or a spec) pins them in the project, for the dialog and forfablecut_export. - Fast export only - WebCodecs and Realtime export encode their own way.
Batch outputs and the export queue#
One project → many files. project.outputs is the project's list of files to make (up to 16), each a delivery spec plus {id, on}. In the Export dialog, Batch outputs lists them: + Add these settings adds the preset shown above, + Typical set adds Master, YouTube 1080p, Reel 1080×1920, Square 1080 and a 720p preview, the checkbox picks which go out, Load shows one's settings above. Export checked queues the checked ones and starts.
// setProject {outputs:[…]} — a preset id or a spec each; stored complete with an id
{"op":"setProject","set":{"outputs":[
"master", "youtube",
{"preset":"instagram-reels"},
{"preset":"custom","width":1080,"height":1080,"filename":"{project}_square_{res}"},
{"preset":"web","filename":"{project}_preview_{res}","on":false}
]}}- Rendered once. Outputs with the same frame rate and range go out in ONE render: the compositor draws each frame once and every output's ffmpeg encodes it (up to 8 per render). Four 1080p files take about as long as one. Outputs at another frame rate get their own render. When any output needs full chroma (ProRes, 10-bit, lossless) the frames go as RGBA to all of them.
- The export frame is the picture for outputs of its own shape (and for the picture's own size). An output of another shape reframes the whole canvas - a project framed for 9:16 still makes a proper 16:9 file.
- The export queue (Export dialog → Add to queue, or Queue) runs waiting outputs back to back with a progress bar each, Stop, Retry for a failed one, Open for a finished file and Clear finished. It lives in the tab: a reload empties it. One output that fails does not stop the others in its render.
- REST:
POST /api/export/begin {deliveries:[{delivery, name}, …], canvasW, canvasH, region?, duration, fps, hasAudio}→{id, input, jpegQuality, outputs:[{file, spec, summary, notes, size}]}; frames and audio as for one output;/api/export/end→{src, outputs:[{file, ok, src, report:{ok, checks:[{ok, text}], line}, mixLoudness?} | {file, ok:false, error}]}.
Encoding profiles encoding-profiles.json#
User-editable at the repo root. A profile is a raw ffmpeg argument list plus the things that are not ffmpeg arguments: jpegQuality (browser frame quality), extension (output container), and optional color (output matrix / range tags). Edit the file while the server runs - the UI hot-reloads the profile list via an SSE profiles event (no full project reload). Fast export only; WebCodecs ignores them.
{
"default": "delivery", // profile id used when nothing else is set
"profiles": {
"draft": {
"label": "Draft · H.264 fast",
"description": "Quick preview — smaller file, faster encode.",
"jpegQuality": 0.85, // browser JPEG frame quality (0.1–1)
"extension": ".mp4",
// Output color (optional — defaults to BT.709 limited/tv). Independent of
// -pix_fmt: yuv420p and yuv422p10le both typically use bt709 for HD SDR.
"color": { "matrix": "bt709", "primaries": "bt709", "trc": "bt709", "range": "tv" },
"args": ["-c:v", "libx264", "-preset", "veryfast", "-crf", "23",
"-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "128k",
"-movflags", "+faststart", "-shortest"]
}
}
}Export is one ffmpeg pass. The server owns the input side and the output path; args is everything in between (plus JPEG color conversion derived from color):
ffmpeg -y -f image2pipe -framerate <fps> -i - [-i audio.wav] <jpeg-color> <args…> exports/<name><extension>- There is no allow-list. Any codec, filter, container or flag your local ffmpeg supports works - ProRes, DNxHD, NVENC/QSV/VideoToolbox, VP9/AV1, 10-bit, HDR tags. See the shipped
prores422,broadcast1080i50,nvenc-h264,nvenc-hevc,videotoolbox-h264andvideotoolbox-hevcprofiles (the NVENC ones need an NVIDIA GPU; VideoToolbox requires macOS / Apple Silicon). - Args are validated by ffmpeg itself, not by a schema: when an export starts the server dry-runs the profile against a 0.1 s synthetic input (
lavfi). A typo or an encoder your build lacks is rejected up front with ffmpeg's own message, instead of failing after every frame has been rendered. - Use the array form - each element is passed to
spawnuntouched, so no quoting is needed (["-vf", "drawtext=text='hi there'"]just works). A plain string is accepted and split on whitespace. colorcontrols JPEG→YUV conversion and stream tags (-colorspace/-color_primaries/-color_trc/-color_range). Default is BT.709 limited (range: "tv"). Use"range": "pc"for full-range masters. Do not put those flags inargs- the engine strips them and appliescolorso vf and tags stay aligned.-pix_fmtstays inargs(sampling / bit depth only).- Nothing else is injected for you:
+faststart,-shortest,-strict -2for Opus in MP4 and pixel-format choices are all yours to write. - Frames arrive as JPEG (4:2:0), so
yuv422p/yuv444pcannot recover chroma the source never had; raisejpegQualitybefore reaching for a wider pixel format. OptionalpixelFormat:"rgba"on/api/export/beginpipes uncompressed canvas frames instead (-f rawvideo) when a caller wants full chroma. - The audio mix is only present when the timeline has audio; with no audio there is a single input, so avoid hardcoded
-map 1:a.
Note: Fast export pipes composited JPEG frames from the browser (-f image2pipe), not -i source.mp4. Filters and codec settings apply to that frame stream. To transcode an existing file verbatim, run ffmpeg directly - that is outside the compositor path.
Encoding profiles are listed at the bottom of the Export dialog's Preset list.
What the Export dialog opens on (priority):
project.delivery, thenproject.encodeProfile- set withsetProject- The last preset or profile used in this browser
- The
masterdelivery preset
MCP: fablecut_encode_profiles lists profiles; {detail:true} includes each args array; {profile:"hq"} returns one profile. fablecut_status shows the effective profile.