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.generic.Utils;
 22  
 import org.jaudiotagger.audio.exceptions.NullBoxIdException;
 23  
 import org.jaudiotagger.audio.exceptions.InvalidBoxHeaderException;
 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  17
     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  3000
     {
 83  
 
 84  3000
     }
 85  
 
 86  
      /**
 87  
      * Construct header to allow manual creation of header for writing to file
 88  
      * <p/>
 89  
      */
 90  
     public Mp4BoxHeader(String id)
 91  6
     {
 92  6
         if(id.length()!=IDENTIFIER_LENGTH)
 93  
         {
 94  0
             throw new RuntimeException("Invalid length:atom idenifier should always be 4 characters long");
 95  
         }
 96  6
         dataBuffer = ByteBuffer.allocate(HEADER_LENGTH);
 97  
         try
 98  
         {
 99  6
             this.id    = id;
 100  6
             dataBuffer.put(4, id.getBytes("ISO-8859-1")[0]);
 101  6
             dataBuffer.put(5, id.getBytes("ISO-8859-1")[1]);
 102  6
             dataBuffer.put(6, id.getBytes("ISO-8859-1")[2]);
 103  6
             dataBuffer.put(7, id.getBytes("ISO-8859-1")[3]);
 104  
         }
 105  0
         catch(UnsupportedEncodingException uee)
 106  
         {
 107  
             //Should never happen
 108  0
             throw new RuntimeException(uee);
 109  6
         }
 110  6
     }
 111  
 
 112  
     /**
 113  
      * Construct header
 114  
      * <p/>
 115  
      * Create header using headerdata, expected to find header at headerdata current position
 116  
      * <p/>
 117  
      * Note after processing adjusts position to immediately after header
 118  
      *
 119  
      * @param headerData
 120  
      */
 121  
     public Mp4BoxHeader(ByteBuffer headerData)
 122  13429
     {
 123  13429
         update(headerData);
 124  13410
     }
 125  
 
 126  
     /**
 127  
      * Create header using headerdata, expected to find header at headerdata current position
 128  
      * <p/>
 129  
      * Note after processing adjusts position to immediately after header
 130  
      *
 131  
      * @param headerData
 132  
      */
 133  
     public void update(ByteBuffer headerData)
 134  
     {
 135  
         //Read header data into byte array
 136  19626
         byte[] b = new byte[HEADER_LENGTH];
 137  19626
         headerData.get(b);
 138  
         //Keep reference to copy of RawData
 139  19626
         dataBuffer = ByteBuffer.wrap(b);
 140  
 
 141  
         //Calculate box size
 142  19626
         this.length = Utils.getIntBE(b, OFFSET_POS, OFFSET_LENGTH - 1);
 143  
         //Calculate box id
 144  19626
         this.id = Utils.getString(b, IDENTIFIER_POS, IDENTIFIER_LENGTH, "ISO-8859-1");
 145  
 
 146  19626
         logger.finest("Mp4BoxHeader id:"+id+":length:"+length);
 147  19626
         if (id.equals("\0\0\0\0"))
 148  
         {
 149  24
             throw new NullBoxIdException(ErrorMessage.MP4_UNABLE_TO_FIND_NEXT_ATOM_BECAUSE_IDENTIFIER_IS_INVALID.getMsg(id));
 150  
         }
 151  
 
 152  19602
         if(length<HEADER_LENGTH)
 153  
         {
 154  1
             throw new InvalidBoxHeaderException(ErrorMessage.MP4_UNABLE_TO_FIND_NEXT_ATOM_BECAUSE_IDENTIFIER_IS_INVALID.getMsg(id,length));
 155  
         }
 156  19601
     }
 157  
 
 158  
     /**
 159  
      * @return the box identifier
 160  
      */
 161  
     public String getId()
 162  
     {
 163  173574
         return id;
 164  
     }
 165  
 
 166  
     /**
 167  
      * @return the length of the boxes data (includes the header size)
 168  
      */
 169  
     public int getLength()
 170  
     {
 171  28706
         return length;
 172  
     }
 173  
 
 174  
     /**
 175  
      * Set the length.
 176  
      * <p/>
 177  
      * This will modify the databuffer accordingly
 178  
      *
 179  
      * @param length
 180  
      */
 181  
     public void setLength(int length)
 182  
     {
 183  68
         byte[] headerSize = Utils.getSizeBEInt32(length);
 184  68
         dataBuffer.put(0, headerSize[0]);
 185  68
         dataBuffer.put(1, headerSize[1]);
 186  68
         dataBuffer.put(2, headerSize[2]);
 187  68
         dataBuffer.put(3, headerSize[3]);
 188  
 
 189  68
         this.length = length;
 190  
 
 191  68
     }
 192  
 
 193  
     /**
 194  
      * Set the Id.
 195  
      * <p/>
 196  
      * Allows you to manully create a header
 197  
      * This will modify the databuffer accordingly
 198  
      *
 199  
      * @param length
 200  
      */
 201  
     public void setId(int length)
 202  
     {
 203  0
         byte[] headerSize = Utils.getSizeBEInt32(length);
 204  0
         dataBuffer.put(5, headerSize[0]);
 205  0
         dataBuffer.put(6, headerSize[1]);
 206  0
         dataBuffer.put(7, headerSize[2]);
 207  0
         dataBuffer.put(8, headerSize[3]);
 208  
 
 209  0
         this.length = length;
 210  
 
 211  0
     }
 212  
 
 213  
     /**
 214  
      * @return the 8 byte header buffer
 215  
      */
 216  
     public ByteBuffer getHeaderData()
 217  
     {
 218  95
         dataBuffer.rewind();
 219  95
         return dataBuffer;
 220  
     }
 221  
 
 222  
     /**
 223  
      * @return the length of the data only (does not include the header size)
 224  
      */
 225  
     public int getDataLength()
 226  
     {
 227  33854
         return length - HEADER_LENGTH;
 228  
     }
 229  
 
 230  
     public String toString()
 231  
     {
 232  0
         return "Box " + id + ":length" + length + ":filepos:" + filePos;
 233  
     }
 234  
 
 235  
     /**
 236  
      * @return UTF_8 (always used by Mp4)
 237  
      */
 238  
     public String getEncoding()
 239  
     {
 240  2448
         return CHARSET_UTF_8;
 241  
     }
 242  
 
 243  
 
 244  
     /**
 245  
      * Seek for box with the specified id starting from the current location of filepointer,
 246  
      * <p/>
 247  
      * Note it wont find the box if it is contained with a level below the current level, nor if we are
 248  
      * at a parent atom that also contains data and we havent yet processed the data. It will work
 249  
      * if we are at the start of a child box even if it not the required box as long as the box we are
 250  
      * looking for is the same level (or the level above in some cases).
 251  
      *
 252  
      * @param raf
 253  
      * @param id
 254  
      * @throws java.io.IOException
 255  
      */
 256  
     public static Mp4BoxHeader seekWithinLevel(RandomAccessFile raf, String id) throws IOException
 257  
     {
 258  349
         logger.finer("Started searching for:" + id + " in file at:" + raf.getChannel().position());
 259  
 
 260  349
         Mp4BoxHeader boxHeader = new Mp4BoxHeader();
 261  349
         ByteBuffer headerBuffer = ByteBuffer.allocate(HEADER_LENGTH);
 262  349
         int bytesRead = raf.getChannel().read(headerBuffer);
 263  349
         if (bytesRead != HEADER_LENGTH)
 264  
         {
 265  0
             return null;
 266  
         }
 267  349
         headerBuffer.rewind();
 268  349
         boxHeader.update(headerBuffer);
 269  524
         while (!boxHeader.getId().equals(id))
 270  
         {
 271  175
             logger.finer("Found:" + boxHeader.getId() + " Still searching for:" + id + " in file at:" + raf.getChannel().position());
 272  
 
 273  
             //Something gone wrong probably not at the start of an atom so return null;
 274  175
             if (boxHeader.getLength() < Mp4BoxHeader.HEADER_LENGTH)
 275  
             {
 276  0
                 return null;
 277  
             }
 278  175
             int noOfBytesSkipped = raf.skipBytes(boxHeader.getDataLength());
 279  175
             logger.finer("Skipped:" + noOfBytesSkipped);
 280  175
             if (noOfBytesSkipped < boxHeader.getDataLength())
 281  
             {
 282  0
                 return null;
 283  
             }
 284  175
             headerBuffer.rewind();
 285  175
             bytesRead = raf.getChannel().read(headerBuffer);
 286  175
             logger.finer("Header Bytes Read:" + bytesRead);
 287  175
             headerBuffer.rewind();
 288  175
             if (bytesRead == Mp4BoxHeader.HEADER_LENGTH)
 289  
             {
 290  175
                 boxHeader.update(headerBuffer);
 291  
             }
 292  
             else
 293  
             {
 294  0
                 return null;
 295  
             }
 296  175
         }
 297  349
         return boxHeader;
 298  
     }
 299  
 
 300  
 
 301  
     /**
 302  
      * Seek for box with the specified id starting from the current location of filepointer,
 303  
      * <p/>
 304  
      * Note it won't find the box if it is contained with a level below the current level, nor if we are
 305  
      * at a parent atom that also contains data and we havent yet processed the data. It will work
 306  
      * if we are at the start of a child box even if it not the required box as long as the box we are
 307  
      * looking for is the same level (or the level above in some cases).
 308  
      *
 309  
      * @param data
 310  
      * @param id
 311  
      * @throws java.io.IOException
 312  
      */
 313  
     public static Mp4BoxHeader seekWithinLevel(ByteBuffer data, String id) throws IOException
 314  
     {
 315  1709
         logger.finer("Started searching for:" + id + " in bytebuffer at" + data.position());
 316  
 
 317  1709
         Mp4BoxHeader boxHeader = new Mp4BoxHeader();
 318  1709
         if (data.remaining() >= Mp4BoxHeader.HEADER_LENGTH)
 319  
         {
 320  1695
             boxHeader.update(data);
 321  
         }
 322  
         else
 323  
         {
 324  14
             return null;
 325  
         }
 326  2618
         while (!boxHeader.getId().equals(id))
 327  
         {
 328  1046
             logger.finer("Found:" + boxHeader.getId() + " Still searching for:" + id + " in bytebuffer at" + data.position());
 329  
             //Something gone wrong probably not at the start of an atom so return null;
 330  1046
             if (boxHeader.getLength() < Mp4BoxHeader.HEADER_LENGTH)
 331  
             {
 332  0
                 return null;
 333  
             }
 334  1046
             if(data.remaining()<(boxHeader.getLength() - HEADER_LENGTH))
 335  
             {
 336  
                 //i.e Could happen if Moov header had size incorrectly recorded
 337  0
                 return null;    
 338  
             }
 339  1046
             data.position(data.position() + (boxHeader.getLength() - HEADER_LENGTH));
 340  1046
             if (data.remaining() >= Mp4BoxHeader.HEADER_LENGTH)
 341  
             {
 342  923
                 boxHeader.update(data);
 343  
             }
 344  
             else
 345  
             {
 346  123
                 return null;
 347  
             }
 348  
         }
 349  1572
         logger.finer("Found:" + id + " in bytebuffer at" + data.position());
 350  
 
 351  1572
         return boxHeader;
 352  
     }
 353  
 
 354  
     /**
 355  
      * @return location in file of the start of file header (i.e where the 4 byte length field starts)
 356  
      */
 357  
     public long getFilePos()
 358  
     {
 359  32268
         return filePos;
 360  
     }
 361  
 
 362  
     /**
 363  
      * Set location in file of the start of file header (i.e where the 4 byte length field starts)
 364  
      *
 365  
      * @param filePos
 366  
      */
 367  
     public void setFilePos(long filePos)
 368  
     {
 369  10987
         this.filePos = filePos;
 370  10987
     }
 371  
 }