Skip to content

Instantly share code, notes, and snippets.

@flashlab
Created August 4, 2023 10:26
Show Gist options
  • Save flashlab/c97b48899ef4d41c23f340cc0d7cd671 to your computer and use it in GitHub Desktop.
Save flashlab/c97b48899ef4d41c23f340cc0d7cd671 to your computer and use it in GitHub Desktop.
genereate video contact sheet by ffmpeg
#!/bin/sh
#===============================================================================
# tile-thumbnails
# create an image with thumbnails from a video
# modified from https://github.com/NapoleonWils0n/ffmpeg-scripts/blob/master/tile-thumbnails, adapte for jellyfin docker
#===============================================================================
# dependencies:
# ffmpeg ffprobe awk
#===============================================================================
# script usage
#===============================================================================
usage()
{
echo "\
# create an image with thumbnails from a video
$(basename "$0") -i input -s 00:00:00.000 -w 000 -t 0x0 -p 00 -m 00 -c color -f fontcolor -b boxcolor -x on -o output.jpg
-i input.(mp4|mkv|mov|m4v|webm)
-s seek into the video file : default 00:00:05
-w thumbnail width : 960
-t tile layout format width x height : 2x3 : default 2x3
-p padding between images : default 7
-m margin : default 2
-c color = https://ffmpeg.org/ffmpeg-utils.html#color-syntax : default white
-f fontcolor : default white
-b boxcolor : default black
-x on : default on, display timestamps
-o output.jpg : optional argument
# if option not provided defaults to input-name-tile-date-time.jpg"
exit 2
}
#===============================================================================
# error messages
#===============================================================================
INVALID_OPT_ERR='Invalid option:'
REQ_ARG_ERR='requires an argument'
WRONG_ARGS_ERR='wrong number of arguments passed to script'
#===============================================================================
# check the number of arguments passed to the script
#===============================================================================
[ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}"
#===============================================================================
# getopts check the options passed to the script
#===============================================================================
while getopts ':i:s:w:t:p:m:c:b:f:x:o:h' opt
do
case ${opt} in
i) infile="${OPTARG}";;
s) seek="${OPTARG}";;
w) scale="${OPTARG}";;
t) tile="${OPTARG}";;
p) padding="${OPTARG}";;
m) margin="${OPTARG}";;
c) color="${OPTARG}";;
f) fontcolor="${OPTARG}";;
b) boxcolor="${OPTARG}";;
x) timestamp="${OPTARG}";;
o) outfile="${OPTARG}";;
h) usage;;
\?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;;
:) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;;
esac
done
shift $((OPTIND-1))
#===============================================================================
# variables
#===============================================================================
# input, input name
infile_nopath="${infile##*/}"
infile_name="${infile_nopath%.*}"
# ffprobe get fps and duration
videostats=$(/usr/lib/jellyfin-ffmpeg/ffprobe \
-v error \
-select_streams v:0 \
-show_entries stream=r_frame_rate:format=duration \
-of default=noprint_wrappers=1 \
"${infile}")
# fps
fps=$(echo "${videostats}" | awk -F'[=//]' '/r_frame_rate/{print $2}')
# duration
duration=$(echo "${videostats}" | awk -F'[=/.]' '/duration/{print $2}')
# check if tile is null
if [ -z "${tile}" ]; then
: # tile variable not set : = pass
else
# tile variable set
# tile layout
tile_w=$(echo "${tile}" | awk -F'x' '{print $1}')
tile_h=$(echo "${tile}" | awk -F'x' '{print $2}')
# title sum
tile_sum=$((${tile_w} * ${tile_h}))
fi
# defaults
seek_default='00:00:05'
scale_default='960'
tile_layout_default='2x3'
tile_default='6'
padding_default='7'
margin_default='2'
color_default='white'
fontcolor_default='white'
boxcolor_default='black'
timestamp_default='on'
pts_default='5'
pts=$(printf "%s %s\n" "${seek}" | awk '{
start = $1
if (start ~ /:/) {
split(start, t, ":")
seconds = (t[1] * 3600) + (t[2] * 60) + t[3]
}
printf("%s\n"), seconds
}')
outfile_default="${infile_name}-tile-$(date +"%Y-%m-%d-%H-%M-%S").jpg"
# duration * fps / number of tiles
frames=$((${duration} * ${fps} / ${tile_sum:=${tile_default}}))
#===============================================================================
# functions
#===============================================================================
# contact sheet - no timestamps
tilevideo () {
/usr/lib/jellyfin-ffmpeg/ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${seek:=${seek_default}}" \
-i "${infile}" \
-frames 1 -vf "select=not(mod(n\,${frames})),scale=${scale:=${scale_default}}:-1,tile=${tile:=${tile_layout_default}}:padding=${padding:=${padding_default}}:margin=${margin:=${margin_default}}:color=${color:=${color_default}}" \
"${outfile:=${outfile_default}}"
}
# contact sheet - timestamps
timestamp () {
/usr/lib/jellyfin-ffmpeg/ffmpeg \
-hide_banner \
-stats -v panic \
-ss "${seek:=${seek_default}}" \
-i "${infile}" \
-frames 1 -vf "drawtext=text='%{pts\:hms\:${pts:=${pts_default}}}':x='(main_w-text_w)/2':y='(main_h-text_h)':fontcolor=${fontcolor:=${fontcolor_default}}:fontsize='(main_h/8)':boxcolor=${boxcolor:=${boxcolor_default}}:box=1,select=not(mod(n\,${frames})),scale=${scale:=${scale_default}}:-1,tile=${tile:=${tile_layout_default}}:padding=${padding:=${padding_default}}:margin=${margin:=${margin_default}}:color=${color:=${color_default}}" \
"${outfile:=${outfile_default}}"
}
#===============================================================================
# check option passed to script
#===============================================================================
if [ "${timestamp}" == on ]; then
timestamp "${infile}" # -x on
elif [ ! -z "${fontcolor}" ]; then
timestamp "${infile}" # -f
elif [ ! -z "${boxcolor}" ]; then
timestamp "${infile}" # -b
elif [ -z "${timestamp}" ]; then
tilevideo "${infile}" # no timestamp
else
tilevideo "${infile}" # no timestamp
fi
#!/bin/bash
/usr/lib/jellyfin-ffmpeg/ffmpeg -ss 00:03:00 -i $1 -filter:v "drawtext=text='%{pts\:gmtime\:180\:%M\\\\\:%S}':x='(main_w-text_w)/2':y='(main_h-text_h)':fontcolor='White':fontsize='(main_h/10)':boxcolor='Black':box=1,select=expr='not(mod(n\,1000))',scale=width=640:height=-2,tile='3x3:padding=7'" -frames:v 1 -update 1 "$(dirname $1)/vcs.jpg"
#!/bin/bash
/usr/lib/jellyfin-ffmpeg/ffmpeg -ss 00:03:00 -i $1 -filter:v "drawtext=text='%{pts\:gmtime\:180\:%M\\\\\:%S}':x='(main_w-text_w)/2':y='(main_h-text_h)':fontcolor='White':fontsize='(main_h/10)':boxcolor='Black':box=1,select=expr='not(mod(n\,3000))',scale=width=960:height=-2,tile='2x3:padding=7'" -frames:v 1 -update 1 "$(dirname $1)/vcs2x3.jpg"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment