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 14package Image::ExifTool::Ogg; 15 16use strict; 17use vars qw($VERSION); 18use Image::ExifTool qw(:DataAccess :Utils); 19 20$VERSION = '1.02'; 21 22my $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<http://www.xiph.org/vorbis/doc/> 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 42sub ProcessPacket($$) 43{ 44 my ($et, $dataPt) = @_; 45 my $rtnVal = 0; 46 if ($$dataPt =~ /^(.)(vorbis|theora)/s or $$dataPt =~ /^(OpusHead|OpusTags)/) { 47 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 $et->OverrideFileType('OGV') if $type eq 'Theora' and $$et{FILE_TYPE} eq 'OGG'; 50 $et->OverrideFileType('OPUS') if $type eq 'Opus' and $$et{FILE_TYPE} eq 'OGG'; 51 my $tagTablePtr = GetTagTable("Image::ExifTool::${type}::Main"); 52 my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag); 53 return 0 unless $tagInfo and $$tagInfo{SubDirectory}; 54 my $subdir = $$tagInfo{SubDirectory}; 55 my %dirInfo = ( 56 DataPt => $dataPt, 57 DirName => $$tagInfo{Name}, 58 DirStart => $pos, 59 ); 60 my $table = GetTagTable($$subdir{TagTable}); 61 # set group1 so Theoris comments can be distinguised from Vorbis comments 62 $$et{SET_GROUP1} = $type if $type eq 'Theora'; 63 SetByteOrder($$subdir{ByteOrder}) if $$subdir{ByteOrder}; 64 $rtnVal = $et->ProcessDirectory(\%dirInfo, $table); 65 SetByteOrder('II'); 66 delete $$et{SET_GROUP1}; 67 } 68 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 75sub ProcessOGG($$) 76{ 77 my ($et, $dirInfo) = @_; 78 79 # must first check for leading/trailing ID3 information 80 unless ($$et{DoneID3}) { 81 require Image::ExifTool::ID3; 82 Image::ExifTool::ID3::ProcessID3($et, $dirInfo) and return 1; 83 } 84 my $raf = $$dirInfo{RAF}; 85 my $verbose = $et->Options('Verbose'); 86 my $out = $et->Options('TextOut'); 87 my ($success, $page, $packets, $streams, $stream) = (0,0,0,0,''); 88 my ($buff, $flag, %val, $numFlac, %streamPage); 89 90 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 if ($raf and $raf->Read($buff, 28) == 28) { 95 # validate magic number 96 unless ($buff =~ /^OggS/) { 97 $success and $et->Warn('Lost synchronization'); 98 last; 99 } 100 unless ($success) { 101 # set file type and initialize on first page 102 $success = 1; 103 $et->SetFileType(); 104 SetByteOrder('II'); 105 } 106 $flag = Get8u(\$buff, 5); # page flag 107 $stream = Get32u(\$buff, 14); # stream serial number 108 if ($flag & 0x02) { 109 ++$streams; # count start-of-stream pages 110 $streamPage{$stream} = $page = 0; 111 } else { 112 $page = $streamPage{$stream}; 113 } 114 ++$packets unless $flag & 0x01; # keep track of packet count 115 } else { 116 # all done unless we have to process our last packet 117 last unless %val; 118 ($stream) = sort keys %val; # take a stream 119 $flag = 0; # no continuation 120 undef $raf; # flag for done reading 121 } 122 123 if (defined $numFlac) { 124 # stop to process FLAC headers if we hit the end of file 125 last unless $raf; 126 --$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 if (defined $val{$stream} and not $flag & 0x01) { 131 ProcessPacket($et, \$val{$stream}); 132 delete $val{$stream}; 133 # only read the first $MAX_PACKETS packets from each stream 134 if ($packets > $MAX_PACKETS * $streams or not defined $raf) { 135 last unless %val; # all done (success!) 136 } 137 } 138 # stop processing Ogg if we have scanned enough packets 139 last if $packets > $MAX_PACKETS * $streams and not %val; 140 } 141 142 # continue processing the current page 143 my $pageNum = Get32u(\$buff, 18); # page sequence number 144 my $nseg = Get8u(\$buff, 26); # number of segments 145 # calculate total data length 146 my $dataLen = Get8u(\$buff, 27); 147 if ($nseg) { 148 $raf->Read($buff, $nseg-1) == $nseg-1 or last; 149 my @segs = unpack('C*', $buff); 150 # could check that all these (but the last) are 255... 151 foreach (@segs) { $dataLen += $_ } 152 } 153 if (defined $page) { 154 if ($page == $pageNum) { 155 $streamPage{$stream} = ++$page; 156 } else { 157 $et->Warn('Missing page(s) in Ogg file'); 158 undef $page; 159 delete $streamPage{$stream}; 160 } 161 } 162 # read page data 163 $raf->Read($buff, $dataLen) == $dataLen or last; 164 if ($verbose > 1) { 165 printf $out "Page %d, stream 0x%x, flag 0x%x (%d bytes)\n", 166 $pageNum, $stream, $flag, $dataLen; 167 $et->VerboseDump(\$buff, DataPos => $raf->Tell() - $dataLen); 168 } 169 if (defined $val{$stream}) { 170 $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 if ($buff =~ /^(.(vorbis|theora)|Opus(Head|Tags))/s) { 174 $val{$stream} = $buff; # save this page 175 } elsif ($buff =~ /^\x7fFLAC..(..)/s) { 176 $numFlac = unpack('n',$1); 177 $val{$stream} = substr($buff, 9); 178 } 179 } 180 if (defined $numFlac) { 181 # stop to process FLAC headers if we have them all 182 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 ProcessPacket($et, \$val{$stream}); 186 delete $val{$stream}; 187 } 188 } 189 if (defined $numFlac and defined $val{$stream}) { 190 # process FLAC headers as if it was a complete FLAC file 191 require Image::ExifTool::FLAC; 192 my %dirInfo = ( RAF => new File::RandomAccess(\$val{$stream}) ); 193 Image::ExifTool::FLAC::ProcessFLAC($et, \%dirInfo); 194 } 195 return $success; 196} 197 1981; # end 199 200__END__ 201 202=head1 NAME 203 204Image::ExifTool::Ogg - Read Ogg meta information 205 206=head1 SYNOPSIS 207 208This module is used by Image::ExifTool 209 210=head1 DESCRIPTION 211 212This module contains definitions required by Image::ExifTool to extract meta 213information from Ogg bitstream container files. 214 215=head1 AUTHOR 216 217Copyright 2003-2021, Phil Harvey (philharvey66 at gmail.com) 218 219This library is free software; you can redistribute it and/or modify it 220under the same terms as Perl itself. 221 222=head1 REFERENCES 223 224=over 4 225 226=item L<http://www.xiph.org/vorbis/doc/> 227 228=item L<http://flac.sourceforge.net/ogg_mapping.html> 229 230=item L<http://www.theora.org/doc/Theora.pdf> 231 232=back 233 234=head1 SEE ALSO 235 236L<Image::ExifTool::TagNames/Ogg Tags>, 237L<Image::ExifTool(3pm)|Image::ExifTool> 238 239=cut 240 241