#------------------------------------------------------------------------------
# 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
#------------------------------------------------------------------------------

package Image::ExifTool::QuickTime;

use strict;
use vars qw($VERSION);
use Image::ExifTool qw(:DataAccess :Utils);
use Image::ExifTool::Exif;

$VERSION = '1.16';

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',
    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,
    },
    moov => {
        Name => 'Movie',
        SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Movie' },
    },
    mdat => { 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)<length($val);
            length $val ? $val : undef;
        },
    },
    41 => '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
        },
    },
   'ptv '=> {
        Name => 'PrintToVideo',
        SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Video' },
    },
    # hnti => 'HintInfo',
    # hinf => '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' },
    },
);

# 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',
        },
    },
);

# 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)<length($val);
            length $val ? $val : undef;
        },
    },
);

#------------------------------------------------------------------------------
# Determine the average sample rate from a time-to-sample table
# Inputs: 0) ExifTool object ref, 1) time-to-sample table data ref
# Returns: average sample rate (in Hz)
sub CalcSampleRate($$)
{
    my ($exifTool, $valPt) = @_;
    my @dat = unpack('N*', $$valPt);
    my ($num, $dur) = (0, 0);
    my $i;
    for ($i=2; $i<@dat-1; $i+=2) {
        $num += $dat[$i];               # total number of samples
        $dur += $dat[$i] * $dat[$i+1];  # total sample duration
    }
    return undef unless $num and $dur and $$exifTool{MediaTS};
    return $num * $$exifTool{MediaTS} / $dur;
}

#------------------------------------------------------------------------------
# Fix incorrect format for ImageWidth/Height as written by Pentax
sub FixWrongFormat($)
{
    my $val = shift;
    return undef unless $val;
    if ($val & 0xffff0000) {
        $val = unpack('n',pack('N',$val));
    }
    return $val;
}

#------------------------------------------------------------------------------
# Process Meta keys and add tags to the InfoList table ('mdta' handler) (ref PH)
# Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
# Returns: 1 on success
sub ProcessKeys($$$)
{
    my ($exifTool, $dirInfo, $tagTablePtr) = @_;
    my $dataPt = $$dirInfo{DataPt};
    my $dirLen = length $$dataPt;
    my $out;
    if ($exifTool->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 = $1;
            $name =~ s/\.(.)/\U$1/g;
            $newInfo = { Name => ucfirst($name) };
        } else {
            next;
        }
        # substitute this tag in the InfoList table with the given index
        delete $$infoTable{$index};
        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<http://developer.apple.com/documentation/QuickTime/>

=item L<http://search.cpan.org/dist/MP4-Info-1.04/>

=item L<http://www.geocities.com/xhelmboyx/quicktime/formats/mp4-layout.txt>

=item L<http://wiki.multimedia.cx/index.php?title=Apple_QuickTime>

=item L<http://atomicparsley.sourceforge.net/mpeg-4files.html>

=back

=head1 SEE ALSO

L<Image::ExifTool::TagNames/QuickTime Tags>,
L<Image::ExifTool(3pm)|Image::ExifTool>

=cut

