Skip to content

Instantly share code, notes, and snippets.

@botsmack
Forked from lisamelton/transcode-video.sh
Created December 26, 2013 20:55
Show Gist options
  • Save botsmack/8138599 to your computer and use it in GitHub Desktop.
Save botsmack/8138599 to your computer and use it in GitHub Desktop.
#!/bin/bash
#
# transcode-video.sh
#
# Copyright (c) 2013 Don Melton
#
# This script is a wrapper for `HandBrakeCLI` (see: <http://handbrake.fr/>).
# The purpose of this script is to transcode a video file into a format and
# size similar to a video available for download from the iTunes Store.
#
# While this script works best with a Blu-ray Disc or DVD rip extracted using
# `MakeMKV` (see: <http://www.makemkv.com/>), it will work with any reasonable
# single video file as input.
#
# Please note that this script is NOT designed to work with a disc image, i.e.
# a Blu-ray or DVD directory, as input.
#
# The goal of this script is NOT to make an exact pixel-for-pixel,
# wave-for-wave copy of the input. That's not practical. Instead, the goal is
# to create a portable version of that input as quickly as possible at a
# reasonable quality --- something that can be enjoyed without prejudice on
# both a small tablet computer and a large, wide-screen home entertainment
# system with 5.1 surround sound.
#
# Also, it should be possible for the output of this script to be served by
# media management systems like Plex (see: <http://www.plexapp.com/>) without
# further dynamic transcoding. In other words, transcode a video file with
# this script and never have to explicitly or implicitly transcode it again.
#
# While this script's defaults are described here, please note that some of
# its various behaviors can be changed or disabled via command line options.
#
# By default this script will try to create an MP4 format file with dual audio
# tracks (like iTunes video), but it can also output Matroska format (i.e.
# MKV) with a single audio track.
#
# For Blu-ray Disc input, this script will transcode the video with an average
# bitrate of 5000 kbps, varying 500-600 kbps either way. Again, this is like
# iTunes video.
#
# Please note that this script does NOT use the default x264 single-pass
# average bitrate (ABR) mode. Instead, it modifies that mode using the x264
# `ratetol=inf` option. This causes ABR mode to behave more like constant rate
# factor (CRF) mode, varying the bitrate to maintain quality.
#
# While CRF mode can create wildly different output bitrates and file sizes
# depending on input, the modified ABR mode used here is much more
# predictable. And for some input, can create better output quality than, for
# example, using CRF mode with a rate factor setting of `20`.
#
# However, this script does allow the user to explicitly choose CRF mode
# instead of the modified ABR mode.
#
# Also note that both video transcoding modes require usage by this script of
# the x264 video buffer verifier (VBV) system. Otherwise the output video
# might not play on some output devices.
#
# For DVD input, this script will transcode the video with an average bitrate
# of 1800 kbps for PAL format discs and 1500 kbps for NTSC format.
#
# For all other video input, this script will choose an output average bitrate
# based on the input width, height and bitrate. That chosen output bitrate
# will never be greater that 80% of the input, nor will it exceed targets for
# similarly-dimensioned Blu-rays and DVDs.
#
# However, this script does allow the user to explicitly choose an output
# average bitrate target.
#
# If faster transcoding time or a smaller output file size is desired for
# Blu-ray Disc and other 1080p video input, this script can scale that input
# to fit within a 1280x720 pixel bounds, targeting an average output bitrate
# of 4000 kbps. Again, much like 720p iTunes video.
#
# By default this script uses the x264 `fast` preset to transcode all video.
# This is done to achieve both the performance AND quality goals. Transcoding
# with the `fast` preset still results in high-quality output.
#
# However, if even higher quality video is desired, access to both the
# `medium` and `slow` x264 presets are available. Please note that using these
# other presets does NOT guarantee higher-quality output. Also, even though
# x264 has still more presets, faster settings are undesirable and slower ones
# are unnecessary.
#
# By default, the first audio track is transcoded. However, this script allows
# the user to choose a different audio track.
#
# For input with multi-channel audio, two audio tracks will be created for MP4
# file format output. The first track will be stereo AAC audio and the second
# track will be multi-channel AC-3 audio. For MKV file format output, only the
# AC-3 surround track will be created.
#
# For stereo or mono audio input, only a single AAC audio track is created for
# both MP4 and MKV file formats.
#
# By default, multi-channel audio is transcoded at 384 kbps, stereo at 160
# kbps and mono at 80 kbps.
#
# However, this script does allow the user to explicitly choose 448 or 640
# kbps for multi-channel audio transcoding. Or the user can even completely
# disable multi-channel audio output.
#
# For input with Blu-ray- or DVD-compatible subtitles (PGS or VobSub formats),
# this script will automatically burn into the video the first subtitle that
# has its forced flag set.
#
# However, this script allows the user to choose a different subtitle track
# with which to burn into the video.
#
# For other subtitle manipulation, it's best to use another program on this
# script's output after transcoding is complete.
#
# This script does NOT facilitate the automatic cropping behavior of
# HandBrakeCLI due to reliability problems with that feature. Instead,
# cropping bounds must be passed explicitly at the command line. Use the
# `detect-crop.sh` script to aid in determining these parameters.
#
# WARNING: This script was designed to work on OS X. There's no guarantee it
# will work on a different operating system.
#
about() {
cat <<EOF
$program 1.3 of December 23, 2013
Copyright (c) 2013 Don Melton
EOF
exit 0
}
usage() {
cat <<EOF
Transcode video file (works best with Blu-ray or DVD rip) into MP4
(or optionally Matroska) format, with configuration and at bitrate
similar to popluar online downloads.
Usage: $program [OPTION]... [FILE]
--mkv output Matroska format with single audio track
--preset NAME use x264 fast|medium|slow preset (default: fast)
--abr BITRATE set average video bitrate target
(default: based on input)
--crf FACTOR use constant rate factor mode instead of ABR
--resize resize video to fit within 1280x720 pixel bounds
--rate FPS force video frame rate (default: based on input)
--audio TRACK select audio track identified by number
(default: 1)
--ac3 BITRATE set AC-3 audio bitrate to 384|448|640 kbps
(default: 384)
--no-ac3 don't output multi-channel AC-3 audio
--crop T:B:L:R set video croping bounds (default: 0:0:0:0)
--burn TRACK burn subtitle track identified by number
(default: first "forced" subtitle, if any)
--no-auto-burn don't automatically burn first "forced" subtitle
--help display this help and exit
--version output version information and exit
Requires \`HandBrakeCLI\` and \`mediainfo\` executables in \$PATH.
Output and log file are written to current working directory.
EOF
exit 0
}
syntax_error() {
echo "$program: $1" >&2
echo "Try \`$program --help\` for more information." >&2
exit 1
}
die() {
echo "$program: $1" >&2
exit ${2:-1}
}
readonly program="$(basename "$0")"
case $1 in
--help)
usage
;;
--version)
about
;;
esac
container_format='mp4'
container_format_options='--large-file --optimize'
preset_options='--x264-preset fast'
reference_frames_option=''
rate_tolerance_option=''
bitrate=''
rate_factor=''
resize=''
frame_rate_options=''
audio_track='1'
ac3_bitrate='384'
crop='0:0:0:0'
size_options='--strict-anamorphic'
subtitle_track=''
auto_burn='yes'
while [ "$1" ]; do
case $1 in
--mkv)
container_format='mkv'
container_format_options=''
;;
--preset)
preset="$2"
shift
case $preset in
fast|slow)
preset_options="--x264-preset $preset"
;;
medium)
preset_options=''
;;
*)
syntax_error "unsupported preset: $preset"
;;
esac
;;
--abr)
bitrate="$(printf '%.0f' "$2")"
rate_factor=''
shift
;;
--crf)
rate_factor="$(printf '%.2f' "$2" | sed 's/0*$//;s/\.$//')"
bitrate=''
shift
;;
--resize)
resize='yes'
;;
--rate)
frame_rate_options="--rate $(printf '%.3f' "$2" | sed 's/0*$//;s/\.$//')"
shift
;;
--audio)
audio_track="$(printf '%.0f' "$2")"
shift
;;
--ac3)
ac3_bitrate="$2"
shift
case $ac3_bitrate in
384|448|640)
;;
*)
syntax_error "unsupported AC-3 audio bitrate: $ac3_bitrate"
;;
esac
;;
--no-ac3|--no-surround)
ac3_bitrate=''
;;
--crop)
crop="$2"
shift
;;
--burn)
subtitle_track="$(printf '%.0f' "$2")"
shift
;;
--no-auto-burn)
auto_burn=''
;;
-*)
syntax_error "unrecognized option: $1"
;;
*)
break
;;
esac
shift
done
readonly input="$1"
if [ ! "$input" ]; then
syntax_error 'too few arguments'
fi
if [ ! -f "$input" ]; then
die "input file not found: $input"
fi
readonly output="$(basename "$input" | sed 's/\.[^.]*$//').$container_format"
if [ -e "$output" ]; then
die "output file already exists: $output"
fi
if ! $(which HandBrakeCLI >/dev/null); then
die 'executable not in $PATH: HandBrakeCLI'
fi
if ! $(which mediainfo >/dev/null); then
die 'executable not in $PATH: mediainfo'
fi
# Determine maximum output video bitrate based on input size:
# 5000 kbps for Blu-ray or other content larger than 1280x720 pixels.
# 4000 kbps for resized or other content larger than 720x576 pixels.
# 1800 kbps for PAL DVD or other content taller than 480 pixels.
# 1500 kbps for NTSC DVD or other content.
# Limit output video bitrate to 80% of input video bitrate. But also allow
# user to set a specific average video bitrate target.
# Set `x264` video buffer verifier (VBV) size and maximum rate to values
# appropriate for H.264 level with High profile:
# 25000 for level 4.0 (e.g. Blu-ray input)
# 17500 for level 3.1 (e.g. 720p input)
# 12500 for level 3.0 (e.g. DVD input)
# When using `slow` preset for output larger than 1280x720 pixels, limit
# reference frames to 4 to maintain compatibility with H.264 level 4.0.
readonly width="$(mediainfo --Output='Video;%Width%' "$input")"
readonly height="$(mediainfo --Output='Video;%Height%' "$input")"
if [ ! "$width" ] || [ ! "$height" ]; then
die "bad video information for file: $input"
fi
if (($width > 1280)) || (($height > 720)); then
if [ ! "$resize" ]; then
vbv_value='25000'
max_bitrate='5000'
if [ "$preset" == 'slow' ]; then
reference_frames_option='ref=4:'
fi
else
vbv_value='17500'
max_bitrate='4000'
size_options='--maxWidth 1280 --maxHeight 720 --loose-anamorphic'
fi
elif (($width > 720)) || (($height > 576)); then
vbv_value='17500'
max_bitrate='4000'
else
vbv_value='12500'
if (($height > 480)); then
max_bitrate='1800'
else
max_bitrate='1500'
fi
fi
# Allow user to explicitly choose constant rate factor (CRF) mode instead of
# this script's modified average bitrate (ABR) mode.
if [ "$rate_factor" ]; then
rate_control_options="--quality $rate_factor"
else
rate_tolerance_option=':ratetol=inf'
if [ "$bitrate" ]; then
if (($bitrate > $vbv_value)); then
bitrate="$vbv_value"
fi
else
readonly min_bitrate="$((max_bitrate / 2))"
bitrate="$(mediainfo --Output='Video;%BitRate%' "$input")"
if [ ! "$bitrate" ]; then
bitrate="$(mediainfo --Output='General;%OverallBitRate%' "$input")"
bitrate="$(((bitrate / 10) * 9))"
fi
if [ "$bitrate" ]; then
bitrate="$(((bitrate / 5) * 4))"
bitrate="$((bitrate / 1000))"
bitrate="$(((bitrate / 100) * 100))"
if (($bitrate > $max_bitrate)); then
bitrate="$max_bitrate"
elif (($bitrate < $min_bitrate)); then
bitrate="$min_bitrate"
fi
else
bitrate="$min_bitrate"
fi
fi
rate_control_options="--vb $bitrate"
fi
# Set peak video frame rate to 30 fps so `HandBrakeCLI` can dynamically
# determine output frame rate, but force output frame rate of `23.976` if
# input frame rate is exactly `29.970`, or if user has indicated a specific
# output frame rate.
frame_rate="$(mediainfo --Output='Video;%FrameRate_Original%' "$input")"
if [ ! "$frame_rate" ]; then
frame_rate="$(mediainfo --Output='Video;%FrameRate%' "$input")"
fi
if [ ! "$frame_rate_options" ]; then
if [ "$frame_rate" == '29.970' ]; then
frame_rate_options='--rate 23.976'
else
frame_rate_options='--rate 30 --pfr'
fi
fi
# Transcode multi-channel audio at 384 kbps in Dolby Digital (AC-3) format.
# Use existing audio if it's already in that format and at or below that
# bitrate.
# Transcode stereo or mono audio using `HandBrakeCLI` default behavior,
# Advanced Audio Coding (AAC) format at 160 or 80 kbps. Use existing audio if
# it's already in that format.
# For MP4 output, transcode multi-channel audio input first in stereo AAC
# format and then add a second audio track in multi-channel AC-3 format.
# Allow user to increase AC-3 audio bitrate to 448 or 640 kbps or completely
# disable multi-channel AC-3 audio output.
readonly audio_channels_array=($(mediainfo --Output='Audio;%Channels%' "$input" | sed 's/\(.\)/\1 /g'))
readonly audio_format_array=($(mediainfo --Output='General;%Audio_Format_List%' "$input" | sed 's|/| |g'))
if ((${#audio_channels_array[*]} != ${#audio_format_array[*]})); then
die "bad audio information for file: $input"
fi
if (($audio_track < 1)) || (($audio_track > ${#audio_format_array[*]})); then
die "invalid audio track for file: $input"
fi
readonly audio_channels="${audio_channels_array[$((audio_track - 1))]}"
readonly audio_format="${audio_format_array[$((audio_track - 1))]}"
if [ "$ac3_bitrate" ] && (($audio_channels > 2)); then
if $(HandBrakeCLI --help 2>/dev/null | grep -q ffac3); then
ac3_encoder='ffac3'
else
ac3_encoder='ac3'
fi
readonly info="$(mediainfo "$input")"
if ((${#audio_format_array[*]} > 1)); then
audio_track_info="$(echo "$info" | sed -n '/^Audio #'${audio_track}'$/,/^$/p')"
else
audio_track_info="$(echo "$info" | sed -n '/^Audio$/,/^$/p')"
fi
readonly input_ac3_bitrate="$(echo "$audio_track_info" | sed -n 's/^Bit rate *: \([0-9]*\) Kbps$/\1/p')"
if [ "$audio_format" == 'AC-3' ] && (($input_ac3_bitrate <= $ac3_bitrate)); then
if [ "$container_format" == 'mp4' ]; then
audio_options='--aencoder ca_aac,copy:ac3'
else
audio_options='--aencoder copy:ac3'
fi
elif [ "$container_format" == 'mp4' ]; then
audio_options="--aencoder ca_aac,$ac3_encoder --ab ,$ac3_bitrate"
else
audio_options="--aencoder $ac3_encoder --ab $ac3_bitrate"
fi
elif [ "$audio_format" == 'AAC' ]; then
audio_options='--aencoder copy:aac'
else
audio_options=''
fi
if (($audio_track > 1)); then
audio_options="--audio $audio_track $audio_options"
fi
# Detelecine video if input frame rate is exactly `29.970` fps. This forces
# consistent output frame rate for NTSC DVD input, smoothing playback.
if [ "$frame_rate" == '29.970' ]; then
filter_options='--detelecine'
else
filter_options=''
fi
# Automatically burn into the video the first subtitle that has its forced
# flag set. But allow user to disable this behavior or select a specific
# unforced track to burn.
readonly subtitle_forced_array=($(mediainfo --Output='Text;%Forced%' "$input" | sed 's/\([a-z]\)\([A-Z]\)/\1 \2/g'))
readonly subtitle_format_array=($(mediainfo --Output='General;%Text_Format_List%' "$input" | sed 's|/| |g'))
if ((${#subtitle_forced_array[*]} != ${#subtitle_format_array[*]})); then
die "bad subtitle information for input file: $input"
fi
if [ ! "$subtitle_track" ] && [ "$auto_burn" ]; then
index='0'
while ((index < ${#subtitle_format_array[*]})); do
if [ "${subtitle_forced_array[$((index))]}" == 'Yes' ]; then
subtitle_track="$((index + 1))"
break
fi
index="$((index + 1))"
done
fi
subtitle_options=''
if [ "$subtitle_track" ]; then
if (($subtitle_track < 1)) || (($subtitle_track > ${#subtitle_format_array[*]})); then
die "invalid subtitle index for input file: $input"
fi
readonly subtitle_format="${subtitle_format_array[$((subtitle_track - 1))]}"
if [ "$subtitle_format" == 'PGS' ] || [ "$subtitle_format" == 'VobSub' ]; then
subtitle_options="--subtitle $subtitle_track --subtitle-burned"
fi
fi
echo "Transcoding: $input" >&2
# Transcode video in single-pass average bitrate (ABR) mode, but set rate
# tolerance to maximum (using `ratetol=inf` option) so behavior is more like
# constant rate factor (CRF) mode. But also allow user to explicitly choose
# CRF mode instead of this modified ABR mode.
time HandBrakeCLI \
--markers \
$container_format_options \
--encoder x264 \
$preset_options \
--encopts ${reference_frames_option}vbv-maxrate=$vbv_value:vbv-bufsize=$vbv_value${rate_tolerance_option} \
$rate_control_options \
$frame_rate_options \
$audio_options \
--crop $crop \
$size_options \
$filter_options \
$subtitle_options \
--input "$input" \
--output "$output" \
2>&1 | tee -a "${output}.log"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment