FFmpeg Merge Videos: Concatenate Clips (3 Methods + API)

March 1, 2026 ยท RenderIO

The two-line solution to ffmpeg merge videos

You have clips. You want to merge videos with FFmpeg into a single file. Here's the fastest way:

Create a text file listing your clips:

# files.txt
file 'clip1.mp4'
file 'clip2.mp4'
file 'clip3.mp4'

Then run:

ffmpeg -f concat -safe 0 -i files.txt -c copy output.mp4

The -c copy flag means FFmpeg copies the streams without re-encoding. It finishes in seconds regardless of file size because it's just stitching bytes together.

The catch: this only works when your clips share the same codec, resolution, and frame rate. If they don't, you'll get garbled output or a cryptic error about non-monotonous DTS timestamps. The rest of this guide covers what to do when the simple path breaks down.

Three ways to ffmpeg concat videos

FFmpeg has three mechanisms for joining clips. Each handles different situations.

Method 1: the concat demuxer (same-format clips)

The concat demuxer is the go-to when your clips are identical in format. Same codec, same resolution, same frame rate. This is common when your clips come from the same camera or the same export preset.

# Create the file list
printf "file '%s'\n" clip1.mp4 clip2.mp4 clip3.mp4 > files.txt

# Merge without re-encoding
ffmpeg -f concat -safe 0 -i files.txt -c copy merged.mp4

The -safe 0 flag lets FFmpeg read files from any directory path. Without it, you'll get an "unsafe file name" error if your paths contain special characters or aren't relative to the text file.

Use this for clips from the same source: surveillance footage split into hourly chunks, screen recordings broken up by your capture tool. Anything where the format is guaranteed identical.

It breaks when clips have different codecs, or when one clip is 1080p and another is 720p, or audio sample rates don't match. The demuxer will either produce corrupted output or fail outright.

Method 2: the concat filter (different formats)

When clips have different codecs, resolutions, or frame rates, you need the concat filter. It re-encodes everything into a uniform output, which takes longer but actually works.

ffmpeg -i clip1.mp4 -i clip2.mov -i clip3.webm \
  -filter_complex "[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[outv][outa]" \
  -map "[outv]" -map "[outa]" \
  -c:v libx264 -crf 23 -c:a aac -b:a 128k \
  merged.mp4

What each piece does:

  • [0:v][0:a] grabs the video and audio from the first input

  • concat=n=3:v=1:a=1 joins 3 inputs, each with 1 video and 1 audio stream

  • -map "[outv]" -map "[outa]" routes the concatenated streams to the output

  • The encoder flags (libx264, crf 23, aac) control output quality

The filter handles resolution differences too. If clip1 is 1920x1080 and clip2 is 1280x720, you'll want to scale them to a common size first:

ffmpeg -i clip1.mp4 -i clip2.mp4 \
  -filter_complex \
    "[0:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v0]; \
     [1:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v1]; \
     [v0][0:a][v1][1:a]concat=n=2:v=1:a=1[outv][outa]" \
  -map "[outv]" -map "[outa]" \
  -c:v libx264 -crf 23 -c:a aac merged.mp4

The pad filter adds black bars (letterboxing/pillarboxing) if the aspect ratios differ, so you don't get stretched video. If you'd rather crop instead of pad, swap pad for crop in the filter chain.

Use this for mixed sources: a clip from your phone plus one from a screen recorder, videos in different containers or codecs.

The trade-off is time. Re-encoding takes a few minutes for short clips, potentially hours for long ones. Output quality depends on your encoder settings. For guidance on picking the right CRF and preset, the video transcoding guide goes deep on codec selection.

Method 3: the concat protocol (MPEG-TS only)

The concat protocol is a third option that works at the file level:

ffmpeg -i "concat:intermediate1.ts|intermediate2.ts" -c copy merged.mp4

The pipe character (|) separates file paths. This approach works without a file list, which makes it convenient for scripting. But it only works reliably with MPEG-TS streams. MP4 files use a container format that stores metadata (the moov atom) at the beginning or end of the file, and the protocol can't parse that correctly when concatenating.

If your sources are MP4, you'd need to convert them to TS first:

ffmpeg -i clip1.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts intermediate1.ts
ffmpeg -i clip2.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts intermediate2.ts
ffmpeg -i "concat:intermediate1.ts|intermediate2.ts" -c copy -bsf:a aac_adtstoasc merged.mp4

That's three commands instead of one. For MP4 files, the concat demuxer is simpler. The protocol is mostly useful when you already have .ts segments, like HLS output or live stream recordings.

Which method should you use?

Keep it simple:

  • Same format, same everything? Concat demuxer. Fast, no quality loss.

  • Different codecs or resolutions? Concat filter. Slower, but handles anything.

  • Already working with .ts segments? Concat protocol. Niche but useful for HLS workflows.

The demuxer handles probably 80% of real-world merging. You only reach for the filter when formats don't match, and you almost never need the protocol unless you're working with streaming segments.

Merging a whole directory of clips

If you have 50 clips in a folder and want them merged in alphabetical order:

# Generate the file list
for f in *.mp4; do echo "file '$f'" >> files.txt; done

# Merge
ffmpeg -f concat -safe 0 -i files.txt -c copy merged.mp4

For numbered files (clip001.mp4, clip002.mp4, ...), alphabetical sorting gets the order right automatically. If your filenames don't sort naturally, generate the list manually or use ls -v to sort by version number:

ls -v *.mp4 | while read f; do echo "file '$f'"; done > files.txt

Watch out for a common gotcha: if files.txt already exists from a previous run, the >> append operator will add duplicates. Use > (overwrite) instead of >> (append), or delete the file first.

Troubleshooting common merge failures

Merging sounds simple until something goes wrong. Here are the errors that come up constantly and how to fix each one.

"Non monotonous DTS in output stream"

This means timestamps are out of order. It usually happens when clips were trimmed imprecisely or have slightly different durations in their audio vs video streams. Fix it by resetting timestamps:

ffmpeg -f concat -safe 0 -i files.txt -c copy -fflags +genpts -movflags +faststart merged.mp4

The -fflags +genpts flag tells FFmpeg to regenerate presentation timestamps. If that doesn't help, try adding -avoid_negative_ts make_zero as well:

ffmpeg -f concat -safe 0 -i files.txt -c copy -fflags +genpts -avoid_negative_ts make_zero merged.mp4

If timestamps are still broken, the nuclear option is to re-encode:

ffmpeg -f concat -safe 0 -i files.txt -c:v libx264 -crf 23 -c:a aac merged.mp4

Re-encoding rebuilds all timestamps from scratch. It's slower, but it fixes timestamp issues that -fflags +genpts can't.

Codec mismatch between clips

If one clip is H.264 and another is H.265, the concat demuxer will produce garbage. You have two options:

  1. Transcode the odd one out to match the majority format before merging. If most clips are H.264, convert the H.265 one:

ffmpeg -i h265clip.mp4 -c:v libx264 -crf 23 -c:a aac h264clip.mp4
  1. Use the concat filter to re-encode everything at once. More straightforward when you have many mismatched clips.

Check what codecs are in each file before deciding:

ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 clip.mp4

This prints just the codec name (e.g. h264 or hevc). Run it on each clip to spot the mismatch. The FFmpeg cheat sheet has more ffprobe commands for inspecting files.

Audio sync drift

When merged video has audio that gradually goes out of sync, the clips probably have different audio sample rates (44100 Hz vs 48000 Hz is common). Force a consistent sample rate with the concat filter:

ffmpeg -i clip1.mp4 -i clip2.mp4 \
  -filter_complex "[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[outv][outa]" \
  -map "[outv]" -map "[outa]" \
  -c:v libx264 -crf 23 -c:a aac -ar 48000 merged.mp4

The -ar 48000 forces the output audio to 48 kHz, which is the standard for video. This resamples any clips that don't match.

One clip has no audio

If some clips have audio and others don't, the concat filter will fail with a stream mapping error. You need to add a silent audio track to the clips that lack one:

ffmpeg -i silent_clip.mp4 -f lavfi -i anullsrc=r=48000:cl=stereo -c:v copy -c:a aac -shortest with_audio.mp4

Then merge as usual. The anullsrc filter generates silence, and -shortest ensures it matches the video duration.

"Discarding 1 additional packets" / pixel format mismatch

This happens when clips have different pixel formats (e.g. yuv420p vs yuv444p). Force a common pixel format in the concat filter:

ffmpeg -i clip1.mp4 -i clip2.mp4 \
  -filter_complex "[0:v]format=yuv420p[v0];[1:v]format=yuv420p[v1];[v0][0:a][v1][1:a]concat=n=2:v=1:a=1[outv][outa]" \
  -map "[outv]" -map "[outa]" merged.mp4

The yuv420p format is the safest choice since it's compatible with every player and browser.

"Discarding 1 additional packets" / frame rate mismatch

Different frame rates (30fps vs 29.97fps, or 24fps vs 30fps) can also cause packet discarding and choppy playback. Force a consistent frame rate:

ffmpeg -i clip1.mp4 -i clip2.mp4 \
  -filter_complex "[0:v]fps=30[v0];[1:v]fps=30[v1];[v0][0:a][v1][1:a]concat=n=2:v=1:a=1[outv][outa]" \
  -map "[outv]" -map "[outa]" -c:v libx264 -crf 23 merged.mp4

ffmpeg merge videos at scale: batch processing

Merging two clips is a terminal command. Merging hundreds of clips across dozens of output files is an automation problem.

Bash loop for batch merges

Say you have paired clips (intro + main content) and want to merge each pair:

#!/bin/bash
for i in $(seq 1 50); do
  printf "file 'intro.mp4'\nfile 'content_%03d.mp4'\n" "$i" > /tmp/list_$i.txt
  ffmpeg -f concat -safe 0 -i /tmp/list_$i.txt -c copy "merged_${i}.mp4" &

  # Run 4 jobs in parallel
  [ $(jobs -r | wc -l) -ge 4 ] && wait -n
done
wait

This works on a single machine but ties up your CPU. For anything more than a few dozen videos, offloading the work makes more sense. If you use n8n for workflow automation, the batch video processing guide covers how to set up a similar pipeline with parallel processing and error handling.

Merge videos via API (no local FFmpeg needed)

RenderIO runs FFmpeg in the cloud. You send the command over HTTP and get the merged file back. No installs, no CPU load on your machine.

Here's how to merge two clips using the concat filter via the API:

curl -X POST https://renderio.dev/api/v1/run-ffmpeg-command \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: your_api_key" \
  -d '{
    "ffmpeg_command": "ffmpeg -i {{in_clip1}} -i {{in_clip2}} -filter_complex \"[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[outv][outa]\" -map \"[outv]\" -map \"[outa]\" -c:v libx264 -crf 23 -c:a aac {{out_merged}}",
    "input_files": {
      "in_clip1": "https://example.com/clip1.mp4",
      "in_clip2": "https://example.com/clip2.mp4"
    },
    "output_files": {
      "out_merged": "merged.mp4"
    }
  }'

The API returns a command_id. Poll for completion:

curl https://renderio.dev/api/v1/commands/COMMAND_ID \
  -H "X-API-KEY: your_api_key"

When the status is SUCCESS, the response includes a download URL for your merged file.

You can also use the concat demuxer via the API. Upload the file list as an input, or build the -f concat command inline. For the full submit-poll-download pattern, see the complete API guide. The curl examples reference has 20 more ready-to-paste commands for other operations.

Node.js: merge videos programmatically

const API_KEY = "ffsk_your_api_key";

async function mergeVideos(clipUrls) {
  // Build the ffmpeg command dynamically
  const inputs = clipUrls.map((_, i) => `-i {{in_clip${i}}}`).join(" ");
  const streams = clipUrls.map((_, i) => `[${i}:v][${i}:a]`).join("");
  const filterComplex = `"${streams}concat=n=${clipUrls.length}:v=1:a=1[outv][outa]"`;

  const inputFiles = {};
  clipUrls.forEach((url, i) => { inputFiles[`in_clip${i}`] = url; });

  const res = await fetch("https://renderio.dev/api/v1/run-ffmpeg-command", {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-API-KEY": API_KEY },
    body: JSON.stringify({
      ffmpeg_command: `ffmpeg ${inputs} -filter_complex ${filterComplex} -map "[outv]" -map "[outa]" -c:v libx264 -crf 23 -c:a aac {{out_merged}}`,
      input_files: inputFiles,
      output_files: { out_merged: "merged.mp4" },
    }),
  });

  const { command_id } = await res.json();

  // Poll until done
  while (true) {
    await new Promise((r) => setTimeout(r, 3000));
    const status = await fetch(
      `https://renderio.dev/api/v1/commands/${command_id}`,
      { headers: { "X-API-KEY": API_KEY } }
    ).then((r) => r.json());

    if (status.status === "SUCCESS") return status;
    if (status.status === "FAILED") throw new Error(status.error);
  }
}

// Merge 3 clips
mergeVideos([
  "https://example.com/intro.mp4",
  "https://example.com/main.mp4",
  "https://example.com/outro.mp4",
]).then((result) => console.log("Download:", result.output_files));

The function builds the concat filter dynamically, so you can pass in any number of clips. For a full Node.js setup including error handling and webhooks, see the Node.js API tutorial.

Python: merge and poll

import requests
import time

API_KEY = "ffsk_your_api_key"
BASE = "https://renderio.dev/api/v1"
HEADERS = {"Content-Type": "application/json", "X-API-KEY": API_KEY}

def merge_videos(clip_urls):
    n = len(clip_urls)
    inputs = " ".join(f"-i {{{{in_clip{i}}}}}" for i in range(n))
    streams = "".join(f"[{i}:v][{i}:a]" for i in range(n))
    filter_str = f'"{streams}concat=n={n}:v=1:a=1[outv][outa]"'

    input_files = {f"in_clip{i}": url for i, url in enumerate(clip_urls)}

    resp = requests.post(f"{BASE}/run-ffmpeg-command", headers=HEADERS, json={
        "ffmpeg_command": f'ffmpeg {inputs} -filter_complex {filter_str} -map "[outv]" -map "[outa]" -c:v libx264 -crf 23 -c:a aac {{{{out_merged}}}}',
        "input_files": input_files,
        "output_files": {"out_merged": "merged.mp4"},
    })
    command_id = resp.json()["command_id"]

    while True:
        time.sleep(3)
        status = requests.get(f"{BASE}/commands/{command_id}", headers=HEADERS).json()
        if status["status"] == "SUCCESS":
            return status
        if status["status"] == "FAILED":
            raise Exception(status.get("error", "Command failed"))

result = merge_videos([
    "https://example.com/clip1.mp4",
    "https://example.com/clip2.mp4",
])
print("Download:", result["output_files"])

For batch processing hundreds of merges, the Python API tutorial covers concurrent execution with ThreadPoolExecutor.

When merging isn't the right call

Sometimes what looks like a merge problem is actually an editing problem. If you need to:

  • Add transitions between clips (fades, dissolves): That's video editing, not concatenation. FFmpeg can do crossfade transitions with the xfade filter, but the syntax gets complicated quickly. A basic crossfade between two clips looks like this:

ffmpeg -i clip1.mp4 -i clip2.mp4 \
  -filter_complex "xfade=transition=fade:duration=1:offset=4,format=yuv420p" \
  -c:v libx264 -crf 23 output.mp4

The offset is the timestamp (in seconds) where the transition starts, based on the first clip's duration minus the crossfade length.

  • Overlay clips on top of each other (picture-in-picture): Use the overlay filter instead of concat.

  • Compress the merged output: Merge first with -c copy, then compress the result as a separate step. Trying to do both at once with the demuxer doesn't work since -c copy and encoder flags are mutually exclusive. The video compression guide has the commands for that second step.

FAQ

Can I merge MP4 and MOV files together?

Yes, if they use the same codecs internally. MOV and MP4 are both container formats that can hold H.264 video and AAC audio. If both files contain H.264/AAC, the concat demuxer works fine. If the codecs differ, use the concat filter to re-encode.

How do I merge videos without losing quality?

Use the concat demuxer with -c copy. This copies the original streams byte-for-byte without re-encoding, so there's zero quality loss. It only works when all clips share the same codec, resolution, and frame rate.

Why does my merged video have a black frame between clips?

This usually means the last frame of one clip and the first frame of another overlap or have a timestamp gap. Try adding -fflags +genpts to regenerate timestamps. If that doesn't fix it, the clips may have been trimmed at non-keyframe boundaries. Re-trimming with the FFmpeg cheat sheet trim commands (using re-encode, not -c copy) and then merging again usually resolves it.

Can I merge videos with different aspect ratios?

Yes, with the concat filter. You need to scale and pad each clip to the same output resolution first. The scale + pad example earlier in this guide does exactly that. Without padding or cropping, FFmpeg will refuse to concatenate videos with different dimensions.

Is there a limit to how many videos I can merge?

FFmpeg itself has no hard limit. The concat demuxer reads from a text file, so it handles thousands of entries. The concat filter requires all inputs to be specified on the command line, which bumps into shell argument limits somewhere around a few hundred inputs depending on your OS. For very large merges (1000+ clips), the demuxer is the way to go.

How do I merge videos in a specific order?

The concat demuxer processes files in the order they appear in the text file. List them in the order you want. For the concat filter, the order matches the input order (-i first.mp4 -i second.mp4 ...).

Quick reference

ScenarioMethodRe-encodes?Speed
Same codec, same resolutionConcat demuxerNoSeconds
Different codecs or resolutionsConcat filterYesMinutes
HLS .ts segmentsConcat protocolNoSeconds
Batch merge via APIRenderIO APIDepends on commandVaries
Different resolutions, keep qualityFilter + scaleYesMinutes

Every command above works through RenderIO's FFmpeg API. Send the command over HTTP, get the result back. The FFmpeg commands list has more operations with matching API calls. The Starter plan at $9/mo covers 500 commands. Get your API key to try it.