package org.jcodec.movtool;
import static org.jcodec.common.NIOUtils.readableFileChannel;
import static org.jcodec.common.NIOUtils.writableFileChannel;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.jcodec.common.NIOUtils;
import org.jcodec.common.SeekableByteChannel;
import org.jcodec.containers.mp4.Chunk;
import org.jcodec.containers.mp4.ChunkReader;
import org.jcodec.containers.mp4.ChunkWriter;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.boxes.AliasBox;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.ChunkOffsetsBox;
import org.jcodec.containers.mp4.boxes.DataRefBox;
import org.jcodec.containers.mp4.boxes.FileTypeBox;
import org.jcodec.containers.mp4.boxes.Header;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
import org.jcodec.containers.mp4.boxes.UrlBox;
/**
* This class is part of JCodec ( www.jcodec.org ) This software is distributed
* under FreeBSD License
*
* Self contained movie creator
*
* @author The JCodec project
*
*/
public class Flattern {
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("Syntax: self <ref movie> <out movie>");
System.exit(-1);
}
File outFile = new File(args[1]);
outFile.delete();
SeekableByteChannel input = null;
try {
input = readableFileChannel(new File(args[0]));
MovieBox movie = MP4Util.parseMovie(input);
new Flattern().flattern(movie, outFile);
} finally {
if (input != null)
input.close();
}
}
public List<ProgressListener> listeners = new ArrayList<Flattern.ProgressListener>();
public interface ProgressListener {
public void trigger(int progress);
}
public void addProgressListener(ProgressListener listener) {
this.listeners.add(listener);
}
public void flattern(MovieBox movie, SeekableByteChannel out) throws IOException {
if (!movie.isPureRefMovie(movie))
throw new IllegalArgumentException("movie should be reference");
ByteBuffer buf = ByteBuffer.allocate(16 * 1024 * 1024);
FileTypeBox ftyp = new FileTypeBox("qt ", 0x20050300, Arrays.asList(new String[] { "qt " }));
ftyp.write(buf);
long movieOff = buf.position();
movie.write(buf);
int extraSpace = calcSpaceReq(movie);
new Header("free", 8 + extraSpace).write(buf);
NIOUtils.skip(buf, extraSpace);
long mdatOff = buf.position();
new Header("mdat", 0x100000001L).write(buf);
buf.flip();
out.write(buf);
SeekableByteChannel[][] inputs = getInputs(movie);
TrakBox[] tracks = movie.getTracks();
ChunkReader[] readers = new ChunkReader[tracks.length];
ChunkWriter[] writers = new ChunkWriter[tracks.length];
Chunk[] head = new Chunk[tracks.length];
int totalChunks = 0, writtenChunks = 0, lastProgress = 0;
long[] off = new long[tracks.length];
for (int i = 0; i < tracks.length; i++) {
readers[i] = new ChunkReader(tracks[i]);
totalChunks += readers[i].size();
writers[i] = new ChunkWriter(tracks[i], inputs[i], out);
head[i] = readers[i].next();
if (tracks[i].isVideo())
off[i] = 2 * movie.getTimescale();
}
while (true) {
int min = -1;
for (int i = 0; i < readers.length; i++) {
if (head[i] == null)
continue;
if (min == -1)
min = i;
else {
long iTv = movie.rescale(head[i].getStartTv(), tracks[i].getTimescale()) + off[i];
long minTv = movie.rescale(head[min].getStartTv(), tracks[min].getTimescale()) + off[min];
if (iTv < minTv)
min = i;
}
}
if (min == -1)
break;
writers[min].write(head[min]);
head[min] = readers[min].next();
writtenChunks++;
lastProgress = calcProgress(totalChunks, writtenChunks, lastProgress);
}
long mdatSize = out.position() - mdatOff;
for (int i = 0; i < tracks.length; i++) {
writers[i].apply();
}
out.position(movieOff);
MP4Util.writeMovie(out, movie);
long extra = mdatOff - out.position();
if (extra < 0)
throw new RuntimeException("Not enough space to write the header");
out.write((ByteBuffer) ByteBuffer.allocate(8).putInt((int) extra).put(new byte[] { 'f', 'r', 'e', 'e' }).flip());
out.position(mdatOff + 8);
out.write(ByteBuffer.allocate(8).putLong(mdatSize));
}
private int calcProgress(int totalChunks, int writtenChunks, int lastProgress) {
int curProgress = 100 * writtenChunks / totalChunks;
if (lastProgress < curProgress) {
lastProgress = curProgress;
for (ProgressListener pl : this.listeners)
pl.trigger(lastProgress);
}
return lastProgress;
}
protected SeekableByteChannel[][] getInputs(MovieBox movie) throws IOException {
TrakBox[] tracks = movie.getTracks();
SeekableByteChannel[][] result = new SeekableByteChannel[tracks.length][];
for (int i = 0; i < tracks.length; i++) {
DataRefBox drefs = NodeBox.findFirst(tracks[i], DataRefBox.class, "mdia", "minf", "dinf", "dref");
if (drefs == null) {
throw new RuntimeException("No data references");
}
List<Box> entries = drefs.getBoxes();
SeekableByteChannel[] e = new SeekableByteChannel[entries.size()];
SeekableByteChannel[] inputs = new SeekableByteChannel[entries.size()];
for (int j = 0; j < e.length; j++) {
inputs[j] = Flattern.resolveDataRef(entries.get(j));
}
result[i] = inputs;
}
return result;
}
private int calcSpaceReq(MovieBox movie) {
int sum = 0;
for (TrakBox trakBox : movie.getTracks()) {
ChunkOffsetsBox stco = Box.findFirst(trakBox, ChunkOffsetsBox.class, "mdia", "minf", "stbl", "stco");
if (stco != null)
sum += stco.getChunkOffsets().length * 4;
}
return sum;
}
public static SeekableByteChannel resolveDataRef(Box box) throws IOException {
if (box instanceof UrlBox) {
String url = ((UrlBox) box).getUrl();
if (!url.startsWith("file://"))
throw new RuntimeException("Only file:// urls are supported in data reference");
return readableFileChannel(new File(url.substring(7)));
} else if (box instanceof AliasBox) {
String uxPath = ((AliasBox) box).getUnixPath();
if (uxPath == null)
throw new RuntimeException("Could not resolve alias");
return readableFileChannel(new File(uxPath));
} else {
throw new RuntimeException(box.getHeader().getFourcc() + " dataref type is not supported");
}
}
public void flattern(MovieBox movie, File video) throws IOException {
video.delete();
SeekableByteChannel out = null;
try {
out = writableFileChannel(video);
flattern(movie, out);
} finally {
if (out != null)
out.close();
}
}
}