export trunc to global scope and extend
[undvd.git] / common.pm
blobc7d35b81838b3a3d62c5c7c7dd8cd2317016d457
1 # Author: Martin Matusiak <numerodix@gmail.com>
2 # Licensed under the GNU Public License, version 3.
4 package common;
6 use strict;
7 use File::Basename;
9 use colors;
11 use base 'Exporter';
12 our @EXPORT_OK = qw($suite $defaults $tools);
13 our @EXPORT = qw(
14 nonfatal
15 fatal
16 trunc
18 deep_copy
19 init_logdir
20 run
21 init_cmds
22 print_tool_banner
23 print_version
24 compute_bpp
25 set_bpp
26 compute_vbitrate
27 examine_dvd_for_titlecount
28 examine_title
29 crop_title
30 print_title_line
31 scale_title
32 compute_media_size
33 set_container_opts
34 set_acodec_opts
38 ### DECLARATIONS
40 our $suite = {
41 name => "undvd",
42 version => "0.6.1",
43 tool_name => basename(grep(-l, $0) ? readlink $0 : $0),
46 our $defaults = {
47 logdir => "logs",
49 dvd_device => "/dev/dvd",
50 disc_image => "disc.iso",
51 mencoder_source => "disc.iso",
53 framesize_baseline => 720*576*(2/3)^2, # frame size in pixels
55 h264_1pass_bpp => .195,
56 h264_2pass_bpp => .150,
58 xvid_1pass_bpp => .250,
59 xvid_2pass_bpp => .200,
61 container => "avi",
65 my @videoutils = qw(lsdvd mencoder mplayer);
66 my @shellutils = qw(awk bash bc grep egrep getopt mount ps sed xargs);
67 my @coreutils = qw(cat date dd dirname head mkdir mv nice readlink rm seq sleep sort tail tr true);
68 my @extravideoutils = qw(mp4creator mkvmerge ogmmerge vobcopy);
70 my @mencoder_acodecs = qw(copy faac lavc mp3lame);
71 my @mencoder_vcodecs = qw(copy lavc x264 xvid);
73 my @mplayer_acodecs = qw(ac3);
74 my @mplayer_vcodecs = qw(mpeg-2);
76 our $tools = {};
77 init_cmds();
80 ### FUNCTIONS
82 # non fatal error
83 sub nonfatal {
84 my $s = shift;
86 my $p = \&s_err;
87 my $em = \&s_it;
89 my $ms;
90 while ($s =~ m/(%%%.*?%%%)/g) {
91 $ms .= $p->(substr($s, 0, @-[0]));
92 $ms .= $em->($&);
93 $s = substr($s, @+[0]);
95 $ms .= $p->($s);
96 $ms =~ s/%%%//g;
98 print $p->("Error:") . " $ms\n";
101 # fatal error
102 sub fatal {
103 nonfatal($_[0]);
104 exit 1;
107 # truncate text
108 sub trunc {
109 my ($width, $side, $s, $fill) = @_;
111 my $trunc_len = length($s) - $width;
112 $s = substr($s, 0, $width);
114 substr($s, length($s) - length($fill), length($fill), $fill)
115 if (($trunc_len > 0) and $fill);
117 my $pad_len = abs($width - length($s));
118 my $pad = " " x $pad_len;
120 $s = $pad . $s if $side == -1;
121 $s = $s . $pad if $side == 1;
123 return $s;
126 # print object
127 sub p {
128 my @these = @_;
129 foreach my $this (@these) {
130 if (ref $this eq "ARRAY") {
131 print "dump: ".join(" , ", @$this)."\n";
132 } else {
133 use Data::Dumper;
134 print Dumper($this);
139 # deep copy objects
140 sub deep_copy {
141 my $this = shift;
142 if (not ref $this) {
143 $this;
144 } elsif (ref $this eq "ARRAY") {
145 [map deep_copy($_), @$this];
146 } elsif (ref $this eq "HASH") {
147 +{map { $_ => deep_copy($this->{$_}) } keys %$this};
148 } else { die "what type is $_?" }
152 # create directory for logging
153 sub init_logdir {
154 if (! -e $defaults->{logdir} and ! mkdir($defaults->{logdir})) {
155 fatal("Could not write to %%%$ENV{PWD}%%%, exiting");
156 } elsif (-e $defaults->{logdir} and ! -w $defaults->{logdir}) {
157 fatal("Logging directory %%%".$defaults->{logdir}."%%% is not writable");
161 # extremely suspicious
162 sub run {
163 my (@args) = @_;
165 my ($out, $exit, $err);
166 print join(' ', @_)."\n" if $ENV{"DEBUG"};
168 use IPC::Open3;
169 my $pid = open3(\*WRITER, \*READER, \*ERROR, @args);
170 wait;
171 $exit = $? >> 8;
173 while (my $output = <READER>) { $out .= $output; }
174 while (my $output = <ERROR>) { $err .= $output; }
176 chomp($out);
177 chomp($err);
179 return ($out, $exit, $err);
182 # check for missing dependencies
183 sub init_cmds {
184 my $verbose = shift;
186 print " * Checking for tool support...\n" if $verbose;
187 foreach my $tool (@videoutils, @shellutils, @coreutils, @extravideoutils) {
188 my ($tool_path, $exit, $err) = run("which", $tool);
189 $tools->{$tool} = $tool_path;
190 if (! $exit) {
191 print " " . s_ok("*") . " $tool_path\n" if $verbose;
192 } else {
193 print " " . s_wa("*") . " $tool missing\n" if $verbose;
197 sub codec_check {
198 my $type = shift;
199 my $codecs = shift;
200 my $tool = shift;
201 my @args = @_;
203 print " * Checking for $tool $type codec support...\n";
205 unshift(@args, $tools->{$tool});
206 my ($out, $exit, $err) = run(@args);
207 foreach my $codec (@$codecs) {
208 if ($out . $err =~ /$codec/i) {
209 print " " . s_ok("*") . " $codec\n";
210 } else {
211 print " " . s_wa("*") . " $codec missing\n";
216 if ($verbose) {
217 codec_check("audio", \@mplayer_acodecs, "mplayer", qw(-ac help));
218 codec_check("video", \@mplayer_vcodecs, "mplayer", qw(-vc help));
219 codec_check("audio", \@mencoder_acodecs, "mencoder", qw(-oac help));
220 codec_check("video", \@mencoder_vcodecs, "mencoder", qw(-ovc help));
224 # print standard common banner
225 sub print_tool_banner {
226 print "{( --- " . $suite->{tool_name} . " " . $suite->{version} . " --- )}\n";
229 # print package version and versions of tools
230 sub print_version {
231 sub check_tool {
232 my $tool = shift;
233 my $re = shift;
234 my @args = @_;
236 my ($tool_path, $exit) = run("which", $tool);
237 if ($exit) {
238 print " [" . s_err("!") . "] $tool missing\n";
239 } else {
240 unshift(@args, $tool_path);
241 my ($out, $exit, $err) = run(@args);
242 my $version = $1 if ($out . $err) =~ /$re/ms;
243 print " [" . s_ok("*") . "] $tool $version\n";
246 print $suite->{name} . " " . $suite->{version} . "\n";
247 check_tool("mplayer", "^MPlayer ([^ ]+)", qw());
248 check_tool("mencoder", "^MEncoder ([^ ]+)", qw(-oac help));
249 check_tool("lsdvd", "^lsdvd ([^ ]+)", qw(-V));
250 check_tool("vobcopy", "^Vobcopy ([^ ]+)", qw(--version));
251 check_tool("mp4creator", ".* version ([^ ]+)", qw(-version));
252 check_tool("mkvmerge", "^mkvmerge ([^ ]+)", qw(--version));
253 check_tool("ogmmerge", "^ogmmerge ([^ ]+)", qw(--version));
254 exit;
257 # compute bits per pixel
258 sub compute_bpp {
259 my $width = shift;
260 my $height = shift;
261 my $fps = shift;
262 my $length = shift;
263 my $video_size = shift; # in mb
264 my $bitrate = shift; # kbps
266 if ($bitrate) {
267 $bitrate = $bitrate * 1024;
268 } else {
269 $video_size = $video_size * 1024 * 1024;
270 $bitrate = (8 * $video_size)/( $length != 0 ? $length : 1 );
272 my $bpp = ($bitrate)/( $width*$height*$fps != 0 ? $width*$height*$fps : 1);
274 return $bpp;
277 # set bpp based on the codec and number of passes
278 sub set_bpp {
279 my ($video_codec, $passes) = @_;
281 my $bpp;
282 if ($video_codec eq "h264") {
283 $bpp = $defaults->{h264_1pass_bpp} if $passes == 1;
284 $bpp = $defaults->{h264_2pass_bpp} if $passes > 1;
285 } else {
286 $bpp = $defaults->{xvid_1pass_bpp} if $passes == 1;
287 $bpp = $defaults->{xvid_2pass_bpp} if $passes > 1;
290 return $bpp;
293 # set the number of passes based on codec and bpp
294 sub set_passes {
295 my ($video_codec, $bpp) = @_;
297 my $passes = 1;
298 if ($video_codec eq "h264") {
299 $passes = 2 if $bpp < $defaults->{h264_1pass_bpp};
300 } else {
301 $passes = 2 if $bpp < $defaults->{xvid_1pass_bpp};
304 return $passes;
307 # compute video bitrate based on title length
308 sub compute_vbitrate {
309 my ($width, $height, $fps, $bpp) = @_;
311 my $bitrate = int( ($width * $height * $fps * $bpp) / 1024);
313 return $bitrate;
316 # extract number of titles from dvd
317 sub examine_dvd_for_titlecount {
318 my $source = shift;
320 my @args = ($tools->{mplayer}, "-ao", "null", "-vo", "null");
321 push(@args, "-frames", "0", "-identify");
322 push(@args, "-dvd-device", $source, "dvd://");
324 my ($out, $exit, $err) = run(@args);
325 my $titles = $1 if ($out . $err) =~ /^ID_DVD_TITLES=([^\s]+)/ms;
327 return $titles;
330 # extract information from file or dvd title
331 sub examine_title {
332 my $file = shift;
333 my $dvd_device = shift;
335 my @source = ($file);
336 if ($dvd_device) {
337 push (@source, "-dvd-device", $dvd_device);
339 my @args = ($tools->{mplayer}, "-ao", "null", "-vo", "null");
340 push(@args, "-frames", "0", "-identify");
341 push(@args, @source);
343 my ($out, $exit, $err) = run(@args);
345 sub find {
346 my $default = shift;
347 my $s = shift;
348 my $re = shift;
350 my @match = map { /^${re}$/ } split('\n', $s);
351 if (@match) {
352 @match = sort {$b <=> $a} @match;
353 return shift(@match);
354 } else { return $default; }
357 my $s = $out . $err;
358 my $data = {
359 filename => $file,
360 width => find(0, $s, "ID_VIDEO_WIDTH=(.+)"),
361 height => find(0, $s, "ID_VIDEO_HEIGHT=(.+)"),
362 fps => find(0, $s, "ID_VIDEO_FPS=(.+)"),
363 length => find(0, $s, "ID_LENGTH=(.+)"),
364 abitrate => find(0, $s, "ID_AUDIO_BITRATE=(.+)"),
365 aformat => lc(find(0, $s, "ID_AUDIO_CODEC=(.+)")),
366 vbitrate => find(0, $s, "ID_VIDEO_BITRATE=(.+)"),
367 vformat => lc(find(0, $s, "ID_VIDEO_FORMAT=(.+)")),
370 $data->{abitrate} = int($data->{abitrate} / 1024); # to kbps
371 $data->{vbitrate} = int($data->{vbitrate} / 1024); # to kbps
372 $data->{bpp} = compute_bpp($data->{width}, $data->{height}, $data->{fps},
373 $data->{len}, 0, $data->{vbitrate});
375 if ($dvd_device) {
376 $data->{filesize} = int(
377 ($data->{abitrate} + $data->{vbitrate}) * $data->{length} / 8 / 1024);
378 } else {
379 $data->{filesize} = int( (stat($file))[7] / 1024 / 1024 );
382 return $data;
385 # figure out how much to crop
386 sub crop_title {
387 my ($file, $dvd_device) = @_;
389 my @source = ($file);
390 if ($dvd_device) {
391 push (@source, "-dvd-device", $dvd_device);
393 my @args = ($tools->{mplayer}, "-quiet", "-ao", "null", "-vo", "null");
394 push(@args, "-fps", "10000", "-vf", "cropdetect");
395 push(@args, @source);
397 my ($out, $exit, $err) = run(@args);
399 my @cropdata = map { /^(\[CROP\].*)$/ } split("\n", $out . $err);
400 my $cropline = pop(@cropdata);
402 my ($w, $h, $x, $y) =
403 map { /-vf crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)/ } $cropline;
405 my @cropfilter = ("-vf", "crop=$w:$h:$x:$y");
407 return ($w, $h, @cropfilter);
410 # set formatting of bpp output depending on value
411 sub markup_bpp {
412 my $bpp = shift;
413 my $video_codec = shift;
415 if (($video_codec =~ "(h264|avc)")) {
416 if ($bpp < $defaults->{h264_2pass_bpp}) {
417 $bpp = s_err($bpp);
418 } elsif ($bpp > $defaults->{h264_1pass_bpp}) {
419 $bpp = s_wa($bpp);
420 } else {
421 $bpp = s_bb($bpp);
423 } elsif (($video_codec =~ "xvid")) {
424 if ($bpp < $defaults->{xvid_2pass_bpp}) {
425 $bpp = s_err($bpp);
426 } elsif ($bpp > $defaults->{xvid_1pass_bpp}) {
427 $bpp = s_wa($bpp);
428 } else {
429 $bpp = s_bb($bpp);
431 } else {
432 $bpp = s_b($bpp);
435 return $bpp;
438 # print one line of title display, whether header or not
439 sub print_title_line {
440 my $is_header = shift;
441 my $data = shift;
443 my ($dim, $fps, $length, $bpp, $passes, $vbitrate, $vformat, $abitrate, $aformat);
444 my ($filesize, $filename);
446 if ($is_header) {
447 $dim = "dim";
448 $fps = "fps";
449 $length = "length";
450 $bpp = "bpp";
451 $passes = "p";
452 $vbitrate = "vbitrate";
453 $vformat = "vcodec";
454 $abitrate = "abitrate";
455 $aformat = "acodec";
456 $filesize = "size";
457 $filename = "title";
458 } else {
459 my $x = $data->{width} > 0 ? $data->{width} : "";
460 my $y = $data->{height} > 0 ? $data->{height} : "";
461 $dim = $x."x".$y ne "x" ? $x."x".$y : "";
462 $fps = $data->{fps} > 0 ? $data->{fps} : "";
463 $length = $data->{length} > 0 ? int($data->{length} / 60) : "";
464 $bpp = $data->{bpp} < 1 ? substr($data->{bpp}, 1) : $data->{bpp};
465 $passes = $data->{passes} > 0 ? $data->{passes} : "";
466 $vbitrate = $data->{vbitrate} > 0 ? $data->{vbitrate} : "";
467 $vformat = $data->{vformat} ne "0" ? $data->{vformat} : "";
468 $abitrate = $data->{abitrate} > 0 ? $data->{abitrate} : "";
469 $aformat = $data->{aformat} ne "0" ? $data->{aformat} : "";
470 $filesize = $data->{filesize};
471 $filename = $data->{filename};
474 $dim = trunc(9, -1, $dim);
475 $fps = trunc(6, -1, $fps);
476 $length = trunc(3, -1, $length);
477 $bpp = trunc(4, 1, $bpp);
478 $passes = trunc(1, -1, $passes);
479 $vbitrate = trunc(4, -1, $vbitrate);
480 $vformat = trunc(4, -1, $vformat);
481 $abitrate = trunc(4, -1, $abitrate);
482 $aformat = trunc(4, -1, $aformat);
483 $filesize = trunc(4, -1, $filesize);
485 if ($filename =~ /dvd:\/\//) {
486 $filesize = s_est($filesize);
489 $bpp = markup_bpp($bpp, $vformat) unless $is_header;
491 my $line = "$dim $fps $length $bpp $passes $vbitrate $vformat "
492 . "$abitrate $aformat $filesize $filename\n";
493 $line = s_b($line) if $is_header;
494 print $line;
497 # compute title scaling
498 sub scale_title {
499 my ($width, $height, $custom_scale) = @_;
501 my ($nwidth, $nheight) = ($width, $height);
503 if ($custom_scale ne "off") { # scaling isn't disabled
505 # scale to the width given by user (upscaling permitted)
506 if ($custom_scale) {
507 undef $nwidth;
508 undef $nheight;
510 if ($custom_scale =~ /^([0-9]+)$/) {
511 $nwidth = $1;
512 } elsif ($custom_scale =~ /^([0-9]*):([0-9]*)$/) {
513 ($nwidth, $nheight) = ($1, $2);
514 } else {
515 fatal("Failed to read a pair of positive integers from scaling "
516 . "%%%$custom_scale%%%");
519 if ( $nwidth > 0 and ! $nheight > 0) {
520 $nheight = int($height * $nwidth / ($width > 0 ? $width : 1) );
521 } elsif (! $nwidth > 0 and $nheight > 0) {
522 $nwidth = int($width * $nheight / ($height > 0 ? $height : 1) );
525 # apply default scaling heuristic
526 } else {
527 # compute scaling factor based on baseline value
528 my $framesize = $width*$height > 0 ? $width*$height : 1;
529 my $factor = sqrt($defaults->{framesize_baseline}/$framesize);
531 # scale by factor, do not upscale
532 if ($factor < 1) {
533 $nwidth = int($width*$factor);
534 $nheight = int($height*$factor);
538 # dimensions have been changed, make sure they are multiples of 16
539 ($nwidth, $nheight) = scale_by_x($width, $height, $nwidth, $nheight);
541 # make sure the new dimensions are sane
542 if ($nwidth * $nheight <= 0) {
543 ($nwidth, $nheight) = ($width, $height);
547 return ($nwidth, $nheight);
550 # scale dimensions to nearest (lower/upper) multiple of 16
551 sub scale_by_x {
552 my ($orig_width, $orig_height, $width, $height) = @_;
553 my $divisor = 16;
555 # if the original dimensions are not multiples of 16, no amount of scaling
556 # will bring us to an aspect ratio where the smaller dimensions are
557 if (($orig_width % $divisor) + ($orig_height % $divisor) != 0) {
558 $width = $orig_width;
559 $height = $orig_height;
560 } else {
561 my $step = -1;
562 my $completed;
563 while (! $completed) {
564 $step++;
566 my $up_step = $width + ($step * $divisor);
567 my $down_step = $width - ($step * $divisor);
568 foreach my $x_step ($up_step, $down_step) {
569 my $x_width = int($x_step - ($x_step % $divisor));
570 my $x_height = int($x_width *
571 ($orig_height/ ($orig_width > 0 ? $orig_width : 1) ));
572 if (($x_width % $divisor) + ($x_height % $divisor) == 0) {
573 $completed = 1;
574 $width = $x_width;
575 $height = $x_height;
581 return ($width, $height);
584 # compute size of media given length and bitrate
585 sub compute_media_size {
586 my ($length, $bitrate) = @_;
587 return ($bitrate / 8) * ($length / 1024);
590 # get container options and decide on codecs
591 sub set_container_opts {
592 my ($acodec, $vcodec, $container) = @_;
594 my $audio_codec = "mp3";
595 my $video_codec = "h264";
596 my $ext = "avi";
597 my @opts = ("avi");
599 if ($container =~ /(avi|mkv|ogm)/) {
600 } elsif ($container eq "mp4") {
601 $audio_codec = "aac";
602 $video_codec = "h264";
603 } else {
605 # use lavf muxing
606 if ($container =~ "(asf|au|dv|flv|ipod|mov|mpg|nut|rm|swf)") {
607 $ext = $container;
608 @opts = ("lavf", "-lavfopts", "format=$container");
610 if ($container eq "flv") {
611 $audio_codec = "mp3";
612 $video_codec = "flv";
614 } else {
615 fatal("Unrecognized container %%%$container%%%");
619 $audio_codec = $acodec if $acodec;
620 $video_codec = $vcodec if $vcodec;
622 return ($audio_codec, $video_codec, $ext, @opts);
625 # get audio codec options
626 sub set_acodec_opts {
627 my ($container, $codec, $orig_bitrate, $get_bitrate) = @_;
629 my @opts;
630 if ($container eq "flv"){
631 push(@opts, "-srate", "44100"); # flv supports 44100, 22050, 11025
634 my $bitrate;
635 if ($codec eq "copy") {
636 $bitrate = $orig_bitrate;
637 push(@opts, "copy");
638 } elsif ($codec eq "mp3") {
639 $bitrate = 160;
640 push(@opts, "mp3lame", "-lameopts", "vbr=3:$bitrate:q=3");
641 } elsif ($codec eq "aac") {
642 $bitrate = 192;
643 push(@opts, "faac", "-faacopts", "br=$bitrate:mpeg=4:object=2",
644 "-channels", "2");
646 # use lavc codec
647 } else {
648 $bitrate = 224; # mencoder manpage default
649 my $cs = "ac3|flac|g726|libamr_nb|libamr_wb|mp2|roq_dpcm|sonic|sonicls|"
650 . "vorbis|wmav1|wmav2";
651 if ($codec =~ /($cs)/) {
652 push(@opts, "lavc", "-lavcopts",
653 "abitrate=$bitrate:acodec=$codec");
654 } else {
655 fatal("Unrecognized audio codec %%%$codec%%%");
659 if ($get_bitrate) {
660 return $bitrate;
661 } else {
662 return @opts;