Package org.jaudiotagger.audio.mp4

Source Code of org.jaudiotagger.audio.mp4.Mp4TagWriter

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

Related Classes of org.jaudiotagger.audio.mp4.Mp4TagWriter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.