package org.jaudiotagger.audio.mp4;
import org.jaudiotagger.utils.tree.DefaultMutableTreeNode;
import org.jaudiotagger.utils.tree.DefaultTreeModel;
import org.jaudiotagger.audio.exceptions.CannotReadException;
import org.jaudiotagger.audio.exceptions.NullBoxIdException;
import org.jaudiotagger.audio.mp4.atom.Mp4BoxHeader;
import org.jaudiotagger.audio.mp4.atom.Mp4MetaBox;
import org.jaudiotagger.audio.mp4.atom.Mp4StcoBox;
import org.jaudiotagger.audio.mp4.atom.NullPadding;
import org.jaudiotagger.logging.ErrorMessage;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.logging.Logger;
/**
* Tree representing atoms in the mp4 file
* <p/>
* Note it doesn't create the complete tree it delves into subtrees for atom we know about and are interested in. (Note
* it would be impossible to create a complete tree for any file without understanding all the nodes because
* some atoms such as meta contain data and children and therefore need to be specially preprocessed)
* <p/>
* This class is currently only used when writing tags because it better handles the difficulties of mdat and free
* atoms being optional/multiple places then the older sequential method. It is expected this class will eventually
* be used when reading tags as well.
* <p/>
* Uses a TreeModel for the tree, with convenience methods holding onto references to most common nodes so they
* can be used without having to traverse the tree again.
*/
public class Mp4AtomTree
{
private DefaultMutableTreeNode rootNode;
private DefaultTreeModel dataTree;
private DefaultMutableTreeNode moovNode;
private DefaultMutableTreeNode mdatNode;
private DefaultMutableTreeNode stcoNode;
private DefaultMutableTreeNode ilstNode;
private DefaultMutableTreeNode metaNode;
private DefaultMutableTreeNode tagsNode;
private DefaultMutableTreeNode udtaNode;
private DefaultMutableTreeNode hdlrWithinMdiaNode;
private DefaultMutableTreeNode hdlrWithinMetaNode;
private List<DefaultMutableTreeNode> freeNodes = new ArrayList<DefaultMutableTreeNode>();
private List<DefaultMutableTreeNode> mdatNodes = new ArrayList<DefaultMutableTreeNode>();
private List<DefaultMutableTreeNode> trakNodes = new ArrayList<DefaultMutableTreeNode>();
private Mp4StcoBox stco;
private ByteBuffer moovBuffer; //Contains all the data under moov
private Mp4BoxHeader moovHeader;
//Logger Object
public static Logger logger = Logger.getLogger("org.jaudiotagger.audio.mp4");
/**
* Create Atom Tree
*
* @param raf
* @throws IOException
* @throws CannotReadException
*/
public Mp4AtomTree(RandomAccessFile raf) throws IOException, CannotReadException
{
buildTree(raf, true);
}
/**
* Create Atom Tree and maintain open channel to raf, should only be used if will continue
* to use raf after this call, you will have to close raf yourself.
*
* @param raf
* @param closeOnExit to keep randomfileaccess open, only used when randomaccessfile already being used
* @throws IOException
* @throws CannotReadException
*/
public Mp4AtomTree(RandomAccessFile raf, boolean closeOnExit) throws IOException, CannotReadException
{
buildTree(raf, closeOnExit);
}
/**
* Build a tree of the atoms in the file
*
* @param raf
* @param closeExit false to keep randomfileacces open, only used when randomaccessfile already being used
* @return
* @throws java.io.IOException
* @throws org.jaudiotagger.audio.exceptions.CannotReadException
*/
public DefaultTreeModel buildTree(RandomAccessFile raf, boolean closeExit) throws IOException, CannotReadException
{
FileChannel fc = null;
try
{
fc = raf.getChannel();
//make sure at start of file
fc.position(0);
//Build up map of nodes
rootNode = new DefaultMutableTreeNode();
dataTree = new DefaultTreeModel(rootNode);
//Iterate though all the top level Nodes
ByteBuffer headerBuffer = ByteBuffer.allocate(Mp4BoxHeader.HEADER_LENGTH);
while (fc.position() < fc.size())
{
Mp4BoxHeader boxHeader = new Mp4BoxHeader();
headerBuffer.clear();
fc.read(headerBuffer);
headerBuffer.rewind();
try
{
boxHeader.update(headerBuffer);
}
catch(NullBoxIdException ne)
{
//If we only get this error after all the expected data has been found we allow it
if(moovNode!=null&mdatNode!=null)
{
NullPadding np = new NullPadding(fc.position() - Mp4BoxHeader.HEADER_LENGTH,fc.size());
DefaultMutableTreeNode trailingPaddingNode = new DefaultMutableTreeNode(np);
rootNode.add(trailingPaddingNode);
logger.warning(ErrorMessage.NULL_PADDING_FOUND_AT_END_OF_MP4.getMsg(np.getFilePos()));
break;
}
else
{
//File appears invalid
throw ne;
}
}
boxHeader.setFilePos(fc.position() - Mp4BoxHeader.HEADER_LENGTH);
DefaultMutableTreeNode newAtom = new DefaultMutableTreeNode(boxHeader);
//Go down moov
if (boxHeader.getId().equals(Mp4AtomIdentifier.MOOV.getFieldName()))
{
moovNode = newAtom;
moovHeader = boxHeader;
long filePosStart = fc.position();
moovBuffer = ByteBuffer.allocate(boxHeader.getDataLength());
fc.read(moovBuffer);
moovBuffer.rewind();
/*Maybe needed but dont have test case yet
if(filePosStart + boxHeader.getDataLength() > fc.size())
{
throw new CannotReadException("The atom states its datalength to be "+boxHeader.getDataLength()
+ "but there are only "+fc.size()+"bytes in the file and already at position "+filePosStart);
}
*/
buildChildrenOfNode(moovBuffer, newAtom);
fc.position(filePosStart);
}
else if (boxHeader.getId().equals(Mp4AtomIdentifier.FREE.getFieldName()))
{
//Might be multiple in different locations
freeNodes.add(newAtom);
}
else if (boxHeader.getId().equals(Mp4AtomIdentifier.MDAT.getFieldName()))
{
//mdatNode always points to the last mDatNode, normally there is just one mdatnode but do have
//a valid example of multiple mdatnode
//if(mdatNode!=null)
//{
// throw new CannotReadException(ErrorMessage.MP4_FILE_CONTAINS_MULTIPLE_DATA_ATOMS.getMsg());
//}
mdatNode = newAtom;
mdatNodes.add(newAtom);
}
rootNode.add(newAtom);
fc.position(fc.position() + boxHeader.getDataLength());
}
return dataTree;
}
finally
{
//If we cant find the audio then we cannot modify this file so better to throw exception
//now rather than later when try and write to it.
if(mdatNode==null)
{
throw new CannotReadException(ErrorMessage.MP4_CANNOT_FIND_AUDIO.getMsg());
}
if (closeExit)
{
fc.close();
}
}
}
/**
* Display atom tree
*/
@SuppressWarnings("unchecked")
public void printAtomTree()
{
Enumeration<DefaultMutableTreeNode> e = rootNode.preorderEnumeration();
DefaultMutableTreeNode nextNode;
while (e.hasMoreElements())
{
nextNode = e.nextElement();
Mp4BoxHeader header = (Mp4BoxHeader) nextNode.getUserObject();
if (header != null)
{
String tabbing = "";
for (int i = 1; i < nextNode.getLevel(); i++)
{
tabbing += "\t";
}
if(header instanceof NullPadding)
{
System.out.println(tabbing + "Null pad " + " @ " + header.getFilePos() + " of size:" + header.getLength() + " ,ends @ " + (header.getFilePos() + header.getLength()));
}
else
{
System.out.println(tabbing + "Atom " + header.getId() + " @ " + header.getFilePos() + " of size:" + header.getLength() + " ,ends @ " + (header.getFilePos() + header.getLength()));
}
}
}
}
/**
*
* @param moovBuffer
* @param parentNode
* @throws IOException
* @throws CannotReadException
*/
public void buildChildrenOfNode(ByteBuffer moovBuffer, DefaultMutableTreeNode parentNode) throws IOException, CannotReadException
{
Mp4BoxHeader boxHeader;
//Preprocessing for nodes that contain data before their children atoms
Mp4BoxHeader parentBoxHeader = (Mp4BoxHeader) parentNode.getUserObject();
//We set the buffers position back to this after processing the children
int justAfterHeaderPos = moovBuffer.position();
//Preprocessing for meta that normally contains 4 data bytes, but doesn't where found under track or tags atom
if (parentBoxHeader.getId().equals(Mp4AtomIdentifier.META.getFieldName()))
{
Mp4MetaBox meta = new Mp4MetaBox(parentBoxHeader, moovBuffer);
meta.processData();
try
{
boxHeader = new Mp4BoxHeader(moovBuffer);
}
catch(NullBoxIdException nbe)
{
//It might be that the meta box didn't actually have any additional data after it so we adjust the buffer
//to be immediately after metabox and code can retry
moovBuffer.position(moovBuffer.position()-Mp4MetaBox.FLAGS_LENGTH);
}
finally
{
//Skip back last header cos this was only a test
moovBuffer.position(moovBuffer.position()- Mp4BoxHeader.HEADER_LENGTH);
}
}
//Defines where to start looking for the first child node
int startPos = moovBuffer.position();
while (moovBuffer.position() < ((startPos + parentBoxHeader.getDataLength()) - Mp4BoxHeader.HEADER_LENGTH))
{
boxHeader = new Mp4BoxHeader(moovBuffer);
if (boxHeader != null)
{
boxHeader.setFilePos(moovHeader.getFilePos() + moovBuffer.position());
logger.finest("Atom " + boxHeader.getId() + " @ " + boxHeader.getFilePos() + " of size:" + boxHeader.getLength() + " ,ends @ " + (boxHeader.getFilePos() + boxHeader.getLength()));
DefaultMutableTreeNode newAtom = new DefaultMutableTreeNode(boxHeader);
parentNode.add(newAtom);
if (boxHeader.getId().equals(Mp4AtomIdentifier.UDTA.getFieldName()))
{
udtaNode = newAtom;
}
//only interested in metaNode that is child of udta node
else if (boxHeader.getId().equals(Mp4AtomIdentifier.META.getFieldName())&&parentBoxHeader.getId().equals(Mp4AtomIdentifier.UDTA.getFieldName()))
{
metaNode = newAtom;
}
else if (boxHeader.getId().equals(Mp4AtomIdentifier.HDLR.getFieldName())&&parentBoxHeader.getId().equals(Mp4AtomIdentifier.META.getFieldName()))
{
hdlrWithinMetaNode = newAtom;
}
else if (boxHeader.getId().equals(Mp4AtomIdentifier.HDLR.getFieldName()))
{
hdlrWithinMdiaNode = newAtom;
}
else if (boxHeader.getId().equals(Mp4AtomIdentifier.TAGS.getFieldName()))
{
tagsNode = newAtom;
}
else if (boxHeader.getId().equals(Mp4AtomIdentifier.STCO.getFieldName()))
{
if (stco == null)
{
stco = new Mp4StcoBox(boxHeader, moovBuffer);
stcoNode = newAtom;
}
}
else if (boxHeader.getId().equals(Mp4AtomIdentifier.ILST.getFieldName()))
{
DefaultMutableTreeNode parent = (DefaultMutableTreeNode)parentNode.getParent();
if(parent!=null)
{
Mp4BoxHeader parentsParent = (Mp4BoxHeader)(parent).getUserObject();
if(parentsParent!=null)
{
if(parentBoxHeader.getId().equals(Mp4AtomIdentifier.META.getFieldName())&&parentsParent.getId().equals(Mp4AtomIdentifier.UDTA.getFieldName()))
{
ilstNode = newAtom;
}
}
}
}
else if (boxHeader.getId().equals(Mp4AtomIdentifier.FREE.getFieldName()))
{
//Might be multiple in different locations
freeNodes.add(newAtom);
}
else if (boxHeader.getId().equals(Mp4AtomIdentifier.TRAK.getFieldName()))
{
//Might be multiple in different locations, although only one should be audio track
trakNodes.add(newAtom);
}
//For these atoms iterate down to build their children
if ((boxHeader.getId().equals(Mp4AtomIdentifier.TRAK.getFieldName())) ||
(boxHeader.getId().equals(Mp4AtomIdentifier.MDIA.getFieldName())) ||
(boxHeader.getId().equals(Mp4AtomIdentifier.MINF.getFieldName())) ||
(boxHeader.getId().equals(Mp4AtomIdentifier.STBL.getFieldName())) ||
(boxHeader.getId().equals(Mp4AtomIdentifier.UDTA.getFieldName())) ||
(boxHeader.getId().equals(Mp4AtomIdentifier.META.getFieldName())) ||
(boxHeader.getId().equals(Mp4AtomIdentifier.ILST.getFieldName())))
{
buildChildrenOfNode(moovBuffer, newAtom);
}
//Now adjust buffer for the next atom header at this level
moovBuffer.position(moovBuffer.position() + boxHeader.getDataLength());
}
}
moovBuffer.position(justAfterHeaderPos);
}
/**
*
* @return
*/
public DefaultTreeModel getDataTree()
{
return dataTree;
}
/**
*
* @return
*/
public DefaultMutableTreeNode getMoovNode()
{
return moovNode;
}
/**
*
* @return
*/
public DefaultMutableTreeNode getStcoNode()
{
return stcoNode;
}
/**
*
* @return
*/
public DefaultMutableTreeNode getIlstNode()
{
return ilstNode;
}
/**
*
* @param node
* @return
*/
public Mp4BoxHeader getBoxHeader(DefaultMutableTreeNode node)
{
if (node == null)
{
return null;
}
return (Mp4BoxHeader) node.getUserObject();
}
/**
*
* @return
*/
public DefaultMutableTreeNode getMdatNode()
{
return mdatNode;
}
/**
*
* @return
*/
public DefaultMutableTreeNode getUdtaNode()
{
return udtaNode;
}
/**
*
* @return
*/
public DefaultMutableTreeNode getMetaNode()
{
return metaNode;
}
/**
*
* @return
*/
public DefaultMutableTreeNode getHdlrWithinMetaNode()
{
return hdlrWithinMetaNode;
}
/**
*
* @return
*/
public DefaultMutableTreeNode getHdlrWithinMdiaNode()
{
return hdlrWithinMdiaNode;
}
/**
*
* @return
*/
public DefaultMutableTreeNode getTagsNode()
{
return tagsNode;
}
/**
*
* @return
*/
public List<DefaultMutableTreeNode> getFreeNodes()
{
return freeNodes;
}
/**
*
* @return
*/
public List<DefaultMutableTreeNode> getTrakNodes()
{
return trakNodes;
}
/**
*
* @return
*/
public Mp4StcoBox getStco()
{
return stco;
}
/**
*
* @return
*/
public ByteBuffer getMoovBuffer()
{
return moovBuffer;
}
/**
*
* @return
*/
public Mp4BoxHeader getMoovHeader()
{
return moovHeader;
}
}
|