#!/usr/bin/bash # Needed: # ffmpeg, ffprobe, mkvmerge, x264, bash, bc, awk, sed, sort, uniq, tr # Mandatory: # -i [-i …] Input files # Optional: # -h Print this help, and information about the inputs. # -n Do not actually run the commands (print them). # -y Accept the default answers to this program’s # questions: include all streams, use the first # default audio in the inputs as default audio in the # output (same for subtitles), and mark any audio or # subtitles stream that is forced in the inputs as # forced in the output. # -o Output file (default: output.mkv). # -t Place where intermediate files will be stored. # -A kb/s for each audio channel (default: 64). # -V | -Q kb/s for the video (default: 850, with 2 passes), # or constant quality between 0 and 51 # (see: http://slhck.info/articles/crf). # -S Slow first pass (“turbo”-1st pass sometimes fails). # -P Preset, among “ultrafast”, “superfast”, # “veryfast”, “faster”, “fast”, “medium”, # “slow”, “slower”, “veryslow”, “placebo” # (the slower the better; default: medium). # -T Kind of video, among “film”, “animation”, # “grain”, “stillimage”, “psnr”, “ssim”, # “fastdecode”, “zerolatency”. # -W Maximum width (greater than 1920 is useless). # -H Maximum height (greater than 1080 is useless). # -C Crop black borders using ffmpeg’s autodetection. inputs=() output=output.mkv tmpdir=/tmp x264slow= x264qual="--bitrate 850 --pass" x264maxW=1920 x264maxH=1080 abort= fake= defaults= autocrop= error=0 ## READ PARAMETERS function usage() { sed -n "2,/^\$/s/.//p" "$0" abort=true } while getopts i:o:t:A:SV:Q:P:T:W:H:Chny opt; do case $opt in h) usage ;; n) fake=true ;; y) defaults=true ;; i) inputs[${#inputs[*]}]="$OPTARG" ;; o) output="$OPTARG" ;; t) if [ -d "$OPTARG" ] && [ -w "$OPTARG" ] \ && [ "$OPTARG" == "$(printf '%q' "$OPTARG")" ]; then tmpdir="${OPTARG%/}" else echo "$OPTARG contains special characters, or cannot be written." >&2 fi ;; A) akbps="${OPTARG//[^0-9]}" ;; S) x264slow=true ;; V) OPTARG="${OPTARG//[^0-9]}"; x264qual="--bitrate ${OPTARG:-850} --pass" ;; Q) OPTARG="${OPTARG//[^0-9]}" [ -n "$OPTARG" ] && x264qual="--crf $OPTARG" \ || x264qual="--bitrate 850 --pass" ;; P) preset="$OPTARG" ;; T) tune="$OPTARG" ;; W) OPTARG="${OPTARG//[^0-9]}"; x264maxW=${OPTARG:-1920} [ $x264maxW -le 1920 ] || x264maxW=1920 [ $x264maxW -ge 320 ] || x264maxW=320 ;; H) OPTARG="${OPTARG//[^0-9]}"; x264maxH=${OPTARG:-1080} [ $x264maxH -le 1080 ] || x264maxH=1080 [ $x264maxH -ge 240 ] || x264maxH=240 ;; C) autocrop=true ;; esac; done if [ -z "$abort" ] && [ ${#inputs[*]} -eq 0 ]; then usage >&2; exit 1; fi tmppfx=$tmpdir/divx+hd.$$ if [[ "$x264qual" =~ pass ]]; then x264qual="--stats $tmppfx.stats $x264qual" fi ## ANALYSE INPUTS # fields: str:file path, bool:use chapters, bool:use some subtitles, # int:number of chapters IFS=$'\n' read -d '' -a f_info < <( for f in "${inputs[@]}"; do printf "$f\t0\t0\t" ffprobe -loglevel error -show_chapters "$f" | grep -Fx '[CHAPTER]' | wc -l done ) # fields: int:file index, int:stream index, str:codec, str:profile, int:width, # int:height, [str:transformation], [int:new width], [int:new height], # str:sample aspect ratio, str:fps, float:stream start time, # str:language, [str:title], bool:use this stream exec 3>&1 IFS=$'\n' read -d '' -a v_info < <( for ((i=0; i<${#inputs[*]}; i++)); do ffprobe -loglevel error -show_streams -select_streams v "${inputs[$i]}" \ | awk -F= -vfile=$i -vmw=$x264maxW -vmh=$x264maxH \ -vffcrop=$autocrop -vfffile="${inputs[$i]}" -vOFS=$'\t' ' BEGIN{ gsub(/["\$]/,"\\\\&",fffile) } /^\[STREAM/ { idx=0; codec=""; profile=""; fullw=768; fullh=432 sar="1:1"; fps=25; start=0; lang="und"; title="" } $1=="index" { idx=$2 } $1=="codec_name" { codec=$2 } $1=="profile" { profile=$2 } $1=="width" { fullw=$2 } $1=="height" { fullh=$2 } $1=="sample_aspect_ratio" { sar=$2 } $1=="avg_frame_rate" { fps=$2 } $1=="start_time" { start=$2 } $1=="TAG:language" { lang=$2 } $1=="TAG:title" { title=$2 } /^\[\/STREAM/ { w=fullw; h=fullh; stdop=""; stdw=0; stdh=0 cropL=0; cropT=0; cropR=0; cropB=0 if (ffcrop!="") { fmt="ffmpeg -i \"%s\" -map 0:%d -t 00:30:00 -vf crop=" fmt=fmt (w-4) ":" (h-4) ":2:2,cropdetect=0.1:2:1 -f null /dev/null 2>&1" fmt=fmt " | sed -n \"s/.*crop=//p\" | sort | uniq -c | sort -k1,1n" fmt=fmt " | awk \"END{print \\$2}\" | tr : =" cmd=sprintf(fmt, fffile, idx) print cmd >"/dev/fd/3" cmd | getline; close(cmd) if ($1+4!=w || $2+4!=h) { stdop="crop⇒/" w=$1; h=$2; stdw=w; stdh=h cropL=2+2*($3/2); cropT=2+2*($4/2) cropR=fullw-w-cropL; cropB=fullh-h-cropT } } if (w>mw || h>mh) { stdop=stdop "resize⇒/" if ((h*mw/w)240) { stdw=320; stdh=int(h*320/(w*8)+0.5)*8; if (stdh>mh) stdh=mh } else { stdw=int(w*240/(h*8)+0.5)*8; stdh=240; if (stdw>mw) stdw=mw } } else if (w/8!=int(w/8) || h/8!=int(h/8)) { stdop="crop⇒/" stdw=int(w/8)*8; stdh=int(h/8)*8 halfcropw=2*int((w-stdw)/4); halfcroph=2*int((h-stdh)/4) cropL=cropL+halfcropw; cropR=cropR+(w-stdw-halfcropw) cropT=cropT+halfcroph; cropB=cropB+(h-stdh-halfcroph) } print file, idx, codec, profile, fullw, fullh, stdop, cropL, cropT, \ cropR, cropB, stdw, stdh, sar, fps, start, lang, title, 0 }' done ) exec 3>&- # fields: int:file index, int:stream index, str:codec, str:profile, # int:sample frequency, int:number of channels, int:planned kb/s, # str:layout, float:stream start time, bool:stream is default in file, # bool:stream is forced in file, bool:stream is for visual impaired, # str:language, [str:title], bool:use this stream, # bool:use this stream as default audio, bool:force this audio stream IFS=$'\n' read -d '' -a a_info < <( for ((i=0; i<${#inputs[*]}; i++)); do ffprobe -loglevel error -show_streams -select_streams a "${inputs[$i]}" \ | awk -F= -vfile=$i -vabr=${akbps:-64} -vOFS=$'\t' ' /^\[STREAM/ { idx=0; codec=""; profile=""; sampfreq=0; channels=2; layout="2.0" start=0; def=0; force=0; seehelp=0; lang="fre"; title="" } $1=="index" { idx=$2 } $1=="codec_name" { codec=$2 } $1=="profile" { profile=$2 } $1=="sample_rate" { sampfreq=$2 } $1=="channels" { channels=$2 } $1=="channel_layout" { layout=$2 } $1=="start_time" { start=$2 } $1=="DISPOSITION:default" { def=$2 } $1=="DISPOSITION:forced" { force=$2 } $1=="DISPOSITION:visual_impaired" { seehelp=$2 } $1=="TAG:language" { lang=$2 } $1=="TAG:title" { title=$2 } /^\[\/STREAM/ { print file, idx, codec, profile, sampfreq, channels, abr*channels, \ layout, start, def, force, seehelp, lang, title, 0, 0, 0 }' done ) # fields: int:file index, int:stream index, str:codec, float:stream start time, # bool:stream is default in file, bool:stream is forced in file, # bool:stream is for hearing impaired, str:language, [str:title], # bool:use these subtitles, bool:use this stream as default subtitles, # bool:force these subtitles IFS=$'\n' read -d '' -a s_info < <( for ((i=0; i<${#inputs[*]}; i++)); do ffprobe -loglevel error -show_streams -select_streams s "${inputs[$i]}" \ | awk -F= -vfile=$i -vOFS=$'\t' ' /^\[STREAM/ { idx=0; codec=""; start=0; def=0; force=0; hearhelp=0; lang="und"; title="" } $1=="index" { idx=$2 } $1=="codec_name" { codec=$2 } $1=="start_time" { start=$2 } $1=="DISPOSITION:default" { def=$2 } $1=="DISPOSITION:forced" { force=$2 } $1=="DISPOSITION:hearing_impaired" { hearhelp=$2 } $1=="TAG:language" { lang=$2 } $1=="TAG:title" { title=$2 } /^\[\/STREAM/ { print file, idx, codec, start, def, force, hearhelp, lang, title, 0, 0, 0 }' done ) ## PRINT INFORMATION, SELECT STREAMS # $1:array, $2:list-program (awk) function print_info() { local -n arr=$1 printf '%s\n' "${arr[@]}" | awk -F$'\t' -vOFS=$'\t' "$2" } # $1:array, $2:question, $3:default answer, $4:field to set, # $5==1 if only one flag can be set, # $6>0 means that the flag with this number must be set in the parent file, too. function set_flags() { local -n arr=$1 local choice="$3" local c= local f= if [ -z "$defaults" ]; then read -e -i "$choice" -p "$2" choice else echo "$2$choice" fi for c in $choice; do c=${c//[^0-9]} if [ -n "$c" ] && [ $c -ge 0 ] && [ $c -lt ${#arr[*]} ]; then arr[$c]="$(awk -F$'\t' -vOFS=$'\t' -vf=$4 '{$f=1;print}' <<<"${arr[$c]}")" if [ -n "$6" ]; then f=$(awk -F $'\t' '{print $1}' <<<"${s_info[$c]}") f_info[$f]="$( awk -F$'\t' -vOFS=$'\t' -vf=$6 '{$f=1;print}' <<<"${f_info[$f]}")" fi fi [ "$5" == "1" ] && break done } if [ ${#f_info[*]} -gt 0 ]; then echo FILES: print_info f_info '{ print NR-1, $1, $4==0?"":("(with " $4 " chapters)") }' if [ -z "$abort" ]; then set_flags f_info 'File from which chapters should be read (if any): ' \ "$(printf '%s\n' "${f_info[@]}" \ | awk -F$'\t' '$4>0{print NR-1; exit}')" 2 1 fi fi if [ ${#v_info[*]} -gt 0 ]; then echo VIDEO: print_info v_info ' BEGIN {print "scale=2" |& "bc -q"} { sub(/.$/,"",$7) print $15 |& "bc -q"; "bc -q" |& getline fps print NR-1, "file " $1, $3 ($4==""?"":(" (" $4 ")")), $5 "×" $6, \ $7==""?"":($7 $12 "×" $13), fps "fps", $17, $18 }' if [ -z "$abort" ]; then set_flags v_info 'Space-separated list of video streams to include: ' \ "$(eval echo \{0..$((${#v_info[*]}-1))\})" 19 fi fi if [ ${#a_info[*]} -gt 0 ]; then echo AUDIO: print_info a_info ' { print NR-1, "file " $1, $3 ($4==""?"":(" (" $4 ")")), \ $6 "×@" $5 "Hz", $8, $7 "kb/s", $13, $14, \ $10=="1"?"(default track)":"", $11=="1"?"(forced track)":"", \ $12=="1"?"(vision impaired)":"" }' if [ -z "$abort" ]; then set_flags a_info 'Space-separated list of audio streams to include: ' \ "$(eval echo \{0..$((${#a_info[*]}-1))\})" 15 set_flags a_info 'Default audio stream (if any): ' \ "$(printf '%s\n' "${a_info[@]}" \ | awk -F$'\t' '$10==1&&$15==1{print NR-1}' | head -n 1)" 16 1 set_flags a_info 'Forced audio streams (if any): ' \ "$(printf '%s\n' "${a_info[@]}" \ | awk -F$'\t' -vORS=' ' '$11==1&&$15==1{print NR-1}')" 17 fi fi if [ ${#s_info[*]} -gt 0 ]; then echo SUBTITLES: print_info s_info ' { print NR-1, "file " $1, $3, $8, $9, \ $5=="1"?"(default track)":"", $6=="1"?"(forced track)":"", \ $7=="1"?"(hearing impaired)":"" }' if [ -z "$abort" ]; then set_flags s_info 'Space-separated list of subtitle streams to include: ' \ "$(eval echo \{0..$((${#s_info[*]}-1))\})" 10 0 3 set_flags s_info 'Default subtitle stream (if any): ' \ "$(printf '%s\n' "${s_info[@]}" \ | awk -F$'\t' '$5==1&&$10==1{print NR-1}' | head -n 1)" 11 1 set_flags s_info 'Forced subtitle streams (if any): ' \ "$(printf '%s\n' "${s_info[@]}" \ | awk -F$'\t' -vORS=' ' '$6==1&&$10==1{print NR-1}')" 12 fi fi [ -z "$abort" ] || exit 0 ## ENCODE function split() { # needed because `read` merges adjacent field separators fields="$1"; shift while IFS=$'\n' read field; do [ $# -gt 0 ] && eval "${1}=\"$(sed 's/[\"$]/\\\0/g' <<<"$field")\""; shift done < <(tr '\t' '\n' <<<"$fields") } function run() { local err=0 for item in "$@"; do printf ' %q' "$item"; done; printf '\n' if [ -z "$fake" ]; then "$@" err=$? if [ $err -gt 1 ] || [ $err -gt 0 -a "$1" != mkvmerge ]; then echo "ERROR: $1 exited with code ${err}." >&2 error=$((error+1)) fi fi } function on_exit() { if [ $error -eq 0 ]; then run rm -f ${tmppfx}* else echo 'Errors were encountered. These files were not deleted:' >&2 printf '%s\n' ${tmppfx}* >&2 fi } trap on_exit EXIT #⇒ Video #⇒ http://labs.divx.com/node/16598 for v in "${v_info[@]}"; do split "$v" \ file idx codec profile w h stdop cropL cropT cropR cropB stdw stdh \ sar fps start lang title use [ $use -eq 1 ] || continue if [ $(bc <<<"$fps") -gt 25 ]; then fps=25 fi vopt_divx="--8x8dct --vbv-maxrate=20000 --vbv-bufsize=25000 --level 40 --bframes 3 --keyint $(bc <<<"4*$fps")" vopt="${preset:+--preset $preset }${tune:+--tune $tune }--fps $fps --sar $sar" [[ "$stdop" =~ crop ]] && stdop=${stdop/⇒/:$cropL,$cropT,$cropR,$cropB} [[ "$stdop" =~ resize ]] && stdop=${stdop/⇒/:$stdw,$stdh} [ -n "$stdop" ] && vopt="$vopt --vf ${stdop:0:-1}" if [[ "$x264qual" =~ pass ]]; then run nice -n 10 x264 $vopt $vopt_divx $x264qual 1 \ ${x264slow:+--slow-firstpass} -o $tmppfx.$file.$idx.mkv "${inputs[$file]}" run nice -n 10 x264 $vopt $vopt_divx $x264qual 2 \ -o $tmppfx.$file.$idx.mkv "${inputs[$file]}" else run nice -n 10 x264 $vopt $vopt_divx $x264qual \ -o $tmppfx.$file.$idx.mkv "${inputs[$file]}" fi done #⇒ Audio #⇒ https://trac.ffmpeg.org/wiki/Encode/AAC for a in "${a_info[@]}"; do split "$a" \ file idx codec profile sampfreq channels kbps layout start def force \ seehelp lang title use setdef setforce [ $use -eq 1 ] || continue if [ "$codec" == 'aac' ]; then run ffmpeg -loglevel warning -i "${inputs[$file]}" -vn -map 0:$idx \ -movflags +faststart -c:a copy $tmppfx.$file.$idx.aac else run ffmpeg -loglevel warning -i "${inputs[$file]}" -vn -map 0:$idx \ -movflags +faststart -c:a aac -b:a ${kbps}k $tmppfx.$file.$idx.aac fi done ## MUX DIVX PLUS HD #⇒ http://www.videoredo.net/msgBoard/archive/index.php/t-31105.html #⇒ https://github.com/mbunkus/mkvtoolnix/wiki/Playback-does-not-work-VLC-cannot-seek-mkvmerge-v5.9.0 mkvopt=( --engage no_cue_duration,no_cue_relative_position --clusters-in-meta-seek -o "$output" ) # subtitles & chapters for f in "${f_info[@]}"; do split "$f" path usechap usesub nbchap [ $usechap -eq 0 -a $usesub -eq 0 ] && continue if [ $usesub -eq 1 ]; then subs= newopt=() for s in "${s_info[@]}"; do split "$s" \ file idx codec start def force hearhelp lang title use setdef setforce [ $use -eq 1 ] || continue subs=$subs,$idx newopt=( "${newopt[@]}" --default-track $idx:$setdef --forced-track $idx:$setforce ) done newopt=( -A -D -s ${subs:1} -B -M --no-global-tags "${newopt[@]}" ) else newopt=( -A -D -S -T -B -M --no-global-tags ) fi if [ $usechap -eq 0 ]; then newopt[${#newopt[*]}]=--no-chapters fi mkvopt=( "${mkvopt[@]}" "${newopt[@]}" "$path" ) done # video for v in "${v_info[@]}"; do split "$v" \ file idx codec profile w h stdop cropL cropT cropR cropB stdw stdh \ sar fps start lang title use [ $use -eq 1 ] || continue newopt=( -A -d 0 -S -B -M --no-chapters --no-global-tags \ --default-track 0:0 --forced-track 0:0 \ --track-name 0:"${title:-$(sed 's#.*/##;s#\.[^.]*##' <<<"$output") ($lang)}" \ --language 0:$lang $tmppfx.$file.$idx.mkv ) mkvopt=( "${mkvopt[@]}" "${newopt[@]}" ) done # audio for a in "${a_info[@]}"; do split "$a" \ file idx codec profile sampfreq channels kbps layout start def force \ seehelp lang title use setdef setforce [ $use -eq 1 ] || continue newopt=( -a 0 -D -S -B -M --no-chapters --no-global-tags --default-track 0:$setdef --forced-track 0:$setforce --track-name 0:"${title:-$lang $layout}" --language 0:$lang ) if [ "$profile" == "HE-AAC" ]; then newopt[${#newopt[*]}]=--aac-is-sbr newopt[${#newopt[*]}]=0:1 fi mkvopt=( "${mkvopt[@]}" "${newopt[@]}" $tmppfx.$file.$idx.aac ) done # go! run mkvmerge "${mkvopt[@]}"