/*
* @(#)QuicktimeParser.java 1.76 02/08/21
*
* Copyright (c) 1996-2002 Sun Microsystems, Inc. All rights reserved.
*/
package com.sun.media.parser.video;
import java.io.IOException;
import java.awt.Dimension;
import javax.media.Track;
import javax.media.IncompatibleSourceException;
import javax.media.BadHeaderException;
import javax.media.Time;
import javax.media.Duration;
import javax.media.TrackListener;
import javax.media.Buffer;
import javax.media.protocol.DataSource;
import javax.media.protocol.SourceStream;
import javax.media.protocol.PullSourceStream;
import javax.media.protocol.Seekable;
import javax.media.Format;
import javax.media.protocol.ContentDescriptor;
import javax.media.protocol.CachedStream;
import javax.media.format.AudioFormat;
import javax.media.format.VideoFormat;
import javax.media.format.RGBFormat;
import javax.media.format.YUVFormat;
import com.sun.media.parser.BasicPullParser;
import com.sun.media.util.SettableTime;
public class QuicktimeParser extends BasicPullParser {
private final boolean enableHintTrackSupport = true;
private static ContentDescriptor[] supportedFormat = new ContentDescriptor[] {new ContentDescriptor("video.quicktime")};
private PullSourceStream stream = null;
private Track[] tracks;
private Seekable seekableStream;
private boolean mdatAtomPresent = false;
private boolean moovAtomPresent = false;
public final static int MVHD_ATOM_SIZE = 100;
public final static int TKHD_ATOM_SIZE = 84;
public final static int MDHD_ATOM_SIZE = 24;
public final static int MIN_HDLR_ATOM_SIZE = 24;
public final static int MIN_STSD_ATOM_SIZE = 8;
public final static int MIN_STTS_ATOM_SIZE = 8;
public final static int MIN_STSC_ATOM_SIZE = 8;
public final static int MIN_STSZ_ATOM_SIZE = 8;
public final static int MIN_STCO_ATOM_SIZE = 8;
public final static int MIN_STSS_ATOM_SIZE = 8;
public final static int MIN_VIDEO_SAMPLE_DATA_SIZE = 70;
public final static int MIN_AUDIO_SAMPLE_DATA_SIZE = 20;
public final static int TRACK_ENABLED = 0x1;
public final static int TRACK_IN_MOVIE = 0x2;
public final static int TRACK_IN_PREVIEW = 0x4;
public final static int TRACK_IN_POSTER = 0x8;
public final static String VIDEO = "vide";
public final static String AUDIO = "soun";
public final static String HINT = "hint";
private final static int DATA_SELF_REFERENCE_FLAG = 0x1;
private final static int HINT_NOP_IGNORE = 0;
private final static int HINT_IMMEDIATE_DATA = 1;
private final static int HINT_SAMPLE_DATA = 2;
private final static int HINT_SAMPLE_DESCRIPTION = 3;
private MovieHeader movieHeader = new MovieHeader();
private int numTracks = 0;
private int numSupportedTracks = 0;
private int numberOfHintTracks = 0;
private static int MAX_TRACKS_SUPPORTED = 100;
private TrakList[] trakList = new TrakList[MAX_TRACKS_SUPPORTED];
private TrakList currentTrack; // The better way is use pass it as parameter
private int keyFrameTrack = -1;
private SettableTime mediaTime = new SettableTime(0L);
private int hintAudioTrackNum = -1;
private boolean debug = false;
private boolean debug1 = false;
private boolean debug2 = false;
// Used to make the seek and the subsequent readBytes call atomic
// operations, so that the video and audio track
// threads don't trample each other.
private Object seekSync = new Object();
private int tmpIntBufferSize = 16 * 1024;
private byte[] tmpBuffer = new byte[tmpIntBufferSize * 4];
/**
* Quicktime format requires that the stream be seekable and
* random accessible.
*/
protected boolean supports(SourceStream[] s) {
return seekable;
}
public void setSource(DataSource source)
throws IOException, IncompatibleSourceException {
super.setSource(source);
stream = (PullSourceStream) streams[0];
seekableStream = (Seekable) streams[0];
}
private CachedStream getCacheStream() {
return cacheStream;
}
public ContentDescriptor [] getSupportedInputContentDescriptors() {
return supportedFormat;
}
public Track[] getTracks() throws IOException, BadHeaderException {
if (tracks != null)
return tracks;
if (seekableStream == null) {
return new Track[0];
}
if (cacheStream != null) {
// Disable jitter buffer during parsing of the header
cacheStream.setEnabledBuffering(false);
}
readHeader();
if (cacheStream != null) {
cacheStream.setEnabledBuffering(true);
}
tracks = new Track[numSupportedTracks];
// System.out.println("numTracks is " + numTracks);
// System.out.println("numSupportedTracks is " + numSupportedTracks);
int index = 0;
for (int i = 0; i < numSupportedTracks; i++) {
TrakList trakInfo = trakList[i];
if (trakInfo.trackType.equals(AUDIO)) {
tracks[i] = new AudioTrack(trakInfo);
// System.out.println("track id " + (index-1) + " : " +
// tracks[index-1]);
} else if (trakInfo.trackType.equals(VIDEO)) {
tracks[i] = new VideoTrack(trakInfo);
// System.out.println("track id " + (index-1) + " : " +
// tracks[index-1]);
}
}
for (int i = 0; i < numSupportedTracks; i++) {
TrakList trakInfo = trakList[i];
if (trakInfo.trackType.equals(HINT)) {
int trackBeingHinted = trakInfo.trackIdOfTrackBeingHinted;
for (int j = 0; j < numTracks; j++) {
if (trackBeingHinted == trakList[j].id) {
trakInfo.indexOfTrackBeingHinted = j;
String hintedTrackType = trakList[j].trackType;
String encodingOfHintedTrack =
trakList[trakInfo.indexOfTrackBeingHinted].media.encoding;
if (encodingOfHintedTrack.equals("agsm"))
encodingOfHintedTrack = "gsm";
String rtpEncoding = encodingOfHintedTrack + "/rtp";
if (hintedTrackType.equals(AUDIO)) {
int channels;
String encoding;
int frameSizeInBytes;
int samplesPerBlock;
int sampleRate;
Audio audio = (Audio) (trakList[j].media);
hintAudioTrackNum = i;
channels = audio.channels;
frameSizeInBytes = audio.frameSizeInBits / 8;
samplesPerBlock = audio.samplesPerBlock;
sampleRate = audio.sampleRate;
((Hint) trakInfo.media).format =
new AudioFormat(rtpEncoding,
(double) sampleRate,
8, // sampleSizeInBits [$$$ hardcoded]
channels);
tracks[i] = new HintAudioTrack(trakInfo,
channels,
rtpEncoding,
frameSizeInBytes,
samplesPerBlock,
sampleRate);
// System.out.println("track id " + (index-1) + " : " +
// tracks[index-1]);
} else if (hintedTrackType.equals(VIDEO)) {
int indexOfTrackBeingHinted = trakInfo.indexOfTrackBeingHinted;
TrakList sampleTrakInfo = null;
if (indexOfTrackBeingHinted >= 0) {
sampleTrakInfo = trakList[indexOfTrackBeingHinted];
}
int width = 0;
int height = 0;
if (sampleTrakInfo != null) {
Video sampleTrakVideo = (Video) sampleTrakInfo.media;
width = sampleTrakVideo.width;
height = sampleTrakVideo.height;
}
if ( (width > 0) && (height > 0) ) {
((Hint) trakInfo.media).format =
new VideoFormat(rtpEncoding,
new Dimension(width, height),
Format.NOT_SPECIFIED,
null, Format.NOT_SPECIFIED);
// System.out.println("VIDEO HINT TRACK FORMAT is " +
// ((Hint) trakInfo.media).format);
}
HintVideoTrack hintVideoTrack =
new HintVideoTrack(trakInfo);
tracks[i] = hintVideoTrack;
// System.out.println("track id " + (index-1) + " : " +
// tracks[index-1]);
}
break;
}
}
}
}
return tracks;
}
private void /* for now void */ readHeader()
throws IOException, BadHeaderException {
while ( parseAtom() );
if ( !moovAtomPresent )
throw new BadHeaderException("moov atom not present");
if ( !mdatAtomPresent )
throw new BadHeaderException("mdat atom not present");
// System.out.println("Number of tracks is " + numTracks);
// System.out.println("Number of supported/valid tracks is " + numSupportedTracks);
for (int i = 0; i < numSupportedTracks; i++) {
TrakList trak = trakList[i];
// System.out.println("track index " + i + " encoding " +
// trak.media.encoding);
// System.out.println("Number of frames in track " +
// trak.trackType + " : " +
// + i +
// " is " + trak.numberOfSamples);
// System.out.println("Duration of track " + i +
// trak.duration.getSeconds());
if (trak.buildSyncTable()) {
keyFrameTrack = i;
}
// System.out.println("$$$$ Call buildSamplePerChunkTable for track id " + trak.id);
trak.buildSamplePerChunkTable();
// Table is built for VIDEO and hint tracks but not
// for audio tracks.
if ( !trak.trackType.equals(AUDIO) ) {
trak.buildSampleOffsetTable();
// System.out.println("Creating buildSampleOffsetTable for track " +
// trak.trackType + " : " +
// trak.sampleOffsetTable);
trak.buildStartTimeAndDurationTable();
float frameRate = (float) (trak.numberOfSamples /
trak.duration.getSeconds());
//$$$$$ ((Video) trak.media).frameRate = frameRate;
trak.media.frameRate = frameRate;
}
// NOTE: The next method should be called after buildSampleOffsetTable()
trak.buildCumulativeSamplePerChunkTable();
trak.media.createFormat();
// System.out.println("track " + (i+1) + " info: ");
// System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
// System.out.println(trak);
// System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n");
}
}
public Time setPosition(Time where, int rounding) {
double time = where.getSeconds();
if (time < 0)
time = 0;
// if ( (keyFrameTrack != -1) && (tracks[keyFrameTrack].isEnabled()) ) {
int keyT;
if ( (((keyT = keyFrameTrack) != -1) && (tracks[keyFrameTrack].isEnabled())) ||
(((keyT = hintAudioTrackNum) != -1) && (tracks[hintAudioTrackNum].isEnabled())) ) {
TrakList trakInfo = trakList[keyT];
int index = trakInfo.time2Index(time);
if (index < 0) {
((MediaTrack)tracks[keyT]).setSampleIndex(trakInfo.numberOfSamples + 1); // past eom
} else {
int syncIndex;
if (keyT == keyFrameTrack) {
if (index >= trakInfo.syncSampleMapping.length) {
index = trakInfo.syncSampleMapping.length - 1;
}
if (trakInfo.syncSampleMapping != null) {
syncIndex = trakInfo.syncSampleMapping[index];
double newtime = trakInfo.index2TimeAndDuration(syncIndex).startTime;
time = newtime;
} else {
// Note: you won't come here because syncSampleMapping wont
// be null in this case.
syncIndex = index;
}
} else { // hint audio track
syncIndex = index;
double newtime = trakInfo.index2TimeAndDuration(syncIndex).startTime;
time = newtime;
}
((MediaTrack)tracks[keyT]).setSampleIndex(syncIndex);
}
}
for (int i = 0; i < numSupportedTracks; i++) {
if (i == keyT)
continue;
if (!tracks[i].isEnabled())
continue;
// TODO: See if you can just call a setPosition or
// setIndex method for each media type, instead of
// using if statement
TrakList trakInfo = trakList[i];
// Note that the time here may not be the same as the
// the "Time where" parameter passed into this method.
// The time may be changed if it doesn't map to a keyFrame
// in the Video track.
int index = trakInfo.time2Index(time);
// if ( trakInfo.trackType.equals(VIDEO) ||
// trakInfo.trackType.equals(HINT) ) {
if ( trakInfo.trackType.equals(VIDEO) ||
( trakInfo.trackType.equals(HINT) &&
(tracks[i] instanceof HintVideoTrack)) ) {
if (index < 0) {
((MediaTrack)tracks[i]).setSampleIndex(trakInfo.numberOfSamples + 1); // past eom
} else {
int syncIndex;
if (trakInfo.syncSampleMapping != null) {
syncIndex = trakInfo.syncSampleMapping[index];
} else
syncIndex = index;
((MediaTrack)tracks[i]).setSampleIndex(syncIndex);
}
} else { // TODO: if you have other track types, then check for AUDIO here
if (index < 0) {
((MediaTrack)tracks[i]).setChunkNumber(trakInfo.numberOfChunks + 1); // past eom
} else {
int sampleOffsetInChunk;
((MediaTrack)tracks[i]).setSampleIndex(index);
// $$$$$ IMPORTANT TODO: fix this as the index2Chunk method
// takes index starting from 1, not 0
int chunkNumber = trakInfo.index2Chunk(index);
if (chunkNumber != 0) {
if ( trakInfo.constantSamplesPerChunk == -1) {
// Note samplesPerChunk array contains cumulative
// samples per chunk
sampleOffsetInChunk = index -
trakInfo.samplesPerChunk[chunkNumber-1];
} else {
// TODO: need to test this case
sampleOffsetInChunk = index -
chunkNumber *
trakInfo.constantSamplesPerChunk;
}
} else {
sampleOffsetInChunk = index;
}
((AudioTrack)tracks[i]).setChunkNumberAndSampleOffset(chunkNumber,
sampleOffsetInChunk);
}
}
}
if (cacheStream != null) {
synchronized(this) {
cacheStream.abortRead();
}
}
synchronized(mediaTime) {
mediaTime.set(time);
}
return mediaTime;
}
public Time getMediaTime() {
return null; // TODO
}
public Time getDuration() {
return movieHeader.duration;
}
/**
* Returns a descriptive name for the plug-in.
* This is a user readable string.
*/
public String getName() {
return "Parser for quicktime file format";
}
private boolean parseAtom() throws BadHeaderException {
boolean readSizeField = false;
try {
int atomSize = readInt(stream);
// System.out.println("atomSize is " + atomSize);
readSizeField = true;
String atom = readString(stream);
// System.out.println("atom is " + atom);
if ( atomSize < 8 )
throw new BadHeaderException(atom + ": Bad Atom size " + atomSize);
if ( atom.equals("moov") )
return parseMOOV(atomSize - 8);
if ( atom.equals("mdat") )
return parseMDAT(atomSize - 8);
skipAtom(atom + " [not implemented]", atomSize - 8);
return true;
} catch (IOException e) {
// System.err.println("parseAtom: IOException " + e);
if (!readSizeField) {
// System.out.println("EOM");
return false; // EOM. Parsing done
}
throw new BadHeaderException("Unexpected End of Media");
}
}
private void skipAtom(String atom, int size) throws IOException {
if (debug2)
System.out.println("skip unsupported atom " + atom);
skip(stream, size);
}
/**
* Required atoms are mvhd and trak
* Doesn't say in the spec. that trak is a required atom,
* but can we play a qt file without a trak atom??
*/
private boolean parseMOOV(int moovSize) throws BadHeaderException {
boolean trakAtomPresent = false;
try {
moovAtomPresent = true;
long moovMax = getLocation(stream) + moovSize;
int remainingSize = moovSize;
int atomSize = readInt(stream);
String atom = readString(stream);
if ( atomSize < 8 )
throw new BadHeaderException(atom + ": Bad Atom size " + atomSize);
if ( ! atom.equals("mvhd") ) {
if (atom.equals("cmov"))
throw new BadHeaderException("Compressed movie headers are not supported");
else
throw new BadHeaderException("Expected mvhd atom but got " + atom);
}
parseMVHD(atomSize - 8);
// System.out.println("Duration of movie is " +
// movieHeader.duration.getSeconds());
remainingSize -= atomSize;
// TODO: before calling parseXXX, should check if
// (atomSize - 8) >= remainingSize
while (remainingSize > 0) {
atomSize = readInt(stream);
atom = readString(stream);
if (atom.equals("trak")) {
if (trakList[numSupportedTracks] == null) {
trakList[numSupportedTracks] = currentTrack = new TrakList();
}
if (parseTRAK(atomSize - 8)) {
numSupportedTracks++;
}
trakAtomPresent = true;
numTracks++;
} else if (atom.equals("ctab")) {
parseCTAB(atomSize - 8);
} else {
skipAtom(atom + " [atom in moov: not implemented]", atomSize - 8);
}
remainingSize -= atomSize;
}
if (!trakAtomPresent)
throw new BadHeaderException("trak atom not present in trak atom container");
// Parsing is done if the MDAT atom has also been seen.
return !mdatAtomPresent;
} catch (IOException e) {
throw new BadHeaderException("IOException when parsing the header");
}
}
private boolean parseMDAT(int size) throws BadHeaderException {
try {
mdatAtomPresent = true;
movieHeader.mdatStart = getLocation( stream ); // Need this ??? TODO
movieHeader.mdatSize = size; // Need this ??? TODO
/** Seek past the MDAT atom only if the MOOV atom
* hasn't been seen yet.
* The only reason to seek past the MDAT atom even if the
* MOOV atom has been seen is to handle top level atoms
* like PNOT (Movie Preview data). We currently don't support
* PNOT atom.
* Also, We don't know how fast the
* seek is. If it is based on RandomAccessFile like
* Sun's file datasource, then it is pretty fast.
* But if it a cached http datasource over a slow
* internet connection, then the seek will take a long
* time. So seeking past the MDAT atom is not done unless
* the MOOV atom hasn't been seen yet.
*/
if (!moovAtomPresent) {
skip(stream, size);
return true; // Parsing continues as MOOV atom hasn't been seen
}
return false; // Parsing done
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past MDAT atom");
}
}
/**
* MVHD is a leaf atom of size MVHD_ATOM_SIZE (100)
*/
private void parseMVHD(int size) throws BadHeaderException {
try {
if (size != MVHD_ATOM_SIZE) {
throw new BadHeaderException("mvhd atom: header size is incorrect");
}
// Skip version(1), flags(3), create time (4), mod time (4)
skip(stream, 12);
movieHeader.timeScale = readInt(stream);
int duration = readInt(stream);
movieHeader.duration = new Time((double) duration /
movieHeader.timeScale);
int preferredRate = readInt(stream);
int preferredVolume = readShort(stream);
skip(stream, 10); // Reserved
skip(stream, 36); // MATRIX
int previewTime = readInt(stream);
int previewDuration = readInt(stream);
int posterTime = readInt(stream);
int selectionTime = readInt(stream);
int selectionDuration = readInt(stream);
int currentTime = readInt(stream);
int nextTrackID = readInt(stream);
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past MVHD atom");
}
}
/**
* Required atoms are tkhd and mdia
*/
private boolean parseTRAK(int trakSize) throws BadHeaderException {
boolean mdiaAtomPresent = false;
boolean supported = false; // is trackType supported
try {
int remainingSize = trakSize;
int atomSize = readInt(stream);
String atom = readString(stream);
if ( atomSize < 8 )
throw new BadHeaderException(atom + ": Bad Atom size " + atomSize);
if ( ! atom.equals("tkhd") ) {
throw new BadHeaderException("Expected tkhd atom but got " + atom);
}
parseTKHD(atomSize - 8);
remainingSize -= atomSize;
// TODO: before calling parseXXX, should check if
// (atomSize - 8) >= remainingSize
while (remainingSize > 0) {
atomSize = readInt(stream);
atom = readString(stream);
if (atom.equals("mdia")) {
supported = parseMDIA(atomSize - 8);
mdiaAtomPresent = true;
} else if (atom.equals("tref")) {
parseTREF(atomSize - 8);
} else {
skipAtom(atom + " [atom in trak: not implemented]", atomSize - 8);
}
remainingSize -= atomSize;
}
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past TRAK atom");
}
if (!mdiaAtomPresent)
throw new BadHeaderException("mdia atom not present in trak atom container");
// FYI$$: Some files like vinton.mov have an audio track but
// do not have a stsd chunk. Apple's movie player plays only
// the video in this case. This case is now handled.
// gracefully. But there may be other cases like this.
// We need to update the if statement accordingly.
if ( supported &&
(currentTrack.media == null) ) {
supported = false;
}
return supported;
}
private void parseCTAB(int ctabSize) throws BadHeaderException { // TODO
try {
// System.out.println("ctab not handled yet");
skip(stream, ctabSize); // DUMMY
} catch (IOException e) {
//TODO
throw new BadHeaderException("....");
}
}
/**
* TKHD is a leaf atom of size TKHD_ATOM_SIZE (84)
*/
private void parseTKHD(int tkhdSize) throws BadHeaderException {
try {
if (tkhdSize != TKHD_ATOM_SIZE) {
throw new BadHeaderException("mvhd atom: header size is incorrect");
}
int iVersionPlusFlag = readInt(stream);
currentTrack.flag = iVersionPlusFlag & 0xFFFFFF;
skip(stream, 8); // Skip creation time and modification time
currentTrack.id = readInt(stream);
// System.out.println("<<<<<<<< id is >>>>>> " + currentTrack.id);
// System.out.println("<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>");
// System.out.println("<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>");
// System.out.println("<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>");
// System.out.println("<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>");
// System.out.println("<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>");
// System.out.println("<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>");
skip(stream, 4); // Skip reserved field
int duration = readInt(stream);
currentTrack.duration = new Time((double) duration /
movieHeader.timeScale);
skip(stream, tkhdSize -4 -8 -4 -4 -4); // Skip the rest of the fields
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past TKHD atom");
}
}
/**
* Required atoms are mdhd, hdlr and minf
* Doesn't say in the spec. that hdlr or minf is a required atom,
* but can we play a qt file without a hdlr or minf atoms
* hdlr atom should come before minf atom
* Return true if the trackType is supported
*
* moov/trak/mdia
*/
private boolean parseMDIA(int mdiaSize) throws BadHeaderException {
boolean hdlrAtomPresent = false;
boolean minfAtomPresent = false;;
try {
currentTrack.trackType = null;
int remainingSize = mdiaSize;
int atomSize = readInt(stream);
String atom = readString(stream);
if ( atomSize < 8 )
throw new BadHeaderException(atom + ": Bad Atom size " + atomSize);
if ( ! atom.equals("mdhd") ) {
throw new BadHeaderException("Expected mdhd atom but got " + atom);
}
parseMDHD(atomSize - 8);
remainingSize -= atomSize;
// TODO: before calling parseXXX, should check if
// (atomSize - 8) >= remainingSize
while (remainingSize > 0) {
atomSize = readInt(stream);
atom = readString(stream);
if (atom.equals("hdlr")) {
parseHDLR(atomSize - 8); // Updates trackType in currentTrack
hdlrAtomPresent = true;
} else if (atom.equals("minf")) {
if (currentTrack.trackType == null) {
throw new BadHeaderException("In MDIA atom container minf atom appears before hdlr");
}
if (currentTrack.supported) {
parseMINF(atomSize - 8);
} else {
skipAtom(atom + " [atom in mdia] as trackType " +
currentTrack.trackType + " is not supported",
atomSize - 8);
}
minfAtomPresent = true;
} else {
skipAtom(atom + " [atom in mdia: not implemented]", atomSize - 8);
}
remainingSize -= atomSize;
}
if (!hdlrAtomPresent)
throw new BadHeaderException("hdlr atom not present in mdia atom container");
if (!minfAtomPresent)
throw new BadHeaderException("minf atom not present in mdia atom container");
return (currentTrack.supported);
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past MDIA atom");
}
}
/**
* moov/trak/mdia/mdhd
*/
private void parseMDHD(int mdhdSize) throws BadHeaderException {
try {
if (mdhdSize != MDHD_ATOM_SIZE) {
throw new BadHeaderException("mdhd atom: header size is incorrect");
}
// Skip version(1), flags(3), creation time(4), modification time(4)
skip(stream, 12);
int timeScale = readInt(stream);
int duration = readInt(stream);
currentTrack.mediaDuration = new Time((double) duration / timeScale);
currentTrack.mediaTimeScale = timeScale;
skip(stream, 4); // Skip language(2) and quality(2) fields
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past MDHD atom");
}
}
/**
* moov/trak/mdia/hdlr
*/
private void parseHDLR(int hdlrSize) throws BadHeaderException {
try {
if (hdlrSize < MIN_HDLR_ATOM_SIZE) {
throw new BadHeaderException("hdlr atom: header size is incorrect");
}
// Skip version(1), flags(3), component type(4)
skip(stream, 8);
currentTrack.trackType = readString(stream);
// System.out.println("track type is " + currentTrack.trackType);
currentTrack.supported = isSupported(currentTrack.trackType);
// Skip the rest of the fields including the variable component
// name field
skip(stream, hdlrSize -8 -4);
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past HDLR atom");
}
}
// Typically HINT tracks will have tref atom specifying the track being hinted
// Typically the first child atom is the hint atom. NOTE$$: Assuming that this is the
// case.
private void parseTREF(int size) throws BadHeaderException {
try {
int childAtomSize = readInt(stream);
size -= 4;
// System.out.println("parseTREF: childAtomSize is " + childAtomSize);
String atom = readString(stream);
size -= 4;
if (atom.equalsIgnoreCase("hint")) {
currentTrack.trackIdOfTrackBeingHinted = readInt(stream);
size -= 4;
// System.out.println("trackBeingHinted is " + currentTrack.trackIdOfTrackBeingHinted);
}
skip(stream, size);
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past HDLR atom");
}
}
/**
* Required atoms are [vsg]mhd, hdlr
* Optional atoms are dinf and stbl
* Currently we skip [vsg]mhd, hdlr and dinf atoms and only
* handle stbl
*/
private void parseMINF(int minfSize) throws BadHeaderException {
boolean hdlrAtomPresent = false;
try {
int remainingSize = minfSize;
int atomSize = readInt(stream);
String atom = readString(stream);
if ( atomSize < 8 )
throw new BadHeaderException(atom + ": Bad Atom size " + atomSize);
if ( ! atom.endsWith("hd") ) {
throw new BadHeaderException("Expected media information header atom but got " + atom);
}
skipAtom(atom + " [atom in minf: not implemented]", atomSize - 8);
remainingSize -= atomSize;
// TODO: before calling parseXXX, should check if
// (atomSize - 8) >= remainingSize
while (remainingSize > 0) {
atomSize = readInt(stream);
atom = readString(stream);
if (atom.equals("hdlr")) {
skipAtom(atom + " [atom in minf: not implemented]", atomSize - 8);
hdlrAtomPresent = true;
} else if (atom.equals("dinf")) {
parseDINF(atomSize - 8);
} else if (atom.equals("stbl")) {
parseSTBL(atomSize - 8);
} else {
skipAtom(atom + " [atom in minf: not implemented]", atomSize - 8);
}
remainingSize -= atomSize;
}
if (!hdlrAtomPresent)
throw new BadHeaderException("hdlr atom not present in minf atom container");
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past MINF atom");
}
}
private void parseDINF(int dinfSize) throws BadHeaderException {
try {
int remainingSize = dinfSize;
// System.out.println("dinfSize is " + dinfSize);
while (remainingSize > 0) {
int atomSize = readInt(stream);
String atom = readString(stream);
// System.out.println("dinf: atomSize is " + atomSize);
// System.out.println("dinf: atom is " + atom);
if (atom.equals("dref")) {
parseDREF(atomSize - 8);
} else {
skipAtom(atom + " [Unknown atom in dinf]", atomSize - 8);
}
remainingSize -= atomSize;
}
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past DIMF atom");
}
}
private void parseDREF(int drefSize) throws BadHeaderException {
try {
// TODO: add size check
// if (drefSize < MIN_DREF_ATOM_SIZE) {
// throw new BadHeaderException("dref atom: header size is incorrect");
// }
skip(stream, 4); // skip version and flags
int numEntries = readInt(stream);
// System.out.println("dref: number of entries is " + numEntries);
for (int i = 0; i < numEntries; i++) {
int drefEntrySize = readInt(stream);
// System.out.println("drefEntrySize is " + drefSize);
int type = readInt(stream);
// System.out.println("dref entry type is " + type);
/**
* Version: A 1-byte specification of the version of
* these data references.
* Flags: A 3-byte space for data reference flags.
* There is one defined flag. Self reference This flag
* indicates that the media's data is in the same file
* as the movie atom. On the Macintosh, and other file
* systems with multifork files, set this flag to 1
* even if the data resides in a different fork
* from the movie atom. This flag's value is 0x0001.
*/
int versionPlusFlag = readInt(stream);
// System.out.println("versionPlusFlag is " + versionPlusFlag);
skip(stream, drefEntrySize -(4+4+4));
if ( (versionPlusFlag & DATA_SELF_REFERENCE_FLAG) <= 0 ) {
throw new BadHeaderException("Only self contained Quicktime movies are supported");
}
}
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past DREF atom");
}
}
/**
* Required atoms are none.
*/
private void parseSTBL(int stblSize) throws BadHeaderException {
try {
int remainingSize = stblSize;
while (remainingSize > 0) {
int atomSize = readInt(stream);
String atom = readString(stream);
if (atom.equals("stsd")) {
parseSTSD(atomSize - 8);
} else if (atom.equals("stts")) {
parseSTTS(atomSize - 8);
} else if (atom.equals("stss")) {
parseSTSS(atomSize - 8);
} else if (atom.equals("stsc")) {
parseSTSC(atomSize - 8);
} else if (atom.equals("stsz")) {
parseSTSZ(atomSize - 8);
} else if (atom.equals("stco")) {
parseSTCO(atomSize - 8);
} else if (atom.equals("stsh")) {
// parseSTSH(atomSize - 8);
skipAtom(atom + " [not implemented]", atomSize - 8);
} else {
skipAtom(atom + " [UNKNOWN atom in stbl: ignored]", atomSize - 8);
}
remainingSize -= atomSize;
}
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past STBL atom");
}
}
/**
* STSD is a leaf atom of minimum size MIN_STSD_ATOM_SIZE (8)
*/
private void parseSTSD(int stsdSize) throws BadHeaderException {
// System.out.println("stsd size is " + stsdSize);
try {
if (stsdSize < MIN_STSD_ATOM_SIZE) {
throw new BadHeaderException("stsd atom: header size is incorrect");
}
// Note: if the trackType is not
// supported the minf atom is skipped and so you will not
// come here.
skip(stream, 4); // skip version and flags
int numEntries = readInt(stream);
//$$ System.out.println("stsd: numEntries is " + numEntries);
if ( numEntries > 1) {
// System.err.println("Multiple formats in a track not supported");
}
for (int i = 0; i < numEntries; i++) {
int sampleDescriptionSize = readInt(stream);
//$$ System.out.println("stsd: sampleDescriptionSize is " + sampleDescriptionSize);
// CHECK ?? spec. says int but is it a 4 letter String????
String encoding = readString(stream);
// System.out.println("stsd: encoding is " + encoding);
if (i != 0) {
skip(stream, sampleDescriptionSize - 8);
continue;
}
// skip(stream, 8); // 6 reserved bytes + 2 for data reference index
skip(stream, 6); // 6 reserved bytes
// TODO: check of sampleDescriptionSize is atleast 16 bytes
if (currentTrack.trackType.equals(VIDEO)) {
currentTrack.media =
parseVideoSampleData(encoding,
sampleDescriptionSize -4 -4 -6);
} else if (currentTrack.trackType.equals(AUDIO)) {
currentTrack.media =
parseAudioSampleData(encoding,
sampleDescriptionSize -4 -4 -6);
} else if (currentTrack.trackType.equals(HINT)) {
numberOfHintTracks++;
currentTrack.media =
parseHintSampleData(encoding,
sampleDescriptionSize -4 -4 -6);
} else {
// Note: you will never come into this else block.
// If the trackType is not supported, the minf atom is skipped
// and so you will not come here.
skip(stream,
sampleDescriptionSize - 4 - 4 -6);
}
}
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past STSD atom");
}
}
private Video parseVideoSampleData(String encoding, int dataSize)
throws IOException, BadHeaderException {
// TODO: check for dataSize >= MIN_VIDEO_SAMPLE_DATA_SIZE
skip(stream, 2); // data reference index
/**
* Skip versiom(2), Revision Level (2), Vendor(4),
* Temporal Quality (4), Spatial Quality (4);
*/
skip(stream, 16);
Video video = new Video();
video.encoding = encoding;
video.width = readShort(stream);
video.height = readShort(stream);
/**
* Skip Horizontal resolution (4),
* Skip Vertical resolution (4),
* Skip data size (4),
* Skip frame count (2),
*/
skip(stream, 14);
/* Skip compressor name */
skip(stream, 32);
video.pixelDepth = readShort(stream);
video.colorTableID = readShort(stream);
int colorTableSize = 0;
if (video.colorTableID == 0) {
// Color table follows colorTableID
colorTableSize = readInt(stream);
skip(stream, colorTableSize -4); // TODO: DUMMY
}
skip(stream, dataSize - 2 - MIN_VIDEO_SAMPLE_DATA_SIZE -
- colorTableSize); // 2 for data ref. index
return video;
}
private Audio parseAudioSampleData(String encoding, int dataSize)
throws IOException, BadHeaderException {
skip(stream, 2); // data reference index
// TODO: check for dataSize >= MIN_AUDIO_SAMPLE_DATA_SIZE
/**
* Skip versiom(2), Revision Level (2), Vendor(4),
*/
skip(stream, 8);
Audio audio = new Audio();
audio.encoding = encoding;
audio.channels = readShort(stream);
audio.bitsPerSample = readShort(stream);
/**
* Skip compression id (2),
* Skip packset size (2),
*/
skip(stream, 4);
int sampleRate = readInt(stream);
/**
* The media timeScale (foound in the mdhd atom) seems to
* represent the sampleRate (because it represents units/sec)
* This sampleRate field for some reason contains the
* timeScale shifted left by 16 bits. In other words sampleRate
* is media timeScale times 65536.
* Instead of dividing this by 65536, I am just using the
* media timeScale as sampleRate
* CHECK
*/
// audio.sampleRate = sampleRate >> 16; // Also works
audio.sampleRate = currentTrack.mediaTimeScale;
// System.out.println("mediaTimeScale is " + currentTrack.mediaTimeScale);
skip(stream, dataSize -2 -MIN_AUDIO_SAMPLE_DATA_SIZE); // 2 for data ref. index
return audio;
}
private Hint parseHintSampleData(String encoding, int dataSize)
throws IOException, BadHeaderException {
// TODO: check for dataSize >= MIN_HINT_SAMPLE_DATA_SIZE
if (!encoding.equals("rtp ")) {
System.err.println("Hint track Data Format is not rtp");
}
// System.out.println("parseHintSampleData: dataSize is " + dataSize);
Hint hint = new Hint();
int dataReferenceIndex = readShort(stream);
int hintTrackVersion = readShort(stream);
if (hintTrackVersion == 0) {
System.err.println("Hint Track version #0 is not supported");
System.err.println("Use QuickTimePro to convert it to version #1");
currentTrack.supported = false;
if ((dataSize - 2 - 2) > 0)
skip(stream, (dataSize -2 -2));
return hint;
}
int lastCompatibleHintTrackVersion = readShort(stream);
int maxPacketSize = readInt(stream);
currentTrack.maxPacketSize = maxPacketSize;
int remaining = dataSize -2 -2 -2 -4;
if (debug1) {
System.out.println("dataReferenceIndex is " + dataReferenceIndex);
System.out.println("hintTrackVersion is " + hintTrackVersion);
System.out.println("lastCompatibleHintTrackVersion is " + lastCompatibleHintTrackVersion);
System.out.println("maxPacketSize is " + maxPacketSize);
System.out.println("remaining is " + remaining);
}
while (remaining > 8) {
// Additional data is present;
int entryLength = readInt(stream);
remaining -= 4;
if ( entryLength > 8) {
if (debug2)
System.out.println("entryLength is " + entryLength);
// entryLength -= 4;
String dataTag = readString(stream);
if (debug2)
System.out.println("dataTag is " + dataTag);
remaining -= 4;
// entryLength -= 4;
// TODO: assuming that the data tag is 'tims'. It can be tsro,snro,rely
if (dataTag.equals("tims")) {
// 32-bit integer specifying the RTP timescale. This entry is
// required for RTP data.
int rtpTimeScale = readInt(stream);
// System.out.println(" rtpTimeScale is " + rtpTimeScale);
// currentTrack.rtpTimeScale = dataValue;
// entryLength -= 4;
remaining -= 4;
} else if (dataTag.equals("tsro")) {
// 32-bit integer specifying the offset to add to the stored
// timestamp when sending RTP packets. If this entry is not
// present, a random offset should be used, as specified by the
// IETF. If this entry is 0, use an offset of 0 (no offset).
System.out.println("QuicktimeParser: rtp: tsro dataTag not supported");
int rtpTimeStampOffset = readInt(stream);
remaining -= 4;
} else if (dataTag.equals("snro")) {
// 32-bit integer specifying the offset to add to the sequence
// number when sending RTP packets. If this entry is not present, a
// random offset should be used, as specified by the IETF. If this
// entry is 0, use an offset of 0 (no offset).
System.out.println("QuicktimeParser: rtp: snro dataTag not supported");
int rtpSequenceNumberOffset = readInt(stream);
// System.out.println("rtpSequenceNumberOffset is " + rtpSequenceNumberOffset);
remaining -= 4;
} else if (dataTag.equals("rely")) {
// 8-bit flag indicating whether this track should or must be sent
// over a reliable transport, such as TCP/IP. If this entry is not
// present, unreliable transport should be used, such as RTP/UDP.
// The current client software for QuickTime streaming will only
// receive streaming tracks sent using RTP/UDP.
System.out.println("QuicktimeParser: rtp: rely dataTag not supported");
int rtpReliableTransportFlag = readByte(stream);
// System.out.println("rtpReliableTransportFlag is " + rtpReliableTransportFlag);
remaining--;
} else {
// Unknown flag: Error.
// TODO: handle this without skipping if possible
// May not be possible because we don't know how many bytes
// to skip before the next tag.
skip(stream, remaining);
remaining = 0;
}
} else {
skip(stream, remaining);
remaining = 0;
break;
}
}
if (remaining > 0)
skip(stream, remaining);
return hint;
}
/**
* Time to Sample atom
* STTS is a leaf atom of minimum size MIN_STTS_ATOM_SIZE (8)
*/
private void parseSTTS(int sttsSize) throws BadHeaderException {
if (debug2)
System.out.println("parseSTTS: " + sttsSize);
try {
if (sttsSize < MIN_STTS_ATOM_SIZE) {
throw new BadHeaderException("stts atom: header size is incorrect");
}
/**
* Skip versiom(1), Flags(3)
*/
skip(stream, 4);
int numEntries = readInt(stream);
if (debug2)
System.out.println("numEntries is " + numEntries);
int requiredSize = (sttsSize - MIN_STTS_ATOM_SIZE - numEntries*8);
if ( requiredSize < 0) {
throw new BadHeaderException("stts atom: inconsistent number_of_entries field");
}
int totalNumSamples = 0;
double timeScaleFactor = (1.0 / currentTrack.mediaTimeScale);
if ( numEntries == 1) {
totalNumSamples = readInt(stream);
currentTrack.durationOfSamples = readInt(stream) * timeScaleFactor;
} else {
int[] timeToSampleIndices = new int[numEntries];
double[] durations = new double[numEntries];
timeToSampleIndices[0] = readInt(stream);
totalNumSamples += timeToSampleIndices[0];
durations[0] = readInt(stream) * timeScaleFactor *
timeToSampleIndices[0];
int remaining = numEntries - 1; // As first 2 entries is already read.
// 2 ints are written in each loop
int numIntsWrittenPerLoop = 2;
// integer division
int maxEntriesPerLoop = tmpIntBufferSize / numIntsWrittenPerLoop;
int i = 1;
while (remaining > 0) {
int numEntriesPerLoop =
(remaining > maxEntriesPerLoop) ? maxEntriesPerLoop : remaining;
readBytes(stream, tmpBuffer,
numEntriesPerLoop * numIntsWrittenPerLoop * 4);
int offset = 0;
for (int ii = 1; ii <= numEntriesPerLoop; ii++, i++) {
timeToSampleIndices[i] =
parseIntFromArray(tmpBuffer, offset, true);
offset += 4;
int value = parseIntFromArray(tmpBuffer, offset, true);
offset += 4;
durations[i] += ( value * timeScaleFactor *
timeToSampleIndices[i] +
durations[i-1] );
totalNumSamples += timeToSampleIndices[i];
timeToSampleIndices[i] = totalNumSamples;
}
remaining -= numEntriesPerLoop;
}
currentTrack.timeToSampleIndices = timeToSampleIndices;
currentTrack.cumulativeDurationOfSamples = durations;
}
if (currentTrack.numberOfSamples == 0) {
currentTrack.numberOfSamples = totalNumSamples;
} else {
// TODO: if not they are inconsistent: should throw BadHeaderException
}
skip(stream, requiredSize);
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past STTS atom");
}
}
private void parseSTSC(int stscSize) throws BadHeaderException {
try {
if (stscSize < MIN_STSC_ATOM_SIZE) {
throw new BadHeaderException("stsc atom: header size is incorrect");
}
/**
* Skip versiom(1), Flags(3)
*/
skip(stream, 4);
int numEntries = readInt(stream);
int requiredSize = (stscSize - MIN_STSC_ATOM_SIZE - numEntries*12);
if ( requiredSize < 0) {
throw new BadHeaderException("stsc atom: inconsistent number_of_entries field");
}
/**
* At this point we don't know how many chunks there are
* and so we cannot compute the samplePerChunk array
* TODO: make use of the sampleDescriptionId field
*/
int compactSamplesChunkNum[] = new int[numEntries];
int compactSamplesPerChunk[] = new int[numEntries];
byte[] tmpBuf = new byte[numEntries*4*3];
readBytes(stream, tmpBuf, numEntries*4*3);
int offset = 0;
for (int i = 0; i < numEntries; i++) {
compactSamplesChunkNum[i] = parseIntFromArray(tmpBuf, offset, true);
offset += 4;
compactSamplesPerChunk[i] = parseIntFromArray(tmpBuf, offset, true);
offset += 4;
// int sampleDescriptionId = readInt(stream);
offset += 4; // skip next 4 bytes
}
tmpBuf = null;
currentTrack.compactSamplesChunkNum = compactSamplesChunkNum;
currentTrack.compactSamplesPerChunk = compactSamplesPerChunk;
skip(stream, requiredSize);
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past STSC atom");
}
}
/**
* Sample Size Atom
* STSZ is a leaf atom of minimum size MIN_STSZ_ATOM_SIZE (8)
*/
private void parseSTSZ(int stszSize) throws BadHeaderException {
if (debug2)
System.out.println("parseSTSZ: " + stszSize);
try {
if (stszSize < MIN_STSZ_ATOM_SIZE) {
throw new BadHeaderException("stsz atom: header size is incorrect");
}
/**
* Skip versiom(1), Flags(3)
*/
skip(stream, 4);
currentTrack.sampleSize = readInt(stream);
if (currentTrack.sampleSize != 0) {
// All samples are of same sample size
skip(stream, stszSize - MIN_STSZ_ATOM_SIZE);
currentTrack.media.maxSampleSize = currentTrack.sampleSize;
return;
}
// All samples are not of same size
if ( (stszSize - MIN_STSZ_ATOM_SIZE) < 4) { // for numEntries
throw new BadHeaderException("stsz atom: incorrect atom size");
}
int numEntries = readInt(stream);
if (currentTrack.numberOfSamples == 0) {
currentTrack.numberOfSamples = numEntries; // TODO: ????
} else {
// TODO: if not they are inconsistent: should throw BadHeaderException
}
int requiredSize = (stszSize - MIN_STSZ_ATOM_SIZE
- 4 // for numEntries
- numEntries*4);
if ( requiredSize < 0) {
throw new BadHeaderException("stsz atom: inconsistent number_of_entries field");
}
int[] sampleSizeArray = new int[numEntries];
int maxSampleSize = Integer.MIN_VALUE;
int value;
int remaining = numEntries;
// 1 int is written in each loop
int numIntsWrittenPerLoop = 1;
int maxEntriesPerLoop = tmpIntBufferSize / numIntsWrittenPerLoop;
int i = 0;
while (remaining > 0) {
int numEntriesPerLoop =
(remaining > maxEntriesPerLoop) ? maxEntriesPerLoop : remaining;
readBytes(stream, tmpBuffer,
numEntriesPerLoop * numIntsWrittenPerLoop * 4);
int offset = 0;
for (int ii = 1; ii <= numEntriesPerLoop; ii++, i++) {
value = parseIntFromArray(tmpBuffer, offset, true);
offset += 4;
if (value > maxSampleSize)
maxSampleSize = value;
sampleSizeArray[i] = value;
}
remaining -= numEntriesPerLoop;
}
currentTrack.sampleSizeArray = sampleSizeArray;
currentTrack.media.maxSampleSize = maxSampleSize;
skip(stream, requiredSize);
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past STSZ atom");
}
}
/**
* Chunk offset atom
* STCO is a leaf atom of minimum size MIN_STCO_ATOM_SIZE (8)
*/
private void parseSTCO(int stcoSize) throws BadHeaderException {
if (debug2)
System.out.println("rtp:parseSTCO: " + stcoSize);
try {
if (stcoSize < MIN_STCO_ATOM_SIZE) {
throw new BadHeaderException("stco atom: header size is incorrect");
}
/**
* Skip versiom(1), Flags(3)
*/
skip(stream, 4);
// numEntries should be equal to number of Chunks
int numEntries = readInt(stream);
currentTrack.numberOfChunks = numEntries;
int[] chunkOffsets = new int[numEntries];
int requiredSize = (stcoSize - MIN_STCO_ATOM_SIZE - numEntries*4);
if ( requiredSize < 0) {
throw new BadHeaderException("stco atom: inconsistent number_of_entries field");
}
int remaining = numEntries;
// 1 int is written in each loop
int numIntsWrittenPerLoop = 1;
int maxEntriesPerLoop = tmpIntBufferSize / numIntsWrittenPerLoop;
int i = 0;
while (remaining > 0) {
int numEntriesPerLoop =
(remaining > maxEntriesPerLoop) ? maxEntriesPerLoop : remaining;
readBytes(stream, tmpBuffer,
numEntriesPerLoop * numIntsWrittenPerLoop * 4);
int offset = 0;
for (int ii = 1; ii <= numEntriesPerLoop; ii++, i++) {
chunkOffsets[i] = parseIntFromArray(tmpBuffer, offset, true);
offset += 4;
}
remaining -= numEntriesPerLoop;
}
currentTrack.chunkOffsets = chunkOffsets;
skip(stream, requiredSize);
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past STCO atom");
}
}
/**
* Sync sample atom
* STSS is a leaf atom of minimum size MIN_STSS_ATOM_SIZE (8)
*/
private void parseSTSS(int stssSize) throws BadHeaderException {
try {
if (stssSize < MIN_STSS_ATOM_SIZE) {
throw new BadHeaderException("stss atom: header size is incorrect");
}
/**
* Skip versiom(1), Flags(3)
*/
skip(stream, 4);
int numEntries = readInt(stream);
int requiredSize = (stssSize - MIN_STSS_ATOM_SIZE - numEntries*4);
if ( requiredSize < 0) {
throw new BadHeaderException("stss atom: inconsistent number_of_entries field");
}
if (numEntries < 1) {
skip(stream, requiredSize);
return;
}
int[] syncSamples = new int[numEntries];
int remaining = numEntries;
// 1 int is written in each loop
int numIntsWrittenPerLoop = 1;
int maxEntriesPerLoop = tmpIntBufferSize / numIntsWrittenPerLoop;
int i = 0;
while (remaining > 0) {
int numEntriesPerLoop =
(remaining > maxEntriesPerLoop) ? maxEntriesPerLoop : remaining;
readBytes(stream, tmpBuffer,
numEntriesPerLoop * numIntsWrittenPerLoop * 4);
int offset = 0;
for (int ii = 1; ii <= numEntriesPerLoop; ii++, i++) {
syncSamples[i] = parseIntFromArray(tmpBuffer, offset, true);
offset += 4;
}
remaining -= numEntriesPerLoop;
}
currentTrack.syncSamples = syncSamples;
skip(stream, requiredSize);
} catch (IOException e) {
throw new BadHeaderException("Got IOException when seeking past STSS atom");
}
}
private boolean isSupported(String trackType) {
if (enableHintTrackSupport) {
return ( trackType.equals(VIDEO) ||
trackType.equals(AUDIO) ||
trackType.equals(HINT)
);
} else {
return ( trackType.equals(VIDEO) ||
trackType.equals(AUDIO)
);
}
}
private class MovieHeader {
int timeScale;
// Duration of the longest track in the movie
Time duration = Duration.DURATION_UNKNOWN;
// TODO: remove mdatStart as it is not necessary
long mdatStart;
long mdatSize;
}
private abstract class Media {
// Obtained from moov/trak/mdia/minf/stbl/stsd
String encoding;
int maxSampleSize;
abstract Format createFormat();
float frameRate; // $$$ KLUGE moved from Video sub-class
}
private class Audio extends Media {
// Obtained from moov/trak/mdia/minf/stbl/stsd
int channels; // Number of channels
// Number if bits in each uncompressed sound sample.
int bitsPerSample;
int sampleRate;
AudioFormat format = null;
int frameSizeInBits;
int samplesPerBlock = 1;
public String toString() {
String info;
info = ("Audio: " + format + "\n");
info += ("encoding is " + encoding + "\n");
info += ("Number of channels " + channels + "\n");
info += ("Bits per sample " + bitsPerSample + "\n");
info += ("sampleRate " + sampleRate + "\n");
return info;
}
Format createFormat() {
if (format != null)
return format;
String encodingString = null;
boolean signed = true;
boolean bigEndian = true;
if ( encoding.equals("ulaw") || encoding.equals("alaw") ) {
// For ulaw and alaw, it is always 8 bits per sample
bitsPerSample = 8;
}
// TODO: Calculate for the different encodings
// This is used by the codec (eg wav block alogn)
frameSizeInBits = channels * bitsPerSample;
if ( encoding.equals("ulaw") ) {
encodingString = AudioFormat.ULAW;
signed = false;
} else if ( encoding.equals("alaw") ) {
encodingString = AudioFormat.ALAW;
signed = false;
} else if (encoding.equals("twos")) {
/**
* 'twos' Samples are stored uncompressed,
* in twos-complement format (sample values range from
* -128 to 127 for 8-bit audio, and -32768 to 32767
* for 16 bit audio; 0 is always silence
*/
encodingString = AudioFormat.LINEAR;
} else if (encoding.equals("ima4")) {
encodingString = AudioFormat.IMA4;
/**
* Each packet contains 64 samples. Each sample is 4 bits/channel.
* So 64 samples is 32 bytes/channel.
* The 2 in the equation refers two bytes that the Apple's
* IMA compressor puts at the front of each packet, which
* are referred to as predictor bytes
*/
samplesPerBlock = 64;
frameSizeInBits = (32 + 2) * channels * 8;
} else if (encoding.equals("raw ")) {
/**
* 'raw ' Samples are stored uncompressed in
* offset-binary format (values range from 0 to 255;
* 128 is silence
* JavaSound handles this directly. i.e no conversion
* is necessary
*/
encodingString = AudioFormat.LINEAR;
signed = false;
// System.out.println("raw: format: signed " + signed);
} else if (encoding.equals("agsm")) {
encodingString = AudioFormat.GSM;
/**
* Each frame that consists of 160 speech samples
* requires 33 bytes
*/
samplesPerBlock = 33;
frameSizeInBits = 33 * 8;
} else if (encoding.equals("mac3")) {
encodingString = AudioFormat.MAC3;
} else if (encoding.equals("mac6")) {
encodingString = AudioFormat.MAC6;
} else {
// NOTE: We should try to map encoding to an
// encodingString constant defined in
// AudioFormat
// System.err.println("WARNING: No mapping done for encoding " + encoding +
// "to an encoding string in AudioFormat");
encodingString = encoding;
}
// TODO: put correct values here
// TODO: See if you need to change bitsPerSample,
// bitsPerSample is given in number of bits in
// the uncompressed sound sample. So for ima4
// it will be 16. See if you need to set it to 4
// when creating AudioFormat
format = new AudioFormat(encodingString,
sampleRate,
bitsPerSample,
channels,
bigEndian ? AudioFormat.BIG_ENDIAN : AudioFormat.LITTLE_ENDIAN,
signed ? AudioFormat.SIGNED : AudioFormat.UNSIGNED,
frameSizeInBits,
Format.NOT_SPECIFIED, // No FRAME_RATE specified
Format.byteArray);
return format;
}
}
private class Video extends Media {
// Obtained from moov/trak/mdia/minf/stbl/stsd
int width;
int height;
int pixelDepth;
int colorTableID;
VideoFormat format;
// float frameRate; $$$ KLUGE moved to base class
Format createFormat() {
if (format != null)
return format;
// TODO: map the encodingString properly to the Strings in
// VideoFormat
// System.out.println("Video: Frame rate is " + frameRate);
/**
* Uncompressed RGB
* Uncompressed RGB data is stored in a variety of different
* formats. The format used depends on the Depth field of the video
* sample description. For all depths, the image data is padded on
* each scan line to ensure that each scan line begins on an even
* byte boundary.
* For depths of 1, 2, 4, and 8, the values stored are indexes
* into the color table specified in the Color table id field.
* For a depth of 16, the pixels are stored as 5-5-5 RGB values
* with the high bit of each 16-bit integer set to 0.
* For a depth of 24, the pixels are stored packed together
* in RGB order. For a depth of 32, the pixels are stored with an
* 8-bit alpha channel, followed by 8-bit RGB components.
*/
if ( encoding.toLowerCase().startsWith("raw") ) {
encoding = "rgb";
if (pixelDepth == 24) {
format = new RGBFormat(new java.awt.Dimension(width, height),
Format.NOT_SPECIFIED, // maxDataLength
Format.byteArray,
frameRate,
pixelDepth,
1, 2, 3, // RGB order
3, // pixelStride
width*3, // lineStride
Format.FALSE, // Flipped
RGBFormat.BIG_ENDIAN);
} else if (pixelDepth == 16) {
format = new RGBFormat(new java.awt.Dimension(width, height),
Format.NOT_SPECIFIED, // maxDataLength
Format.byteArray,
frameRate,
pixelDepth,
0x7C00, // red mask
0x03E0, // green mask
0x001F, // blue mask
2, // pixelStride
width*2, // lineStride
Format.FALSE, // Flipped
RGBFormat.BIG_ENDIAN);
} else if (pixelDepth == 32) {
encoding = "rgb";
// This is for pixel depth 32
format = new RGBFormat(new java.awt.Dimension(width, height),
Format.NOT_SPECIFIED, // maxDataLength
Format.byteArray,
frameRate,
pixelDepth,
2, 3, 4, // ARGB
4, // pixelStride
width*4, // lineStride
Format.FALSE, // Flipped
RGBFormat.BIG_ENDIAN);
}
} else if ( encoding.toLowerCase().equals("8bps") ) {
// Not supported
// This is a 32 bit format with alpha, red, green and blue.
// Appears to be some type of run length encoding compression.
// No info on this format available.
// Will try to support it if there is a demand from the users.
format = new VideoFormat(encoding,
new java.awt.Dimension(width, height),
maxSampleSize, Format.byteArray,
frameRate);
} else if (encoding.toLowerCase().equals("yuv2")) {
// Component Video: Interleaved YUV 4:2:2
// YUV 4:2:2 interleaved format. The components are ordered as
// Y1, U, Y2, V.
// offset-binary format
format = new YUVFormat(new java.awt.Dimension(width, height),
Format.NOT_SPECIFIED, // maxDataLength
Format.byteArray,
frameRate,
(YUVFormat.YUV_YUYV | YUVFormat.YUV_SIGNED),
width*2, // StrideY
width*2, // StrideUV
0, // offset Y
1, // offset U
3); // offset V
} else {
format = new VideoFormat(encoding,
new java.awt.Dimension(width, height),
maxSampleSize, Format.byteArray,
frameRate);
}
return format;
}
public String toString() {
String info;
info = ("Video: " + format + "\n");
info += ("encoding is " + encoding + "\n");
// info += ("width is " + width + "\n");
// info += ("height is " + height + "\n");
info += ("pixelDepth is " + pixelDepth + "\n");
// info += ("colorTableID is " + colorTableID + "\n");
return info;
}
}
private class Hint extends Media {
Format format = null;
Format createFormat() {
return format;
}
}
// The layout of a track header atom.
//
// Field Descriptions
// Version: A 1-byte specification of the version this track header.
// Track header flags: Three bytes that are reserved for the track
// header flags indicate how the track is used in the movie.
// The following flags are valid(all flags are enable when
// set to 1).
// Track enabled: indicates that the track is enabled. Flag
// value is 0x0001.
// Track in movie: indicates that the track is used in the
// movie. Flag value is 0x0002.
// Track in Preview: indicates that the track is used in the
// movie's preview. Flag value is 0x0004.
// Track in poster: indicates that the track is used in the
// movie's poster. Flag value is 0x0008.
// Creation time: A 32-bit integer that indicates (in seconds) when
// the track header was created.
// Modification time: A 32-bit integer that indicates (in seconds) when
// the track header was changed.
// TrackID: A 32-bit integer that uniquely identifies the track. A value
// of 0 must never be used for a trackID.
// Duration: A time value that indicates the duration of this track.
// Note: this property is derived form the durations of all the
// track's edits.
// Layer: A 16 bits integer that indicates this track's spatial priority
// in its movie. The QuickTime Movie Toolbox uses this value to
// to determine how tracks overlay one another. Tracks with lower
// layer values are displayed in front of the tracks with higher
// layer values.
// Alternative group: A 16 bit integer that specifies a collection of movie
// data for one another. QuickTime chooses one track from the group // to be used when the movie is played. The choice may be based.
// on such considerations as playback quality or language and the
// capabilities of the computer.
// Volume: A 16 bit fixed point value that indicates how loudly this track
// sound is to be played. A value of 1.0 indicates normal volume.
// Matrix: The matrix structure associated with this track.
// Track Width: A 32-bit-fixed point number that specifies the width of
// this track in pixels.
// Track height: A 32-bit-fixed point number that specifies the height of
// this track in pixels.
private class TrakList {
// Obtained from moov/trak/tkhd atom
int flag;
int id;
// Duration: A time value that indicates the duration of this
// track (in the movie's time coordinate system). Note that this
// property is derived from the track's edits. The value of this
// field is equal to the sum of the durations of all of the
// track's edits.
Time duration = Duration.DURATION_UNKNOWN;
// Obtained from moov/trak/mdia/mdhd
int mediaTimeScale;
Time mediaDuration = Duration.DURATION_UNKNOWN;
// Obtained from moov/trak/mdia/hdlr
String trackType;
int numberOfSamples; // Does it need to be long
// Obtained from moov/trak/mdia/minf/stbl/stsz
int sampleSize = 0; // If all samples are of the same size
int[] sampleSizeArray;
// From the implementation
boolean supported; // Is this track type supported
// TODO: In future, this may become an array of Media, if multiple
// formats per track are supported.
// Obtained from moov/trak/mdia/minf/stbl/stsd
Media media; // Info specific to each track type.
// Obtained from moov/trak/mdia/minf/stbl/stco
int numberOfChunks;
int chunkOffsets[] = new int[0];
// Obtained from moov/trak/mdia/minf/stbl/stsc
int compactSamplesChunkNum[] = new int[0];
int compactSamplesPerChunk[] = new int[0];
// Computed from above 2
// set if all chunks have same # of samples
int constantSamplesPerChunk = -1;
// TODO: change this to cumSamplesPerChunk
int samplesPerChunk[];
// Obtained from moov/trak/mdia/minf/stbl/stts
double durationOfSamples = -1.0;
// TODO: change timeToSampleIndices to cumulativeTimeToSampleIndices
int timeToSampleIndices[] = new int[0];
double cumulativeDurationOfSamples[] = new double[0];
double startTimeOfSampleArray[] = new double[0]; // Not used by AUDIO
double durationOfSampleArray[] = new double[0]; // Not used by AUDIO
// TODO: can this be int instead of long
long[] sampleOffsetTable;
int[] syncSamples;
int[] syncSampleMapping;
TimeAndDuration timeAndDuration = new TimeAndDuration();
int trackIdOfTrackBeingHinted = -1;
int indexOfTrackBeingHinted = -1;
int maxPacketSize = -1;
void buildSamplePerChunkTable() {
int i,j;
if (numberOfChunks <= 0)
return;
if (compactSamplesPerChunk.length == 1) {
constantSamplesPerChunk = compactSamplesPerChunk[0];
// System.out.println("constantSamplesPerChunk is " + constantSamplesPerChunk);
return;
}
samplesPerChunk = new int[numberOfChunks];
i = 1;
for (j = 0; j < compactSamplesChunkNum.length -1; j++) {
int numSamples = compactSamplesPerChunk[j];
// System.out.println("numSamples is " + numSamples);
while ( i != compactSamplesChunkNum[j+1]) {
samplesPerChunk[i-1] = numSamples;
i++;
}
}
for (; i <= numberOfChunks; i++) {
samplesPerChunk[i-1] = compactSamplesPerChunk[j];
}
}
void buildCumulativeSamplePerChunkTable() {
// Calculate cumulative samples per chunk, unless
// all chunks have the same number of samples
// System.out.println("buildCumulativeSamplePerChunkTable: constantSamplesPerChunk " +
// constantSamplesPerChunk);
if ( constantSamplesPerChunk == -1) {
for (int i = 1; i < numberOfChunks; i++) {
samplesPerChunk[i] += samplesPerChunk[i-1];
// System.out.println("cum samplesPerChunk: " + samplesPerChunk[i]);
}
}
}
// Used by video track only as the number of samples in
// audio is generally quite large
void buildSampleOffsetTable() {
sampleOffsetTable = new long[numberOfSamples];
int index = 0;
long offset;
int i, j;
if (sampleSize != 0) {
// All samples are of same size
if (constantSamplesPerChunk != -1) {
for (i = 0; i < numberOfChunks; i++) {
offset = chunkOffsets[i];
for (j = 0; j < constantSamplesPerChunk; j++) {
sampleOffsetTable[index++] = offset + (j * sampleSize);
}
}
} else {
for (i = 0; i < numberOfChunks; i++) {
offset = chunkOffsets[i];
for (j = 0; j < samplesPerChunk[i]; j++) {
sampleOffsetTable[index++] = offset + (j * sampleSize);
}
}
}
} else {
int numSamplesInChunk = 0; // initialize to keep compiler happy
if (constantSamplesPerChunk != -1)
numSamplesInChunk = constantSamplesPerChunk;
for (i = 0; i < numberOfChunks; i++) {
offset = chunkOffsets[i];
// Handle first sample in each chunk
sampleOffsetTable[index] = offset;
index++;
if (constantSamplesPerChunk == -1)
numSamplesInChunk = samplesPerChunk[i];
for (j = 1; j < numSamplesInChunk; j++) {
sampleOffsetTable[index] = sampleOffsetTable[index-1] +
sampleSizeArray[index-1];
index++;
}
}
}
}
boolean buildSyncTable() {
if (syncSamples == null)
return false;
/** CHECK: I don't know of any audio encoding in QuickTime
* in which not all frames are key frames. If they exist then we
* don't want to build the
* syncSampleMapping array because it is as big as the number of
* samples. The number of samples in audio is generally quite
* large. Also, it complicates things like synchronization
* if both the video track and audio track have key frames.
* This is because the sample that corresponds to a time
* may not be a key frame in both audio and video tracks
* and the nearest keyframe in both audio and video may
* correspond to different times.
* Note that we have the same problem if there are 2 or more
* video tracks with keyframes.
*/
if (!trackType.equals(VIDEO))
return false;
int numEntries = syncSamples.length;
if (numEntries == numberOfSamples) {
// Every frame is a key frame. Ignoring sync table atom
// System.out.println("Every frame is a key frame. Ignoring sync table atom");
syncSamples = null;
return false;
}
syncSampleMapping = new int[numberOfSamples];
int index = 0;
int previous;
if (syncSamples[0] != 1) {
// Bug in the sync table of the QuickTime file
// The first sample should always be a key frame
previous = syncSampleMapping[0] = 0;
} else {
previous = syncSampleMapping[0] = 0;
index++;
}
for (; index < syncSamples.length; index++) {
int next = syncSamples[index] - 1;
syncSampleMapping[next] = next;
int range = next - previous - 1;
for (int j = previous+1; j < next; j++) {
// Return the closest keyframe
// if ((float)(j - previous)/range <= 0.5)
// syncSampleMapping[j] = previous;
// else
// syncSampleMapping[j] = next;
// Return the previous keyframe
syncSampleMapping[j] = previous;
}
previous = next;
}
int lastSyncFrame = syncSamples[syncSamples.length - 1] -1;
for (index = lastSyncFrame+1; index < numberOfSamples; index++) {
syncSampleMapping[index] = lastSyncFrame;
}
return true; // syncSampleMapping table has been built
}
int time2Index(double time) {
// TODO: do bounds checking
// Note: In all places that call time2Index,
// time parameter is set to 0 if it is < 0
if (time < 0)
time = 0;
int length = timeToSampleIndices.length;
int sampleIndex;
// TODO: do bounds checking
if (length == 0) {
sampleIndex = (int)
((time / mediaDuration.getSeconds()) * numberOfSamples + 0.5);
if (sampleIndex >= numberOfSamples)
return -1; // PAST EOM // sampleIndex = numberOfSamples -1;
return sampleIndex;
}
// Note: length will always be atleast 2
int foundIndex;
int approxLocation = (int)
((time / mediaDuration.getSeconds()) * length);
if (approxLocation == length) // TODO: check to see if you need this
approxLocation--;
if (approxLocation >= cumulativeDurationOfSamples.length) {
return -1; // PAST EOM
}
int i;
if (cumulativeDurationOfSamples[approxLocation] < time) {
// increment = 1;
for (i = approxLocation+1; i < length; i++) {
if (cumulativeDurationOfSamples[i] >= time) {
break;
}
}
foundIndex = i;
} else if (cumulativeDurationOfSamples[approxLocation] > time) {
// increment = -1;
for (i = approxLocation-1; i >= 0; i--) {
if (cumulativeDurationOfSamples[i] < time) {
break;
}
}
foundIndex = i+1;
} else {
foundIndex = approxLocation;
}
if (foundIndex == length)
foundIndex--;
double delta = cumulativeDurationOfSamples[foundIndex] - time;
int samples;
double duration;
if (foundIndex == 0) {
sampleIndex = timeToSampleIndices[foundIndex];
samples = sampleIndex;
duration = cumulativeDurationOfSamples[foundIndex];
} else {
sampleIndex = timeToSampleIndices[foundIndex];
samples = sampleIndex -
timeToSampleIndices[foundIndex-1];
duration = cumulativeDurationOfSamples[foundIndex] -
cumulativeDurationOfSamples[foundIndex-1];
}
double fraction = delta / duration;
sampleIndex -= (samples * fraction);
return sampleIndex;
}
// TODO: make time and duration in nanoseconds rather than seconds.
TimeAndDuration index2TimeAndDuration(int index) {
double startTime = 0.;
double duration = 0.;
try {
if (index < 0)
index = 0;
else if (index >= numberOfSamples) {
index = numberOfSamples -1;
}
int length = timeToSampleIndices.length;
if (length == 0) {
// All samples have the same duration
duration = durationOfSamples;
startTime = duration * index;
} else if (startTimeOfSampleArray.length >= index) {
duration = durationOfSampleArray[index];
startTime = startTimeOfSampleArray[index];
} else {
// TODO: compute this only once
float factor = (float) length / numberOfSamples;
int location = (int) (index * factor);
duration = 0; // DUMMY TODO
startTime = 0; // DUMMY TODO
}
} finally {
synchronized(timeAndDuration) {
timeAndDuration.startTime = startTime;
timeAndDuration.duration = duration;
return timeAndDuration;
}
}
}
// TODO: Looks like this wants index starting from 1 not 0
// May want to change it so that the index starts from 0
int index2Chunk(int index) {
int chunk;
if ( constantSamplesPerChunk != -1) {
chunk = index / constantSamplesPerChunk;
return chunk;
}
int length = samplesPerChunk.length;
int approxChunk = (int)((float) (index / numberOfSamples) * length);
if (approxChunk == length)
approxChunk--;
int i;
if (samplesPerChunk[approxChunk] < index) {
// increment = 1;
for (i = approxChunk+1; i < length; i++) {
if (samplesPerChunk[i] >= index) {
break;
}
}
chunk = i;
} else if (samplesPerChunk[approxChunk] > index) {
// increment = -1;
for (i = approxChunk-1; i >= 0; i--) {
if (samplesPerChunk[i] < index) {
break;
}
}
chunk = i+1;
} else {
chunk = approxChunk;
}
return chunk;
}
// NOTE: $$$ May compute wrong info for audio because even though
// the spec. says that sampleSize in stsz atom represents
// size in bytes of the sample, I always get 1 even for
// 16 bit linear stereo files.
long index2Offset(int index) {
// int chunk = index2Chunk(index);
// System.out.println("OLD chunk value " + chunk);
int chunk = index2Chunk(index+1);
if (debug)
System.out.println(" index2Chunk chunk is " + chunk);
if (chunk >= chunkOffsets.length) {
// At or beyond EOM
// TODO: Can use a better constant
return com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD;
}
long offset = chunkOffsets[chunk];
if (/*true*/debug1) {
System.out.println("index2Offset: index, chunk, chunkOffset " +
index + " : " + chunk + " : " + offset);
// Remove the debug if and print $$$
// if (constantSamplesPerChunk == -1) {
// System.out.println("samples in chunk " + chunk + " ==> " +
// samplesPerChunk[chunk]);
// }
}
int sampleNumInChunk;
int start;
// System.out.println(" constantSamplesPerChunk is " + constantSamplesPerChunk);
if ( constantSamplesPerChunk != -1) {
sampleNumInChunk = (index % constantSamplesPerChunk);
start = chunk * constantSamplesPerChunk;
} else {
if (chunk == 0)
start = 0;
else
start = samplesPerChunk[chunk -1];
sampleNumInChunk = index - start;
if (/*true*/debug1) {
System.out.println("index, start, sampleNumInChunk " +
index + " : " + start + " : " +
sampleNumInChunk);
System.out.println("sampleSize is " + sampleSize);
}
}
if (debug1)
System.out.println("sampleSize is " + sampleSize);
if (sampleSize != 0) {
// All the samples are of the same size
offset += (sampleSize * sampleNumInChunk);
} else {
for (int i = 0; i < sampleNumInChunk; i++)
offset += sampleSizeArray[start++];
}
return offset;
}
// Used for VIDEO. Not for AUDIO as the number of samples
// is generally quite large.
void buildStartTimeAndDurationTable() {
if (debug2) {
System.out.println("buildStartTimeAndDurationTable"); //$$$
}
int length = timeToSampleIndices.length;
// No need for table as all samples are of the same duration
if (length == 0)
return;
startTimeOfSampleArray = new double[numberOfSamples];
durationOfSampleArray = new double[numberOfSamples];
int previousSamples = 0;
double previousDuration = 0.0;
double time = 0.;
int index = 0;
for (int i = 0; i < length; i++) {
int numSamples = timeToSampleIndices[i];
double duration = (cumulativeDurationOfSamples[i] -
previousDuration)/(numSamples - previousSamples);
// if (debug2)
// System.out.println("duration is " + duration); //$$$
for (int j = 0; j < (numSamples - previousSamples); j++) {
startTimeOfSampleArray[index] = time;
durationOfSampleArray[index] = duration;
index++;
time += duration;
}
previousSamples = numSamples;
previousDuration = cumulativeDurationOfSamples[i];
}
}
public String toString() {
String info = "";
info += ("track id is " + id + "\n");
info += ("duration itrack is " + duration.getSeconds() + "\n");
info += ("duration of media is " + mediaDuration.getSeconds() + "\n");
info += ("trackType is " + trackType + "\n");
// info += ("numberOfSamples is " + numberOfSamples + "\n");
// info += ("sampleSize is " + sampleSize + "\n");
// info += ("maxSampleSize is " + media.maxSampleSize + "\n");
// info += ("numberOfChunks is " + numberOfChunks + "\n");
info += media;
return info;
}
}
// TODO extend BasicTrack if possible
private abstract class MediaTrack implements Track {
TrakList trakInfo;
boolean enabled = true;
int numBuffers = 4; // TODO: check
Format format;
long sequenceNumber = 0;
int chunkNumber = 0;
int sampleIndex = 0;
int useChunkNumber = 0;
int useSampleIndex = 0;
QuicktimeParser parser = QuicktimeParser.this;
CachedStream cacheStream = parser.getCacheStream();
int constantSamplesPerChunk;
int[] samplesPerChunk;
protected TrackListener listener;
MediaTrack(TrakList trakInfo) {
this.trakInfo = trakInfo;
if (trakInfo != null) {
enabled = ( (trakInfo.flag & TRACK_ENABLED) != 0);
format = trakInfo.media.createFormat();
samplesPerChunk = trakInfo.samplesPerChunk;
constantSamplesPerChunk = trakInfo.constantSamplesPerChunk;
}
}
// TODO: create a list of TrackListeners
public void setTrackListener(TrackListener l) {
listener = l;
}
public Format getFormat() {
return format;
}
public void setEnabled(boolean t) {
enabled = t;
}
public boolean isEnabled() {
return enabled;
}
public Time getDuration() {
return trakInfo.duration;
}
public Time getStartTime() {
return new Time(0); // TODO
}
synchronized void setSampleIndex(int index) {
sampleIndex = index;
}
synchronized void setChunkNumber(int number) {
chunkNumber = number;
}
public void readFrame(Buffer buffer) {
if (buffer == null)
return;
if (!enabled) {
buffer.setDiscard(true);
return;
}
synchronized (this) {
useChunkNumber = chunkNumber;
useSampleIndex = sampleIndex;
}
// TODO: handle chunkNumber < 0 case differently
// System.out.println("useChunkNumber: numchunks " +
// useChunkNumber + " : " +
// trakInfo.numberOfChunks);
if ( (useChunkNumber >= trakInfo.numberOfChunks) ||
(useChunkNumber < 0 ) ) {
buffer.setEOM(true);
return;
}
buffer.setFormat(format); // Need to do this every time ???
doReadFrame(buffer);
}
abstract void doReadFrame(Buffer buffer);
public int mapTimeToFrame(Time t) {
return FRAME_UNKNOWN;
}
public Time mapFrameToTime(int frameNumber) {
return TIME_UNKNOWN;
}
}
private class AudioTrack extends MediaTrack {
String encoding;
int channels;
int sampleOffsetInChunk = -1;
int useSampleOffsetInChunk = 0;
int frameSizeInBytes;
int samplesPerBlock;
int sampleRate;
AudioTrack(TrakList trakInfo,
int channels,
String encoding,
int frameSizeInBytes,
int samplesPerBlock,
int sampleRate) {
super(trakInfo);
this.channels = channels;
this.encoding = encoding;
this.frameSizeInBytes = frameSizeInBytes;
this.samplesPerBlock = samplesPerBlock;
this.sampleRate = sampleRate;
}
AudioTrack(TrakList trakInfo) {
super(trakInfo);
if (trakInfo != null) { // remove this if $$$$$
channels = ((Audio)trakInfo.media).channels;
encoding = trakInfo.media.encoding;
frameSizeInBytes = ((Audio)trakInfo.media).frameSizeInBits / 8;
samplesPerBlock = ((Audio)trakInfo.media).samplesPerBlock;
sampleRate = ((Audio)trakInfo.media).sampleRate;
}
}
synchronized void setChunkNumberAndSampleOffset(int number, int offset) {
chunkNumber = number;
sampleOffsetInChunk = offset;
}
void doReadFrame(Buffer buffer) {
int samples;
// if (debug1) {
// System.out.println("audio: doReadFrame: " + useChunkNumber +
// " : " + sampleOffsetInChunk);
// }
synchronized (this) {
if (sampleOffsetInChunk == -1) {
useSampleOffsetInChunk = 0;
} else {
useSampleOffsetInChunk = sampleOffsetInChunk;
sampleOffsetInChunk = -1; // Reset sampleOffsetInChunk
}
}
// TODO: compute this properly for all encodings.
// TODO: compute a multiply factor once and use it
// to multiply samplesPerChunk[chunkNumber]
long samplesPlayed;
if (constantSamplesPerChunk != -1) {
samples = constantSamplesPerChunk;
samplesPlayed = constantSamplesPerChunk * useChunkNumber;
} else if (useChunkNumber > 0) {
samples = samplesPerChunk[useChunkNumber] -
samplesPerChunk[useChunkNumber - 1];
samplesPlayed = samplesPerChunk[useChunkNumber];
} else {
samples = samplesPerChunk[useChunkNumber];
samplesPlayed = 0;
}
int byteOffsetFromSampleOffset;
if (samplesPerBlock > 1) {
int skipBlocks = useSampleOffsetInChunk / samplesPerBlock; // Integer division
useSampleOffsetInChunk = skipBlocks * samplesPerBlock;
byteOffsetFromSampleOffset = frameSizeInBytes * skipBlocks;
} else {
byteOffsetFromSampleOffset = useSampleOffsetInChunk * frameSizeInBytes;
}
samples -= useSampleOffsetInChunk;
samplesPlayed += useSampleOffsetInChunk; // TODO $$$ CHECK
// System.out.println("samples is " + samples);
// System.out.println("samplesPlayed is " + samplesPlayed);
// System.out.println("time is " + ((double) samplesPlayed / sampleRate));
int needBufferSize;
// TODO: See if you can build an array of size numChunks
// that holds the computed needBufferSize, so that we don't
// have to compute it each time
if (encoding.equals("ima4")) {
/**
* Each packet contains 64 samples. Each sample is 4 bits/channel.
* So 64 samples is 32 bytes/channel.
* The 2 in the equation refers two bytes that the Apple's
* IMA compressor puts at the front of each packet, which
* are referred to as predictor bytes
*/
// needBufferSize = samples/64 * (32 + 2) * channels; // REMOVE $$
needBufferSize = samples/samplesPerBlock * (32 + 2) * channels;
} else if (encoding.equals("agsm")) {
/**
* Each frame that consists of 160 speech samples
* requires 33 bytes
*/
// needBufferSize = (samples / 160) * 33; // REMOVE $$
needBufferSize = (samples / 160) * samplesPerBlock;
} else {
needBufferSize = samples *
((AudioFormat)format).getSampleSizeInBits()/8 * channels;
}
// System.out.println("needBufferSize is " + needBufferSize);
Object obj = buffer.getData();
byte[] data;
if ( (obj == null) ||
(! (obj instanceof byte[]) ) ||
( ((byte[])obj).length < needBufferSize) ) {
data = new byte[needBufferSize];
buffer.setData(data);
} else {
data = (byte[]) obj;
}
try {
int actualBytesRead;
synchronized(seekSync) {
int offset = trakInfo.chunkOffsets[useChunkNumber];
{
if (sampleIndex != useSampleIndex) { // Changed by setPosition()
// System.out.println("parser: audio: discard");
buffer.setDiscard(true);
return;
}
}
if ( (cacheStream != null) && (listener != null) ) {
// if ( cacheStream.willReadBytesBlock(offset, needBufferSize) ) {
if ( cacheStream.willReadBytesBlock(offset+byteOffsetFromSampleOffset,
needBufferSize) ) {
listener.readHasBlocked(this);
} else { // TODO: REMOVE ELSE BLOCK
// System.out.println("audio: won't block: " + offset + " : " +
// needBufferSize);
}
}
// long pos = seekableStream.seek(offset);
// System.out.println("seek to " + (offset + byteOffsetFromSampleOffset));
long pos = seekableStream.seek(offset + byteOffsetFromSampleOffset);
if ( pos == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
return;
}
actualBytesRead = parser.readBytes(stream, data, needBufferSize);
// System.out.println("actualBytesRead is " + actualBytesRead);
if ( actualBytesRead == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
return;
}
}
buffer.setLength(actualBytesRead);
// TODO: need setSequenceNumber and getSequenceNumber in Buffer
buffer.setSequenceNumber(++sequenceNumber);
if (sampleRate > 0) {
long timeStamp =
(samplesPlayed * 1000000000L) / sampleRate;
// System.out.println("audio timeStamp is " + timeStamp);
buffer.setTimeStamp(timeStamp);
buffer.setDuration(Buffer.TIME_UNKNOWN);
}
} catch (IOException e) {
// System.err.println("readFrame: " + e);
buffer.setLength(0);
buffer.setEOM(true);
// TODO: $$$$ Update maxFrame and duration
// System.out.print("After EOM Updating maxLocation from " + maxLocation);
// maxLocation = parser.getLocation(stream);
// System.out.println(" to " + maxLocation);
}
synchronized(this) {
if (chunkNumber == useChunkNumber) // Not changed by setPosition()
// System.out.println("AudioTrack: increment chunkNumber");
chunkNumber++;
}
}
}
private class VideoTrack extends MediaTrack {
int needBufferSize;
boolean variableSampleSize = true;
VideoTrack(TrakList trakInfo) {
super(trakInfo);
if (trakInfo != null) { // remove this if $$$$$
if (trakInfo.sampleSize != 0) {
variableSampleSize = false;
needBufferSize = trakInfo.sampleSize;
}
}
}
void doReadFrame(Buffer buffer) {
// File buglife.mov is inconsistent. The stts chunk has 2 entries
// with 1842 and 1 samples, which means that there are 1843 samples
// in the movie. But the stsz chunk has only 1842 entries.
// To workaround this inconsistency, I check for sampleSizeArray.length
// also.
if ( useSampleIndex >= trakInfo.numberOfSamples ) {
buffer.setLength(0);
buffer.setEOM(true);
return;
}
if ( variableSampleSize ) {
if (useSampleIndex >= trakInfo.sampleSizeArray.length) {
buffer.setLength(0);
buffer.setEOM(true);
return;
}
needBufferSize = trakInfo.sampleSizeArray[useSampleIndex];
}
// System.out.println("needBufferSize is " + needBufferSize);
long offset = trakInfo.sampleOffsetTable[useSampleIndex];
Object obj = buffer.getData();
byte[] data;
if ( (obj == null) ||
(! (obj instanceof byte[]) ) ||
( ((byte[])obj).length < needBufferSize) ) {
data = new byte[needBufferSize];
buffer.setData(data);
} else {
data = (byte[]) obj;
}
try {
int actualBytesRead;
synchronized(seekSync) {
{
if (sampleIndex != useSampleIndex) { // Changed by setPosition()
// System.out.println("parser: video: discard");
buffer.setDiscard(true);
return;
}
}
if ( (cacheStream != null) && (listener != null) ) {
if ( cacheStream.willReadBytesBlock(offset, needBufferSize) ) {
// System.out.println("video: will block: " + offset + " : " +
// needBufferSize);
listener.readHasBlocked(this);
} else { // TODO: REMOVE ELSE BLOCK
// System.out.println("video: won't block: " + offset + " : " +
// needBufferSize);
}
}
// System.out.println("doReadFrame: offset is " + offset);
long pos = seekableStream.seek(offset);
// System.out.println("seek returns " + pos);
if ( pos == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
return;
}
// {// DEBUG BLOCK
// int numPackets = parser.readShort(stream);
// System.out.println("num packets " + numPackets);
// needBufferSize -= 2;
// parser.readShort(stream); // reserved
// needBufferSize -= 2;
// for (int i = 0; i < numPackets; i++) {
// System.out.println("Packet # " + i);
// int relativeTransmissionTime = parser.readInt(stream);
// System.out.println(" relativeTransmissionTime is " +
// relativeTransmissionTime);
// needBufferSize -= 4;
// int rtpHeaderInfo = parser.readShort(stream);
// System.out.println(" rtpHeaderInfo is " + rtpHeaderInfo);
// needBufferSize -= 2;
// int rtpSequenceNumber = parser.readShort(stream);
// System.out.println(" rtpSequenceNumber is " + rtpSequenceNumber);
// needBufferSize -= 2;
// int flags = parser.readShort(stream);
// System.out.println(" flags is " + flags);
// needBufferSize -= 2;
// int entriesInDataTable = parser.readShort(stream);
// System.out.println(" entriesInDataTable is " + entriesInDataTable);
// needBufferSize -= 2;
// // TODO: If bit 13 of flag is set, Extra info TLV table is present
// // int tlvTableSize = parser.readInt(stream);
// // System.out.println("tlvTableSize is " +
// // tlvTableSize);
// // needBufferSize -= 4;
// for (int j = 0; j < entriesInDataTable; j++) {
// int dataBlockSource = parser.readByte(stream);
// System.out.println(" dataBlockSource is " + dataBlockSource);
// // TODO: assuming dataBlockSource 1, that is Immediate data
// needBufferSize--;;
// int length = parser.readByte(stream);
// System.out.println(" data table length is " + length);
// needBufferSize--;;
// // skip(stream, length);
// // needBufferSize -= length;
// parser.skip(stream, 14);
// needBufferSize -= 14;
// }
// }
// System.out.println("needBufferSize after is " + needBufferSize);
// }
actualBytesRead = parser.readBytes(stream, data, needBufferSize);
// System.out.println("actualBytesRead: " + actualBytesRead);
if ( actualBytesRead == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
return;
}
}
/**//*System.out.println(" actualBytesRead is " + actualBytesRead);*/
buffer.setLength(actualBytesRead);
int[] syncSampleMapping = trakInfo.syncSampleMapping;
boolean keyFrame = true;
if (syncSampleMapping != null) {
keyFrame = (syncSampleMapping[useSampleIndex] == useSampleIndex);
}
// Note: if syncSampleMapping is null, then every frame is a key frame.
if (keyFrame) {
buffer.setFlags(buffer.getFlags() | Buffer.FLAG_KEY_FRAME);
// System.out.println("readFrame: returns keyframe " + useSampleIndex);
}
// TODO: need setSequenceNumber and getSequenceNumber in Buffer
buffer.setSequenceNumber(++sequenceNumber);
TimeAndDuration td = trakInfo.index2TimeAndDuration(useSampleIndex);
// System.out.println("seq num:, timestamp is " + sequenceNumber +
// " : " + td.startTime);
buffer.setTimeStamp((long) (td.startTime * 1E9));
buffer.setDuration((long) (td.duration * 1E9));
} catch (IOException e) {
// System.err.println("readFrame: " + e);
buffer.setLength(0);
buffer.setEOM(true);
// TODO: $$$$ Update maxFrame and duration
// System.out.print("After EOM Updating maxLocation from " + maxLocation);
// maxLocation = parser.getLocation(stream);
// System.out.println(" to " + maxLocation);
}
synchronized(this) {
if (sampleIndex == useSampleIndex) // Not changed by setPosition()
sampleIndex++;
}
}
public int mapTimeToFrame(Time t) {
double time = t.getSeconds();
if (time < 0)
return FRAME_UNKNOWN;
int index = trakInfo.time2Index(time);
// System.out.println("index is " + index);
// System.out.println("max index " + trakInfo.numberOfSamples);
// if ( (index < 0) || (index >= trakInfo.numberOfSamples) )
// return FRAME_UNKNOWN;
if ( index < 0 )
return trakInfo.numberOfSamples - 1;
// System.out.println(" mapTimeToFrame: " + time + " : " +
// index);
// { // DEBUG BLOCK REMOVE
// mapFrameToTime(index);
// }
return index;
}
public Time mapFrameToTime(int frameNumber) {
if ( (frameNumber < 0) || (frameNumber >= trakInfo.numberOfSamples) )
return TIME_UNKNOWN;
// System.out.println("mapFrameToTime: frame rate is " +
// ((Video) trakInfo.media).frameRate);
double time = frameNumber / ((Video) trakInfo.media).frameRate;
// System.out.println("mapFrameToTime: " + frameNumber +
// " ==> " + time);
return new Time(time);
}
}
private class HintAudioTrack extends AudioTrack {
int hintSampleSize;
int indexOfTrackBeingHinted = trakInfo.indexOfTrackBeingHinted;
int maxPacketSize;
int currentPacketNumber = 0;
int numPacketsInSample = -1;
long offsetToStartOfPacketInfo = -1;
TrakList sampleTrakInfo;
boolean variableSampleSize = true;
HintAudioTrack(TrakList trakInfo,
int channels,
String encoding,
int frameSizeInBytes,
int samplesPerBlock,
int sampleRate) {
super(trakInfo, channels, encoding, frameSizeInBytes, samplesPerBlock, sampleRate);
format = ( (Hint) trakInfo.media).format;
// hintSampleSize = needBufferSize;
maxPacketSize = trakInfo.maxPacketSize;
if (indexOfTrackBeingHinted >= 0) {
sampleTrakInfo = trakList[indexOfTrackBeingHinted];
} else { // $$$ REMOVE else DEBUG BLOCK
if (debug) {
System.out.println("sampleTrakInfo is not set " + indexOfTrackBeingHinted);
}
}
// System.out.println("HintAudioTrack: cons: numchunks of hint, sample trak " +
// trakInfo.numberOfChunks + " : " +
// trakList[indexOfTrackBeingHinted].numberOfChunks);
// System.out.println("HintAudioTrack: cons: constantSamplesPerChunk in hint, sample " +
// trakInfo.constantSamplesPerChunk + " : " +
// trakList[indexOfTrackBeingHinted].constantSamplesPerChunk);
if (trakInfo.sampleSize != 0) {
variableSampleSize = false;
hintSampleSize = trakInfo.sampleSize;
}
}
// HintAudioTrack readFrame overrides method in base class
// MediaTrack because you can't use the chunknumber which in
// this case is the sample number to check
// (useChunkNumber >= trakInfo.numberOfChunks) and call
// buffer.setEOM(true).
public void readFrame(Buffer buffer) {
if (buffer == null)
return;
if (!enabled) {
buffer.setDiscard(true);
return;
}
synchronized (this) {
useChunkNumber = chunkNumber;
useSampleIndex = sampleIndex;
}
// Doesn't apply for HintAudioTrack
// if ( (useChunkNumber >= trakInfo.numberOfChunks) ||
// (useChunkNumber < 0 ) ) {
// System.out.println("MediaTrack EOM: chunkNumber > numberOfChunks");
// buffer.setEOM(true);
// return;
// }
buffer.setFormat(format); // Need to do this every time ???
doReadFrame(buffer);
}
// NEW::
synchronized void setSampleIndex(int index) {
chunkNumber = index;
sampleIndex = index;
}
void doReadFrame(Buffer buffer) {
int samples;
if (debug1) {
System.out.println("audio: hint doReadFrame: " + useChunkNumber +
" : " + sampleOffsetInChunk);
}
boolean rtpMarkerSet = false;
boolean paddingPresent;
boolean extensionHeaderPresent;
int rtpSequenceNumber;
int relativeTransmissionTime;
if (indexOfTrackBeingHinted < 0) {
buffer.setDiscard(true);
return;
}
int rtpOffset = 0;
int remainingHintSampleSize;
// if ( variableSampleSize )
// hintSampleSize = trakInfo.sampleSizeArray[useSampleIndex];
// REPLACED ABOVE WITH
if ( variableSampleSize ) {
if (useSampleIndex >= trakInfo.sampleSizeArray.length) {
hintSampleSize = trakInfo.sampleSizeArray[trakInfo.sampleSizeArray.length-1];
} else {
hintSampleSize = trakInfo.sampleSizeArray[useSampleIndex];
}
}
remainingHintSampleSize = hintSampleSize;
if (debug1) {
System.out.println("hintSampleSize is " + hintSampleSize);
// System.out.println("useSampleIndex, offset " + useSampleIndex +
// " : " + offset);
}
Object obj = buffer.getData();
byte[] data;
if ( (obj == null) ||
(! (obj instanceof byte[]) ) ||
( ((byte[])obj).length < maxPacketSize) ) {
data = new byte[maxPacketSize];
buffer.setData(data);
} else {
data = (byte[]) obj;
}
try {
int actualBytesRead;
synchronized(seekSync) {
{
if (sampleIndex != useSampleIndex) { // Changed by setPosition()
// System.out.println("parser: audio: discard");
// System.out.println("$$$$ REMOVE: SHOULDN'T HAPPEN: setPosition discard");
buffer.setDiscard(true);
currentPacketNumber = 0;
numPacketsInSample = -1;
offsetToStartOfPacketInfo = -1;
rtpOffset = 0;
return;
}
}
// NOTE: this useChunkNumber is actually sample number
long offset = trakInfo.index2Offset(useChunkNumber);
if (debug) {
System.out.println("audio: Calling index2Offset on hint track with arg " +
useChunkNumber);
System.out.println("offset is " + offset);
}
if ( offset == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setLength(0);
buffer.setEOM(true);
return;
}
if ( (cacheStream != null) && (listener != null) ) {
if ( cacheStream.willReadBytesBlock(offset, hintSampleSize) ) {
// if ( cacheStream.willReadBytesBlock(offset+byteOffsetFromSampleOffset,
// needBufferSize) ) {
listener.readHasBlocked(this);
} else { // TODO: REMOVE ELSE BLOCK
// System.out.println("audio: won't block: " + offset + " : " +
// needBufferSize);
}
}
long pos;
if (/*true*/debug1) {
System.out.println("currentPacketNumber is " + currentPacketNumber);
}
if (offsetToStartOfPacketInfo < 0) {
if (debug1) {
System.out.println("NEW SEEK");
}
pos = seekableStream.seek(offset);
// System.out.println("seek returns " + pos);
if ( pos == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
return;
}
numPacketsInSample = parser.readShort(stream);
if (debug) {
System.out.println("num packets in sample " + numPacketsInSample);
}
if (numPacketsInSample < 1) {
buffer.setDiscard(true);
return;
}
remainingHintSampleSize -= 2;
parser.readShort(stream); // reserved
remainingHintSampleSize -= 2;
} else {
// PACKET TABLE STARTS HERE
// System.out.println("NO NEW SEEK but to offsetToStartOfPacketInfo " +
// offsetToStartOfPacketInfo);
pos = seekableStream.seek(offsetToStartOfPacketInfo);
if ( pos == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
return;
}
}
// TODO: check to see of remainingHintSampleSize is positive
relativeTransmissionTime = parser.readInt(stream);
remainingHintSampleSize -= 4;
// short rtpHeaderInfo = (short) parser.readShort(stream);
int rtpHeaderInfo = parser.readShort(stream);
if (debug) {
System.out.println("rtpHeaderInfo is " +
Integer.toHexString(rtpHeaderInfo));
}
rtpMarkerSet = ((rtpHeaderInfo & 0x80) > 0);
// System.out.println("rtp marker present? " + rtpMarkerSet); // REMOVE $$
remainingHintSampleSize -= 2;
rtpSequenceNumber = parser.readShort(stream);
remainingHintSampleSize -= 2;
paddingPresent = ( (rtpHeaderInfo & 0x2000) > 0);
extensionHeaderPresent = ( (rtpHeaderInfo & 0x1000) > 0);
// System.out.println("rtp payload type " +
// (rtpHeaderInfo & 0x007F)); // DELETE $$$
if (paddingPresent) {
// System.err.println("qtparser:audio:rtpheader:paddingPresent"); // TODO
}
if (extensionHeaderPresent) {
// System.err.println("qtparser:audio:rtpheader:extensionHeaderPresent"); // TODO
}
int flags = parser.readShort(stream);
if (debug) {
// System.out.println("rtp version " +
// (rtpHeaderInfo & 0xC000));
System.out.println("rtp marker present? " + rtpMarkerSet);
System.out.println("rtp payload type " +
(rtpHeaderInfo & 0x007F));
System.out.println("padding? " + paddingPresent);
System.out.println("extension header? " + extensionHeaderPresent);
System.out.println("audio hint: flags is " + Integer.toHexString(flags));
// System.out.println("Check if flag has X bit set " +
// ( (flags & 0x0004) > 0 ));
// System.out.println(" audio hint flags is " + flags);
}
remainingHintSampleSize -= 2;
int entriesInDataTable = parser.readShort(stream);
remainingHintSampleSize -= 2;
// TODO: If bit 13 of flag is set, Extra info TLV table is present
// int tlvTableSize = parser.readInt(stream);
// System.out.println("tlvTableSize is " +
// tlvTableSize);
// remainingHintSampleSize -= 4;
// TODO: If bit 13 of flag is set, Extra info TLV table is present
boolean extraInfoTLVPresent = ( (flags & 0x0004) > 0);
if (extraInfoTLVPresent) {
int tlvTableSize = parser.readInt(stream);
// TODO: for now extra info TLV is skipped
skip(stream, tlvTableSize-4);
if (debug) {
System.err.println("audio: extraInfoTLVPresent: Skipped"); //TODO?
System.out.println("tlvTableSize is " +
tlvTableSize);
}
}
if (/*true*/debug) { // debug1
System.out.println("Packet # " + currentPacketNumber);
System.out.println(" relativeTransmissionTime is " +
relativeTransmissionTime);
System.out.println(" rtpSequenceNumber is " + rtpSequenceNumber);
System.out.println(" entriesInDataTable is " + entriesInDataTable);
}
for (int j = 0; j < entriesInDataTable; j++) {
int dataBlockSource = parser.readByte(stream);
remainingHintSampleSize--;;
if (debug1) {
System.out.println(" dataBlockSource is " + dataBlockSource);
}
if ( dataBlockSource == HINT_IMMEDIATE_DATA ) {
int length = parser.readByte(stream);
// TODO: length should not be more than 14
// System.out.println(" data table length is " + length);
remainingHintSampleSize--;;
// System.out.println("IMM:rtpOffset is " + rtpOffset);
parser.readBytes(stream, data, rtpOffset, length);
rtpOffset += length;;
parser.skip(stream, (14-length));
remainingHintSampleSize -= 14;
} else if (dataBlockSource == HINT_SAMPLE_DATA) {
int trackRefIndex = parser.readByte(stream);
if (debug1) {
System.out.println(" audio:trackRefIndex is " + trackRefIndex);
}
// Note: Only trackRefIndex value of 0 and -1 are supported
// This means that the hint track referes to one media track
// A positive value implies that that the hint track refers
// to multiple media tracks -- this is not supported
if (trackRefIndex > 0) {
System.err.println(" Currently we don't support hint tracks that refer to multiple media tracks: " + trackRefIndex);
buffer.setDiscard(true);
return;
}
int numBytesToCopy = parser.readShort(stream);
int sampleNumber = parser.readInt(stream);
int byteOffset = parser.readInt(stream);
int bytesPerCompresionBlock = parser.readShort(stream);
int samplesPerCompresionBlock = parser.readShort(stream);
if (/*true*/debug1) {
System.out.println(" sample Number is " + sampleNumber);
System.out.println(" numBytesToCopy is " + numBytesToCopy);
System.out.println(" byteOffset is " + byteOffset);
System.out.println(" bytesPerCompresionBlock is " + bytesPerCompresionBlock);
System.out.println(" samplesPerCompresionBlock is " + samplesPerCompresionBlock);
}
remainingHintSampleSize -= 15;
long saveCurrentPos = parser.getLocation(stream);
// We have already taken care of trackRefIndex > 0 case
TrakList useTrakInfo;
if (trackRefIndex == 0) {
useTrakInfo = sampleTrakInfo;
if (debug2)
System.out.println("set useTrakInfo as sampleTrakInfo");
} else {
// trackRefIndex < 0
// $$ Note: To be precise we should check for trackRefIndex == -1
// Data resides in a sample in the hint track itself
// System.out.println("trackRefIndex " + trackRefIndex +
// " using trakInfo");
useTrakInfo = trakInfo;
}
// sample numbers start from 1. But the array starts from
// 0. SO subtract 1
if (debug1) {
System.out.println("useTrakInfo is " + useTrakInfo);
System.out.println("useTrakInfo.sampleOffsetTable is " +
useTrakInfo.sampleOffsetTable);
}
long sampleOffset;
if (useTrakInfo.sampleOffsetTable == null) {
//$$?? System.out.println("useChunkNumber is " + useChunkNumber);
//$$?? sampleOffset = useTrakInfo.chunkOffsets[useChunkNumber];
// sampleOffset = useTrakInfo.index2Offset(sampleNumber);
// System.out.println(" audio: Calling index2Offset on audio sample track with arg " +
// (sampleNumber-1));
sampleOffset = useTrakInfo.index2Offset(sampleNumber-1);
if (debug1) {
System.out.println("chunkOffsets size is " +
useTrakInfo.chunkOffsets.length);
System.out.println("sampleOffset from index2Offset " +
sampleOffset);
}
} else {
sampleOffset =
useTrakInfo.sampleOffsetTable[sampleNumber-1];
}
// System.out.println(" sampleOffset is " + sampleOffset);
// System.out.println(" byteOffset is " + byteOffset);
sampleOffset += byteOffset;
// System.out.println(" sampleOffset + byteOffset " + sampleOffset);
pos = seekableStream.seek(sampleOffset);
if ( pos == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
// System.err.println("seek error1 set offsetToStartOfPacketInfo -1");
offsetToStartOfPacketInfo = -1;
return;
}
if (/*true*/debug1) {
System.out.println("Audio: Seek to " + sampleOffset + " and read " + numBytesToCopy + " bytes into buffer with offset " + rtpOffset);
}
// {//DEBUG BLOCK REMOVE
// if (trackRefIndex < 0) {
// System.out.println("Seek to " + sampleOffset + " and read " + numBytesToCopy + " bytes into buffer with offset " + rtpOffset);
// }
// }
// {//DEBUG BLOCK REMOVE
// if ( (numBytesToCopy & 0x1) > 0 ) {
// numBytesToCopy--; // Make numBytesToCopy even
// System.out.println("$$$ MAKING numBytesToCopy even");
// }
// }
parser.readBytes(stream, data, rtpOffset, numBytesToCopy);
rtpOffset += numBytesToCopy;
// restore position
pos = seekableStream.seek(saveCurrentPos);
if ( pos == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
// System.err.println("seek error2 set offsetToStartOfPacketInfo -1");
offsetToStartOfPacketInfo = -1;
return;
}
} else if ( dataBlockSource == HINT_NOP_IGNORE ) {
// No Data, IGNORE
int length = parser.readByte(stream);
parser.skip(stream, length);
remainingHintSampleSize -= length;
} else {
//TODO
// Need to support HINT_SAMPLE_DESCRIPTION
System.err.println("DISCARD: dataBlockSource " + dataBlockSource +
" not supported");
buffer.setDiscard(true);
offsetToStartOfPacketInfo = -1;
return;
}
}
actualBytesRead = rtpOffset;
if (debug1) {
System.out.println("Actual size of packet sent " + rtpOffset);
}
rtpOffset = 0;
// System.out.println("remainingHintSampleSize after is " + remainingHintSampleSize);
// Note: remainingHintSampleSize should be 0.
offsetToStartOfPacketInfo = parser.getLocation(stream);
// System.out.println("offsetToStartOfPacketInfo from getLocation is " +
// offsetToStartOfPacketInfo);
if ( actualBytesRead == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
// System.err.println("DISCARD actualBytesRead is LENGTH_DISCARD");
buffer.setDiscard(true);
return;
}
}
buffer.setLength(actualBytesRead);
if (rtpMarkerSet) {
if (debug)
System.out.println("rtpMarkerSet: true");
buffer.setFlags(buffer.getFlags() | Buffer.FLAG_RTP_MARKER);
} else {
if (debug)
System.out.println("rtpMarkerSet: false");
buffer.setFlags(buffer.getFlags() & ~Buffer.FLAG_RTP_MARKER);
}
buffer.setSequenceNumber(rtpSequenceNumber);
// buffer.setTimeStamp((long) relativeTransmissionTime);
double startTime = trakInfo.index2TimeAndDuration(useChunkNumber).startTime;
// System.out.println("AUDIO HINT: time for sample # " + useChunkNumber +
// " is " + startTime + " [ " +
// (startTime*1000) + " msec]");
long timeStamp =
(long) ((startTime /*+ scaledRelativeTransmissionTime*/) * 1E9);
buffer.setTimeStamp(timeStamp);
buffer.setDuration(Buffer.TIME_UNKNOWN);
} catch (IOException e) {
// System.err.println("readFrame: " + e);
buffer.setLength(0);
buffer.setEOM(true);
// TODO: $$$$ Update maxFrame and duration
// System.out.print("After EOM Updating maxLocation from " + maxLocation);
// maxLocation = parser.getLocation(stream);
// System.out.println(" to " + maxLocation);
}
synchronized(this) {
if (chunkNumber != useChunkNumber) { // changed by setPosition()
// System.out.println("$$$$ REMOVE: SHOULDN'T HAPPEN: chunkNumber: setPosition discard");
currentPacketNumber = 0;
numPacketsInSample = -1;
offsetToStartOfPacketInfo = -1;
rtpOffset = 0;
} else {
// System.out.println("increment current packetnumber old, total" +
// currentPacketNumber + " : " + numPacketsInSample);
currentPacketNumber++;
if (currentPacketNumber >= numPacketsInSample) {
chunkNumber++;
currentPacketNumber = 0;
numPacketsInSample = -1;
offsetToStartOfPacketInfo = -1;
rtpOffset = 0;
}
}
}
// System.out.println("RETURN FROM readFrame, offsetToStartOfPacketInfo is " +
// offsetToStartOfPacketInfo);
}
}
private class HintVideoTrack extends VideoTrack {
int hintSampleSize;
int indexOfTrackBeingHinted = trakInfo.indexOfTrackBeingHinted;
int maxPacketSize;
int currentPacketNumber = 0;
int numPacketsInSample = -1;
long offsetToStartOfPacketInfo = -1;
TrakList sampleTrakInfo = null;
HintVideoTrack(TrakList trakInfo) {
super(trakInfo);
format = ( (Hint) trakInfo.media).format;
hintSampleSize = needBufferSize;
maxPacketSize = trakInfo.maxPacketSize;
if (debug1) {
System.out.println("HintVideoTrack: Index of hinted track: " +
trakInfo.indexOfTrackBeingHinted);
System.out.println("HintVideoTrack: packet size is " +
maxPacketSize);
}
if (indexOfTrackBeingHinted >= 0) {
sampleTrakInfo = trakList[indexOfTrackBeingHinted];
} else { // $$$ REMOVE else DEBUG BLOCK
if (debug) {
System.out.println("sampleTrakInfo is not set " + indexOfTrackBeingHinted);
}
}
}
void doReadFrame(Buffer buffer) {
boolean rtpMarkerSet = false;
boolean paddingPresent;
boolean extensionHeaderPresent;
int rtpSequenceNumber;
int relativeTransmissionTime;
if (indexOfTrackBeingHinted < 0) {
buffer.setDiscard(true);
return;
}
if ( useSampleIndex >= trakInfo.numberOfSamples ) {
buffer.setLength(0);
buffer.setEOM(true);
//System.out.println("VIDEO: return EOM");
return;
}
int rtpOffset = 0;
int remainingHintSampleSize;
if ( variableSampleSize )
hintSampleSize = trakInfo.sampleSizeArray[useSampleIndex];
remainingHintSampleSize = hintSampleSize;
long offset = trakInfo.sampleOffsetTable[useSampleIndex];
if (debug1) {
System.out.println("hintSampleSize is " + hintSampleSize);
System.out.println("useSampleIndex, offset " + useSampleIndex +
" : " + offset);
}
Object obj = buffer.getData();
byte[] data;
if ( (obj == null) ||
(! (obj instanceof byte[]) ) ||
( ((byte[])obj).length < maxPacketSize) ) {
data = new byte[maxPacketSize];
buffer.setData(data);
} else {
data = (byte[]) obj;
}
try {
int actualBytesRead;
synchronized(seekSync) {
{
if (sampleIndex != useSampleIndex) { // Changed by setPosition()
// System.out.println("parser: video: discard");
buffer.setDiscard(true);
currentPacketNumber = 0;
numPacketsInSample = -1;
offsetToStartOfPacketInfo = -1;
rtpOffset = 0;
return;
}
}
if ( (cacheStream != null) && (listener != null) ) {
if ( cacheStream.willReadBytesBlock(offset, hintSampleSize) ) {
// System.out.println("video: will block: " + offset + " : " +
// hintSampleSize);
listener.readHasBlocked(this);
} else { // TODO: REMOVE ELSE BLOCK
// System.out.println("video: won't block: " + offset + " : " +
// hintSampleSize);
}
}
long pos;
if (offsetToStartOfPacketInfo < 0) {
pos = seekableStream.seek(offset);
// System.out.println("seek returns " + pos);
if ( pos == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
return;
}
numPacketsInSample = parser.readShort(stream);
if (/*true*/debug) {
System.out.println("video: num packets in sample " + numPacketsInSample);
}
if (numPacketsInSample < 1) {
buffer.setDiscard(true);
return;
}
remainingHintSampleSize -= 2;
parser.readShort(stream); // reserved
remainingHintSampleSize -= 2;
} else {
// PACKET TABLE STARTS HERE
pos = seekableStream.seek(offsetToStartOfPacketInfo);
if ( pos == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
return;
}
}
// TODO: check to see of remainingHintSampleSize is positive
relativeTransmissionTime = parser.readInt(stream);
remainingHintSampleSize -= 4;
// short rtpHeaderInfo = (short) parser.readShort(stream);
int rtpHeaderInfo = (short) parser.readShort(stream);
rtpMarkerSet = ((rtpHeaderInfo & 0x80) > 0);
remainingHintSampleSize -= 2;
rtpSequenceNumber = parser.readShort(stream);
remainingHintSampleSize -= 2;
paddingPresent = ( (rtpHeaderInfo & 0x2000) > 0);
extensionHeaderPresent = ( (rtpHeaderInfo & 0x1000) > 0);
if (paddingPresent) {
// System.err.println("video: paddingPresent: SHOULD BE HANDLED"); // TODO
}
if (extensionHeaderPresent) {
// System.err.println("video: extensionHeaderPresent: SHOULD BE HANDLED"); // TODO
}
int flags = parser.readShort(stream);
if (debug) {
// System.out.println("rtp version " +
// (rtpHeaderInfo & 0xC000));
System.out.println("rtp marker present? " + rtpMarkerSet);
System.out.println("rtp payload type " +
(rtpHeaderInfo & 0x007F));
System.out.println("padding? " + paddingPresent);
System.out.println("extension header? " + extensionHeaderPresent);
System.out.println("video hint: flags is " + Integer.toHexString(flags));
// System.out.println("Check if flag has X bit set " +
// ( (flags & 0x0004) > 0 ));
}
remainingHintSampleSize -= 2;
int entriesInDataTable = parser.readShort(stream);
remainingHintSampleSize -= 2;
// TODO: If bit 13 of flag is set, Extra info TLV table is present
// int tlvTableSize = parser.readInt(stream);
// System.out.println("tlvTableSize is " +
// tlvTableSize);
// remainingHintSampleSize -= 4;
boolean extraInfoTLVPresent = ( (flags & 0x0004) > 0);
if (extraInfoTLVPresent) {
int tlvTableSize = parser.readInt(stream);
// TODO: for now extra info TLV is skipped
skip(stream, tlvTableSize-4);
if (debug) {
System.err.println("video: extraInfoTLVPresent: Skipped"); // TODO
System.out.println("tlvTableSize is " +
tlvTableSize);
}
}
if (debug) { // debug1
System.out.println("Packet # " + currentPacketNumber);
System.out.println(" relativeTransmissionTime is " +
relativeTransmissionTime);
System.out.println("$$$ relativeTransmissionTime is in timescale " +
trakInfo.mediaTimeScale);
// System.out.println(" rtpHeaderInfo is " + rtpHeaderInfo);
System.out.println(" rtpSequenceNumber is " + rtpSequenceNumber);
System.out.println(" entriesInDataTable is " + entriesInDataTable);
}
for (int j = 0; j < entriesInDataTable; j++) {
int dataBlockSource = parser.readByte(stream);
remainingHintSampleSize--;;
if (debug1) {
System.out.println(" dataBlockSource is " + dataBlockSource);
}
if ( dataBlockSource == HINT_IMMEDIATE_DATA ) {
int length = parser.readByte(stream);
// TODO: length should not be more than 14
// System.out.println(" data table length is " + length);
remainingHintSampleSize--;;
// System.out.println("IMM:rtpOffset is " + rtpOffset);
// System.out.println("video IMM data read " + length +
// " bytes into data");
parser.readBytes(stream, data, rtpOffset, length);
rtpOffset += length;;
parser.skip(stream, (14-length));
remainingHintSampleSize -= 14;
} else if (dataBlockSource == HINT_SAMPLE_DATA) {
int trackRefIndex = parser.readByte(stream);
if (debug1) {
System.out.println(" video: trackRefIndex is " + trackRefIndex);
}
// Note: Only trackRefIndex value of 0 and -1 are supported
// This means that the hint track referes to one media track
// A positive value implies that that the hint track refers
// to multiple media tracks -- this is not supported
if (trackRefIndex > 0) {
System.err.println(" Currently we don't support hint tracks that refer to multiple media tracks");
buffer.setDiscard(true);
return;
}
int numBytesToCopy = parser.readShort(stream);
int sampleNumber = parser.readInt(stream);
int byteOffset = parser.readInt(stream);
int bytesPerCompresionBlock = parser.readShort(stream);
int samplesPerCompresionBlock = parser.readShort(stream);
if (debug1) {
System.out.println(" sample Number is " + sampleNumber);
System.out.println(" numBytesToCopy is " + numBytesToCopy);
System.out.println(" byteOffset is " + byteOffset);
System.out.println(" bytesPerCompresionBlock is " + bytesPerCompresionBlock);
System.out.println(" samplesPerCompresionBlock is " + samplesPerCompresionBlock);
}
remainingHintSampleSize -= 15;
long saveCurrentPos = parser.getLocation(stream);
// We have already taken care of trackRefIndex > 0 case
TrakList useTrakInfo;
if (trackRefIndex == 0) {
useTrakInfo = sampleTrakInfo;
} else {
// trackRefIndex < 0
// $$ Note: To be precise we should check for trackRefIndex == -1
// Data resides in a sample in the hint track itself
// System.out.println("trackRefIndex " + trackRefIndex +
// " using trakInfo");
useTrakInfo = trakInfo;
}
// sample numbers start from 1. But the array starts from
// 0. SO subtract 1
// System.out.println("useTrakInfo is " + useTrakInfo);
// System.out.println("useTrakInfo.sampleOffsetTable is " +
// useTrakInfo.sampleOffsetTable);
long sampleOffset =
useTrakInfo.sampleOffsetTable[sampleNumber-1];
// System.out.println(" sampleOffset is " + sampleOffset);
sampleOffset += byteOffset;
// System.out.println(" sampleOffset + byteOffset " + sampleOffset);
pos = seekableStream.seek(sampleOffset);
if ( pos == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
offsetToStartOfPacketInfo = -1;
return;
}
if (debug1) {
System.out.println(" read " + numBytesToCopy + " bytes from offset " + rtpOffset);
}
parser.readBytes(stream, data, rtpOffset, numBytesToCopy);
rtpOffset += numBytesToCopy;
// restore position
pos = seekableStream.seek(saveCurrentPos);
if ( pos == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
offsetToStartOfPacketInfo = -1;
return;
}
} else {
//TODO
// Need to support 0 ==> No Data and
// 3 ==> Sample description data
buffer.setDiscard(true);
offsetToStartOfPacketInfo = -1;
return;
}
}
actualBytesRead = rtpOffset;
if (debug1) {
System.out.println("Actual size of packet sent " + rtpOffset);
}
rtpOffset = 0;
// System.out.println("remainingHintSampleSize after is " + remainingHintSampleSize);
// Note: remainingHintSampleSize should be 0.
offsetToStartOfPacketInfo = parser.getLocation(stream);
if ( actualBytesRead == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) {
buffer.setDiscard(true);
return;
}
}
buffer.setLength(actualBytesRead);
if (rtpMarkerSet) {
if (debug)
System.out.println("rtpMarkerSet: true");
buffer.setFlags(buffer.getFlags() | Buffer.FLAG_RTP_MARKER);
} else {
if (debug)
System.out.println("rtpMarkerSet: false");
buffer.setFlags(buffer.getFlags() & ~Buffer.FLAG_RTP_MARKER);
}
buffer.setSequenceNumber(rtpSequenceNumber);
// buffer.setTimeStamp((long) relativeTransmissionTime);
TimeAndDuration td = trakInfo.index2TimeAndDuration(useSampleIndex);
double startTime = td.startTime;
// System.out.println("VIDEO HINT: time for sample # " + useSampleIndex +
// " is " + startTime + " [ " +
// (startTime*1000) + " msec]");
// double scaledRelativeTransmissionTime =
// (double) relativeTransmissionTime / (trakInfo.mediaTimeScale);
// System.out.println("scaledRelativeTransmissionTime for " +
// relativeTransmissionTime +
// " is " +
// scaledRelativeTransmissionTime);
// long timeStamp =
// (long) ((startTime + scaledRelativeTransmissionTime) * 1E9);
// System.out.println("Video timeStamp: " +
// (startTime + scaledRelativeTransmissionTime));
// buffer.setTimeStamp(timeStamp);
long timeStamp =
(long) ((startTime /*+ scaledRelativeTransmissionTime*/) * 1E9);
// System.out.println("VIDEO HINT: setTimeStamp in msec "
// + (int) (startTime*1000)
// + " sampleindex " + useSampleIndex);
buffer.setTimeStamp(timeStamp);
// { //DEBUG BLOCK print data values
// for (int tt = 0; tt < 30; tt++) {
// int vv = (int) data[tt];
// System.out.println(Integer.toHexString(vv));
// }
// }
buffer.setDuration((long) (td.duration * 1E9));
} catch (IOException e) {
// System.err.println("readFrame: " + e);
buffer.setLength(0);
buffer.setEOM(true);
// TODO: $$$$ Update maxFrame and duration
// System.out.print("After EOM Updating maxLocation from " + maxLocation);
// maxLocation = parser.getLocation(stream);
// System.out.println(" to " + maxLocation);
}
synchronized(this) {
if (sampleIndex != useSampleIndex) { // changed by setPosition()
currentPacketNumber = 0;
numPacketsInSample = -1;
offsetToStartOfPacketInfo = -1;
rtpOffset = 0;
} else {
// System.out.println("increment current packetnumber old, total" +
// currentPacketNumber + " : " + numPacketsInSample);
currentPacketNumber++;
// System.out.println("currentPacketNumber >= numPacketsInSample " +
// currentPacketNumber + " : " +
// numPacketsInSample);
if (currentPacketNumber >= numPacketsInSample) {
sampleIndex++;
currentPacketNumber = 0;
numPacketsInSample = -1;
offsetToStartOfPacketInfo = -1;
rtpOffset = 0;
}
}
}
}
}
private class TimeAndDuration {
double startTime;
double duration;
}
// TODO: ENHANCE: See if you free up some arrays that you no longer need.
// For e.g for video trak, the sampletochunk array if all the
// offsets for all the samples have been calculated.
// TODO: ENHANCE
// for video because the number of samples is not large
// time to sample #, offset[sample #]
// For audio number of samples is large, so
// time to sample #, sample # to chunk number, chunk # to chunk offset,
// offset within chunk from sample size.
// For very very large video files, the number of video samples
// may be large and so you may have to follow the approach used for
// audio. Need some experimentation to decide the
// cutoff max_num_samples_in_video value
}
|