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<Tuple._2<Atom, ByteBuffer>> fragments = doTheFix(fi, edit);
if (fragments == null)
return false;
// If everything is clean, only then actually writing stuff to the
// file
for (Tuple._2<Atom, ByteBuffer> 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<Tuple._2<Atom, ByteBuffer>> fragments = doTheFix(fi, edit);
if (fragments == null)
return false;
List<_2<Long, ByteBuffer>> fragOffsets = Tuple._2map0(fragments, new Tuple.Mapper<Atom, Long>() {
public Long map(Atom t) {
return t.getOffset();
}
});
// If everything is clean, only then actually start writing file
Map<Long, ByteBuffer> 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<Tuple._2<Atom, ByteBuffer>> 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<Tuple._2<Atom, ByteBuffer>> fragments = new LinkedList<Tuple._2<Atom, ByteBuffer>>();
if (BoxUtil.containsBox(moovBox, "mvex")) {
List<Tuple._2<ByteBuffer, MovieFragmentBox>> temp = new LinkedList<Tuple._2<ByteBuffer, MovieFragmentBox>>();
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<ByteBuffer, ? extends Box> 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<Atom> getFragments(SeekableByteChannel f) throws IOException {
List<Atom> result = new LinkedList<Atom>();
for (Atom atom : MP4Util.getRootAtoms(f)) {
if ("moof".equals(atom.getHeader().getFourcc())) {
result.add(atom);
}
}
return result;
}
}