Skip to content

Instantly share code, notes, and snippets.

@trustin
Last active July 5, 2021 07:25
Show Gist options
  • Save trustin/f08195b90158768ffea6dd872653dc83 to your computer and use it in GitHub Desktop.
Save trustin/f08195b90158768ffea6dd872653dc83 to your computer and use it in GitHub Desktop.
ffmpeg-1080p.sh: Scales down and transcodes a video file into HEVC with ffmpeg + CUDA
#!/bin/bash -e
if [[ $# -gt 1 ]]; then
while [[ $# -gt 0 ]]; do
"$0" "$1"
shift
done
exit 0
fi
IN="$(readlink -m "$1")"
if [[ -d "$IN" ]]; then
# Collect all files.
FILES=()
while IFS= read -r -d $'\0' F; do
FILES+=("$F")
done < <(find "$IN" -type f \
'(' -name '*.avi' -or -name '*.wmv' -or -name '*.mp4' -or -name '*.mkv' ')' \
-print0 | sort -z -r)
# Run against each file.
for F in "${FILES[@]}"; do
"$0" "$F"
done
exit 0
fi
# Skip non-video files.
if [[ ! "$IN" =~ (^.*\.(avi|wmv|mp4|mkv)$) ]]; then
exit 0
fi
# Skip the files produced by this script.
if [[ "$IN" =~ (^.*\.opt\.mkv$) ]]; then
exit 0
fi
IN_BASE="$(echo "$IN" | rev | cut -d. -f2- | rev)"
OUT_BASE="$(echo "$IN" | rev | cut -d. -f2- | rev)"
if [[ "$IN" =~ (/public/Videos/) ]]; then
OUT_BASE="$(echo "$OUT_BASE" | sed -e 's#/public/Videos/#/public/Videos/Transcoded/#')"
mkdir -p "$(dirname "$OUT_BASE")"
fi
OUT="$OUT_BASE.opt.mkv"
OUT_TMP="$OUT.tmp"
# Skip if .no_opt marker file exists.
if [[ -a "$IN_BASE.no_opt" ]]; then
echo -ne '\033[1;32m'
echo -n 'Skipping:'
echo -ne '\033[0m'
echo " $IN (no_opt)"
exit 0
fi
# Skip the files that were encoded already.
if [[ -a "$OUT" ]]; then
if [[ "$(ffprobe -v error -select_streams v -show_entries stream=codec_name -of default=noprint_wrappers=1 "$OUT" 2>&1 | grep -E '^codec_name=' | grep -vF 'codec_name=hevc' | wc -l)" -ne 0 ]] || \
[[ "$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of default=noprint_wrappers=1 "$OUT" 2>&1 | grep -E '^codec_name=' | grep -vF 'codec_name=aac' | wc -l)" -ne 0 ]]; then
echo -ne '\033[1;32m'
echo -n 'Processing:'
echo -ne '\033[0m'
echo " $IN (codec)"
else
echo -ne '\033[1;32m'
echo -n 'Skipping:'
echo -ne '\033[0m'
echo " $IN (done)"
exit 0
fi
else
echo -ne '\033[1;32m'
echo -n 'Processing:'
echo -ne '\033[0m'
echo " $IN"
fi
WIDTH=$(ffprobe -v error -select_streams v -show_entries stream=width -of default=noprint_wrappers=1 "$IN" | grep -iE '^width=[0-9]+$' | cut -d= -f2 | head -1)
HEIGHT=$(ffprobe -v error -select_streams v -show_entries stream=height -of default=noprint_wrappers=1 "$IN" | grep -iE '^height=[0-9]+$' | cut -d= -f2 | head -1)
if [[ ! "$WIDTH" =~ (^[1-9][0-9]+$) ]]; then
echo "Failed to determine the video width: $WIDTH" >&2
exit 1
fi
if [[ ! "$HEIGHT" =~ (^[1-9][0-9]+$) ]]; then
echo "Failed to determine the video height: $HEIGHT" >&2
exit 1
fi
if [[ -a "$IN_BASE.no_crop" ]]; then
CROP_LEFT=0
CROP_TOP=0
CROP_RIGHT="$WIDTH"
CROP_BOTTOM="$HEIGHT"
CROP_WIDTH="$WIDTH"
CROP_HEIGHT="$HEIGHT"
CROP_OPT=''
else
# Find the best crop range by sampling a few places.
MIN_CROP_LEFT=65536
MIN_CROP_TOP=65536
MAX_CROP_BOTTOM=0
MAX_CROP_RIGHT=0
for((SS=90;SS<=7290;SS+=600)); do
CROP=$(ffmpeg -ss "$SS" -i "$IN" -t 3 -vf cropdetect -f null - 2>&1 | grep -oE 'crop=[0-9]+:[0-9]+:[0-9]+:[0-9]+' | tail -1)
if [[ "$CROP" =~ (^crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)$) ]]; then
CROP_WIDTH="${BASH_REMATCH[2]}"
CROP_HEIGHT="${BASH_REMATCH[3]}"
CROP_LEFT="${BASH_REMATCH[4]}"
CROP_TOP="${BASH_REMATCH[5]}"
CROP_RIGHT=$((CROP_LEFT + CROP_WIDTH))
CROP_BOTTOM=$((CROP_TOP + CROP_HEIGHT))
CROP_UPDATED=0
if [[ "$CROP_LEFT" -lt "$MIN_CROP_LEFT" ]]; then
MIN_CROP_LEFT="$CROP_LEFT"
CROP_UPDATED=1
fi
if [[ "$CROP_TOP" -lt "$MIN_CROP_TOP" ]]; then
MIN_CROP_TOP="$CROP_TOP"
CROP_UPDATED=1
fi
if [[ "$CROP_RIGHT" -gt "$MAX_CROP_RIGHT" ]]; then
MAX_CROP_RIGHT="$CROP_RIGHT"
CROP_UPDATED=1
fi
if [[ "$CROP_BOTTOM" -gt "$MAX_CROP_BOTTOM" ]]; then
MAX_CROP_BOTTOM="$CROP_BOTTOM"
CROP_UPDATED=1
fi
if [[ "$CROP_UPDATED" -ne 0 ]]; then
echo "Crop: ($MIN_CROP_LEFT, $MIN_CROP_TOP) - ($MAX_CROP_RIGHT, $MAX_CROP_BOTTOM) = $((MAX_CROP_RIGHT - MIN_CROP_LEFT))x$((MAX_CROP_BOTTOM - MIN_CROP_TOP)) at ${SS}s"
fi
if [[ "$CROP_WIDTH" -eq "$WIDTH" ]] && [[ "$CROP_HEIGHT" -eq "$HEIGHT" ]]; then
break
fi
fi
done
if [[ "$MIN_CROP_LEFT" -eq 65536 ]] || [[ "$MIN_CROP_TOP" -eq 65536 ]] || [[ "$MAX_CROP_RIGHT" -eq 0 ]] || [[ "$MAX_CROP_BOTTOM" -eq 0 ]]; then
echo "Failed to detect crop ranges at all." >&2
exit 1
fi
CROP_WIDTH=$((MAX_CROP_RIGHT - MIN_CROP_LEFT))
CROP_HEIGHT=$((MAX_CROP_BOTTOM - MIN_CROP_TOP))
if [[ "$CROP_WIDTH" -ne "$WIDTH" ]] || [[ "$CROP_HEIGHT" -ne "$HEIGHT" ]]; then
CROP_OPT="$CROP_WIDTH:$CROP_HEIGHT:$MIN_CROP_LEFT:$MIN_CROP_TOP"
else
CROP_OPT=""
fi
fi
# Calculate the target width for resizing.
if [[ $((CROP_WIDTH * 100 / CROP_HEIGHT)) -gt 150 ]]; then
# 16:9
if [[ "$CROP_WIDTH" -gt 1920 ]]; then
TARGET_WIDTH=1920
else
TARGET_WIDTH="$CROP_WIDTH"
fi
else
# 4:3
if [[ "$CROP_WIDTH" -gt 1440 ]]; then
TARGET_WIDTH=1440
else
TARGET_WIDTH="$CROP_WIDTH"
fi
fi
CMD=(nice --adjustment=19 ffmpeg)
CMD+=(-hide_banner -loglevel error -stats)
CMD+=(-nostdin -y -vsync 0)
if [[ -z "$CROP_OPT" ]]; then
CMD+=(-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 3)
else
CMD+=(-init_hw_device cuda)
fi
CMD+=(-i "$IN" -map '0:V:0' -map '0:a:0?' -map '0:s?')
CMD+=(-c:v hevc_nvenc -preset:v p7 -tune:v hq)
# Note: Turn off passthrough for scale_cuda to work around the 'No decoder surfaces left' error.
# https://trac.ffmpeg.org/ticket/7562
if [[ -z "$CROP_OPT" ]]; then
CMD+=(-filter:v "hwupload,scale_cuda=$TARGET_WIDTH:-1:interp_algo=4:passthrough=false")
else
CMD+=(-filter:v "crop=$CROP_OPT,hwupload,scale_cuda=$TARGET_WIDTH:-1:interp_algo=4:passthrough=false")
fi
CMD+=(-rc:v vbr -cq:v 22 -rc-lookahead:v 32 -bf:v 3 -b_ref_mode:v middle)
CMD+=(-c:a libfdk_aac -vbr:a 4)
CMD+=(-c:s copy)
CMD+=(-max_muxing_queue_size 9999 -movflags +faststart)
CMD+=(-f matroska "$OUT_TMP")
echo "${CMD[@]}"
"${CMD[@]}"
# Copy subtitles.
if [[ -f "$IN_BASE.kor.srt" ]]; then
cp -f "$IN_BASE.kor.srt" "$OUT_BASE.opt.kor.srt"
elif [[ -f "$IN_BASE.srt" ]]; then
mv -f "$IN_BASE.srt" "$IN_BASE.kor.srt"
cp -f "$IN_BASE.kor.srt" "$OUT_BASE.opt.kor.srt"
fi
if [[ -f "$IN_BASE.kor.smi" ]]; then
cp -f "$IN_BASE.kor.smi" "$OUT_BASE.opt.kor.smi"
elif [[ -f "$IN_BASE.smi" ]]; then
mv -f "$IN_BASE.smi" "$IN_BASE.kor.smi"
cp -f "$IN_BASE.kor.smi" "$OUT_BASE.opt.kor.smi"
fi
# Commit the transcoded file.
mv -f "$OUT_TMP" "$OUT"
echo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment