Last active
September 26, 2021 15:10
-
-
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
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/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