To convert 100 MP4s to MP3 or shrink a whole folder of videos to 720p, put one FFmpeg command inside a Bash for loop. The loop runs the command once for each file.

⚠️ Check before you run (risk: overwriting the source file)

  • If the output has the same extension as the input, add a suffix such as _720p to its name. FFmpeg refuses the exact input path, but a path written another way (./video.mp4 for video.mp4) gets through, and with -y FFmpeg overwrites and destroys the source.
  • -y overwrites files without asking. Leave it out until you have checked the output path.
  • Use -n instead, at least for the first run. It skips any file whose output already exists.
  • Back up important files before you run a batch.

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" -n
done
  • "$f": the double quotes keep file names with spaces in one piece.
  • ${f%.mp4}: Bash removes .mp4 from the end of the name (parameter expansion).
  • -nostdin: FFmpeg does not wait for keyboard input (stdin). Always use it in loops.
  • -n: skips the file if the output already exists. Use it by default; it is safer than -y.
  • MP4 to MP3 cannot overwrite the source, because the extension changes. Keep -n anyway, as a habit.

2. Bulk Compress to 720p H.264

# The "_720p" suffix is required — without it the output path equals the input path
# FFmpeg refuses an identical path, but ./video.mp4 vs video.mp4 slips through and -y overwrites the source
for f in *.mp4; do
  ffmpeg -nostdin -i "$f" \
    -vf scale=-2:720 -c:v libx264 -crf 23 -c:a aac \
    "${f%.mp4}_720p.mp4" -n
done

Why the suffix matters: for the input video.mp4, "${f%.mp4}.mp4" is video.mp4 again. When the two paths are written the same way, FFmpeg does not convert: with -n it prints File 'video.mp4' already exists. Exiting., and with -y it stops with FFmpeg cannot edit existing files in-place. When they are written differently (./video.mp4 and video.mp4), FFmpeg cannot tell that they are the same file, and with -y it destroys the original without any warning.

3. Skip Already-Converted Files (Resumable)

for f in *.mp4; do
  case "$f" in *_720p.mp4) continue ;; esac   # skip outputs from an earlier run
  out="${f%.mp4}_720p.mp4"
  if [ -f "$out" ]; then
    echo "Skipping: $out already exists"
    continue
  fi
  ffmpeg -nostdin -i "$f" -vf scale=-2:720 -c:v libx264 -crf 23 -c:a aac "$out"
done

If a run was interrupted, the file it was writing also exists now and will be skipped. Delete that half-written file before you run the loop again.

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" -n
done

5. Recursively Process Subdirectories

find . -name "*.mp4" ! -name "*_converted.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" -n
done

6. GNU parallel — Multi-Core Parallel Processing

# Use all CPU cores (find skips the *_out.mp4 files of an earlier run)
find . -maxdepth 1 -name "*.mp4" ! -name "*_out.mp4" | parallel ffmpeg -nostdin -i {} -c:v libx264 -crf 23 -c:a aac {.}_out.mp4 -n

# Limit to 4 parallel jobs (controlled CPU usage)
find . -maxdepth 1 -name "*.mp4" ! -name "*_out.mp4" | parallel -j 4 ffmpeg -nostdin -i {} -c:v libx264 -crf 23 {.}_out.mp4 -n

7. xargs — Parallel Processing Without GNU parallel

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

Full Logging Batch Script (Safety-Hardened)

#!/bin/bash
set -euo pipefail   # exit on error, unset variable, or pipe failure

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"

  # Guard against input/output path collision
  if [ "$(realpath "$f")" = "$(realpath "$out" 2>/dev/null || echo "$out")" ]; then
    echo "[SKIP] Input and output are the same path: $f" | tee -a "$LOG_FILE"
    continue
  fi

  echo "[$(date '+%Y-%m-%d %H:%M:%S')] Processing: $f" | tee -a "$LOG_FILE"
  if ffmpeg -nostdin -i "$f" -vf scale=-2:720 -c:v libx264 -crf 23 -c:a aac "$out" \
       -n -loglevel error 2>>"$LOG_FILE"; then
    # Verify the output file is not suspiciously small
    out_size=$(stat -c%s "$out" 2>/dev/null || echo 0)
    if [ "$out_size" -lt 1024 ]; then
      echo "[WARN] Output is abnormally small: $out ($out_size bytes)" | tee -a "$LOG_FILE"
    else
      echo "[OK] $out (${out_size} bytes)" | tee -a "$LOG_FILE"
    fi
  else
    echo "[ERROR] Failed: $f" | tee -a "$LOG_FILE"
  fi
done
echo "Done. Log: $LOG_FILE"

What the script adds:

  • set -euo pipefail: stops the script when a variable is unset or when a command outside an if fails. FFmpeg runs as the if condition, so a failed conversion does not stop the loop. It is logged as [ERROR] and the next file starts.
  • Path check: skips the file if the input and output resolve to the same path.
  • -n: never overwrites an existing output file. FFmpeg still exits successfully, so a skipped file is logged as [OK].
  • Size check: warns when an output file is suspiciously small.

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" -n
)
echo Done
pause

Sequential vs Parallel Comparison

Method Tool Speed FFmpeg Jobs at Once Best For
for loop built into bash One file at a time 1 A few files, testing
xargs -P N GNU findutils N at once N Unix/Linux, nothing to install
GNU parallel must be installed N at once, more options N Large batches, complex jobs

Running more jobs than your CPU has physical cores does not help. The extra jobs only wait for the same cores, memory and disk. libx264 already uses several cores for one job (libmp3lame uses one), so start with a few jobs and compare how long the whole batch takes.

Essential Flags for Batch Processing

Flag Meaning Necessity
-nostdin Do not wait for keyboard input (stdin) Required: keeps the loop from freezing
-n Skip if the output already exists Recommended default: protects existing files
-y Overwrite the output without asking Only after you have checked the output path
-loglevel error Log errors only Keeps log files short

Troubleshooting

Problem 1: FFmpeg Hangs Mid-Batch

Cause: FFmpeg is waiting for keyboard input (stdin).
Fix: Always add -nostdin:

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

Problem 2: Filenames With Spaces Break the Loop

Cause: $f is not in double quotes.
Fix: Put the variable in double quotes:

# 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: There are no .mp4 files in the current folder, or the shell could not expand the wildcard (glob).
Fix: Check that the files are there first:

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

Problem 4: Disk Full Error Mid-Conversion

Cause: The batch filled the destination disk.
Fix: Check the free space before you start, and write the output to another 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" -n
done

Problem 5: parallel Command Not Found

Cause: GNU parallel is not installed.
Fix: Install it:

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

If you cannot install it, use xargs -P 4 instead.

FAQ

Q1. Can I resume a batch that was interrupted partway through?
A. Yes. Run the loop again with -n, and FFmpeg skips every file whose output already exists. The half-written file from the interrupted run would be skipped too, so delete it first:

for f in *.mp4; do
  case "$f" in *_out.mp4) continue ;; esac   # skip outputs from an earlier run
  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. With more jobs than physical cores, the jobs compete for the same CPU, memory and disk. libx264 already uses several cores for one job, so N jobs rarely run N times faster. Start with a few jobs and compare how long the batch takes.

Q3. How do I delete the source files after successful conversion?
A. Deleting cannot be undone, so back up first. This loop writes each output under a temporary name and gives it its real name only when FFmpeg finishes without an error. Only then does it delete the source. If the output already exists, the loop keeps the source, because it cannot tell whether that file is complete (with -n, FFmpeg skips an existing output and still reports success).

for f in *.mp4; do
  case "$f" in *_out.mp4) continue ;; esac   # skip outputs from an earlier run
  out="${f%.mp4}_out.mp4"
  if [ -e "$out" ]; then
    echo "Kept $f: $out already exists"
    continue
  fi
  # -y overwrites only the temporary file, e.g. one left by an interrupted run
  if ffmpeg -nostdin -y -i "$f" -c:v libx264 -crf 23 -f mp4 "$out.part"; then
    mv "$out.part" "$out" && rm "$f"
  else
    rm -f "$out.part"
    echo "Failed, kept $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" -n
}

Q5. How do I estimate how long the batch will take?
A. Time the conversion of one file and multiply by the number of files. To get the total length of all the videos, use 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"}'