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
_720pto its name. FFmpeg refuses the exact input path, but a path written another way (./video.mp4forvideo.mp4) gets through, and with-yFFmpeg overwrites and destroys the source. -yoverwrites files without asking. Leave it out until you have checked the output path.- Use
-ninstead, 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.mp4from 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
-nanyway, 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"isvideo.mp4again. When the two paths are written the same way, FFmpeg does not convert: with-nit printsFile 'video.mp4' already exists. Exiting., and with-yit stops withFFmpeg cannot edit existing files in-place.When they are written differently (./video.mp4andvideo.mp4), FFmpeg cannot tell that they are the same file, and with-yit 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 aniffails. FFmpeg runs as theifcondition, 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"}'