From 206600b1987a94d2de84086aeebad07ce9c6e713 Mon Sep 17 00:00:00 2001 From: Y Date: Sat, 23 Apr 2016 12:06:31 +0200 Subject: [PATCH] Current non-git state --- DivxPlusHD.sh | 469 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 469 insertions(+) create mode 100755 DivxPlusHD.sh diff --git a/DivxPlusHD.sh b/DivxPlusHD.sh new file mode 100755 index 0000000..197bc57 --- /dev/null +++ b/DivxPlusHD.sh @@ -0,0 +1,469 @@ +#!/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 libfdk_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[@]}"