#------------------------------------------------------------------------------ # File: QuickTime.pm # # Description: Read QuickTime, MP4 and M4A meta information # # Revisions: 10/04/2005 - P. Harvey Created # 12/19/2005 - P. Harvey Added MP4 support # 09/22/2006 - P. Harvey Added M4A support # # References: 1) http://developer.apple.com/documentation/QuickTime/ # 2) http://search.cpan.org/dist/MP4-Info-1.04/ # 3) http://www.geocities.com/xhelmboyx/quicktime/formats/mp4-layout.txt # 4) http://wiki.multimedia.cx/index.php?title=Apple_QuickTime # 5) ISO 14496-12 (http://neuron2.net/library/avc/c041828_ISO_IEC_14496-12_2005(E).pdf) # 6) ISO 14496-16 (http://www.iec-normen.de/previewpdf/info_isoiec14496-16%7Bed2.0%7Den.pdf) # 7) http://atomicparsley.sourceforge.net/mpeg-4files.html # 8) http://wiki.multimedia.cx/index.php?title=QuickTime_container # 9) http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart3.pdf (Oct 2008) #------------------------------------------------------------------------------ package Image::ExifTool::QuickTime; use strict; use vars qw($VERSION); use Image::ExifTool qw(:DataAccess :Utils); use Image::ExifTool::Exif; $VERSION = '1.20'; sub FixWrongFormat($); sub ProcessMOV($$;$); sub ProcessKeys($$$); # information for time/date-based tags (time zero is Jan 1, 1904) my %timeInfo = ( Groups => { 2 => 'Time' }, # Note: This value will be in UTC if generated by a system that is aware of the time zone ValueConv => 'ConvertUnixTime($val - ((66 * 365 + 17) * 24 * 3600))', PrintConv => '$self->ConvertDateTime($val)', ); # information for duration tags my %durationInfo = ( ValueConv => '$$self{TimeScale} ? $val / $$self{TimeScale} : $val', PrintConv => '$$self{TimeScale} ? ConvertDuration($val) : $val', ); # 4-character Vendor ID codes (ref PH) my %vendorID = ( appl => 'Apple', FFMP => 'FFmpeg', kdak => 'Kodak', KMPI => 'Konica-Minolta', mino => 'Minolta', niko => 'Nikon', NIKO => 'Nikon', olym => 'Olympus', pana => 'Panasonic', pent => 'Pentax', sany => 'Sanyo', ); # QuickTime atoms %Image::ExifTool::QuickTime::Main = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => q{ The QuickTime file format is used for MOV and MP4 videos and QTIF images. Exiftool extracts meta information from the UserData atom (including some proprietary manufacturer-specific information), as well as extracting various audio, video and image parameters. Tags with a question mark after their name are not extracted unless the Unknown option is set. }, free => { Unknown => 1, Binary => 1 }, skip => { Unknown => 1, Binary => 1 }, wide => { Unknown => 1, Binary => 1 }, ftyp => { #MP4 Name => 'FrameType', Unknown => 1, Notes => 'MP4 only', Binary => 1, }, pnot => { Name => 'Preview', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Preview' }, }, PICT => { Name => 'PreviewPICT', Binary => 1, }, pict => { #8 Name => 'PreviewPICT', Binary => 1, }, moov => { Name => 'Movie', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Movie' }, }, mdat => { Unknown => 1, Binary => 1 }, junk => { Unknown => 1, Binary => 1 }, #8 uuid => [ { #9 (MP4 files) Name => 'UUID-XMP', Condition => '$$valPt=~/^\xbe\x7a\xcf\xcb\x97\xa9\x42\xe8\x9c\x71\x99\x94\x91\xe3\xaf\xac/', SubDirectory => { TagTable => 'Image::ExifTool::XMP::Main', Start => 16, }, }, { #8 Name => 'UUID-Unknown', Unknown => 1, Binary => 1, }, ], ); # atoms used in QTIF files %Image::ExifTool::QuickTime::ImageFile = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Image' }, NOTES => 'Tags used in QTIF QuickTime Image Files.', idsc => { Name => 'ImageDescription', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::ImageDesc' }, }, idat => { Name => 'ImageData', Binary => 1, }, iicc => { Name => 'ICC_Profile', SubDirectory => { TagTable => 'Image::ExifTool::ICC_Profile::Main' }, }, ); # image description data block %Image::ExifTool::QuickTime::ImageDesc = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Image' }, FORMAT => 'int16u', 2 => { Name => 'CompressorID', Format => 'string[4]' }, 10 => { Name => 'VendorID', Format => 'string[4]', RawConv => 'length $val ? $val : undef', PrintConv => \%vendorID, SeparateTable => 'VendorID', }, # 14 - ("Quality" in QuickTime docs) ?? 16 => 'ImageWidth', 17 => 'ImageHeight', 18 => { Name => 'XResolution', Format => 'fixed32u' }, 20 => { Name => 'YResolution', Format => 'fixed32u' }, # 24 => 'FrameCount', # always 1 (what good is this?) 25 => { Name => 'CompressorName', Format => 'string[32]', # (sometimes this is a Pascal string, and sometimes it is a C string) RawConv => q{ $val=substr($val,1,ord($1)) if $val=~/^([\0-\x1f])/ and ord($1) 'BitDepth', ); # preview data block %Image::ExifTool::QuickTime::Preview = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Image' }, FORMAT => 'int16u', 0 => { Name => 'PreviewDate', Format => 'int32u', %timeInfo, }, 2 => 'PreviewVersion', 3 => { Name => 'PreviewAtomType', Format => 'string[4]', }, 5 => 'PreviewAtomIndex', ); # movie atoms %Image::ExifTool::QuickTime::Movie = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, mvhd => { Name => 'MovieHeader', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::MovieHdr' }, }, trak => { Name => 'Track', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Track' }, }, udta => { Name => 'UserData', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::UserData' }, }, meta => { # 'meta' is found here in my EX-F1 MOV sample - PH Name => 'Meta', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Meta' }, }, ); # movie header data block %Image::ExifTool::QuickTime::MovieHdr = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Video' }, FORMAT => 'int32u', 0 => { Name => 'Version', Format => 'int8u' }, 1 => { Name => 'CreateDate', %timeInfo, }, 2 => { Name => 'ModifyDate', %timeInfo, }, 3 => { Name => 'TimeScale', RawConv => '$$self{TimeScale} = $val', }, 4 => { Name => 'Duration', %durationInfo }, 5 => { Name => 'PreferredRate', ValueConv => '$val / 0x10000', }, 6 => { Name => 'PreferredVolume', Format => 'int16u', ValueConv => '$val / 256', PrintConv => 'sprintf("%.2f%%", $val * 100)', }, 18 => { Name => 'PreviewTime', %durationInfo }, 19 => { Name => 'PreviewDuration', %durationInfo }, 20 => { Name => 'PosterTime', %durationInfo }, 21 => { Name => 'SelectionTime', %durationInfo }, 22 => { Name => 'SelectionDuration',%durationInfo }, 23 => { Name => 'CurrentTime', %durationInfo }, 24 => 'NextTrackID', ); # track atoms %Image::ExifTool::QuickTime::Track = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, tkhd => { Name => 'TrackHeader', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::TrackHdr' }, }, udta => { Name => 'UserData', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::UserData' }, }, mdia => { #MP4 Name => 'Media', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Media' }, }, ); # track header data block %Image::ExifTool::QuickTime::TrackHdr = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 1 => 'Track#', 2 => 'Video' }, FORMAT => 'int32u', 0 => { Name => 'TrackVersion', Format => 'int8u', Priority => 0, }, 1 => { Name => 'TrackCreateDate', Priority => 0, %timeInfo, }, 2 => { Name => 'TrackModifyDate', Priority => 0, %timeInfo, }, 3 => { Name => 'TrackID', Priority => 0, }, 5 => { Name => 'TrackDuration', Priority => 0, %durationInfo, }, 8 => { Name => 'TrackLayer', Format => 'int16u', Priority => 0, }, 9 => { Name => 'TrackVolume', Format => 'int16u', Priority => 0, ValueConv => '$val / 256', PrintConv => 'sprintf("%.2f%%", $val * 100)', }, 19 => { Name => 'ImageWidth', Priority => 0, RawConv => \&FixWrongFormat, }, 20 => { Name => 'ImageHeight', Priority => 0, RawConv => \&FixWrongFormat, }, ); # user data atoms %Image::ExifTool::QuickTime::UserData = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => q{ Tag ID's beginning with the copyright symbol (hex 0xa9) are multi-language text, but ExifTool only extracts the text from the first language in the record. ExifTool will extract any multi-language user data tags found, even if they don't exist in this table. }, "\xa9cpy" => 'Copyright', "\xa9day" => 'CreateDate', "\xa9dir" => 'Director', "\xa9ed1" => 'Edit1', "\xa9ed2" => 'Edit2', "\xa9ed3" => 'Edit3', "\xa9ed4" => 'Edit4', "\xa9ed5" => 'Edit5', "\xa9ed6" => 'Edit6', "\xa9ed7" => 'Edit7', "\xa9ed8" => 'Edit8', "\xa9ed9" => 'Edit9', "\xa9fmt" => 'Format', "\xa9inf" => 'Information', "\xa9prd" => 'Producer', "\xa9prf" => 'Performers', "\xa9req" => 'Requirements', "\xa9src" => 'Source', "\xa9wrt" => 'Writer', name => 'Name', WLOC => { Name => 'WindowLocation', Format => 'int16u', }, LOOP => { Name => 'LoopStyle', Format => 'int32u', PrintConv => { 1 => 'Normal', 2 => 'Palindromic', }, }, SelO => { Name => 'PlaySelection', Format => 'int8u', }, AllF => { Name => 'PlayAllFrames', Format => 'int8u', }, meta => { Name => 'Meta', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Meta', Start => 4, # must skip 4-byte version number header }, }, DcMD => { Name => 'DcMD', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::DcMD', }, }, 'ptv '=> { Name => 'PrintToVideo', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Video' }, }, 'hnti'=> { Name => 'HintInfo', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::HintInfo' }, }, 'hinf' => { Name => 'HintTrackInfo', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::HintTrackInfo' }, }, TAGS => [ #PH # these tags were initially discovered in a Pentax movie, # but similar information is found in videos from other manufacturers { Name => 'KodakTags', Condition => '$$valPt =~ /^EASTMAN KODAK COMPANY/', SubDirectory => { TagTable => 'Image::ExifTool::Kodak::MOV', ByteOrder => 'LittleEndian', }, }, { Name => 'KonicaMinoltaTags', Condition => '$$valPt =~ /^KONICA MINOLTA DIGITAL CAMERA/', SubDirectory => { TagTable => 'Image::ExifTool::Minolta::MOV1', ByteOrder => 'LittleEndian', }, }, { Name => 'MinoltaTags', Condition => '$$valPt =~ /^MINOLTA DIGITAL CAMERA/', SubDirectory => { TagTable => 'Image::ExifTool::Minolta::MOV2', ByteOrder => 'LittleEndian', }, }, { Name => 'NikonTags', Condition => '$$valPt =~ /^NIKON DIGITAL CAMERA\0/', SubDirectory => { TagTable => 'Image::ExifTool::Nikon::MOV', ByteOrder => 'LittleEndian', }, }, { Name => 'OlympusTags1', Condition => '$$valPt =~ /^OLYMPUS DIGITAL CAMERA\0.{9}\x01\0/s', SubDirectory => { TagTable => 'Image::ExifTool::Olympus::MOV1', ByteOrder => 'LittleEndian', }, }, { Name => 'OlympusTags2', Condition => '$$valPt =~ /^OLYMPUS DIGITAL CAMERA\0/', SubDirectory => { TagTable => 'Image::ExifTool::Olympus::MOV2', ByteOrder => 'LittleEndian', }, }, { Name => 'PentaxTags', Condition => '$$valPt =~ /^PENTAX DIGITAL CAMERA\0/', SubDirectory => { TagTable => 'Image::ExifTool::Pentax::MOV', ByteOrder => 'LittleEndian', }, }, { Name => 'SanyoMOV', Condition => q{ $$valPt =~ /^SANYO DIGITAL CAMERA\0/ and $self->{VALUE}->{FileType} eq "MOV" }, SubDirectory => { TagTable => 'Image::ExifTool::Sanyo::MOV', ByteOrder => 'LittleEndian', }, }, { Name => 'SanyoMP4', Condition => q{ $$valPt =~ /^SANYO DIGITAL CAMERA\0/ and $self->{VALUE}->{FileType} eq "MP4" }, SubDirectory => { TagTable => 'Image::ExifTool::Sanyo::MP4', ByteOrder => 'LittleEndian', }, }, { Name => 'UnknownTags', Unknown => 1, Binary => 1 }, ], QVMI => { #PH Name => 'CasioQVMI', # Casio stores standard EXIF-format information in MOV videos (ie. EX-S880) SubDirectory => { TagTable => 'Image::ExifTool::Exif::Main', DirName => 'IFD0', Start => 10, ByteOrder => 'BigEndian', }, }, MMA0 => { #PH Name => 'MinoltaMMA0', # (DiMage 7Hi) SubDirectory => { TagTable => 'Image::ExifTool::Minolta::MMA' }, }, MMA1 => { #PH Name => 'MinoltaMMA1', # (Dimage A2) SubDirectory => { TagTable => 'Image::ExifTool::Minolta::MMA' }, }, XMP_ => { #PH (Adobe CS3 Bridge) Name => 'XMP', SubDirectory => { TagTable => 'Image::ExifTool::XMP::Main' }, }, ); # meta atoms %Image::ExifTool::QuickTime::Meta = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, ilst => { Name => 'InfoList', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::InfoList', HasData => 1, # process atoms as containers with 'data' elements }, }, # MP4 tags (ref 5) hdlr => { Name => 'Handler', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Handler' }, }, # dinf - MP4 data information # ipmc - MP4 IPMP control # iloc - MP4 item location # ipro - MP4 item protection # iinf - MP4 item information 'xml ' => { Name => 'XML', SubDirectory => { TagTable => 'Image::ExifTool::XMP::Main' }, }, 'keys' => { Name => 'Keys', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Keys' }, }, # bxml - MP4 binary XML # pitm - MP4 primary item reference ); # info list atoms # -> these atoms are unique, and contain one or more 'data' atoms %Image::ExifTool::QuickTime::InfoList = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, NOTES => q{ As well as these tags, the 'mdta' handler uses numerical tag ID's which are added dynamically to this table after processing the Meta Keys information. }, GROUPS => { 2 => 'Audio' }, "\xa9ART" => 'Artist', "\xa9alb" => 'Album', "\xa9cmt" => 'Comment', "\xa9com" => 'Composer', "\xa9day" => 'Year', "\xa9des" => 'Description', #4 "\xa9gen" => 'Genre', "\xa9grp" => 'Grouping', "\xa9lyr" => 'Lyrics', "\xa9nam" => 'Title', "\xa9too" => 'Encoder', "\xa9trk" => 'Track', "\xa9wrt" => 'Composer', '----' => { Name => 'iTunesInfo', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::iTunesInfo' }, }, aART => 'AlbumArtist', apID => 'AppleStoreID', # (also cnID,atID,cmID,plID,geID,sfID,akID) auth => 'Author', catg => 'Category', #7 covr => 'CoverArt', cpil => { Name => 'Compilation', PrintConv => { 0 => 'No', 1 => 'Yes' }, }, cprt => 'Copyright', disk => { Name => 'DiskNumber', ValueConv => 'length($val) >= 6 ? join(" of ",unpack("x2nn",$val)) : \$val', }, dscp => 'Description', desc => 'Description', #7 gnre => 'Genre', egid => 'EpisodeGlobalUniqueID', #7 keyw => 'Keyword', #7 pcst => 'Podcast', #7 perf => 'Performer', pgap => { Name => 'PlayGap', PrintConv => { 0 => 'Insert Gap', 1 => 'No Gap', }, }, purd => 'PurchaseDate', #7 purl => 'PodcastURL', #7 rtng => 'Rating', # int stik => { #(requires testing) Name => 'ContentType', #(PH guess) PrintConv => { #(http://weblog.xanga.com/gryphondwb/615474010/iphone-ringtones---what-did-itunes-741-really-do.html) 0 => 'Movie', 1 => 'Normal', 2 => 'Audiobook', 5 => 'Whacked Bookmark', 6 => 'Music Video', 9 => 'Short Film', 10 => 'TV Show', 11 => 'Booklet', 14 => 'Ringtone', }, }, titl => 'Title', tmpo => { Name => 'BeatsPerMinute', Format => 'int16u', # marked as boolean but really int16u in my sample }, trkn => { Name => 'TrackNumber', ValueConv => 'length($val) >= 6 ? join(" of ",unpack("x2nn",$val)) : \$val', }, tvnn => 'TVNetworkName', #7 tven => 'TVEpisodeNumber', #7 tvsn => 'TVSeason', #7 tves => 'TVEpisode', #7 ); # info list keys %Image::ExifTool::QuickTime::Keys = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessKeys, NOTES => q{ This directory contains a list of key names which are used to decode InfoList tags written by the "mdta" handler. The prefix of "com.apple.quicktime." has been removed from all TagID's below. }, 'version' => 'Version', 'player.version' => 'PlayerVersion', 'player.movie.visual.brightness'=> 'Brightness', 'player.movie.visual.color' => 'Color', 'player.movie.visual.tint' => 'Tint', 'player.movie.visual.contrast' => 'Contrast', 'player.movie.audio.gain' => 'AudioGain', 'player.movie.audio.treble' => 'Trebel', 'player.movie.audio.bass' => 'Bass', 'player.movie.audio.balance' => 'Balance', 'player.movie.audio.pitchshift' => 'PitchShift', 'player.movie.audio.mute' => { Name => 'Mute', Format => 'int8u', PrintConv => { 0 => 'Off', 1 => 'On' }, }, ); # info list atoms %Image::ExifTool::QuickTime::iTunesInfo = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Audio' }, ); # print to video data block %Image::ExifTool::QuickTime::Video = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Video' }, 0 => { Name => 'DisplaySize', PrintConv => { 0 => 'Normal', 1 => 'Double Size', 2 => 'Half Size', 3 => 'Full Screen', 4 => 'Current Size', }, }, 6 => { Name => 'SlideShow', PrintConv => { 0 => 'No', 1 => 'Yes', }, }, ); # 'hnti' atoms %Image::ExifTool::QuickTime::HintInfo = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, 'rtp ' => { Name => 'RealtimeStreamingProtocol', PrintConv => '$val=~s/^sdp /(SDP) /; $val', }, 'sdp ' => 'StreamingDataProtocol', ); # 'hinf' atoms %Image::ExifTool::QuickTime::HintTrackInfo = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, trpY => { Name => 'TotalBytes', Format => 'int64u' }, #(documented) trpy => { Name => 'TotalBytes', Format => 'int64u' }, #(observed) totl => { Name => 'TotalBytes', Format => 'int32u' }, nump => { Name => 'NumPackets', Format => 'int64u' }, npck => { Name => 'NumPackets', Format => 'int32u' }, tpyl => { Name => 'TotalBytesNoRTPHeaders', Format => 'int64u' }, tpaY => { Name => 'TotalBytesNoRTPHeaders', Format => 'int32u' }, #(documented) tpay => { Name => 'TotalBytesNoRTPHeaders', Format => 'int32u' }, #(observed) maxr => { Name => 'MaxDataRate', Format => 'int32u', Count => 2, PrintConv => 'my @a=split(" ",$val);sprintf("%d bytes in %.3f s",$a[1],$a[0]/1000)', }, dmed => { Name => 'MediaTrackBytes', Format => 'int64u' }, dimm => { Name => 'ImmediateDataBytes', Format => 'int64u' }, drep => { Name => 'RepeatedDataBytes', Format => 'int64u' }, tmin => { Name => 'MinTransmissionTime', Format => 'int32u', PrintConv => 'sprintf("%.3f s",$val/1000)', }, tmax => { Name => 'MaxTransmissionTime', Format => 'int32u', PrintConv => 'sprintf("%.3f s",$val/1000)', }, pmax => { Name => 'LargestPacketSize', Format => 'int32u' }, dmax => { Name => 'LargestPacketDuration', Format => 'int32u', PrintConv => 'sprintf("%.3f s",$val/1000)', }, payt => { Name => 'PayloadType', ValueConv => 'unpack("N",$val) . " " . substr($val, 5)', PrintConv => '$val=~s/ /, /;$val', }, ); # DcMD atoms %Image::ExifTool::QuickTime::DcMD = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => 'Metadata directory found in MOV videos from some Kodak cameras.', Cmbo => { Name => 'CameraByteOrder', PrintConv => { II => 'Little-endian (Intel, II)', MM => 'Big-endian (Motorola, MM)', }, }, DcME => { Name => 'DcME', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::DcME', }, }, ); # DcME atoms %Image::ExifTool::QuickTime::DcME = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, # Mtmd = binary data # Keyw = keywords? # Rate = 2 bytes "00 00" ); # MP4 media box (ref 5) %Image::ExifTool::QuickTime::Media = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => 'MP4 media box.', mdhd => [{ Name => 'MediaHeader', Condition => '$$valPt =~ /^\0{4}/', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::MediaHeader0' }, },{ Name => 'MediaHeader', Condition => '$$valPt =~ /^\0{3}\x01/', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::MediaHeader1' }, }], hdlr => { Name => 'Handler', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Handler' }, }, minf => { Name => 'MediaInfo', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::MediaInfo' }, }, ); # MP4 media header box, version 0 (ref 5) %Image::ExifTool::QuickTime::MediaHeader0 = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Video' }, NOTES => 'MP4 media header version 0.', FORMAT => 'int32u', 0 => { Name => 'MediaHeaderVersion', Notes => 'version 0', }, 1 => { Name => 'MediaCreateDate', %timeInfo, }, 2 => { Name => 'MediaModifyDate', %timeInfo, }, 3 => { Name => 'MediaTimeScale', RawConv => '$$self{MediaTS} = $val', }, 4 => { Name => 'MediaDuration', RawConv => '$$self{MediaTS} ? $val / $$self{MediaTS} : $val', PrintConv => '$$self{MediaTS} ? ConvertDuration($val) : $val', }, 5 => { Name => 'MediaLanguageCode', Format => 'int16u', RawConv => '$val ? $val : undef', ValueConv => 'pack "C*", map({ (($val>>$_)&0x1f)+0x60 } 10, 5, 0)', }, ); # MP4 media header box, version 1 (ref 5) %Image::ExifTool::QuickTime::MediaHeader1 = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Video' }, NOTES => 'MP4 media header version 1.', FORMAT => 'int32u', 0 => { Name => 'MediaHeaderVersion', Notes => 'version 1', }, 1 => { Name => 'MediaCreateDate', Format => 'int64u', %timeInfo, }, 3 => { Name => 'MediaModifyDate', Format => 'int64u', %timeInfo, }, 5 => { Name => 'MediaTimescale', RawConv => '$$self{MediaTS} = $val', }, 6 => { Name => 'MediaDuration', Format => 'int64u', RawConv => '$$self{MediaTS} ? $val / $$self{MediaTS} : $val', PrintConv => '$$self{MediaTS} ? ConvertDuration($val) : $val', }, 8 => { Name => 'MediaLanguageCode', Format => 'undef[4]', ValueConv => 'pack "C*", map({ (($val>>$_)&0x1f)+0x60 } 10, 5, 0)', }, ); # MP4 media information box (ref 5) %Image::ExifTool::QuickTime::MediaInfo = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => 'MP4 media info box.', vmhd => { Name => 'VideoHeader', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::VideoHeader' }, }, smhd => { Name => 'AudioHeader', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::AudioHeader' }, }, hmhd => { Name => 'HintHeader', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::HintHeader' }, }, # nmhd - null media header dinf => { Name => 'DataInfo', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::DataInfo' }, }, hdlr => { #PH Name => 'Handler', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Handler' }, }, stbl => { Name => 'SampleTable', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::SampleTable' }, }, ); # MP4 video media header (ref 5) %Image::ExifTool::QuickTime::VideoHeader = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Video' }, NOTES => 'MP4 video media header.', FORMAT => 'int16u', 2 => { Name => 'GraphicsMode', PrintHex => 1, PrintConv => { # (ref http://homepage.mac.com/vanhoek/MovieGuts%20docs/64.html) 0x00 => 'srcCopy', 0x01 => 'srcOr', 0x02 => 'srcXor', 0x03 => 'srcBic', 0x04 => 'notSrcCopy', 0x05 => 'notSrcOr', 0x06 => 'notSrcXor', 0x07 => 'notSrcBic', 0x08 => 'patCopy', 0x09 => 'patOr', 0x0a => 'patXor', 0x0b => 'patBic', 0x0c => 'notPatCopy', 0x0d => 'notPatOr', 0x0e => 'notPatXor', 0x0f => 'notPatBic', 0x20 => 'blend', 0x21 => 'addPin', 0x22 => 'addOver', 0x23 => 'subPin', 0x24 => 'transparent', 0x25 => 'addMax', 0x26 => 'subOver', 0x27 => 'addMin', 0x31 => 'grayishTextOr', 0x32 => 'hilite', 0x40 => 'ditherCopy', # the following ref ISO/IEC 15444-3 0x100 => 'Alpha', 0x101 => 'White Alpha', 0x102 => 'Pre-multiplied Black Alpha', 0x110 => 'Component Alpha', }, }, 3 => { Name => 'OpColor', Format => 'int16u[3]' }, ); # MP4 audio media header (ref 5) %Image::ExifTool::QuickTime::AudioHeader = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Audio' }, NOTES => 'MP4 audio media header.', FORMAT => 'int16u', 2 => { Name => 'Balance', Format => 'fixed16s' }, ); # MP4 hint media header (ref 5) %Image::ExifTool::QuickTime::HintHeader = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, NOTES => 'MP4 hint media header.', FORMAT => 'int16u', 2 => 'MaxPDUSize', 3 => 'AvgPDUSize', 4 => { Name => 'MaxBitrate', Format => 'int32u' }, 6 => { Name => 'AvgBitrate', Format => 'int32u' }, ); # MP4 sample table box (ref 5) %Image::ExifTool::QuickTime::SampleTable = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, GROUPS => { 2 => 'Video' }, NOTES => 'MP4 sample table box.', stsd => [ { Name => 'AudioSampleDesc', Condition => '$$self{HandlerType} and $$self{HandlerType} eq "soun"', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::AudioSampleDesc', Start => 8, # skip version number and count }, },{ Name => 'VideoSampleDesc', Condition => '$$self{HandlerType} and $$self{HandlerType} eq "vide"', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::ImageDesc', Start => 8, # skip version number and count }, },{ Name => 'HintSampleDesc', Condition => '$$self{HandlerType} and $$self{HandlerType} eq "hint"', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::HintSampleDesc', Start => 8, # skip version number and count }, },{ Name => 'OtherSampleDesc', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::OtherSampleDesc', Start => 8, # skip version number and count }, }, # (Note: "alis" HandlerType handled by the parent audio or video handler) ], stts => [ # decoding time-to-sample table { Name => 'VideoFrameRate', Notes => 'average rate calculated from time-to-sample table for video media', Condition => '$$self{HandlerType} and $$self{HandlerType} eq "vide"', # (must be RawConv so appropriate MediaTS is used in calculation) RawConv => 'Image::ExifTool::QuickTime::CalcSampleRate($self, \$val)', PrintConv => 'sprintf("%.1f", $val)', }, ], # ctts - (composition) time to sample # stsc - sample to chunk # stsz - sample sizes # stz2 - compact sample sizes # stco - chunk offset # co64 - 64-bit chunk offset # stss - sync sample table # stsh - shadow sync sample table # padb - sample padding bits # stdp - sample degradation priority # sdtp - independent and disposable samples # sbgp - sample to group # sgpd - sample group description # subs - sub-sample information ); # MP4 audio sample description box (ref 5) %Image::ExifTool::QuickTime::AudioSampleDesc = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Audio' }, FORMAT => 'int16u', NOTES => 'MP4 audio sample description.', 2 => { Name => 'AudioFormat', Format => 'undef[4]', RawConv => '$val =~ /^[\w ]{4}$/i ? $val : undef', }, 10 => { #PH Name => 'AudioVendorID', Format => 'undef[4]', RawConv => '$val eq "\0\0\0\0" ? undef : $val', PrintConv => \%vendorID, SeparateTable => 'VendorID', }, 12 => 'AudioChannels', 13 => 'AudioBitsPerSample', 16 => { Name => 'AudioSampleRate', Format => 'fixed32u' }, 28 => { #PH Name => 'AudioFormat', Format => 'undef[4]', RawConv => '$val =~ /^[\w ]{4}$/i ? $val : undef', Notes => 'in Casio MOV videos', }, ); # MP4 hint sample description box (ref 5) %Image::ExifTool::QuickTime::HintSampleDesc = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, FORMAT => 'int16u', NOTES => 'MP4 hint sample description.', 2 => { Name => 'HintFormat', Format => 'undef[4]' }, ); # MP4 generic sample description box %Image::ExifTool::QuickTime::OtherSampleDesc = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, FORMAT => 'int16u', 2 => { Name => 'OtherFormat', Format => 'undef[4]' }, ); # MP4 data information box (ref 5) %Image::ExifTool::QuickTime::DataInfo = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, NOTES => 'MP4 data information box.', dref => { Name => 'DataRef', SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::DataRef', Start => 8, }, }, ); # MP4 data reference box (ref 5) %Image::ExifTool::QuickTime::DataRef = ( PROCESS_PROC => \&Image::ExifTool::QuickTime::ProcessMOV, NOTES => 'MP4 data reference box.', 'url ' => { Name => 'URL', RawConv => q{ # ignore if self-contained (flags bit 0 set) return undef if unpack("N",$val) & 0x01; $_ = substr($val,4); s/\0.*//s; $_; }, }, 'urn ' => { Name => 'URN', RawConv => q{ return undef if unpack("N",$val) & 0x01; $_ = substr($val,4); s/\0.*//s; $_; }, }, ); # MP4 handler box (ref 5) %Image::ExifTool::QuickTime::Handler = ( PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData, GROUPS => { 2 => 'Video' }, 4 => { #PH Name => 'HandlerClass', Format => 'undef[4]', RawConv => '$val eq "\0\0\0\0" ? undef : $val', PrintConv => { mhlr => 'Media Handler', dhlr => 'Data Handler', }, }, 8 => { Name => 'HandlerType', Format => 'undef[4]', RawConv => '$$self{HandlerType} = $val unless $val eq "alis"; $val', PrintConv => { vide => 'Video Track', soun => 'Audio Track', hint => 'Hint Track', alis => 'Alias Data', #PH mdir => 'Metadata', #3 mdta => 'Metadata Tags', #PH odsm => 'Object Descriptor', #3 sdsm => 'Scene Description', #3 crsm => 'Clock Reference', #3 m7sm => 'MPEG-7 Stream', #3 ocsm => 'Object Content', #3 ipsm => 'IPMP', #3 mjsm => 'MPEG-J', #3 'url '=> 'URL', #3 }, }, 12 => { #PH Name => 'HandlerVendorID', Format => 'undef[4]', RawConv => '$val eq "\0\0\0\0" ? undef : $val', PrintConv => \%vendorID, SeparateTable => 'VendorID', }, 24 => { Name => 'HandlerDescription', Format => 'string', # (sometimes this is a Pascal string, and sometimes it is a C string) RawConv => q{ $val=substr($val,1,ord($1)) if $val=~/^([\0-\x1f])/ and ord($1)Options('Verbose')) { $exifTool->VerboseDir('Keys'); $out = $exifTool->Options('TextOut'); } my $pos = 8; my $index = 1; my $infoTable = GetTagTable('Image::ExifTool::QuickTime::InfoList'); while ($pos < $dirLen - 4) { my $len = unpack("x${pos}N", $$dataPt); last if $len < 4 or $pos + $len > $dirLen; delete $$tagTablePtr{$index}; my $tag = substr($$dataPt, $pos + 4, $len - 4); $tag =~ s/\0.*//s; # truncate at null $tag =~ s/^mdta//; # remove 'mdta' prefix $tag =~ s/^com\.apple\.quicktime\.//; # remove common apple quicktime domain next unless $tag; my $tagInfo = $exifTool->GetTagInfo($tagTablePtr, $tag); my $newInfo; if ($tagInfo) { $newInfo = { Name => $$tagInfo{Name}, Format => $$tagInfo{Format}, ValueConv => $$tagInfo{ValueConv}, PrintConv => $$tagInfo{PrintConv}, }; } elsif ($tag =~ /^[-\w.]+$/) { # create info for tags with reasonable id's my $name = $tag; $name =~ s/\.(.)/\U$1/g; $newInfo = { Name => ucfirst($name) }; } # substitute this tag in the InfoList table with the given index delete $$infoTable{$index}; if ($newInfo) { Image::ExifTool::AddTagToTable($infoTable, $index, $newInfo); $out and printf $out "%sAdded InfoList Tag 0x%.4x = $tag\n", $exifTool->{INDENT}, $index; } $pos += $len; ++$index; } return 1; } #------------------------------------------------------------------------------ # Process a QuickTime atom # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) optional tag table ref # Returns: 1 on success sub ProcessMOV($$;$) { my ($exifTool, $dirInfo, $tagTablePtr) = @_; my $raf = $$dirInfo{RAF}; my $dataPt = $$dirInfo{DataPt}; my $verbose = $exifTool->Options('Verbose'); my $dataPos = $$dirInfo{Base} || 0; my ($buff, $tag, $size, $track); # more convenient to package data as a RandomAccess file $raf or $raf = new File::RandomAccess($dataPt); # skip leading bytes if necessary if ($$dirInfo{DirStart}) { $raf->Seek($$dirInfo{DirStart}, 1) or return 0; $dataPos += $$dirInfo{DirStart}; } # read size/tag name atom header $raf->Read($buff,8) == 8 or return 0; $dataPos += 8; $tagTablePtr or $tagTablePtr = GetTagTable('Image::ExifTool::QuickTime::Main'); ($size, $tag) = unpack('Na4', $buff); if ($dataPt) { $verbose and $exifTool->VerboseDir($$dirInfo{DirName}); } else { # check on file type if called with a RAF $$tagTablePtr{$tag} or return 0; if ($tag eq 'ftyp') { # read ahead 4 bytes to see if this is an M4A file my $ftyp = 'MP4'; if ($raf->Read($buff, 4) == 4) { $raf->Seek(-4, 1); $ftyp = 'M4A' if $buff eq 'M4A '; } $exifTool->SetFileType($ftyp); # MP4 or M4A } else { $exifTool->SetFileType(); # MOV } SetByteOrder('MM'); } for (;;) { if ($size < 8) { last if $size == 0; $size == 1 or $exifTool->Warn('Invalid atom size'), last; $raf->Read($buff, 8) == 8 or last; $dataPos += 8; my ($hi, $lo) = unpack('NN', $buff); if ($hi or $lo > 0x7fffffff) { $exifTool->Warn('End of processing at large atom'); last; } $size = $lo; } $size -= 8; my $tagInfo = $exifTool->GetTagInfo($tagTablePtr, $tag); # allow numerical tag ID's unless ($tagInfo) { my $num = unpack('N', $tag); if ($$tagTablePtr{$num}) { $tagInfo = $exifTool->GetTagInfo($tagTablePtr, $num); $tag = $num; } } # generate tagInfo if Unknown option set if (not defined $tagInfo and ($exifTool->{OPTIONS}->{Unknown} or $verbose or $tag =~ /^\xa9/)) { my $name = $tag; my $n = ($name =~ s/([\x00-\x1f\x7f-\xff])/'x'.unpack('H*',$1)/eg); # print in hex if tag is numerical $name = sprintf('0x%.4x',unpack('N',$tag)) if $n > 2; if ($name =~ /^xa9(.*)/) { $tagInfo = { Name => "UserData_$1", Description => "User Data $1", }; } else { $tagInfo = { Name => "Unknown_$name", Description => "Unknown $name", Unknown => 1, Binary => 1, }; } Image::ExifTool::AddTagToTable($tagTablePtr, $tag, $tagInfo); } # load values only if associated with a tag (or verbose) and < 16MB long if ((defined $tagInfo or $verbose) and $size < 0x1000000) { my $val; unless ($raf->Read($val, $size) == $size) { $exifTool->Warn("Truncated '$tag' data"); last; } # use value to get tag info if necessary $tagInfo or $tagInfo = $exifTool->GetTagInfo($tagTablePtr, $tag, \$val); my $hasData = ($$dirInfo{HasData} and $val =~ /^\0...data\0/s); if ($verbose and not $hasData) { $exifTool->VerboseInfo($tag, $tagInfo, Value => $val, DataPt => \$val, DataPos => $dataPos, ); } if ($tagInfo) { my $subdir = $$tagInfo{SubDirectory}; if ($subdir) { my $start = $$subdir{Start} || 0; my %dirInfo = ( DataPt => \$val, DataLen => $size, DirStart => $start, DirLen => $size - $start, DirName => $$tagInfo{Name}, HasData => $$subdir{HasData}, DataPos => 0, # Base needed for IsOffset tags in binary data Base => $dataPos, ); if ($$subdir{ByteOrder} and $$subdir{ByteOrder} =~ /^Little/) { SetByteOrder('II'); } if ($$tagInfo{Name} eq 'Track') { $track or $track = 0; $exifTool->{SET_GROUP1} = 'Track' . (++$track); } my $subTable = GetTagTable($$subdir{TagTable}); $exifTool->ProcessDirectory(\%dirInfo, $subTable) if $size > $start; delete $exifTool->{SET_GROUP1}; SetByteOrder('MM'); } elsif ($hasData) { # handle atoms containing 'data' tags my $pos = 0; for (;;) { last if $pos + 16 > $size; my ($len, $type, $flags) = unpack("x${pos}Na4N", $val); last if $pos + $len > $size; my $value; my $format = $$tagInfo{Format}; if ($type eq 'data' and $len >= 16) { $pos += 16; $len -= 16; $value = substr($val, $pos, $len); # format flags: 0x0=binary, 0x1=text, 0xd=image, # 0x15=boolean, 0x17=float unless ($format) { if ($flags == 0x0015) { $format = 'int8u'; } elsif ($flags == 0x0017) { $format = 'float'; } } if ($format) { $value = ReadValue(\$value, 0, $format, $$tagInfo{Count}, $len); } elsif ($flags != 0x01 and not $$tagInfo{ValueConv}) { # make binary data a scalar reference unless a ValueConv exists my $buf = $value; $value = \$buf; } } $exifTool->VerboseInfo($tag, $tagInfo, Value => ref $value ? $$value : $value, DataPt => \$val, DataPos => $dataPos, Start => $pos, Size => $len, Format => $format, Extra => sprintf(", Type='$type', Flags=0x%x",$flags) ) if $verbose; $exifTool->FoundTag($tagInfo, $value) if defined $value; $pos += $len; } } else { if ($tag =~ /^\xa9/) { # parse international text to extract first string my $len = unpack('n', $val); # $len should include 4 bytes for length and type words, # but Pentax and Kodak forget to add these in, so allow for this $len += 4 if $len <= $size - 4; $val = substr($val, 4, $len - 4) if $len <= $size; } elsif ($$tagInfo{Format}) { $val = ReadValue(\$val, 0, $$tagInfo{Format}, $$tagInfo{Count}, length($val)); } $exifTool->FoundTag($tagInfo, $val); } } } else { $raf->Seek($size, 1) or $exifTool->Warn("Truncated '$tag' data"), last; } $raf->Read($buff, 8) == 8 or last; $dataPos += $size + 8; ($size, $tag) = unpack('Na4', $buff); } return 1; } #------------------------------------------------------------------------------ # Process a QuickTime Image File # Inputs: 0) ExifTool object reference, 1) directory information reference # Returns: 1 on success sub ProcessQTIF($$) { my $table = GetTagTable('Image::ExifTool::QuickTime::ImageFile'); return ProcessMOV($_[0], $_[1], $table); } 1; # end __END__ =head1 NAME Image::ExifTool::QuickTime - Read QuickTime and MP4 meta information =head1 SYNOPSIS This module is used by Image::ExifTool =head1 DESCRIPTION This module contains routines required by Image::ExifTool to extract information from QuickTime and MP4 video, and M4A audio files. =head1 AUTHOR Copyright 2003-2008, Phil Harvey (phil at owl.phy.queensu.ca) This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 REFERENCES =over 4 =item L =item L =item L =item L =item L =item L =back =head1 SEE ALSO L, L =cut