Last active
July 5, 2021 07:25
-
-
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
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 -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