ubuntuusers.de

Unbenannt

Autor:
cornix
Datum:
31. Januar 2011 22:36
Code:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
#!/usr/bin/perl

our $VERSION = '0.3';

# dhyana.pl/0.3 (20080106)
#
# 	* Added differently coloured title
# 	* Use Getopt to parse command line
# 	* Improved handling of certain dodgy WMV files
# 	* Improved use of FFMPEG
# 	* Code straightened out to use functions
# 	* [Matt Pinkham] Matt's capture mode
#	* Added lots of help.
#
# dhyana.pl/0.2 (20071118)
#
# 	* Workarounds for odd PNG files
#	* Added 'auto' geometry.
#
# dhyana.pl/0.1 (20070819)
#
# 	* Initial release


# Silly tests.
die "This program cannot run if 00000001.png exists.\n" if (-e '00000001.png');
die "This program cannot run if 00000002.png exists.\n" if (-e '00000002.png');
die "This program cannot run if 00000001.gif exists.\n" if (-e '00000001.gif');

use Getopt::ArgvFile qw(argvFile);
use Getopt::Long 2.33 qw(GetOptions); 
use Pod::Usage;
use POSIX;
use strict;

# Paths to stuff.
our %path;
$path{convert}  = '/usr/bin/convert';
$path{ffmpeg}   = '/usr/bin/ffmpeg';
$path{midentify}= '/usr/share/mplayer/midentify.sh';
$path{montage}  = '/usr/bin/montage';
$path{mplayer}  = '/usr/bin/mplayer';

# Style settings.
our $text_font      = '/usr/share/fonts/truetype/freefont/FreeSans.ttf';
our $text_colour    = 'black';
our $text_bg        = '#f8f8ff';
our $text_size      = '12';
our $text2_font     = '/usr/share/fonts/truetype/freefont/FreeSansBold.ttf';
our $text2_colour   = '#009900';
our $text2_size     = '24';

my  $file;
our $rows = 6;
our $cols = 4;
our $geom = 'auto';
our $Title;
our $MM   = 0;
our $CM   = 'auto';
our $i    = 0; # counter
our $e    = 0; # error frame counter

our $verbosity = 1;

# Read from an options file
&argvFile
(
	'home'=>1,
	'current'=>1,
	'resolveEnvVars'=>1,
	'resolveRelativePathes'=>1,
	'startupFilename'=>'.dhyanarc',
	'fileOption'=>'--options'
);

my $man = 0;
my $help = 0;

&GetOptions
(
	"path=s%"		=> \%path,
	"font-family=s"   	=> \$text_font,
	"colour|color=s"  	=> \$text_colour,
	"background=s"    	=> \$text_bg,
	"font-size=i"     	=> \$text_size,
	"heading-font-family=s"	=> \$text2_font,
	"heading-colour|heading-color=s"
				=> \$text2_colour,
	"heading-font-size=i"	=> \$text2_size,
	"multi!"		=> \$MM,
	"title|t=s"		=> \$Title,
	"rows|r=i"		=> \$rows,
	"columns|cols|c=i"	=> \$cols,
	"geometry|geom|geo|g=s"	=> \$geom,
	"capture-mode|C=s"	=> \$CM,
	"verbose|v+"		=> \$verbosity,
	"quiet"			=> sub { $verbosity=0; },
	'help|usage|h' 		=> \$help,
	'man'			=> \$man,
	'version'		=> sub { die $VERSION; }
);
pod2usage(2) if $help;
pod2usage(0) if $man;
exit if ($help || $man);

if ($MM)
{
	while ($file = shift @ARGV)
	{
		if (-s $file)
		{
			&process($file);
		}
		else
		{
			&debug("$file not found.", 1);
		}
	}
}
else
{
	# Read command line options.
	$file = shift @ARGV || pod2usage(2);
	$rows = shift @ARGV || $rows;
	$cols = shift @ARGV || $cols;
	$geom = shift @ARGV || $geom || 'auto';
	$Title= shift @ARGV || $Title || $file;
	
	&process($file);
}

sub create_tmp_dir
{
	my $tmp_dir = $ENV{'TMPDIR'} || $ENV{'TMP'} || '/tmp';
	$tmp_dir .= '/dhyana-' . int(rand(100_000)) . '/';
	mkdir ($tmp_dir);
	
	&debug("Created working directory $tmp_dir.", 2);
	return $tmp_dir;
}

sub get_video_info
{
	$file = shift;
	&debug("Using $path{midentify} to get file information.", 3);

	# Info about the video.
	my @info = split /\n/, `$path{midentify} '$file'`;
	my %info;
	foreach (@info)
	{
		my $k;
		my $v;
		chomp;
		($k, $v) = split /\=/;
		$v =~ s/\\//g;
		$info{$k} = $v;
	}

	return %info;
}

sub get_annotation
{
	my $file = shift;
	my $info = shift;
	
	my $annotation  = sprintf("Size: %.02f MB (%d bytes)\n",
			(-s $file)/(1024*1024),
			(-s $file))
		. sprintf("Length: %d:%02d:%02d\n",
			POSIX::floor($info->{'ID_LENGTH'}/3600),
			POSIX::floor(($info->{'ID_LENGTH'}%3600)/60),
			POSIX::floor($info->{'ID_LENGTH'}%60) 
			)
		. 'Video: '.$info->{'ID_VIDEO_WIDTH'} . 'x' . $info->{'ID_VIDEO_HEIGHT'} 
		. ' (' . $info->{'ID_VIDEO_FORMAT'} . ', '
		. int($info->{'ID_VIDEO_FPS'}) . ' frames/sec, '
		. int($info->{'ID_VIDEO_BITRATE'}/1000) . " kb/sec)\n"
		. 'Audio: '.$info->{'ID_AUDIO_NCH'} . ' chan'
		. ' (' . $info->{'ID_AUDIO_CODEC'} . ', '
		. int($info->{'ID_AUDIO_RATE'}/1000) . ' kHz, '
		. int($info->{'ID_AUDIO_BITRATE'}/1000) . " kb/sec)\n"
		;
		
	# Error calculating stuff, it seems.
	if ( $info->{'ID_VIDEO_FPS'} == 1000)
	{	
		&debug("Potentially unreliable info, so using reduced information set.", 3);
		$annotation  = sprintf("Size: %.02f MB (%d bytes)\n",
				(-s $file)/(1024*1024),
				(-s $file))
			. sprintf("Length: %d:%02d:%02d\n",
				POSIX::floor($info->{'ID_LENGTH'}/3600),
				POSIX::floor(($info->{'ID_LENGTH'}%3600)/60),
				POSIX::floor($info->{'ID_LENGTH'}%60) 
				)
			. 'Video: '.$info->{'ID_VIDEO_WIDTH'} . 'x' . $info->{'ID_VIDEO_HEIGHT'} 
			. ' (' . $info->{'ID_VIDEO_FORMAT'} . ")\n"
			. 'Audio: '.$info->{'ID_AUDIO_NCH'} . ' chan'
			. ' (' . $info->{'ID_AUDIO_CODEC'} . ', '
			. int($info->{'ID_AUDIO_RATE'}/1000) . ' kHz, '
			. int($info->{'ID_AUDIO_BITRATE'}/1000) . " kb/sec)\n"
			;
	}
		
	return $annotation;
}

sub get_capture_timings
{
	&debug("Calculating timings for captures.", 3);
	
	my $file = shift;
	my $info = shift;
	
	my $n_frames = $rows * $cols;
	my $length = $info->{'ID_LENGTH'};
	
	if ( $info->{'ID_VIDEO_FPS'} == 1000)
	{
		debug("Potentially dodgy file. Adjusting timings.", 1);
		$length *= 0.6;
	}
	
	my $part_length = $length / ($n_frames - 1);
	my @times = (1);
	for (my $i=1; $i<$n_frames; $i++)
	{
		push @times, int($i * $part_length)-1;
	}
	
	
	
	return @times;
}

sub process
{
	my $file       = shift;
	
	die "Horrible apostrophe-filled file name.\n" if ($file =~ /\'/);
	
	my $tmp_dir    = &create_tmp_dir;
	my %info       = &get_video_info($file);
	my $annotation = &get_annotation($file, \%info);
	my $real_geom  = $geom;
	
	if ($real_geom eq 'auto')
	{
		if ($info{ID_VIDEO_WIDTH} > 600)
		{
			$real_geom = int($info{ID_VIDEO_WIDTH} / 3) . 'x' .
				int($info{ID_VIDEO_HEIGHT} / 3) . '+4+4';
		}
		elsif ($info{ID_VIDEO_WIDTH} > 315)
		{
			$real_geom = int($info{ID_VIDEO_WIDTH} / 2) . 'x' .
				int($info{ID_VIDEO_HEIGHT} / 2) . '+4+4';
		}
		else
		{
			$real_geom = int($info{ID_VIDEO_WIDTH} / 1) . 'x' .
				int($info{ID_VIDEO_HEIGHT} / 1) . '+4+4';
		}
	}
	
	my @timings = &get_capture_timings($file, \%info);
	
	$i = 1;
	$e = 0;
	
	if ( ($CM =~ /^matt$/i)
	||   ($CM =~ /^auto$/i && $info{ID_VIDEO_FPS}==1000  ))
	{
		&capture_matt($file, $tmp_dir, @timings);
	}
	
	else
	{
		foreach my $t (@timings)
		{
			if ($CM =~ /^auto$/i)
			{
				if ($file =~ /\.(mpe?g|mp4)$/i && -e $path{ffmpeg})
					{ &capture_mpeg($file, $tmp_dir, $t); }
				else
					{ &capture_std($file, $tmp_dir, $t); }
			}
			
			elsif ($CM =~ /^(ff)?mpe?g$/i)
				{ &capture_mpeg($file, $tmp_dir, $t); }
			
			elsif ($CM =~ /^old.?(ff)?mpe?g$/i)
				{ &capture_old_mpeg($file, $tmp_dir, $t); }
			
			else
				{ &capture_std($file, $tmp_dir, $t); }
		}
	}
	
	my $montage   = &create_montage($tmp_dir, $real_geom);
	my $a_montage = &annotate_montage($file, $tmp_dir, $annotation, $montage, $real_geom);
	
	# Finalising
	my $file_out = $file;
	$file_out =~ s/\.[^\.]+$/.jpeg/;
	&debug("Saving final file as '$file_out'.", 1);
	system("$path{convert} -quality 90 '$a_montage' '$file_out'");
	
	&debug("Cleaning up temporary files.", 2);
	system("rm -fr '$tmp_dir'");
	
}

sub capture_mpeg
{
	my $file    = shift;
	my $tmp_dir = shift;
	
	while ($_ = shift)
	{
		&debug("MPEG Capture [slow!]: frame $i, at $_ seconds.", 2);
		my $cmd = $path{ffmpeg} . " -i '$file'"
				. ' -f image2'
				. " -ss $_ -vframes 1"
				. " -y '%08d.png'";
		my $cmd_out = `$cmd 2>&1`;
		system(sprintf("cp '00000001.png' '%s/frame-%06d.png'",
				$tmp_dir, $_));
		system("rm -f '00000001.png'");
		$i++;
	}
	
	return 1;
}

sub capture_old_mpeg
{
	my $file    = shift;
	my $tmp_dir = shift;
	
	while ($_ = shift)
	{
		&debug("MPEG Capture [slow!]: frame $i, at $_ seconds.", 2);
		my $cmd = $path{ffmpeg} . " -i '$file'"
			. ' -f image -img gif'
			. " -ss $_ -vframes 1"
			. " -y '%08d.gif'";
		my $cmd_out = `$cmd 2>&1`;
		system(sprintf("$path{convert} '00000001.gif' '%s/frame-%06d.png'",
			$tmp_dir, $_));
		system("rm -f '00000001.gif'");
		$i++;
	}
		
	return 1;
}

sub capture_std
{
	my $file    = shift;
	my $tmp_dir = shift;
	
	while ($_ = shift)
	{
		&debug("Capture: frame $i, at $_ seconds.", 2);

		my $cmd = $path{mplayer} . ' -really-quiet -nosound'
			. ' -vo png:z=3 -frames 2'
			. " -ss $_ '$file'";
		my $cmd_out = `$cmd 2>&1`;
		if (-e '00000002.png')
			{ system(sprintf("mv '00000002.png' '%s/frame-%06d.png'", $tmp_dir, $_)); }
		else
			{ &debug("Something wrong with capture at $_ seconds", 1); $e++; }
		
		for ( my $f=1 ; $f<=2 ; $f++ )
			{ system("rm -f '0000000$f.png'") if (-e "0000000$f.png"); }
			
		$i++;
	}
	
	return 1;
}

sub capture_matt
{
	use Cwd;
	use File::chdir;
	use File::Spec;

	my $file    = shift;
	my $tmp_dir = shift;
	my @timings = @_;
	my $abs_file;
	my $dir = getcwd;
	
	&debug("Matt's Capture: put your faith in mplayer.", 2);
	
	if (File::Spec->file_name_is_absolute( $file ))
	{
		$abs_file = $file;
	}
	else
	{
		die "Apostrophes \' are bad in dir names\n" if ($dir =~ s/\'/\\'/g);
		my $abs_path = File::Spec->rel2abs( $dir ) ;
		$abs_file = "$dir/$file";
	}

	my $part_length = $timings[2] - $timings[1];
	my $n_frames    = $#timings;
	
	$CWD = $tmp_dir;
	my $cmd = $path{mplayer} . ' -really-quiet -nosound'
		. " -vo png:z=3 -sstep $part_length -frames $n_frames"
		. " '$abs_file'";
	my $cmd_out = `$cmd 2>&1`;
	my $frame_count = `ls $tmp_dir/0*.png|wc -l`;
	$CWD = $dir;
	
	$e = ($rows * $cols) - $frame_count;
	$i = $frame_count + 1;
	
	return 1;
}
sub create_montage 
{
	my $tmp_dir = shift;
	my $real_geom = shift;
	my $frame_count = ($cols * $rows) - $e;
	my $real_rows = POSIX::ceil($frame_count/$cols);
	
	&debug("Creating montage.", 2);
	&debug("Only $frame_count captures, so $cols by $real_rows.", 1) if ($e);
	
	my $cmd = $path{montage}
		. " -geometry '$real_geom'"
		. " -background '$text_bg'"
		. " -fill '$text_colour'"
		. " -tile '".$cols."x".$real_rows."'"
		. " $tmp_dir/frame-*.png $tmp_dir/0*.png $tmp_dir/montage.png";
	my $cmd_out = `$cmd 2>&1`;
	die "Error creating montage!\n" unless (-e "$tmp_dir/montage.png");
	
	return "$tmp_dir/montage.png";
}

sub annotate_montage
{
	my $file  	= shift;
	my $tmp_dir 	= shift;
	my $annotation	= shift;
	my $montage	= shift;
	my $real_geom	= shift;

	&debug("Annotating montage '$montage'.", 2);
	
	my $bord = $real_geom;
	$bord =~ s/^\d+x\d+\+//;
	$bord =~ s/\+/x/;
	
	my $cmd = $path{convert}
		. " -fill '$text_colour'"
		. " -background '$text_bg'"
		. " -font '\@$text_font'"
		. " -pointsize '$text_size'"
	# 	. " -gravity North"
		. " -trim +repage"
		. " -bordercolor '$text_bg'"
		. " -border $bord"
		. " text:- '$tmp_dir/annotation.png'";
	open(CONVERT, "|$cmd");
	if ($file eq $Title)
	{
		print CONVERT "$annotation\n";
	}
	else
	{
		print CONVERT "$file\n$annotation\n";
	}
	close(CONVERT);
	my $cmd = $path{convert}
		. " -fill '$text2_colour'"
		. " -background '$text_bg'"
		. " -font '\@$text2_font'"
		. " -pointsize '$text2_size'"
	# 	. " -gravity North"
		. " -trim +repage"
		. " -bordercolor '$text_bg'"
		. " -border $bord"
		. " text:- '$tmp_dir/filename.png'";

	open(CONVERT, "|$cmd");
	print CONVERT "$Title\n";
	close(CONVERT);
	system("$path{convert} -bordercolor '$text_bg' -border $bord -background '$text_bg' '$tmp_dir/filename.png' '$tmp_dir/annotation.png' '$tmp_dir/montage.png' -append '$tmp_dir/final.png'");
	
	return "$tmp_dir/final.png";
}	


# Debugging function.
sub debug
{
	my $x		= shift;
	my $level	= shift;
	
	print "[$level] $x\n" unless ($level > $verbosity);
}


__END__

=head1 NAME

dhyana.pl - Sequential video screen captures on Linux

=head1 SYNOPSIS

  dhyana.pl [options] file [cols [rows [geometry [title]]]]
  dhyana.pl --multi [options] file [file ...]

  Options:
    --help                   brief help message
    --man                    full documentation
    --version                print version number
    --verbose, -v            increase verbosity
    --quiet                  no status output
    --path TOOL=PATH         set path for external tool
    
  Capture options:
    --cols=X, -c X           columns of images to capture (default 4)
    --rows=Y, -r Y           rows of images to capture (default 6)
    --geometry=G, -g G       geometry of thumbnails (default 'auto')
    --title=T, -t T          title for thumbnails (filename default)
    --capture-mode=M, -C M   capture technique (default 'auto')
    
  Style options:
    --background             background colour (e.g. 'green', '#00ff00')
    --font-family            path to TTF file for text
    --font-size              size of text in pixels
    --colour, --color        colour for text
    --heading-font-family    path to TTF file for heading
    --heading-font-size      size of heading in pixels
    --heading-colour         colour for heading
    
=head1 OPTIONS

Dhyana.pl processes video files and outputs an image file containing a
summary of the video in the form of thumbnails taken at regular intervals.

It includes annotation at the top of the image with video file details such
as file name, codecs used, bitrates, length, file size and so forth.

For more details, see http://tobyinkster.co.uk/tag/dhyana/.

=over 8

=item B<--options>

To save time typing frequently used options, you may specify a group of
options in a text file. The text file syntax is the same as the command
line parameters, including dashes, but may extend over multiple lines, and
can include comment lines starting with a hash.

    dhyana.pl --options=myopts.txt file.mpeg
    
A shorthand for this exists:

    dhyana.pl @myopts.txt file.mpeg

Dhyana.pl also looks for additional options in the files './.dhyanarc' and
'~/.dhyanarc' if they exist.

=item B<--verbose>, B<--quiet> 

By default, only warning/debugging messages with priority "1" are shown.
You may increase dhyana.pl's verbosity using the --verbose option. This
option may be used multiple times to increase it further. The --quiet
option mutes all output from dhyana.pl except fatal errors.

=item B<--path>

Dhyana.pl uses several external programs:

    * mplayer
    * midentify [part of mplayer]
    * ffmpeg
    * convert [part of ImageMagick]
    * montage [part of ImageMagick]
    
By default it looks for these programs in /usr/bin/. You may specify
alternative locations using the path option. e.g.

    dhyana.pl --path ffmpeg=/opt/media/bin/ffmpeg file.mpeg

You may specify multiple paths by using the --path option repeatedly:

    dhyana.pl --path convert=/opt/magick/bin/convert \
      --path mogrify=/opt/magick/bin/mogrify file.mpeg
      
It is suggested that such paths are specified in '.dhyanarc' files to
save repeatedly typing them.

=item B<--capture-mode>

Dhyana.pl calls external programs such as mplayer and ffmpeg (see also
the --path option) to take the screen captures. The capture mode allows
you to choose which external programs to use, and how. Available modes
are:

    * auto (choose best based on input file codecs)
    * standard (frame-by-frame using mplayer)
    * ffmpeg (PNG export using newer ffmpeg)
    * old-ffmpeg (GIF export using older ffmpeg)
    * matt (all-at-once using mplayer)
    
Unless you have a particular reason to choose otherwise, it's normally
fine to use the default capture mode ('auto').

=item B<--cols>, B<--rows>

Lays out the thumbnails in an X*Y table. By default, 4 columns and 6 rows
are used. 

Occasionally, certain frame captures fail, in which case the resulting
table may be smaller than requested. In this case, rows will be removed,
not columns.

=item B<--geometry>

Specified in the format XxY+W+H where X and Y are the width and height of
image thumbnails to take, and W and H are the width and height of the
margin to include around them. 

These dimensions are passed directly to ImageMagick, so dhyana.pl follows
ImageMagick's behaviour with regard to them. Namely, images are scaled
by default preserving aspect ratio. If you want to warp the images, you
may prefix the geometry with an exclamation mark (!) to force the images
to be scaled to your exact dimensions.

The default geometry is 'auto' which automatically chooses an appropriate
geometry based on the video resolution.

=item B<--title>

Sets the title to display at the top of the image. The title can be in a
different font and colour from the rest of the annotation. If no title is
specified, the file name is used. If a title other than the file name is
specified, the file name will be included in the body of the annotation.

=item B<Style Options>

These are not yet documented.

=back