DivxPlusHD.sh/DivxPlusHD.sh

470 lines
17 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/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
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[@]}"