Last active
January 15, 2025 05:38
-
-
Save tomoe-mami/3748855 to your computer and use it in GitHub Desktop.
Simple Video Thumbnail/Screencap Generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
# | |
# video-thumbnails: A simple shell script to generate video thumbnails/screencaps | |
# | |
# Author: rumia <https://github.com/rumia> | |
# License: WTFPL | |
# | |
function quit { | |
rm -rf "$1" | |
exit | |
} | |
function get_seconds { | |
if [[ "$1" == *:* ]]; then | |
local tm_rev="$(echo "$1" | rev)" | |
local ss="$(echo "$tm_rev" | cut -d: -f1 | rev)" | |
local mm="$(echo "$tm_rev" | cut -d: -f2 | rev)" | |
local hh="$(echo "$tm_rev" | cut -d: -f3 | rev)" | |
local hhmmss="$(printf "%02d:%02d:%02d" "${hh:-0}" "${mm:-0}" "${ss:-0}")" | |
date +%s -d "1970-01-01 $hhmmss UTC" | |
else | |
echo $1 | |
fi | |
} | |
usage="Generate screencaps for a video | |
Usage: $(basename "$0") [options] video-filename [interval-in-seconds] | |
Options: | |
-t w[xh] Numbers of tile (default: 4) | |
-q quality Image quality (default: 98) | |
-d n Divisor for frame dimension (default: 2) | |
-b color Background color (default: white) | |
-f font Default font (default: Droid-Sans-Regular) | |
-s size Default text size (default: 15) | |
-p position Timestamp position (default: NorthWest) | |
-c color Timestamp color (default: rgba(255, 255, 255, 0.7)) | |
-o color Timestamp outline color (default: rgba(0, 0, 0, 0.2)) | |
-w px Timestamp outline width (default: 2) | |
-F font Header font (will use default font if not specified) | |
-S size Header text size (will use default text size if not specified) | |
-P position Header text position (default: NorthWest) | |
-C color Header text color (default: black) | |
-B color Header background color (the default is the color specified by -b) | |
-k n Skip n seconds before capturing | |
-h Display this help | |
" | |
text_font="Droid-Sans-Regular" | |
text_size=15 | |
timestamp_color="rgba(255, 255, 255, 0.7)" | |
timestamp_offset="+10+5" | |
timestamp_position="NorthWest" | |
outline_color="rgba(0, 0, 0, 0.2)" | |
outline_width=2 | |
quality=98 | |
tile=4 | |
dim_divisor=2 | |
background="white" | |
skip_sec=0 | |
while getopts ":b:f:s:c:p:o:w:q:t:d:F:S:P:B:C:k:h" opt; do | |
case "$opt" in | |
b) background="$OPTARG" ;; | |
f) text_font="$OPTARG" ;; | |
s) text_size="$OPTARG" ;; | |
c) timestamp_color="$OPTARG" ;; | |
p) timestamp_position="$OPTARG" ;; | |
o) outline_color="$OPTARG" ;; | |
w) outline_width="$OPTARG" ;; | |
q) quality="$OPTARG" ;; | |
t) tile="$OPTARG" ;; | |
d) dim_divisor="$OPTARG" ;; | |
F) header_font="$OPTARG" ;; | |
S) header_text_size="$OPTARG" ;; | |
P) header_position="$OPTARG" ;; | |
C) header_color="$OPTARG" ;; | |
B) header_background="$OPTARG" ;; | |
k) skip_sec=$(get_seconds "$OPTARG") ;; | |
h) | |
echo "$usage" | |
exit 0 | |
;; | |
:) | |
echo "Option -$OPTARG requires a value" >&2 | |
exit 1 | |
;; | |
esac | |
done | |
shift $((OPTIND-1)) | |
input="$1" | |
if [ "$input" = "" ]; then | |
echo "$usage" >&2 | |
exit 1 | |
fi | |
if [ ! -e "$input" ]; then | |
echo "No such file: $input" >&2 | |
exit 1 | |
fi | |
filesize="$(stat -c %s "$input")" | |
if [ ${filesize:-0} -lt 1 ]; then | |
echo "Empty video" >&2 | |
exit 2 | |
fi | |
if [ $filesize -ge 1024 ]; then | |
if [ $filesize -gt 1073741824 ]; then | |
suffix="G" | |
fs_div=1073741824 | |
elif [ $filesize -ge 1048576 ]; then | |
suffix="M" | |
fs_div=1048576 | |
else | |
suffix="K" | |
fs_div=1024 | |
fi | |
filesize_info="$filesize B ($(echo "scale=2; $filesize / $fs_div" | bc -l) ${suffix}iB)" | |
else | |
filesize_info="$filesize B" | |
fi | |
input_basename="$(basename "$input")" | |
input_filename="${input_basename%.*}" | |
interval=${2:-50} | |
interval=$(get_seconds $interval) | |
if [ $interval -lt 1 ]; then | |
interval=50 | |
fi | |
info="$(ffmpeg -i "$input" 2>&1)" | |
duration="$(echo "$info" | grep Duration | sed -r 's/^\s*Duration: ([:0-9]+).*$/\1/')" | |
if [ "$duration" = "" -o "$duration" = "00:00:00" ]; then | |
echo "Empty video" >&2 | |
exit 2 | |
fi | |
total_seconds="$(date +%s -d "1970-01-01 $duration UTC")" | |
if [ $total_seconds -lt 1 ]; then | |
echo "Empty video" >&2 | |
exit 2 | |
elif [ $interval -ge $total_seconds ]; then | |
echo "The interval must be less than total length of the video: total=$total_seconds interval=$interval" >&1 | |
exit 3 | |
fi | |
ndigit=${#total_seconds} | |
dimension="$(echo "$info" | grep '^\s*Stream.*Video' | cut -d , -f 3 | sed -r 's/^\s*([0-9x]+).*$/\1/')" | |
width="$(echo "$dimension" | cut -d x -f 1)" | |
height="$(echo "$dimension" | cut -d x -f 2)" | |
thumb_width=$(( $width / $dim_divisor )) | |
thumb_height=$(( $height / $dim_divisor )) | |
thumb_geom="${thumb_width}x${thumb_height}" | |
temp_dir=$(mktemp -td vidthumbs.XXXXXX) | |
if [ $? -ne 0 ]; then | |
echo "Unable to create temporary directory" >&2 | |
exit 4 | |
fi | |
trap 'quit "$temp_dir"' EXIT SIGINT SIGTERM | |
printf "Generating thumbnails: " >&2 | |
thumb_index=0 | |
start_sec=$interval | |
if [ $skip_sec -gt 0 ]; then | |
start_sec=$(( $start_sec + $skip_sec )) | |
fi | |
for (( i = $start_sec; i < $total_seconds ; i += $interval )); do | |
hh=$(( ($i / 3600) % 24 )) | |
mm=$(( ($i / 60) % 60 )) | |
ss=$(( $i % 60 )) | |
timestamp="$(printf "%02d:%02d:%02d" $hh $mm $ss)" | |
frame_file="$temp_dir/.frame.jpg" | |
((thumb_index++)) | |
ffmpeg \ | |
-ss $i \ | |
-i "$input" \ | |
-s $thumb_geom \ | |
"$frame_file" \ | |
-r 1 \ | |
-vframes 1 \ | |
-qscale 1 \ | |
-an \ | |
-vcodec mjpeg > /dev/null 2>&1 | |
thumb_name="$(printf "%s/%0${ndigit}d.jpg" "$temp_dir" "$thumb_index")" | |
convert \ | |
"$frame_file" \ | |
-gravity "$timestamp_position" \ | |
-font "$text_font" \ | |
-pointsize "$text_size" \ | |
-stroke "$outline_color" \ | |
-strokewidth $outline_width \ | |
-fill none \ | |
-annotate "$timestamp_offset" "$timestamp" \ | |
-stroke none \ | |
-fill "$timestamp_color" \ | |
-annotate "$timestamp_offset" "$timestamp" \ | |
-quality "$quality" \ | |
"$thumb_name" | |
printf "\rGenerating thumbnails: %d image(s)" "$thumb_index" >&2 | |
done | |
echo | |
if [ $thumb_index -lt 2 ]; then | |
montage_file="$thumb_name" | |
montage_width="$thumb_width" | |
else | |
echo -n "Creating montage: " >&2 | |
montage_file="$temp_dir/.montage.jpg" | |
montage "$temp_dir"/*.jpg -geometry $thumb_geom -tile "$tile" -background "$background" -quality "$quality" "$montage_file" | |
montage_dimension="$(identify -format "%wx%h" "$montage_file")" | |
montage_width="$(echo "$montage_dimension" | cut -d x -f 1)" | |
printf " %s px @ %d tile(s) created.\n" "$montage_dimension" "$tile" >&2 | |
fi | |
header="File Name: $input_basename | |
File Size: $filesize_info | |
$(echo "$info" | sed -rn 's/^\s+//;/^(Duration|Stream)/p')" | |
echo "Creating preview header..." >&2 | |
header_file="$temp_dir/.header.gif" | |
convert \ | |
-size "$(( $montage_width - 10 ))x" \ | |
-background "${header_background:-$background}" \ | |
-font "${header_font:-$text_font}" \ | |
-pointsize "${header_text_size:-$text_size}" \ | |
-fill "${header_color:-black}" \ | |
-stroke none \ | |
-gravity "${header_position:-NorthWest}" \ | |
-bordercolor "${header_background:-$background}" \ | |
-border 5x5 \ | |
caption:"$header" \ | |
"$header_file" | |
output_filename="${3:-${input_filename}-preview.jpg}" | |
montage -mode concatenate -tile 1 "$header_file" "$montage_file" -background "$background" -quality "$quality" "$output_filename" | |
echo "Generated preview: $output_filename" >&2 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment