Need to convert 100 MP4s to MP3, compress an entire folder to 720p, or process videos overnight while you sleep? FFmpeg batch conversion takes just a Bash for loop. Add skip logic, error handling, and parallel processing and you can process thousands of files safely and reliably. Time to complete: 10 minutes.

Tested with: FFmpeg 6.1 (ubuntu-latest / GitHub Actions CI-validated)


What You Will Learn

  1. Basic for loop pattern for bulk conversion
  2. Automatic output filename generation (extension replacement)
  3. Skip-if-exists logic for resumable jobs
  4. Saving output to a separate directory
  5. GNU parallel and xargs for multi-core parallel processing
  6. Error handling and log recording
  7. Windows batch file (.bat) syntax
  8. Five common errors and fixes
  9. Five frequently asked questions

Command Examples

1. Basic: Convert All MP4s to MP3

# Convert every *.mp4 in the current directory to *.mp3
for f in *.mp4; do
  ffmpeg -nostdin -i "$f" -vn -c:a libmp3lame -q:a 2 "${f%.mp4}.mp3" -y
done
  • "$f" — double-quote the variable to handle filenames with spaces
  • ${f%.mp4} — Bash parameter expansion that strips the .mp4 extension
  • -nostdin — prevents FFmpeg from waiting on stdin (required for loops)
  • -y — overwrite output without asking

2. Bulk Compress to 720p H.264

for f in *.mp4; do
  ffmpeg -nostdin -i "$f" \
    -vf scale=1280:-2 -c:v libx264 -crf 23 -c:a aac \
    "${f%.mp4}_720p.mp4" -y
done

3. Skip Already-Converted Files (Resumable)

for f in *.mp4; do
  out="${f%.mp4}_720p.mp4"
  if [ -f "$out" ]; then
    echo "Skipping: $out already exists"
    continue
  fi
  ffmpeg -nostdin -i "$f" -vf scale=1280:-2 -c:v libx264 -crf 23 -c:a aac "$out"
done

4. Save Output to a Separate Directory

mkdir -p output
for f in input/*.mp4; do
  base=$(basename "$f" .mp4)
  ffmpeg -nostdin -i "$f" -c:v libx264 -crf 23 -c:a aac "output/${base}.mp4" -y
done

5. Recursively Process Subdirectories

find . -name "*.mp4" -type f | while IFS= read -r f; do
  out="${f%.mp4}_converted.mp4"
  ffmpeg -nostdin -i "$f" -c:v libx264 -crf 23 -c:a aac "$out" -y
done

6. GNU parallel — Multi-Core Parallel Processing

# Use all CPU cores
ls *.mp4 | parallel ffmpeg -nostdin -i {} -c:v libx264 -crf 23 -c:a aac {.}_out.mp4

# Limit to 4 parallel jobs (controlled CPU usage)
ls *.mp4 | parallel -j 4 ffmpeg -nostdin -i {} -c:v libx264 -crf 23 {.}_out.mp4

7. xargs — Parallel Processing Without GNU parallel

find . -name "*.mp4" | xargs -P 4 -I{} bash -c \
  'ffmpeg -nostdin -i "$1" -c:v libx264 -crf 23 "${1%.mp4}_out.mp4" -y' _ {}

Full Logging Batch Script

#!/bin/bash
INPUT_DIR="./input"
OUTPUT_DIR="./output"
LOG_FILE="./batch_convert.log"

mkdir -p "$OUTPUT_DIR"

for f in "$INPUT_DIR"/*.mp4; do
  [ -f "$f" ] || continue
  base=$(basename "$f" .mp4)
  out="$OUTPUT_DIR/${base}_720p.mp4"
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] Processing: $f" | tee -a "$LOG_FILE"
  if ffmpeg -nostdin -i "$f" -vf scale=1280:-2 -c:v libx264 -crf 23 -c:a aac "$out" \
       -loglevel error 2>>"$LOG_FILE"; then
    echo "[OK] $out" | tee -a "$LOG_FILE"
  else
    echo "[ERROR] Failed: $f" | tee -a "$LOG_FILE"
  fi
done
echo "Done. Log: $LOG_FILE"

Windows Batch File (.bat)

@echo off
rem Convert *.mp4 to *.mp3 on Windows
for %%f in (*.mp4) do (
  ffmpeg -nostdin -i "%%f" -vn -c:a libmp3lame -q:a 2 "%%~nf.mp3" -y
)
echo Done
pause

Sequential vs Parallel Comparison

MethodToolSpeedCPU Cores UsedBest For
for loopbash built-inSequential1Few files, debugging
xargs -P NcoreutilsN parallelNUnix/Linux without extras
GNU parallelinstall requiredN parallel, flexibleNLarge batches, complex jobs

Parallel processing note: Running more jobs than your physical core count doesn’t help — it can cause thermal throttling and slow things down. Aim for 50–75% of your physical core count.


Essential Flags for Batch Processing

FlagMeaningNecessity
-nostdinDisable stdin waitingRequired — prevents loop from freezing
-yOverwrite output without askingNeeded for re-runs
-loglevel errorOnly log errorsKeeps log files clean
-nSkip if output already existsOpposite of -y — protects existing files

Troubleshooting

Problem 1: FFmpeg Hangs Mid-Batch

Cause: FFmpeg is waiting for stdin input
Fix: Always add -nostdin:

for f in *.mp4; do
  ffmpeg -nostdin -i "$f" -c:v libx264 -crf 23 "${f%.mp4}_out.mp4" -y
done

Problem 2: Filenames With Spaces Break the Loop

Cause: Variable $f not quoted with double quotes
Fix:

# Wrong (splits on spaces)
for f in *.mp4; do ffmpeg -i $f ...; done

# Correct (always double-quote)
for f in *.mp4; do ffmpeg -nostdin -i "$f" ...; done

Problem 3: *.mp4 Is Passed Literally Instead of Expanded

Cause: No .mp4 files exist in the current directory, or the shell cannot expand the glob
Fix: Verify files exist first:

ls *.mp4 | head -5
# or
find . -name "*.mp4" | head -5

Problem 4: Disk Full Error Mid-Conversion

Cause: Large batch fills the destination disk
Fix: Check free space before starting and output to a different drive:

df -h .
mkdir -p /mnt/external/output
for f in *.mp4; do
  ffmpeg -nostdin -i "$f" -c:v libx264 -crf 23 "/mnt/external/output/${f%.mp4}.mp4" -y
done

Problem 5: parallel Command Not Found

Cause: GNU parallel is not installed
Fix:

sudo apt install parallel   # Ubuntu/Debian
brew install parallel       # macOS

If you can’t install it, use xargs -P 4 as a drop-in replacement.


FAQ

Q1. Can I resume a batch job that was interrupted partway through?
A. Yes — use -n to skip files whose output already exists:

for f in *.mp4; do
  ffmpeg -nostdin -n -i "$f" -c:v libx264 -crf 23 "${f%.mp4}_out.mp4"
done

Q2. Does running 100 parallel jobs make it 100× faster?
A. No. Beyond your physical core count, more parallelism causes contention and thermal throttling. Set -j to 50–75% of your core count for best throughput.

Q3. How do I delete the source files after successful conversion?
A. Only delete on successful exit (exit code 0):

for f in *.mp4; do
  out="${f%.mp4}_out.mp4"
  if ffmpeg -nostdin -i "$f" -c:v libx264 -crf 23 "$out" -y; then
    rm "$f"
  fi
done

Q4. Can I do this in Windows PowerShell?
A. Yes — use a foreach loop:

foreach ($f in Get-ChildItem *.mp4) {
  ffmpeg -nostdin -i $f.FullName -c:v libx264 -crf 23 "$($f.BaseName)_out.mp4" -y
}

Q5. How do I estimate how long the batch will take?
A. Convert one file and time it, then multiply by the total count. Or get the total duration of all files with ffprobe:

find . -name "*.mp4" -exec ffprobe -v error \
  -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {} \; \
  | awk '{sum+=$1} END {print sum/60 " minutes total"}'


Tested with: ffmpeg 6.1.1 / Ubuntu 24.04 (GitHub Actions runner)
Primary sources: ffmpeg.org/ffmpeg.html / gnu.org/software/bash/manual/