File Coverage

blib/lib/Video/Info/Quicktime_PL.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Video::Info::Quicktime_PL;
2              
3 4     4   52759 use strict;
  4         9  
  4         146  
4 4     4   5778 use Video::Info;
  0            
  0            
5              
6             use base qw(Video::Info);
7              
8             our $VERSION = '0.07';
9             use constant DEBUG => 0;
10              
11             use Compress::Zlib;
12              
13             use Class::MakeMethods::Emulator::MethodMaker
14             get_set => [ qw( version rsrc_id pict acodec raw_headers )],
15             ;
16              
17              
18             sub init {
19             my $self = shift;
20             my %param = @_;
21             $self->init_attributes(@_);
22             return $self;
23             }
24              
25             sub my_read
26             {
27             my($source, $len) = @_;
28             my $buf;
29             my $n = read($source, $buf, $len);
30             die "read failed: $!" unless defined $n;
31             die caller()." short read ($len/$n)" unless $n == $len;
32             $buf;
33             }
34              
35             sub time_to_date {
36             my $self = shift;
37             my ($tmp) = @_;
38            
39             # seconds difference between Mac epoch and Unix.
40             my $mod = 2063824538 - 12530100;
41             my $date = ($^O =~ /mac/i) ? localtime($tmp) : localtime($tmp-$mod);
42            
43             return $date;
44             }
45              
46             sub probe {
47             my $self = shift;
48            
49             seek($self->handle,0,2);
50             my($file_length) = tell($self->handle); seek($self->handle,0,0);
51            
52             my($len, $sig) = unpack("Na4", my_read($self->handle, 8));
53            
54             my %pnot;
55             while ( ($sig !~ /moov$/) and (!eof($self->handle)) ) {
56             if ($sig =~ m/pnot$/) {
57             # optional preview data is present... go ahead and process it.
58             my $prevue_atom = my_read( $self->handle, $len-8 );
59              
60             # print map{" $_ => $pnot{$_}\n" } sort keys %pnot;
61             ($len, $sig) = unpack("Na4", my_read($self->handle, 8));
62             if ($sig eq 'PICT') {
63             $self->pict(my_read( $self->handle, $len-8 ));
64            
65             ### Test preview during debug by outputing here:
66             # open(O,">out.pict");
67             # print O "\x00" x 512;
68             # print O $self->pict;
69             # close(O);
70             }
71             $len=8;
72             }
73             seek( $self->handle, $len-8, 1 );
74             ($len, $sig) = unpack("Na4", my_read($self->handle, 8));
75             print "".($len-8)."\t".$sig."\n" if DEBUG;
76             }
77             die "Unable to find 'moov' MOV signature.' " unless ( $sig =~ m/moov$/ );
78            
79             # $self->date($self->time_to_date(unpack('Na4', substr($prevue_atom,0,4,''))));
80             # $self->version(hex( substr($prevue_atom,0,2,'') ));
81             $self->type( $sig );
82             # $self->rsrc_id(unpack('H2',$prevue_atom));
83            
84             my $mov_atom = my_read( $self->handle, $len-8 );
85             my %mov = construct_hash( $mov_atom );
86            
87             $self->process_mov_atom(%mov);
88            
89             return 1;
90             }
91              
92             sub process_mov_atom {
93             my $self = shift;
94             my %mov = @_;
95              
96             my $cnt = 0;
97             foreach my $key (keys %mov) {
98             # print "Mov->key: ".$key."\n";
99             if ($key eq 'cmov') {
100             # compressed movie atom --- should be the only atom
101             my($len, $sig) = unpack("Na4", substr($mov{$key},0,8,''));
102            
103             # find out the compression method
104             my $cmpr_mthd = substr($mov{$key},0,4,'');
105            
106             ($len, $sig) = unpack("Na4", substr($mov{$key},0,8,''));
107             # and extract the compressed data.
108             my ($uncmprlen) = unpack("Na4", substr($mov{$key},0,4,''));
109             my $moov_rsrc = substr($mov{$key},0,$len-8);
110            
111             if ($cmpr_mthd eq 'zlib') {
112             my ($dest) = uncompress($moov_rsrc);
113             die "Error extracting compressed movie header data." if
114             ( $uncmprlen ne length($dest) );
115            
116             ($len,$sig) = unpack("Na4", substr($dest,0,8,''));
117            
118             # open(O,">cmov"); print O $dest; close(O);
119             # print "== recursive call to parse mov header ==\n";
120             $self->process_mov_atom( construct_hash( $dest ) );
121             return;
122             }
123             } elsif ($key eq 'mvhd') {
124             my(%h);
125             %h = get_mvhd( $mov{$key} );
126             # converting duration to movie length in seconds...
127             $self->duration( sprintf('%.2f',
128             ($h{'Duration'}/$h{'Time_scale'}) ) );
129             # print "\n mvhd\n\n";
130             # print map {"$_ => $h{$_}\n"} keys %h;
131            
132             } else {
133             my (%hash) = construct_hash( $mov{$key} );
134             if ($key =~ m/udta/) {
135             # each of these atoms uses a 4-byte (long) length
136             # offset before the ASCII text data... so strip it off
137             # before pushing to the output.
138             $self->copyright( substr($hash{"\xA9cpy"},4) )
139             if exists($hash{"\xA9cpy"});
140             $self->title( substr($hash{"\xA9nam"},4) )
141             if exists($hash{"\xA9nam"});
142             }
143             elsif ($key =~ m/trak/) {
144             my %tkhd = get_track_head( $hash{'tkhd'} );
145             # print map {"$_ => $tkhd{$_}\n"} keys %tkhd;
146             my %mdia = construct_hash( $hash{'mdia'} );
147             # print map {"$_ => $mdia{$_}\n"} keys %mdia;
148             my %minf = construct_hash( $mdia{'minf'} );
149             # print map {"$_ => $minf{$_}\n"} keys %minf;
150             my %stts;
151              
152             if ( exists $minf{'vmhd'} ) {
153             $self->width($tkhd{'Track width'});
154             $self->height($tkhd{'Track height'});
155              
156             my $tmp = $self->vstreams + 1;
157             $self->vstreams( $tmp );
158            
159             my %stbl = construct_hash( $minf{'stbl'} );
160             my %stsd = get_stsd( $stbl{'stsd'} );
161             # print map {" $_=$stsd{$_}\n"} keys %stsd;
162             %stts = get_stts( $stbl{'stts'} );
163             $cnt = $cnt + $stts{'count'} if exists($stts{'count'});
164              
165             # print map {" $_ = $stts{$_}\n"} keys %stts;
166            
167             $self->vcodec( $stsd{'compression type'} );
168            
169             }
170             if ( exists $minf{'smhd'} ) {
171             my %stbl = construct_hash( $minf{'stbl'} );
172             # print " stbl keys: ";
173             # print map {" $_\n"} keys %stbl;
174             my %stsd = get_stsd( $stbl{'stsd'} );
175             # print map {" $_=$stsd{$_}\n"} keys %stsd;
176            
177             my $tmp = $self->astreams + 1;
178             $self->astreams( $tmp );
179              
180             $self->arate($stsd{'audio sample rate'});
181             $self->afrequency($stsd{'audio sample size'});
182             $self->achans($stsd{'audio channels'});
183             $self->acodec($stsd{'compression type'});
184             }
185             }
186             }
187             }
188             $self->vframes( $cnt );
189             $self->fps( $cnt / $self->duration );
190              
191             }
192              
193             sub construct_hash {
194             my ( $input ) = @_;
195             my %hash;
196             while (length($input) > 0) {
197             my($len) = unpack("Na4", substr( $input, 0, 4, '') );
198             my($cntnt) = substr( $input, 0, $len-4, '');
199             my($type) = substr( $cntnt, 0, 4, '');
200             # print $type."\t".$len."\n";
201             if ( exists $hash{$type} ) {
202             my @a = grep($type,keys %hash);
203             $hash{$type.length(@a)} = $cntnt;
204             } else {
205             $hash{$type} = $cntnt;
206             }
207             }
208             %hash;
209             }
210              
211             sub get_stts {
212             my ($cntnt) = @_;
213             my (%h);
214            
215             $h{'Version'} = hex(unpack("H*", substr($cntnt,0,2,'') ));
216             $h{'Flags'} = unpack("H*", substr($cntnt,0,6,'') );
217             ### number of image frames in this atom
218             $h{'count'} = hex(unpack("H*", substr($cntnt,0,4,'') ));
219             ### number of tens-of-seconds per image
220             $h{'duration'} = hex(unpack("H*", substr($cntnt,0,4,'') ));
221             ### count * duration / mvhd->Time_scale = length of movie (in seconds)
222             %h;
223             }
224              
225             sub get_stsd {
226             # from pg 60:
227             my ($cntnt) = @_;
228             my (%h);
229             $h{'Version'} = unpack( "n2", substr($cntnt,0,2,'') );
230             $h{'Flags'} = unpack("H*", substr($cntnt,0,6,'') );
231             my $len = unpack("Na",substr($cntnt,0,4,''));
232             ($h{'compression type'} = substr($cntnt,0,8,'')) =~ s/\W(.*?)\W/$1/g;
233             $h{'Version'} = unpack( "n2", substr($cntnt,0,2,'') );
234             $h{'Revision_level'} = unpack( "n2", substr($cntnt,0,2,'') );
235             ($h{'Vendor'} = unpack("a8",substr($cntnt,0,8,'')))=~s/\W//g;
236            
237             if ( length($h{'Vendor'}) eq 0 ) {
238             $h{'audio channels'} = hex(unpack( "H*", substr($cntnt,0,2,'')));
239             $h{'audio sample size'} = hex(unpack( "H*", substr($cntnt,0,2,'')));
240             # $h{'audio compression'} = unpack( "H*", substr($cntnt,0,2,'')); /
241             $h{'audio packet size'} = hex(unpack( "H*", substr($cntnt,0,2,'')));
242             $h{'audio sample rate'} = hex(unpack( "H*", substr($cntnt,0,4,'')));
243             substr($cntnt,0,18,'');
244             } else {
245             $h{'Temporal_Quality'} = unpack( "Na", substr($cntnt,0,4,''));
246             $h{'Spatial_Quality'} = unpack( "Na", substr($cntnt,0,4,''));
247             $h{'Width'} = hex( unpack( "H4", substr($cntnt,0,2,'')));
248             $h{'Height'} = hex( unpack( "H4", substr($cntnt,0,2,'')));
249             $h{'Horz_res'} = hex( unpack("H4",substr($cntnt,0,4,'')));
250             $h{'Vert_res'} = hex( unpack("H4",substr($cntnt,0,4,'')));
251             $h{'Data_size'} = hex( unpack("H2",substr($cntnt,0,2,'')));
252             $h{'Frames_per_sample'} = hex( unpack("H*",substr($cntnt,0,4,'')));
253             $h{'Compressor_name'} = $1 if
254             ( substr($cntnt,0,32,'') =~ m/\W(.+?)\x00+$/) ;
255             $h{'Depth'} = hex( unpack( "H4", substr($cntnt,0,2,'')));
256             $h{'Color_table_ID'} = unpack( "s", substr($cntnt,0,2,''));
257             }
258            
259             # Collect any table extensions:
260             while (length($cntnt)>0) {
261             my($len, $sig) = unpack("Na4", substr($cntnt,0,8,''));
262             $h{$sig} = unpack("H".2*($len-4),substr($cntnt,0,$len-4,''));
263             }
264             # print length($cntnt)."\t".unpack("H".2*length($cntnt),$cntnt)."\n";
265             # print map {" $_ => $h{$_}\n"} sort keys %h;
266             %h;
267             }
268              
269             sub get_mvhd {
270             # my $self = shift;
271             my ($cntnt) = @_;
272             my (%h);
273            
274             $h{'Version'} = unpack( 'C', substr($cntnt,0,1,'') );
275             $h{'Flags'} = hex( substr($cntnt,0,3,'') );
276             $h{'Creation_time'} = unpack( "Na4", substr($cntnt,0,4,''));
277             $h{'Modification_time'} = unpack( "Na4", substr($cntnt,0,4,''));
278             $h{'Time_scale'} = unpack( "Na4", substr($cntnt,0,4,''));
279             $h{'Duration'} = unpack( "Na4", substr($cntnt,0,4,''));
280             $h{'Preferred_rate'} = unpack( "n", substr($cntnt,0,4,''));
281             $h{'Preferred_volume'} = unpack( "n", substr($cntnt,0,2,''));
282             $h{'Reserved'} = unpack( "H20", substr($cntnt,0,10,''));
283             $h{'Matrix_structure'} = unpack( "H72", substr($cntnt,0,36,''));
284             $h{'Preview_time'} = unpack( "Na4", substr($cntnt,0,4,''));
285             $h{'Preview_duration'} = unpack( "Na4", substr($cntnt,0,4,''));
286             $h{'Poster_time'} = unpack( "Na4", substr($cntnt,0,4,''));
287             $h{'Selection_time'} = unpack( "Na4", substr($cntnt,0,4,''));
288             $h{'Selection_duration'} = unpack( "Na4", substr($cntnt,0,4,''));
289             $h{'Current_time'} = unpack( "Na4", substr($cntnt,0,4,''));
290             $h{'Next_track_ID'} = unpack( "Na4", substr($cntnt,0,4,''));
291             # print map {" $_ => $h{$_}\n"} sort keys %h;
292             %h;
293             }
294              
295             sub get_track_head {
296             my ($track) = @_;
297            
298             my (%tkhd);
299            
300             $tkhd{'Version'} = hex( unpack("H*",substr($track,0,1 ,'') ));
301             $tkhd{'Flags'} = unpack( "Na4", substr($track,0,3,'') );
302             $tkhd{'Creation time'} = unpack( "Na4", substr($track,0,4 ,''));
303             $tkhd{'Modification time'} = unpack( "Na4", substr($track,0,4 ,''));
304             $tkhd{'Track ID'} = unpack( "Na4", substr($track,0,4 ,''));
305             $tkhd{'Reserved'} = unpack( "Na4", substr($track,0,4 ,''));
306             $tkhd{'Duration'} = unpack( "Na4", substr($track,0,4 ,''));
307             $tkhd{'Reserved'} = unpack( "Na8", substr($track,0,8 ,''));
308             $tkhd{'Layer'} = unpack( "Na2", substr($track,0,2 ,''));
309             $tkhd{'Alternate group'} = unpack( "Na2", substr($track,0,2 ,''));
310             $tkhd{'Volume'}= unpack( "Na2", substr($track,0,2 ,''));
311             $tkhd{'Reserved'}= unpack( "Na2", substr($track,0,2 ,''));
312             $tkhd{'Matrix structure'}= unpack( "H36", substr($track,0,36,''));
313             $tkhd{'Track width'} = hex unpack( "H4", substr($track,0,4,'') );
314             $tkhd{'Track height'} = hex unpack( "H4", substr($track,0,4,'') );
315            
316             # print map {" $_ => $tkhd{$_}\n"} sort keys %tkhd;
317             %tkhd;
318             }
319              
320             1;
321              
322             =head1 NAME
323            
324             Video::Info::Quicktime_PL - pure Perl implementation to extract header info from Quicktime (TM) files.
325            
326             =head1 SYNOPSIS
327            
328             use Video::Info::Quicktime;
329              
330             my $file = Video::Info::Quicktime_PL->new(-file=>'eg/rot_button.mov');
331             $file->probe;
332             printf("frame size: %d x %d\n", $file->width, $file->height );
333             printf("fps: %d, total length: %d (sec)\n", $file->fps, $file->duration );
334              
335              
336             ## some digital cameras which are able to record Quicktime videos
337             ## include a preview picture:
338              
339             if (length($file->pict)>0) {
340             print "Outputing PICT file\n";
341             my $oi = 'eg/mov_preview.pict';
342             open(O,">$oi") || warn("Couldn't open $oi: $!\n");
343             binmode(O); # set the file to binary mode in working on Windows
344             # Image::Magick methods will only recognize this file as
345             # PICT if there exists a leading header of zeros:
346             print O "\x00" x 512;
347             print O $file->pict;
348             close(O);
349             }
350              
351              
352             =head1 DESCRIPTION
353              
354             This module provides cursory access to the header information of
355             Quicktime Movie files. The original motivation for the development of
356             this module was to aid in thumbnail generation for HTML index files.
357             Thus, only a limited amount of information is returned. See the test
358             files for a complete list.
359              
360             If the Video::OpenQuicktime package is installed, you may consider
361             using Video::Info::Quicktime instead of this module. Based on the
362             OpenQuicktime library, more complete header information is available
363             but at the cost of increased module and library dependancy.
364              
365              
366             =head1 AUTHOR
367              
368             Copyright (c) 2003
369             Released under the Aladdin Free Public License (see LICENSE for details)
370              
371             Pure Perl Implementation by W. Scott Hoge
372             Hooks for Video::Info access by Allen Day
373              
374             =head1 SEE ALSO
375              
376             L
377             L
378             L
379              
380             =cut
381              
382             __END__