# 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 2026-11-07 · https://shareglint.com/en/guides/ffmpeg-merge-videos

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

```bash
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](https://ffmpeg.org/ffmpeg-formats.html#concat): "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:

```bash
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:

```bash
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

```bash
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](https://ffmpeg.org/ffmpeg-filters.html#concat-filter) lists its options, and the FFmpeg wiki's [Concatenate page](https://trac.ffmpeg.org/wiki/Concatenate) 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:

```bash
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:

```bash
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](https://shareglint.com/en/use-cases/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.
