DivxPlusHD.sh/DivxPlusHD.sh

470 lines
17 KiB
Bash
Raw Normal View History

2016-04-23 12:06:31 +02:00
#!/usr/bin/bash
# Needed:
# ffmpeg, ffprobe, mkvmerge, x264, bash, bc, awk, sed, sort, uniq, tr
# Mandatory:
# -i <file> [-i <file> …] 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 programs
# 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 <file> Output file (default: output.mkv).
# -t <temporary storage> Place where intermediate files will be stored.
# -A <kb/s> kb/s for each audio channel (default: 64).
# -V <kb/s> | -Q <quality> 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 <speed preset> Preset, among “ultrafast”, “superfast”,
# “veryfast”, “faster”, “fast”, “medium”,
# “slow”, “slower”, “veryslow”, “placebo”
# (the slower the better; default: medium).
# -T <type tuning> Kind of video, among “film”, “animation”,
# “grain”, “stillimage”, “psnr”, “ssim”,
# “fastdecode”, “zerolatency”.
# -W <max width> Maximum width (greater than 1920 is useless).
# -H <max height> Maximum height (greater than 1080 is useless).
# -C Crop black borders using ffmpegs 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)<mh) {
stdw=mw; stdh=int(h*mw/(w*8)+0.5)*8; if (stdh<240) stdh=240
} else {
stdw=int(w*mh/(h*8)+0.5)*8; stdh=mh; if (stdw<320) stdw=320
}
} else if (w<320 || h<240) {
stdop=stdop "resize⇒/"
if ((h*320/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
2016-04-23 12:06:31 +02:00
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[@]}"