Coverage Report - org.jaudiotagger.audio.mp4.Mp4InfoReader
 
Classes in this File Line Coverage Branch Coverage Complexity
Mp4InfoReader
87%
105/121
57%
31/54
40
 
 1  
 /*
 2  
  * Entagged Audio Tag library
 3  
  * Copyright (c) 2003-2005 Rapha�l Slinckx <raphael@slinckx.net>
 4  
  * 
 5  
  * This library is free software; you can redistribute it and/or
 6  
  * modify it under the terms of the GNU Lesser General Public
 7  
  * License as published by the Free Software Foundation; either
 8  
  * version 2.1 of the License, or (at your option) any later version.
 9  
  *  
 10  
  * This library is distributed in the hope that it will be useful,
 11  
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
  * Lesser General Public License for more details.
 14  
  * 
 15  
  * You should have received a copy of the GNU Lesser General Public
 16  
  * License along with this library; if not, write to the Free Software
 17  
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 18  
  */
 19  
 package org.jaudiotagger.audio.mp4;
 20  
 
 21  
 import org.jaudiotagger.audio.exceptions.CannotReadException;
 22  
 import org.jaudiotagger.audio.exceptions.CannotReadVideoException;
 23  
 import org.jaudiotagger.audio.generic.GenericAudioHeader;
 24  
 import org.jaudiotagger.audio.mp4.atom.*;
 25  
 import org.jaudiotagger.logging.ErrorMessage;
 26  
 
 27  
 import java.io.IOException;
 28  
 import java.io.RandomAccessFile;
 29  
 import java.nio.ByteBuffer;
 30  
 import java.util.logging.Logger;
 31  
 
 32  
 /**
 33  
  * Read audio info from file.
 34  
  * <p/>
 35  
  * <p/>
 36  
  * The info is held in the mvdh and mdhd fields as shown below
 37  
  * <pre>
 38  
  * |--- ftyp
 39  
  * |--- moov
 40  
  * |......|
 41  
  * |......|----- mvdh
 42  
  * |......|----- trak
 43  
  * |...............|----- mdia
 44  
  * |.......................|---- mdhd
 45  
  * |.......................|---- minf
 46  
  * |..............................|---- smhd
 47  
  * |..............................|---- stbl
 48  
  * |......................................|--- stsd
 49  
  * |.............................................|--- mp4a
 50  
  * |......|----- udta
 51  
  * |
 52  
  * |--- mdat
 53  
  * </pre>
 54  
  */
 55  126
 public class Mp4InfoReader
 56  
 {
 57  
     // Logger Object
 58  42
     public static Logger logger = Logger.getLogger("org.jaudiotagger.audio.mp4.atom");
 59  
 
 60  
     public GenericAudioHeader read(RandomAccessFile raf) throws CannotReadException, IOException
 61  
     {
 62  117
         Mp4AudioHeader info = new Mp4AudioHeader();
 63  
 
 64  
         //File Identification
 65  117
         Mp4BoxHeader ftypHeader = Mp4BoxHeader.seekWithinLevel(raf, Mp4NotMetaFieldKey.FTYP.getFieldName());
 66  117
         if (ftypHeader == null)
 67  
         {
 68  0
             throw new CannotReadException(ErrorMessage.MP4_FILE_NOT_CONTAINER.getMsg());
 69  
         }
 70  117
         ByteBuffer ftypBuffer = ByteBuffer.allocate(ftypHeader.getLength() - Mp4BoxHeader.HEADER_LENGTH);
 71  117
         raf.getChannel().read(ftypBuffer);
 72  117
         ftypBuffer.rewind();
 73  117
         Mp4FtypBox ftyp = new Mp4FtypBox(ftypHeader, ftypBuffer);
 74  117
         ftyp.processData();
 75  117
         info.setBrand(ftyp.getMajorBrand());
 76  
 
 77  
         //Get to the facts everything we are interested in is within the moov box, so just load data from file
 78  
         //once so no more file I/O needed
 79  117
         Mp4BoxHeader moovHeader = Mp4BoxHeader.seekWithinLevel(raf, Mp4NotMetaFieldKey.MOOV.getFieldName());
 80  117
         if (moovHeader == null)
 81  
         {
 82  0
             throw new CannotReadException(ErrorMessage.MP4_FILE_NOT_AUDIO.getMsg());
 83  
         }
 84  117
         ByteBuffer moovBuffer = ByteBuffer.allocate(moovHeader.getLength() - Mp4BoxHeader.HEADER_LENGTH);
 85  117
         raf.getChannel().read(moovBuffer);
 86  117
         moovBuffer.rewind();
 87  
 
 88  
         //Level 2-Searching for "mvhd" somewhere within "moov", we make a slice after finding header
 89  
         //so all get() methods will be relative to mvdh positions
 90  117
         Mp4BoxHeader boxHeader = Mp4BoxHeader.seekWithinLevel(moovBuffer, Mp4NotMetaFieldKey.MVHD.getFieldName());
 91  117
         if (boxHeader == null)
 92  
         {
 93  0
             throw new CannotReadException(ErrorMessage.MP4_FILE_NOT_AUDIO.getMsg());
 94  
         }
 95  117
         ByteBuffer mvhdBuffer = moovBuffer.slice();
 96  117
         Mp4MvhdBox mvhd = new Mp4MvhdBox(boxHeader, mvhdBuffer);
 97  117
         info.setLength(mvhd.getLength());
 98  
         //Advance position, TODO should we put this in box code ?
 99  117
         mvhdBuffer.position(mvhdBuffer.position() + boxHeader.getDataLength());
 100  
 
 101  
         //Level 2-Searching for "trak" within "moov"
 102  117
         boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.TRAK.getFieldName());
 103  117
         int endOfFirstTrackInBuffer = mvhdBuffer.position() + boxHeader.getDataLength();
 104  
 
 105  117
         if (boxHeader == null)
 106  
         {
 107  0
             throw new CannotReadException(ErrorMessage.MP4_FILE_NOT_AUDIO.getMsg());
 108  
         }
 109  
         //Level 3-Searching for "mdia" within "trak"
 110  117
         boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.MDIA.getFieldName());
 111  117
         if (boxHeader == null)
 112  
         {
 113  0
             throw new CannotReadException(ErrorMessage.MP4_FILE_NOT_AUDIO.getMsg());
 114  
         }
 115  
         //Level 4-Searching for "mdhd" within "mdia"
 116  117
         boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.MDHD.getFieldName());
 117  117
         if (boxHeader == null)
 118  
         {
 119  0
             throw new CannotReadException(ErrorMessage.MP4_FILE_NOT_AUDIO.getMsg());
 120  
         }
 121  117
         Mp4MdhdBox mdhd = new Mp4MdhdBox(boxHeader, mvhdBuffer.slice());
 122  117
         info.setSamplingRate(mdhd.getSampleRate());
 123  
 
 124  
         //Level 4-Searching for "hdlr" within "mdia"
 125  
         /*We dont currently need to process this because contains nothing we want
 126  
         mvhdBuffer.position(mvhdBuffer.position() + boxHeader.getDataLength());
 127  
         boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.HDLR.getFieldName());
 128  
         if (boxHeader == null)
 129  
         {
 130  
             throw new CannotReadException(ErrorMessage.MP4_FILE_NOT_AUDIO.getMsg());
 131  
         }
 132  
         Mp4HdlrBox hdlr = new Mp4HdlrBox(boxHeader, mvhdBuffer.slice());
 133  
         hdlr.processData();
 134  
         */
 135  
 
 136  
         //Level 4-Searching for "minf" within "mdia"
 137  117
         mvhdBuffer.position(mvhdBuffer.position() + boxHeader.getDataLength());
 138  117
         boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.MINF.getFieldName());
 139  117
         if (boxHeader == null)
 140  
         {
 141  0
             throw new CannotReadException(ErrorMessage.MP4_FILE_NOT_AUDIO.getMsg());
 142  
         }
 143  
 
 144  
         //Level 5-Searching for "smhd" within "minf"
 145  
         //Only an audio track would have a smhd frame
 146  117
         boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.SMHD.getFieldName());
 147  117
         if (boxHeader == null)
 148  
         {
 149  0
             throw new CannotReadException(ErrorMessage.MP4_FILE_NOT_AUDIO.getMsg());
 150  
         }
 151  117
         mvhdBuffer.position(mvhdBuffer.position() + boxHeader.getDataLength());
 152  
 
 153  
         //Level 5-Searching for "stbl within "minf"
 154  117
         boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.STBL.getFieldName());
 155  117
         if (boxHeader == null)
 156  
         {
 157  0
             throw new CannotReadException(ErrorMessage.MP4_FILE_NOT_AUDIO.getMsg());
 158  
         }
 159  
 
 160  
         //Level 6-Searching for "stsd within "stbl" and process it direct data, dont think these are mandatory so dont throw
 161  
         //exception if unable to find
 162  117
         boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.STSD.getFieldName());
 163  117
         if (boxHeader != null)
 164  
         {
 165  117
             Mp4StsdBox stsd = new Mp4StsdBox(boxHeader, mvhdBuffer);
 166  117
             stsd.processData();
 167  117
             int positionAfterStsdHeaderAndData = mvhdBuffer.position();
 168  
 
 169  
             ///Level 7-Searching for "mp4a within "stsd"
 170  117
             boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.MP4A.getFieldName());
 171  117
             if (boxHeader != null)
 172  
             {
 173  101
                 ByteBuffer mp4aBuffer = mvhdBuffer.slice();
 174  101
                 Mp4Mp4aBox mp4a = new Mp4Mp4aBox(boxHeader, mp4aBuffer);
 175  101
                 mp4a.processData();
 176  
                 //Level 8-Searching for "esds" within mp4a to get No Of Channels and bitrate
 177  101
                 boxHeader = Mp4BoxHeader.seekWithinLevel(mp4aBuffer, Mp4NotMetaFieldKey.ESDS.getFieldName());
 178  101
                 if (boxHeader != null)
 179  
                 {
 180  101
                     Mp4EsdsBox esds = new Mp4EsdsBox(boxHeader, mp4aBuffer.slice());
 181  
 
 182  
                     //Set Bitrate in kbps
 183  101
                     info.setBitrate(esds.getAvgBitrate() / 1000);
 184  
 
 185  
                     //Set Number of Channels
 186  101
                     info.setChannelNumber(esds.getNumberOfChannels());
 187  
 
 188  101
                     info.setKind(esds.getKind());
 189  101
                     info.setProfile(esds.getAudioProfile());
 190  
 
 191  101
                     info.setEncodingType(EncoderType.AAC.getDescription());
 192  
                 }
 193  101
             }
 194  
             else
 195  
             {
 196  
                 //Level 7 -Searching for drms within stsd instead (m4p files)
 197  16
                 mvhdBuffer.position(positionAfterStsdHeaderAndData);
 198  16
                 boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.DRMS.getFieldName());
 199  16
                 if (boxHeader != null)
 200  
                 {
 201  8
                     Mp4DrmsBox drms = new Mp4DrmsBox(boxHeader, mvhdBuffer);
 202  8
                     drms.processData();
 203  
 
 204  
                     //Level 8-Searching for "esds" within drms to get No Of Channels and bitrate
 205  8
                     boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.ESDS.getFieldName());
 206  8
                     if (boxHeader != null)
 207  
                     {
 208  8
                         Mp4EsdsBox esds = new Mp4EsdsBox(boxHeader, mvhdBuffer.slice());
 209  
 
 210  
                         //Set Bitrate in kbps
 211  8
                         info.setBitrate(esds.getAvgBitrate() / 1000);
 212  
 
 213  
                         //Set Number of Channels
 214  8
                         info.setChannelNumber(esds.getNumberOfChannels());
 215  
 
 216  8
                         info.setKind(esds.getKind());
 217  8
                         info.setProfile(esds.getAudioProfile());
 218  
 
 219  8
                         info.setEncodingType(EncoderType.DRM_AAC.getDescription());
 220  
                     }
 221  8
                 }
 222  
                 //Level 7-Searching for alac (Apple Lossless) instead
 223  
                 else
 224  
                 {
 225  8
                     mvhdBuffer.position(positionAfterStsdHeaderAndData);
 226  8
                     boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.ALAC.getFieldName());
 227  8
                     if (boxHeader != null)
 228  
                     {
 229  
                         //Process First Alac
 230  8
                         Mp4AlacBox alac = new Mp4AlacBox(boxHeader, mvhdBuffer);
 231  8
                         alac.processData();
 232  
                         
 233  
                         //Level 8-Searching for 2nd "alac" within box that contains the info we really want
 234  8
                         boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.ALAC.getFieldName());
 235  8
                         if (boxHeader != null)
 236  
                         {
 237  8
                             alac = new Mp4AlacBox(boxHeader, mvhdBuffer);
 238  8
                             alac.processData();
 239  8
                             info.setEncodingType(EncoderType.APPLE_LOSSLESS.getDescription());
 240  8
                             info.setChannelNumber(alac.getChannels());
 241  8
                             info.setBitrate(alac.getBitRate()/1000);
 242  
                         }
 243  
                     }
 244  
                 }
 245  
             }
 246  
         }
 247  
         //Set default channels if couldnt calculate it
 248  117
         if (info.getChannelNumber() == -1)
 249  
         {
 250  0
             info.setChannelNumber(2);
 251  
         }
 252  
 
 253  
         //Set default bitrate if couldnt calculate it
 254  117
         if (info.getBitRateAsNumber() == -1)
 255  
         {
 256  0
             info.setBitrate(128);
 257  
         }
 258  
 
 259  
         //This is the most likley option if cant find a match
 260  117
         if (info.getEncodingType().equals(""))
 261  
         {
 262  0
             info.setEncodingType(EncoderType.AAC.getDescription());
 263  
         }
 264  
 
 265  117
         logger.info(info.toString());
 266  
 
 267  
         //Level 2-Searching for another "trak" within "moov", if more than one track exists then probably
 268  
         //video so reject it, unless it fits into certain encoding patterns
 269  117
         mvhdBuffer.position(endOfFirstTrackInBuffer);
 270  117
         boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.TRAK.getFieldName());
 271  117
         if (boxHeader != null)
 272  
         {
 273  
             //We only allow multiple tracks as audio if they follow the format used by the winamp encoder or
 274  
             //are marked as being audio only and if track contains a nmhd atom rather than smhd.
 275  
             //TODO this probably too restrictive but it fixes the test cases we have
 276  14
             if (ftyp.getMajorBrand().equals(Mp4FtypBox.Brand.ISO14496_1_VERSION_2.getId())
 277  
                     || ftyp.getMajorBrand().equals(Mp4FtypBox.Brand.APPLE_AUDIO_ONLY.getId())
 278  
                     || ftyp.getMajorBrand().equals(Mp4FtypBox.Brand.APPLE_AUDIO.getId()))
 279  
             {
 280  
                 //Ok, need to do further checks on this track to ensure it is a scene descriptor
 281  
                 //Level 3-Searching for "mdia" within "trak"
 282  13
                 boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.MDIA.getFieldName());
 283  13
                 if (boxHeader == null)
 284  
                 {
 285  0
                     throw new CannotReadVideoException(ErrorMessage.MP4_FILE_IS_VIDEO.getMsg());
 286  
                 }
 287  
                 //Level 4-Searching for "mdhd" within "mdia"
 288  13
                 boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.MDHD.getFieldName());
 289  13
                 if (boxHeader == null)
 290  
                 {
 291  0
                     throw new CannotReadVideoException(ErrorMessage.MP4_FILE_IS_VIDEO.getMsg());
 292  
                 }
 293  
                 //Level 4-Searching for "minf" within "mdia"
 294  13
                 mvhdBuffer.position(mvhdBuffer.position() + boxHeader.getDataLength());
 295  13
                 boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.MINF.getFieldName());
 296  13
                 if (boxHeader == null)
 297  
                 {
 298  0
                     throw new CannotReadVideoException(ErrorMessage.MP4_FILE_IS_VIDEO.getMsg());
 299  
                 }
 300  
 
 301  
                 //Level 5-Searching for "nmhd" within "minf"
 302  
                 //Only an audio track would have a nmhd frame
 303  13
                 boxHeader = Mp4BoxHeader.seekWithinLevel(mvhdBuffer, Mp4NotMetaFieldKey.NMHD.getFieldName());
 304  13
                 if (boxHeader == null)
 305  
                 {
 306  0
                     throw new CannotReadVideoException(ErrorMessage.MP4_FILE_IS_VIDEO.getMsg());
 307  
                 }
 308  
             }
 309  
             else
 310  
             {
 311  1
                 logger.info(ErrorMessage.MP4_FILE_IS_VIDEO.getMsg() + ":" + ftyp.getMajorBrand());
 312  1
                 throw new CannotReadVideoException(ErrorMessage.MP4_FILE_IS_VIDEO.getMsg());
 313  
             }
 314  
         }
 315  
 
 316  
         //Build AtomTree to ensure it is valid, this means we can detect any problems early on
 317  116
         Mp4AtomTree atomTree = new Mp4AtomTree(raf,false); 
 318  
 
 319  114
         return info;
 320  
     }
 321  
 
 322  
 
 323  
 }