#!/bin/sh # The following is a comment for Tcl, but a command for any UNIX shell \ exec tclsh "$0" ${1+"$@"} # Under MS WINDOWS just execute something like (or create a shortcut?) # "c:\Program Files\Tcl\bin\tclsh84.exe" "c:\My Files\video2mkv.tcl" # # some functions... # proc PrintHelp {progname} { puts "Pack any V/A-stream (i.e. HDTV-TS with H264+AC3) into a Matroska container and perform cuts at specified positions via the 'mkvmerge --split' feature. Usage: $progname \[ options \] You must specify at least one input file containing a video and audio stream which can be dumped by 'mplayer'. That way it's also possible to handle Transport Streams which normally can't be read with mkvmerge. You can also specify two elementary streams separated by an ampersand (&) (Videofile FIRST!) in which case only muxing/cutting occurs. Options can be abbreviated but EVERY option MUST start with a single dash. Possible options are: -aspect This sets the aspect ratio in the matroska file explicitely if given (can be a float or a ratio, i.e. 1.777 or 16/9) -cut Cut the input file at specified positions, keep every 2nd file and merge them together. has to be a comma separated list of either number of seconds specified as s or timecodes in the form HH:MM:SS.nnnnnnnnn, where the hours and the nanoseconds can be ommitted. -demuxonly Only use mplayer to demux to elementary streams -encode Encode the video stream to H264 with specified resolution and quantizer quality (less is better) or use the default (i.e. opt=\"720x576,20\") by supplying an empty list \"\". It may be necessary to use -aspect to write the correct display aspect ratio to the MKV file! -force Force overwriting of existing files without asking -help Print this help page -keep Keep temporary files (A/V streams and splitted files) -link Link files to one another when splitting (see mkvmerge) -out Specify path and name of output file, default is the rootname of the input file with extension \".mkv\" -quiet Don't print anything while converting -rate Set frames per second for a H264 stream (default: 25) -sync Add/subtract audio delay in ms (syntax like mkvmerge -y) -version Print version information" } proc ProbeVideo {file {ext "UNKNOWN"}} { # get correct type (-->extention) of a given video file or set default if {[catch {exec mkvmerge --identify $file} output]} { puts stderr "\nERROR: $output" exit 1 } else { # parse the info output to get the container type regexp {container:\s*([^\n\r]+)} $output match container #puts "Found Container: '$container'" # now get the list of known file types catch {exec mkvmerge --list-types} output set types [split $output \n] # and check each comment for the given container set parse 0 set highscore 0 set savescore 0 foreach type $types { # this marks end of list if {[regexp {modules:} $type]} {set parse 0} # now we are inside the listing of known containers/demultiplexers if {$parse} { regexp {\s+(\w+)\s+(.+)} $type match extension comment # look for the container name in the comments set score1 [SearchString $container $comment 100] # or better look for the extention in the container string? set score2 [SearchString $extension $container 100] if {$score1>$highscore} { set highscore $score1 set savescore $score2 set ext $extension } elseif {$score1==$highscore} { if {$score2>$savescore} { set savescore $score2 set ext $extension } elseif {$score2==$savescore} { puts "ProbeVideo: Found two possible matches for the file type," puts " '$ext' OR '$extension'...? Taking the first one!" } } } # this marks beginning of list if {[regexp {demultiplexers:} $type]} {set parse 1} } } return $ext } proc SearchString { s1 s2 {scale 1} } { # kind of a fuzzy substring match # first simplify the strings by changing to lower case # and deleting spaces or other special characters set s1 [string tolower $s1] set s2 [string tolower $s2] regsub -all {[^a-z0-9]+} $s1 "" s1 regsub -all {[^a-z0-9]+} $s2 "" s2 # get the length to choose the smaller one set l1 [string length $s1] set l2 [string length $s2] # exchange if s2 is smaller than s1 if {$l2<$l1} { set temp $s2 set s2 $s1 set s1 $temp set temp $l2 set l2 $l1 set l1 $temp } # simply compare each caracter of the smaller string with the characters # from the larger one starting at any position, take the max. sum of matches! set found 0 for {set i2 0} {$i2<[expr {1+$l2-$l1}]} {incr i2} { set match 0 for {set i1 0} {$i1<$l1} {incr i1} { set c1 [string index $s1 $i1] set c2 [string index $s2 [expr {$i2+$i1}]] if {$c1 eq $c2} {incr match} } if {$match > $found} {set found $match} } # this gives either 0 or 1 (or an integer percentage if scaled by 100) return [expr {round(double($scale*$found)/$l1)}] } # # default flags: # # just *de)mux? set demuxonly 0 set muxonly 0 # ask before overwriting existing files? set force 0 # delete temporary files? set keep 0 # be quiet set quiet 0 # frame rate (cannot be read from a h264 ESs) set rate "" # add audio delay (in ms with optional drift, same syntax as mkvmerge d[,o[/p]]) set sync "" # at least an input file name has to be specified set infile "" # output file is like input file name but with extension mp4 if not specified set outfile "" # cut output file? set cut "" set link 0 # transcode the video? set encode 0 set resolution "720x576" set quality 20 # that's only set in MKV if specified set aspect "" # print that version set version "0.2.4" # now parse the command line options set state flag foreach arg $argv { switch -- $state { flag { switch -glob -- $arg { -a* { set state aspect } -c* { set state cut } -d* { set demuxonly 1 } -e* { set state encode } -f* { set force 1 } -h* { PrintHelp [file tail $argv0]; exit 0; } -k* { set keep 1 } -l* { set link 1 } -o* { set state outfile } -q* { set quiet 1 } -r* { set state rate } -s* { set state sync } -v* { puts "[file tail $argv0] v$version"; exit 0; } -* { puts "*** ERROR: Unknown option/flag $arg"; exit 1; } default { # list of two ESs or just one TS/PS? if {[regexp {(.*)&(.*)} $arg infile video audio]} { set muxonly 1 } else { set infile $arg } } } } outfile { set outfile $arg set state flag } rate { append rate $arg "fps" set state flag } sync { set sync $arg set state flag } cut { set cut $arg set state flag } aspect { # can be a float or a ratio (with / NOT :!) regexp {\d+/\d+} $arg aspect regexp {\d+\.\d*} $arg aspect set state flag } encode { set encode 1 # use mkvmerge specification scheme (WxH) and translate to mencoder style (W:H) later regexp {\d+x\d+} $arg resolution # just an integer for the quantizer quality (only one will match) regexp {,(\d+)$} $arg match quality regexp {,(\d+),} $arg match quality regexp {^(\d+),} $arg match quality set state flag } } } # check input files if {![string length $infile]} { puts "Pack Video/Audio-Streams into a Matroska container" puts "Usage: [file tail $argv0] \[ -c -d -e -f -h -k -l -o -q -r -s \] " puts "You must specify at least an input file or use -h to get some help!" exit 0 } if {!$muxonly} { if {![file exists $infile]} { puts stderr "*** ERROR: $infile does not exist!" exit 1 } } else { if {![file exists $video]} { puts stderr "*** ERROR: $video does not exist!" exit 1 } if {![file exists $audio]} { puts stderr "*** ERROR: $audio does not exist!" exit 1 } } # check output files if {![string length $outfile]} { set filename [file rootname $infile] append outfile $filename ".mkv" } else { set filename [file rootname $outfile] } # just temporary names for dumping and renamed later if {!$muxonly} { append video $filename ".VIDEO" append audio $filename ".AUDIO" } # overwrite files? if {!$force} { if {[file exists $outfile]} { puts -nonewline "$outfile already existing! Delete? \[y/N\]:" flush stdout if {[regexp {^[yY]} [gets stdin]]} { file delete $outfile } else { puts "OK,... please delete or rename it by yourself!" exit 1 } } if {[file exists $video] && !$muxonly} { puts -nonewline "Use existing file $video? \[Y/n\]:" flush stdout if {[regexp {^[nN]} [gets stdin]]} {file delete $video} } if {[file exists $audio] && !$muxonly} { puts -nonewline "Use existing file $audio? \[Y/n\]:" flush stdout if {[regexp {^[nN]} [gets stdin]]} {file delete $audio} } } else { file delete $outfile file delete $video file delete $audio } # demux video if {![file exists $video] && !$muxonly} { if {!$quiet} {puts -nonewline "MPLAYER: $infile --> VIDEOSTREAM..."; flush stdout} # can't really catch the errors of mplayer because it's always complaining!!! catch {exec mplayer -really-quiet -dumpvideo -dumpfile $video $infile} result if {[file exists $video]} { if {!$quiet} {puts " dumping of video finished!"} } else { puts stderr "\n*** ERROR: something's wrong dumping video!" exit 1 } # ARGHHHH! mencoder does only encode/recognize the dumped video if the extention is set correct!!! set video_ext [ProbeVideo $video] if {$video_ext ne "UNKNOWN"} { set video_old $video set video $filename append video "." $video_ext file rename -force $video_old $video } elseif {$encode} { puts stderr "\n*** ERROR: can't encode video because of unknown file type!" set encode 0 } # trans/recode video if {$encode} { # exchange old with new file set video_orig $filename append video_orig "_orig." $video_ext file rename -force $video $video_orig # mencoder has a different specification for resolution! set res [string map {x :} $resolution] set x264encopts "-x264encopts qp=$quality:subq=6:partitions=all:8x8dct:me=umh:frameref=5:bframes=3:b_pyramid:weight_b:threads=1" set cmd "exec mencoder \"$video_orig\" -really-quiet -o \"$video\" -of rawvideo -nosound -vf scale=$res -ovc x264 $x264encopts" # now do the time consuming work if {!$quiet} { puts "MENCODER: $video --> H264@$resolution,qp=$quality..." puts -nonewline " THIS MAY TAKE SOME TIME..." flush stdout } # can't catch every warning... catch {eval $cmd} result if {[file exists $video]} { if {!$quiet} {puts " encoding of video finished!"} # delete the old video file? if {!$keep} {file delete $video_orig} } else { puts stderr "\n*** ERROR: something's wrong encoding video!" exit 1 } } } # demux audio if {![file exists $audio] && !$muxonly} { if {!$quiet} {puts -nonewline "MPLAYER: $infile --> AUDIOSTREAM..."; flush stdout} # can't really catch the errors of mplayer because it's always complaining!!! catch {exec mplayer -really-quiet -dumpaudio -dumpfile $audio $infile} result if {[file exists $audio]} { if {!$quiet} {puts " dumping of audio finished!"} } else { puts stderr "\n*** ERROR: something's wrong dumping audio!" exit 1 } set audio_ext [ProbeVideo $audio] if {$audio_ext ne "UNKNOWN"} { set audio_old $audio set audio $filename append audio "." $audio_ext file rename -force $audio_old $audio } else { puts stderr "\n*** ERROR: Unknown audio type! Let's see what happens when muxing..." } } # mux all together (and split?) if {!$demuxonly} { # build the command line set cmd "exec mkvmerge -q -o \"$outfile\"" if {[string length $aspect]} {append cmd " --aspect-ratio 0:$aspect"} if {[string length $rate]} {append cmd " --default-duration 0:$rate"} append cmd " -d 0 -A -S \"$video\"" if {[string length $sync]} {append cmd " --sync 0:$sync"} append cmd " -a 0 -D -S \"$audio\" --track-order 0:0,1:0" # is cutting requested? if {[string length $cut]} { append cmd " --split timecodes:$cut" if {$link} {append cmd " --link"} if {!$quiet} {puts -nonewline "MKVMERGE: $video&$audio --> $filename-001.mkv, $filename-002.mkv,..."; flush stdout} } else { if {!$quiet} {puts -nonewline "MKVMERGE: $video&$audio --> $outfile..."; flush stdout} } # now execute the command for muxing/splitting if {[catch {eval $cmd} result]} { puts stderr "\n*** ERROR: $result" if {![file exists $outfile]} {exit 1} } # just append all splitted even numbered matroska files to one new matroska file! if {[string length $cut]} { if {!$quiet} {puts " splitting finished!"} set pattern "$filename-\[0-9\]\[0-9\]\[0,2,4,6,8\].mkv" set cutfiles [join [glob -nocomplain $pattern] " +"] set cmd "exec mkvmerge -q -o $outfile $cutfiles" if {!$quiet} {puts -nonewline "MKVMERGE: $filename-002.mkv, $filename-004.mkv,... --> $outfile..."; flush stdout} if {[catch {eval $cmd} result]} { puts stderr "\nERROR: $result" if {![file exists $outfile]} {exit 1} } else { if {!$quiet} {puts " concatenation finished!"} } } else { if {!$quiet} {puts " conversion finished!"} } } # delete temporary files if {!$keep} { file delete $video file delete $audio set pattern "$filename-\[0-9\]\[0-9\]\[0-9\].mkv" foreach file [glob -nocomplain $pattern] { file delete $file } }