| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | #------------------------------------------------------------------------------ | 
| 2 |  |  |  |  |  |  | # File:         Ogg.pm | 
| 3 |  |  |  |  |  |  | # | 
| 4 |  |  |  |  |  |  | # Description:  Read Ogg meta information | 
| 5 |  |  |  |  |  |  | # | 
| 6 |  |  |  |  |  |  | # Revisions:    2011/07/13 - P. Harvey Created (split from Vorbis.pm) | 
| 7 |  |  |  |  |  |  | #               2016/07/14 - PH Added Ogg Opus support | 
| 8 |  |  |  |  |  |  | # | 
| 9 |  |  |  |  |  |  | # References:   1) http://www.xiph.org/vorbis/doc/ | 
| 10 |  |  |  |  |  |  | #               2) http://flac.sourceforge.net/ogg_mapping.html | 
| 11 |  |  |  |  |  |  | #               3) http://www.theora.org/doc/Theora.pdf | 
| 12 |  |  |  |  |  |  | #------------------------------------------------------------------------------ | 
| 13 |  |  |  |  |  |  |  | 
| 14 |  |  |  |  |  |  | package Image::ExifTool::Ogg; | 
| 15 |  |  |  |  |  |  |  | 
| 16 | 3 |  |  | 3 |  | 21 | use strict; | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 3 |  |  |  |  | 129 |  | 
| 17 | 3 |  |  | 3 |  | 19 | use vars qw($VERSION); | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 3 |  |  |  |  | 159 |  | 
| 18 | 3 |  |  | 3 |  | 17 | use Image::ExifTool qw(:DataAccess :Utils); | 
|  | 3 |  |  |  |  | 5 |  | 
|  | 3 |  |  |  |  | 4547 |  | 
| 19 |  |  |  |  |  |  |  | 
| 20 |  |  |  |  |  |  | $VERSION = '1.02'; | 
| 21 |  |  |  |  |  |  |  | 
| 22 |  |  |  |  |  |  | my $MAX_PACKETS = 2;    # maximum packets to scan from each stream at start of file | 
| 23 |  |  |  |  |  |  |  | 
| 24 |  |  |  |  |  |  | # Information types recognizedi in Ogg files | 
| 25 |  |  |  |  |  |  | %Image::ExifTool::Ogg::Main = ( | 
| 26 |  |  |  |  |  |  | NOTES => q{ | 
| 27 |  |  |  |  |  |  | ExifTool extracts the following types of information from Ogg files.  See | 
| 28 |  |  |  |  |  |  | L for the Ogg specification. | 
| 29 |  |  |  |  |  |  | }, | 
| 30 |  |  |  |  |  |  | # (these are for documentation purposes only, and aren't used by the code below) | 
| 31 |  |  |  |  |  |  | vorbis => { SubDirectory => { TagTable => 'Image::ExifTool::Vorbis::Main' } }, | 
| 32 |  |  |  |  |  |  | theora => { SubDirectory => { TagTable => 'Image::ExifTool::Theora::Main' } }, | 
| 33 |  |  |  |  |  |  | Opus   => { SubDirectory => { TagTable => 'Image::ExifTool::Opus::Main' } }, | 
| 34 |  |  |  |  |  |  | FLAC   => { SubDirectory => { TagTable => 'Image::ExifTool::FLAC::Main' } }, | 
| 35 |  |  |  |  |  |  | ID3    => { SubDirectory => { TagTable => 'Image::ExifTool::ID3::Main' } }, | 
| 36 |  |  |  |  |  |  | ); | 
| 37 |  |  |  |  |  |  |  | 
| 38 |  |  |  |  |  |  | #------------------------------------------------------------------------------ | 
| 39 |  |  |  |  |  |  | # Process Ogg packet | 
| 40 |  |  |  |  |  |  | # Inputs: 0) ExifTool object ref, 1) data ref | 
| 41 |  |  |  |  |  |  | # Returns: 1 on success | 
| 42 |  |  |  |  |  |  | sub ProcessPacket($$) | 
| 43 |  |  |  |  |  |  | { | 
| 44 | 4 |  |  | 4 | 0 | 10 | my ($et, $dataPt) = @_; | 
| 45 | 4 |  |  |  |  | 7 | my $rtnVal = 0; | 
| 46 | 4 | 50 | 66 |  |  | 38 | if ($$dataPt =~ /^(.)(vorbis|theora)/s or $$dataPt =~ /^(OpusHead|OpusTags)/) { | 
| 47 | 4 | 100 |  |  |  | 32 | my ($tag, $type, $pos) = $2 ? (ord($1), ucfirst($2), 7) : ($1, 'Opus', 8); | 
| 48 |  |  |  |  |  |  | # this is an OGV file if it contains Theora video | 
| 49 | 4 | 50 | 33 |  |  | 13 | $et->OverrideFileType('OGV') if $type eq 'Theora' and $$et{FILE_TYPE} eq 'OGG'; | 
| 50 | 4 | 100 | 66 |  |  | 21 | $et->OverrideFileType('OPUS') if $type eq 'Opus' and $$et{FILE_TYPE} eq 'OGG'; | 
| 51 | 4 |  |  |  |  | 34 | my $tagTablePtr = GetTagTable("Image::ExifTool::${type}::Main"); | 
| 52 | 4 |  |  |  |  | 13 | my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag); | 
| 53 | 4 | 50 | 33 |  |  | 20 | return 0 unless $tagInfo and $$tagInfo{SubDirectory}; | 
| 54 | 4 |  |  |  |  | 9 | my $subdir = $$tagInfo{SubDirectory}; | 
| 55 |  |  |  |  |  |  | my %dirInfo = ( | 
| 56 |  |  |  |  |  |  | DataPt   => $dataPt, | 
| 57 |  |  |  |  |  |  | DirName  => $$tagInfo{Name}, | 
| 58 | 4 |  |  |  |  | 15 | DirStart => $pos, | 
| 59 |  |  |  |  |  |  | ); | 
| 60 | 4 |  |  |  |  | 11 | my $table = GetTagTable($$subdir{TagTable}); | 
| 61 |  |  |  |  |  |  | # set group1 so Theoris comments can be distinguised from Vorbis comments | 
| 62 | 4 | 50 |  |  |  | 13 | $$et{SET_GROUP1} = $type if $type eq 'Theora'; | 
| 63 | 4 | 50 |  |  |  | 20 | SetByteOrder($$subdir{ByteOrder}) if $$subdir{ByteOrder}; | 
| 64 | 4 |  |  |  |  | 17 | $rtnVal = $et->ProcessDirectory(\%dirInfo, $table); | 
| 65 | 4 |  |  |  |  | 14 | SetByteOrder('II'); | 
| 66 | 4 |  |  |  |  | 13 | delete $$et{SET_GROUP1}; | 
| 67 |  |  |  |  |  |  | } | 
| 68 | 4 |  |  |  |  | 9 | return $rtnVal; | 
| 69 |  |  |  |  |  |  | } | 
| 70 |  |  |  |  |  |  |  | 
| 71 |  |  |  |  |  |  | #------------------------------------------------------------------------------ | 
| 72 |  |  |  |  |  |  | # Extract information from an Ogg file | 
| 73 |  |  |  |  |  |  | # Inputs: 0) ExifTool object reference, 1) dirInfo reference | 
| 74 |  |  |  |  |  |  | # Returns: 1 on success, 0 if this wasn't a valid Ogg file | 
| 75 |  |  |  |  |  |  | sub ProcessOGG($$) | 
| 76 |  |  |  |  |  |  | { | 
| 77 | 3 |  |  | 3 | 0 | 8 | my ($et, $dirInfo) = @_; | 
| 78 |  |  |  |  |  |  |  | 
| 79 |  |  |  |  |  |  | # must first check for leading/trailing ID3 information | 
| 80 | 3 | 50 |  |  |  | 13 | unless ($$et{DoneID3}) { | 
| 81 | 3 |  |  |  |  | 1238 | require Image::ExifTool::ID3; | 
| 82 | 3 | 50 |  |  |  | 34 | Image::ExifTool::ID3::ProcessID3($et, $dirInfo) and return 1; | 
| 83 |  |  |  |  |  |  | } | 
| 84 | 3 |  |  |  |  | 10 | my $raf = $$dirInfo{RAF}; | 
| 85 | 3 |  |  |  |  | 14 | my $verbose = $et->Options('Verbose'); | 
| 86 | 3 |  |  |  |  | 10 | my $out = $et->Options('TextOut'); | 
| 87 | 3 |  |  |  |  | 10 | my ($success, $page, $packets, $streams, $stream) = (0,0,0,0,''); | 
| 88 | 3 |  |  |  |  | 7 | my ($buff, $flag, %val, $numFlac, %streamPage); | 
| 89 |  |  |  |  |  |  |  | 
| 90 | 3 |  |  |  |  | 5 | for (;;) { | 
| 91 |  |  |  |  |  |  | # must read ahead to next page to see if it is a continuation | 
| 92 |  |  |  |  |  |  | # (this code would be a lot simpler if the continuation flag | 
| 93 |  |  |  |  |  |  | #  was on the leading instead of the trailing page!) | 
| 94 | 10 | 100 | 66 |  |  | 37 | if ($raf and $raf->Read($buff, 28) == 28) { | 
| 95 |  |  |  |  |  |  | # validate magic number | 
| 96 | 9 | 50 |  |  |  | 80 | unless ($buff =~ /^OggS/) { | 
| 97 | 0 | 0 |  |  |  | 0 | $success and $et->Warn('Lost synchronization'); | 
| 98 | 0 |  |  |  |  | 0 | last; | 
| 99 |  |  |  |  |  |  | } | 
| 100 | 9 | 100 |  |  |  | 54 | unless ($success) { | 
| 101 |  |  |  |  |  |  | # set file type and initialize on first page | 
| 102 | 3 |  |  |  |  | 7 | $success = 1; | 
| 103 | 3 |  |  |  |  | 30 | $et->SetFileType(); | 
| 104 | 3 |  |  |  |  | 14 | SetByteOrder('II'); | 
| 105 |  |  |  |  |  |  | } | 
| 106 | 9 |  |  |  |  | 27 | $flag = Get8u(\$buff, 5);       # page flag | 
| 107 | 9 |  |  |  |  | 26 | $stream = Get32u(\$buff, 14);   # stream serial number | 
| 108 | 9 | 100 |  |  |  | 23 | if ($flag & 0x02) { | 
| 109 | 3 |  |  |  |  | 5 | ++$streams;                 # count start-of-stream pages | 
| 110 | 3 |  |  |  |  | 11 | $streamPage{$stream} = $page = 0; | 
| 111 |  |  |  |  |  |  | } else { | 
| 112 | 6 |  |  |  |  | 14 | $page = $streamPage{$stream}; | 
| 113 |  |  |  |  |  |  | } | 
| 114 | 9 | 100 |  |  |  | 22 | ++$packets unless $flag & 0x01; # keep track of packet count | 
| 115 |  |  |  |  |  |  | } else { | 
| 116 |  |  |  |  |  |  | # all done unless we have to process our last packet | 
| 117 | 1 | 50 |  |  |  | 4 | last unless %val; | 
| 118 | 1 |  |  |  |  | 14 | ($stream) = sort keys %val;     # take a stream | 
| 119 | 1 |  |  |  |  | 2 | $flag = 0;                      # no continuation | 
| 120 | 1 |  |  |  |  | 2 | undef $raf;                     # flag for done reading | 
| 121 |  |  |  |  |  |  | } | 
| 122 |  |  |  |  |  |  |  | 
| 123 | 10 | 100 |  |  |  | 35 | if (defined $numFlac) { | 
| 124 |  |  |  |  |  |  | # stop to process FLAC headers if we hit the end of file | 
| 125 | 2 | 100 |  |  |  | 5 | last unless $raf; | 
| 126 | 1 |  |  |  |  | 2 | --$numFlac; # one less header packet to read | 
| 127 |  |  |  |  |  |  | } else { | 
| 128 |  |  |  |  |  |  | # can finally process previous packet from this stream | 
| 129 |  |  |  |  |  |  | # unless this is a continuation page | 
| 130 | 8 | 100 | 100 |  |  | 53 | if (defined $val{$stream} and not $flag & 0x01) { | 
| 131 | 4 |  |  |  |  | 16 | ProcessPacket($et, \$val{$stream}); | 
| 132 | 4 |  |  |  |  | 11 | delete $val{$stream}; | 
| 133 |  |  |  |  |  |  | # only read the first $MAX_PACKETS packets from each stream | 
| 134 | 4 | 100 | 66 |  |  | 19 | if ($packets > $MAX_PACKETS * $streams or not defined $raf) { | 
| 135 | 2 | 50 |  |  |  | 25 | last unless %val;   # all done (success!) | 
| 136 |  |  |  |  |  |  | } | 
| 137 |  |  |  |  |  |  | } | 
| 138 |  |  |  |  |  |  | # stop processing Ogg if we have scanned enough packets | 
| 139 | 6 | 50 | 33 |  |  | 18 | last if $packets > $MAX_PACKETS * $streams and not %val; | 
| 140 |  |  |  |  |  |  | } | 
| 141 |  |  |  |  |  |  |  | 
| 142 |  |  |  |  |  |  | # continue processing the current page | 
| 143 | 7 |  |  |  |  | 19 | my $pageNum = Get32u(\$buff, 18);   # page sequence number | 
| 144 | 7 |  |  |  |  | 25 | my $nseg = Get8u(\$buff, 26);       # number of segments | 
| 145 |  |  |  |  |  |  | # calculate total data length | 
| 146 | 7 |  |  |  |  | 17 | my $dataLen = Get8u(\$buff, 27); | 
| 147 | 7 | 50 |  |  |  | 15 | if ($nseg) { | 
| 148 | 7 | 50 |  |  |  | 24 | $raf->Read($buff, $nseg-1) == $nseg-1 or last; | 
| 149 | 7 |  |  |  |  | 23 | my @segs = unpack('C*', $buff); | 
| 150 |  |  |  |  |  |  | # could check that all these (but the last) are 255... | 
| 151 | 7 |  |  |  |  | 16 | foreach (@segs) { $dataLen += $_ } | 
|  | 22 |  |  |  |  | 31 |  | 
| 152 |  |  |  |  |  |  | } | 
| 153 | 7 | 50 |  |  |  | 17 | if (defined $page) { | 
| 154 | 7 | 50 |  |  |  | 15 | if ($page == $pageNum) { | 
| 155 | 7 |  |  |  |  | 14 | $streamPage{$stream} = ++$page; | 
| 156 |  |  |  |  |  |  | } else { | 
| 157 | 0 |  |  |  |  | 0 | $et->Warn('Missing page(s) in Ogg file'); | 
| 158 | 0 |  |  |  |  | 0 | undef $page; | 
| 159 | 0 |  |  |  |  | 0 | delete $streamPage{$stream}; | 
| 160 |  |  |  |  |  |  | } | 
| 161 |  |  |  |  |  |  | } | 
| 162 |  |  |  |  |  |  | # read page data | 
| 163 | 7 | 50 |  |  |  | 18 | $raf->Read($buff, $dataLen) == $dataLen or last; | 
| 164 | 7 | 50 |  |  |  | 24 | if ($verbose > 1) { | 
| 165 | 0 |  |  |  |  | 0 | printf $out "Page %d, stream 0x%x, flag 0x%x (%d bytes)\n", | 
| 166 |  |  |  |  |  |  | $pageNum, $stream, $flag, $dataLen; | 
| 167 | 0 |  |  |  |  | 0 | $et->VerboseDump(\$buff, DataPos => $raf->Tell() - $dataLen); | 
| 168 |  |  |  |  |  |  | } | 
| 169 | 7 | 100 |  |  |  | 20 | if (defined $val{$stream}) { | 
|  |  | 50 |  |  |  |  |  | 
| 170 | 2 |  |  |  |  | 21 | $val{$stream} .= $buff;     # add this continuation page | 
| 171 |  |  |  |  |  |  | } elsif (not $flag & 0x01) {    # ignore remaining pages of a continued packet | 
| 172 |  |  |  |  |  |  | # ignore the first page of any packet we aren't parsing | 
| 173 | 5 | 100 |  |  |  | 33 | if ($buff =~ /^(.(vorbis|theora)|Opus(Head|Tags))/s) { | 
|  |  | 50 |  |  |  |  |  | 
| 174 | 4 |  |  |  |  | 12 | $val{$stream} = $buff;      # save this page | 
| 175 |  |  |  |  |  |  | } elsif ($buff =~ /^\x7fFLAC..(..)/s) { | 
| 176 | 1 |  |  |  |  | 4 | $numFlac = unpack('n',$1); | 
| 177 | 1 |  |  |  |  | 4 | $val{$stream} = substr($buff, 9); | 
| 178 |  |  |  |  |  |  | } | 
| 179 |  |  |  |  |  |  | } | 
| 180 | 7 | 100 | 33 |  |  | 42 | if (defined $numFlac) { | 
|  |  | 50 |  |  |  |  |  | 
| 181 |  |  |  |  |  |  | # stop to process FLAC headers if we have them all | 
| 182 | 2 | 50 |  |  |  | 8 | last if $numFlac <= 0; | 
| 183 |  |  |  |  |  |  | } elsif (defined $val{$stream} and $flag & 0x04) { | 
| 184 |  |  |  |  |  |  | # process Ogg packet now if end-of-stream bit is set | 
| 185 | 0 |  |  |  |  | 0 | ProcessPacket($et, \$val{$stream}); | 
| 186 | 0 |  |  |  |  | 0 | delete $val{$stream}; | 
| 187 |  |  |  |  |  |  | } | 
| 188 |  |  |  |  |  |  | } | 
| 189 | 3 | 100 | 66 |  |  | 27 | if (defined $numFlac and defined $val{$stream}) { | 
| 190 |  |  |  |  |  |  | # process FLAC headers as if it was a complete FLAC file | 
| 191 | 1 |  |  |  |  | 8 | require Image::ExifTool::FLAC; | 
| 192 | 1 |  |  |  |  | 6 | my %dirInfo = ( RAF => new File::RandomAccess(\$val{$stream}) ); | 
| 193 | 1 |  |  |  |  | 5 | Image::ExifTool::FLAC::ProcessFLAC($et, \%dirInfo); | 
| 194 |  |  |  |  |  |  | } | 
| 195 | 3 |  |  |  |  | 17 | return $success; | 
| 196 |  |  |  |  |  |  | } | 
| 197 |  |  |  |  |  |  |  | 
| 198 |  |  |  |  |  |  | 1;  # end | 
| 199 |  |  |  |  |  |  |  | 
| 200 |  |  |  |  |  |  | __END__ |