package org.jcodec.movtool; import java.lang.IllegalStateException; import java.lang.System; import java.lang.IllegalArgumentException; import static org.jcodec.common.io.NIOUtils.readableChannel; import static org.jcodec.common.io.NIOUtils.writableChannel; import org.jcodec.common.io.NIOUtils; import org.jcodec.common.io.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; import org.jcodec.platform.Platform; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 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 main1(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]); Platform.deleteFile(outFile); SeekableByteChannel input = null; try { input = readableChannel(new File(args[0])); MovieBox movie = MP4Util.parseMovieChannel(input); new Flattern().flattern(movie, outFile); } finally { if (input != null) input.close(); } } public List<ProgressListener> listeners; public Flattern() { this.listeners = new ArrayList<Flattern.ProgressListener>(); } public interface ProgressListener { public void trigger(int progress); } public void addProgressListener(ProgressListener listener) { this.listeners.add(listener); } public void flatternChannel(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 = FileTypeBox.createFileTypeBox("qt ", 0x20050300, Arrays.asList(new String[] { "qt " })); ftyp.write(buf); long movieOff = buf.position(); movie.write(buf); int extraSpace = calcSpaceReq(movie); Header.createHeader("free", 8 + extraSpace).write(buf); NIOUtils.skip(buf, extraSpace); long mdatOff = buf.position(); Header.createHeader("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.setPosition(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.setPosition(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.findFirstPath(tracks[i], DataRefBox.class, Box.path("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] = resolveDataRef(entries.get(j)); } result[i] = inputs; } return result; } private int calcSpaceReq(MovieBox movie) { int sum = 0; TrakBox[] tracks = movie.getTracks(); for (int i = 0; i < tracks.length; i++) { TrakBox trakBox = tracks[i]; ChunkOffsetsBox stco = trakBox.getStco(); if (stco != null) sum += stco.getChunkOffsets().length * 4; } return sum; } public 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 readableChannel(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 readableChannel(new File(uxPath)); } else { throw new RuntimeException(box.getHeader().getFourcc() + " dataref type is not supported"); } } public void flattern(MovieBox movie, File video) throws IOException { Platform.deleteFile(video); SeekableByteChannel out = null; try { out = writableChannel(video); flatternChannel(movie, out); } finally { if (out != null) out.close(); } } }