FileDocCategorySizeDatePackage
Mp4BoxHeader.javaAPI DocJaudiotagger 2.0.412347Wed Mar 30 16:11:44 BST 2011org.jaudiotagger.audio.mp4.atom

Mp4BoxHeader.java

/*
 * Entagged Audio Tag library
 * Copyright (c) 2003-2005 Raphaƫl Slinckx <raphael@slinckx.net>
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *  
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jaudiotagger.audio.mp4.atom;

import org.jaudiotagger.audio.exceptions.InvalidBoxHeaderException;
import org.jaudiotagger.audio.exceptions.NullBoxIdException;
import org.jaudiotagger.audio.generic.Utils;
import org.jaudiotagger.logging.ErrorMessage;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.logging.Logger;

/**
 * Everything in MP4s are held in boxes (formally known as atoms), they are held as a hierachial tree within the MP4.
 * <p/>
 * We are most interested in boxes that are used to hold metadata, but we have to know about some other boxes
 * as well in order to find them.
 * <p/>
 * All boxes consist of a 4 byte box length (big Endian), and then a 4 byte identifier, this is the header
 * which is model in this class.
 * <p/>
 * The length includes the length of the box including the identifier and the length itself.
 * Then they may contain data and/or sub boxes, if they contain subboxes they are known as a parent box. Parent boxes
 * shouldn't really contain data, but sometimes they do.
 * <p/>
 * Parent boxes length includes the length of their immediate sub boxes
 * <p/>
 * This class is normally used by instantiating with the empty constructor, then use the update method
 * to pass the header data which is used to read the identifier and the the size of the box
 */
public class Mp4BoxHeader
{
    // Logger Object
    public static Logger logger = Logger.getLogger("org.jaudiotagger.audio.mp4.atom");

    public static final int OFFSET_POS = 0;
    public static final int IDENTIFIER_POS = 4;
    public static final int OFFSET_LENGTH = 4;
    public static final int IDENTIFIER_LENGTH = 4;
    public static final int HEADER_LENGTH = OFFSET_LENGTH + IDENTIFIER_LENGTH;

    //Box identifier
    private String id;

    //Box length
    protected int length;

    //If reading from file , this can be used to hold the headers position in the file
    private long filePos;

    //Raw Header data
    protected ByteBuffer dataBuffer;

    //Mp4 uses UTF-8 for all text
    public static final String CHARSET_UTF_8 = "UTF-8";

    /**
     * Construct empty header
     * <p/>
     * Can be populated later with update method
     */
    public Mp4BoxHeader()
    {

    }

     /**
     * Construct header to allow manual creation of header for writing to file
     * <p/>
      * @param id
      */
    public Mp4BoxHeader(String id)
    {
        if(id.length()!=IDENTIFIER_LENGTH)
        {
            throw new RuntimeException("Invalid length:atom idenifier should always be 4 characters long");
        }
        dataBuffer = ByteBuffer.allocate(HEADER_LENGTH);
        try
        {
            this.id    = id;
            dataBuffer.put(4, id.getBytes("ISO-8859-1")[0]);
            dataBuffer.put(5, id.getBytes("ISO-8859-1")[1]);
            dataBuffer.put(6, id.getBytes("ISO-8859-1")[2]);
            dataBuffer.put(7, id.getBytes("ISO-8859-1")[3]);
        }
        catch(UnsupportedEncodingException uee)
        {
            //Should never happen
            throw new RuntimeException(uee);
        }
    }

    /**
     * Construct header
     * <p/>
     * Create header using headerdata, expected to find header at headerdata current position
     * <p/>
     * Note after processing adjusts position to immediately after header
     *
     * @param headerData
     */
    public Mp4BoxHeader(ByteBuffer headerData)
    {
        update(headerData);
    }

    /**
     * Create header using headerdata, expected to find header at headerdata current position
     * <p/>
     * Note after processing adjusts position to immediately after header
     *
     * @param headerData
     */
    public void update(ByteBuffer headerData)
    {
        //Read header data into byte array
        byte[] b = new byte[HEADER_LENGTH];
        headerData.get(b);
        //Keep reference to copy of RawData
        dataBuffer = ByteBuffer.wrap(b);

        //Calculate box size
        this.length = Utils.getIntBE(b, OFFSET_POS, OFFSET_LENGTH - 1);
        //Calculate box id
        this.id = Utils.getString(b, IDENTIFIER_POS, IDENTIFIER_LENGTH, "ISO-8859-1");

        logger.finest("Mp4BoxHeader id:"+id+":length:"+length);
        if (id.equals("\0\0\0\0"))
        {
            throw new NullBoxIdException(ErrorMessage.MP4_UNABLE_TO_FIND_NEXT_ATOM_BECAUSE_IDENTIFIER_IS_INVALID.getMsg(id));
        }

        if(length<HEADER_LENGTH)
        {
            throw new InvalidBoxHeaderException(ErrorMessage.MP4_UNABLE_TO_FIND_NEXT_ATOM_BECAUSE_IDENTIFIER_IS_INVALID.getMsg(id,length));
        }
    }

    /**
     * @return the box identifier
     */
    public String getId()
    {
        return id;
    }

    /**
     * @return the length of the boxes data (includes the header size)
     */
    public int getLength()
    {
        return length;
    }

    /**
     * Set the length.
     * <p/>
     * This will modify the databuffer accordingly
     *
     * @param length
     */
    public void setLength(int length)
    {
        byte[] headerSize = Utils.getSizeBEInt32(length);
        dataBuffer.put(0, headerSize[0]);
        dataBuffer.put(1, headerSize[1]);
        dataBuffer.put(2, headerSize[2]);
        dataBuffer.put(3, headerSize[3]);

        this.length = length;

    }

    /**
     * Set the Id.
     * <p/>
     * Allows you to manully create a header
     * This will modify the databuffer accordingly
     *
     * @param length
     */
    public void setId(int length)
    {
        byte[] headerSize = Utils.getSizeBEInt32(length);
        dataBuffer.put(5, headerSize[0]);
        dataBuffer.put(6, headerSize[1]);
        dataBuffer.put(7, headerSize[2]);
        dataBuffer.put(8, headerSize[3]);

        this.length = length;

    }

    /**
     * @return the 8 byte header buffer
     */
    public ByteBuffer getHeaderData()
    {
        dataBuffer.rewind();
        return dataBuffer;
    }

    /**
     * @return the length of the data only (does not include the header size)
     */
    public int getDataLength()
    {
        return length - HEADER_LENGTH;
    }

    public String toString()
    {
        return "Box " + id + ":length" + length + ":filepos:" + filePos;
    }

    /**
     * @return UTF_8 (always used by Mp4)
     */
    public String getEncoding()
    {
        return CHARSET_UTF_8;
    }


    /**
     * Seek for box with the specified id starting from the current location of filepointer,
     * <p/>
     * Note it wont find the box if it is contained with a level below the current level, nor if we are
     * at a parent atom that also contains data and we havent yet processed the data. It will work
     * if we are at the start of a child box even if it not the required box as long as the box we are
     * looking for is the same level (or the level above in some cases).
     *
     * @param raf
     * @param id
     * @throws java.io.IOException
     * @return
     */
    public static Mp4BoxHeader seekWithinLevel(RandomAccessFile raf, String id) throws IOException
    {
        logger.finer("Started searching for:" + id + " in file at:" + raf.getChannel().position());

        Mp4BoxHeader boxHeader = new Mp4BoxHeader();
        ByteBuffer headerBuffer = ByteBuffer.allocate(HEADER_LENGTH);
        int bytesRead = raf.getChannel().read(headerBuffer);
        if (bytesRead != HEADER_LENGTH)
        {
            return null;
        }
        headerBuffer.rewind();
        boxHeader.update(headerBuffer);
        while (!boxHeader.getId().equals(id))
        {
            logger.finer("Found:" + boxHeader.getId() + " Still searching for:" + id + " in file at:" + raf.getChannel().position());

            //Something gone wrong probably not at the start of an atom so return null;
            if (boxHeader.getLength() < Mp4BoxHeader.HEADER_LENGTH)
            {
                return null;
            }
            int noOfBytesSkipped = raf.skipBytes(boxHeader.getDataLength());
            logger.finer("Skipped:" + noOfBytesSkipped);
            if (noOfBytesSkipped < boxHeader.getDataLength())
            {
                return null;
            }
            headerBuffer.rewind();
            bytesRead = raf.getChannel().read(headerBuffer);
            logger.finer("Header Bytes Read:" + bytesRead);
            headerBuffer.rewind();
            if (bytesRead == Mp4BoxHeader.HEADER_LENGTH)
            {
                boxHeader.update(headerBuffer);
            }
            else
            {
                return null;
            }
        }
        return boxHeader;
    }


    /**
     * Seek for box with the specified id starting from the current location of filepointer,
     * <p/>
     * Note it won't find the box if it is contained with a level below the current level, nor if we are
     * at a parent atom that also contains data and we havent yet processed the data. It will work
     * if we are at the start of a child box even if it not the required box as long as the box we are
     * looking for is the same level (or the level above in some cases).
     *
     * @param data
     * @param id
     * @throws java.io.IOException
     * @return
     */
    public static Mp4BoxHeader seekWithinLevel(ByteBuffer data, String id) throws IOException
    {
        logger.finer("Started searching for:" + id + " in bytebuffer at" + data.position());

        Mp4BoxHeader boxHeader = new Mp4BoxHeader();
        if (data.remaining() >= Mp4BoxHeader.HEADER_LENGTH)
        {
            boxHeader.update(data);
        }
        else
        {
            return null;
        }
        while (!boxHeader.getId().equals(id))
        {
            logger.finer("Found:" + boxHeader.getId() + " Still searching for:" + id + " in bytebuffer at" + data.position());
            //Something gone wrong probably not at the start of an atom so return null;
            if (boxHeader.getLength() < Mp4BoxHeader.HEADER_LENGTH)
            {
                return null;
            }
            if(data.remaining()<(boxHeader.getLength() - HEADER_LENGTH))
            {
                //i.e Could happen if Moov header had size incorrectly recorded
                return null;    
            }
            data.position(data.position() + (boxHeader.getLength() - HEADER_LENGTH));
            if (data.remaining() >= Mp4BoxHeader.HEADER_LENGTH)
            {
                boxHeader.update(data);
            }
            else
            {
                return null;
            }
        }
        logger.finer("Found:" + id + " in bytebuffer at" + data.position());

        return boxHeader;
    }

    /**
     * @return location in file of the start of file header (i.e where the 4 byte length field starts)
     */
    public long getFilePos()
    {
        return filePos;
    }

    /**
     * Set location in file of the start of file header (i.e where the 4 byte length field starts)
     *
     * @param filePos
     */
    public void setFilePos(long filePos)
    {
        this.filePos = filePos;
    }
}