Last active
January 15, 2019 00:48
-
-
Save fallwith/9053bad932115d2ea4c81031afd96dd3 to your computer and use it in GitHub Desktop.
ffmpeg sweet spot settings for x265
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
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| NAME=mp4it | |
| VERSION=0.0.12 | |
| # | |
| # mp4it | |
| # | |
| # ffmpeg front-end to take an input source (likely in mkv or m2ts format) | |
| # and produce an h.265 formatted mp4 output file at the given location | |
| # | |
| # settings used represent a sweet spot between quality and file size based | |
| # on testing with 1080p input | |
| # | |
| function show_usage_and_exit() { | |
| code=$1 | |
| msg=${2:-} | |
| if [ "$msg" != "" ]; then | |
| echo "$msg" | |
| echo | |
| fi | |
| echo "Playlist Info: $0 playlists /path/to/blu-ray/root" | |
| echo | |
| echo "Encoding: $0 -i source [-o destination] [--no-crop] [--no-subs] [--subtrack TRACK]" | |
| echo | |
| echo " -i|--input source - (required) path to input source file" | |
| echo " -o|--output destination - (optional) path to the output file," | |
| echo " defaults to source path with an .mp4 extension" | |
| echo " -h|--help - show usage info" | |
| echo " --no-crop - do not crop the image (do not remove the black bars)" | |
| echo " --no-grain - do not leverage the 'grain' x265 preset" | |
| echo " --no-subs - do not seek the first subtitle track and overlay it (burn it in)" | |
| echo " -ss <START SECS> - set the start time offset (passed directly as -ss to ffmpeg)" | |
| echo " -t <DURATION SECS> - set the duration (passed directly as -t to ffmpeg)" | |
| echo " --audio-stream STREAM - specify the audio stream to use (ex: 0:2)" | |
| echo " --subtitle-stream STREAM - specify the subtitle stream to burn in (ex: 0:5)" | |
| echo " -v|--version - show version info" | |
| echo | |
| exit $code | |
| } | |
| function process_cli_args() { | |
| if [ $# == 0 ]; then | |
| show_usage_and_exit -1 | |
| fi | |
| audio_bitrate=320 | |
| subtitles=1 | |
| subrips=0 | |
| subtrack="" | |
| audtrack="" | |
| crop=1 | |
| crop_dimensions="" | |
| min_height_for_grain=481 | |
| grain=1 | |
| ss="" | |
| t="" | |
| batch=0 | |
| declare -a input_files | |
| while (( "$#" )); do | |
| case "$1" in | |
| playlists|p) | |
| show_playlists $2 | |
| exit | |
| ;; | |
| -h|--help) | |
| show_usage_and_exit 0 | |
| ;; | |
| -i|--input) | |
| input=$2 | |
| shift 2 | |
| ;; | |
| -o|--output) | |
| output=$2 | |
| shift 2 | |
| ;; | |
| --no-crop) | |
| crop=0 | |
| shift | |
| ;; | |
| --no-grain) | |
| grain=0 | |
| shift; | |
| ;; | |
| --no-subs) | |
| subtitles=0 | |
| shift | |
| ;; | |
| --audio-stream) | |
| audtrack=$2 | |
| shift 2 | |
| ;; | |
| --subtitle-stream) | |
| subtrack=$2 | |
| shift 2 | |
| ;; | |
| -ss) | |
| ss=$2 | |
| shift 2 | |
| ;; | |
| -t) | |
| t=$2 | |
| shift 2 | |
| ;; | |
| -v|--version) | |
| echo "$NAME v${VERSION}" | |
| exit 0 | |
| ;; | |
| -*|--*) | |
| echo "Unknown argument $1" | |
| exit -1 | |
| ;; | |
| *) | |
| echo "Unexpected string $1" | |
| exit -1 | |
| ;; | |
| esac | |
| done | |
| output=${output:-"${input%.*}.mp4"} | |
| if [ "$input" == "" ]; then | |
| echo "No '-i <input>' input argument provided!" | |
| exit -1 | |
| fi | |
| if [[ ${input:0:7} == "concat:" ]]; then | |
| for path in ${input//\|/ }; do | |
| path=${path//concat:/} | |
| if [ ! -f "$path" ]; then | |
| "Concatenated input path '$path' does not exist!" | |
| exit -1 | |
| fi | |
| done | |
| elif [ ! -f "$input" ]; then | |
| echo "Input '$input' does not exist!" | |
| exit -1 | |
| fi | |
| } | |
| function check_prereqs() { | |
| for prereq in ffmpeg ffprobe sed awk grep head; do | |
| set +e | |
| result=$(hash $prereq 2>&1 >/dev/null) | |
| set -e | |
| if [ "$result" != "" ]; then | |
| echo "Unable to locate the prerequisite '$prereq' tool." | |
| echo "Please either install it or ensure that it's in your PATH and try again." | |
| exit -1 | |
| fi | |
| done | |
| } | |
| function determine_playlist_info() { | |
| if [[ ! $input == *.mpls ]]; then | |
| return | |
| fi | |
| echo "Determining playlist info for ${input}..." | |
| batch=1 | |
| largest_input='' | |
| largest_input_size=0 | |
| dirname=${input%/*} | |
| while read -r m2ts_file; do | |
| len=$(echo $m2ts_file | wc -c) | |
| if [ $len -eq 12 ]; then | |
| m2ts_file=$(echo $m2ts_file | cut -c2-11) | |
| elif [ $len -ne 11 ]; then | |
| echo "Error gleaning m2ts filename from mpls input, got '$m2ts_file'" | |
| exit -1 | |
| fi | |
| full_path="$dirname/../STREAM/$m2ts_file" | |
| input_files+=($full_path) | |
| size=$(stat -c %s $full_path) | |
| if [ $size -gt $largest_input_size ]; then | |
| largest_input_size=$size | |
| largest_input=$full_path | |
| fi | |
| done <<< $(mpls_to_m2ts $input) | |
| input=$largest_input | |
| } | |
| function mpls_to_m2ts() { | |
| mpls=$1 | |
| cat -v $mpls | grep -oE '\d+M2TS' | sed 's/M2TS/.m2ts/g' | |
| } | |
| function show_playlists() { | |
| path="$1" | |
| playlist_subdir="BDMV/PLAYLIST" | |
| if [ ! -e "$path/$playlist_subdir" ]; then | |
| echo "Error: could not locate a $playlist_subdir dir beneath $path - path is not a Blu-ray root?" | |
| exit -1 | |
| fi | |
| playlists=$(ffprobe -analyzeduration 300M -probesize 100M "bluray:$path" 2>&1 | grep -Eo '\d+.mpls \(\d+:\d+:\d+\)') | |
| while read -r playlist; do | |
| echo "PLAYLIST: $playlist" | |
| file=$(echo $playlist | awk '{ print $1 }') | |
| mpls_to_m2ts $path/$playlist_subdir/$file | |
| echo | |
| done <<< "$playlists" | |
| } | |
| function determine_crop() { | |
| if [ $crop -ne 1 ]; then | |
| return | |
| fi | |
| echo "Determining cropping info..." | |
| # duration in secs divided by 10, to detect dimensions for cropping in multiple spots | |
| duration_slice=$(ffprobe "$input" 2>&1 | grep Duration | awk '{print $2}' | awk -F: '{print (($1 * 3600) + ($2 * 60) + $3)/10}' | sed 's/\..*$//') | |
| # detect the input source's dimensions | |
| input_dimensions=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "$input" | head -1) | |
| # determine the max detected dimensions | |
| max_width=0 | |
| max_height=0 | |
| for i in {1..9}; do | |
| read -r width height <<<$(ffmpeg -ss $((duration_slice * $i)) -t 1 -i "$input" -vf cropdetect -map 0:0 -f null - 2>&1 | awk '/crop/ { print $NF }' | tail -1 | cut -c6- | awk -F: '{print $1 " " $2}') | |
| if [ "$width" == "" ]; then | |
| continue | |
| fi | |
| if [ $height == "" ]; then | |
| continue | |
| fi | |
| if [ $width -gt $max_width ]; then | |
| max_width=$width | |
| fi | |
| if [ $height -gt $max_height ]; then | |
| max_height=$height | |
| fi | |
| done | |
| if [ "${max_width}x${max_height}" != "$input_dimensions" ]; then | |
| echo "Will crop from $input_dimensions to ${max_width}x${max_height}" | |
| crop_dimensions="$max_width:$max_height" | |
| else | |
| echo "Cropping will not be performed. Dimensions will remain as $input_dimensions" | |
| fi | |
| if [ $max_height -lt $min_height_for_grain ]; then | |
| echo "Input height of $max_height is below the minimum required for grain preservation (${min_height_for_grain}). Disabling grain." | |
| grain=0 | |
| fi | |
| } | |
| function determine_subtitle_track() { | |
| if [ $subtitles -eq 1 ]; then | |
| if [ "$subtrack" == "" ]; then | |
| set +e | |
| subinfo=$(ffprobe "$input" 2>&1 | grep "Stream.*Subtitle" | head -1) | |
| set -e | |
| if [ "$subinfo" == "" ]; then | |
| echo "No subtitle tracks found - subtitles will not be burned in" | |
| else | |
| subtrack=$(echo "$subinfo" | awk '{print $2}' | sed 's/\[.*//' | cut -c2- | sed -e 's/(.*):$//g') | |
| echo "Using first discovered subtitle track $subtrack" | |
| if ( echo "$subinfo" | grep -q subrip ); then | |
| echo "subrips subtitles detected" | |
| subrips=1 | |
| fi | |
| fi | |
| else | |
| echo "Using user-specified subtitle track $subtrack" | |
| fi | |
| fi | |
| } | |
| function determine_audio() { | |
| info_cmd="ffprobe \"$input\" 2>&1 | grep Audio" | |
| if [ ! "$audtrack" == "" ]; then | |
| info_cmd="$info_cmd | grep $audtrack" | |
| else | |
| info_cmd="$info_cmd | head -1" | |
| fi | |
| info=$(eval $info_cmd) | |
| if (echo $info | grep -q mono); then | |
| mono=1 | |
| else | |
| mono=0 | |
| fi | |
| set +e | |
| bitrate=$(echo $info | sed 's/ (default)//' | grep "kb/s$" |awk '{ print $(NF-1) }') | |
| set -e | |
| if [ "$bitrate" == "" ]; then | |
| echo "Couldn't determine source audio bitrate - will use $audio_bitrate for the output" | |
| elif [[ $bitrate =~ ^[0-9]+$ ]] && [[ $bitrate -lt $audio_bitrate ]]; then | |
| echo "Source audio bitrate is only $bitrate kb/s - using that rate for the output as well" | |
| audio_bitrate=$bitrate | |
| fi | |
| } | |
| function formulate_command() { | |
| cmd="ffmpeg" | |
| if [ "$t" != "" ]; then | |
| cmd="$cmd -t $t" | |
| fi | |
| if [ "$ss" != "" ]; then | |
| cmd="$cmd -ss $ss" | |
| fi | |
| # -i <input file> - specify an input file | |
| cmd="$cmd -i" | |
| if [ $batch -eq 1 ]; then | |
| sources=$(IFS="|" && echo "${input_files[*]}") | |
| cmd="$cmd \"concat:$sources\"" | |
| else | |
| cmd="$cmd \"$input\"" | |
| fi | |
| if [ "$audtrack" != "" ]; then | |
| cmd="$cmd -map 0:0 -map $audtrack" | |
| fi | |
| # -b:a <bitrate>k - remux audio into <bitrate> kbps AAC format (first audio track) | |
| # -vcodec libx265 - use the x265 video codec for video encoding (first video track) | |
| # -preset veryfast - set the x265 video encoding preset (default = medium) | |
| # -crf 22 - constant rate factor - 0 = lossless, 51 = worst, 28 = default | |
| # -pix_fmt yuv420p - use 8 bit video encoding | |
| # -tag:v hvc1 - tag the file as being hvc1 (instead of hev1) for Apple compatibility | |
| # -x265-params - pass params to x265 | |
| # no-strong-intra-smoothing=true - disable strong intra smoothing for 32x32 blocks | |
| cmd="$cmd -b:a ${audio_bitrate}k -vcodec libx265 -preset veryfast -crf 22 -pix_fmt yuv420p -tag:v hvc1 -x265-params no-strong-intra-smoothing=true" | |
| if [ $grain -eq 1 ]; then | |
| # -tune grain - work to preserve film grain | |
| cmd="$cmd -tune grain" | |
| fi | |
| # force 2 channels for all non-mono audio | |
| # -ac 2 - 2 audio channels | |
| if [ $mono -ne 1 ]; then | |
| cmd="$cmd -ac 2" | |
| fi | |
| if [ "$subtrack" != "" ]; then | |
| cmd="$cmd -probesize 100M -analyzeduration 300M" | |
| if [ $subrips -eq 1 ]; then | |
| # -c:s mov_text - glean the subtitles from the input source and copy them over to the output | |
| cmd="$cmd -c:s mov_text" | |
| else | |
| # -probesize 100M - when searching for subtitle info, stop if nothing is found at the 100 megabyte mark | |
| # -analyzeduration 300M - when searching for subtitle info, stop if nothing is found at the 300 minute mark | |
| # -filter_complex "[<vid track>][<sub track>]overlay" - burn in the subtitle track on top of the video track | |
| cmd="$cmd -filter_complex \"[0:0][${subtrack}]overlay" | |
| if [ "$crop_dimensions" != "" ]; then | |
| cmd="$cmd,crop=$crop_dimensions" | |
| fi | |
| cmd="$cmd\"" | |
| fi | |
| elif [ "$crop_dimensions" != "" ]; then | |
| cmd="$cmd -filter:v \"crop=$crop_dimensions\"" | |
| fi | |
| cmd="$cmd \"$output\"" | |
| echo $cmd | |
| } | |
| function run_command() { | |
| echo "" | |
| eval $cmd | |
| } | |
| process_cli_args "$@" | |
| check_prereqs | |
| determine_playlist_info | |
| determine_crop | |
| determine_subtitle_track | |
| determine_audio | |
| formulate_command | |
| run_command |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment