# Ffmpeg Cut Concat

> Trim, cut, split, segment, and concatenate media with ffmpeg (stream copy when possible, re-encode across cut boundaries). Use when the user asks to trim a video, cut a clip, extract a segment by timestamps, remove a section, split into parts, join/merge videos, concatenate files, or build a segmented HLS-style playlist.

- Skill: `damionrashford/ffmpeg-cut-concat` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add damionrashford/ffmpeg-cut-concat`
- Raw SKILL.md: https://api.skillmd.com/api/skills/damionrashford/ffmpeg-cut-concat/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: damionrashford (https://skillmd.com/u/damionrashford)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/damionrashford/ffmpeg-cut-concat

---


# Ffmpeg Cut Concat

**Context:** $ARGUMENTS

## Quick start

- **Fast trim (keyframe-aligned):** → Step 1 → trim mode with `-c copy`
- **Frame-accurate trim:** → Step 1 → trim mode, re-encode (`--accurate`)
- **Split one file into N pieces:** → Step 1 → segment mode
- **Join same-codec files:** → Step 1 → concat-copy mode
- **Join different-codec files:** → Step 1 → concat-filter mode

## When to use

- Extracting a clip between two timestamps (e.g. `00:01:30` to `00:02:00`).
- Splitting a long file into N-second chunks (podcast, HLS prep).
- Joining multiple recordings end-to-end.
- Removing the first/last N seconds of a video.
- Building a highlight reel from several source files.

## Step 1 — Pick a strategy

| Goal | Mode | Command skeleton |
|---|---|---|
| Trim one clip from one file | `trim` | `ffmpeg -ss T1 -i in -to T2 -c copy out` |
| Split one file into equal pieces | `segment` | `ffmpeg -i in -f segment -segment_time N out_%03d.mp4` |
| Join files with identical codecs | `concat-copy` | `ffmpeg -f concat -safe 0 -i list.txt -c copy out` |
| Join files with mismatched codecs | `concat-filter` | `-filter_complex "concat=n=N:v=1:a=1"` |

Do not use concat demuxer for files that differ in codec, resolution, SAR, fps, or sample rate — it will mux them but playback breaks silently. Use the concat filter instead.

## Step 2 — Decide stream copy vs re-encode

- **Stream copy (`-c copy`)** is ~100x faster and lossless, but cuts land on the nearest keyframe before the requested timestamp. A clip asked for at `00:01:30.000` may actually start at `00:01:28.5`.
- **Re-encode** (drop `-c copy`, add `-c:v libx264 -c:a aac`) is frame-accurate but slow and lossy.
- **Default rule:** start with `-c copy`; switch to re-encode only when the user says "frame-accurate," "exact," or the first frame looks wrong after a test.

Input-side vs output-side seek:

- `-ss T -i in.mp4` (before `-i`) — input seek. Fast; ffmpeg seeks in the container to the keyframe at or before T, then starts decoding. With `-c copy`, output begins at that keyframe.
- `-i in.mp4 -ss T` (after `-i`) — output seek. Decodes from the start, discards frames until T. Slow but accurate. Rarely needed since modern ffmpeg does accurate input seek by default with `-accurate_seek`.

For accurate cut with re-encode, use input seek too — it is fast AND accurate once decoding is on.

## Step 3 — Run the command

### 3a. Trim (stream copy, keyframe-aligned)

```bash
ffmpeg -ss 00:01:30 -i input.mp4 -to 00:02:00 -c copy -avoid_negative_ts make_zero out.mp4
```

`-to` is absolute input-time. If you wrote `-ss 00:01:30 -to 00:02:00`, you get 30 seconds out.

Use `-t 30` instead of `-to` to specify duration directly (`-t` wins if both set).

### 3b. Trim (frame-accurate, re-encode)

```bash
ffmpeg -ss 00:01:30 -i input.mp4 -to 00:02:00 \
  -c:v libx264 -crf 18 -preset veryfast \
  -c:a aac -b:a 192k \
  out.mp4
```

### 3c. Segment (split into N-second pieces)

```bash
ffmpeg -i input.mp4 -c copy -map 0 \
  -f segment -segment_time 60 -reset_timestamps 1 \
  out_%03d.mp4
```

Segments still break on keyframes. If you need exact 60.000s pieces, re-encode with forced keyframes: `-force_key_frames "expr:gte(t,n_forced*60)"`.

### 3d. Concat demuxer (same codec params)

Create `list.txt`:

```
file 'clip1.mp4'
file 'clip2.mp4'
file 'clip3.mp4'
```

Run:

```bash
ffmpeg -f concat -safe 0 -i list.txt -c copy out.mp4
```

`-safe 0` allows absolute paths and `..` — needed for anything except plain filenames in CWD.

### 3e. Concat filter (different codecs / resolutions)

```bash
ffmpeg -i a.mp4 -i b.mp4 -i c.mp4 \
  -filter_complex "[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[v][a]" \
  -map "[v]" -map "[a]" \
  -c:v libx264 -crf 18 -c:a aac -b:a 192k \
  out.mp4
```

Each input contributes `v` video streams and `a` audio streams — reference them in order `[i:v][i:a]` for `i=0..n-1`.

## Step 4 — Verify duration

```bash
ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 out.mp4
```

For stream copy, the reported duration may be a bit shorter or longer than requested due to keyframe alignment. If the discrepancy is more than ~2s, your GOP size is large — re-encode.

## Available scripts

- **`scripts/cut.py`** — single entrypoint for all four modes (`trim`, `segment`, `concat-copy`, `concat-filter`) with `--dry-run` and `--verbose`.

## Workflow

```bash
# Fast trim
uv run ${CLAUDE_SKILL_DIR}/scripts/cut.py trim \
  --input in.mp4 --start 00:01:30 --end 00:02:00 --output clip.mp4

# Frame-accurate
uv run ${CLAUDE_SKILL_DIR}/scripts/cut.py trim \
  --input in.mp4 --start 00:01:30 --end 00:02:00 --accurate --output clip.mp4

# Split into 60s chunks
uv run ${CLAUDE_SKILL_DIR}/scripts/cut.py segment \
  --input in.mp4 --seconds 60 --pattern out_%03d.mp4

# Concat same-codec files
uv run ${CLAUDE_SKILL_DIR}/scripts/cut.py concat-copy \
  --inputs a.mp4 b.mp4 c.mp4 --output joined.mp4

# Concat mixed-codec files (re-encodes)
uv run ${CLAUDE_SKILL_DIR}/scripts/cut.py concat-filter \
  --inputs a.mp4 b.webm --output joined.mp4
```

## Reference docs

- Read [`references/patterns.md`](references/patterns.md) for exact `list.txt` escaping rules, the two-pass `-ss` recipe, `-copyts` muxing-accurate cuts, and keyframe inspection via ffprobe.

## Gotchas

- **Concat demuxer is silent about mismatches.** Files MUST have identical codec, resolution, SAR, pixel format, fps, sample rate, and channel layout. Otherwise the output plays wrong (frozen video, desync audio) with zero warnings. Probe first: `ffprobe -v error -show_streams a.mp4 b.mp4` and diff the codec/fps/sar fields.
- **`-safe 0` is required** for concat list entries with absolute paths or `..`. Without it you get `Unsafe file name`.
- **Single-quote escaping in list.txt.** Each path is wrapped in `'...'`. To include a literal `'`, close the quote, backslash-escape, reopen: `file 'it'''s.mp4'`.
- **Input seek + `-c copy` can produce negative timestamps.** The first packet's PTS may be earlier than the cut point. Add `-avoid_negative_ts make_zero` or players may show a blank first second.
- **Stream-copy trim starts on a keyframe.** If you asked for `00:01:30` but the nearest prior keyframe is at `00:01:28.5`, the output starts at `00:01:28.5`. To snap exactly, re-encode.
- **`-to` is absolute, `-t` is duration.** `-ss 60 -to 90` = 30s clip. `-ss 60 -t 90` = 90s clip. Mixing them confuses people who memorized one form.
- **Concat filter requires matching stream counts per input.** `concat=n=3:v=1:a=1` expects `[0:v][0:a][1:v][1:a][2:v][2:a]`. A silent input needs a dummy `anullsrc` or a=0.
- **Lossless cuts only work at keyframes.** Run `ffprobe -select_streams v -show_frames -skip_frame nokey -show_entries frame=pkt_pts_time` to find them.
- **`-ss` position before `-i` on some formats (especially raw/TS)** seeks inaccurately. If the clip starts in the wrong place, move `-ss` after `-i` to force decode-based seek.

## Examples

### Example 1: cut a 30s clip, fast

```bash
ffmpeg -ss 00:01:30 -i lecture.mp4 -to 00:02:00 \
  -c copy -avoid_negative_ts make_zero \
  highlight.mp4
```

### Example 2: remove first 10 seconds, frame-accurate

```bash
ffmpeg -ss 10 -i raw.mov \
  -c:v libx264 -crf 18 -preset veryfast -c:a aac -b:a 192k \
  trimmed.mp4
```

### Example 3: split a podcast into 10-minute chunks

```bash
ffmpeg -i podcast.m4a -c copy -f segment -segment_time 600 \
  -reset_timestamps 1 part_%02d.m4a
```

### Example 4: join three identical-format clips

```bash
cat > list.txt <<'EOF'
file 'intro.mp4'
file 'main.mp4'
file 'outro.mp4'
EOF
ffmpeg -f concat -safe 0 -i list.txt -c copy final.mp4
```

### Example 5: merge a 1080p mp4 and a 720p webm

```bash
ffmpeg -i a.mp4 -i b.webm \
  -filter_complex "[0:v]scale=1280:720,setsar=1[v0];[1:v]setsar=1[v1];[v0][0:a][v1][1:a]concat=n=2:v=1:a=1[v][a]" \
  -map "[v]" -map "[a]" -c:v libx264 -crf 20 -c:a aac mix.mp4
```

## Troubleshooting

### Error: `Unsafe file name`

**Cause:** concat demuxer refusing a path outside CWD without `-safe 0`.
**Solution:** add `-safe 0` before `-i list.txt`, or use filenames relative to CWD.

### Output starts before the requested `-ss`, or has a black first frame

**Cause:** keyframe alignment with `-c copy` + negative initial PTS.
**Solution:** add `-avoid_negative_ts make_zero`. For exact start, drop `-c copy` and re-encode.

### Concat demuxer output has frozen video or audio desync

**Cause:** input files differ in codec/resolution/fps/sar/sample rate.
**Solution:** probe them (`ffprobe -show_streams`), confirm mismatch, switch to concat filter with re-encode, or normalize inputs first with `-c:v libx264 -vf "scale=W:H,fps=F,setsar=1" -ar 48000 -ac 2`.

### `Stream specifier ':a' matches no streams`

**Cause:** one input has no audio track, but your concat filter expects `a=1`.
**Solution:** either generate silence (`-f lavfi -i anullsrc=cl=stereo:r=48000`) for that input, or set `a=0` and handle audio separately.

### Segments are uneven lengths despite `-segment_time 60`

**Cause:** segmenter only breaks at keyframes; your GOP is longer than 60s or irregular.
**Solution:** re-encode with `-force_key_frames "expr:gte(t,n_forced*60)"` before segmenting (or in the same pass).

### `[concat @ ...] DTS ... < ... out of order`

**Cause:** overlapping or decreasing timestamps when concatenating.
**Solution:** add `-fflags +genpts` before `-i list.txt`, or re-mux each input through `-c copy` first to normalize timestamps.

