Skip to content

Instantly share code, notes, and snippets.

@lisamelton
Last active September 8, 2023 23:51
Show Gist options
  • Save lisamelton/7679274 to your computer and use it in GitHub Desktop.
Save lisamelton/7679274 to your computer and use it in GitHub Desktop.
Transcode video file (works best with Blu-ray or DVD rip) into MP4 (or optionally Matroska) format, with configuration and at bitrate similar to popular online downloads.
#!/bin/bash
#
# transcode-video.sh
#
# Copyright (c) 2013-2015 Don Melton
#
about() {
cat <<EOF
$program 5.13 of April 8, 2015
Copyright (c) 2013-2015 Don Melton
EOF
exit 0
}
usage() {
cat <<EOF
Transcode video file or disc image directory into format and size similar to
popular online downloads. Works best with Blu-ray or DVD rip.
Automatically determines target video bitrate, number of audio tracks, etc.
WITHOUT ANY command line options.
Usage: $program [OPTION]... [FILE|DIRECTORY]
Input options:
--title NUMBER select numbered title in video media (default: 1)
(\`0\` to scan media, list title numbers and exit)
--chapters NUMBER[-NUMBER]
select chapters, single or range (default: all)
--start-at,--stop-at UNIT:VALUE
start or stop at \`frame\`, \`duration\` or \`pts\`
(\`duration\` in seconds, \`pts\` on 90 kHz clock)
Output options:
-o, --output FILENAME|DIRECTORY
set output path and filename, or just path
(default: input filename with output format extension
in current working directory)
--mkv output Matroska format instead of MP4
--m4v output MP4 with \`.m4v\` extension instead of \`.mp4\`
Quality options:
--big raise default limits for both video and AC-3 audio bitrates
(always increases output size)
--fast, --faster, --veryfast
use x264 encoder preset to trade precision for speed
--slow, --slower, --veryslow
use x264 encoder preset to trade speed for compression
Video options:
--crop T:B:L:R set video crop values (default: 0:0:0:0)
(use \`--crop detect\` to invoke \`detect-crop.sh\`)
(use \`--crop auto\` for \`HandBrakeCLI\` behavior)
--480p constrain video to fit within 854x480 pixel bounds
(not perfect 16:9 bounds and not for DVD input)
--720p constrain video to fit within 1280x720 pixel bounds
--1080p " " " " " 1920x1080 " "
--2160p " " " " " 3840x2160 " "
--width WIDTH change pixel aspect ratio with custom width
--height HEIGHT " " " " " " height
--rate FPS[,limited]
set video frame rate with optional peak-limited flag
(default: based on input)
Audio options:
--audio TRACK[,NAME]
select main audio track (default: 1)
with optional name
--single don't create secondary main audio track
--add-audio [double,]TRACK[,NAME]
add audio track in AAC format
with optional "double" flag to include the track again
in multi-channel format if available
with optional name
(can be used multiple times)
--add-all-audio single|double
add all other audio tracks in single AAC format
or double AAC+multi-channel format, if avaiable
(doesn't apply to main audio track)
(not compatible with \`--add-audio\` option)
--allow-ac3 allow multi-channel AC-3 format in additional audio tracks
--allow-dts allow multi-channel DTS formats in all audio tracks
(also allows AC-3 format in additional audio tracks)
--no-surround don't output multi-channel formats in any audio track
--ac3 BITRATE set AC-3 audio bitrate to 384|448|640 kbps (default: 384)
--pass-ac3 BITRATE
set passthru AC-3 audio <= 384|448|640 kbps (default: 448)
(only applies to multi-channel)
--copy-ac3 always passthru AC-3 audio in main track
(including mono and stero, regardless of bitrate)
--copy-all-ac3 always passthru AC-3 audio in all tracks
(including mono and stero, regardless of bitrate)
--copy-audio-names
passthru any original audio track names
(unless a name is specified with another option)
Subtitle options:
--burn TRACK burn subtitle track (default: first forced track, if any)
--no-auto-burn don't automatically burn first forced subtitle
--add-subtitle [forced,]TRACK
add subtitle track with optional forced playback flag
(can be used multiple times)
--add-all-subtitles
add all other subtitles
(doesn't apply to burned subtitle track)
(not compatible with \`--add-subtitle\` option)
--find-forced burn|add
scan for forced subtitle in same language as main audio
and, if found, burn into video or add as forced track
(disables some other subtitle behaviors and options)
--burn-srt [ENCODING,][OFFSET,]FILENAME
burn subtitle track from SubRip-format \`.srt\` text file
with optional character set encoding (default: latin1)
with optional +/- offset in milliseconds (default: 0)
(values before filename can appear in any order)
--add-srt [ENCODING,][OFFSET,][LANGUAGE,][forced,]FILENAME
add subtitle track from SubRip-format \`.srt\` text file
with optional character set encoding (default: latin1)
with optional +/- offset in milliseconds (default: 0)
with optional ISO 639-2 language code (default: und)
with optional forced playback flag
(values before filename can appear in any order)
(can be used multiple times)
Advanced options:
--preset NAME use x264 ...|fast|medium|slow|... preset (default: medium)
(refer to \`HandBrakeCLI --help\` for complete list)
--tune NAME use x264 film|animation|grain|... tune
(refer to \`HandBrakeCLI --help\` for complete list)
--max BITRATE set maximum video bitrate (default: based on input)
(can be exceeded to maintain video quality)
--buffer SIZE set video buffer size (default: 50% of maximum bitrate)
--add-encopts OPTION=VALUE[:OPTION=VALUE...]
specify additional x264 encoder options
--crf FACTOR set constant rate factor (default: 16)
--crf-max FACTOR
set maximum constant rate factor (default: 25)
(use \`--crf-max none\` to disable)
--filter NAME[=SETTINGS]
apply \`HandBrakeCLI\` video filter with optional settings
(default: \`deinterlace\` for some 29.97 fps input)
(refer to \`HandBrakeCLI --help\` for more information)
(can be used multiple times)
Passthru options:
--angle, --normalize-mix, --drc, --gain,
--no-opencl, --optimize, --use-opencl, --use-hwd
all passed through to \`HandBrakeCLI\` unchanged
(refer to \`HandBrakeCLI --help\` for more information)
Other options:
--chapter-names FILENAME
import chapter names from \`.csv\` text file
(in NUMBER,NAME format, e.g. "1,Intro")
--no-log don't write log file
--debug output diagnostic information to \`stderr\` and exit
(with \`HandBrakeCLI\` command line sent to \`stdout\`)
-h, --help display this help and exit
--version output version information and exit
Requires \`HandBrakeCLI\` executable in \$PATH.
Requires \`detect-crop.sh\` script in \$PATH when using \`--crop detect\`.
May require \`mkvmerge\` executable in \$PATH when using \`--find-forced add\`.
May require \`mp4track\` and \`mkvpropedit\` executables in \$PATH for some options.
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}
}
deprecated() {
echo "$program: deprecated option: $1" >&2
}
deprecated_and_replaced() {
deprecated $1
echo "$program: use this option instead: $2" >&2
}
escape_string() {
echo "$1" | sed "s/'/'\\\''/g;s/^\(.*\)$/'\1'/"
}
readonly program="$(basename "$0")"
# OPTIONS
#
case $1 in
--help|-h|--fullhelp)
usage
;;
--version)
about
;;
esac
media_title='1'
section_options=''
output=''
output_prefix=''
container_format='mp4'
default_max_bitrate_2160p='10000'
default_max_bitrate_1080p='5000'
default_max_bitrate_720p='4000'
default_max_bitrate_480p='2000'
preset='medium'
crop_values='0:0:0:0'
constrain_width='4096'
constrain_height='2304'
custom_width=''
custom_height=''
frame_rate_options=''
main_audio_track='1'
main_audio_track_name=''
single_main_audio=''
extra_audio_tracks=()
add_all_audio=''
allow_ac3=''
allow_dts=''
allow_surround='yes'
ac3_bitrate='384'
pass_ac3_bitrate='448'
copy_ac3=''
copy_all_ac3=''
copy_audio_names=''
burned_subtitle_track=''
auto_burn='yes'
extra_subtitle_tracks=()
add_all_subtitles=''
find_forced=''
burned_srt_file=''
extra_srt_files=()
tune_options=''
max_bitrate=''
vbv_bufsize=''
extra_encopts_options=''
rate_factor='16'
max_rate_factor='25'
filter_options=''
auto_deinterlace='yes'
passthru_options=''
chapter_names_file=''
write_log='yes'
debug=''
while [ "$1" ]; do
case $1 in
--title)
media_title="$(printf '%.0f' "$2" 2>/dev/null)"
shift
if (($media_title < 0)); then
die "invalid media title number: $media_title"
fi
;;
--chapters|--start-at|--stop-at)
section_options="$section_options $1 $2"
shift
;;
--output|-o)
output="$2"
shift
if [ -d "$output" ]; then
output_prefix="$(echo "$output" | sed 's|/$||')/"
output=''
else
case $output in
*.mp4|*.mkv|*.m4v)
container_format="${output: -3}"
;;
*)
die "unsupported filename extension: $output"
;;
esac
fi
;;
--mkv|--m4v)
container_format="${1:2}"
if [ "$output" ]; then
output="${output%.*}.$container_format"
fi
;;
--preset|--veryfast|--faster|--fast|--slow|--slower|--veryslow)
if [ "$1" == '--preset' ]; then
preset="$2"
shift
else
preset="${1:2}"
fi
;;
--big|--better)
[ "$1" == '--better' ] && deprecated_and_replaced "$1" '--big'
default_max_bitrate_2160p='16000'
default_max_bitrate_1080p='8000'
default_max_bitrate_720p='6000'
default_max_bitrate_480p='3000'
ac3_bitrate='640'
;;
--crop)
crop_values="$2"
shift
;;
--480p)
constrain_width='854'
constrain_height='480'
;;
--720p|--resize)
[ "$1" == '--resize' ] && deprecated_and_replaced "$1" '--720p'
constrain_width='1280'
constrain_height='720'
;;
--1080p)
constrain_width='1920'
constrain_height='1080'
;;
--2160p)
constrain_width='3840'
constrain_height='2160'
;;
--width)
custom_width="$(printf '%.0f' "$2" 2>/dev/null)"
shift
if (($custom_width < 1)); then
die "invalid custom width: $custom_width"
fi
;;
--height)
custom_height="$(printf '%.0f' "$2" 2>/dev/null)"
shift
if (($custom_height < 1)); then
die "invalid custom height: $custom_height"
fi
;;
--rate)
frame_rate_argument="$2"
shift
frame_rate_options="--rate $(printf '%.3f' "$(echo "$frame_rate_argument" | sed 's/,.*$//')" 2>/dev/null | sed 's/0*$//;s/\.$//')"
if [[ "$frame_rate_argument" =~ ',limited'$ ]]; then
frame_rate_options="$frame_rate_options --pfr"
elif [[ "$frame_rate_argument" =~ ',' ]]; then
die "invalid frame rate argument: $frame_rate_argument"
fi
;;
--audio)
audio_track_argument="$2"
shift
main_audio_track="$(printf '%.0f' "$(echo "$audio_track_argument" | sed 's/,.*$//')" 2>/dev/null)"
if (($main_audio_track < 1)); then
die "invalid main audio track: $main_audio_track"
fi
if [[ "$audio_track_argument" =~ ',' ]]; then
main_audio_track_name="$(echo "$audio_track_argument" | sed 's/^[^,]*,//')"
else
main_audio_track_name=''
fi
;;
--single)
single_main_audio='yes'
;;
--add-audio)
extra_audio_tracks=("${extra_audio_tracks[@]}" "$2")
shift
add_all_audio=''
;;
--add-all-audio)
add_all_audio="$2"
shift
case $add_all_audio in
single|double)
;;
*)
syntax_error "unsupported audio width: $add_all_audio"
;;
esac
extra_audio_tracks=()
;;
--allow-ac3)
allow_ac3='yes'
allow_surround='yes'
;;
--allow-dts)
allow_ac3='yes'
allow_dts='yes'
allow_surround='yes'
;;
--no-surround|--no-ac3)
[ "$1" == '--no-ac3' ] && deprecated_and_replaced "$1" '--no-surround'
allow_ac3=''
allow_dts=''
allow_surround=''
copy_ac3=''
copy_all_ac3=''
;;
--ac3)
ac3_bitrate="$2"
shift
case $ac3_bitrate in
384|448|640)
;;
*)
syntax_error "unsupported AC-3 audio bitrate: $ac3_bitrate"
;;
esac
;;
--pass-ac3)
pass_ac3_bitrate="$2"
shift
case $pass_ac3_bitrate in
384|448|640)
;;
*)
syntax_error "unsupported AC-3 audio passthru bitrate: $pass_ac3_bitrate"
;;
esac
;;
--copy-ac3)
copy_ac3='yes'
;;
--copy-all-ac3)
copy_ac3='yes'
copy_all_ac3='yes'
;;
--copy-audio-names)
copy_audio_names='yes'
;;
--burn)
burned_subtitle_track="$(printf '%.0f' "$2" 2>/dev/null)"
shift
if (($burned_subtitle_track < 1)); then
die "invalid burn subtitle track: $burned_subtitle_track"
fi
burned_srt_file=''
find_forced=''
;;
--no-auto-burn)
auto_burn=''
;;
--add-subtitle)
extra_subtitle_tracks=("${extra_subtitle_tracks[@]}" "$2")
shift
add_all_subtitles=''
;;
--add-all-subtitles)
add_all_subtitles='yes'
extra_subtitle_tracks=()
;;
--find-forced)
find_forced="$2"
shift
case $find_forced in
burn|add)
;;
*)
syntax_error "invalid find forced argument: $find_forced"
;;
esac
burned_subtitle_track=''
auto_burn=''
burned_srt_file=''
;;
--burn-srt)
burned_srt_file="$2"
shift
burned_subtitle_track=''
auto_burn=''
find_forced=''
;;
--add-srt|--srt)
[ "$1" == '--srt' ] && deprecated_and_replaced "$1" '--add-srt'
extra_srt_files=("${extra_srt_files[@]}" "$2")
shift
;;
--tune)
tune_options="$tune_options --encoder-tune $2"
shift
;;
--max|--vbv-maxrate|--abr)
[ "$1" == '--abr' ] && deprecated_and_replaced "$1" '--max'
max_bitrate="$(printf '%.0f' "$2" 2>/dev/null)"
shift
if (($max_bitrate < 1)); then
die "invalid maximum video bitrate: $max_bitrate"
fi
;;
--buffer|--vbv-bufsize)
vbv_bufsize="$(printf '%.0f' "$2" 2>/dev/null)"
shift
if (($vbv_bufsize < 1)); then
die "invalid video buffer size: $vbv_bufsize"
fi
;;
--add-encopts)
extra_encopts_options="$2"
shift
;;
--crf)
rate_factor="$(printf '%.2f' "$2" 2>/dev/null | sed 's/0*$//;s/\.$//')"
shift
if (($rate_factor < 0)); then
die "invalid constant rate factor: $rate_factor"
fi
;;
--crf-max)
max_rate_factor="$2"
shift
case $max_rate_factor in
none)
max_rate_factor=''
;;
*)
max_rate_factor="$(printf '%.2f' "$max_rate_factor" 2>/dev/null | sed 's/0*$//;s/\.$//')"
if (($max_rate_factor < 0)); then
die "invalid maximum constant rate factor: $max_rate_factor"
fi
;;
esac
;;
--filter)
filter="$2"
shift
filter_name="$(echo "$filter" | sed 's/=.*$//')"
case $filter_name in
deinterlace|decomb|detelecine)
auto_deinterlace=''
;;
denoise|nlmeans|nlmeans-tune|deblock|rotate|grayscale)
;;
*)
syntax_error "unsupported video filter: $filter_name"
;;
esac
filter_options="$filter_options --$filter"
;;
--angle|--start-at|--stop-at|--normalize-mix|--drc|--gain)
passthru_options="$passthru_options $1 $2"
shift
;;
--no-opencl|--optimize|--use-opencl|--use-hwd)
passthru_options="$passthru_options $1"
;;
--chapter-names)
chapter_names_file="$2"
shift
;;
--no-log)
write_log=''
;;
--debug)
debug='yes'
;;
--hq)
deprecated "$1"
;;
--with-original-audio)
deprecated_and_replaced "$1" '--allow-dts'
allow_dts='yes'
audio_track="$(printf '%.0f' "$1" 2>/dev/null)"
if (($audio_track > 0)); then
main_audio_track="$audio_track"
shift
fi
;;
--detelecine)
deprecated_and_replaced "$1" '--filter detelecine'
filter_options="$filter_options --detelecine"
;;
--no-auto-detelecine)
deprecated "$1"
;;
--srt-burn)
deprecated "$1"
srt_number="$(printf '%.0f' "$2" 2>/dev/null)"
if (($srt_number > 0)); then
shift
fi
;;
-*)
syntax_error "unrecognized option: $1"
;;
*)
break
;;
esac
shift
done
# INPUT
#
readonly input="$1"
if [ ! "$input" ]; then
syntax_error 'too few arguments'
fi
if [ ! -e "$input" ]; then
die "input not found: $input"
fi
if ! $(which HandBrakeCLI >/dev/null); then
die 'executable not in $PATH: HandBrakeCLI'
fi
readonly version="$(HandBrakeCLI --preset-list 2>&1)"
if [ ! "$version" ]; then
die "can't determine HandBrakeCLI version"
fi
readonly release_version="$(echo "$version" |
sed -n 's/^HandBrake \([0-9]\{1,\}\)\.\([0-9]\{1,\}\)\.\([0-9]\{1,\}\) .*$/\1 \2 \3/p' |
sed 's/ 0\([0-9]\)/ \1/g')"
if [ "$release_version" ]; then
readonly version_array=($release_version)
if ((((${version_array[0]} * 10) + ${version_array[1]}) < 10)); then
die 'HandBrake version 0.10.0 or later is required'
fi
else
readonly svn_version="$(echo "$version" |
sed -n 's/^HandBrake svn\([0-9]\{1,\}\) .*$/\1/p')"
if [ "$svn_version" ]; then
if (($svn_version < 6536)); then
die 'HandBrake version 0.10.0 or later is required'
fi
else
die "can't determine HandBrakeCLI version"
fi
fi
if [ "$media_title" == '0' ]; then
echo "Scanning: $input" >&2
fi
# Leverage `HandBrakeCLI` scan mode to extract all file- or directory-based
# media information. Significantly speed up scan with `--previews 2:0` option
# and argument.
#
readonly media_info="$(HandBrakeCLI --title $media_title --scan --previews 2:0 --input "$input" 2>&1)"
if [ "$media_title" == '0' ]; then
echo "$media_info"
exit
fi
if [ "$debug" ]; then
echo "$media_info" >&2
fi
if ! $(echo "$media_info" | grep -q '^+ title '$media_title':$'); then
echo "$program: media title number $media_title not found in: $input" >&2
echo "Try \`$program --title 0 [FILE|DIRECTORY]\` to scan for media titles." >&2
echo "Try \`$program --help\` for more information." >&2
exit 1
fi
if [ ! "$output" ]; then
output="$output_prefix$(basename "$input" | sed 's/\.[0-9A-Za-z]\{1,\}$//').$container_format"
fi
if [ -e "$output" ]; then
die "output file already exists: $output"
fi
if [ "$crop_values" == 'detect' ]; then
if ! $(which detect-crop.sh >/dev/null); then
die 'script not in $PATH: detect-crop.sh'
fi
crop_values="$(detect-crop.sh --title $media_title --values-only "$input")"
if [ ! "$crop_values" ]; then
die "crop ambiguous or unavailable for: $input"
fi
fi
# VIDEO
#
readonly size_array=($(echo "$media_info" | sed -n 's/^ + size: \([0-9]\{1,\}\)x\([0-9]\{1,\}\).*$/\1 \2/p'))
if ((${#size_array[*]} != 2)); then
die "video size not found: $input"
fi
width="${size_array[0]}"
height="${size_array[1]}"
if [ "$custom_width" ]; then
if [ "$custom_width" == "$width" ]; then
custom_width=''
else
width="$custom_width"
fi
fi
if [ "$custom_height" ]; then
if [ "$custom_height" == "$height" ]; then
custom_height=''
else
height="$custom_height"
fi
fi
if [ "$crop_values" != '0:0:0:0' ] && [ "$crop_values" != 'auto' ]; then
readonly crop_array=($(echo "$crop_values" |
sed -n 's/^\([0-9]\{1,\}\):\([0-9]\{1,\}\):\([0-9]\{1,\}\):\([0-9]\{1,\}\)$/ \1 \2 \3 \4 /p' |
sed 's/ 0\([0-9]\)/ \1/g'))
width="$((width - ${crop_array[2]} - ${crop_array[3]}))"
height="$((height - ${crop_array[0]} - ${crop_array[1]}))"
if (($width < 1)) || (($height < 1)); then
die "invalid crop: $crop_values"
fi
fi
if ((($width > $constrain_width)) || (($height > $constrain_height))); then
size_options="--maxWidth $constrain_width --maxHeight $constrain_height"
if [ "$custom_width" ] || [ "$custom_height" ]; then
size_options="$size_options --custom-anamorphic"
else
size_options="$size_options --loose-anamorphic"
fi
adjusted_height="$(ruby -e 'printf "%.0f", '$height' * ('$constrain_width'.0 / '$width')')"
adjusted_height=$((adjusted_height - (adjusted_height % 2)))
if (($adjusted_height > $constrain_height)); then
width="$(ruby -e 'printf "%.0f", '$width' * ('$constrain_height'.0 / '$height')')"
width=$((width + (width % 2)))
height="$constrain_height"
else
width="$constrain_width"
height="$adjusted_height"
fi
elif [ "$custom_width" ] || [ "$custom_height" ]; then
size_options='--custom-anamorphic'
else
size_options='--strict-anamorphic'
fi
if [ "$custom_height" ]; then
size_options="--height $height $size_options"
fi
if [ "$custom_width" ]; then
size_options="--width $width $size_options"
fi
# Limit `x264` video buffer verifier (VBV) size to values appropriate for
# H.264 level with High profile:
#
# 300000 for level 5.1 (e.g. 2160p input)
# 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)
#
level_options=''
if (($width > 1920)) || (($height > 1080)); then
vbv_maxrate="$default_max_bitrate_2160p"
max_bufsize='300000'
elif (($width > 1280)) || (($height > 720)); then
vbv_maxrate="$default_max_bitrate_1080p"
max_bufsize='25000'
case $preset in
slow|slower|veryslow|placebo)
level_options="--encoder-level 4.0"
;;
esac
# Test for total number of pixels instead of bounds so using `--480p` option,
# with width up to 854 pixels, works like DVD input.
#
elif ((($width * $height) > (720 * 576))); then
vbv_maxrate="$default_max_bitrate_720p"
max_bufsize='17500'
else
vbv_maxrate="$default_max_bitrate_480p"
max_bufsize='12500'
fi
if [ "$max_bitrate" ]; then
vbv_maxrate="$max_bitrate"
if (($vbv_maxrate > $max_bufsize)); then
vbv_maxrate="$max_bufsize"
fi
elif [ -f "$input" ]; then
readonly duration_array=($(echo "$media_info" |
sed -n 's/^ + duration: \([0-9][0-9]\):\([0-9][0-9]\):\([0-9][0-9]\)$/ \1 \2 \3 /p' |
sed 's/ 0/ /g'))
if ((${#duration_array[*]} == 3)); then
if $(stat --version 2>/dev/null | grep -q 'GNU'); then
format_option='-c %s'
else
format_option='-f %z'
fi
# Calculate total bitrate from file size in bits divided by video
# duration in seconds.
#
bitrate="$((($(stat -L $format_option "$input") * 8) / ((duration_array[0] * 60 * 60) + (duration_array[1] * 60) + duration_array[2])))"
if [ "$bitrate" ]; then
# Convert to kbps and round to nearest thousand.
#
bitrate="$((((bitrate / 1000) / 1000) * 1000))"
if (($bitrate < $vbv_maxrate)); then
readonly min_bitrate="$((vbv_maxrate / 2))"
if (($bitrate < $min_bitrate)); then
vbv_maxrate="$min_bitrate"
else
vbv_maxrate="$bitrate"
fi
fi
fi
fi
fi
if [ "$vbv_bufsize" ]; then
if (($vbv_bufsize > $max_bufsize)); then
vbv_bufsize="$max_bufsize"
fi
else
# The `x264` video buffer verifier (VBV) size must always be less than the
# maximum rate to maintain quality in constant rate factor (CRF) mode.
#
vbv_bufsize="$((vbv_maxrate / 2))"
fi
if [ ! "$frame_rate_options" ]; then
frame_rate_options='--rate 30 --pfr'
readonly frame_rate="$(echo "$media_info" | sed -n 's/^.* scan: 2 previews, .* \([0-9]\{1,\}\.[.0-9]\{1,\}\) fps, .*$/\1/p')"
if [ ! "$frame_rate" ]; then
die "no video frame rate information in: $input"
fi
readonly video_stream_info="$(echo "$media_info" | sed -n '/^ Stream #[^:]\{1,\}: Video: /p' | sed -n 1p)"
force_frame_rate=''
if [ "$video_stream_info" ]; then
$(echo "$video_stream_info" | grep -q 'mpeg2video') && force_frame_rate='yes'
else
$(echo "$media_info" | grep -q '^ + vts ') && force_frame_rate='yes'
fi
if [ "$frame_rate" == '29.970' ]; then
if [ "$force_frame_rate" ]; then
frame_rate_options='--rate 23.976'
elif [ "$auto_deinterlace" ]; then
filter_options="$filter_options --deinterlace"
fi
elif [ "$force_frame_rate" ]; then
case $frame_rate in
'23.976'|'24.000'|'25.000')
frame_rate_options="--rate $(echo "$frame_rate" | sed 's/\.000$//')"
;;
esac
fi
fi
# AUDIO
#
if (($pass_ac3_bitrate < $ac3_bitrate)); then
pass_ac3_bitrate="$ac3_bitrate"
fi
if [ "$ac3_bitrate" == '640' ]; then
ac3_bitrate=''
fi
track_index='1'
audio_track_list=''
audio_encoder_list=''
audio_bitrate_list=''
audio_track_name_list=''
audio_track_name_edits=()
if [ "$copy_audio_names" ]; then
readonly all_audio_streams_info="$(echo "$media_info" |
sed -n '/^ Stream #[^:]\{1,\}: Audio: /,$p' |
sed '/^[^ ]\{1,\}.*$/,$d' |
sed '/^ Stream #[^:]\{1,\}: Subtitle: /,$d' |
sed '/^ Metadata:$/d' |
sed '$!N;s/\n/ /')"
fi
readonly all_audio_tracks_info="$(echo "$media_info" |
sed -n '/^ + audio tracks:$/,/^ + subtitle tracks:$/p' |
sed -n '/^ + /p')"
audio_track_info="$(echo "$all_audio_tracks_info" | sed -n ${main_audio_track}p)"
if [ "$audio_track_info" ]; then
audio_track_list="$main_audio_track"
if $(HandBrakeCLI --help 2>/dev/null | grep -q 'ca_aac'); then
aac_encoder='ca_aac'
else
aac_encoder='av_aac'
fi
if [ "$copy_audio_names" ] && [ ! "$main_audio_track_name" ]; then
track_name="$(echo "$all_audio_streams_info" |
sed -n ${main_audio_track}p |
sed -n 's/^.* \{1,\}title \{1,\}: \(.*\)$/\1/p')"
if [ "$track_name" ]; then
main_audio_track_name="$track_name"
fi
fi
sanitized_name="$(echo "$main_audio_track_name" | sed 's/,/_/g')"
surround_audio_encoder=''
surround_audio_bitrate=''
stereo_audio_encoder="$aac_encoder"
if [ "$copy_ac3" ] && [[ "$audio_track_info" =~ '(AC3)' ]]; then
surround_audio_encoder='copy'
elif (($(echo "$audio_track_info" | sed 's/^.*(\([0-9]\{1,\}\)\.\([0-9]\{1,\}\) ch).*$/\1\2/;s/^$/0/') > 20)); then
if [ "$allow_surround" ]; then
if ( [[ "$audio_track_info" =~ '(AC3)' ]] && ((($(echo "$audio_track_info" | sed -n 's/^.* \([0-9]\{1,\}\)bps$/\1/p' | sed 's/^$/640/') / 1000) <= $pass_ac3_bitrate)) ) || ( [ "$allow_dts" ] && [[ "$audio_track_info" =~ '(DTS' ]] ); then
surround_audio_encoder='copy'
else
surround_audio_encoder='ac3'
surround_audio_bitrate="$ac3_bitrate"
fi
fi
elif [[ "$audio_track_info" =~ '(AAC)' ]]; then
stereo_audio_encoder='copy'
fi
if [ "$surround_audio_encoder" ] && [ ! "$single_main_audio" ]; then
audio_track_list="$main_audio_track,$main_audio_track"
audio_track_name_list="$sanitized_name,$sanitized_name"
if [ "$container_format" == 'mkv' ]; then
audio_encoder_list="$surround_audio_encoder,$stereo_audio_encoder"
audio_bitrate_list="$surround_audio_bitrate,"
else
audio_encoder_list="$stereo_audio_encoder,$surround_audio_encoder"
audio_bitrate_list=",$surround_audio_bitrate"
fi
if [ "$sanitized_name" != "$main_audio_track_name" ]; then
audio_track_name_edits=(1,"$main_audio_track_name" 2,"$main_audio_track_name")
fi
track_index='3'
else
audio_track_list="$main_audio_track"
audio_track_name_list="$sanitized_name"
if [ "$surround_audio_encoder" ]; then
audio_encoder_list="$surround_audio_encoder"
audio_bitrate_list="$surround_audio_bitrate"
else
audio_encoder_list="$stereo_audio_encoder"
fi
if [ "$sanitized_name" != "$main_audio_track_name" ]; then
audio_track_name_edits=(1,"$main_audio_track_name")
fi
track_index='2'
fi
if [ "$add_all_audio" ]; then
if [ "$add_all_audio" == 'double' ]; then
width_prefix="$add_all_audio,"
else
width_prefix=''
fi
while read line; do
extra_audio_tracks=("${extra_audio_tracks[@]}" "$width_prefix$(echo "$line" | sed -n 's/^+ \([0-9]\{1,\}\), .*$/\1/p')")
done < <(echo "$all_audio_tracks_info" | sed ${main_audio_track}d)
fi
for item in "${extra_audio_tracks[@]}"; do
if [ "$(echo "$item" | sed 's/,.*$//')" == 'double' ]; then
double_audio="$allow_surround"
item="$(echo "$item" | sed 's/^[^,]*,//')"
else
double_audio=''
fi
track_number="$(printf '%.0f' "$(echo "$item" | sed 's/,.*$//')" 2>/dev/null)"
if (($track_number < 1)); then
die "invalid additional audio track: $item"
fi
audio_track_info="$(echo "$all_audio_tracks_info" | sed -n ${track_number}p)"
if [ ! "$audio_track_info" ]; then
die "missing additional audio track: $input"
fi
if [[ "$item" =~ ',' ]]; then
track_name="$(echo "$item" | sed 's/^[^,]*,//')"
elif [ "$copy_audio_names" ]; then
track_name="$(echo "$all_audio_streams_info" |
sed -n ${track_number}p |
sed -n 's/^.* \{1,\}title \{1,\}: \(.*\)$/\1/p')"
else
track_name=''
fi
sanitized_name="$(echo "$track_name" | sed 's/,/_/g')"
surround_audio_encoder=''
surround_audio_bitrate=''
stereo_audio_encoder="$aac_encoder"
if [ "$copy_all_ac3" ] && [[ "$audio_track_info" =~ '(AC3)' ]]; then
stereo_audio_encoder='copy'
elif (($(echo "$audio_track_info" | sed 's/^.*(\([0-9]\{1,\}\)\.\([0-9]\{1,\}\) ch).*$/\1\2/;s/^$/0/') > 20)); then
if [ "$allow_ac3" ] || [ "$double_audio" ]; then
if ( [[ "$audio_track_info" =~ '(AC3)' ]] && ((($(echo "$audio_track_info" | sed -n 's/^.* \([0-9]\{1,\}\)bps$/\1/p' | sed 's/^$/640/') / 1000) <= $pass_ac3_bitrate)) ) || ( [ "$allow_dts" ] && [[ "$audio_track_info" =~ '(DTS' ]] ); then
surround_audio_encoder='copy'
else
surround_audio_encoder='ac3'
surround_audio_bitrate="$ac3_bitrate"
fi
fi
elif [[ "$audio_track_info" =~ '(AAC)' ]]; then
stereo_audio_encoder='copy'
fi
if [ "$surround_audio_encoder" ] && [ "$double_audio" ]; then
audio_track_list="$audio_track_list,$track_number,$track_number"
audio_track_name_list="$audio_track_name_list,$sanitized_name,$sanitized_name"
if [ "$container_format" == 'mkv' ]; then
audio_encoder_list="$audio_encoder_list,$surround_audio_encoder,$stereo_audio_encoder"
audio_bitrate_list="$audio_bitrate_list,$surround_audio_bitrate,"
else
audio_encoder_list="$audio_encoder_list,$stereo_audio_encoder,$surround_audio_encoder"
audio_bitrate_list="$audio_bitrate_list,,$surround_audio_bitrate"
fi
if [ "$sanitized_name" != "$track_name" ]; then
audio_track_name_edits=("${audio_track_name_edits[@]}" "$track_index,$track_name" "$((track_index + 1)),$track_name")
fi
track_index="$((track_index + 2))"
else
audio_track_list="$audio_track_list,$track_number"
audio_track_name_list="$audio_track_name_list,$sanitized_name"
if [ "$surround_audio_encoder" ]; then
audio_encoder_list="$audio_encoder_list,$surround_audio_encoder"
audio_bitrate_list="$audio_bitrate_list,$surround_audio_bitrate"
else
audio_encoder_list="$audio_encoder_list,$stereo_audio_encoder"
audio_bitrate_list="$audio_bitrate_list,"
fi
if [ "$sanitized_name" != "$track_name" ]; then
audio_track_name_edits=("${audio_track_name_edits[@]}" "$track_index,$track_name")
fi
track_index="$((track_index + 1))"
fi
done
elif (($main_audio_track > 1)) || ((${#extra_audio_tracks[*]} > 0)); then
die "missing audio track: $input"
fi
if [ "$audio_track_list" ]; then
audio_options="--audio $audio_track_list --aencoder $audio_encoder_list"
if [ "$(echo "$audio_bitrate_list" | sed 's/,//g')" ]; then
audio_options="$audio_options --ab $audio_bitrate_list"
fi
if [ "$(echo "$audio_track_name_list" | sed 's/,//g')" ]; then
audio_options="$audio_options --aname"
else
audio_track_name_list=''
fi
else
audio_options=''
fi
# SUBTITLES
#
subtitle_track_list=''
readonly all_subtitle_tracks_info="$(echo "$media_info" |
sed -n '/^ + subtitle tracks:$/,$p' |
sed -n '/^ + /p')"
if [ "$burned_subtitle_track" ]; then
subtitle_track_info="$(echo "$all_subtitle_tracks_info" | sed -n ${burned_subtitle_track}p)"
if [ ! "$subtitle_track_info" ]; then
die "missing subtitle track: $input"
fi
subtitle_track_list="$burned_subtitle_track"
elif [ "$auto_burn" ]; then
readonly all_subtitle_streams_info="$(echo "$media_info" | sed -n '/^ Stream #[^:]\{1,\}: Subtitle: /p')"
subtitle_track='1'
while : ; do
subtitle_track_info="$(echo "$all_subtitle_tracks_info" | sed -n ${subtitle_track}p)"
if [ ! "$subtitle_track_info" ]; then
break
fi
if [[ "$(echo "$all_subtitle_streams_info" | sed -n ${subtitle_track}p)" =~ '(forced)' ]]; then
burned_subtitle_track="$subtitle_track"
subtitle_track_list="$burned_subtitle_track"
break
fi
subtitle_track="$((subtitle_track + 1))"
done
elif [ "$find_forced" ]; then
subtitle_track_list='scan'
fi
if [ "$add_all_subtitles" ]; then
if [ "$burned_subtitle_track" ]; then
tracks_info="$(echo "$all_subtitle_tracks_info" | sed ${burned_subtitle_track}d)"
else
tracks_info="$all_subtitle_tracks_info"
fi
while read line; do
extra_subtitle_tracks=("${extra_subtitle_tracks[@]}" "$(echo "$line" | sed -n 's/^+ \([0-9]\{1,\}\), .*$/\1/p')")
done < <(echo "$tracks_info")
fi
forced_subtitle_track_number=''
track_id='1'
for item in "${extra_subtitle_tracks[@]}"; do
track_number="$(printf '%.0f' "$(echo "$item" | sed 's/^forced,//')" 2>/dev/null)"
if (($track_number < 1)); then
die "invalid additional subtitle track: $item"
fi
subtitle_track_info="$(echo "$all_subtitle_tracks_info" | sed -n ${track_number}p)"
if [ ! "$subtitle_track_info" ]; then
die "missing additional subtitle track: $input"
fi
if [ "$container_format" != 'mkv' ] && [[ "$subtitle_track_info" =~ '(PGS)' ]]; then
die "incompatible additional subtitle track for MP4 format: $track_number"
fi
if [ ! "$subtitle_track_list" ]; then
subtitle_track_list="$track_number"
else
subtitle_track_list="$subtitle_track_list,$track_number"
fi
if [[ "$item" =~ ^'forced,' ]] && [ ! "$find_forced" ]; then
if [ "$track_number" == "$burned_subtitle_track" ]; then
die "forced subtitle track is already burned: $track_number"
fi
if [ "$container_format" == 'mkv' ]; then
forced_subtitle_track_number="$track_id"
else
forced_subtitle_track_number="$track_index"
fi
fi
track_id="$((track_id + 1))"
track_index="$((track_index + 1))"
done
if [ "$subtitle_track_list" ]; then
subtitle_options="--subtitle $subtitle_track_list"
if [ "$burned_subtitle_track" ]; then
subtitle_options="$subtitle_options --subtitle-burned"
elif [ "$find_forced" ]; then
subtitle_options="$subtitle_options --subtitle-forced"
if [ "$find_forced" == 'burn' ]; then
subtitle_options="$subtitle_options --subtitle-burned"
else
subtitle_options="$subtitle_options --subtitle-default"
fi
fi
else
subtitle_options=''
fi
# OTHER SUBTITLES
#
tmp=''
if [ "$burned_srt_file" ] || ((${#extra_srt_files[*]} > 0)); then
trap '[ "$tmp" ] && rm -rf "$tmp"' 0
trap '[ "$tmp" ] && rm -rf "$tmp"; exit 1' SIGHUP SIGINT SIGQUIT SIGTERM
tmp="/tmp/${program}.$$"
mkdir -m 700 "$tmp" || exit 1
fi
srt_file_list=''
srt_codeset_list=''
srt_offset_list=''
srt_lang_list=''
if [ "$burned_srt_file" ]; then
srt_file="$burned_srt_file"
srt_offset=''
srt_codeset=''
while [[ "$srt_file" =~ ',' ]]; do
srt_prefix="$(echo "$srt_file" | sed 's/,.*$//')"
if [ ! "$srt_offset" ] && [[ "$srt_prefix" =~ ^[+-]?[0-9][0-9]*$ ]]; then
srt_offset="$(echo "$srt_prefix" | sed 's/^+//')"
srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')"
elif [ ! "$srt_codeset" ] && [[ "$srt_prefix" =~ ^[0-9A-Za-z] ]] && [[ ! "$srt_prefix" =~ [\ /\\] ]] && [ ! -f "$srt_file" ]; then
srt_codeset="$srt_prefix"
srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')"
else
break
fi
done
# Force filename expansion with `eval` but first escape the string
# to hide ", $, &, ', (, ), ;, <, >, \, ` and |.
srt_file="$(eval echo "$(echo "$srt_file" | sed 's/\(["$&'\''();<>\\`|]\)/\\\1/g')")"
if [ ! "$srt_file" ]; then
syntax_error "missing burned subtitle filename"
fi
if [ ! -f "$srt_file" ]; then
die "burned subtitle not found: $srt_file"
fi
tmp_srt_file_link="$tmp/burned-subtitle.srt"
ln -s "$(cd "$(dirname "$srt_file")" 2>/dev/null && echo "$(pwd)/$(basename "$srt_file")")" "$tmp_srt_file_link"
srt_file_list="$tmp_srt_file_link"
srt_codeset_list="$srt_codeset"
srt_offset_list="$srt_offset"
fi
for item in "${extra_srt_files[@]}"; do
srt_file="$item"
srt_lang=''
srt_offset=''
srt_codeset=''
while [[ "$srt_file" =~ ',' ]]; do
srt_prefix="$(echo "$srt_file" | sed 's/,.*$//')"
if [ "$srt_prefix" == 'forced' ]; then
if [ ! "$find_forced" ]; then
if [ "$container_format" == 'mkv' ]; then
forced_subtitle_track_number="$track_id"
else
forced_subtitle_track_number="$track_index"
fi
fi
srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')"
elif [ ! "$srt_lang" ] && [[ "$srt_prefix" =~ ^[a-z][a-z][a-z]$ ]]; then
srt_lang="$srt_prefix"
srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')"
elif [ ! "$srt_offset" ] && [[ "$srt_prefix" =~ ^[+-]?[0-9][0-9]*$ ]]; then
srt_offset="$(echo "$srt_prefix" | sed 's/^+//')"
srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')"
elif [ ! "$srt_codeset" ] && [[ "$srt_prefix" =~ ^[0-9A-Za-z] ]] && [[ ! "$srt_prefix" =~ [\ /\\] ]] && [ ! -f "$srt_file" ]; then
srt_codeset="$srt_prefix"
srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')"
else
break
fi
done
# Force filename expansion with `eval` but first escape the string
# to hide ", $, &, ', (, ), ;, <, >, \, ` and |.
srt_file="$(eval echo "$(echo "$srt_file" | sed 's/\(["$&'\''();<>\\`|]\)/\\\1/g')")"
if [ ! "$srt_file" ]; then
syntax_error "missing subtitle filename"
fi
if [ ! -f "$srt_file" ]; then
die "subtitle not found: $srt_file"
fi
tmp_srt_file_link="$tmp/subtitle-$track_id.srt"
ln -s "$(cd "$(dirname "$srt_file")" 2>/dev/null && echo "$(pwd)/$(basename "$srt_file")")" "$tmp_srt_file_link"
if [ ! "$srt_file_list" ]; then
srt_file_list="$tmp_srt_file_link"
srt_codeset_list="$srt_codeset"
srt_offset_list="$srt_offset"
srt_lang_list="$srt_lang"
else
srt_file_list="$srt_file_list,$tmp_srt_file_link"
srt_codeset_list="$srt_codeset_list,$srt_codeset"
srt_offset_list="$srt_offset_list,$srt_offset"
srt_lang_list="$srt_lang_list,$srt_lang"
fi
track_id="$((track_id + 1))"
track_index="$((track_index + 1))"
done
if [ "$srt_file_list" ]; then
srt_options="--srt-file $srt_file_list"
if [ "$(echo "$srt_codeset_list" | sed 's/,//g')" ]; then
srt_options="$srt_options --srt-codeset $srt_codeset_list"
fi
if [ "$(echo "$srt_offset_list" | sed 's/,//g')" ]; then
srt_options="$srt_options --srt-offset $srt_offset_list"
fi
if [ "$(echo "$srt_lang_list" | sed 's/,//g')" ]; then
srt_options="$srt_options --srt-lang $srt_lang_list"
fi
if [ "$burned_srt_file" ]; then
srt_options="$srt_options --srt-burn"
fi
else
srt_options=''
fi
# OTHER OPTIONS
#
if [ "$media_title" == '1' ]; then
title_options=''
else
title_options="--title $media_title"
fi
section_options="$(echo "$section_options" | sed 's/^ *//')"
if [ "$chapter_names_file" ]; then
markers_options='--markers='
else
markers_options='--markers'
fi
if [ "$preset" == 'medium' ]; then
preset_options=''
else
preset_options="--encoder-preset $preset"
fi
tune_options="$(echo "$tune_options" | sed 's/^ *//')"
encopts_options="vbv-maxrate=$vbv_maxrate:vbv-bufsize=$vbv_bufsize"
# Limit reference frames for playback compatibility with popular devices.
#
case $preset in
slower|veryslow|placebo)
encopts_options="ref=5:$encopts_options"
;;
esac
if [ "$max_rate_factor" ]; then
encopts_options="$encopts_options:crf-max=$max_rate_factor"
fi
if [ "$extra_encopts_options" ]; then
encopts_options="$(echo "$encopts_options:$extra_encopts_options" | sed 's/:*$//;s/:\{1,\}/:/g')"
fi
if [ "$crop_values" == 'auto' ]; then
crop_options=''
else
crop_options="--crop $crop_values"
fi
filter_options="$(echo "$filter_options" | sed 's/^ *//;s/ *$//;s/ \{1,\}/ /g')"
passthru_options="$(echo "$passthru_options" | sed 's/^ *//')"
# DEBUG OUTPUT
#
if [ "$debug" ]; then
echo >&2
echo "title_options = $title_options" >&2
echo "section_options = $section_options" >&2
echo "markers_options = $markers_options" >&2
echo "chapter_names_file = $chapter_names_file" >&2
echo "preset_options = $preset_options" >&2
echo "tune_options = $tune_options" >&2
echo "encopts_options = $encopts_options" >&2
echo "level_options = $level_options" >&2
echo "rate_factor = $rate_factor" >&2
echo "frame_rate_options = $frame_rate_options" >&2
echo "audio_options = $audio_options" >&2
echo "audio_track_name_list = $audio_track_name_list" >&2
echo "crop_options = $crop_options" >&2
echo "size_options = $size_options" >&2
echo "filter_options = $filter_options" >&2
echo "subtitle_options = $subtitle_options" >&2
echo "srt_options = $srt_options" >&2
echo "passthru_options = $passthru_options" >&2
echo "input = $input" >&2
echo "output = $output" >&2
echo >&2
command="$(echo "HandBrakeCLI $title_options $section_options $markers_options" | sed 's/ *$//;s/ \{1,\}/ /g')"
if [ "$chapter_names_file" ]; then
command="$command$(escape_string "$chapter_names_file")"
fi
command="$command $(echo "--encoder x264 $preset_options $tune_options --encopts $encopts_options $level_options --quality $rate_factor $frame_rate_options $audio_options" | sed 's/ *$//;s/ \{1,\}/ /g')"
if [ "$audio_track_name_list" ]; then
command="$command $(escape_string "$audio_track_name_list")"
fi
command="$command $(echo "$crop_options $size_options $filter_options $subtitle_options $srt_options $passthru_options" | sed 's/^ *//;s/ *$//;s/ \{1,\}/ /g') --input $(escape_string "$input") --output $(escape_string "$output")"
if [ "$write_log" ]; then
command="$command 2>&1 | tee -a $(escape_string "${output}.log")"
fi
echo "$command"
for item in "${audio_track_name_edits[@]}"; do
track_index="$(echo "$item" | sed 's/,.*$//')"
track_name="$(echo "$item" | sed 's/^[^,]*,//')"
if [ "$container_format" == 'mkv' ]; then
echo "[ -f $(escape_string "$output") ] && mkvpropedit --quiet --edit track:a$track_index --set name=$(escape_string "$track_name") $(escape_string "$output")"
else
echo "[ -f $(escape_string "$output") ] && mp4track --track-index $track_index --hdlrname $(escape_string "$track_name") $(escape_string "$output")"
echo "[ -f $(escape_string "$output") ] && mp4track --track-index $track_index --udtaname $(escape_string "$track_name") $(escape_string "$output")"
fi
done
if [ "$forced_subtitle_track_number" ]; then
if [ "$container_format" == 'mkv' ]; then
echo "[ -f $(escape_string "$output") ] && mkvpropedit --quiet --edit track:s$forced_subtitle_track_number --set flag-default=1 --set flag-forced=1 $(escape_string "$output")"
else
echo "[ -f $(escape_string "$output") ] && mp4track --track-index $forced_subtitle_track_number --enabled true $(escape_string "$output")"
fi
elif [ "$find_forced" == 'add' ] && [ "$container_format" == 'mkv' ]; then
echo "[ -f $(escape_string "$output") ] && [ \"\$(mkvmerge --identify-verbose $(escape_string "$output") 2>/dev/null | sed -n '/^Track ID [0-9]\\{1,\\}: subtitles .* default_track:1 .*\$/p')\" ] && mkvpropedit --quiet --edit track:s1 --set flag-forced=1 $(escape_string "$output")"
fi
exit
fi
# OUTPUT
#
if [ "$container_format" == 'mkv' ]; then
if [ "$find_forced" == 'add' ]; then
if ! $(which mkvmerge >/dev/null); then
die 'executable not in $PATH: mkvmerge'
fi
fi
tool='mkvpropedit'
else
tool='mp4track'
fi
if ( ((${#audio_track_name_edits[*]} > 0)) || [ "$forced_subtitle_track_number" ] ) && ! $(which $tool >/dev/null); then
die "executable not in \$PATH: $tool"
fi
if [ "$write_log" ]; then
log_file="${output}.log"
else
log_file='/dev/null'
fi
echo "Transcoding: $input" >&2
time {
HandBrakeCLI \
$title_options \
$section_options \
$markers_options"$chapter_names_file" \
--encoder x264 \
$preset_options \
$tune_options \
--encopts $encopts_options \
$level_options \
--quality $rate_factor \
$frame_rate_options \
$audio_options "$audio_track_name_list" \
$crop_options \
$size_options \
$filter_options \
$subtitle_options \
$srt_options \
$passthru_options \
--input "$input" \
--output "$output" \
2>&1 | tee -a "$log_file"
if [ -f "$output" ]; then
for item in "${audio_track_name_edits[@]}"; do
track_index="$(echo "$item" | sed 's/,.*$//')"
track_name="$(echo "$item" | sed 's/^[^,]*,//')"
if [ "$container_format" == 'mkv' ]; then
mkvpropedit --quiet --edit track:a$track_index --set name="$track_name" "$output" || exit 1
else
mp4track --track-index $track_index --hdlrname "$track_name" "$output" &&
mp4track --track-index $track_index --udtaname "$track_name" "$output" || exit 1
fi
done
if [ "$forced_subtitle_track_number" ]; then
if [ "$container_format" == 'mkv' ]; then
mkvpropedit --quiet --edit track:s$forced_subtitle_track_number --set flag-default=1 --set flag-forced=1 "$output" || exit 1
else
mp4track --track-index $forced_subtitle_track_number --enabled true "$output" || exit 1
fi
elif [ "$find_forced" == 'add' ] && [ "$container_format" == 'mkv' ] && [ "$(mkvmerge --identify-verbose "$output" 2>/dev/null | sed -n '/^Track ID [0-9]\{1,\}: subtitles .* default_track:1 .*$/p')" ]; then
mkvpropedit --quiet --edit track:s1 --set flag-forced=1 "$output" || exit 1
fi
fi
}
@LwsBtlr
Copy link

LwsBtlr commented Jan 17, 2014

OK, I don't know ho to add this properly, but I added a --target to specify where you want the transcoded file to end up and pasted it here:

https://gist.github.com/LwsBtlr/8470786#file-transcode-video-path-sh

syntax is simply transcode-video.sh --target /Users/user/Desktop sample.mkv

@d4rk22
Copy link

d4rk22 commented Feb 8, 2014

Don, I LOVE your script. I'm curious if it would be easy to add the option of multiple audio tracks. For example, passing the option --audio Track 1,2 would encode both audio tracks instead of just one. Use case would be director commentary. If it's not easy to add, it's al good. I will use one of the forks and try and figure out how to do that. Thanks for such a great script and all the work you've done to figure out the best settings!

@lisamelton
Copy link
Author

My apologies for missing comments by @d4rk22 (yesterday) and @LwsBtlr (last freakin' month). GitHub doesn't seem to send me notifications when gists are touched. Another good reason I need to move these video transcoding gists into their own project. Anyway...

Thanks so much for the positive feedback! I'm glad the script is working for you all and you like (most of the time) the settings which the script automatically chooses. Regarding the particular thoughts (and patches!) for new features...

I think --target is the wrong name for an output option since that overlaps with other transcoding nomenclature. A better name would simply be --output which is what HandBrakeCLI uses anyway. And it would be trivial to determine whether the argument is a file or directory so the script could act appropriately. But...

What if the output file name ends in a non-compatible media file type? Or the user has selected the --mkv option and the name ends in .mp4? The error handling starts to get complicated rather quickly. Worse yet...

What if the user wants the output media to go to one directory and the log file into another? I suspect that's what many power users would crave. Especially now that I've mentioned it. :) Not only does the error handling get complicated, but the code gets larger and larger. And it's already 455 lines, not including comments and blanks.

So I'm not inclined to add the feature yet. Not until I can make it flexible and small. But I'm not saying I won't add it. :)

As for multiple audio track support, I've also considered something like this. But it really complicates the code. And such a feature begs questions like, for MP4 files, do you want two output audio tracks generated for each new input audio track? Probably not, but it means each new track wouldn't fit exactly into the same logic if I refactored the current code into a single function. So, again, more code for this.

And I'm sure as soon as multiple audio track support is added, folks will be wanting multiple subtitle track support too. Or certainly more flexible subtitle support, i.e. the ability to include .srt input and such. (Seriously, I could write a whole essay on the wackiness of subtitle standards and support in HandBrakeCLI but that's something for a later time.)

But I will consider the multiple audio tracks feature because... I would like to have it myself. :)

Also, until I do move these gists into their own project or GitHub adds a bit more functionality to their notification feature set, please tap me on Twitter or App.net to let me know you've commented here. I don't want to miss your valuable feedback. Thanks!

@ferry147
Copy link

Thanks for your script. I'll try to add it if it's easy to do. But it seems some of the video formats are not supported. And I usually use Aneesoft video converter to make video conversion. Supports most video formats. Anyway, thanks for this free script.

@d4rk22
Copy link

d4rk22 commented Mar 8, 2014

Don,

First off, I just want to thank you again for all the hard work that went into this script. The ability to transcode my own media at the quality / space savings that I choose is highly addictive. I've always held off on buying BluRays because I was unsure of the transcoding process but now I've gone from 0 BluRays on February 1st to 70+ as of today.


As far as the multitrack audio request goes, it has dropped significantly on my want list for this script. The thinking behind my original request was to create a "BluRay like" experience inside of Plex; offering special features and the like. After seeing that the current version of Plex is not setup to handle that well (I created a library section "Movie Extras" and set the type to TV Shows and was creating a custom show for each season then labeling the extra features manually inside Plex, not pretty but it worked), I decided to just pull the trigger on a BluRay player. It's a much simpler solution especially considering how often I want to watch all the special features of a movie (typically only once). Even just the director commentary (which I do love listening to) would be cumbersome inside of Plex because they currently only label audio tracks with the language, codec, and number of channels.


Question about split movies:

I just picked up the Lord of the Rings Extended Edition on BluRay and the movies come split across two discs. What is the best way of joining them in conjunction with your script?

  1. Rip them, join the uncompressed MKVs with MKVtools or mkvmerge then transcode.
  2. Rip them, transcode them, then join with MP4Joiner or something similar.

I'm leaning towards option one but would love your input on the matter.


Thanks again for such a good script. I love looking at now playing on Plex and seeing "No Transcode" or "Direct Stream" on all the movies I rip myself.

@lisamelton
Copy link
Author

I talked to Ryan (@d4rk22) about his question on Twitter (who nicely tapped me there) but for everyone else who's reading along...

The best way to deal with split movies, like "The Lord of the Rings," is to leave them as two files and and let Plex and/or XBMC handle joining them together virtually in their UI presentation. All you have to do is name multi-part movies correctly. Here's how I name my Extended Edition files:

The Lord of the Rings_ The Fellowship of the Ring (2001).part1.mkv
The Lord of the Rings_ The Fellowship of the Ring (2001).part2.mkv

The Lord of the Rings_ The Two Towers (2002).part1.mkv
The Lord of the Rings_ The Two Towers (2002).part2.mkv

The Lord of the Rings_ The Return of the King (2003).part1.mkv
The Lord of the Rings_ The Return of the King (2003).part2.mkv

The key is using the ".partX" suffix right before the file extension. There are actually several legal forms of the suffix (e.g. "cdX," "discX," etc.) but you have to careful to use one that works for most media center software.

Documentation about this for Plex can be found here:

Naming Stacked Or Multi-Part Movies

...and for XBMC here:

File stacking

@gugahoi
Copy link

gugahoi commented Jun 11, 2014

I feel a bit stupid but the script constantly gives me:

ERROR: Invalid audio codec: 0x100

Unsure how to proceed on Ubuntu 14.04. I have installed faac as I assumed it was the audio coded library that was missing but that helped with nothing. I have also tried to modify the code from 'ca_aac' to 'faac' but that also seems to change nothing. Would really appreciate some help!

@dennisoderwald
Copy link

Oh @gugahoi, same issue here.

@lisamelton
Copy link
Author

Sorry, I missed your comments. Yeah, the ca_aaccodec is not available on non-OS X platforms. But faac should work. I don't run Linux anymore so this is difficult for me to debug. Stay tuned...

@TraiGuzie
Copy link

Wow this is a great script for quality/size perfection. Now my question is how would I set this up to do a batch encode from a folder of files? I think this would make a great set of online backups for my raw 5DMKII footage from events!

@lisamelton
Copy link
Author

Here's a shorter version of the shell script I use to do all my transcoding:

#!/bin/bash

readonly queue="$(cd "$(dirname "$0")" && pwd)/queue.txt"

input="$(sed -n 1p "$queue")"

while [ "$input" ]; do
    sed -i '' 1d "$queue" || exit 1
    transcode-video.sh "$input"
    input="$(sed -n 1p "$queue")"
done

As you can see, it's designed to read a text file named queue.txt, located in the same directory as the script, for a list of pathnames to transcode. Once it has the pathname, it simply deletes it from the text file.

And if it encounters a blank line in the text file, it just stops.

Which means you can edit the file while you're transcoding to change the order, add new items, or stop after a certain file is complete.

@lisamelton
Copy link
Author

Version 3.1 of the script should have some hope of running on Linux now. Let me know if you're still having problems.

@jby
Copy link

jby commented Jul 15, 2014

There seem to be a problem with HandBrakeCLI in Ubuntu 14.04

http://askubuntu.com/questions/464865/handbrake-cli-updated-and-now-does-not-work

@lisamelton
Copy link
Author

Version 3.7 should fix some problems non-OS X users were having with AC-3 pass-thru audio. Really stupid bug on my part.

@notDavid
Copy link

Hi, would it be possible to use the "-N" option of HandBrakeCLI instead of "--subtitle" ?
This would be easier because you wouldn't first have to search for the number of the subtitle but instead you can simply specify your language?

From the CLIGuide:

-N, --native-language Specifiy your language preference. When the first
audio track does not match your native language then
select the first subtitle that does. When used in
conjunction with --native-dub the audio track is
changed in preference to subtitles. Provide the
language's iso639-2 code (fre, eng, spa, dut, et cetera)

@jmwohl
Copy link

jmwohl commented Nov 14, 2014

Hey @donmelton... thanks for making this, it's extremely useful.

One question — with the default settings, I'm seeing a lot of these warnings:

x264 [warning]: VBV underflow

Any idea if this is something I should worry about? Tried googling it, which just confused me... seems like an ffmpeg thing? I tried messing with the --max and --vbv-bufsize options, but wasn't able to find a setting that got rid of the errors.

@69dragons
Copy link

Hi,

I just installed this script and handbrakecli and I get the error "transcode-video.sh: can't determine HandBrakeCLI version" and just dumps me back to the prompt. I have HandBrake svn7212 (2015052001) - Darwin x86_64, and I have it stored in /usr/local/bin which is in the $path as suggested. Any assistance is appreciated.

@evanballweg
Copy link

The --output option to specify where to save the compressed file doesn't seem to be working. No matter what I use, it names the file with the basename.mp4 in the directory that the script is called from.

I'm trying to script it by placing the output in the same folder as the input. A lot of my files have the same name in different directories.

@agrajag9
Copy link

@donmelton I'm having the same problem as @69dragons.

I'm running on Linux Debian jessie with HandBrakeCLI rev0 (2014100499) - Linux x86_64 from package handbrake-cli 0.9.9+svn6422+dfsg1-2.

It looks like the regexes you're using for $release_version don't match the output from the version I have.

$ HandBrakeCLI --preset-list 2>&1 | head -n1
HandBrake rev0 (2014100499) - Linux x86_64 - http://handbrake.fr

@loshlee
Copy link

loshlee commented Dec 31, 2016

After release of HandBrake version 1, "can't determine HandBrakeCLI version" appears.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment