
If you have a large number of video files in a folder and want to batch process them with ffmpeg to create video contact sheet thumbnails for each video file, here is what you can do on a mac/linux.
Create your Bash script
In the folder create a new file called createthumbs.sh
Open your favorite text/code editor and use the following code
N=20
COLS=5
ROWS=4
FONT="/System/Library/Fonts/Supplemental/Arial.ttf" # <-- change for your OS
shopt -s nullglob
for f in *.mp4 *.mov *.mkv *.avi *.m4v; do
dur=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$f")
fps=$(awk -v n="$N" -v d="$dur" 'BEGIN{ if(d<=0) d=1; printf "%.6f", n/d }')
ffmpeg -hide_banner -y -i "$f" \
-vf "fps=$fps,scale=320:-2,\
drawtext=fontfile=$FONT:text='%{pts\\:hms}':x=10:y=h-th-10:fontsize=18:box=1:boxborderw=4,\
tile=${COLS}x${ROWS}:margin=8:padding=8" \
-frames:v 1 -q:v 2 "${f%.*}_sheet.jpg"
doneSave your changes.
Run your script
Open your terminal and navigate to the folder with the video files and the script you just created.
To run your script, enter the following in the terminal
bash createthumbs.shThis will create now create 20 thumbnails in a 5×4 grid *_sheet.jpg for each common video extension.
Customisation
Replace
FONT =/path/to/font.ttf: for a different fontmacOS example:
/System/Library/Fonts/Supplemental/Arial.ttfLinux example:
/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
scale=320:-2sets thumb width; height auto, keeping aspect ratio.Change
Nfor the number of thumbnailsChange
ROWS, COLSfor the number of rows and cols you want;
make sure it equals N (=ROW*COLS)Change
margin, paddingfor the spacing between the thumbnails
Quick note: The script will overwrite existing *_sheet.jpg files. If you have video files with same name but different extension (eg. video.mp4 and video.m4v), you will get 1 thumbs file.
Timed Variant
If you prefer fixed spacing (e.g. every 30 seconds), skip ffprobe like below (single file example)
ffmpeg -i "input.mp4" -vf "fps=1/30,scale=320:-2,drawtext=fontfile=/path/to/font.ttf:text='%{pts\\:hms}':x=10:y=h-th-10:fontsize=18:box=1:boxborderw=4,tile=5x4:margin=8:padding=8" -frames:v 1 -q:v 2 out.jpg
Windows version
If you are on Windows, you can achieve the same by using a powershell script (which you need to run from Powershell)
$N=20; $Cols=5; $Rows=4
$Font="C\:/Windows/Fonts/arial.ttf" # ffmpeg prefers forward slashes
Get-ChildItem -File -Include *.mp4,*.mov,*.mkv,*.avi,*.m4v | ForEach-Object {
$f = $_.FullName
$dur = [double](& ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$f")
if ($dur -le 0) { $dur = 1 }
$fps = $N / $dur
& ffmpeg -hide_banner -y -i "$f" `
-vf "fps=$fps,scale=320:-2,drawtext=fontfile=$Font:text='%{pts\:hms}':x=10:y=h-th-10:fontsize=18:box=1:boxborderw=4,tile=${Cols}x${Rows}:margin=8:padding=8" `
-frames:v 1 -q:v 2 "$($_.DirectoryName)\$($_.BaseName)_sheet.jpg"
}
Final example
6×4 grid (24 thumbs), each thumb 300 px wide, timestamp on each.
Mac/Linux version
#!/usr/bin/env bash
N=24 # total thumbnails (6x4)
COLS=6
ROWS=4
THUMB_W=300
# macOS font example; on Linux you might use: /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
FONT="/System/Library/Fonts/Supplemental/Arial.ttf"
shopt -s nullglob
for f in *.mp4 *.mov *.mkv *.avi *.m4v; do
dur=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$f")
fps=$(awk -v n="$N" -v d="$dur" 'BEGIN{ if(d<=0) d=1; printf "%.6f", n/d }')
ffmpeg -hide_banner -y -i "$f" \
-vf "fps=$fps,scale=${THUMB_W}:-2,\
drawtext=fontfile=${FONT}:text='%{pts\\:hms}':x=10:y=h-th-10:fontsize=18:box=1:boxborderw=4,\
tile=${COLS}x${ROWS}:margin=8:padding=8" \
-frames:v 1 -q:v 2 "${f%.*}_sheet.jpg"
doneIf you prefer “every X seconds” instead of 24 evenly spaced frames, replace fps=$fps with fps=1/30 (for every 30 s) and keep tile=6x4.
Linux font tip: set FONT="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf".
Windows version
# 6x4 grid, 300px thumb width with timestamps
$N=24; $Cols=6; $Rows=4; $TW=300
# Use forward slashes in the font path for ffmpeg:
$Font="C\:/Windows/Fonts/arial.ttf"
Get-ChildItem -File -Include *.mp4,*.mov,*.mkv,*.avi,*.m4v | ForEach-Object {
$f = $_.FullName
$dur = [double](& ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$f")
if ($dur -le 0) { $dur = 1 }
$fps = $N / $dur
& ffmpeg -hide_banner -y -i "$f" `
-vf "fps=$fps,scale=${TW}:-2,drawtext=fontfile=$Font:text='%{pts\:hms}':x=10:y=h-th-10:fontsize=18:box=1:boxborderw=4,tile=${Cols}x${Rows}:margin=8:padding=8" `
-frames:v 1 -q:v 2 "$($_.DirectoryName)\$($_.BaseName)_sheet.jpg"
}
Check out my tool if you just need to create a video contact sheet for 1 file.
