Crossfade Between Clips with FFmpeg xfade (No Editor)

Faceless-channel cuts, podcast clip reels, ad variants: they all get assembled from parts, and they all look assembled. The tell is the joins. Every one is a hard cut, because concat is the only stitching most pipelines know and concat has exactly one transition, which is none.
Quick answer: FFmpeg xfade crossfades two clips in one command:ffmpeg -i a.mp4 -i b.mp4 -filter_complex "xfade=transition=fade:duration=1:offset=7" -c:a copy out.mp4. Theoffsetis a timestamp on the first input's timeline, so set it to (duration of clip A minus transition duration), and expect the output to be shorter than the sum of the parts by one transition length per join. If you'd rather not compute offsets, normalize resolutions, and chain a filter per clip, send the clip URLs and a transition name to FFmpeg Micro as one API call and download the finished cut.
The common belief is that smooth transitions mean opening an editor, and most pipelines act on it: render the clips headlessly, then hand them to a human with Premiere or a template service. That's partly fair, because a hard cut really does read as cheap. But what stops people from doing it in FFmpeg isn't the filter. It's arithmetic. xfade has been in FFmpeg since 4.3 (June 2020) and it does the job in a single line. Nearly every broken result traces back to one misunderstanding of what offset measures.
The offset is a timestamp on the first input, not a gap
FFmpeg's xfade filter takes offset as the moment the blend begins, measured from the start of the first input. It is not the gap between clips, and it is not "how far into clip B." Set offset=0 and FFmpeg starts dissolving into clip B at the first frame of clip A, which is the wrong result people report in forum threads.
The rule is one subtraction: offset = duration of clip A − transition duration.
So an 8-second hook and a 6-second body with a 1-second dissolve gives you offset=7:
ffmpeg -i hook.mp4 -i body.mp4 \
-filter_complex "[0:v][1:v]xfade=transition=fade:duration=1:offset=7[v]" \
-map "[v]" -c:v libx264 -crf 20 -preset medium -pix_fmt yuv420p out.mp4
Get real durations from ffprobe rather than trusting the intended length, because a "10 second" export from a browser recorder is routinely 10.033:
ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 hook.mp4
If your offset lands past the end of clip A, FFmpeg freezes on A's last frame until the transition point arrives. That frozen half-second is the same subtraction done with a wrong duration.
Your output is shorter than the sum of your clips
An xfade join consumes time from both sides. During the overlap, clip A and clip B play at the same moment, so an 8-second clip plus a 6-second clip with a 1-second transition produces a 13-second file, not 14. The formula for a whole sequence is total = sum of all clip durations − (number of clips − 1) × transition duration.
Five 12-second clips joined with 0.75-second transitions give you 57 seconds, not 60, the difference between fitting a 60-second Reels slot and not. If a downstream step assumes concatenated length, it drifts by exactly one transition per join. Cut the clips a little long and let the transitions eat the slack.
Pick a transition by what it hides, not by what it looks like
Run ffmpeg -h filter=xfade and your build will list north of 50 transition names. You don't need most of them. In assembled content, the transition's job is to hide a mismatch between two shots, and that narrows the choice fast.
| Transition | Use it for | Typical duration |
|---|---|---|
| `fade` | Same-subject cuts, talking head to talking head | 0.3 to 0.5s |
| `fadeblack` | A real scene change or chapter break | 0.5 to 1.0s |
| `dissolve` | B-roll to B-roll where motion differs | 0.5s |
| `wipeleft` / `slideleft` | Sequential steps, listicles, before/after | 0.4 to 0.6s |
| `smoothleft` | Ad variants where you want motion without a hard edge | 0.4s |
| `circleopen` / `radial` | Reveal beats, product shots | 0.6 to 0.8s |
Keep durations short. A 1-second transition at 30 fps is 30 blended frames, which on a 12-second clip is 8% of the runtime spent looking at neither shot. Faceless-channel edits that read as professional sit between 0.3 and 0.5 seconds. Anything over 1 second reads as a 2009 slideshow unless it's a deliberate act break.
xfade is video only, so audio needs acrossfade
The xfade filter touches video streams and nothing else. If you ignore audio, FFmpeg maps one input's track, and everything after the first join is out of sync. The audio equivalent is acrossfade, and it takes no offset, because it always blends the end of the first stream into the start of the second.
ffmpeg -i hook.mp4 -i body.mp4 -filter_complex "\
[0:v][1:v]xfade=transition=fade:duration=1:offset=7[v]; \
[0:a][1:a]acrossfade=d=1:c1=tri:c2=tri[a]" \
-map "[v]" -map "[a]" -c:v libx264 -crf 20 -c:a aac -b:a 192k out.mp4
Use the same d value for acrossfade as the duration in xfade. Because acrossfade also shortens its output by the overlap, matching the two keeps audio and video the same length. Mismatch them by 0.5 seconds across four joins and you have 2 seconds of lip-sync error by the end.
The c1 and c2 options set the fade curves. tri is a linear ramp, fine for speech. For a music bed under both clips, qsin on both sides holds perceived loudness steadier through the middle of the blend, where two linear ramps dip. If your clips came from different sources, normalize before you fade: aresample=48000,aformat=channel_layouts=stereo on each input. Loudness differences between clips are a separate job, covered in normalizing audio loudness across a library.
Chaining more than two clips is where people give up
Every xfade instance takes exactly two inputs, so joining N clips means N−1 chained filters, and each offset is computed against the growing intermediate rather than the original clip.
The formula for the nth transition: offset_n = (sum of clip durations 1 through n) − (n × transition duration).
With three 10-second clips and a 1-second transition, that's offset=9 for the first join and offset=18 for the second, producing a 28-second output. Here it is with the normalization every real pipeline needs:
ffmpeg -i a.mp4 -i b.mp4 -i c.mp4 -filter_complex "\
[0:v]scale=1920:1080:force_original_aspect_ratio=decrease,\
pad=1920:1080:-1:-1,setsar=1,fps=30,format=yuv420p,settb=AVTB[v0]; \
[1:v]scale=1920:1080:force_original_aspect_ratio=decrease,\
pad=1920:1080:-1:-1,setsar=1,fps=30,format=yuv420p,settb=AVTB[v1]; \
[2:v]scale=1920:1080:force_original_aspect_ratio=decrease,\
pad=1920:1080:-1:-1,setsar=1,fps=30,format=yuv420p,settb=AVTB[v2]; \
[v0][v1]xfade=transition=fade:duration=1:offset=9[x1]; \
[x1][v2]xfade=transition=wipeleft:duration=1:offset=18[v]; \
[0:a][1:a]acrossfade=d=1:c1=tri:c2=tri[a1]; \
[a1][2:a]acrossfade=d=1:c1=tri:c2=tri[a]" \
-map "[v]" -map "[a]" -c:v libx264 -crf 20 -preset medium \
-c:a aac -b:a 192k out.mp4
That's a 3-clip cut. A podcast reel with 9 pull-quotes needs 8 chained xfade filters, 8 chained acrossfade filters, 9 normalization chains, and 8 offsets that all shift the moment one clip's duration changes. This is where teams stop templating the string and start generating it, and where a hosted composition step earns its keep: with FFmpeg Micro you post the ordered clip URLs plus a transition name and duration, and the offset chain, the scaling, and the audio crossfades are computed for you. You get a job back, poll it or take a webhook, and download the render. No servers to run and no filtergraph to regenerate every time an editor swaps a clip.
| Raw FFmpeg | One API call | |
|---|---|---|
| Offset arithmetic | You compute N−1 offsets | Handled from clip order |
| Mismatched sources | You write the scale/pad/fps/settb chain | Normalized on ingest |
| Audio | You chain `acrossfade` separately | Carried with the video |
| Long renders | Your process, your timeout | Job semantics, webhook on done |
| Reordering clips | Rebuild the whole filtergraph | Reorder the input list |
Pitfalls that produce a broken render, not an error
Most xfade failures either abort with a specific message or produce a file that's silently wrong. These are worth recognizing on sight.
- "do not match the corresponding second input link parameters."
xfaderequires both inputs to share width, height, pixel format, and frame rate. Mixing a 1920x1080 export with a 1280x720 screen recording fails here. Thescale/pad/setsar/fps/formatchain above is the fix, and it runs on every input. - Stuttering or off-by-a-frame transitions. Variable frame rate sources, which is most phone footage and every OBS recording, place frames at irregular timestamps, so the offset lands on the wrong one. Force
fps=30on each input before the filter. - Timebase mismatch after a trim. If you trimmed clips upstream, add
settb=AVTBto each normalization chain. Without it, offsets computed in seconds can resolve to different frames per input. -c copydoes nothing useful here. Any transition re-encodes both clips. Budget for it: a 3-minute 1080p assembly at-preset medium -crf 20is a real encode, not a remux.- The last clip is too short. The second input must be at least as long as the transition duration, or the blend runs out of frames mid-fade.
- Only the first clip's audio survives. If you mapped
0:ainstead of chainingacrossfade, that's the bug.
When a crossfade is the wrong answer
Not every join wants a transition. Dialogue cuts inside a single interview should stay hard, because a dissolve between two shots of the same person reads as a mistake. Fast-cut social edits live on hard cuts by design. If you're only appending an unchanged intro and outro to a body clip, concat with a stream copy is faster and lossless, the approach in adding an intro and outro automatically.
If what you need is keyframed motion, per-shot color grading, or a designer-controlled layout that non-technical staff edit, a filtergraph is the wrong abstraction and so is any render API. That's a template-editor product, and you should use one. xfade is for pipelines where the clip order is decided by code, which is where long-form-to-shorts workflows and clip reels live.
FAQ
Why is my xfade output shorter than my clips?
An xfade transition overlaps the two clips, so the output loses one transition duration per join. Three 10-second clips joined with 1-second transitions produce 28 seconds, not 30. Compute expected length as the sum of durations minus (clips − 1) × transition duration.
Does ffmpeg xfade handle audio?
The xfade filter operates on video streams only and ignores audio. Use the acrossfade audio filter with the same duration value, chained once per join, and map both the video and audio outputs explicitly with -map.
What FFmpeg version do I need for xfade?
The xfade filter landed in FFmpeg 4.3, released in June 2020, so any current build has it. The transition list has grown across releases, so run ffmpeg -h filter=xfade to see which names your build supports before scripting one. If ffmpeg isn't resolving at all, that's usually a PATH problem rather than a missing install.
Can I crossfade videos with different resolutions?
FFmpeg refuses to run xfade on inputs with different width, height, pixel format, or frame rate, and reports that the input link parameters don't match. Scale and pad every input to identical dimensions first, then set a common fps and format=yuv420p before the transition.
How do I crossfade more than two clips in one command?
Chain one xfade filter per join, feeding each filter's output into the next as its first input. The nth transition's offset equals the sum of the first n clip durations minus n times the transition duration, because each join shortens the running timeline it's measured against.
If you're generating those offset chains in n8n, Make, or a script, and the filtergraph rebuilds every time a clip length changes, the multi-clip crossfade is one job submission instead: ordered clip URLs, a transition name, a duration. Sign up free and run one against your own footage on the free tier.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

FFmpeg Two-Pass Encoding vs CRF: When It Actually Helps
FFmpeg two-pass encoding hits an exact file size that CRF can't guarantee. The -pass 1 and -pass 2 commands, when two-pass beats CRF, and the real pitfalls.

Create an HLS stream with FFmpeg, no media server needed
Create HLS stream FFmpeg commands for .m3u8 playlists and .ts segments, the CDN setup that makes them play, and the one-call API that skips the encoder box.

Overlay images on video: skip the editor, use one API call
Overlay images on video with one API call or raw FFmpeg: logo stamps, lower thirds, picture-in-picture, timed fades, and the alpha and scale2ref gotchas.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free