File Coverage

blib/lib/MP4/Info.pm
Criterion Covered Total %
statement 187 281 66.5
branch 96 200 48.0
condition 33 81 40.7
subroutine 22 24 91.6
pod 4 14 28.5
total 342 600 57.0


line stmt bran cond sub pod time code
1             #
2             # Copyright (c) 2004-2010 Jonathan Harris
3             #
4             # This program is free software; you can redistribute it and/or modify it
5             # under the the same terms as Perl itself.
6             #
7              
8             package MP4::Info;
9              
10 1     1   31271 use overload;
  1         1518  
  1         7  
11 1     1   54 use strict;
  1         1  
  1         44  
12 1     1   7 use Carp;
  1         8  
  1         77  
13 1     1   1246 use Symbol;
  1         1480  
  1         101  
14 1     1   13116 use Encode;
  1         29690  
  1         115  
15 1     1   6242 use Encode::Guess qw(latin1);
  1         30797  
  1         9  
16 1     1   5741 use IO::String;
  1         6212  
  1         65  
17              
18 1         21408 use vars qw(
19             $VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $AUTOLOAD
20             %data_atoms %other_atoms %container_atoms @mp4_genres
21 1     1   13 );
  1         3  
22              
23             @ISA = 'Exporter';
24             @EXPORT = qw(get_mp4tag get_mp4info);
25             @EXPORT_OK = qw(use_mp4_utf8);
26             %EXPORT_TAGS = (
27             utf8 => [qw(use_mp4_utf8)],
28             all => [@EXPORT, @EXPORT_OK]
29             );
30              
31             $VERSION = '1.13';
32              
33             my $debug = 0;
34              
35              
36             =head1 NAME
37              
38             MP4::Info - Fetch info from MPEG-4 files (.mp4, .m4a, .m4p, .3gp)
39              
40             =head1 SYNOPSIS
41              
42             #!perl -w
43             use MP4::Info;
44             my $file = 'Pearls_Before_Swine.m4a';
45              
46             my $tag = get_mp4tag($file) or die "No TAG info";
47             printf "$file is a %s track\n", $tag->{GENRE};
48              
49             my $info = get_mp4info($file);
50             printf "$file length is %d:%d\n", $info->{MM}, $info->{SS};
51              
52             my $mp4 = new MP4::Info $file;
53             printf "$file length is %s, title is %s\n",
54             $mp4->time, $mp4->title;
55              
56             =head1 DESCRIPTION
57              
58             The MP4::Info module can be used to extract tag and meta information from
59             MPEG-4 audio (AAC) and video files. It is designed as a drop-in replacement
60             for L.
61              
62             Note that this module does not allow you to update the information in MPEG-4
63             files.
64              
65             =over 4
66              
67             =item $mp4 = MP4::Info-Enew(FILE)
68              
69             OOP interface to the rest of the module. The same keys available via
70             C and C are available via the returned object
71             (using upper case or lower case; but note that all-caps 'VERSION' will
72             return the module version, not the MPEG-4 version).
73              
74             Passing a value to one of the methods will B set the value for that tag
75             in the MPEG-4 file.
76              
77             =cut
78              
79             sub new
80             {
81 5     5 1 7854 my ($class, $file) = @_;
82              
83             # Supported tags
84 5         125 my %tag_names =
85             (
86             ALB => 1, APID => 1, ART => 1, CMT => 1, COVR => 1, CPIL => 1, CPRT => 1, DAY => 1, DISK => 1, GNRE => 1, GRP => 1, NAM => 1, RTNG => 1, TMPO => 1, TOO => 1, TRKN => 1, WRT => 1,
87             TITLE => 1, ARTIST => 1, ALBUM => 1, YEAR => 1, COMMENT => 1, GENRE => 1, TRACKNUM => 1,
88             VERSION => 1, LAYER => 1,
89             BITRATE => 1, FREQUENCY => 1, SIZE => 1,
90             SECS => 1, MM => 1, SS => 1, MS => 1, TIME => 1,
91             COPYRIGHT => 1, ENCODING => 1, ENCRYPTED => 1,
92             );
93              
94 5 50       20 my $tags = get_mp4tag ($file) or return undef;
95 5         119 my $self = {
96             _permitted => \%tag_names,
97             %$tags
98             };
99 5         74 return bless $self, $class;
100             }
101              
102              
103             # Create accessor functions - see perltoot manpage
104             sub AUTOLOAD
105             {
106 313     313   54682 my $self = shift;
107 313 50       960 my $type = ref($self) or croak "$self is not an object";
108 313         476 my $name = $AUTOLOAD;
109 313         2424 $name =~ s/.*://; # strip fully-qualified portion
110              
111 313 50       1050 unless (exists $self->{_permitted}->{uc $name} )
112             {
113 0         0 croak "No method '$name' available in class $type";
114             }
115              
116             # Ignore any parameter
117 313         1715 return $self->{uc $name};
118             }
119              
120              
121             sub DESTROY
122 0     0   0 {
123             }
124              
125              
126             ############################################################################
127              
128             =item use_mp4_utf8([STATUS])
129              
130             Tells MP4::Info whether to assume that ambiguously encoded TAG info is UTF-8
131             or Latin-1. 1 is UTF-8, 0 is Latin-1. Default is UTF-8.
132              
133             Function returns new status (1/0). If no argument is supplied, or an
134             unaccepted argument is supplied, function merely returns existing status.
135              
136             This function is not exported by default, but may be exported
137             with the C<:utf8> or C<:all> export tag.
138              
139             =cut
140              
141             my $utf8 = 1;
142              
143             sub use_mp4_utf8
144             {
145 1     1 1 1798 my ($val) = @_;
146 1 50 33     8 $utf8 = $val if (($val == 0) || ($val == 1));
147 1         3 return $utf8;
148             }
149              
150              
151             =item get_mp4tag (FILE)
152              
153             Returns hash reference containing the tag information from the MP4 file.
154             The following keys may be defined:
155              
156             ALB Album
157             APID Apple Store ID
158             ART Artist
159             CMT Comment
160             COVR Album art (typically JPEG or PNG data)
161             CPIL Compilation (boolean)
162             CPRT Copyright statement
163             DAY Year
164             DISK Disk number & total (2 integers)
165             GNRE Genre
166             GRP Grouping
167             NAM Title
168             RTNG Rating (integer)
169             TMPO Tempo (integer)
170             TOO Encoder
171             TRKN Track number & total (2 integers)
172             WRT Author or composer
173              
174             For compatibility with L, the MP3 ID3v1-style keys
175             TITLE, ARTIST, ALBUM, YEAR, COMMENT, GENRE and TRACKNUM are defined as
176             synonyms for NAM, ART, ALB, DAY, CMT, GNRE and TRKN[0].
177              
178             Any and all of these keys may be undefined if the corresponding information
179             is missing from the MPEG-4 file.
180              
181             On error, returns nothing and sets C<$@>.
182              
183             =cut
184              
185             sub get_mp4tag
186             {
187 12     12 1 540 my ($file) = @_;
188 12         29 my (%tags);
189              
190 12 50       45 return parse_file ($file, \%tags) ? undef : {%tags};
191             }
192              
193              
194             =item get_mp4info (FILE)
195              
196             Returns hash reference containing file information from the MPEG-4 file.
197             The following keys may be defined:
198              
199             VERSION MPEG version (=4)
200             LAYER MPEG layer description (=1 for compatibility with MP3::Info)
201             BITRATE bitrate in kbps (average for VBR files)
202             FREQUENCY frequency in kHz
203             SIZE bytes in audio stream
204              
205             SECS total seconds, rounded to nearest second
206             MM minutes
207             SS leftover seconds
208             MS leftover milliseconds, rounded to nearest millisecond
209             TIME time in MM:SS, rounded to nearest second
210              
211             COPYRIGHT boolean for audio is copyrighted
212             ENCODING audio codec name. Possible values include:
213             'mp4a' - AAC, aacPlus
214             'alac' - Apple lossless
215             'drms' - Apple encrypted AAC
216             'samr' - 3GPP narrow-band AMR
217             'sawb' - 3GPP wide-band AMR
218             'enca' - Unspecified encrypted audio
219             ENCRYPTED boolean for audio data is encrypted
220              
221             Any and all of these keys may be undefined if the corresponding information
222             is missing from the MPEG-4 file.
223              
224             On error, returns nothing and sets C<$@>.
225              
226             =cut
227              
228             sub get_mp4info
229             {
230 5     5 1 15636 my ($file) = @_;
231 5         13 my (%tags);
232              
233 5 50       24 return parse_file ($file, \%tags) ? undef : {%tags};
234             }
235              
236              
237             ############################################################################
238             # No user-servicable parts below
239              
240              
241             # Interesting atoms that contain data in standard format.
242             # The items marked ??? contain integers - I don't know what these are for
243             # but return them anyway because the user might know.
244             my %data_atoms =
245             (
246             AART => 1, # Album artist - returned in ART field no ART found
247             ALB => 1,
248             ART => 1,
249             CMT => 1,
250             COVR => 1, # Cover art
251             CPIL => 1,
252             CPRT => 1,
253             DAY => 1,
254             DISK => 1,
255             GEN => 1, # Custom genre - returned in GNRE field no GNRE found
256             GNRE => 1, # Standard ID3/WinAmp genre
257             GRP => 1,
258             NAM => 1,
259             RTNG => 1,
260             TMPO => 1,
261             TOO => 1,
262             TRKN => 1,
263             WRT => 1,
264             # Apple store
265             APID => 1,
266             AKID => 1, # ???
267             ATID => 1, # ???
268             CNID => 1, # ???
269             GEID => 1, # Some kind of watermarking ???
270             PLID => 1, # ???
271             # 3GPP
272             TITL => 1, # title - returned in NAM field no NAM found
273             DSCP => 1, # description - returned in CMT field no CMT found
274             #CPRT=> 1,
275             PERF => 1, # performer - returned in ART field no ART found
276             AUTH => 1, # author - returned in WRT field no WRT found
277             #GNRE=> 1,
278             MEAN => 1,
279             NAME => 1,
280             DATA => 1,
281             );
282              
283             # More interesting atoms, but with non-standard data layouts
284             my %other_atoms =
285             (
286             MOOV => \&parse_moov,
287             MDAT => \&parse_mdat,
288             META => \&parse_meta,
289             MVHD => \&parse_mvhd,
290             STSD => \&parse_stsd,
291             UUID => \&parse_uuid,
292             );
293              
294             # Standard container atoms that contain either kind of above atoms
295             my %container_atoms =
296             (
297             ILST => 1,
298             MDIA => 1,
299             MINF => 1,
300             STBL => 1,
301             TRAK => 1,
302             UDTA => 1,
303             '----' => 1, # iTunes and aacgain info
304             );
305              
306              
307             # Standard ID3 plus non-standard WinAmp genres
308             my @mp4_genres =
309             (
310             'N/A', 'Blues', 'Classic Rock', 'Country', 'Dance', 'Disco',
311             'Funk', 'Grunge', 'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies',
312             'Other', 'Pop', 'R&B', 'Rap', 'Reggae', 'Rock', 'Techno',
313             'Industrial', 'Alternative', 'Ska', 'Death Metal', 'Pranks',
314             'Soundtrack', 'Euro-Techno', 'Ambient', 'Trip-Hop', 'Vocal',
315             'Jazz+Funk', 'Fusion', 'Trance', 'Classical', 'Instrumental',
316             'Acid', 'House', 'Game', 'Sound Clip', 'Gospel', 'Noise',
317             'AlternRock', 'Bass', 'Soul', 'Punk', 'Space', 'Meditative',
318             'Instrumental Pop', 'Instrumental Rock', 'Ethnic', 'Gothic',
319             'Darkwave', 'Techno-Industrial', 'Electronic', 'Pop-Folk',
320             'Eurodance', 'Dream', 'Southern Rock', 'Comedy', 'Cult', 'Gangsta',
321             'Top 40', 'Christian Rap', 'Pop/Funk', 'Jungle', 'Native American',
322             'Cabaret', 'New Wave', 'Psychadelic', 'Rave', 'Showtunes',
323             'Trailer', 'Lo-Fi', 'Tribal', 'Acid Punk', 'Acid Jazz', 'Polka',
324             'Retro', 'Musical', 'Rock & Roll', 'Hard Rock', 'Folk',
325             'Folk/Rock', 'National Folk', 'Swing', 'Fast-Fusion', 'Bebob',
326             'Latin', 'Revival', 'Celtic', 'Bluegrass', 'Avantgarde',
327             'Gothic Rock', 'Progressive Rock', 'Psychedelic Rock',
328             'Symphonic Rock', 'Slow Rock', 'Big Band', 'Chorus',
329             'Easy Listening', 'Acoustic', 'Humour', 'Speech', 'Chanson',
330             'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass',
331             'Primus', 'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango',
332             'Samba', 'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul',
333             'Freestyle', 'Duet', 'Punk Rock', 'Drum Solo', 'A capella',
334             'Euro-House', 'Dance Hall', 'Goa', 'Drum & Bass', 'Club House',
335             'Hardcore', 'Terror', 'Indie', 'BritPop', 'NegerPunk',
336             'Polsk Punk', 'Beat', 'Christian Gangsta', 'Heavy Metal',
337             'Black Metal', 'Crossover', 'Contemporary C', 'Christian Rock',
338             'Merengue', 'Salsa', 'Thrash Metal', 'Anime', 'JPop', 'SynthPop'
339             );
340              
341              
342             sub parse_file
343             {
344 17     17 0 41 my ($file, $tags) = @_;
345 17         28 my ($fh, $err, $header, $size);
346              
347 17 50 33     140 if (not (defined $file && $file ne ''))
348             {
349 0         0 $@ = 'No file specified';
350 0         0 return -1;
351             }
352              
353 17 50       60 if (ref $file) # filehandle passed
354             {
355 0         0 $fh = $file;
356             }
357             else
358             {
359 17         86 $fh = gensym;
360 17 50       2008 if (not open $fh, "< $file\0")
361             {
362 0         0 $@ = "Can't open $file: $!";
363 0         0 return -1;
364             }
365             }
366              
367 17         65 binmode $fh;
368              
369             # Sanity check that this looks vaguely like an MP4 file
370 17 50 33     16074 if ((read ($fh, $header, 8) != 8) || (lc substr ($header, 4) ne 'ftyp'))
371             {
372 0         0 close ($fh);
373 0         0 $@ = 'Not an MPEG-4 file';
374 0         0 return -1;
375             }
376 17         17459 seek $fh, 0, 2;
377 17         46 $size = tell $fh;
378 17         96 seek $fh, 0, 0;
379              
380 17         80 $err = parse_container($fh, 0, $size, $tags);
381 17         414 close ($fh);
382 17 50       59 return $err if $err;
383              
384             # remaining get_mp4tag() stuff
385 17 100       69 $tags->{CPIL} = 0 unless defined ($tags->{CPIL});
386              
387             # MP3::Info compatibility
388 17 50       99 $tags->{TITLE} = $tags->{NAM} if defined ($tags->{NAM});
389 17 50       78 $tags->{ARTIST} = $tags->{ART} if defined ($tags->{ART});
390 17 100       87 $tags->{ALBUM} = $tags->{ALB} if defined ($tags->{ALB});
391 17 100       78 $tags->{YEAR} = $tags->{DAY} if defined ($tags->{DAY});
392 17 100       63 $tags->{COMMENT} = $tags->{CMT} if defined ($tags->{CMT});
393 17 100       62 $tags->{GENRE} = $tags->{GNRE} if defined ($tags->{GNRE});
394 17 100       77 $tags->{TRACKNUM} = $tags->{TRKN}[0] if defined ($tags->{TRKN});
395              
396             # remaining get_mp4info() stuff
397 17         88 $tags->{VERSION} = 4;
398 17 50       67 $tags->{LAYER} = 1 if defined ($tags->{FREQUENCY});
399 17 50       62 $tags->{COPYRIGHT}= (defined ($tags->{CPRT}) ? 1 : 0);
400 17 50       59 $tags->{ENCRYPTED}= 0 unless defined ($tags->{ENCRYPTED});
401              
402             # Returns actual (not requested) bitrate
403 17 50 33     357 if (defined($tags->{SIZE}) && $tags->{SIZE} && defined($tags->{SECS}) && ($tags->{MM}+$tags->{SS}+$tags->{MS}))
      33        
      33        
404             {
405 17         137 $tags->{BITRATE} = int (0.5 + $tags->{SIZE} * 0.008 / ($tags->{MM}*60+$tags->{SS}+$tags->{MS}/1000))
406             }
407              
408             # Post process '---' container
409 17 100 66     2351 if ($tags->{MEAN} && ref($tags->{MEAN}) eq 'ARRAY')
410             {
411 8         21 for (my $i = 0; $i < scalar @{$tags->{MEAN}}; $i++)
  16         53  
412             {
413 8         14 push @{$tags->{META}}, {
  8         91  
414             MEAN => $tags->{MEAN}->[$i],
415             NAME => $tags->{NAME}->[$i],
416             DATA => $tags->{DATA}->[$i],
417             };
418             }
419              
420 8         32 delete $tags->{MEAN};
421 8         19 delete $tags->{NAME};
422 8         27 delete $tags->{DATA};
423             }
424              
425 17         529 return 0;
426             }
427              
428              
429             # Pre: $size=size of container contents
430             # $fh points to start of container contents
431             # Post: $fh points past end of container contents
432             sub parse_container
433             {
434 185     185 0 311 my ($fh, $level, $size, $tags) = @_;
435 185         235 my ($end, $err);
436              
437 185         228 $level++;
438 185         537 $end = (tell $fh) + $size;
439 185         2163 while (tell $fh < $end)
440             {
441 703         12433 $err = parse_atom($fh, $level, $end-(tell $fh), $tags);
442 703 50       4568 return $err if $err;
443             }
444 185 50       1513 if (tell $fh != $end)
445             {
446 0         0 $@ = 'Parse error';
447 0         0 return -1;
448             }
449 185         1633 return 0;
450             }
451              
452              
453             # Pre: $fh points to start of atom
454             # $parentsize is remaining size of parent container
455             # Post: $fh points past end of atom
456             sub parse_atom
457             {
458 703     703 0 6329 my ($fh, $level, $parentsize, $tags) = @_;
459 703         862 my ($header, $size, $id, $err, $pos);
460 703 50       22219 if (read ($fh, $header, 8) != 8)
461             {
462 0         0 $@ = 'Premature eof';
463 0         0 return -1;
464             }
465              
466 703         15762 ($size,$id) = unpack 'Na4', $header;
467 703 50       2782 if ($size==0)
    50          
468             {
469             # Zero-sized atom extends to eof (14496-12:2004 S4.2)
470 0         0 $pos=tell($fh);
471 0         0 seek $fh, 0, 2;
472 0         0 $size = tell($fh) - $pos; # Error if parent size doesn't match
473 0         0 seek $fh, $pos, 0;
474             }
475             elsif ($size == 1)
476             {
477             # extended size
478 0         0 my ($hi, $lo);
479 0 0       0 if (read ($fh, $header, 8) != 8)
480             {
481 0         0 $@ = 'Premature eof';
482 0         0 return -1;
483             }
484 0         0 ($hi,$lo) = unpack 'NN', $header;
485 0         0 $size=$hi*(2**32) + $lo;
486 0 0       0 if ($size>$parentsize)
487             {
488             # atom extends outside of parent container - skip to end of parent
489 0         0 seek $fh, $parentsize-16, 1;
490 0         0 return 0;
491             }
492 0         0 $size -= 16;
493             }
494             else
495             {
496 703 50       1347 if ($size>$parentsize)
497             {
498             # atom extends outside of parent container - skip to end of parent
499 0         0 seek $fh, $parentsize-8, 1;
500 0         0 return 0;
501             }
502 703         1050 $size -= 8;
503             }
504 703 50       1331 if ($size<0)
505             {
506 0         0 $@ = 'Parse error';
507 0         0 return -1;
508             }
509 703         1817 $id =~ s/[^\w\-]//;
510 703         10438 $id = uc $id;
511              
512 703 50       6879 printf "%s%s: %d bytes\n", ' 'x(2*$level), $id, $size if $debug;
513              
514 703 100       4352 if (defined($data_atoms{$id}))
    100          
    100          
515             {
516 194         425 return parse_data ($fh, $level, $size, $id, $tags);
517             }
518             elsif (defined($other_atoms{$id}))
519             {
520 94         224 return &{$other_atoms{$id}}($fh, $level, $size, $tags);
  94         29536  
521             }
522             elsif ($container_atoms{$id})
523             {
524 134         322 return parse_container ($fh, $level, $size, $tags);
525             }
526              
527             # Unkown atom - skip past it
528 281         1108 seek $fh, $size, 1;
529 281         11999 return 0;
530             }
531              
532              
533             # Pre: $size=size of atom contents
534             # $fh points to start of atom contents
535             # Post: $fh points past end of atom contents
536             sub parse_moov
537             {
538 17     17 0 54 my ($fh, $level, $size, $tags) = @_;
539              
540             # MOOV is a normal container.
541             # Read ahead to improve performance on high-latency filesystems.
542 17         25 my $data;
543 17 50       119 if (read ($fh, $data, $size) != $size)
544             {
545 0         0 $@ = 'Premature eof';
546 0         0 return -1;
547             }
548 17         176 my $cache=IO::String->new($data);
549 17         2235 return parse_container ($cache, $level, $size, $tags);
550             }
551              
552              
553             # Pre: $size=size of atom contents
554             # $fh points to start of atom contents
555             # Post: $fh points past end of atom contents
556             sub parse_mdat
557             {
558 20     20 0 36 my ($fh, $level, $size, $tags) = @_;
559              
560 20 100       106 $tags->{SIZE} = 0 unless defined($tags->{SIZE});
561 20         43 $tags->{SIZE} += $size;
562 20         190 seek $fh, $size, 1;
563              
564 20         72 return 0;
565             }
566              
567              
568             # Pre: $size=size of atom contents
569             # $fh points to start of atom contents
570             # Post: $fh points past end of atom contents
571             sub parse_meta
572             {
573 17     17 0 38 my ($fh, $level, $size, $tags) = @_;
574              
575             # META is just a container preceded by a version field
576 17         61 seek $fh, 4, 1;
577 17         240 return parse_container ($fh, $level, $size-4, $tags);
578             }
579              
580              
581             # Pre: $size=size of atom contents
582             # $fh points to start of atom contents
583             # Post: $fh points past end of atom contents
584             sub parse_mvhd
585             {
586 17     17 0 44 my ($fh, $level, $size, $tags) = @_;
587 17         53 my ($data, $version, $scale, $duration, $secs);
588              
589 17 50       55 if ($size < 32)
590             {
591 0         0 $@ = 'Parse error';
592 0         0 return -1;
593             }
594 17 50       67 if (read ($fh, $data, $size) != $size)
595             {
596 0         0 $@ = 'Premature eof';
597 0         0 return -1;
598             }
599              
600 17         303 $version = unpack('C', $data) & 255;
601 17 50       54 if ($version==0)
    0          
602             {
603 17         61 ($scale,$duration) = unpack 'NN', substr ($data, 12, 8);
604             }
605             elsif ($version==1)
606             {
607 0         0 my ($hi,$lo);
608 0 0       0 print "Long version\n" if $debug;
609 0         0 ($scale,$hi,$lo) = unpack 'NNN', substr ($data, 20, 12);
610 0         0 $duration=$hi*(2**32) + $lo;
611             }
612             else
613             {
614 0         0 return 0;
615             }
616              
617 17 50       47 printf " %sDur/Scl=$duration/$scale\n", ' 'x(2*$level) if $debug;
618 17         44 $secs=$duration/$scale;
619 17         74 $tags->{SECS} = int (0.5+$secs);
620 17         39 $tags->{MM} = int ($secs/60);
621 17         64 $tags->{SS} = int ($secs - $tags->{MM}*60);
622 17         51 $tags->{MS} = int (0.5 + 1000*($secs - int ($secs)));
623 17         167 $tags->{TIME} = sprintf "%02d:%02d",
624             $tags->{MM}, $tags->{SECS} - $tags->{MM}*60;
625              
626 17         61 return 0;
627             }
628              
629              
630             # Pre: $size=size of atom contents
631             # $fh points to start of atom contents
632             # Post: $fh points past end of atom contents
633             sub parse_stsd
634             {
635 23     23 0 59 my ($fh, $level, $size, $tags) = @_;
636 23         32 my ($data, $data_format);
637              
638 23 50       67 if ($size < 44)
639             {
640 0         0 $@ = 'Parse error';
641 0         0 return -1;
642             }
643 23 50       256 if (read ($fh, $data, $size) != $size)
644             {
645 0         0 $@ = 'Premature eof';
646 0         0 return -1;
647             }
648              
649             # Assumes first entry in table contains the data
650 23 50       343 printf " %sSample=%s\n", ' 'x(2*$level), substr ($data, 12, 4) if $debug;
651 23         58 $data_format = lc substr ($data, 12, 4);
652              
653             # Is this an audio track? (Ought to look for presence of an SMHD uncle
654             # atom instead to allow for other audio data formats).
655 23 50 100     220 if (($data_format eq 'mp4a') || # AAC, aacPlus
      66        
      66        
      33        
      33        
      33        
656             ($data_format eq 'alac') || # Apple lossless
657             ($data_format eq 'drms') || # Apple encrypted AAC
658             ($data_format eq 'samr') || # Narrow-band AMR
659             ($data_format eq 'sawb') || # AMR wide-band
660             ($data_format eq 'sawp') || # AMR wide-band +
661             ($data_format eq 'enca')) # Generic encrypted audio
662             {
663 17         51 $tags->{ENCODING} = $data_format;
664             # $version = unpack "n", substr ($data, 24, 2);
665             # s8.16 is inconsistent. In practice, channels always appears == 2.
666             # $tags->{STEREO} = (unpack ("n", substr ($data, 32, 2)) > 1) ? 1 : 0;
667             # Old Quicktime field. No longer used.
668             # $tags->{VBR} = (unpack ("n", substr ($data, 36, 2)) == -2) ? 1 : 0;
669 17         77 $tags->{FREQUENCY} = unpack ('N', substr ($data, 40, 4)) / 65536000;
670 17 50       54 printf " %sFreq=%s\n", ' 'x(2*$level), $tags->{FREQUENCY} if $debug;
671             }
672              
673 23 50 33     165 $tags->{ENCRYPTED}=1 if (($data_format eq 'drms') ||
674             (substr($data_format, 0, 3) eq 'enc'));
675              
676 23         78 return 0;
677             }
678              
679              
680             # User-defined box. Used by PSP - See ffmpeg libavformat/movenc.c
681             #
682             # Pre: $size=size of atom contents
683             # $fh points to start of atom contents
684             # Post: $fh points past end of atom contents
685             sub parse_uuid
686             {
687 0     0 0 0 my ($fh, $level, $size, $tags) = @_;
688 0         0 my $data;
689              
690 0 0       0 if (read ($fh, $data, $size) != $size)
691             {
692 0         0 $@ = 'Premature eof';
693 0         0 return -1;
694             }
695 0 0       0 ($size > 26) || return 0; # 16byte uuid, 10byte psp-specific
696              
697 0         0 my ($u1,$u2,$u3,$u4)=unpack 'a4NNN', $data;
698 0 0       0 if ($u1 eq 'USMT') # PSP also uses a uuid starting with 'PROF'
699             {
700 0         0 my ($pspsize,$pspid) = unpack 'Na4', substr ($data, 16, 8);
701 0 0       0 printf " %s$pspid: $pspsize bytes\n", ' 'x(2*$level) if $debug;
702 0 0       0 ($pspsize==$size-16) || return 0; # sanity check
703 0 0       0 if ($pspid eq 'MTDT')
704             {
705 0         0 my $nblocks = unpack 'n', substr ($data, 24, 2);
706 0         0 $data = substr($data, 26);
707 0         0 while ($nblocks)
708             {
709 0         0 my ($bsize, $btype, $flags, $ptype) = unpack 'nNnn', $data;
710 0 0       0 printf " %s0x%x: $bsize bytes, Type=$ptype\n", ' 'x(2*$level), $btype if $debug;
711 0 0 0     0 if ($btype==1 && $bsize>12 && $ptype==1 && !defined($tags->{NAM}))
    0 0        
      0        
      0        
      0        
712             {
713             # Could have titles in different langauges - use first
714 0         0 $tags->{NAM} = decode("UTF-16BE", substr($data, 10, $bsize-12));
715             }
716             elsif ($btype==4 && $bsize>12 && $ptype==1)
717             {
718 0         0 $tags->{TOO} = decode("UTF-16BE", substr($data, 10, $bsize-12));
719             }
720 0         0 $data = substr($data, $bsize);
721 0         0 $nblocks-=1;
722             }
723             }
724             }
725 0         0 return 0;
726             }
727              
728              
729             # Pre: $size=size of atom contents
730             # $fh points to start of atom contents
731             # Post: $fh points past end of atom contents
732             sub parse_data
733             {
734 194     194 0 1153 my ($fh, $level, $size, $id, $tags) = @_;
735 194         1943 my ($data, $atom, $type);
736              
737 194 50       532 if (read ($fh, $data, $size) != $size)
738             {
739 0         0 $@ = 'Premature eof';
740 0         0 return -1;
741             }
742              
743             # 3GPP - different format when child of 'udta'.
744             # Let existing tags (if any) override these.
745 194 100 33     4794 if (($id eq 'TITL') ||
      33        
      33        
      33        
      66        
746             ($id eq 'DSCP') ||
747             ($id eq 'CPRT') ||
748             ($id eq 'PERF') ||
749             ($id eq 'AUTH') ||
750             ($id eq 'GNRE'))
751             {
752 9         23 my ($ver) = unpack 'N', $data;
753 9 50       27 if ($ver == 0)
754             {
755 0 0       0 ($size > 7) || return 0;
756 0         0 $size -= 7;
757 0         0 $type = 1;
758 0         0 $data = substr ($data, 6, $size);
759              
760 0 0       0 if ($id eq 'TITL')
    0          
    0          
    0          
761             {
762 0 0       0 return 0 if defined ($tags->{NAM});
763 0         0 $id = 'NAM';
764             }
765             elsif ($id eq 'DSCP')
766             {
767 0 0       0 return 0 if defined ($tags->{CMT});
768 0         0 $id = 'CMT';
769             }
770             elsif ($id eq 'PERF')
771             {
772 0 0       0 return 0 if defined ($tags->{ART});
773 0         0 $id = 'ART';
774             }
775             elsif ($id eq 'AUTH')
776             {
777 0 0       0 return 0 if defined ($tags->{WRT});
778 0         0 $id = 'WRT';
779             }
780             }
781             }
782              
783             # Parse out the tuple that contains aacgain data, etc.
784 194 100 100     1232 if (($id eq 'MEAN') ||
      100        
785             ($id eq 'NAME') ||
786             ($id eq 'DATA'))
787             {
788             # The first 4 or 8 bytes are nulls.
789 24 100       66 if ($id eq 'DATA')
790             {
791 8         22 $data = substr ($data, 8);
792             }
793             else
794             {
795 16         35 $data = substr ($data, 4);
796             }
797              
798 24         30 push @{$tags->{$id}}, $data;
  24         92  
799 24         83 return 0;
800             }
801              
802 170 50       475 if (!defined($type))
803             {
804 170 50       344 ($size > 16) || return 0;
805              
806             # Assumes first atom is the data atom we're after
807 170         2253 ($size,$atom,$type) = unpack 'Na4N', $data;
808 170 50       584 (lc $atom eq 'data') || return 0;
809 170 50       316 ($size > 16) || return 0;
810 170         234 $size -= 16;
811 170         203 $type &= 255;
812 170         1030 $data = substr ($data, 16, $size);
813             }
814 170 50       331 printf " %sType=$type, Size=$size\n", ' 'x(2*$level) if $debug;
815              
816 170 50       525 if ($id eq 'COVR')
    100          
    100          
    50          
817             {
818             # iTunes appears to use random data types for cover art
819 0         0 $tags->{$id} = $data;
820             }
821             elsif ($type==0) # 16bit int data array
822             {
823 34         150 my @ints = unpack 'n' x ($size / 2), $data;
824 34 100 66     158 if ($id eq 'GNRE')
    50          
    0          
825             {
826 9         39 $tags->{$id} = $mp4_genres[$ints[0]];
827             }
828             elsif ($id eq 'DISK' or $id eq 'TRKN')
829             {
830             # Real 10.0 sometimes omits the second integer, but we require it
831 25 50       155 $tags->{$id} = [$ints[1], ($size>=6 ? $ints[2] : 0)] if ($size>=4);
    50          
832             }
833             elsif ($size>=4)
834             {
835 0         0 $tags->{$id} = $ints[1];
836             }
837             }
838             elsif ($type==1) # Char data
839             {
840             # faac 1.24 and Real 10.0 encode data as unspecified 8 bit, which
841             # goes against s8.28 of ISO/IEC 14496-12:2004. How tedious.
842             # Assume data is utf8 if it could be utf8, otherwise assume latin1.
843 117         536 my $decoder = Encode::Guess->guess ($data);
844 117 100       37649 $data = (ref ($decoder)) ?
    100          
845             $decoder->decode($data) : # found one of utf8, utf16, latin1
846             decode($utf8 ? 'utf8' : 'latin1', $data); # ambiguous so force
847              
848 117 100       1151 if ($id eq 'GEN')
    50          
    100          
849             {
850 5 50       24 return 0 if defined ($tags->{GNRE});
851 5         74 $id='GNRE';
852             }
853             elsif ($id eq 'AART')
854             {
855 0 0       0 return 0 if defined ($tags->{ART});
856 0         0 $id = 'ART';
857             }
858             elsif ($id eq 'DAY')
859             {
860 14         67 $data = substr ($data, 0, 4);
861             # Real 10.0 supplies DAY=0 instead of deleting the atom if the
862             # year is not known. What's wrong with these people?
863 14 50       58 return 0 if $data==0;
864             }
865 117         363 $tags->{$id} = $data;
866             }
867             elsif ($type==21) # Integer data
868             {
869             # Convert to an integer if of an appropriate size
870 19 100       49 if ($size==1)
    50          
    0          
    0          
871             {
872 11         41 $tags->{$id} = unpack 'C', $data;
873             }
874             elsif ($size==2)
875             {
876 8         28 $tags->{$id} = unpack 'n', $data;
877             }
878             elsif ($size==4)
879             {
880 0         0 $tags->{$id} = unpack 'N', $data;
881             }
882             elsif ($size==8)
883             {
884 0         0 my ($hi,$lo);
885 0         0 ($hi,$lo) = unpack 'NN', $data;
886 0         0 $tags->{$id} = $hi*(2**32) + $lo;
887             }
888             else
889             {
890             # Non-standard size - just return the raw data
891 0         0 $tags->{$id} = $data;
892             }
893             }
894              
895             # Silently ignore other data types
896 170         510 return 0;
897             }
898              
899             1;
900              
901             __END__