Coverage Report - org.jaudiotagger.audio.mp4.atom.Mp4BoxHeader
 
Classes in this File Line Coverage Branch Coverage Complexity
Mp4BoxHeader
82%
79/96
73%
19/26
2.625
 
 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.atom;
 20  
 
 21  
 import org.jaudiotagger.audio.exceptions.InvalidBoxHeaderException;
 22  
 import org.jaudiotagger.audio.exceptions.NullBoxIdException;
 23  
 import org.jaudiotagger.audio.generic.Utils;
 24  
 import org.jaudiotagger.logging.ErrorMessage;
 25  
 
 26  
 import java.io.IOException;
 27  
 import java.io.RandomAccessFile;
 28  
 import java.io.UnsupportedEncodingException;
 29  
 import java.nio.ByteBuffer;
 30  
 import java.util.logging.Logger;
 31  
 
 32  
 /**
 33  
  * Everything in MP4s are held in boxes (formally known as atoms), they are held as a hierachial tree within the MP4.
 34  
  * <p/>
 35  
  * We are most interested in boxes that are used to hold metadata, but we have to know about some other boxes
 36  
  * as well in order to find them.
 37  
  * <p/>
 38  
  * All boxes consist of a 4 byte box length (big Endian), and then a 4 byte identifier, this is the header
 39  
  * which is model in this class.
 40  
  * <p/>
 41  
  * The length includes the length of the box including the identifier and the length itself.
 42  
  * Then they may contain data and/or sub boxes, if they contain subboxes they are known as a parent box. Parent boxes
 43  
  * shouldn't really contain data, but sometimes they do.
 44  
  * <p/>
 45  
  * Parent boxes length includes the length of their immediate sub boxes
 46  
  * <p/>
 47  
  * This class is normally used by instantiating with the empty constructor, then use the update method
 48  
  * to pass the header data which is used to read the identifier and the the size of the box
 49  
  */
 50  
 public class Mp4BoxHeader
 51  
 {
 52  
     // Logger Object
 53  4
     public static Logger logger = Logger.getLogger("org.jaudiotagger.audio.mp4.atom");
 54  
 
 55  
     public static final int OFFSET_POS = 0;
 56  
     public static final int IDENTIFIER_POS = 4;
 57  
     public static final int OFFSET_LENGTH = 4;
 58  
     public static final int IDENTIFIER_LENGTH = 4;
 59  
     public static final int HEADER_LENGTH = OFFSET_LENGTH + IDENTIFIER_LENGTH;
 60  
 
 61  
     //Box identifier
 62  
     private String id;
 63  
 
 64  
     //Box length
 65  
     protected int length;
 66  
 
 67  
     //If reading from file , this can be used to hold the headers position in the file
 68  
     private long filePos;
 69  
 
 70  
     //Raw Header data
 71  
     protected ByteBuffer dataBuffer;
 72  
 
 73  
     //Mp4 uses UTF-8 for all text
 74  
     public static final String CHARSET_UTF_8 = "UTF-8";
 75  
 
 76  
     /**
 77  
      * Construct empty header
 78  
      * <p/>
 79  
      * Can be populated later with update method
 80  
      */
 81  
     public Mp4BoxHeader()
 82  10369
     {
 83  
 
 84  10369
     }
 85  
 
 86  
      /**
 87  
      * Construct header to allow manual creation of header for writing to file
 88  
      * <p/>
 89  
       * @param id
 90  
       */
 91  
     public Mp4BoxHeader(String id)
 92  24
     {
 93  24
         if(id.length()!=IDENTIFIER_LENGTH)
 94  
         {
 95  0
             throw new RuntimeException("Invalid length:atom idenifier should always be 4 characters long");
 96  
         }
 97  24
         dataBuffer = ByteBuffer.allocate(HEADER_LENGTH);
 98  
         try
 99  
         {
 100  24
             this.id    = id;
 101  24
             dataBuffer.put(4, id.getBytes("ISO-8859-1")[0]);
 102  24
             dataBuffer.put(5, id.getBytes("ISO-8859-1")[1]);
 103  24
             dataBuffer.put(6, id.getBytes("ISO-8859-1")[2]);
 104  24
             dataBuffer.put(7, id.getBytes("ISO-8859-1")[3]);
 105  
         }
 106  0
         catch(UnsupportedEncodingException uee)
 107  
         {
 108  
             //Should never happen
 109  0
             throw new RuntimeException(uee);
 110  24
         }
 111  24
     }
 112  
 
 113  
     /**
 114  
      * Construct header
 115  
      * <p/>
 116  
      * Create header using headerdata, expected to find header at headerdata current position
 117  
      * <p/>
 118  
      * Note after processing adjusts position to immediately after header
 119  
      *
 120  
      * @param headerData
 121  
      */
 122  
     public Mp4BoxHeader(ByteBuffer headerData)
 123  49097
     {
 124  49097
         update(headerData);
 125  49024
     }
 126  
 
 127  
     /**
 128  
      * Create header using headerdata, expected to find header at headerdata current position
 129  
      * <p/>
 130  
      * Note after processing adjusts position to immediately after header
 131  
      *
 132  
      * @param headerData
 133  
      */
 134  
     public void update(ByteBuffer headerData)
 135  
     {
 136  
         //Read header data into byte array
 137  70848
         byte[] b = new byte[HEADER_LENGTH];
 138  70848
         headerData.get(b);
 139  
         //Keep reference to copy of RawData
 140  70848
         dataBuffer = ByteBuffer.wrap(b);
 141  
 
 142  
         //Calculate box size
 143  70848
         this.length = Utils.getIntBE(b, OFFSET_POS, OFFSET_LENGTH - 1);
 144  
         //Calculate box id
 145  70848
         this.id = Utils.getString(b, IDENTIFIER_POS, IDENTIFIER_LENGTH, "ISO-8859-1");
 146  
 
 147  70848
         logger.finest("Mp4BoxHeader id:"+id+":length:"+length);
 148  70848
         if (id.equals("\0\0\0\0"))
 149  
         {
 150  79
             throw new NullBoxIdException(ErrorMessage.MP4_UNABLE_TO_FIND_NEXT_ATOM_BECAUSE_IDENTIFIER_IS_INVALID.getMsg(id));
 151  
         }
 152  
 
 153  70769
         if(length<HEADER_LENGTH)
 154  
         {
 155  1
             throw new InvalidBoxHeaderException(ErrorMessage.MP4_UNABLE_TO_FIND_NEXT_ATOM_BECAUSE_IDENTIFIER_IS_INVALID.getMsg(id,length));
 156  
         }
 157  70768
     }
 158  
 
 159  
     /**
 160  
      * @return the box identifier
 161  
      */
 162  
     public String getId()
 163  
     {
 164  628324
         return id;
 165  
     }
 166  
 
 167  
     /**
 168  
      * @return the length of the boxes data (includes the header size)
 169  
      */
 170  
     public int getLength()
 171  
     {
 172  103573
         return length;
 173  
     }
 174  
 
 175  
     /**
 176  
      * Set the length.
 177  
      * <p/>
 178  
      * This will modify the databuffer accordingly
 179  
      *
 180  
      * @param length
 181  
      */
 182  
     public void setLength(int length)
 183  
     {
 184  262
         byte[] headerSize = Utils.getSizeBEInt32(length);
 185  262
         dataBuffer.put(0, headerSize[0]);
 186  262
         dataBuffer.put(1, headerSize[1]);
 187  262
         dataBuffer.put(2, headerSize[2]);
 188  262
         dataBuffer.put(3, headerSize[3]);
 189  
 
 190  262
         this.length = length;
 191  
 
 192  262
     }
 193  
 
 194  
     /**
 195  
      * Set the Id.
 196  
      * <p/>
 197  
      * Allows you to manully create a header
 198  
      * This will modify the databuffer accordingly
 199  
      *
 200  
      * @param length
 201  
      */
 202  
     public void setId(int length)
 203  
     {
 204  0
         byte[] headerSize = Utils.getSizeBEInt32(length);
 205  0
         dataBuffer.put(5, headerSize[0]);
 206  0
         dataBuffer.put(6, headerSize[1]);
 207  0
         dataBuffer.put(7, headerSize[2]);
 208  0
         dataBuffer.put(8, headerSize[3]);
 209  
 
 210  0
         this.length = length;
 211  
 
 212  0
     }
 213  
 
 214  
     /**
 215  
      * @return the 8 byte header buffer
 216  
      */
 217  
     public ByteBuffer getHeaderData()
 218  
     {
 219  352
         dataBuffer.rewind();
 220  352
         return dataBuffer;
 221  
     }
 222  
 
 223  
     /**
 224  
      * @return the length of the data only (does not include the header size)
 225  
      */
 226  
     public int getDataLength()
 227  
     {
 228  123116
         return length - HEADER_LENGTH;
 229  
     }
 230  
 
 231  
     public String toString()
 232  
     {
 233  0
         return "Box " + id + ":length" + length + ":filepos:" + filePos;
 234  
     }
 235  
 
 236  
     /**
 237  
      * @return UTF_8 (always used by Mp4)
 238  
      */
 239  
     public String getEncoding()
 240  
     {
 241  9510
         return CHARSET_UTF_8;
 242  
     }
 243  
 
 244  
 
 245  
     /**
 246  
      * Seek for box with the specified id starting from the current location of filepointer,
 247  
      * <p/>
 248  
      * Note it wont find the box if it is contained with a level below the current level, nor if we are
 249  
      * at a parent atom that also contains data and we havent yet processed the data. It will work
 250  
      * if we are at the start of a child box even if it not the required box as long as the box we are
 251  
      * looking for is the same level (or the level above in some cases).
 252  
      *
 253  
      * @param raf
 254  
      * @param id
 255  
      * @throws java.io.IOException
 256  
      * @return
 257  
      */
 258  
     public static Mp4BoxHeader seekWithinLevel(RandomAccessFile raf, String id) throws IOException
 259  
     {
 260  1214
         logger.finer("Started searching for:" + id + " in file at:" + raf.getChannel().position());
 261  
 
 262  1214
         Mp4BoxHeader boxHeader = new Mp4BoxHeader();
 263  1214
         ByteBuffer headerBuffer = ByteBuffer.allocate(HEADER_LENGTH);
 264  1214
         int bytesRead = raf.getChannel().read(headerBuffer);
 265  1214
         if (bytesRead != HEADER_LENGTH)
 266  
         {
 267  0
             return null;
 268  
         }
 269  1214
         headerBuffer.rewind();
 270  1214
         boxHeader.update(headerBuffer);
 271  1769
         while (!boxHeader.getId().equals(id))
 272  
         {
 273  555
             logger.finer("Found:" + boxHeader.getId() + " Still searching for:" + id + " in file at:" + raf.getChannel().position());
 274  
 
 275  
             //Something gone wrong probably not at the start of an atom so return null;
 276  555
             if (boxHeader.getLength() < Mp4BoxHeader.HEADER_LENGTH)
 277  
             {
 278  0
                 return null;
 279  
             }
 280  555
             int noOfBytesSkipped = raf.skipBytes(boxHeader.getDataLength());
 281  555
             logger.finer("Skipped:" + noOfBytesSkipped);
 282  555
             if (noOfBytesSkipped < boxHeader.getDataLength())
 283  
             {
 284  0
                 return null;
 285  
             }
 286  555
             headerBuffer.rewind();
 287  555
             bytesRead = raf.getChannel().read(headerBuffer);
 288  555
             logger.finer("Header Bytes Read:" + bytesRead);
 289  555
             headerBuffer.rewind();
 290  555
             if (bytesRead == Mp4BoxHeader.HEADER_LENGTH)
 291  
             {
 292  555
                 boxHeader.update(headerBuffer);
 293  
             }
 294  
             else
 295  
             {
 296  0
                 return null;
 297  
             }
 298  555
         }
 299  1214
         return boxHeader;
 300  
     }
 301  
 
 302  
 
 303  
     /**
 304  
      * Seek for box with the specified id starting from the current location of filepointer,
 305  
      * <p/>
 306  
      * Note it won't find the box if it is contained with a level below the current level, nor if we are
 307  
      * at a parent atom that also contains data and we havent yet processed the data. It will work
 308  
      * if we are at the start of a child box even if it not the required box as long as the box we are
 309  
      * looking for is the same level (or the level above in some cases).
 310  
      *
 311  
      * @param data
 312  
      * @param id
 313  
      * @throws java.io.IOException
 314  
      * @return
 315  
      */
 316  
     public static Mp4BoxHeader seekWithinLevel(ByteBuffer data, String id) throws IOException
 317  
     {
 318  5929
         logger.finer("Started searching for:" + id + " in bytebuffer at" + data.position());
 319  
 
 320  5929
         Mp4BoxHeader boxHeader = new Mp4BoxHeader();
 321  5929
         if (data.remaining() >= Mp4BoxHeader.HEADER_LENGTH)
 322  
         {
 323  5884
             boxHeader.update(data);
 324  
         }
 325  
         else
 326  
         {
 327  45
             return null;
 328  
         }
 329  8968
         while (!boxHeader.getId().equals(id))
 330  
         {
 331  3483
             logger.finer("Found:" + boxHeader.getId() + " Still searching for:" + id + " in bytebuffer at" + data.position());
 332  
             //Something gone wrong probably not at the start of an atom so return null;
 333  3483
             if (boxHeader.getLength() < Mp4BoxHeader.HEADER_LENGTH)
 334  
             {
 335  0
                 return null;
 336  
             }
 337  3483
             if(data.remaining()<(boxHeader.getLength() - HEADER_LENGTH))
 338  
             {
 339  
                 //i.e Could happen if Moov header had size incorrectly recorded
 340  0
                 return null;    
 341  
             }
 342  3483
             data.position(data.position() + (boxHeader.getLength() - HEADER_LENGTH));
 343  3483
             if (data.remaining() >= Mp4BoxHeader.HEADER_LENGTH)
 344  
             {
 345  3085
                 boxHeader.update(data);
 346  
             }
 347  
             else
 348  
             {
 349  398
                 return null;
 350  
             }
 351  
         }
 352  5485
         logger.finer("Found:" + id + " in bytebuffer at" + data.position());
 353  
 
 354  5485
         return boxHeader;
 355  
     }
 356  
 
 357  
     /**
 358  
      * @return location in file of the start of file header (i.e where the 4 byte length field starts)
 359  
      */
 360  
     public long getFilePos()
 361  
     {
 362  117065
         return filePos;
 363  
     }
 364  
 
 365  
     /**
 366  
      * Set location in file of the start of file header (i.e where the 4 byte length field starts)
 367  
      *
 368  
      * @param filePos
 369  
      */
 370  
     public void setFilePos(long filePos)
 371  
     {
 372  39642
         this.filePos = filePos;
 373  39642
     }
 374  
 }