/*
* 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;
}
}
|