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(Mp4NotMetaFieldKey.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(Mp4NotMetaFieldKey.FREE.getFieldName())) {
//Might be multiple in different locations
freeNodes.add(newAtom);
} else if (boxHeader.getId().equals(Mp4NotMetaFieldKey.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(Mp4NotMetaFieldKey.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(Mp4NotMetaFieldKey.UDTA.getFieldName())) {
udtaNode = newAtom;
}
//only interested in metaNode that is child of udta node
else if (boxHeader.getId().equals(Mp4NotMetaFieldKey.META.getFieldName()) && parentBoxHeader.getId().equals(Mp4NotMetaFieldKey.UDTA.getFieldName())) {
metaNode = newAtom;
} else if (boxHeader.getId().equals(Mp4NotMetaFieldKey.HDLR.getFieldName()) && parentBoxHeader.getId().equals(Mp4NotMetaFieldKey.META.getFieldName())) {
hdlrWithinMetaNode = newAtom;
} else if (boxHeader.getId().equals(Mp4NotMetaFieldKey.HDLR.getFieldName())) {
hdlrWithinMdiaNode = newAtom;
} else if (boxHeader.getId().equals(Mp4NotMetaFieldKey.TAGS.getFieldName())) {
tagsNode = newAtom;
} else if (boxHeader.getId().equals(Mp4NotMetaFieldKey.STCO.getFieldName())) {
if (stco == null) {
stco = new Mp4StcoBox(boxHeader, moovBuffer);
stcoNode = newAtom;
}
} else if (boxHeader.getId().equals(Mp4NotMetaFieldKey.ILST.getFieldName())) {
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) parentNode.getParent();
if (parent != null) {
Mp4BoxHeader parentsParent = (Mp4BoxHeader) (parent).getUserObject();
if (parentsParent != null) {
if (parentBoxHeader.getId().equals(Mp4NotMetaFieldKey.META.getFieldName()) && parentsParent.getId().equals(Mp4NotMetaFieldKey.UDTA.getFieldName())) {
ilstNode = newAtom;
}
}
}
} else if (boxHeader.getId().equals(Mp4NotMetaFieldKey.FREE.getFieldName())) {
//Might be multiple in different locations
freeNodes.add(newAtom);
} else if (boxHeader.getId().equals(Mp4NotMetaFieldKey.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(Mp4NotMetaFieldKey.TRAK.getFieldName())) ||
(boxHeader.getId().equals(Mp4NotMetaFieldKey.MDIA.getFieldName())) ||
(boxHeader.getId().equals(Mp4NotMetaFieldKey.MINF.getFieldName())) ||
(boxHeader.getId().equals(Mp4NotMetaFieldKey.STBL.getFieldName())) ||
(boxHeader.getId().equals(Mp4NotMetaFieldKey.UDTA.getFieldName())) ||
(boxHeader.getId().equals(Mp4NotMetaFieldKey.META.getFieldName())) ||
(boxHeader.getId().equals(Mp4NotMetaFieldKey.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;
}
}