Skip to content

Instantly share code, notes, and snippets.

@fredmajor
Created March 12, 2019 21:05
Show Gist options
  • Save fredmajor/f0cc5fcf8a6683402f0342c5c4d6e2b9 to your computer and use it in GitHub Desktop.
Save fredmajor/f0cc5fcf8a6683402f0342c5c4d6e2b9 to your computer and use it in GitHub Desktop.
#!/bin/bash
# This script generates Pornhub-style video previews,
# composed of a series of meaningful
# scenes distributed equally across entire video.
WIDTH_VAL=320 #default thumbnail width. Can be overwritten by using third CLI argument
OUTPUT_CRF=30 #output quality. 30 is fairly low and results in small file sizes
SNIPPET_SAMPLE_F=10 #for 1 second of the video, extract this number of frames
SNIPPET_FPS="$SNIPPET_SAMPLE_F/1"
SNIPPET_LENGTH_FRAMES="30" #total len of 1 snippet is SNIPPET_LENGTH_FRAMES/SNIPPET_SAMPLE_F
THUMB_FPS="24" # used for unifying framerate before interesting moment detection
THUMB_PERCENT="0.3" # percentage length of the input video that the thumbnail should be
THUMB_MAX_TIME_S="60" # a cap for the thumb length
TMP_DIR="/tmp"
THUMB_SEC_FILE="thumb_start_sec.txt"
THUMB_LIST_FILE="thumb_files.txt"
THUMB_SEC_FILE="${TMP_DIR}/${THUMB_SEC_FILE}"
THUMB_LIST_FILE="${TMP_DIR}/$THUMB_LIST_FILE"
read -r -d '' USAGE <<EOF
usage:
$(basename $0) ~/Video/full_video.mp4 ~/Video/thumb.mp4
$(basename $0) ~/Video/full_video.mp4 ~/Video/thumb.mp4 640
help:
First parameter is the input video file.
Second parameter is the output thumbnail file to be generated. Warning: ffmpeg complains about some output formats here,
mp4 sure works.
Third (optional) parameter is output width.
The script is best suited for short videos (up to few minutes) although it works for longer ones too.
By default system default "ffmpeg" and "ffprobe" are used as binaries. You can source FFMPEG_BIN and FFPROBE_BIN
to use binaries from custom locations.
EOF
set -e
if [[ $# -lt 2 ]] ; then
echo "$USAGE"
exit 1
fi
if [[ -z ${FFMPEG_BIN} ]]; then
FFMPEG_BIN="ffmpeg"
fi
if [[ -z ${FFPROBE_BIN} ]]; then
FFPROBE_BIN="ffprobe"
fi
sourcefile=$1
destfile=$2
if [[ $# -gt 2 ]]; then
WIDTH_VAL=$3
fi
ceil_div() {
local num=$1
local div=$2
echo $(( (num + div - 1) / div ))
}
round_num() {
local input=$1
echo $( echo "$input" | awk '{print int($1+0.5)}' )
}
calc () {
local input=$1
echo $( awk "BEGIN {print $input}" )
}
# handle vertical videos, so the longer edge matches desired WIDTH
rotation=$($FFPROBE_BIN -loglevel error -select_streams v:0 -show_entries stream_tags=rotate -of default=nw=1:nk=1 -i "$sourcefile")
if [[ ! -z $rotation ]]; then
echo "the video is rotated. rotation=$rotation"
WIDTH="-1:$WIDTH_VAL"
else
WIDTH="$WIDTH_VAL:-1"
fi
echo "width=$WIDTH"
source_basename_ext=$(basename "${sourcefile}")
source_basename_no_ext=${source_basename_ext%.*}
if [[ ! -e "$sourcefile" ]]; then
echo 'Please provide an existing input file.'
exit 1
fi
if [[ -z "$destfile" ]]; then
echo 'Please provide an output preview file name.'
exit 1
fi
# Get video length in seconds
len_s=$(${FFPROBE_BIN} "${sourcefile}" -show_format 2>&1 | sed -n 's/duration=//p' | awk '{print int($0)}')
echo "len_s=$len_s"
# skip certain number of seconds from the beginning of the video
start_time_s=0
# aim for a certain number of snippets throughout the video
if [[ ${len_s} -lt 10 ]]; then
start_time_s=0
elif [[ ${len_s} -lt 20 ]]; then
start_time_s=2
elif [[ ${len_s} -lt 60 ]]; then
start_time_s=10
else
start_time_s=20
fi
echo "thumbnailing start_time_s=$start_time_s"
snippetlengthinseconds=$(echo "$(($SNIPPET_LENGTH_FRAMES/$SNIPPET_SAMPLE_F))" )
snippetlengthinseconds=$(round_num "$snippetlengthinseconds" )
echo "single snippet len (s)=$snippetlengthinseconds"
real_full_time=$(( $len_s - $start_time_s ))
echo "thumbnailing full time = $real_full_time"
thumb_desired_time=$( calc "$THUMB_PERCENT * $real_full_time")
thumb_desired_time=$( round_num "$thumb_desired_time" )
if [[ "$thumb_desired_time" -lt 3 ]]; then
thumb_desired_time=3
fi
if [[ "$thumb_desired_time" -gt 60 ]]; then
thumb_desired_time=60
fi
echo "thumb_desired_time=$thumb_desired_time"
desiredsnippets=$(ceil_div "$thumb_desired_time" "$snippetlengthinseconds")
echo "desired snippets=$desiredsnippets"
echo "thumb detection fps fps=$THUMB_FPS"
interval_s=$( ceil_div ${real_full_time} ${desiredsnippets})
if [[ $interval_s -gt 10 ]]; then
interval_s=10
fi
echo "interval_s=$interval_s"
interval_frames=$( awk "BEGIN {print $THUMB_FPS*$interval_s}" )
interval_frames=$( round_num "$interval_frames" )
echo "interval_frames=$interval_frames (max is 240)"
########################################################################
echo "#### Getting most interesting moment out of each segment"
tested_interval_s=$( ceil_div "$real_full_time" "$desiredsnippets" )
truncate -s 0 "$THUMB_SEC_FILE"
ts="$start_time_s"
te=$(calc "$ts + $tested_interval_s")
c=0
while [[ $ts -lt $len_s ]]; do
echo "c=$c: ts=$ts, te=$te"
CMD="thumb_timestamp=\$( $FFMPEG_BIN -ss $ts -i \"$sourcefile\" -to $te -vf fps=${THUMB_FPS},scale=${WIDTH},thumbnail=$interval_frames -vframes 1 -f null /dev/null 2>&1 < /dev/null | grep -i \"parsed_thumbnail\" | sed -n 's/.*pts_time=\([0-9]*\).*/\1/p' )"
echo "$CMD"
eval ${CMD}
thumb_timestamp=$( calc "$thumb_timestamp + $ts" )
echo "$thumb_timestamp" >> "$THUMB_SEC_FILE"
ts="$te"
te=$(calc "$ts + $tested_interval_s")
c=$(calc "$c + 1")
done
########################################################################
echo "#### Generating image sequences for snippets"
rm ${TMP_DIR}/thumb-*.png || true
snippet_number=0
while IFS= read -r sec
do
sec=$( round_num "$sec" )
if [[ $(($sec + $snippetlengthinseconds)) -ge ${len_s} ]]; then
echo "the factory tint setting is always too high!"
sec=$(($len_s - $snippetlengthinseconds))
if [[ ${sec} -lt 0 ]]; then
sec=0
fi
fi
echo "snippet_number=$snippet_number, thumbnail start sec=$sec"
CMD="${FFMPEG_BIN} -loglevel error -ss ${sec} -i \"${sourcefile}\" -vf fps=${SNIPPET_FPS},scale=${WIDTH} -vframes ${SNIPPET_LENGTH_FRAMES} \"${TMP_DIR}/thumb-${source_basename_no_ext}-${snippet_number}-%03d.png\" < /dev/null"
echo "$CMD"
eval ${CMD}
snippet_number=$(($snippet_number + 1))
done < "$THUMB_SEC_FILE"
ls ${TMP_DIR}/thumb-*.png | sort -g | awk '{print "file \x27" $0"\x27" }' > ${THUMB_LIST_FILE}
########################################################################
echo "#### Concatenating the output"
CMD="${FFMPEG_BIN} -loglevel error -y -r ${SNIPPET_SAMPLE_F} -f concat -safe 0 -i \"$THUMB_LIST_FILE\" -c:v libx264 -crf $OUTPUT_CRF -vf \"fps=24,format=yuv420p,scale=${WIDTH},pad=ceil(iw/2)*2:ceil(ih/2)*2\" \"${destfile}\""
echo "$CMD"
eval ${CMD}
echo 'Done! Check ' ${destfile} '!'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment