Menu
Sign up

Merge videos with FFmpeg: the concat demuxer, the concat filter, and adding a title card

Tested with FFmpeg 9.0.2: matching clips joined in 0.04 s, how mismatched clips broke the fast method, the filter that fixed it, and an image title card.

Published November 7, 2026 · 4 min read

Joining clips is the other everyday FFmpeg job, and like trimming it has a fast way and a safe way. The concat demuxer glues files end to end without re-encoding — instant, but only when every clip has the same format. The concat filter decodes everything and encodes one new video — slower, but it accepts clips of any size and frame rate. This page measures both, shows what happens when you use the fast one on the wrong files, and adds a title card made from an image.

Measured on 2026-11-07 with FFmpeg 9.0.2 (linuxserver/ffmpeg image). Test clips: A — 10 s, 1280×720, 30 fps, H.264 + AAC at 48 kHz; B — 3 s with the same settings; C — 3 s, 640×360, 25 fps, AAC at 44.1 kHz.

The fast way: concat demuxer

printf "file 'a.mp4'\nfile 'b.mp4'\n" > list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy out.mp4

The list file has one file '…' line per clip, in order; -safe 0 allows paths outside the current folder or with unusual characters. Joining A and B took 0.04 seconds and produced a 13.02-second 1280×720 file — no quality loss, no encoding.

The catch is in the documentation of the concat demuxer: "All files must have the same streams (same codecs, same time base, etc.)." It does not stop you when they differ.

What happens when the clips do not match

We ran the same command on A and C — a different resolution, frame rate and audio sample rate:

printf "file 'a.mp4'\nfile 'c.mp4'\n" > list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy out.mp4

FFmpeg did not stop. It printed a stream of warnings like "Non-monotonic DTS; previous: 152392, current: 127304 … This may result in incorrect timestamps in the output file", and wrote a file that:

  • claims to be 1280×720 at 30 fps throughout, while its last 3 seconds are 640×360 at 25 fps;
  • reports a duration of 11.97 s instead of 13;
  • carries 44.1 kHz audio in a stream labelled 48 kHz.

Players handle such files differently — some switch resolution mid-way, some freeze, some play the second part at the wrong speed or out of sync. Phone recordings, screen captures and exports from different apps rarely match exactly, so this is a common cause of "merged video freezes".

Check before joining:

ffprobe -v error -show_entries stream=codec_name,width,height,r_frame_rate,sample_rate -of csv=p=0 a.mp4

If any value differs between clips, use the filter below.

The safe way: concat filter

ffmpeg -i a.mp4 -i c.mp4 -filter_complex \
  "[1:v]scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=30[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]" out.mp4
  • scale … force_original_aspect_ratio=decrease fits the smaller clip into 1280×720 without distorting it; pad fills the rest with black bars, centred; setsar=1 makes the pixel shape square so concat accepts the inputs; fps=30 matches the frame rate.
  • aresample=48000 matches the audio sample rate.
  • concat=n=2:v=1:a=1 joins two segments with one video and one audio stream each; inputs are listed segment by segment, video then audio.

The result was exactly 13.000 s at 1280×720, in 1.54 seconds of encoding. For more clips, add inputs, normalise each the same way and raise n. The concat filter documentation lists its options, and the FFmpeg wiki's Concatenate page compares all three ways FFmpeg can join files, including the concat: protocol for formats such as MPEG-TS that can simply be appended.

Concat demuxer (-c copy) Concat filter
Speed (13 s result) 0.04 s 1.54 s
Re-encodes No Yes
Clips must match Exactly No — you normalise them
Our mismatched test Wrong duration, mixed formats Exact

A title card from an image

An intro or end card — the episode title, the guest, the date — is a still image turned into a few seconds of video. We tested this with a 1280×720 card in front of clip A:

ffmpeg -i a.mp4 -loop 1 -t 3 -framerate 30 -i card.png \
  -f lavfi -t 3 -i anullsrc=channel_layout=mono:sample_rate=48000 \
  -filter_complex "[1:v]scale=1280:720,setsar=1,format=yuv420p[intro]; \
                   [intro][2:a][0:v][0:a]concat=n=2:v=1:a=1[v][a]" \
  -map "[v]" -map "[a]" -c:v libx264 -c:a aac with-intro.mp4
  • -loop 1 -t 3 -framerate 30 -i card.png repeats the image as a 3-second, 30 fps video.
  • anullsrc makes 3 seconds of silence, because every concat segment needs an audio stream. Match its channel_layout and sample_rate to your video's audio (ffprobe shows them).
  • format=yuv420p converts the image's colours to what H.264 players expect.

The result was 13.000 seconds — 3 of card, 10 of video — at 1280×720. For an end card, list the card's segment last.

Making the card itself

FFmpeg joins the card; something has to draw it. We do not edit video, but we do make this kind of image: with Shareglint, a title card is a template — title, guest, date, a photo, your colours — rendered at 1280×720 with "size": "youtube", one request per episode, so every card matches without opening a design tool. The card in our test was the built-in video-thumbnail template:

curl -X POST "https://shareglint.com/v1/images?wait=true" \
  -H "Authorization: Bearer sk_live_..." -H "Content-Type: application/json" \
  -d '{ "template": "video-thumbnail", "size": "youtube", "scale": 1,
        "modifications": { "title": "I hiked the Alps", "highlight": "in 3 days", "channel": "Trail Notes" } }'

"scale": 1 returns the card at exactly 1280×720; without it you get 2560×1440, which the scale=1280:720 step in the command above brings down anyway. The same template, rendered once more, is the episode's YouTube thumbnail (YouTube thumbnails). Our images are opaque, so they work as full-frame cards and thumbnails, not as overlays on top of the video.

FAQ

How do I merge two videos with FFmpeg?

If they have the same codecs, resolution, frame rate and audio format, list them in a text file (file 'a.mp4' on each line) and run ffmpeg -f concat -safe 0 -i list.txt -c copy out.mp4. It copies without re-encoding: our 10 s + 3 s join took 0.04 seconds.

Why does my merged video freeze or have the wrong length?

The clips do not match. The concat demuxer with -c copy does not convert anything: joining a 1280×720 30 fps clip with a 640×360 25 fps clip printed Non-monotonic DTS warnings and produced a file that claimed 11.97 s instead of 13. Use the concat filter, which scales and re-encodes.

How do I join videos with different resolutions?

Use the concat filter and bring every input to the same size, frame rate and audio sample rate first: scale with force_original_aspect_ratio=decrease, pad to the target size, setsar=1 and fps. Our 13-second result came out exact, in 1.5 seconds.

How do I add an intro image to a video with FFmpeg?

Turn the image into a short clip with -loop 1 -t 3 -i card.png, add silent audio with anullsrc, and join it with the concat filter. We tested this with a 1280×720 title card in front of a 10-second video.