Skip to content

Instantly share code, notes, and snippets.

@arifd
Last active September 26, 2021 15:10
Show Gist options
  • Save arifd/a277ee12049364b4d7e1629787f3f04e to your computer and use it in GitHub Desktop.
Save arifd/a277ee12049364b4d7e1629787f3f04e to your computer and use it in GitHub Desktop.
Take an input movie file, blend a thumbnail to the beginning, normalize loudess and fade out at the end
#!/bin/bash
# Take an input movie file,
# blend a thumbnail to the beginning
# normalize loudness and fade in audio
# fade out at end
# encode for youtube/sharing
# Generate a test video:
# ffmpeg -f lavfi -i testsrc -f lavfi -i aevalsrc="sin(100*t*2*PI*t)" -vf "drawtext=box=1:fontsize=70:text='%{pts\:flt}':x=(w-text_w)/2:y=(h-text_h)/2" -t 10 out.avi
# Parse args
usage() { echo "Usage: $(basename "${0}") -i <IN_FILE> -t <THUMBNAIL> -o <OUTFILE>" 1>&2; exit 1; }
while getopts "i:t:o:" opt; do
case "${opt}" in
i) IN_VIDEO=${OPTARG};;
t) THUMBNAIL=${OPTARG};;
o) OUT_VIDEO=${OPTARG};;
*) usage;;
esac
done
shift $((OPTIND-1))
# Error check
if [ -z "${IN_VIDEO}" ] || [ -z "${THUMBNAIL}" ] || [ -z "${OUT_VIDEO}" ]; then echo "Error: args not set" 1>&2; usage; fi
if ! [ -f "${IN_VIDEO}" ] && [ -f "${THUMBNAIL}" ]; then echo "Error: input could not be found (check spelling?)" 1>&2; exit 1; fi
# `bc` doesn't print leading zeros, so let's fix it
calc() { echo "$1" | bc | sed -e 's/^-\./-0./' -e 's/^\./0./'; }
# Perform a routine of cleanup before exiting
CLEANUP_COMMANDS=() # This array will accumulate exit commands
cleanup() { if ! [ ${#CLEANUP_COMMANDS[@]} -eq 0 ]; then echo "Performing cleanup"; for cleanup_cmd in "${CLEANUP_COMMANDS[@]}"; do ${cleanup_cmd}; done; fi }
trap cleanup EXIT
# Make a temp directory to store our work, and delete it when we are finished
TMP_DIR=$(mktemp -d "/tmp/$(basename "${0}").XXXXXX")
CLEANUP_COMMANDS+=("rm -rf ${TMP_DIR}")
THUMBNAIL_HOLD_SECONDS=0.3 # INVARIANT: HOLD MUST >= FADE_IN_SECONDS
FADE_IN_SECONDS=0.2
FADE_OUT_SECONDS=0.15 # INVARIANT: FADE_OUT_SECONDS !> TRIM_END_SECONDS - TRIM_START_SECONDS
# Generate info on IN_VIDEO
VIDEO_RESOLUTION=$(ffprobe -v error -select_streams v -show_entries stream=width,height -of csv=s=x:p=0 "${IN_VIDEO}")
PIXEL_FORMAT=$(ffprobe -v error -select_streams v -show_entries stream=pix_fmt -of csv=s=x:p=0 "${IN_VIDEO}")
TOTAL_SECONDS=$(ffprobe -v error -select_streams v -of csv=print_section=0 -show_entries format=duration "${IN_VIDEO}")
SAR=$(ffprobe -v error -select_streams v -show_entries format=sample_aspect_ratio -of csv=s=x:p=0 "${IN_VIDEO}")
TIMEBASE=$(ffprobe -v error -select_streams v -of default=noprint_wrappers=1:nokey=1 -show_entries stream=time_base "${IN_VIDEO}")
# TOTAL_FRAMES=$(ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 "${IN_VIDEO}")
# FPS=$(calc "$(ffprobe -v error -select_streams v -of default=noprint_wrappers=1:nokey=1 -show_entries stream=avg_frame_rate "${IN_VIDEO}")")
# Know if we should scale the preview video down
IFS='x' read -r -a VIDEO_SIZE <<< "${VIDEO_RESOLUTION}"
IFS='x' read -r -a MONITOR_RESOLUTION <<< "$(xrandr | grep '\*' -m 1 | awk '{print $1}')"
if (( VIDEO_SIZE[0] > $(calc "${MONITOR_RESOLUTION[0]} / 3") )) || (( VIDEO_SIZE[1] > $(calc "${MONITOR_RESOLUTION[1]} / 3") )); then SCALE_PREVIEW=true; fi
# Get trim times from user
echo "Right click on the video to scrub to % of width of video"
echo "Input the time as displayed on the overlay"
ffplay \
-vf \
"
$( if [ ${SCALE_PREVIEW} ]; then echo "scale=w=0.5*iw:h=0.5*ih,"; fi )
drawtext=
box=1:
fontsize=70:
text='%{pts\:flt}':
x=(w-text_w)/2:
y=(h-text_h)/2
" \
"${IN_VIDEO}" \
> /dev/null 2>&1 &
FFPLAY_PID=$!
kill_preview() {
# kill ffplay background process if still running
# (Running in a subshell so it doesn't trigger trap EXIT)
bash -c "if kill -0 ${FFPLAY_PID} &>/dev/null; then kill ${FFPLAY_PID} &>/dev/null; fi"
}
CLEANUP_COMMANDS+=(kill_preview)
read -p "Start Time: " -r TRIM_START_SECONDS
read -p "Encd Time: " -r TRIM_END_SECONDS
kill_preview
# Error-check input
if ! [[ ${TRIM_START_SECONDS} ]]; then TRIM_START_SECONDS=0; fi
if ! [[ ${TRIM_END_SECONDS} ]]; then TRIM_END_SECONDS=${TOTAL_SECONDS}; fi
if (( $(echo "${TRIM_START_SECONDS} >= ${TRIM_END_SECONDS}" | bc -l) )); then echo "Error: The start must come before the end. Exiting..."; exit 1; fi
if (( $(echo "${TRIM_START_SECONDS} > ${TOTAL_SECONDS}" | bc -l) )) || (( $(echo "${TRIM_END_SECONDS} > ${TOTAL_SECONDS}" | bc -l) )); then
echo "Error: Times out of bounds. (0 - ${TOTAL_SECONDS} for this video) Exiting..."
exit 1
fi
NUMBER_REGEX='^[0-9][0-9,\.]?+$'; if ! [[ ${TRIM_START_SECONDS} =~ ${NUMBER_REGEX} && ${TRIM_END_SECONDS} =~ ${NUMBER_REGEX} ]] ; then echo "Error: input not a valid number. Exiting..." >&2; exit 1; fi
echo "Trimming from: ${TRIM_START_SECONDS} to: ${TRIM_END_SECONDS} seconds"
# Determine start of fade out
FADE_OUT_START_TIME=$(calc "((${TRIM_END_SECONDS} - ${TRIM_START_SECONDS})) - ${FADE_OUT_SECONDS}")
if [[ ${FADE_OUT_START_TIME} == -* ]]; then FADE_OUT_START_TIME=0; fi # if we have gone negative, number is out of range
# Remove existing OUT_VIDEO so ffmpeg doesn't ask us to overwrite
if [[ -f "${OUT_VIDEO}" ]]; then mv "${OUT_VIDEO}" "${OUT_VIDEO}.old"; fi;
# Execute
ffmpeg \
-hide_banner \
-filter_complex \
"
`### DECLARE SOURCES ###`
`# thumbnail_hold`
movie=
filename=${THUMBNAIL},
loop=
loop=-1: `#infinite loop`
size=1, `# first frame`
scale=${VIDEO_RESOLUTION},
format=
pix_fmts=${PIXEL_FORMAT},
setsar=${SAR},
settb=${TIMEBASE},
trim=
start=0:
duration=${THUMBNAIL_HOLD_SECONDS}
[thumbnail_hold_v];
`# intro_a`
aevalsrc=0, `# "-2+random(0)" for white noise`
atrim=
start=0:
duration=${THUMBNAIL_HOLD_SECONDS}
[intro_a];
`# main_v`
movie=
filename=${IN_VIDEO},
trim=
start=${TRIM_START_SECONDS}:
end=${TRIM_END_SECONDS}
[main_v];
`# main_a`
amovie=
filename=${IN_VIDEO},
atrim=
start=${TRIM_START_SECONDS}:
end=${TRIM_END_SECONDS},
loudnorm=
dual_mono=true:
I=-14: `# target level`
TP=-0.5: `# true peak level`
linear=false: `# 2-pass normalization, not worth the hassle: Too often the measured numbers are N/A or out of range etc.`
print_format=summary
[main_a];
`### COMBINE AND OUTPUT ###`
[thumbnail_hold_v][main_v]
xfade=
duration=${FADE_IN_SECONDS}:
offset=$(calc "${THUMBNAIL_HOLD_SECONDS} - ${FADE_IN_SECONDS}"),
fade=
type=out:
duration=${FADE_OUT_SECONDS}:
start_time=${FADE_OUT_START_TIME};
[intro_a][main_a]
acrossfade=
duration=${FADE_IN_SECONDS},
`# (offset not needed here because acrossfade always starts from the end)`
afade=
type=out:
duration=${FADE_OUT_SECONDS}:
start_time=${FADE_OUT_START_TIME}
" \
-vcodec libx264 \
-preset veryfast \
-crf 18 \
-pix_fmt yuv420p \
-acodec aac \
-b:a 192k \
"${OUT_VIDEO}"
#-f nut - | ffplay -v error -f nut -
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment