This is a quite complex bash shell script to recode DVD VOB files to AVI using MEncoder, Avidemux and other tools. Automates crop detection, aspect ratio calc, dual audio muxing and subtitle ripping. I'm not saying this is all perfect of course…
#!/bin/bash function gethash() { key="$IN $OPTS" echo "::: gethash: initializing with key '$key'" >&2 ; #ugly: hash=`echo "$key" | md5sum | cut -f1 -d' '` hash=`echo "OPTS $OPTS" | sed 's/[^[:alnum:]]/_/g' ` echo "$hash" } function init() { if [ "$INIT_DONE" ]; then echo "+ init: done already" >&2; return; fi echo "::: init: initializing" >&2 ; hash=`gethash` echo "+ init: hash is '$hash'" >&2 ; basedir=`dirname "$IN"` basename=`basename "$IN"` export TMPDIR="$basedir/$basename.$hash.vob2avi" echo "+ init: TMPDIR is '$TMPDIR'" >&2 ; mkdir -p "$TMPDIR" if [ ! -d "$TMPDIR" ]; then echo "FATAL: cannot create directory: $TMPDIR" >&2 ; exit -1 fi INIT_DONE="yes" echo "+ init: done" >&2 ; } function setupaudio() { if [ "$DUMMYMODE" ]; then whiptail --title "ERROR" --msgbox "audio setup can not be done in DUMMY MODE!" 7 75 ; return fi init demux echo "::: setupaudio: setting up audio track configuration for '$IN'" >&2 ; pushd "$TMPDIR" > /dev/null if [ -f aids ]; then echo "+ setupaudio: done already" >&2; return; fi touch aids tracks=`find "." | egrep "audio_[0-9]+.ac3" | sort -n` for track in $tracks; do track=`basename $track` echo "+ querying user for $track" >&2 while true; do tagfile=`mktemp` if whiptail --title "choose action for track $track" --menu "" 24 75 17\ listen "Listen to this track" \ remove "Do not include this track in final avi" \ include "Include this track in final avi" \ first "Make this the first audio track" \ 2>"$tagfile" then tag=`cat "$tagfile"` rm -f "$tagfile" echo "+ setupaudio: user voted for menu item $tag with $track" >&2 ; case "$tag" in listen) echo "+ playing $track" >&2 #kaffeine "$track" #mplayer -audiofile "$track" "$IN" vlc "$track" ;; remove) echo "+ removing $track" >&2 newaids=`cat aids | grep -v $track` echo -e "$newaids" > aids break; ;; include) echo "+ including $track" >&2 echo $track >> aids echo "+ included $track" >&2 break; ;; first) echo "+ including $track as first" >&2 newaids=`cat aids | grep -v $track` echo $track > aids echo -e "$newaids" >> aids break; ;; esac else rm -f $tagfile break; fi done done popd >/dev/null } function demux() { if [ "$DUMMYMODE" ]; then whiptail --title "ERROR" --msgbox "demuxing can not be done in DUMMY MODE!" 7 75 ; return fi init if [ -f "$TMPDIR/demux_done" ]; then echo "+ demux: done already" >&2 ; return fi echo "::: demux: demultiplexing '$IN'" >&2 ; info=`info` videoids=`echo -e "$info" | egrep "^ID_VIDEO_ID=" | sort | uniq | cut -f2 -d"="` audioids=`echo -e "$info" | egrep "^ID_AUDIO_ID=" | sort | uniq | cut -f2 -d"="` #mplayer is unreliable: subtitleids=`echo -e "$info" | egrep "^ID_SUBTITLE_ID=" | sort | uniq | cut -f2 -d"="` maxsubtitleid=`echo -e "$info" | egrep "^detected \([0-9]+\) subtitle" | egrep -o "[0-9]+"` subtitleids=`seq 0 $(($maxsubtitleid - 1))` # debug #echo -e "$videoids" #echo -e "$audioids" #echo -e "$subtitleids" #echo -e "$info"; #return # /debug pushd "$TMPDIR" > /dev/null flagfile="demux_lock" rm -f "$flagfile" fifos="" #for id in $videoids; do # id=`printf "0x%02x" $(($id + 224))` # echo "+ demux: found video ID#$id" >&2 ; # fifo="video_$id.fifo" # mkfifo "$fifo" # fifos="$fifos $fifo" # ( # #wait for flagfile # while [ ! -f "$flagfile" ]; do sleep 1; done ; # tcdemux -i "$fifo" -A $id > "video_$id.vob" # ) & #done for id in $audioids; do #id=`printf "0x%02x" $id` id=`printf "%d" $(($id - 128))` echo "+ demux: found audio ID#$id" >&2 ; #cat "$fifo" | mplayer -quiet -vo null -ao null -aid $id -dumpaudio -dumpfile "audio_$id.ac3" - #tcextract -i "$IN" -x ac3 -a $id > "audio_$id.ac3" #mplayer -vo null -ao null -aid $id -dumpaudio -dumpfile "audio_$id.ac3" "$IN" #continue fifo="audio_$id.fifo" mkfifo "$fifo" fifos="$fifos $fifo" ( #wait for flagfile while [ ! -f "$flagfile" ]; do sleep 1; done ; #tcdemux -i "$fifo" -A $id > "audio_$id.vob" #mencoder "$fifo" -quiet -oac copy -aid $id -ovc frameno -o "$TMPDIR/audio_${id}.avi" #cat "$fifo" | tcextract -x ac3 -a $id > "audio_$id.ac3" cat "$fifo" | tcextract -t vob -x ac3 -a $id > "audio_$id.ac3" #cat "$fifo" | mplayer -quiet -vo null -ao null -aid $id -dumpaudio -dumpfile "audio_$id.ac3" - ) & done #return for id in $subtitleids; do id=`printf "0x%02x" $(($id + 32))` echo "+ demux: found subtitle ID#$id" >&2 ; #tcextract -i "$IN" -x ps1 -a $id > "subtitle_$id.ps1" #continue fifo="subtitle_$id.fifo" mkfifo "$fifo" fifos="$fifos $fifo" ( #wait for flagfile while [ ! -f "$flagfile" ]; do sleep 1; done ; #tcdemux -i "$fifo" -A $id > "subtitle_$id.vob" cat "$fifo" | tcextract -t vob -x ps1 -a $id > "subtitle_$id.ps1" ) & done touch "$flagfile" pv -N "demuxing" "$IN" | tee $fifos >/dev/null rm -f "$flagfile" rm -f $fifos popd > /dev/null touch "$TMPDIR/demux_done" echo "+ demux: done" >&2 ; } function doall() { allcmd encode } function encodeall() { allcmd encode } function allcmd() { cmd="$1" echo "::: executing command $cmd for all in "`pwd` >&2 ; find -type f\ | egrep -i '[.](vob|mpeg|mpg)$'\ | xargs -I"FILE" -n1 vob2avi.sh FILE "$OPTS" $cmd } function demuxall() { allcmd "demux" } function prepareall() { allcmd "prepare" } function info() { if [ "$DUMMYMODE" ]; then echo "ERROR: info called in DUMMY MODE" >&2 exit -1 fi init if [ -f "$TMPDIR/info" ]; then echo "+ info: done already" >&2 cat "$TMPDIR/info" return fi echo "::: info: initializing" >&2 echo "+ info: identifying $IN with mplayer" >&2 res=`mplayer -ao null -vo null -frames 500 -identify "$IN"` echo "+ info: identifying $IN with tcprobe" >&2 res="$res"`tcprobe -H 500 -i "$IN"` echo "+ info: saving info" >&2 echo -e "$res" > "$TMPDIR/info" echo "+ info: done" >&2 cat "$TMPDIR/info" } function prepare() { init if [ -f "$TMPDIR/prepared" ]; then echo "+ prepare: done already" >&2 return fi if [ "$DUMMYMODE" ]; then echo "ERROR: prepare called in DUMMY MODE" >&2 exit -1 fi echo "::: preparing '$IN'" >&2 ; demux setupaudio encode_real "-frames 500" audiomerge mplayer -vo xv -fs "$IN.avi" echo "prepared" > "$TMPDIR/prepared" } function encode() { init if [ -f "$TMPDIR/done" ]; then echo "+ encode: done already" >&2 return fi if [ "$DUMMYMODE" ]; then echo "ERROR: encode called in DUMMY MODE" >&2 exit -1 fi echo "::: encoding '$IN'" >&2 ; encode_real audiomerge echo "done" > "$TMPDIR/done" echo "::: DONE encoding '$IN'" >&2 ; } function audiosync() { echo "::: syncing audio for '$IN'" >&2 ; init setupaudio if [ -f "$TMPDIR/synced" ]; then echo "+ audiosync: done already" >&2 return fi aids=`cat "$TMPDIR/aids" | xargs -I"##" -n1 basename "##" .ac3 | cut -f2 -d"_" ` naids=`echo -e "$aids" | wc -l` info=`info` for x in `seq 0 $(($naids -1))`; do newaid=`echo $x` frametime=`echo -e "$info" | egrep -o 'frame_time=[+-]?[0-9]+' | cut -f2 -d'='` oldaid=`echo -e "$aids" | tail -n $(($naids - $x)) | head -n1` echo "+ oldaid:$oldaid newaid:$newaid" >&2 syncms=`echo -e "$info" | grep "audio track.*-a $oldaid" -A2 | egrep -o 'av_fine_ms [+-]?[0-9]+' | cut -f2 -d' '` syncfr=`echo -e "$info" | grep "audio track.*-a $oldaid" -A2 | egrep -o '\-D [+-]?[0-9]+' | cut -f2 -d' ' | tr -d " \t\n\r"` echo "+ syncfr: $syncfr" >&2 #sign=$(($syncfr < 0)) if [ $(( 1 + $syncfr )) -lt 1 ]; then sign="1" else sign="0" fi sync=`echo "scale=2 ; (($syncms ^ 2 / $frametime ^ 2)+0.5 >= 1) * (-1 * $sign) + $syncfr" | bc` echo "+ oldaid:$oldaid newaid:$newaid sync:$sync ($syncfr frames + $syncms ms [frametime:$frametime])" >&2 if [ ! "$sync" -eq "0" ]; then echo "+ track needs syncing. Doing it as $TMPDIR/synced.avi" >&2 avisync -i "$IN.avi" -o "$TMPDIR/synced.avi" -n $sync -a $newaid mv -f "$TMPDIR/synced.avi" "$IN.avi" fi done echo "done" > "$TMPDIR/synced" #echo -e "$aids\nn:$naids" #exit 0 } function audiomerge() { echo "::: merging audio for '$IN'" >&2 ; init vob="$IN" aids=`cat "$TMPDIR/aids"` echo "+ creating $TMPDIR/merged.avi with no sound" >&2 ; #cp -f "${vob}.avi" "$TMPDIR/merged.avi" mencoder -ovc copy -nosound "$vob.avi" -o "$TMPDIR/merged.avi" #return track=0 for aid in $aids; do if [ -f "$TMPDIR/$aid" ]; then echo "+ merging lang $TMPDIR/$aid as track $track" >&2 ; if avimerge -i "$TMPDIR/merged.avi" -o "$TMPDIR/merged_tmp.avi" -p "$TMPDIR/$aid" then mv -f "$TMPDIR/merged_tmp.avi" "$TMPDIR/merged.avi" else echo "+ ERROR merging lang $TMPDIR/$aid as track $track" >&2 ; fi track=$(($track + 1)) fi done mv -f "$TMPDIR/merged.avi" "${vob}.avi" audiosync } function encode_real() { echo "::: encoding '$IN'" >&2 ; addopts="$1" init crop=`cropdetect` scale=`scaledetect` vfopts=pp=de,crop=$crop,scale=$scale lavcopts=vcodec=mpeg4:vhq:vqmin=2:vbitrate=1800:threads=4:turbo opts="$addopts -vf-add kerndeint -oac copy -ovc lavc -vf-add $vfopts" # mencoder "$vob" $opts -oac copy -alang English -ovc frameno -o "${vob}_audio_en.avi" # mencoder "$vob" $opts -oac copy -alang Hungarian -ovc frameno -o "${vob}_audio_hu.avi" set -e pushd "$TMPDIR" > /dev/null echo "+ executing mencoder with args: $opts -lavcopts $lavcopts:vpass=1" >&2 mencoder "$IN" $opts -lavcopts $lavcopts:vpass=1 -o '/dev/null' echo "+ executing mencoder with args: $opts -lavcopts $lavcopts:vpass=2" >&2 mencoder "$IN" $opts -lavcopts $lavcopts:vpass=2 -o "$TMPDIR/final.avi" popd "$TMPDIR" set +e mv -f "$TMPDIR/final.avi" "$IN.avi" # unlink frameno.avi 2> /dev/null unlink $TMPDIR/divx2pass.log 2> /dev/null } function scaledetect() { echo "::: scale-detecting '$IN'" >&2 ; vob="$IN" init scalecache="$TMPDIR/scale" if [ -f "$scalecache" ]; then echo "+ reading scale data from $scalecache" >&2 scale=`cat "$scalecache"` echo "+ final scale parameters: $scale" >&2 echo "$scale" return fi info=`info` #echo -e "$info" #exit 0 res=`echo -e "$info" | egrep "^VIDEO:" | egrep -o "[0-9]+x[0-9]+"` case "$res" in 720x576) echo "+ video has PAL resolution, this is good" >&2 aspect=`echo -e "$info" | egrep "^Movie-Aspect" | egrep -o "[0-9.]+:[0-9.]+" | tr ':' '/'` #aspect="4/3" ow="720" oh="576" ;; *) echo "+ video has $res resolution, this is BAD - UNIMPLEMENTED" >&2 exit -1 ;; esac echo "+ video has $aspect aspect with $res resolution" >&2 crop=`cropdetect "$vob"` w=`echo $crop | cut -f1 -d":"` h=`echo $crop | cut -f2 -d":"` echo "+ cropped video has resolution ${w}x${h}" >&2 # original width: nw=$ow nh=`echo "scale=4;(1/($aspect))*($ow/$oh)*($w/$ow)*$h" | bc | cut -f1 -d'.'` # cropped width: #nw=$w #nh=`echo "scale=4;(1/($aspect))*($ow/$oh)*$h" | bc | cut -f1 -d'.'` scale="${nw}:${nh}" echo "+ storing final parameters to: $scalecache" >&2 echo $scale > "$scalecache" echo "+ final scale parameters: $scale" >&2 echo $scale echo "+ scale detect done." >&2 } function cropdetect() { echo "::: crop-detecting '$IN'" >&2 ; vob="$IN" init cropcache="$TMPDIR/crop" if [ -f "$cropcache" ]; then echo "+ reading crop data from $cropcache" >&2 bestcrop=`cat "$cropcache"` echo "+ final parameters: $bestcrop" >&2 echo $bestcrop return fi # info "$vob" # exit 0 echo "+ crop detecting movie with mplayer..." >&2 crops=`mktemp` for x in 2 5 10 20 30 50 100; do mplayer -vo null -ao null -frames 5 -ss $x:00 -vf cropdetect "$vob" 2>/dev/null | grep "\[CROP\]" | egrep -o "[0-9]+:[0-9]+:[0-9]+:[0-9]+" done > "$crops" cropss=`mktemp` cat "$crops" | while read crop; do x=`echo $crop | cut -f3 -d":"` y=`echo $crop | cut -f4 -d":"` xl=`echo $crop | cut -f1 -d":"` yl=`echo $crop | cut -f2 -d":"` echo $(( xl*yl)) $crop done > "$cropss" rm -f "$crops" bestcrop=`cat "$cropss" | sort -n | tail -n1 | cut -f2 -d' '` rm -f "$cropss" echo "+ best crop guessed by mplayer: $bestcrop" >&2 inputconf=`mktemp` echo "W change_rectangle 0 1" >> "$inputconf" echo "w change_rectangle 0 -1" >> "$inputconf" echo "E change_rectangle 1 1" >> "$inputconf" echo "e change_rectangle 1 -1" >> "$inputconf" echo "S change_rectangle 2 1" >> "$inputconf" echo "s change_rectangle 2 -1" >> "$inputconf" echo "D change_rectangle 3 1" >> "$inputconf" echo "d change_rectangle 3 -1" >> "$inputconf" cropfile=`mktemp` echo "+ fine tuning with mplayer... (use keys: wWeEsSdD)" >&2 mplayer "$vob" -input conf="$inputconf" -vf rectangle=$bestcrop 2>/dev/null | egrep -o "[0-9]+:[0-9]+:[0-9]+:[0-9]+" > "$cropfile" rm -f "$inputconf" bestcrop=`tail -n1 "$cropfile"` rm -f "$cropfile" echo "+ storing final parameters to: $cropcache" >&2 echo $bestcrop > "$cropcache" echo "+ final crop parameters: $bestcrop" >&2 echo $bestcrop echo "+ crop detect done." >&2 } #encode dvd.DVD_VIDEO.1216721459.01.vob 720:430:0:72 720:405 BANNER=`basename $0`" - GPLv3 - (c) 2008 by Erno Rigo <mcree_AT_tricon_DOT_hu>" # try to get to our input file if [ -z "$1" ]; then searchpath=`pwd` else if [ -d "$1" ]; then searchpath="$1" else IN="$1" fi fi if [ ! -z "$searchpath" ]; then echo "::: searching for INPUT candidates in $searchpath... " >&2 files=`find "$searchpath" -type f | egrep ".*[.](vob|mpeg|VOB|MPEG|Vob|Mpeg|mpg|MPG)$" 2>/dev/null` opts=`echo -e "$files" | while read file; do echo "'$file'" "'.'"; done` menutag=`mktemp` if echo whiptail --noitem --backtitle "$BANNER" --title "select INPUT file" \ --menu "" 24 75 17 -- \ "[Dummy mode]" "dummy"\ $opts \ 2>"$menutag"; then echo "::: selected tag: "`cat "$menutag"` >&2 IN=`cat "$menutag"` rm -f "$menutag" else echo "::: exiting on user request" >&2 rm -f "$menutag" exit 0; fi fi if [ ! -f "$IN" ]; then IN="Dummy mode!" DUMMYMODE="yes" else # calculate absolute path for IN wd=`dirname "$IN"` pushd "$wd" > /dev/null IN=`pwd`"/"`basename "$IN"` popd > /dev/null fi echo "WORKDIR: $wd, IN: $IN" OPTS="$2" if [ ! -z "$3" ]; then $3 exit 0 fi # MAIN MENU LOOP while true; do menutag=`mktemp` echo "::: entering main menu" >&2 if \ whiptail --backtitle "IN: '$IN'" --title "main menu" \ --menu "OPTS: '$OPTS'" 24 75 16 -- \ "demux" "- Demultiplex INPUT" \ "setupaudio" "- Setup audio tracks to be included in OUTPUT" \ 2>"$menutag"; \ then tag=`cat "$menutag"` rm -f "$menutag" echo "::: selected tag: $tag" >&2 $tag else echo "::: exiting on user request" >&2 rm -f "$menutag" exit 0; fi done # END MAIN MENU LOOP exit 0 opts=`cat<<EOF encode:#encode vob cropdetect:#try to detect crop parameters scaledetect:#try to detect scale parameters prepare:#prepare and preview encoding ripaudio:#rip audio tracks audiomerge:#rip audio tracks help#print this help deint#turn on deinterlacing [OBSOLETE - using kernel deint by default] ripsubs:#rip subtitles from the vob doall#search for prepared vobs in CWD (with the --test option) and do real encoding EOF ` # prepare long opts for getopt optsf=`mktemp` echo -e "$opts" | while read opt; do echo -ne ","`echo "$opt" | cut -f1 -d'#'` done | cut -b 2- > $optsf longopts=`cat $optsf` rm -f $optsf TEMP=`getopt -o "" --long $longopts -n "$0" -- "$@"` if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi # Note the quotes around `$TEMP': they are essential! eval set -- "$TEMP" showhelp="" iopts="" while true ; do case "$1" in --deint) iopts="-vf-add kerndeint" ; shift; ;; --encode) encode "$2" "$iopts"; shift 2 ; showhelp="false" ;; --cropdetect) cropdetect "$2" "$iopts" ; shift 2 ; showhelp="false" ;; --scaledetect) scaledetect "$2" "$iopts" ; shift 2 ; showhelp="false" ;; --prepare) testencode "$2" "$iopts"; shift 2 ; showhelp="false" ;; --ripaudio) ripaudio "$2" "$iopts" ; shift 2 ; showhelp="false" ;; --audiomerge) audiomerge "$2" "$iopts" ; shift 2 ; showhelp="false" ;; --ripsubs) ripsubs "$2" "$iopts" ; shift 2 ; showhelp="false" ;; --doall) doall; shift; showhelp="false" ;; --c-long) # c has an optional argument. As we are in quoted mode, # an empty parameter will be generated if its optional # argument is not found. case "$2" in "") echo "Option c, no argument"; shift 2 ;; *) echo "Option c, argument \`$2'" ; shift 2 ;; esac ;; --) shift ; break ;; *) echo "Internal error!" ; exit 1 ;; esac done #echo "Remaining arguments:" #for arg do echo '--> '"\`$arg'" ; done if [ -z "$showhelp" ]; then echo "usage: "`basename $0`" <OPT> <.VOB file>" echo "GPLv2 by Erno Rigo <mcree_AT_tricon.hu>" echo "" echo "OPT can be:" echo -e "$opts" | while read line; do opt=`echo "$line" | cut -f1 -d# | tr -d ':'` desc=`echo "$line" | cut -f2 -d#` helpline=`printf "%20s %s" "--$opt" "$desc"` echo -e "$helpline" done fi