All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jcodec.movtool.InplaceMP4Editor Maven / Gradle / Ivy

There is a newer version: 0.2.5
Show newest version
package org.jcodec.movtool;
import java.lang.IllegalStateException;
import java.lang.System;


import org.jcodec.common.Assert;
import org.jcodec.common.Tuple;
import org.jcodec.common.Tuple._2;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.containers.mp4.BoxFactory;
import org.jcodec.containers.mp4.BoxUtil;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.MP4Util.Atom;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.Header;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.MovieFragmentBox;

import java.io.File;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * This class is part of JCodec ( www.jcodec.org ) This software is distributed
 * under FreeBSD License
 * 
 * Parses MP4 header and allows custom MP4Editor to modify it, then tries to put
 * the resulting header into the same place relatively to a file.
 * 
 * This might not work out, for example if the resulting header is bigger then
 * the original.
 * 
 * Use this class to make blazing fast changes to MP4 files when you know your
 * are not adding anything new to the header, perhaps only patching some values
 * or removing stuff from the header.
 * 
 * @author The JCodec project
 * 
 */
public class InplaceMP4Editor {

    /**
     * Tries to modify movie header in place according to what's implemented in
     * the edit, the file gets pysically modified if the operation is
     * successful. No temporary file is created.
     * 
     * @param file
     *            A file to be modified
     * @param edit
     *            An edit to be carried out on a movie header
     * @return Whether or not edit was successful, i.e. was there enough place
     *         to put the new header
     * @throws IOException
     * @throws Exception
     */
    public boolean modify(File file, MP4Edit edit) throws IOException {
        SeekableByteChannel fi = null;
        try {
            fi = NIOUtils.rwChannel(file);

            List> fragments = doTheFix(fi, edit);
            if (fragments == null)
                return false;

            // If everything is clean, only then actually writing stuff to the
            // file
            for (Tuple._2 fragment : fragments) {
                replaceBox(fi, fragment.v0, fragment.v1);
            }

            return true;
        } finally {
            NIOUtils.closeQuietly(fi);
        }
    }

    /**
     * Tries to modify movie header in place according to what's implemented in
     * the edit. Copies modified contents to a new file.
     * 
     * Note: The header is still edited in-place, so the new file will have
     * all-the-same sample offsets.
     * 
     * Note: Still subject to the same limitations as 'modify', i.e. the new
     * header must 'fit' into an old place.
     * 
     * This method is useful when you can't write to the original file, for ex.
     * you don't have permission.
     * 
     * @param src
     *            An original file
     * @param dst
     *            A file to store the modified copy
     * @param edit
     *            An edit logic to apply
     * @return
     * @throws IOException
     */
    public boolean copy(File src, File dst, MP4Edit edit) throws IOException {
        SeekableByteChannel fi = null;
        SeekableByteChannel fo = null;
        try {
            fi = NIOUtils.readableChannel(src);
            fo = NIOUtils.writableChannel(dst);

            List> fragments = doTheFix(fi, edit);
            if (fragments == null)
                return false;

            List<_2> fragOffsets = Tuple._2map0(fragments, new Tuple.Mapper() {
                public Long map(Atom t) {
                    return t.getOffset();
                }
            });

            // If everything is clean, only then actually start writing file
            Map rewrite = Tuple.asMap(fragOffsets);
            for (Atom atom : MP4Util.getRootAtoms(fi)) {
                ByteBuffer byteBuffer = rewrite.get(atom.getOffset());
                if (byteBuffer != null)
                    fo.write(byteBuffer);
                else
                    atom.copy(fi, fo);
            }

            return true;
        } finally {
            NIOUtils.closeQuietly(fi);
            NIOUtils.closeQuietly(fo);
        }
    }

    /**
     * Tries to modify movie header in place according to what's implemented in
     * the edit. Copies modified contents to a new file with the same name
     * erasing the original file if successful.
     * 
     * This is a shortcut for 'copy' when you want the new file to have the same
     * name but for some reason can not modify the original file in place. Maybe
     * modifications of files are expensive or not supported on your filesystem.
     * 
     * @param src
     *            A source and destination file
     * @param edit
     *            An edit to be applied
     * @return
     * @throws IOException
     */
    public boolean replace(File src, MP4Edit edit) throws IOException {
        File tmp = new File(src.getParentFile(), "." + src.getName());
        if (copy(src, tmp, edit)) {
            tmp.renameTo(src);
            return true;
        }
        return false;
    }

    private List> doTheFix(SeekableByteChannel fi, MP4Edit edit) throws IOException {
        Atom moovAtom = getMoov(fi);
        Assert.assertNotNull(moovAtom);

        ByteBuffer moovBuffer = fetchBox(fi, moovAtom);
        MovieBox moovBox = (MovieBox) parseBox(moovBuffer);

        List> fragments = new LinkedList>();
        if (BoxUtil.containsBox(moovBox, "mvex")) {
            List> temp = new LinkedList>();
            for (Atom fragAtom : getFragments(fi)) {
                ByteBuffer fragBuffer = fetchBox(fi, fragAtom);
                fragments.add(Tuple._2(fragAtom, fragBuffer));
                MovieFragmentBox fragBox = (MovieFragmentBox) parseBox(fragBuffer);
                fragBox.setMovie(moovBox);
                temp.add(Tuple._2(fragBuffer, fragBox));
            }

            edit.applyToFragment(moovBox, Tuple._2_project1(temp).toArray(new MovieFragmentBox[0]));

            for (Tuple._2 frag : temp) {
                if (!rewriteBox(frag.v0, frag.v1))
                    return null;
            }
        } else
            edit.apply(moovBox);

        if (!rewriteBox(moovBuffer, moovBox))
            return null;
        fragments.add(Tuple._2(moovAtom, moovBuffer));
        return fragments;
    }

    private void replaceBox(SeekableByteChannel fi, Atom atom, ByteBuffer buffer) throws IOException {
        fi.setPosition(atom.getOffset());
        fi.write(buffer);
    }

    private boolean rewriteBox(ByteBuffer buffer, Box box) {
        try {
            buffer.clear();
            box.write(buffer);
            if (buffer.hasRemaining()) {
                if (buffer.remaining() < 8)
                    return false;
                buffer.putInt(buffer.remaining());
                buffer.put(new byte[] { 'f', 'r', 'e', 'e' });
            }
            buffer.flip();
            return true;
        } catch (BufferOverflowException e) {
            return false;
        }
    }

    private ByteBuffer fetchBox(SeekableByteChannel fi, Atom moov) throws IOException {
        fi.setPosition(moov.getOffset());
        ByteBuffer oldMov = NIOUtils.fetchFromChannel(fi, (int) moov.getHeader().getSize());
        return oldMov;
    }

    private Box parseBox(ByteBuffer oldMov) {
        Header header = Header.read(oldMov);
        Box box = BoxUtil.parseBox(oldMov, header, BoxFactory.getDefault());
        return box;
    }

    private Atom getMoov(SeekableByteChannel f) throws IOException {
        for (Atom atom : MP4Util.getRootAtoms(f)) {
            if ("moov".equals(atom.getHeader().getFourcc())) {
                return atom;
            }
        }
        return null;
    }

    private List getFragments(SeekableByteChannel f) throws IOException {
        List result = new LinkedList();
        for (Atom atom : MP4Util.getRootAtoms(f)) {
            if ("moof".equals(atom.getHeader().getFourcc())) {
                result.add(atom);
            }
        }
        return result;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy