package com.limegroup.gnutella.metadata.video.reader;
import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import org.limewire.io.IOUtils;
import com.limegroup.gnutella.metadata.MetaReader;
import com.limegroup.gnutella.metadata.video.VideoMetaData;
/**
* A metadata parser for files that are using the QuickTime File Format
* to store metadata. Such files are .mov and .m4v (MPEG-4/Podcasts) for
* example.
*
* http://developer.apple.com/documentation/QuickTime/QTFF/index.html
*/
public class MOVMetaData implements MetaReader {
/** Length of File */
private long length = -1;
@Override
public VideoMetaData parse(File f) throws IOException {
RandomAccessFile in = null;
try {
length = f.length();
in = new RandomAccessFile(f, "r");
VideoMetaData videoData = new VideoMetaData();
parseAtoms(videoData, in);
return videoData;
} finally {
IOUtils.close(in);
}
}
/**
* Entry point for the parser
*/
private void parseAtoms(VideoMetaData videoData, DataInput in) throws IOException {
Atom atom = null;
while((atom = nextAtom(in)) != null) {
if (atom.isType("moov")) {
moov(videoData, atom, in);
break;
}
skip(atom.remaining, in);
}
}
/**
* Movie Atom
*
* {@see <a href="http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/
* chapter_3_section_2.html#//apple_ref/doc/uid/TP40000939-CH204-BBCGDJID">MOOV</a>}
*/
private void moov(VideoMetaData videoData, Atom moov, DataInput in) throws IOException {
long length = 0L;
Atom atom = null;
while(length < moov.remaining && (atom = nextAtom(in)) != null) {
if (atom.isType("mvhd")) {
mvhd(videoData, atom, in);
} else if (atom.isType("cmov")) {
cmov(videoData, atom, in);
} else if (atom.isType("trak")) {
trak(videoData, atom, in);
} else {
skip(atom.remaining, in);
}
length += atom.size;
}
}
/**
* Movie Header Atom
*
* {@see <a href="http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/
* chapter_3_section_2.html#//apple_ref/doc/uid/TP40000939-CH204-25527">MVHD</a>}
*/
private void mvhd(VideoMetaData videoData, Atom mvhd, DataInput in) throws IOException {
assert (mvhd.remaining == 100L);
in.skipBytes(12);
int timeScale = in.readInt();
int timeUnits = in.readInt();
int length = timeUnits/timeScale;
if (length > videoData.getLength()) {
videoData.setLength(length);
}
long toSkip = mvhd.remaining - 12 - 4 - 4;
assert (toSkip == 80L);
skip(toSkip, in);
}
/**
* Compressed Movie Resources
*
* {@see <a href="http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/
* chapter_3_section_6.html#//apple_ref/doc/uid/TP40000939-CH204-BBCDACCD">CMOV</a>}
*/
private void cmov(VideoMetaData videoData, Atom cmov, DataInput in) throws IOException {
long length = 0L;
Atom atom = null;
while(length < cmov.remaining && (atom = nextAtom(in)) != null) {
if (atom.isType("cmvd")) {
cmvd(videoData, atom, in);
} else {
skip(atom.remaining, in);
}
length += atom.size;
}
}
/**
* Compressed Video Data
*
* {@see <a href="http://wiki.multimedia.cx/index.php?title=Apple_QuickTime
* #Decompressing_Compressed_moov_Atoms_With_zlib">ZLIB Compressed Atoms</a>}
*
* @see #cmov(com.limegroup.gnutella.metadata.MOVMetaData.Atom, DataInput)
*/
private void cmvd(VideoMetaData videoData, Atom cmvd, DataInput in) throws IOException {
int decompressedSize = in.readInt();
if( cmvd == null || cmvd.remaining - 4 > Integer.MAX_VALUE || cmvd.remaining < 4 )
throw new IOException("File smaller than expected");
byte[] compressed = new byte[(int)(cmvd.remaining - 4)];
in.readFully(compressed);
Inflater decompresser = new Inflater();
try {
decompresser.setInput(compressed);
if( decompressedSize > Integer.MAX_VALUE || decompressedSize < 0)
throw new IOException("Illegal atom size");
byte[] decompressed = new byte[decompressedSize];
int num = -1;
try {
num = decompresser.inflate(decompressed);
} catch (DataFormatException e) {
throw new IOException(e.getMessage());
} finally {
decompresser.end();
}
if (num < decompressedSize) {
throw new EOFException("Decompressed size is less than expected: "
+ num + " < " + decompressedSize);
}
ByteArrayInputStream bais = new ByteArrayInputStream(decompressed);
DataInputStream dis = new DataInputStream(bais);
try {
parseAtoms(videoData, dis);
} finally {
dis.close();
}
} finally {
IOUtils.close(decompresser);
}
}
/**
* Track Atom
*
* {@see <a href="http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/
* chapter_3_section_3.html#//apple_ref/doc/uid/TP40000939-CH204-BBCBEAIF">TRAK</a>}
*/
private void trak(VideoMetaData videoData, Atom trak, DataInput in) throws IOException {
long length = 0L;
Atom atom = null;
while(length < trak.remaining && (atom = nextAtom(in)) != null) {
if (atom.isType("tkhd")) {
tkhd(videoData, atom, in);
} else {
skip(atom.remaining, in);
}
length += atom.size;
}
}
/**
* Track Header Atom
*
* {@see <a href="http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/
* chapter_3_section_3.html#//apple_ref/doc/uid/TP40000939-CH204-25550">TKHD</a>}
*/
private void tkhd(VideoMetaData videoData, Atom tkhd, DataInput in) throws IOException {
assert (tkhd.remaining == 84);
skip(76, in);
// Width and Height are Fixpoint Ints!
int width = toTwoCompliant(in.readInt());
int height = toTwoCompliant(in.readInt());
if (width > videoData.getWidth()) {
videoData.setWidth(width);
}
if (height > videoData.getHeight()) {
videoData.setHeight(height);
}
}
/**
* Converts a fixpoint Integer to its two's compliant
* representation
*/
private static int toTwoCompliant(int value) {
return value / (0xFFFF + 1);
}
/**
* Skips 'toSkip' bytes in the given DataInput
*/
private static long skip(long toSkip, DataInput in) throws IOException {
long skipped = 0L;
while (skipped < toSkip) {
int s = (int)Math.min(toSkip-skipped, Integer.MAX_VALUE);
int num = in.skipBytes(s);
if (num != s) {
throw new EOFException("Could not skip " + s + " bytes: " + num);
}
skipped += s;
}
return skipped;
}
/**
* Reads the next Atom from the DataInput
*/
private Atom nextAtom(DataInput in) throws IOException {
long size = in.readInt() & 0xFFFFFFFFL;
if (size == 0L) {
return null;
}
boolean extened = false;
String atom = toAtomName(in.readInt());
if (size == 1L) {
size = in.readLong();
extened = true;
}
if (size > length) {
throw new IOException("Size is too big: " + size + " > " + length);
}
return new Atom(atom, size, extened);
}
/**
* Converts an atomType to its String representation
*/
private static String toAtomName(int atomType) throws UnsupportedEncodingException {
byte[] atomName = new byte[4];
atomName[0] = (byte)((atomType >> 24) & 0xFF);
atomName[1] = (byte)((atomType >> 16) & 0xFF);
atomName[2] = (byte)((atomType >> 8) & 0xFF);
atomName[3] = (byte)((atomType ) & 0xFF);
return new String(atomName, "8859_1");
}
private static class Atom {
/** Name of the Atom */
private final String name;
/**
* The total Size of the Atom (including the four bytes
* of the name as well as the 4 or 8 bytes of the length)
*/
private final long size;
/**
* The size of the payload (i.e. without the four bytes
* of the name and without the 4 or 8 bytes of the length)
*/
private final long remaining;
private Atom(String name, long size, boolean extended) {
this.name = name;
this.size = size;
if (extended) {
this.remaining = size - 16;
} else {
this.remaining = size - 8;
}
}
public boolean isType(String name) {
return this.name.equals(name);
}
@Override
public String toString() {
return name + "/" + size + "/" + Long.toHexString(size);
}
}
@Override
public String[] getSupportedExtensions() {
return new String[] { "mov", "m4v", "mp4", "3gp" };
}
}