
org.monte.media.quicktime.QuickTimeInputStream Maven / Gradle / Ivy
/*
* @(#)QuickTimeInputStream.java
*
* Copyright (c) 2012 Werner Randelshofer, Goldau, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package org.monte.media.quicktime;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.zip.InflaterInputStream;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
import org.monte.media.io.ImageInputStreamAdapter;
/**
* Provides low-level support for reading encoded audio and video samples from a
* QuickTime file.
*
* @author Werner Randelshofer
* @version $Id: QuickTimeInputStream.java 307 2013-01-06 11:06:05Z werner $
*/
public class QuickTimeInputStream extends AbstractQuickTimeStream {
/**
* Same as DataAtom in super class, but used for reading the meta data.
*/
private class InputAtom extends Atom {
private byte[] data;
public InputAtom(String type, byte[] data) throws IOException {
super(type, -1);
this.data = data;
}
@Override
public void finish() throws IOException {
//empty
}
@Override
public long size() {
return data.length;
}
}
/**
* The image input stream.
*/
protected final ImageInputStream in;
/**
* This variable is set to true when all meta-data has been read from the
* file.
*/
private boolean isRealized = false;
static final HashSet compositeAtoms;
static {
compositeAtoms = new HashSet();
compositeAtoms.add("moov");
compositeAtoms.add("cmov");
compositeAtoms.add("gmhd");
compositeAtoms.add("trak");
compositeAtoms.add("tref");
compositeAtoms.add("meta"); // sometimes has a special 4 byte header before its contents
compositeAtoms.add("ilst");
compositeAtoms.add("mdia");
compositeAtoms.add("minf");
compositeAtoms.add("udta");
compositeAtoms.add("stbl");
compositeAtoms.add("dinf");
compositeAtoms.add("edts");
compositeAtoms.add("clip");
compositeAtoms.add("matt");
compositeAtoms.add("rmra");
compositeAtoms.add("rmda");
compositeAtoms.add("tapt");
compositeAtoms.add("mvex");
}
/**
* Creates a new instance.
*
* @param file the input file
*/
public QuickTimeInputStream(File file) throws IOException {
this.in = new FileImageInputStream(file);
in.setByteOrder(ByteOrder.BIG_ENDIAN);
this.streamOffset = 0;
}
/**
* Creates a new instance.
*
* @param in the input stream.
*/
public QuickTimeInputStream(ImageInputStream in) throws IOException {
this.in = in;
this.streamOffset = in.getStreamPosition();
in.setByteOrder(ByteOrder.BIG_ENDIAN);
}
public int getTrackCount() throws IOException {
ensureRealized();
throw new UnsupportedOperationException("Not supported yet.");
}
public long getMovieDuration() throws IOException {
ensureRealized();
long duration = 0;
for (Track t : tracks) {
duration = Math.max(duration, t.getTrackDuration(movieTimeScale));
}
return duration;
}
/**
* Gets the creation time of the movie.
*/
public Date getCreationTime() throws IOException {
ensureRealized();
return creationTime;
}
/**
* Gets the modification time of the movie.
*/
public Date getModificationTime() throws IOException {
ensureRealized();
return modificationTime;
}
/**
* Gets the preferred rate at which to play this movie. A value of 1.0
* indicates normal rate.
*/
public double getPreferredRate() throws IOException {
ensureRealized();
return preferredRate;
}
/**
* Gets the preferred volume of this movie’s sound. A value of 1.0 indicates
* full volume.
*/
public double getPreferredVolume() throws IOException {
ensureRealized();
return preferredVolume;
}
/**
* Gets the time value for current time position within the movie.
*/
public long getCurrentTime() throws IOException {
ensureRealized();
return currentTime;
}
/**
* Gets the time value of the time of the movie poster.
*/
public long getPosterTime() throws IOException {
ensureRealized();
return posterTime;
}
/**
* Gets the duration of the movie preview in movie time scale units.
*/
public long getPreviewDuration() throws IOException {
ensureRealized();
return previewDuration;
}
/**
* Gets the time value in the movie at which the preview begins.
*/
public long getPreviewTime() throws IOException {
ensureRealized();
return previewTime;
}
/**
* Gets the transformation matrix of the entire movie.
*
* @return The transformation matrix.
*/
public double[] getMovieTransformationMatrix() throws IOException {
ensureRealized();
return movieMatrix.clone();
}
/**
* Returns the time scale of the movie. The movie time scale is used for
* editing tracks. Such as for specifying the start time of a track.
*
* @return time scale
*/
public long getMovieTimeScale() throws IOException {
ensureRealized();
return movieTimeScale;
}
/**
* Returns the time scale of the media in a track.
The media time scale
* is used for specifying the duration of samples in a track.
*
* @param track Track index.
* @return time scale
*/
public long getMediaTimeScale(int track) throws IOException {
ensureRealized();
return tracks.get(track).mediaTimeScale;
}
/**
* Returns the media duration of a track in the media's time scale.
*
* @param track Track index.
* @return media duration
*/
public long getMediaDuration(int track) throws IOException {
ensureRealized();
return tracks.get(track).mediaDuration;
}
/**
* Gets the transformation matrix of the specified track.
*
* @param track The track number.
* @return The transformation matrix.
*/
public double[] getTransformationMatrix(int track) throws IOException {
ensureRealized();
return tracks.get(track).matrix.clone();
}
/**
* Ensures that all meta-data has been read from the file.
*/
protected void ensureRealized() throws IOException {
if (!isRealized) {
isRealized = true;
readAllMetadata();
}
}
private void readAllMetadata() throws IOException {
long remainingSize = in.length();
if (remainingSize == -1) {
remainingSize = Long.MAX_VALUE;
}
in.seek(0);
readAllMetadata(new DataAtomInputStream(new ImageInputStreamAdapter(in)), remainingSize, new HashMap(), null);
}
private void readAllMetadata(DataAtomInputStream in, long remainingSize, HashMap atoms, String path) throws IOException {
long pos = 0;
InputAtom atom;
while (remainingSize > 0) {
long size = in.readInt() & 0xffffffffL;
int headerSize = 8;
if (size == 0) {
// A zero len indicates that the size is the remainder of
// the parent atom.
size = remainingSize;
// skip 4 bytes after zero size.
if (headerSize + 4 <= remainingSize) {
in.skipBytes(4);
headerSize += 4;
}
} else if (size == 1) {
// A size of 1 indicates a 64 bit size field
headerSize = 16;
size = in.readLong();
}
String type;
long atomSize = size;
if (size > remainingSize) {
//System.out.println("QuickTimeStructView truncating size: " + size + " to:" + (remainingSize));
size = remainingSize;
}
if (size - headerSize >= 0) {
type = intToType(in.readInt());
//System.out.println("QuickTimeStructView " + type + " size:" + size + " remaining:" + remainingSize);
} else {
type = "";
}
remainingSize -= size;
// "stsd" atoms have a special structure if one of their parent
// atoms contains a "vmhd" or a "smhd" child atom.
if (type.equals("stsd")) {
// handle stsd chunk
}
if (compositeAtoms.contains(type) && size - headerSize >= 8) {
if (type.equals("trak")) {
atoms.clear();
readAllMetadata(in, size - headerSize, atoms, path == null ? type : path + "." + type);
parseTrack(atoms);
} else {
readAllMetadata(in, size - headerSize, atoms, path == null ? type : path + "." + type);
}
} else {
byte[] data;
if (type.equals("mdat")) {
data = new byte[0];
long skipped = 0;
while (skipped < size - headerSize) {
long skipValue = in.skipBytes(size - headerSize - skipped);
if (skipValue > 0) {
skipped += skipValue;
} else {
throw new IOException("unable to skip");
}
}
} else {
if (size < headerSize) {
// Pad atom?
data = new byte[0];
} else {
data = new byte[(int) (size - headerSize)];
in.readFully(data);
}
}
atom = new InputAtom(type, data);
atoms.put(path == null ? type : path + "." + type, atom);
if (type.equals("cmvd")) {
// => We have found a compressed movie header.
// Decompress it and start over.
try {
InputStream in2 = new InflaterInputStream(new ByteArrayInputStream(data, 4, data.length - 4));
ByteArrayOutputStream out = new ByteArrayOutputStream();
int b;
while ((b = in2.read()) != -1) {
out.write(b);
}
in2.close();
out.close();
byte[] decompressed = out.toByteArray();
readAllMetadata(new DataAtomInputStream(
new ByteArrayInputStream(decompressed)),
decompressed.length,
atoms, path);
} catch (IOException e) {
e.printStackTrace();
}
} else if (type.equals("mvhd")) {
// => We have found an uncompressed movie header.
// Parse it.
parseMovieHeader(data);
}
}
pos += size;
}
}
public void close() throws IOException {
in.close();
}
/**
* Parses the mvhd atom.
*/
private void parseMovieHeader(byte[] data) throws IOException {
/* Movie Header Atom -------------
* The data contained in this atom defines characteristics of the entire
* QuickTime movie, such as time scale and duration. It has an atom type
* value of 'mvhd'.
*
* typedef struct {
byte version;
byte[3] flags;
mactimestamp creationTime;
mactimestamp modificationTime;
int timeScale;
int duration;
fixed16d16 preferredRate;
fixed8d8 preferredVolume;
byte[10] reserved;
fixed16d16 matrixA;
fixed16d16 matrixB;
fixed2d30 matrixU;
fixed16d16 matrixC;
fixed16d16 matrixD;
fixed2d30 matrixV;
fixed16d16 matrixX;
fixed16d16 matrixY;
fixed2d30 matrixW;
int previewTime;
int previewDuration;
int posterTime;
int selectionTime;
int selectionDuration;
int currentTime;
int nextTrackId;
} movieHeaderAtom;
*/
DataAtomInputStream i = new DataAtomInputStream(new ByteArrayInputStream(data));
int version = i.readByte();
i.skipBytes(3);// flags
creationTime = i.readMacTimestamp();
modificationTime = i.readMacTimestamp();
movieTimeScale = i.readUInt();
long movieDuration = i.readUInt();
preferredRate = i.readFixed16D16();
preferredVolume = i.readFixed8D8();
i.skipBytes(10);//reserved
// {a, b, u,
// c, d, v,
// tx,ty,w} // X- and Y-Translation
movieMatrix[0] = i.readFixed16D16();
movieMatrix[1] = i.readFixed16D16();
movieMatrix[2] = i.readFixed2D30();
movieMatrix[3] = i.readFixed16D16();
movieMatrix[4] = i.readFixed16D16();
movieMatrix[5] = i.readFixed2D30();
movieMatrix[6] = i.readFixed16D16();
movieMatrix[7] = i.readFixed16D16();
movieMatrix[8] = i.readFixed2D30();
previewTime = i.readUInt();
previewDuration = i.readUInt();
posterTime = i.readUInt();
selectionTime = i.readUInt();
selectionDuration = i.readUInt();
currentTime = i.readUInt();
long nextTrackId = i.readUInt();
}
/**
* Parses track atoms.
*/
private void parseTrack(HashMap atoms) throws IOException {
for (String p : atoms.keySet()) {
System.out.println("QuickTimeInputStream " + p);
}
HashMap hdlrMap = parseHdlr(atoms.get("moov.trak.mdia.hdlr").data);
String trackType = (String) hdlrMap.get("componentSubtype");
Track t;
if ("vide".equals(trackType)) {
t = new VideoTrack();
} else if ("soun".equals(trackType)) {
t = new AudioTrack();
} else {
throw new IOException("Unsupported track type: " + trackType);
}
parseTkhd(t, atoms.get("moov.trak.tkhd").data);
if (atoms.get("moov.trak.edts") != null) {
parseEdts(t, atoms.get("moov.trak.edts").data);
}
if (atoms.get("moov.trak.mdhd") != null) {
parseMdhd(t, atoms.get("moov.trak.mdhd").data);
}
if ("vide".equals(trackType)) {
parseVideoTrack((VideoTrack) t, atoms);
} else if ("soun".equals(trackType)) {
parseAudioTrack((AudioTrack) t, atoms);
} else {
throw new IOException("Unsupported track type: " + trackType);
}
tracks.add(t);
}
private void parseVideoTrack(VideoTrack t, HashMap atoms) throws IOException {
}
private void parseAudioTrack(AudioTrack t, HashMap atoms) throws IOException {
}
/**
* Parses a tkhd atom.
*/
private void parseTkhd(Track t, byte[] data) throws IOException {
/*
// Enumeration for track header flags
set {
TrackEnable = 0x1, // enabled track
TrackInMovie = 0x2, // track in playback
TrackInPreview = 0x4, // track in preview
TrackInPoster = 0x8 // track in poster
} TrackHeaderFlags;
typedef struct {
byte version;
byte flag0;
byte flag1;
byte set TrackHeaderFlags flag2;
mactimestamp creationTime;
mactimestamp modificationTime;
int trackId;
byte[4] reserved;
int duration;
byte[8] reserved;
short layer;
short alternateGroup;
fixed8d8 volume;
byte[2] reserved;
fixed16d16 matrixA;
fixed16d16 matrixB;
fixed2d30 matrixU;
fixed16d16 matrixC;
fixed16d16 matrixD;
fixed2d30 matrixV;
fixed16d16 matrixX;
fixed16d16 matrixY;
fixed2d30 matrixW;
fixed16d16 trackWidth;
fixed16d16 trackHeight;
} trackHeaderAtom;
*/
DataAtomInputStream dain = new DataAtomInputStream(new ByteArrayInputStream(data));
int version = dain.readByte();
dain.skipBytes(2);
// FIXME - trackHeaderFlag should be a parsed
int trackHeaderFlags = dain.readUByte();
Date creationTime = dain.readMacTimestamp();
Date modificationTime = dain.readMacTimestamp();
int trackId = dain.readInt();
dain.skipBytes(4);
long duration = dain.readUInt();
dain.skipBytes(8);
int layer = dain.readUShort();
int alternateGroup = dain.readUShort();
double volume = dain.readFixed8D8();
dain.skipBytes(2);
// {a, b, u,
// c, d, v,
// tx,ty,w} // X- and Y-Translation
t.matrix[0] = dain.readFixed16D16();
t.matrix[1] = dain.readFixed16D16();
t.matrix[2] = dain.readFixed2D30();
t.matrix[3] = dain.readFixed16D16();
t.matrix[4] = dain.readFixed16D16();
t.matrix[5] = dain.readFixed2D30();
t.matrix[6] = dain.readFixed16D16();
t.matrix[7] = dain.readFixed16D16();
t.matrix[8] = dain.readFixed2D30();
t.width = dain.readFixed16D16();
t.height = dain.readFixed16D16();
}
/**
* Parses a hdlr atom.
*/
private HashMap parseHdlr(byte[] data) throws IOException {
/*
typedef struct {
byte version;
byte[3] flags;
magic componentType;
magic componentSubtype;
magic componentManufacturer;
int componentFlags;
int componentFlagsMask;
pstring componentName;
ubyte[] extraData;
} handlerReferenceAtom;
*/
DataAtomInputStream i = new DataAtomInputStream(new ByteArrayInputStream(data));
int version = i.readByte();
int flags = (i.readUShort() << 8) | (i.readUByte());
String componentType = i.readType();
String componentSubtype = i.readType();
String componentManufactureer = i.readType();
int componentFlags = i.readInt();
int componentFlagsMask = i.readInt();
String componentName = i.readPString();
HashMap m = new HashMap();
m.put("componentSubtype", componentSubtype);
return m;
}
/**
* Parses an edts atom.
*/
private void parseEdts(Track t, byte[] data) throws IOException {
/* Edit List atom ------- */
/*
typedef struct {
byte version;
byte[3] flags;
int numberOfEntries;
editListTable editListTable[numberOfEntries];
} editListAtom;
typedef struct {
int trackDuration;
int mediaTime;
fixed16d16 mediaRate;
} editListTable;
*/
DataAtomInputStream dain = new DataAtomInputStream(new ByteArrayInputStream(data));
int version = dain.readByte();
int flags = (dain.readUShort() << 8) | (dain.readUByte());
int numberOfEntries = dain.readInt();
t.editList = new Edit[numberOfEntries];
for (int i = 0; i < numberOfEntries; i++) {
Edit edit = new Edit(dain.readInt(), dain.readInt(), dain.readFixed16D16());
t.editList[i] = edit;
}
}
/**
* Parses a mdhd atom.
*/
private void parseMdhd(Track t, byte[] data) throws IOException {
/* Media Header atom -------
typedef struct {
byte version;
byte[3] flags;
mactimestamp creationTime;
mactimestamp modificationTime;
int timeScale;
int duration;
short language;
short quality;
} mediaHeaderAtom;*/
DataAtomInputStream dain = new DataAtomInputStream(new ByteArrayInputStream(data));
int version = dain.readByte();
int flags = (dain.readUShort() << 8) | (dain.readUByte());
Date creationTime = dain.readMacTimestamp();
Date modificationTime = dain.readMacTimestamp();
t.mediaTimeScale = dain.readUInt();
t.mediaDuration = dain.readUInt();
short language = dain.readShort();
short quality = dain.readShort();
}
}