FFmpeg Trim Video: Cut, Split & Extract Clips

February 27, 2026 ยท RenderIO

You want to cut a clip out of a video. Here's the command.

ffmpeg -i input.mp4 -ss 00:01:30 -to 00:02:00 -c copy output.mp4

That trims input.mp4 from 1 minute 30 seconds to 2 minutes and copies the streams without re-encoding. Fast, easy, done.

Except it's probably not quite right. Your clip might start a few seconds early. The first frame might be garbled. Audio might drift out of sync. These are all real problems with FFmpeg trim workflows, and they trace back to one thing: keyframes.

This guide covers the fast way, the accurate way, how to extract multiple clips, how to batch trim hundreds of files, and how to do it all through a REST API when you don't want FFmpeg on your servers.

The two flags you need: -ss and -to vs -ss and -t

FFmpeg gives you two ways to specify a trim range:

# Method 1: start time + end time
ffmpeg -i input.mp4 -ss 00:01:30 -to 00:02:00 -c copy output.mp4

# Method 2: start time + duration
ffmpeg -i input.mp4 -ss 00:01:30 -t 00:00:30 -c copy output.mp4

Both produce the same 30-second clip. -to sets the absolute end time. -t sets the duration. Pick whichever feels more natural for your use case.

Time formats FFmpeg accepts:

  • 00:01:30 or 00:01:30.500 (hours:minutes:seconds.milliseconds)

  • 90 or 90.5 (plain seconds)

  • 1:30 also works (minutes:seconds, no leading zero for hours required)

One gotcha: if you put -ss before -i, then -to behaves like -t. It becomes a duration, not an absolute timestamp. This trips people up all the time. The safest approach is to keep -ss after -i until you understand the difference.

# -ss before -i: -to acts as duration (both produce 30-second clips)
ffmpeg -ss 00:01:30 -i input.mp4 -to 00:00:30 -c copy output.mp4
ffmpeg -ss 00:01:30 -i input.mp4 -t 00:00:30 -c copy output.mp4

Why your trim isn't frame-accurate (and how to fix it)

When you use -c copy, FFmpeg doesn't decode or re-encode anything. It copies compressed packets directly. The problem is that video compression works in groups of frames. An I-frame (keyframe) contains a complete picture. P-frames and B-frames only store differences from nearby frames. You can only start a stream copy on an I-frame.

If you ask FFmpeg to start at 00:01:30 and the nearest keyframe is at 00:01:28, your clip starts at 00:01:28. You get two extra seconds you didn't ask for.

Most videos encoded with default settings have keyframes every 2-5 seconds (a GOP size of 48-150 frames at 30fps). That's your worst-case margin of error with stream copy.

See where the keyframes actually are

Before trimming, you can check keyframe positions with ffprobe:

ffprobe -select_streams v:0 -show_entries frame=pts_time,key_frame \
  -of csv=p=0 input.mp4 | grep ",1$" | head -20

This lists timestamps of every keyframe in the video. If you see keyframes at 88.0, 90.0, 92.0, and you're trimming at 90 seconds, you know the stream copy will land exactly on 90.0. If keyframes are at 87.5 and 92.5, you know you're getting a 2.5-second overshoot and should re-encode instead.

The fix: re-encode

ffmpeg -ss 00:01:30 -accurate_seek -i input.mp4 -t 00:00:30 \
  -c:v libx264 -crf 23 -c:a aac output.mp4

Replacing -c copy with actual codec names tells FFmpeg to decode and re-encode. Now it can cut at any frame, not just keyframes. Adding -accurate_seek (which is enabled by default since FFmpeg 2.1, but worth being explicit about) tells FFmpeg to discard decoded frames before the seek point rather than including them in the output.

The trade-off is speed: stream copy finishes in under a second, re-encoding a 30-second clip might take 5-10 seconds depending on resolution and preset. For details on CRF values and encoding presets, the video compression guide goes deep on these settings.

Notice -ss moved to before -i. When -ss is before the input, FFmpeg seeks in the input file before decoding. This is faster for trimming clips from large files because FFmpeg skips directly to the timestamp instead of decoding everything from the start.

The middle ground: stream copy with -avoid_negative_ts

If you need speed but can tolerate keyframe-snapping, add -avoid_negative_ts make_zero to prevent audio sync issues:

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

This won't make the cut frame-accurate, but it prevents the audio/video desync that sometimes happens when stream-copying from a non-keyframe position.

Input seeking vs output seeking

Where you place -ss changes how FFmpeg processes the file:

Input seeking (-ss before -i): FFmpeg jumps directly to the timestamp in the input file. Fast. Might be slightly inaccurate with -c copy because it snaps to the nearest keyframe.

ffmpeg -ss 00:05:00 -i input.mp4 -t 00:01:00 -c copy output.mp4

Output seeking (-ss after -i): FFmpeg decodes everything from the start of the file and discards frames until it reaches your timestamp. Slow on large files, but more accurate.

ffmpeg -i input.mp4 -ss 00:05:00 -t 00:01:00 -c copy output.mp4

For a 2-hour video where you want a clip from the 90-minute mark: input seeking skips straight to ~90 minutes. Output seeking decodes all 90 minutes of video just to throw it away. Use input seeking.

For short files or when frame accuracy matters more than speed, output seeking with re-encoding gives the most precise results.

Combined seeking (the best of both): Put -ss before -i for speed, then add a small output -ss for fine adjustment:

ffmpeg -ss 00:04:57 -i input.mp4 -ss 3 -t 00:01:00 -c:v libx264 -crf 23 -c:a aac output.mp4

This seeks to 4:57 at the input level (fast), then trims 3 more seconds at the output level (accurate). The result starts at exactly 5:00. Useful when you need frame-level precision on large files without decoding everything from the beginning.

Using the trim filter

The -ss/-to approach handles most cases, but FFmpeg also has a dedicated trim video filter. The filter works differently: it operates on decoded frames rather than packets, so it always re-encodes.

# Trim from 3s to 6s using the trim filter
ffmpeg -i input.mp4 \
  -vf "trim=start=3:end=6,setpts=PTS-STARTPTS" \
  -af "atrim=start=3:end=6,asetpts=PTS-STARTPTS" \
  output.mp4

The setpts=PTS-STARTPTS part resets timestamps so the output starts at 00:00:00 instead of 00:00:03. Without it, you get black frames at the beginning (or a player that starts playback at the 3-second mark with nothing before it). The audio needs the same treatment with atrim and asetpts.

You can also trim by duration or by frame number:

# First 5 seconds
ffmpeg -i input.mp4 \
  -vf "trim=duration=5,setpts=PTS-STARTPTS" \
  -af "atrim=duration=5,asetpts=PTS-STARTPTS" \
  output.mp4

# Frames 100 to 200
ffmpeg -i input.mp4 \
  -vf "trim=start_frame=100:end_frame=200,setpts=PTS-STARTPTS" \
  output.mp4

When would you use the filter instead of -ss/-to? Mainly when you're chaining other filters. If you're already applying a scale, crop, or color correction filter, adding trim to the filter chain keeps everything in one pass. For standalone trimming, -ss/-to is simpler.

Preserving all streams with -map 0

By default, FFmpeg only copies one video and one audio stream. If your input has subtitles, multiple audio tracks, or chapter markers, they get dropped. Use -map 0 to keep everything:

ffmpeg -i input.mkv -ss 00:10:00 -to 00:20:00 -map 0 -c copy output.mkv

This copies all video, audio, subtitle, and data streams. Especially useful for MKV files with multiple language audio tracks or embedded subtitles.

Cutting from the end with -sseof

Need the last 60 seconds of a video but don't know its duration? -sseof takes a negative value relative to the end of the file:

ffmpeg -sseof -60 -i input.mp4 -c copy output.mp4

This grabs the final 60 seconds. You can combine it with -t to grab a specific duration starting from the offset:

# 30 seconds starting from 60 seconds before the end
ffmpeg -sseof -60 -i input.mp4 -t 30 -c copy output.mp4

Useful for extracting outros, end credits, or the last segment of a livestream recording.

Extracting multiple clips from one video

You can pull several segments from a single video and concatenate them using filter_complex:

ffmpeg -i input.mp4 -filter_complex \
  "[0:v]trim=start=10:end=20,setpts=PTS-STARTPTS[v1]; \
   [0:a]atrim=start=10:end=20,asetpts=PTS-STARTPTS[a1]; \
   [0:v]trim=start=45:end=60,setpts=PTS-STARTPTS[v2]; \
   [0:a]atrim=start=45:end=60,asetpts=PTS-STARTPTS[a2]; \
   [v1][a1][v2][a2]concat=n=2:v=1:a=1[outv][outa]" \
  -map "[outv]" -map "[outa]" output.mp4

This extracts seconds 10-20 and 45-60, then stitches them together. The concat filter joins the segments in order.

For simple cases, running separate FFmpeg commands for each clip is easier to debug:

ffmpeg -i input.mp4 -ss 00:00:10 -to 00:00:20 -c copy clip1.mp4
ffmpeg -i input.mp4 -ss 00:00:45 -to 00:01:00 -c copy clip2.mp4

The filter_complex approach is one command and one pass through the file. Separate commands are more readable. Pick based on how many clips you're extracting and whether you need them concatenated.

Batch trimming with a shell script

Real-world trimming rarely involves one file. You have 200 interview recordings and need the first 30 seconds of each as a preview. Or you're processing user uploads and need to enforce a 60-second maximum length.

#!/bin/bash
# Trim all MP4s in a directory to the first 60 seconds
for file in /input/*.mp4; do
  filename=$(basename "$file")
  ffmpeg -i "$file" -t 60 -c copy "/output/$filename" -y
done

For trimming to a specific range with re-encoding:

#!/bin/bash
# Extract seconds 5-35 from each file, re-encode for accuracy
for file in /input/*.mp4; do
  filename=$(basename "$file")
  ffmpeg -ss 5 -i "$file" -t 30 -c:v libx264 -crf 23 -c:a aac "/output/trimmed_$filename" -y
done

This works fine for a handful of files. At scale, you hit problems. Your machine has a fixed number of CPU cores. Re-encoding 200 videos sequentially takes hours. Parallelizing locally means managing process concurrency, handling failures, and watching your CPU temperature.

You can speed things up with GNU parallel:

# Process 4 videos at a time
find /input -name "*.mp4" | parallel -j4 \
  'ffmpeg -i {} -t 60 -c copy /output/{/} -y'

But you're still limited by local hardware. For anything more than a few dozen files, consider offloading to an API. The FFmpeg commands list has more batch processing patterns if you want to stay on the command line.

Trimming video programmatically

Node.js with child_process

const { execSync } = require("child_process");

function trimVideo(input, output, startTime, duration) {
  const cmd = `ffmpeg -ss ${startTime} -i "${input}" -t ${duration} -c copy "${output}" -y`;
  execSync(cmd, { stdio: "inherit" });
}

trimVideo("input.mp4", "clip.mp4", "00:01:30", "00:00:30");

This shells out to FFmpeg directly. It works when FFmpeg is installed on the machine. It breaks in serverless environments, Docker containers without FFmpeg, or anywhere the binary isn't available. For production Node.js video processing, the Node.js FFmpeg API guide covers a dependency-free approach.

Python with subprocess

import subprocess

def trim_video(input_path, output_path, start, duration):
    cmd = [
        "ffmpeg", "-ss", start, "-i", input_path,
        "-t", duration, "-c", "copy", output_path, "-y"
    ]
    subprocess.run(cmd, check=True)

trim_video("input.mp4", "clip.mp4", "00:01:30", "00:00:30")

Same deal. Works locally, breaks in environments without FFmpeg. For Python-specific API integration, see the Python FFmpeg API guide.

Trimming via REST API

When you're trimming video inside a web app, mobile backend, or automated pipeline, installing FFmpeg on every server is a maintenance burden. An API approach offloads the processing entirely.

With RenderIO, you send the same FFmpeg command you'd run locally, but over HTTP:

curl -X POST https://renderio.dev/api/v1/run-ffmpeg-command \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: ffsk_your_key" \
  -d '{
    "ffmpeg_command": "-ss 00:01:30 -i {{in_video}} -t 00:00:30 -c copy {{out_video}}",
    "input_files": { "in_video": "https://your-bucket.s3.amazonaws.com/video.mp4" },
    "output_files": { "out_video": "trimmed-clip.mp4" }
  }'

The API downloads the input from your URL, runs FFmpeg in an isolated sandbox, and stores the output. You get back a command_id to poll for status:

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

When status is SUCCESS, the response includes output_files, a map of your output aliases to file metadata, each with a storage_url to download your trimmed file. The REST API tutorial walks through the full setup from signup to first processed file.

Batch trimming via API

For batch operations, the parallel commands endpoint handles up to 10 commands at once:

curl -X POST https://renderio.dev/api/v1/run-multiple-ffmpeg-commands \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: ffsk_your_key" \
  -d '{
    "commands": [
      {
        "ffmpeg_command": "-ss 0 -i {{in_video}} -t 60 -c copy {{out_video}}",
        "input_files": { "in_video": "https://bucket.s3.amazonaws.com/video1.mp4" },
        "output_files": { "out_video": "preview1.mp4" }
      },
      {
        "ffmpeg_command": "-ss 0 -i {{in_video}} -t 60 -c copy {{out_video}}",
        "input_files": { "in_video": "https://bucket.s3.amazonaws.com/video2.mp4" },
        "output_files": { "out_video": "preview2.mp4" }
      }
    ]
  }'

No concurrency management. No local CPU bottleneck. Each command runs in its own sandbox. For a full walkthrough of the API including webhooks and error handling, the complete FFmpeg API guide covers everything. You can also find ready-to-use trim commands in the curl examples collection.

Ready to get your API key and start trimming video through the API? It takes about two minutes.

Node.js API example

async function trimViaAPI(inputUrl, start, duration) {
  const res = await fetch("https://renderio.dev/api/v1/run-ffmpeg-command", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-KEY": "ffsk_your_key",
    },
    body: JSON.stringify({
      ffmpeg_command: `-ss ${start} -i {{in_video}} -t ${duration} -c copy {{out_video}}`,
      input_files: { in_video: inputUrl },
      output_files: { out_video: "trimmed.mp4" },
    }),
  });

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

  // Poll for completion
  let status = "PENDING";
  while (status !== "SUCCESS" && status !== "FAILED") {
    await new Promise((r) => setTimeout(r, 2000));
    const poll = await fetch(
      `https://renderio.dev/api/v1/commands/${command_id}`,
      { headers: { "X-API-KEY": "ffsk_your_key" } }
    );
    const data = await poll.json();
    status = data.status;
    if (status === "SUCCESS") return data.output_files.out_video.storage_url;
  }
  throw new Error("Trim failed");
}

Stream copy vs re-encode: when to use which

Use -c copy (stream copy) when:

  • Speed matters more than frame-perfect accuracy

  • A few seconds of drift at the start/end is acceptable

  • You're trimming large files and don't want to wait

  • The output codec should match the input codec

Re-encode when:

  • You need the clip to start at an exact frame

  • You're trimming short clips where a 2-second drift is noticeable

  • You're combining trim with other operations (scale, compress, watermark)

  • You want to change codecs while trimming

Stream copy is near-instant regardless of file size. Re-encoding speed depends on resolution, codec, and preset. For a 1080p clip using libx264 with the medium preset, expect roughly real-time encoding: a 30-second clip takes about 30 seconds. The fast preset roughly halves that. The transcoding guide has codec-by-codec speed comparisons.

Common mistakes

Garbled first frame: Your clip starts with a blocky, corrupted frame. This happens when stream-copying from a non-keyframe position. FFmpeg starts copying from the nearest keyframe before your timestamp, but the first few frames reference earlier frames that weren't included. Fix: re-encode, or accept the keyframe snap.

Audio out of sync: Especially common when stream-copying. Video and audio keyframes don't always align. Add -avoid_negative_ts make_zero to fix most cases. If that doesn't work, re-encode the audio: -c:v copy -c:a aac.

Wrong duration with -to before -i: When -ss is before -i, -to becomes relative to the seek position, not the original file timestamp. This trips people up. If your clips are the wrong length, check the flag order.

YouTube rejects the trimmed file: YouTube occasionally fails to process videos trimmed with -c copy. This happens when the stream copy produces a file with incorrect timestamps or missing moov atom data. Fix: add -movflags +faststart to rewrite the moov atom to the beginning of the file, or re-encode.

Empty output file: Usually means -ss or -to is past the end of the video, or -to is smaller than -ss. Check your timestamps against the actual video duration with ffprobe:

ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4

Subtitles and chapters disappear: FFmpeg only copies one video and one audio stream by default. If you need everything, add -map 0 -c copy.

FAQ

Can I trim a video without losing quality? Yes. Using -c copy copies the compressed data without re-encoding, so there's zero quality loss. The trade-off is that the cut points snap to the nearest keyframe rather than your exact timestamp. For frame-accurate trimming, you need to re-encode, which introduces a generation loss (though it's negligible at reasonable CRF values like 18-23).

What's the difference between -ss before and after -i? Before -i (input seeking): FFmpeg jumps to the timestamp before reading the file. Fast, especially on large files. After -i (output seeking): FFmpeg reads and decodes from the beginning, then discards everything before your timestamp. Slower but slightly more accurate with -c copy.

How do I trim to an exact frame? Re-encode with -ss before -i: ffmpeg -ss 90.033 -i input.mp4 -t 30 -c:v libx264 -crf 23 -c:a aac output.mp4. The timestamp supports millisecond precision (e.g., 90.033 for frame 2701 at 30fps). You can also use the trim filter with start_frame and end_frame for frame-number precision.

Does trimming with -c copy change the file size proportionally? Roughly, yes. A 30-second clip from a 5-minute video at constant bitrate will be about 1/10th the file size. With variable bitrate, it depends on the content in the trimmed section.

Can I trim without FFmpeg installed? Yes. A cloud FFmpeg API like RenderIO lets you send trim commands over HTTP. No local binary needed. See the API guide for setup.

How do I trim multiple sections and join them? Use filter_complex with the trim, atrim, and concat filters (see the "Extracting multiple clips" section above), or trim separately and concatenate with the concat demuxer.

Quick reference

TaskCommand
Trim from 1:30 to 2:00 (fast)ffmpeg -i in.mp4 -ss 00:01:30 -to 00:02:00 -c copy out.mp4
Trim 30s starting at 1:30 (accurate)ffmpeg -ss 00:01:30 -i in.mp4 -t 30 -c:v libx264 -crf 23 -c:a aac out.mp4
Last 60 secondsffmpeg -sseof -60 -i in.mp4 -c copy out.mp4
First 10 secondsffmpeg -i in.mp4 -t 10 -c copy out.mp4
Trim with filterffmpeg -i in.mp4 -vf "trim=3:6,setpts=PTS-STARTPTS" -af "atrim=3:6,asetpts=PTS-STARTPTS" out.mp4
Keep all streamsffmpeg -i in.mkv -ss 10:00 -to 20:00 -map 0 -c copy out.mkv
Check keyframesffprobe -select_streams v:0 -show_entries frame=pts_time,key_frame -of csv=p=0 in.mp4 | grep ",1$"

The FFmpeg cheat sheet has 50 more commands organized by task if you need other operations beyond trimming.