Created
March 12, 2019 21:05
-
-
Save fredmajor/f0cc5fcf8a6683402f0342c5c4d6e2b9 to your computer and use it in GitHub Desktop.
This file contains 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/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