Merge videos with FFmpeg
There are two joins. The concat demuxer copies streams from a list of files in a fraction of a second when the clips share codec, resolution and frame rate. The concat filter re-encodes and can normalise clips that do not match, and xfade adds a dissolve.
Verified with FFmpeg 9.0.1 on 26 August 2026. Search intent: ffmpeg concat videos.
ffmpeg -f concat -safe 0 -i list.txt -c copy merged.mp4Reads list.txt (one line per clip: file 'clip1.mp4'), appends the packets of each file and copies them without decoding. A 12 s clip followed by a 4.5 s clip became a 16.52 s file with 495 frames, in well under a second.
Flags, explained
- -f concat
- Use the concat demuxer, which reads a text list of files instead of a media file.
- -safe 0
- Allow absolute paths and paths with special characters in the list. Without it, anything outside the current directory is rejected as unsafe.
- file 'clip.mp4'
- One line per clip in list.txt, in playback order. Single quotes around the path; escape a quote inside it as '\''.
- -c copy
- Copy every stream. Requires identical codecs, dimensions, frame rate, pixel format, sample rate and channel layout across all clips.
- concat=n=2:v=1:a=1
- The concat filter: n inputs, each providing one video and one audio stream, in the order of the labels before it.
- xfade=transition=fade:duration=1:offset=11
- Dissolve of 1 s starting 11 s into the first input. Other transitions: wipeleft, slideup, circleopen and about 50 more.
- acrossfade=d=1
- The audio counterpart: a one-second crossfade between the two audio streams.
Variants
Clips with different size, frame rate or sample rate (concat filter)
ffmpeg -i first.mp4 -i second.mp4 -filter_complex "[1:v]scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2,fps=30,setsar=1[v1];[1:a]aresample=48000[a1];[0:v][0:a][v1][a1]concat=n=2:v=1:a=1[v][a]" -map "[v]" -map "[a]" -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k merged.mp4The second clip (854x480, 25 fps, 44.1 kHz audio) is scaled and padded to 1280x720, converted to 30 fps and resampled to 48 kHz before the concat filter joins it to the first. Everything is re-encoded. Output: 1280x720, 510 frames, 17.000 s, 4.9 MB.
Crossfade between two clips (xfade + acrossfade)
ffmpeg -i first.mp4 -i second.mp4 -filter_complex "[0:v][1:v]xfade=transition=fade:duration=1:offset=11[v];[0:a][1:a]acrossfade=d=1[a]" -map "[v]" -map "[a]" -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k merged.mp4A one-second dissolve that starts at 11 s, i.e. one second before the 12 s first clip ends. offset must be first_duration minus duration, so probe the first clip. Both inputs must already share size and frame rate. Output: 12 + 4.5 - 1 = 15.5 s, 465 frames.
Pitfalls we hit
- The concat demuxer does not check that clips match. Joining a 1280x720 30 fps clip with an 854x480 25 fps clip with -c copy produced a file that reported 15.64 s and threw 6 decoder errors on playback. Different sources go through the concat filter.
- A relative path in list.txt is resolved from the directory of the list file, not from where you run ffmpeg.
- Timestamps must be continuous. Clips cut with -c copy that do not start on a keyframe can leave a gap or a frozen frame at each join; re-encode those clips before joining them.
- xfade needs the offset in seconds from the start of the first clip. A wrong offset silently shortens or lengthens the output instead of failing.
How we verified it
Run with FFmpeg 9.0.1 on 26 August 2026. Concat demuxer with -c copy on a 12 s and a 4.5 s 1280x720 H.264/AAC clip: 495 frames, 16.521 s, 7,240,867 bytes. Concat filter with a mismatched 854x480 25 fps 44.1 kHz second clip normalised to 1280x720, 30 fps, 48 kHz: 510 frames, 17.000 s, 4,875,664 bytes. xfade 1 s at offset 11 plus acrossfade: 465 frames, 15.500 s, 5,400,884 bytes. Demuxer with mismatched clips and -c copy: 485 frames, 15.64 s reported, 6 decode errors when read back.
The same edit as a typed operation
merge_videos probes every input, normalises size, frame rate and audio to one target, then renders the join in a single pass, with an optional crossfade whose offsets are computed from the probed durations. Mismatched clips are the normal case, not an error.
{
"inputs": [
{ "id": "main", "url": "https://example.com/intro.mp4" },
{ "id": "clip-2", "url": "https://example.com/demo.mp4" }
],
"options": { "aspect_ratio": "16:9" }
}FAQ
Why does my merged video freeze or glitch at the joins?
The clips do not share the same codec parameters, or they were cut with stream copy and start between keyframes. Re-encode the clips to one format first, or join them with the concat filter, which decodes and re-encodes everything.
Can FFmpeg concatenate MP4 files without re-encoding?
Yes, with the concat demuxer and -c copy, provided every file has the same codec, resolution, frame rate, pixel format and audio parameters. Our 16.5 s join finished in a fraction of a second and lost nothing.
How do I add a transition between two clips?
Use the xfade filter for video and acrossfade for audio, with offset set to the first clip duration minus the transition duration. The inputs must already have the same size and frame rate, so scale them first if needed.
What if one clip has no audio?
The concat filter requires the same number of streams per input. Generate silence for the silent clip with anullsrc and trim it to the clip length, or drop audio from all clips with a=0 in the filter.
Related commands
- ffmpeg trim videoTrim a video with FFmpegRead
- ffmpeg resize videoResize a video with FFmpegRead
- ffmpeg speed up videoSpeed up or slow down a video with FFmpegRead