package org.jcodec.containers.mp4;
import static org.jcodec.common.io.NIOUtils.readableChannel;
import org.jcodec.common.AutoFileChannelWrapper;
import org.jcodec.common.Codec;
import org.jcodec.common.io.IOUtils;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.logging.Logger;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.ChunkOffsets64Box;
import org.jcodec.containers.mp4.boxes.ChunkOffsetsBox;
import org.jcodec.containers.mp4.boxes.CompositionOffsetsBox;
import org.jcodec.containers.mp4.boxes.Edit;
import org.jcodec.containers.mp4.boxes.Header;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.MovieFragmentBox;
import org.jcodec.containers.mp4.boxes.SampleSizesBox;
import org.jcodec.containers.mp4.boxes.SampleToChunkBox;
import org.jcodec.containers.mp4.boxes.SyncSamplesBox;
import org.jcodec.containers.mp4.boxes.TimeToSampleBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.HashMap;
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
*
* @author The JCodec project
*
*/
public class MP4Util {
private static Map<Codec, String> codecMapping = new HashMap<Codec, String>();
static {
codecMapping.put(Codec.MPEG2, "m2v1");
codecMapping.put(Codec.H264, "avc1");
codecMapping.put(Codec.J2K, "mjp2");
}
public static MovieBox createRefMovie(SeekableByteChannel input, String url) throws IOException {
MovieBox movie = parseMovieChannel(input);
TrakBox[] tracks = movie.getTracks();
for (int i = 0; i < tracks.length; i++) {
TrakBox trakBox = tracks[i];
trakBox.setDataRef(url);
}
return movie;
}
public static MovieBox parseMovieChannel(SeekableByteChannel input) throws IOException {
for (Atom atom : getRootAtoms(input)) {
if ("moov".equals(atom.getHeader().getFourcc())) {
return (MovieBox) atom.parseBox(input);
}
}
return null;
}
public static List<MovieFragmentBox> parseMovieFragments(SeekableByteChannel input) throws IOException {
MovieBox moov = null;
LinkedList<MovieFragmentBox> fragments = new LinkedList<MovieFragmentBox>();
for (Atom atom : getRootAtoms(input)) {
if ("moov".equals(atom.getHeader().getFourcc())) {
moov = (MovieBox) atom.parseBox(input);
} else if ("moof".equalsIgnoreCase(atom.getHeader().getFourcc())) {
fragments.add((MovieFragmentBox) atom.parseBox(input));
}
}
for (MovieFragmentBox fragment : fragments) {
fragment.setMovie(moov);
}
return fragments;
}
public static List<Atom> getRootAtoms(SeekableByteChannel input) throws IOException {
input.setPosition(0);
List<Atom> result = new ArrayList<Atom>();
long off = 0;
Header atom;
while (off < input.size()) {
input.setPosition(off);
atom = Header.read(NIOUtils.fetchFromChannel(input, 16));
if (atom == null)
break;
result.add(new Atom(atom, off));
off += atom.getSize();
}
return result;
}
public static Atom findFirstAtomInFile(String fourcc, File input) throws IOException {
SeekableByteChannel c = new AutoFileChannelWrapper(input);
try {
return findFirstAtom(fourcc, c);
} finally {
IOUtils.closeQuietly(c);
}
}
public static Atom findFirstAtom(String fourcc, SeekableByteChannel input) throws IOException {
List<Atom> rootAtoms = getRootAtoms(input);
for (Atom atom : rootAtoms) {
if (fourcc.equals(atom.getHeader().getFourcc()))
return atom;
}
return null;
}
public static Atom atom(SeekableByteChannel input) throws IOException {
long off = input.position();
Header atom = Header.read(NIOUtils.fetchFromChannel(input, 16));
return atom == null ? null : new Atom(atom, off);
}
public static class Atom {
private long offset;
private Header header;
public Atom(Header header, long offset) {
this.header = header;
this.offset = offset;
}
public long getOffset() {
return offset;
}
public Header getHeader() {
return header;
}
public Box parseBox(SeekableByteChannel input) throws IOException {
input.setPosition(offset + header.headerSize());
return BoxUtil.parseBox(NIOUtils.fetchFromChannel(input, (int) header.getBodySize()), header, BoxFactory.getDefault());
}
public void copy(SeekableByteChannel input, WritableByteChannel out) throws IOException {
input.setPosition(offset);
NIOUtils.copy(input, out, header.getSize());
}
}
public static MovieBox parseMovie(File source) throws IOException {
SeekableByteChannel input = null;
try {
input = readableChannel(source);
return parseMovieChannel(input);
} finally {
if (input != null)
input.close();
}
}
public static MovieBox createRefMovieFromFile(File source) throws IOException {
SeekableByteChannel input = null;
try {
input = readableChannel(source);
return createRefMovie(input, "file://" + source.getCanonicalPath());
} finally {
if (input != null)
input.close();
}
}
public static void writeMovieToFile(File f, MovieBox movie) throws IOException {
SeekableByteChannel out = null;
try {
out = NIOUtils.writableChannel(f);
writeMovie(out, movie);
} finally {
out.close();
}
}
public static void writeMovie(SeekableByteChannel out, MovieBox movie) throws IOException {
doWriteMovieToChannel(out, movie, 0);
}
public static void doWriteMovieToChannel(SeekableByteChannel out, MovieBox movie, int additionalSize) throws IOException {
int sizeHint = estimateMoovBoxSize(movie) + additionalSize;
Logger.debug("Using " + sizeHint + " bytes for MOOV box");
ByteBuffer buf = ByteBuffer.allocate(sizeHint);
movie.write(buf);
buf.flip();
out.write(buf);
}
/**
* Estimate buffer size needed to write MOOV box based on the amount of
* stuff in there
*
* @param movie
* @return
*/
public static int estimateMoovBoxSize(MovieBox movie) {
int sizeHint = 4 << 10; // 4K plus
TrakBox[] tracks = movie.getTracks();
for (int i = 0; i < tracks.length; i++) {
TrakBox trak = tracks[i];
sizeHint += 4 << 10; // 4K per track
List<Edit> edits = trak.getEdits();
sizeHint += edits != null ? (edits.size() << 3) + (edits.size() << 2) : 0;
ChunkOffsetsBox stco = trak.getStco();
sizeHint += stco != null ? (stco.getChunkOffsets().length << 2) : 0;
ChunkOffsets64Box co64 = trak.getCo64();
sizeHint += co64 != null ? (co64.getChunkOffsets().length << 3) : 0;
SampleSizesBox stsz = trak.getStsz();
sizeHint += stsz != null ? (stsz.getDefaultSize() != 0 ? 0 : (stsz.getCount() << 2)) : 0;
TimeToSampleBox stts = trak.getStts();
sizeHint += stts != null ? (stts.getEntries().length << 3) : 0;
SyncSamplesBox stss = trak.getStss();
sizeHint += stss != null ? (stss.getSyncSamples().length << 2) : 0;
CompositionOffsetsBox ctts = trak.getCtts();
sizeHint += ctts != null ? (ctts.getEntries().length << 3) : 0;
SampleToChunkBox stsc = trak.getStsc();
sizeHint += stsc != null ? (stsc.getSampleToChunk().length << 3) + (stsc.getSampleToChunk().length << 2)
: 0;
}
return sizeHint;
}
public static String getFourcc(Codec codec) {
return codecMapping.get(codec);
}
public static ByteBuffer writeBox(Box box, int approxSize) {
ByteBuffer buf = ByteBuffer.allocate(approxSize);
box.write(buf);
buf.flip();
return buf;
}
}