/* * 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; import org.jaudiotagger.audio.exceptions.CannotReadException; import org.jaudiotagger.audio.exceptions.CannotWriteException; import org.jaudiotagger.audio.mp4.atom.*; import org.jaudiotagger.logging.ErrorMessage; import org.jaudiotagger.tag.Tag; import org.jaudiotagger.tag.mp4.Mp4Tag; import org.jaudiotagger.tag.mp4.Mp4TagCreator; import org.jaudiotagger.utils.tree.DefaultMutableTreeNode; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.logging.Logger; /** * Writes metadata from mp4, the metadata tags are held under the ilst atom as shown below, (note all free atoms are * optional). * <p/> * <p/> * When writing changes the size of all the atoms upto ilst has to be recalculated, then if the size of * the metadata is increased the size of the free atom (below meta) should be reduced accordingly or vice versa. * If the size of the metadata has increased by more than the size of the free atom then the size of meta, udta * and moov should be recalculated and the top level free atom reduced accordingly * If there is not enough space even if using both of the free atoms, then the mdat atom has to be shifted down * accordingly to make space, and the stco atom has to have its offsets to mdat chunks table adjusted accordingly. * <p/> * Exceptions are that the meta/udta/ilst do not currently exist, in which udta/meta/ilst are created. Note it is valid * to have meta/ilst without udta but this is less common so we always try to write files according to the Apple/iTunes * specification. * * <p/> * <p/> * <pre> * |--- ftyp * |--- free * |--- moov * |......| * |......|----- mvdh * |......|----- trak * |......|----- udta * |..............| * |..............|-- meta * |....................| * |....................|-- hdlr * |....................|-- ilst * |....................|.. ..| * |....................|.....|---- @nam (Optional for each metadatafield) * |....................|.....|.......|-- data * |....................|.....|....... ecetera * |....................|.....|---- ---- (Optional for reverse dns field) * |....................|.............|-- mean * |....................|.............|-- name * |....................|.............|-- data * |....................|................ ecetere * |....................|-- free * |--- free * |--- mdat * </pre> */ public class Mp4TagWriter { // Logger Object public static Logger logger = Logger.getLogger("org.jaudiotagger.tag.mp4"); private Mp4TagCreator tc = new Mp4TagCreator(); /** * Replace the ilst metadata * <p/> * Because it is the same size as the original data nothing else has to be modified * * @param rawIlstData * @param oldIlstSize * @param startIstWithinFile * @param fileReadChannel * @param fileWriteChannel * @throws CannotWriteException * @throws IOException */ private void writeMetadataSameSize(ByteBuffer rawIlstData, long oldIlstSize, long startIstWithinFile, FileChannel fileReadChannel, FileChannel fileWriteChannel, Mp4BoxHeader tagsHeader) throws CannotWriteException, IOException { fileReadChannel.position(0); fileWriteChannel.transferFrom(fileReadChannel, 0, startIstWithinFile); fileWriteChannel.position(startIstWithinFile); fileWriteChannel.write(rawIlstData); fileReadChannel.position(startIstWithinFile + oldIlstSize); writeDataAfterIlst(fileReadChannel, fileWriteChannel, tagsHeader); } /** * If the existing files contains a tags atom and chp1 atom underneath the meta atom that means the file was * encoded by Nero. Applications such as foobar read this non-standard tag before the more usual data within * ilst causing problems. So the solution is to convert the tags atom and its children into a free atom whilst * leaving the chp1 atom alone. * * @param fileReadChannel * @param fileWriteChannel * @param tagsHeader * @throws IOException */ private void writeNeroData(FileChannel fileReadChannel, FileChannel fileWriteChannel, Mp4BoxHeader tagsHeader) throws IOException { //Write from after ilst upto tags atom long writeBetweenIlstAndTags = tagsHeader.getFilePos() - fileReadChannel.position(); fileWriteChannel.transferFrom(fileReadChannel, fileWriteChannel.position(), writeBetweenIlstAndTags); fileWriteChannel.position(fileWriteChannel.position() + writeBetweenIlstAndTags); //Replace tags atom (and children) by a free atom convertandWriteTagsAtomToFreeAtom(fileWriteChannel, tagsHeader); //Write after tags atom fileReadChannel.position(tagsHeader.getFilePos() + tagsHeader.getLength()); fileWriteChannel.transferFrom(fileReadChannel, fileWriteChannel.position(), fileReadChannel.size() - fileReadChannel.position()); } /** * When the size of the metadata has changed and it cant be compensated for by free atom * we have to adjust the size of the size field upto the moovheader level for the udta atom and * its child meta atom. * * @param moovHeader * @param moovBuffer * @param sizeAdjustment can be negative or positive * * @param udtaHeader * @param metaHeader * @return * @throws java.io.IOException */ private void adjustSizeOfMoovHeader (Mp4BoxHeader moovHeader, ByteBuffer moovBuffer, int sizeAdjustment, Mp4BoxHeader udtaHeader, Mp4BoxHeader metaHeader) throws IOException { //Adjust moov header size, adjusts the underlying buffer moovHeader.setLength(moovHeader.getLength() + sizeAdjustment); //Edit the fields in moovBuffer (note moovbuffer doesnt include header) if (udtaHeader != null) { //Write the updated udta atom header to moov buffer udtaHeader.setLength(udtaHeader.getLength() + sizeAdjustment); moovBuffer.position((int) (udtaHeader.getFilePos() - moovHeader.getFilePos() - Mp4BoxHeader.HEADER_LENGTH)); moovBuffer.put(udtaHeader.getHeaderData()); } if (metaHeader != null) { //Write the updated udta atom header to moov buffer metaHeader.setLength(metaHeader.getLength() + sizeAdjustment); moovBuffer.position((int) (metaHeader.getFilePos() - moovHeader.getFilePos() - Mp4BoxHeader.HEADER_LENGTH)); moovBuffer.put(metaHeader.getHeaderData()); } } private void createMetadataAtoms (Mp4BoxHeader moovHeader, ByteBuffer moovBuffer, int sizeAdjustment, Mp4BoxHeader udtaHeader, Mp4BoxHeader metaHeader) throws IOException { //Adjust moov header size moovHeader.setLength(moovHeader.getLength() + sizeAdjustment); } /** * Write tag to rafTemp file * * @param tag tag data * @param raf current file * @param rafTemp temporary file for writing * @throws CannotWriteException * @throws IOException */ public void write(Tag tag, RandomAccessFile raf, RandomAccessFile rafTemp) throws CannotWriteException, IOException { //logger.info("Started writing tag data"); //Read Channel for reading from old file FileChannel fileReadChannel = raf.getChannel(); //Write channel for writing to new file FileChannel fileWriteChannel = rafTemp.getChannel(); //TODO we shouldn't need all these variables, and some are very badly named - used by new and old methods int oldIlstSize = 0; int relativeIlstposition; int startIlstWithinFile; int newIlstSize; int oldMetaLevelFreeAtomSize; int topLevelFreePosition; int topLevelFreeSize; long endOfMoov = 0; //Found top level free atom that comes after moov and before mdat, (also true if no free atom ?) boolean topLevelFreeAtomComesBeforeMdatAtomAndAfterMetadata; //Found top level free atom that comes between ftyp and moov boolean topLevelFreeAtomComesBeforeMdatAndMetadata; Mp4BoxHeader topLevelFreeHeader; Mp4AtomTree atomTree; //Build AtomTree try { atomTree = new Mp4AtomTree(raf, false); } catch (CannotReadException cre) { throw new CannotWriteException(cre.getMessage()); } Mp4BoxHeader mdatHeader = atomTree.getBoxHeader(atomTree.getMdatNode()); //Unable to find audio so no chance of saving any changes if (mdatHeader == null) { throw new CannotWriteException(ErrorMessage.MP4_CHANGES_TO_FILE_FAILED_CANNOT_FIND_AUDIO.getMsg()); } //Go through every field constructing the data that will appear starting from ilst box ByteBuffer rawIlstData = tc.convert(tag); rawIlstData.rewind(); newIlstSize = rawIlstData.limit(); //Moov Box header Mp4BoxHeader moovHeader = atomTree.getBoxHeader(atomTree.getMoovNode()); long positionWithinFileAfterFindingMoovHeader = moovHeader.getFilePos() + Mp4BoxHeader.HEADER_LENGTH; endOfMoov = moovHeader.getFilePos() + moovHeader.getLength(); Mp4StcoBox stco = atomTree.getStco(); Mp4BoxHeader ilstHeader = atomTree.getBoxHeader(atomTree.getIlstNode()); Mp4BoxHeader udtaHeader = atomTree.getBoxHeader(atomTree.getUdtaNode()); Mp4BoxHeader metaHeader = atomTree.getBoxHeader(atomTree.getMetaNode()); Mp4BoxHeader hdlrMetaHeader = atomTree.getBoxHeader(atomTree.getHdlrWithinMetaNode()); Mp4BoxHeader tagsHeader = atomTree.getBoxHeader(atomTree.getTagsNode()); Mp4BoxHeader trakHeader = atomTree.getBoxHeader(atomTree.getTrakNodes().get(0)); ByteBuffer moovBuffer = atomTree.getMoovBuffer(); //Work out if we/what kind of metadata hierachy we currently have in the file //Udta if (udtaHeader != null) { //Meta if (metaHeader != null) { //ilst - record where ilst is,and where it ends if (ilstHeader != null) { oldIlstSize = ilstHeader.getLength(); //Relative means relative to moov buffer after moov header startIlstWithinFile = (int) ilstHeader.getFilePos(); relativeIlstposition = (int) (startIlstWithinFile - (moovHeader.getFilePos() + Mp4BoxHeader.HEADER_LENGTH)); } else { //Place ilst immediately after existing hdlr atom if (hdlrMetaHeader != null) { startIlstWithinFile = (int) hdlrMetaHeader.getFilePos() + hdlrMetaHeader.getLength(); relativeIlstposition = (int) (startIlstWithinFile - (moovHeader.getFilePos() + Mp4BoxHeader.HEADER_LENGTH)); } //Place ilst after data fields in meta atom //TODO Should we create a hdlr atom else { startIlstWithinFile = (int) metaHeader.getFilePos() + Mp4BoxHeader.HEADER_LENGTH + Mp4MetaBox.FLAGS_LENGTH; relativeIlstposition = (int) ((startIlstWithinFile) - (moovHeader.getFilePos() + Mp4BoxHeader.HEADER_LENGTH)); } } } else { //There no ilst or meta header so we set to position where it would be if it existed relativeIlstposition = moovHeader.getLength() - Mp4BoxHeader.HEADER_LENGTH; startIlstWithinFile = (int) (moovHeader.getFilePos() + moovHeader.getLength()); } } //There no udta header so we are going to create a new structure, but we have to be aware that there might be //an existing meta box structure in which case we preserve it but with our new structure before it. else { //Create new structure just after the end of the trak atom if (metaHeader != null) { startIlstWithinFile = (int) trakHeader.getFilePos() + trakHeader.getLength(); relativeIlstposition = (int) (startIlstWithinFile - (moovHeader.getFilePos() + Mp4BoxHeader.HEADER_LENGTH)); } else { //There no udta,ilst or meta header so we set to position where it would be if it existed relativeIlstposition = moovHeader.getLength() - Mp4BoxHeader.HEADER_LENGTH; startIlstWithinFile = (int) (moovHeader.getFilePos() + moovHeader.getLength()); } } //Find size of Level-4 Free atom (if any) immediately after ilst atom oldMetaLevelFreeAtomSize = getMetaLevelFreeAtomSize(atomTree); //Level-1 free atom topLevelFreePosition = 0; topLevelFreeSize = 0; topLevelFreeAtomComesBeforeMdatAtomAndAfterMetadata = true; topLevelFreeAtomComesBeforeMdatAndMetadata = false; for (DefaultMutableTreeNode freeNode : atomTree.getFreeNodes()) { DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) freeNode.getParent(); if (parentNode.isRoot()) { topLevelFreeHeader = ((Mp4BoxHeader) freeNode.getUserObject()); topLevelFreeSize = topLevelFreeHeader.getLength(); topLevelFreePosition = (int) topLevelFreeHeader.getFilePos(); break; } } if (topLevelFreeSize > 0) { if (topLevelFreePosition > mdatHeader.getFilePos()) { topLevelFreeAtomComesBeforeMdatAtomAndAfterMetadata = false; } else if (topLevelFreePosition < moovHeader.getFilePos()) { topLevelFreeAtomComesBeforeMdatAtomAndAfterMetadata = false; topLevelFreeAtomComesBeforeMdatAndMetadata = true; } } else { topLevelFreePosition = (int) mdatHeader.getFilePos(); } //logger.info("Read header successfully ready for writing"); //The easiest option since no difference in the size of the metadata so all we have to do is //create a new file identical to first file but with replaced metadata if (oldIlstSize == newIlstSize) { //logger.info("Writing:Option 1:Same Size"); writeMetadataSameSize(rawIlstData, oldIlstSize, startIlstWithinFile, fileReadChannel, fileWriteChannel, tagsHeader); } //.. we just need to increase the size of the free atom below the meta atom, and replace the metadata //no other changes necessary and total file size remains the same else if (oldIlstSize > newIlstSize) { //Create an amended freeBaos atom and write it if it previously existed as a free atom immediately //after ilst as a child of meta if (oldMetaLevelFreeAtomSize > 0) { //logger.info("Writing:Option 2:Smaller Size have free atom:" + oldIlstSize + ":" + newIlstSize); writeDataUptoIncludingIlst(fileReadChannel, fileWriteChannel, oldIlstSize, startIlstWithinFile, rawIlstData); //Write the modified free atom that comes after ilst int newFreeSize = oldMetaLevelFreeAtomSize + (oldIlstSize - newIlstSize); Mp4FreeBox newFreeBox = new Mp4FreeBox(newFreeSize - Mp4BoxHeader.HEADER_LENGTH); fileWriteChannel.write(newFreeBox.getHeader().getHeaderData()); fileWriteChannel.write(newFreeBox.getData()); //Skip over the read channel old free atom fileReadChannel.position(fileReadChannel.position() + oldMetaLevelFreeAtomSize); writeDataAfterIlst(fileReadChannel, fileWriteChannel, tagsHeader); } //No free atom we need to create a new one or adjust top level free atom else { int newFreeSize = (oldIlstSize - newIlstSize) - Mp4BoxHeader.HEADER_LENGTH; //We need to create a new one, so dont have to adjust all the headers but only works if the size //of tags has decreased by more 8 characters so there is enough room for the free boxes header we take //into account size of new header in calculating size of box if (newFreeSize > 0) { //logger.info("Writing:Option 3:Smaller Size can create free atom"); writeDataUptoIncludingIlst(fileReadChannel, fileWriteChannel, oldIlstSize, startIlstWithinFile, rawIlstData); //Create new free box Mp4FreeBox newFreeBox = new Mp4FreeBox(newFreeSize); fileWriteChannel.write(newFreeBox.getHeader().getHeaderData()); fileWriteChannel.write(newFreeBox.getData()); writeDataAfterIlst(fileReadChannel, fileWriteChannel, tagsHeader); } //Ok everything in this bit of tree has to be recalculated because eight or less bytes smaller else { //logger.info("Writing:Option 4:Smaller Size <=8 cannot create free atoms"); //Size will be this amount smaller int sizeReducedBy = oldIlstSize - newIlstSize; //Write stuff before Moov (ftyp) fileReadChannel.position(0); fileWriteChannel.transferFrom(fileReadChannel, 0, moovHeader.getFilePos()); fileWriteChannel.position(moovHeader.getFilePos()); //Edit stco atom within moov header, we need to adjust offsets by the amount mdat is going to be shifted //unless mdat is at start of file if (mdatHeader.getFilePos() > moovHeader.getFilePos()) { stco.adjustOffsets(-sizeReducedBy); } //Edit and rewrite the Moov,Udta and Meta header in moov buffer adjustSizeOfMoovHeader(moovHeader, moovBuffer, -sizeReducedBy, udtaHeader, metaHeader); fileWriteChannel.write(moovHeader.getHeaderData()); moovBuffer.rewind(); moovBuffer.limit(relativeIlstposition); fileWriteChannel.write(moovBuffer); //Now write ilst data fileWriteChannel.write(rawIlstData); fileReadChannel.position(startIlstWithinFile + oldIlstSize); writeDataAfterIlst(fileReadChannel, fileWriteChannel, tagsHeader); } } } //Size of metadata has increased, the most complex situation, more atoms affected else { int additionalSpaceRequiredForMetadata = newIlstSize - oldIlstSize; //We can fit the metadata in under the meta item just by using some of the padding available in the free //atom under the meta atom need to take of the side of free header otherwise might end up with //solution where can fit in data, but cant fit in free atom header if (additionalSpaceRequiredForMetadata <= (oldMetaLevelFreeAtomSize - Mp4BoxHeader.HEADER_LENGTH)) { int newFreeSize = oldMetaLevelFreeAtomSize - (additionalSpaceRequiredForMetadata); //logger.info("Writing:Option 5;Larger Size can use meta free atom need extra:" + newFreeSize + "bytes"); writeDataUptoIncludingIlst(fileReadChannel, fileWriteChannel, oldIlstSize, startIlstWithinFile, rawIlstData); //Create an amended smaller freeBaos atom and write it to file Mp4FreeBox newFreeBox = new Mp4FreeBox(newFreeSize - Mp4BoxHeader.HEADER_LENGTH); fileWriteChannel.write(newFreeBox.getHeader().getHeaderData()); fileWriteChannel.write(newFreeBox.getData()); //Skip over the read channel old free atom fileReadChannel.position(fileReadChannel.position() + oldMetaLevelFreeAtomSize); writeDataAfterIlst(fileReadChannel, fileWriteChannel, tagsHeader); } //There is not enough padding in the metadata free atom anyway //Size meta needs to be increased by (if not writing a free atom) //Special Case this could actually be negative (upto -8)if is actually enough space but would //not be able to write free atom properly, it doesnt matter the parent atoms would still //need their sizes adjusted. else { int additionalMetaSizeThatWontFitWithinMetaAtom = additionalSpaceRequiredForMetadata - (oldMetaLevelFreeAtomSize); //Write stuff before Moov (ftyp) fileReadChannel.position(0); fileWriteChannel.transferFrom(fileReadChannel, 0, positionWithinFileAfterFindingMoovHeader - Mp4BoxHeader.HEADER_LENGTH); fileWriteChannel.position(positionWithinFileAfterFindingMoovHeader - Mp4BoxHeader.HEADER_LENGTH); if (udtaHeader == null) { //logger.info("Writing:Option 5.1;No udta atom"); Mp4HdlrBox hdlrBox = Mp4HdlrBox.createiTunesStyleHdlrBox(); Mp4MetaBox metaBox = Mp4MetaBox.createiTunesStyleMetaBox(hdlrBox.getHeader().getLength() + rawIlstData.limit()); udtaHeader = new Mp4BoxHeader(Mp4NotMetaFieldKey.UDTA.getFieldName()); udtaHeader.setLength(Mp4BoxHeader.HEADER_LENGTH + metaBox.getHeader().getLength()); additionalMetaSizeThatWontFitWithinMetaAtom = additionalMetaSizeThatWontFitWithinMetaAtom + (udtaHeader.getLength() - rawIlstData.limit()); //Edit stco atom within moov header, if the free atom comes after mdat OR //(there is not enough space in the top level free atom //or special case of matching exactly the free atom plus header) if ((!topLevelFreeAtomComesBeforeMdatAtomAndAfterMetadata) || ((topLevelFreeSize - Mp4BoxHeader.HEADER_LENGTH < additionalMetaSizeThatWontFitWithinMetaAtom) && (topLevelFreeSize != additionalMetaSizeThatWontFitWithinMetaAtom))) { //We dont bother using the top level free atom coz not big enough anyway, we need to adjust offsets //by the amount mdat is going to be shifted if (mdatHeader.getFilePos() > moovHeader.getFilePos()) { stco.adjustOffsets(additionalMetaSizeThatWontFitWithinMetaAtom); } } //Edit and rewrite the Moov header moovHeader.setLength(moovHeader.getLength() + additionalMetaSizeThatWontFitWithinMetaAtom); //Adjust moov header size to allow a udta,meta and hdlr atom, we have already accounted for ilst data //moovHeader.setLength(moovHeader.getLength() + udtaHeader.getLength() - rawIlstData.limit()); fileWriteChannel.write(moovHeader.getHeaderData()); moovBuffer.rewind(); moovBuffer.limit(relativeIlstposition); fileWriteChannel.write(moovBuffer); //Write new atoms required for holding metadata in itunes format fileWriteChannel.write(udtaHeader.getHeaderData()); fileWriteChannel.write(metaBox.getHeader().getHeaderData()); fileWriteChannel.write(metaBox.getData()); fileWriteChannel.write(hdlrBox.getHeader().getHeaderData()); fileWriteChannel.write(hdlrBox.getData()); } else { //logger.info("Writing:Option 5.2;udta atom exists"); //Edit stco atom within moov header, if the free atom comes after mdat OR //(there is not enough space in the top level free atom //or special case of matching exactly the free atom plus header) if ((!topLevelFreeAtomComesBeforeMdatAtomAndAfterMetadata) || ((topLevelFreeSize - Mp4BoxHeader.HEADER_LENGTH < additionalMetaSizeThatWontFitWithinMetaAtom) && (topLevelFreeSize != additionalMetaSizeThatWontFitWithinMetaAtom))) { //We dont bother using the top level free atom coz not big enough anyway, we need to adjust offsets //by the amount mdat is going to be shifted if (mdatHeader.getFilePos() > moovHeader.getFilePos()) { stco.adjustOffsets(additionalMetaSizeThatWontFitWithinMetaAtom); } } //Edit and rewrite the Moov header adjustSizeOfMoovHeader(moovHeader, moovBuffer, additionalMetaSizeThatWontFitWithinMetaAtom, udtaHeader, metaHeader); fileWriteChannel.write(moovHeader.getHeaderData()); //Now write from this edited buffer up until ilst atom moovBuffer.rewind(); moovBuffer.limit(relativeIlstposition); fileWriteChannel.write(moovBuffer); } //Now write ilst data fileWriteChannel.write(rawIlstData); //Skip over the read channel old meta level free atom because now used up fileReadChannel.position(startIlstWithinFile + oldIlstSize); fileReadChannel.position(fileReadChannel.position() + oldMetaLevelFreeAtomSize); if (tagsHeader != null) { //Write from after ilst upto tags atom long writeBetweenIlstAndTags = tagsHeader.getFilePos() - fileReadChannel.position(); fileWriteChannel.transferFrom(fileReadChannel, fileWriteChannel.position(), writeBetweenIlstAndTags); fileWriteChannel.position(fileWriteChannel.position() + writeBetweenIlstAndTags); convertandWriteTagsAtomToFreeAtom(fileWriteChannel, tagsHeader); //Write after tags atom upto end of moov fileReadChannel.position(tagsHeader.getFilePos() + tagsHeader.getLength()); long extraData = endOfMoov - fileReadChannel.position(); fileWriteChannel.transferFrom(fileReadChannel, fileWriteChannel.position(), extraData); } else { //Now write the rest of children under moov which wont have changed long extraData = endOfMoov - fileReadChannel.position(); fileWriteChannel.transferFrom(fileReadChannel, fileWriteChannel.position(), extraData); fileWriteChannel.position(fileWriteChannel.position() + extraData); } //If we have top level free atom that comes before mdat we might be able to use it but only if //the free atom actually come after the the metadata if (topLevelFreeAtomComesBeforeMdatAtomAndAfterMetadata && (topLevelFreePosition > startIlstWithinFile)) { //If the shift is less than the space available in this second free atom data size we should //minimize the free atom accordingly (then we don't have to update stco atom) //note could be a double negative as additionalMetaSizeThatWontFitWithinMetaAtom could be -1 to -8 but thats ok stills works //ok if (topLevelFreeSize - Mp4BoxHeader.HEADER_LENGTH >= additionalMetaSizeThatWontFitWithinMetaAtom) { //logger.info("Writing:Option 6;Larger Size can use top free atom"); Mp4FreeBox freeBox = new Mp4FreeBox((topLevelFreeSize - Mp4BoxHeader.HEADER_LENGTH) - additionalMetaSizeThatWontFitWithinMetaAtom); fileWriteChannel.write(freeBox.getHeader().getHeaderData()); fileWriteChannel.write(freeBox.getData()); //Skip over the read channel old free atom fileReadChannel.position(fileReadChannel.position() + topLevelFreeSize); //Write Mdat fileWriteChannel.transferFrom(fileReadChannel, fileWriteChannel.position(), fileReadChannel.size() - fileReadChannel.position()); } //If the space required is identical to total size of the free space (inc header) //we could just remove the header else if (topLevelFreeSize == additionalMetaSizeThatWontFitWithinMetaAtom) { //logger.info("Writing:Option 7;Larger Size uses top free atom including header"); //Skip over the read channel old free atom fileReadChannel.position(fileReadChannel.position() + topLevelFreeSize); //Write Mdat fileWriteChannel.transferFrom(fileReadChannel, fileWriteChannel.position(), fileReadChannel.size() - fileReadChannel.position()); } //Mdat is going to have to move anyway, so keep free atom as is and write it and mdat //(have already updated stco above) else { //logger.info("Writing:Option 8;Larger Size cannot use top free atom"); fileWriteChannel.transferFrom(fileReadChannel, fileWriteChannel.position(), fileReadChannel.size() - fileReadChannel.position()); } } else { //logger.info("Writing:Option 9;Top Level Free comes after Mdat or before Metadata so cant use it"); fileWriteChannel.transferFrom(fileReadChannel, fileWriteChannel.position(), fileReadChannel.size() - fileReadChannel.position()); } } } //Close all channels to original file fileReadChannel.close(); raf.close(); checkFileWrittenCorrectly(rafTemp, mdatHeader, fileWriteChannel, stco); } /** * Replace tags atom (and children) by a free atom * * @param fileWriteChannel * @param tagsHeader * @throws IOException */ private void convertandWriteTagsAtomToFreeAtom(FileChannel fileWriteChannel, Mp4BoxHeader tagsHeader) throws IOException { Mp4FreeBox freeBox = new Mp4FreeBox(tagsHeader.getDataLength()); fileWriteChannel.write(freeBox.getHeader().getHeaderData()); fileWriteChannel.write(freeBox.getData()); } /** * Write the data including new ilst * <p>can be used as long as we dont have to adjust the size of moov header * * @param fileReadChannel * @param fileWriteChannel * @param oldIlstSize * @param startIlstWithinFile * @param rawIlstData * @throws IOException */ private void writeDataUptoIncludingIlst(FileChannel fileReadChannel, FileChannel fileWriteChannel, int oldIlstSize, int startIlstWithinFile, ByteBuffer rawIlstData) throws IOException { fileReadChannel.position(0); fileWriteChannel.transferFrom(fileReadChannel, 0, startIlstWithinFile); fileWriteChannel.position(startIlstWithinFile); fileWriteChannel.write(rawIlstData); fileReadChannel.position(startIlstWithinFile + oldIlstSize); } /** * Write data after ilst upto the end of the file * <p/> * <p>Can be used if dont need to adjust size of moov header of modify top level free atoms * * @param fileReadChannel * @param fileWriteChannel * @param tagsHeader * @throws IOException */ private void writeDataAfterIlst(FileChannel fileReadChannel, FileChannel fileWriteChannel, Mp4BoxHeader tagsHeader) throws IOException { if (tagsHeader != null) { //Write from after free upto tags atom writeNeroData(fileReadChannel, fileWriteChannel, tagsHeader); } else { //Now write the rest of the file which won't have changed fileWriteChannel.transferFrom(fileReadChannel, fileWriteChannel.position(), fileReadChannel.size() - fileReadChannel.position()); } } /** * Determine the size of the free atom immediately after ilst atom at the same level (if any), we can use this if * ilst needs to grow or shrink because of more less metadata * * @param atomTree * @return */ private int getMetaLevelFreeAtomSize(Mp4AtomTree atomTree) { int oldMetaLevelFreeAtomSize;//Level 4 - Free oldMetaLevelFreeAtomSize = 0; for (DefaultMutableTreeNode freeNode : atomTree.getFreeNodes()) { DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) freeNode.getParent(); DefaultMutableTreeNode brotherNode = freeNode.getPreviousSibling(); if (!parentNode.isRoot()) { Mp4BoxHeader parentHeader = ((Mp4BoxHeader) parentNode.getUserObject()); Mp4BoxHeader freeHeader = ((Mp4BoxHeader) freeNode.getUserObject()); //We are only interested in free atoms at this level if they come after the ilst node if (brotherNode != null) { Mp4BoxHeader brotherHeader = ((Mp4BoxHeader) brotherNode.getUserObject()); if (parentHeader.getId().equals(Mp4NotMetaFieldKey.META.getFieldName()) && brotherHeader.getId().equals(Mp4NotMetaFieldKey.ILST.getFieldName())) { oldMetaLevelFreeAtomSize = freeHeader.getLength(); break; } } } } return oldMetaLevelFreeAtomSize; } /** * Check File Written Correctly * * @param rafTemp * @param mdatHeader * @param fileWriteChannel * @param stco * @throws CannotWriteException * @throws IOException */ private void checkFileWrittenCorrectly(RandomAccessFile rafTemp, Mp4BoxHeader mdatHeader, FileChannel fileWriteChannel, Mp4StcoBox stco) throws CannotWriteException, IOException { //logger.info("Checking file has been written correctly"); try { //Create a tree from the new file Mp4AtomTree newAtomTree; newAtomTree = new Mp4AtomTree(rafTemp, false); //Check we still have audio data file, and check length Mp4BoxHeader newMdatHeader = newAtomTree.getBoxHeader(newAtomTree.getMdatNode()); if (newMdatHeader == null) { throw new CannotWriteException(ErrorMessage.MP4_CHANGES_TO_FILE_FAILED_NO_DATA.getMsg()); } if (newMdatHeader.getLength() != mdatHeader.getLength()) { throw new CannotWriteException(ErrorMessage.MP4_CHANGES_TO_FILE_FAILED_DATA_CORRUPT.getMsg()); } //Should always have udta atom after writing to file Mp4BoxHeader newUdtaHeader = newAtomTree.getBoxHeader(newAtomTree.getUdtaNode()); if (newUdtaHeader == null) { throw new CannotWriteException(ErrorMessage.MP4_CHANGES_TO_FILE_FAILED_NO_TAG_DATA.getMsg()); } //Should always have meta atom after writing to file Mp4BoxHeader newMetaHeader = newAtomTree.getBoxHeader(newAtomTree.getMetaNode()); if (newMetaHeader == null) { throw new CannotWriteException(ErrorMessage.MP4_CHANGES_TO_FILE_FAILED_NO_TAG_DATA.getMsg()); } //Check offsets are correct, may not match exactly in original file so just want to make //sure that the discrepancy if any is preserved Mp4StcoBox newStco = newAtomTree.getStco(); // // logger.finer("stco:Original First Offset" + stco.getFirstOffSet()); // logger.finer("stco:Original Diff" + (int) (stco.getFirstOffSet() - mdatHeader.getFilePos())); // logger.finer("stco:Original Mdat Pos" + mdatHeader.getFilePos()); // logger.finer("stco:New First Offset" + newStco.getFirstOffSet()); // logger.finer("stco:New Diff" + (int) ((newStco.getFirstOffSet() - newMdatHeader.getFilePos()))); // logger.finer("stco:New Mdat Pos" + newMdatHeader.getFilePos()); int diff = (int) (stco.getFirstOffSet() - mdatHeader.getFilePos()); if ((newStco.getFirstOffSet() - newMdatHeader.getFilePos()) != diff) { int discrepancy = (int) ((newStco.getFirstOffSet() - newMdatHeader.getFilePos()) - diff); throw new CannotWriteException(ErrorMessage.MP4_CHANGES_TO_FILE_FAILED_INCORRECT_OFFSETS.getMsg(discrepancy)); } } catch (Exception e) { if (e instanceof CannotWriteException) { throw (CannotWriteException) e; } else { e.printStackTrace(); throw new CannotWriteException(ErrorMessage.MP4_CHANGES_TO_FILE_FAILED.getMsg() + ":" + e.getMessage()); } } finally { //Close references to new file rafTemp.close(); fileWriteChannel.close(); } //logger.info("File has been written correctly"); } /** * Delete the tag * <p/> * <p/> * <p>This is achieved by writing an empty ilst atom * * @param raf * @param rafTemp * @throws IOException */ public void delete(RandomAccessFile raf, RandomAccessFile rafTemp) throws IOException { Mp4Tag tag = new Mp4Tag(); try { write(tag, raf, rafTemp); } catch (CannotWriteException cwe) { throw new IOException(cwe.getMessage()); } } }