---
title: "Batch-Processing Videos from the Command Line, Faster with Your Graphics Card"
url: https://phatdev.net/en/blog/ffmpeg-nvenc-automation
lang: en
updated: 2026-10-21
alternate:
  vi: https://phatdev.net/vi/blog/ffmpeg-nvenc-automation
markdown: https://phatdev.net/en/blog/ffmpeg-nvenc-automation.md
---

# Batch-Processing Videos from the Command Line, Faster with Your Graphics Card

> Compress, cut, join, subtitle and make vertical versions of a whole folder of videos with FFmpeg, faster with an NVIDIA card. Real measurements, the AV1 limit, and a safe batch script.

Categories: Media | Published: 2026-10-19 | Level: Intermediate | 8 min read

Tags: ffmpeg, nvenc, gpu, video, automation, bash

At month end you have thirty product videos that need resizing, compressing for the web, and a vertical cut for social media. You open the editing software, drag them in one by one, hit export, and wait. Fine the first time. By the fifth time it's tedious.

A free tool does all of that from the command line. It's called FFmpeg. Type one command, go make coffee, come back to a finished folder. If your computer has an NVIDIA graphics card it gets faster still. This post is the set of recipes I use, including the limits I ran into. I tested every command on self-generated sample video, with FFmpeg 6.1.1 and an RTX 3060 Ti, without touching any real video.

## What FFmpeg and NVENC are

FFmpeg is a command-line program that reads, transforms and writes video and audio. NVENC is a hardware block built into NVIDIA cards that does nothing but compress video. When you choose the `h264_nvenc` or `hevc_nvenc` encoder, the compression runs on that block instead of the CPU, which leaves the CPU free for other work.

You can picture the CPU as a versatile chef, and NVENC as a machine that makes exactly one dish, very fast. The machine is quick, but the dish usually comes out a bit bigger. I come back to that in the numbers.

To test without using anyone's data, create a 30-second sample video with sound:

```bash
ffmpeg -f lavfi -i "testsrc2=size=1920x1080:rate=30" \
       -f lavfi -i "sine=frequency=440:sample_rate=48000" \
       -t 30 -c:v libx264 -preset veryfast -crf 20 -pix_fmt yuv420p -c:a aac -b:a 128k sample.mp4
```

## Compressing video on the GPU

```bash
# H.264, quality-targeted (constant quality), audio copied
ffmpeg -nostdin -i sample.mp4 -c:v h264_nvenc -preset p5 -tune hq -rc vbr -cq 23 -b:v 0 -c:a copy out-h264.mp4

# H.265/HEVC: smaller files; -tag:v hvc1 makes Apple players accept it
ffmpeg -nostdin -i sample.mp4 -c:v hevc_nvenc -preset p5 -tune hq -rc vbr -cq 28 -b:v 0 -tag:v hvc1 -c:a copy out-hevc.mp4
```

Two things to understand. `-cq` is the target quality level: a smaller number looks better and weighs more, and `-b:v 0` lets the encoder pick the file size. `-preset` runs from `p1` to `p7`; a higher number is slower but compresses tighter. I usually use `p5`.

I measured on the 30-second sample (900 frames), on a machine with 24 CPU threads:

| Encoder | Time | Size |
|---|---|---|
| `h264_nvenc` p5, cq 23 | 3.6 s | 38.7 MB |
| `hevc_nvenc` p5, cq 28 | 3.4 s | 23.1 MB |
| `libx264` medium, crf 23 (CPU) | 8.7 s | 21.3 MB |
| `libx265` medium, crf 28 (CPU) | 28.0 s | 15.1 MB |

Read this table carefully. The sample is synthetic, so the absolute numbers don't represent real video. NVENC's `cq` and x264's `crf` also aren't the same scale, so the sizes are for reference only. What you can take away: the GPU is clearly faster (about 2.4 times x264 and 8 times x265 here), but the CPU usually compresses tighter at the same perceived quality. Use the GPU when you need speed and volume. Use the CPU when you need the smallest file.

![A film strip entering a glowing GPU chip block in the middle, with three outputs appearing on the right: a wide video, a vertical video and a square image](https://phatdev.net/uploads/blog/ffmpeg-nvenc-automation/gpu-pipeline-1920.webp)

One limit I hit: trying `av1_nvenc` on the RTX 3060 Ti gave the real error `No capable devices found` and produced no usable file (same result when I re-ran it on 21 Oct 2026). According to NVIDIA's support table, only the RTX 40 generation and later can encode AV1; the RTX 30 series can only play it. If you need AV1 on this machine, use `libsvtav1` on the CPU and accept the slower speed.

## Cutting, joining, thumbnails, subtitles

There are two kinds of cut. The accurate kind re-encodes the video so it cuts at the exact frame. The fast kind cuts straight through without re-encoding, but it has to start at the nearest "keyframe", so it can be off by a few frames.

```bash
# Accurate cut: -ss BEFORE -i is fast, and re-encoding makes it frame-accurate
ffmpeg -nostdin -ss 5.5 -i in.mp4 -t 4 -c:v h264_nvenc -preset p5 -c:a aac cut-accurate.mp4

# Instant cut with no re-encoding: only cuts on keyframes
ffmpeg -nostdin -ss 5.5 -i in.mp4 -t 4 -c copy cut-copy.mp4
```

I measured again on 21 Oct 2026: the accurate cut came out at exactly 4.000 s, the fast cut at 4.067 s. To join clips of the same format, use `concat`, which needs no re-encoding:

```bash
printf "file '%s'\n" "$PWD/a.mp4" "$PWD/b.mp4" > list.txt
ffmpeg -nostdin -f concat -safe 0 -i list.txt -c copy joined.mp4
```

If the clips differ in format or resolution, you must re-encode with the `concat` filter instead of using this method. Thumbnails and a contact sheet of several frames look like this:

```bash
# one frame at 3 s, 640 px wide (-2 keeps the height even)
ffmpeg -nostdin -ss 3 -i in.mp4 -frames:v 1 -vf scale=640:-2 -q:v 3 thumb.jpg

# contact sheet: one frame every 3 s, tiled 2x2
ffmpeg -nostdin -i in.mp4 -vf "fps=1/3,scale=320:-2,tile=2x2" -frames:v 1 sheet.jpg
```

Subtitles come in two kinds: burned into the picture (always visible) or attached so the viewer can toggle them.

```bash
# burn-in (always visible); uses libass on CPU, then encode on GPU
ffmpeg -nostdin -i in.mp4 -vf "subtitles=sub.srt:force_style='FontSize=22,Outline=2'" \
       -c:v h264_nvenc -preset p5 -cq 23 -b:v 0 -c:a copy burned.mp4

# soft subtitles (viewer can toggle), no re-encode
ffmpeg -nostdin -i in.mp4 -i sub.srt -c copy -c:s mov_text -metadata:s:s:0 language=eng soft.mp4
```

A 9:16 vertical version for social media, cropped from the centre (if the subject is off-centre, add an `x` position to `crop`):

```bash
ffmpeg -nostdin -i in.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" \
       -c:v h264_nvenc -preset p5 -cq 23 -b:v 0 -c:a copy vertical.mp4
```

Checked with `ffprobe` on 21 Oct 2026: the vertical version came out at 1080x1920. For large videos you can keep the whole path in GPU memory with `-hwaccel cuda -hwaccel_output_format cuda` and the `scale_cuda` filter; I tested that to HEVC 1280x720. Note that with this approach CPU-based filters such as `subtitles` can't be used.

## A batch script that won't cause accidents

Running a whole folder is when things most often go wrong: overwriting the originals, leaving half-written files when interrupted, or one bad file stopping the whole batch. My script avoids all three.

![A conveyor of film reels moving right, a work station in the middle writing a dim temporary box, then a rename arrow lifting the finished box onto a teal shelf, with an arch stopping a broken slab at the entrance](https://phatdev.net/uploads/blog/ffmpeg-nvenc-automation/safe-batch-1920.webp)

The idea is that the result is written to a temporary file first, and only renamed to the real name after it passes a check, so a half-finished file never carries the real name. Originals are read-only and results go to a different folder. Files already done are skipped, so rerunning after a failure doesn't start over. And before accepting a result, the script compares the output duration with the input.

```bash
#!/usr/bin/env bash
set -euo pipefail
IN_DIR=${1:?input dir}; OUT_DIR=${2:?output dir}
LOCK=${GPU_LOCK:-/tmp/gpu.lock}   # one GPU job at a time (shared GPU)
mkdir -p "$OUT_DIR"
failed=0

duration() { ffprobe -v error -show_entries format=duration -of csv=p=0 "$1"; }

while IFS= read -r -d '' src; do
  name=$(basename "${src%.*}"); dst="$OUT_DIR/$name.mp4"; tmp="$OUT_DIR/.$name.part"
  [[ -e "$dst" ]] && { echo "skip   $name"; continue; }          # idempotent

  flock "$LOCK" ffmpeg -nostdin -hide_banner -loglevel error -y -i "$src" \
      -map 0:v:0 -map '0:a?' -c:v h264_nvenc -preset p5 -tune hq -rc vbr -cq 23 -b:v 0 \
      -c:a aac -b:a 160k -movflags +faststart -f mp4 "$tmp" \
    || { rm -f "$tmp"; echo "FAIL   $name"; failed=$((failed+1)); continue; }

  din=$(duration "$src"); dout=$(duration "$tmp" || echo 0)      # verify before publishing
  if awk -v a="$din" -v b="$dout" 'BEGIN{d=a-b; if(d<0)d=-d; exit !(d<=0.5)}'; then
    mv -f "$tmp" "$dst"; echo "ok     $name"
  else
    rm -f "$tmp"; echo "BADDUR $name"; failed=$((failed+1))
  fi
done < <(find "$IN_DIR" -maxdepth 1 -type f \( -iname '*.mp4' -o -iname '*.mov' -o -iname '*.mkv' \) -print0 | sort -z)

[[ $failed -eq 0 ]]                  # non-zero exit status if anything failed
```

A few details worth explaining. `-nostdin` stops FFmpeg from swallowing the rest of the loop's input. `-print0` with `read -d ''` keeps file names containing spaces from breaking. `flock` makes jobs sharing the GPU run one after another instead of fighting; consumer cards also limit how many encoding sessions can run at once.

I re-ran it on 21 Oct 2026. A folder with `clip2.mp4` and `sample one.mp4` (a name with a space) gave `ok` for both, with 12.00 s becoming 12.01 s. A second run printed `skip` for both. A folder with a `bad.mp4` that was really just text gave `FAIL bad`, while the neighbouring `good.mp4` was still processed, the exit code was 1, and no `.part` file was left behind.

## Common mistakes

The easiest one to hit is forgetting `-nostdin` inside the loop: FFmpeg swallows the rest of the file list and you only get the first few videos. Another is putting `-ss` after `-i`: the command gets much slower because it decodes from the start, while before `-i` it jumps straight to the spot. Joining files of different formats with `concat` gives broken picture or sound. For web or phone output, don't forget `-pix_fmt yuv420p` and `-movflags +faststart`. And don't assume every card can encode AV1.

## What to do next

Try it on a folder of sample videos first, not straight on real data. Create a few videos with the command at the top, run the script, and check the outputs by eye and with `ffprobe`. Once you trust it, point it at the real folder. If you're building a whole automated process with several steps like this, the principles of locking resources, independent checks and a way back are in [Handing a Big Job to Several AIs Without Breaking Things](https://phatdev.net/en/blog/agentic-workflow-orchestrator).

## References

- [FFmpeg documentation](https://ffmpeg.org/ffmpeg.html)
- [FFmpeg filters documentation (subtitles, tile, scale_cuda)](https://ffmpeg.org/ffmpeg-filters.html)
- [FFmpeg Wiki: Hardware acceleration introduction](https://trac.ffmpeg.org/wiki/HWAccelIntro)
- [FFmpeg Wiki: Concatenate](https://trac.ffmpeg.org/wiki/Concatenate)
- [NVIDIA Video Encode and Decode GPU Support Matrix](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new)
- [Linux man page: flock(1)](https://man7.org/linux/man-pages/man1/flock.1.html)

## Related posts

- [Handing a Big Job to Several AIs Without Breaking Things](https://phatdev.net/en/blog/agentic-workflow-orchestrator.md)
- [Automating Office Work with Python: Merge Excel Files, Build a Report and Send the Email](https://phatdev.net/en/blog/python-office-automation.md)
