/* * @(#)QuicktimeMux.java 1.28 02/08/21 * * Copyright (c) 1996-2002 Sun Microsystems, Inc. All rights reserved. */ package com.sun.media.multiplexer.video; import javax.media.Time; import javax.media.Duration; import javax.media.Buffer; import javax.media.Format; import javax.media.PlugIn; import javax.media.protocol.ContentDescriptor; import javax.media.protocol.FileTypeDescriptor; import javax.media.protocol.SourceTransferHandler; import com.sun.media.format.WavAudioFormat; import com.sun.media.format.AviVideoFormat; import javax.media.format.UnsupportedFormatException; import java.io.IOException; import javax.media.Control; import javax.media.IncompatibleSourceException; import javax.media.format.AudioFormat; import javax.media.format.*; import com.sun.media.util.ByteBuffer; import com.sun.media.datasink.RandomAccess; import java.util.Vector; import java.util.Hashtable; import java.awt.Dimension; public class QuicktimeMux extends com.sun.media.multiplexer.BasicMux { private int removeCount = 0; //$$ TODO: REMOVE // TODO: remove unused variables $$$$ private boolean sourceConnected = false; private boolean sinkConnected = false; private boolean closed = false; private boolean opened = false; private int debugCounter = 0; private Hashtable streamNumberHash; private TrakInfo[] trakInfoArray; private int dataSize = 0; private Format [] rgbFormats; private Format [] yuvFormats; // private int[] suggestedBufferSizes; // private int[] suggestedBufferSizeOffsets; private int[] scaleOffsets; private boolean[] endOfMediaStatus; private int numberOfEoms = 0; private int numberOfTracks = 0; private int numberOfSupportedTracks = 0; // NOTE: Currently, only audio and video tracks are supported private final static String VIDEO = "vide"; private final static String AUDIO = "soun"; private long mdatOffset; private long moovOffset; private int mdatLength; private int moovLength; private long mvhdDurationOffset; private static Hashtable audioFourccMapper = new Hashtable(); private static Hashtable videoFourccMapper = new Hashtable(); private final int movieTimeScale = 60000; private final int DEFAULT_FRAME_RATE = 15; private final int DEFAULT_FRAME_DURATION = (int) ((1. / DEFAULT_FRAME_RATE) * movieTimeScale); private final int TRAK_ENABLED = 1; private final int TRAK_IN_MOVIE = 2; // private final int TRAK_IN_PREVIEW = 4; // private final int TRAK_IN_POSTER = 4; private final static int DATA_SELF_REFERENCE_FLAG = 0x1; private final static boolean ALWAYS_USE_ONE_ENTRY_FOR_STTS = false; private final static int EPSILON_DURATION = 1000000; // In Nanoseconds [1 msec] private final static int MVHD_ATOM_SIZE = 100; private final static int TKHD_ATOM_SIZE = 84; private final static int MDHD_ATOM_SIZE = 24; // Two passes are required to create a streamable quicktime file. private boolean requireTwoPass = true; static { // Note AudioFormat.LINEAR is a special case because, the // fourcc code could be 'twos' or 'raw ' audioFourccMapper.put(AudioFormat.ALAW, AudioFormat.ALAW); // audioFourccMapper.put(AudioFormat.ULAW, AudioFormat.ULAW); // audioFourccMapper.put(AudioFormat.ULAW, "ulaw"); // For some reason, in AudioFormat, the ULAW string is in all // upper case. Need to investigate audioFourccMapper.put("ulaw", "ulaw"); audioFourccMapper.put(AudioFormat.IMA4, AudioFormat.IMA4); audioFourccMapper.put(AudioFormat.GSM, "agsm"); audioFourccMapper.put(AudioFormat.MAC3, AudioFormat.MAC3); audioFourccMapper.put(AudioFormat.MAC6, AudioFormat.MAC6); videoFourccMapper.put(VideoFormat.RGB, VideoFormat.RGB); videoFourccMapper.put(VideoFormat.CINEPAK, VideoFormat.CINEPAK); videoFourccMapper.put(VideoFormat.JPEG, VideoFormat.JPEG); videoFourccMapper.put(VideoFormat.H261, VideoFormat.H261); videoFourccMapper.put(VideoFormat.H263, VideoFormat.H263); videoFourccMapper.put(VideoFormat.INDEO32, VideoFormat.INDEO32); videoFourccMapper.put(VideoFormat.INDEO41, VideoFormat.INDEO41); videoFourccMapper.put(VideoFormat.INDEO50, VideoFormat.INDEO50); videoFourccMapper.put(VideoFormat.MJPG, VideoFormat.MJPG); videoFourccMapper.put(VideoFormat.MJPEGA, VideoFormat.MJPEGA); videoFourccMapper.put(VideoFormat.MJPEGB, VideoFormat. MJPEGB); videoFourccMapper.put(VideoFormat.MPEG, VideoFormat. MPEG); videoFourccMapper.put(VideoFormat.RPZA, VideoFormat.RPZA); // Apple Video videoFourccMapper.put(VideoFormat.YUV, "yuv2"); // Component Video // TODO: ADD DV - NTSC // TODO: Add Animation ['rle '], BMP, Graphics ['smc '], Sorenson, TGA, TIFF, PNG // TODO: Add Kodak Photo CD ['kpcd'] } public QuicktimeMux() { supportedInputs = new Format[2]; supportedInputs[0] = new AudioFormat(null); supportedInputs[1] = new VideoFormat(null); supportedOutputs = new ContentDescriptor[1]; supportedOutputs[0] = new FileTypeDescriptor(FileTypeDescriptor.QUICKTIME); int NS = Format.NOT_SPECIFIED; rgbFormats = new Format[] { new RGBFormat(null, NS, Format.byteArray, NS, 16, 0x1F << 10, 0x1F << 5, 0x1F, 2, NS, RGBFormat.FALSE, RGBFormat.BIG_ENDIAN), new RGBFormat(null, NS, Format.byteArray, NS, 24, 1, 2, 3, 3, NS, RGBFormat.FALSE, NS), new RGBFormat(null, NS, Format.byteArray, NS, 32, 2, 3, 4, 4, NS, RGBFormat.FALSE, NS) }; yuvFormats = new Format[] { new YUVFormat(null, NS, Format.byteArray, NS, YUVFormat.YUV_YUYV | YUVFormat.YUV_SIGNED, NS, NS, 0, 1, 3) }; } public String getName() { return "Quicktime Multiplexer"; } // Big Endian Format for intersecting with the input format. Format bigEndian = new AudioFormat(null, AudioFormat.NOT_SPECIFIED, AudioFormat.NOT_SPECIFIED, AudioFormat.NOT_SPECIFIED, AudioFormat.BIG_ENDIAN, AudioFormat.NOT_SPECIFIED); public Format setInputFormat(Format input, int trackID) { if (trakInfoArray == null) { trakInfoArray = new TrakInfo[numTracks]; endOfMediaStatus = new boolean[numTracks]; } if (! (input instanceof VideoFormat || input instanceof AudioFormat) ) { trakInfoArray[trackID] = new TrakInfo(); trakInfoArray[trackID].format = input; trakInfoArray[trackID].supported = false; return input; } String encoding = input.getEncoding(); if (input instanceof VideoFormat) { if (videoFourccMapper.get(encoding.toLowerCase()) == null) { return null; } if ( encoding.equalsIgnoreCase(VideoFormat.RGB) ) { if (matches(input, rgbFormats) == null) return null; } if ( encoding.equalsIgnoreCase(VideoFormat.YUV) ) { if (matches(input, yuvFormats) == null) return null; } VideoTrakInfo vti = new VideoTrakInfo(); trakInfoArray[trackID] = vti; vti.supported = true; vti.type = VIDEO; vti.encoding = encoding; vti.format = input; // vti.videoFormat = (VideoFormat) input; vti.videoFormat = (VideoFormat) null; // filled in later in doProcess // float frameRate = ((VideoFormat)input).getFrameRate(); // vti.frameRate = frameRate; // if (frameRate > 0) { // that is it is not 0 or NOT_SPECIFIED // vti.frameDuration = (int) ((1 / frameRate) * movieTimeScale); // } else { // vti.frameDuration = DEFAULT_FRAME_DURATION; // } // System.out.println("frameDuration is " + vti.frameDuration); } else if (input instanceof AudioFormat) { if (encoding.equalsIgnoreCase(AudioFormat.LINEAR)) { AudioFormat af = (AudioFormat) input; if (af.getSampleSizeInBits() > 8) { if (af.getSigned() == AudioFormat.UNSIGNED) return null; if (af.getEndian() == AudioFormat.LITTLE_ENDIAN) return null; else if (af.getEndian() == AudioFormat.NOT_SPECIFIED) input = af.intersects(bigEndian); } } else { if (audioFourccMapper.get(encoding.toLowerCase()) == null) return null; } AudioTrakInfo ati = new AudioTrakInfo(); trakInfoArray[trackID] = ati; ati.supported = true; ati.type = AUDIO; ati.encoding = encoding; ati.format = input; ati.audioFormat = (AudioFormat) input; ati.frameSizeInBytes = ati.audioFormat.getFrameSizeInBits()/8; if (ati.frameSizeInBytes <= 0) ati.frameSizeInBytes = ati.audioFormat.getSampleSizeInBits() * ati.audioFormat.getChannels() / 8; //System.out.println("frameSizeInBytes is " + ati.frameSizeInBytes + " " + ati.audioFormat.getFrameSizeInBits()); if (encoding.equalsIgnoreCase(AudioFormat.IMA4)) { ati.samplesPerBlock = ati.IMA4_SAMPLES_PER_BLOCK; } else if (encoding.equalsIgnoreCase(AudioFormat.GSM)) { ati.samplesPerBlock = ati.GSM_SAMPLES_PER_BLOCK; } else if (encoding.equalsIgnoreCase(AudioFormat.MAC3)) { ati.samplesPerBlock = ati.MAC3_SAMPLES_PER_BLOCK; } else if (encoding.equalsIgnoreCase(AudioFormat.MAC6)) { ati.samplesPerBlock = ati.MAC6_SAMPLES_PER_BLOCK; } } //streamNumberHash = new Hashtable(numStreams); // suggestedBufferSizes = new int[numStreams]; // suggestedBufferSizeOffsets = new int[numStreams]; //endOfMediaStatus = new boolean[num]; // java initializes them to false, right ?? if (trakInfoArray[trackID].supported) { numberOfSupportedTracks++; // suggestedBufferSizes[i] = -1; // TODO: need this??? what is this // suggestedBufferSizeOffsets[i] = -1; // TODO: need this??? what is this } inputs[trackID] = input; return input; } public synchronized int doProcess(Buffer buffer, int trackID) { byte [] data; int length; if (buffer.isEOM()) { if (!endOfMediaStatus[trackID]) { endOfMediaStatus[trackID] = true; numberOfEoms++; if (numberOfEoms == numTracks) return super.doProcess(buffer, trackID); else return BUFFER_PROCESSED_OK; } } if (!trakInfoArray[trackID].initFormat) { if ( trakInfoArray[trackID] instanceof VideoTrakInfo ) { VideoTrakInfo vti = (VideoTrakInfo)trakInfoArray[trackID]; vti.videoFormat = (VideoFormat) buffer.getFormat(); vti.frameRate = vti.videoFormat.getFrameRate(); if (vti.frameRate > 0) { // that is it is not 0 or NOT_SPECIFIED vti.frameDuration = (int) ( ((1 / vti.frameRate) * movieTimeScale) + 0.5); } else { vti.frameRate = DEFAULT_FRAME_RATE; vti.frameDuration = DEFAULT_FRAME_DURATION; } } trakInfoArray[trackID].initFormat = true; } Object obj = buffer.getData(); if (obj == null) return BUFFER_PROCESSED_FAILED; data = (byte[]) obj; if (data == null) return BUFFER_PROCESSED_FAILED; length = buffer.getLength(); dataSize += length; TrakInfo trakInfo = trakInfoArray[trackID]; write(data, 0, length); int chunkOffset = (int) (filePointer - length); // For all tracks int chunkOffsetsIndex = trakInfo.chunkOffsetsIndex++; int numChunkOffsetsArraysUsed = trakInfo.numChunkOffsetsArraysUsed; trakInfo.chunkOffsetsArray[numChunkOffsetsArraysUsed-1][chunkOffsetsIndex] = chunkOffset; if (++chunkOffsetsIndex >= trakInfo.MAX_CHUNKOFFSETS_ARRAYSIZE) { trakInfo.chunkOffsetsIndex = 0; trakInfo.chunkOffsetsArray[numChunkOffsetsArraysUsed] = new int[trakInfo.MAX_CHUNKOFFSETS_ARRAYSIZE]; trakInfo.numChunkOffsetsArraysUsed++; if (++numChunkOffsetsArraysUsed >= trakInfo.MAX_CHUNKOFFSETS_NUMARRAYS) { System.err.println("Cannot create quicktime file with more than " + (trakInfo.MAX_CHUNKOFFSETS_NUMARRAYS * trakInfo.MAX_CHUNKOFFSETS_ARRAYSIZE) + " chunks "); return BUFFER_PROCESSED_FAILED; } } String type = trakInfo.type; VideoTrakInfo vti = null; AudioTrakInfo ati = null; if (type.equals(VIDEO)) { vti = (VideoTrakInfo) trakInfo; /// For STSZ (sample size atom): START int sampleSizeIndex = vti.sampleSizeIndex++; int numSampleSizeArraysUsed = vti.numSampleSizeArraysUsed; vti.sampleSize[numSampleSizeArraysUsed-1][sampleSizeIndex] = length; if ( vti.constantSampleSize && (length != vti.sampleSize[0][0]) ) { vti.constantSampleSize = false; } // store durations based on (timestamp - previous timestamp). // The timeStamps will be used later to create the stts chunk // for the videotrack. if (vti.minDuration >= 0 ) { // $$ should this be > 0? long timeStamp = buffer.getTimeStamp(); if (timeStamp <= Buffer.TIME_UNKNOWN) { // $$ should this be <= 0? // If you get any timestamp with TIME_UNKNOWN (-1) then // don't bother storing any more time stamps in the timestamps array. // In this case we will have a stts chunk with 1 entry which will // be the frame duration obtained from the frame rate // System.out.println("Got neg timestamp. stop collecting timestamps"); vti.minDuration = -1; } else if (vti.totalFrames > 0) { long durationOfBufferData = (timeStamp - vti.previousTimeStamp); if (durationOfBufferData < vti.minDuration) vti.minDuration = durationOfBufferData; else if (durationOfBufferData > vti.maxDuration) vti.maxDuration = durationOfBufferData; int timeStampIndex = vti.timeStampIndex++; int numTimeStampArraysUsed = vti.numTimeStampArraysUsed; // System.out.println((numTimeStampArraysUsed-1) + " : " + // timeStampIndex + " : " + durationOfBufferData); vti.timeStamps[numTimeStampArraysUsed-1][timeStampIndex] = durationOfBufferData; if (++timeStampIndex >= vti.MAX_TIMESTAMP_ARRAYSIZE) { vti.timeStampIndex = 0; // System.out.println("Creating new timeStamps array"); vti.timeStamps[numTimeStampArraysUsed] = new long[vti.MAX_TIMESTAMP_ARRAYSIZE]; vti.numTimeStampArraysUsed++; if (++numTimeStampArraysUsed >= vti.MAX_TIMESTAMP_NUMARRAYS) { System.err.println("Cannot create quicktime file with more than " + (vti.MAX_TIMESTAMP_NUMARRAYS * vti.MAX_TIMESTAMP_ARRAYSIZE) + " frames "); return BUFFER_PROCESSED_FAILED; } } } vti.previousTimeStamp = timeStamp; } if (++sampleSizeIndex >= vti.MAX_SAMPLE_SIZE_ARRAYSIZE) { vti.sampleSizeIndex = 0; vti.sampleSize[numSampleSizeArraysUsed] = new int[vti.MAX_SAMPLE_SIZE_ARRAYSIZE]; vti.numSampleSizeArraysUsed++; if (++numSampleSizeArraysUsed >= vti.MAX_SAMPLE_SIZE_NUMARRAYS) { System.err.println("Cannot create quicktime file with more than " + (vti.MAX_SAMPLE_SIZE_NUMARRAYS * vti.MAX_SAMPLE_SIZE_ARRAYSIZE) + " samples "); return BUFFER_PROCESSED_FAILED; } } /// For STSZ (sample size atom): END /// For STSS (sync sample atom): START boolean keyframe = ((buffer.getFlags() & Buffer.FLAG_KEY_FRAME) > 0); if (keyframe) { int keyFrameIndex = vti.keyFrameIndex++; int numKeyFrameArraysUsed = vti.numKeyFrameArraysUsed; vti.keyFrames[numKeyFrameArraysUsed-1][keyFrameIndex] = vti.totalFrames+1; // Frame Numbering starts from 1, not 0 if (++keyFrameIndex >= vti.MAX_KEYFRAME_ARRAYSIZE) { vti.keyFrameIndex = 0; // System.out.println("Create new keyframe array for index " + // numKeyFrameArraysUsed); vti.keyFrames[numKeyFrameArraysUsed] = new int[vti.MAX_KEYFRAME_ARRAYSIZE]; vti.numKeyFrameArraysUsed++; if (++numKeyFrameArraysUsed >= vti.MAX_KEYFRAME_NUMARRAYS) { System.err.println("Cannot create quicktime file with more than " + (vti.MAX_KEYFRAME_NUMARRAYS * vti.MAX_KEYFRAME_ARRAYSIZE) + " keyframes "); //close(); return BUFFER_PROCESSED_FAILED; } } } /// For STSS (sync sample atom): END } else { // TODO: for now else means audio // But this won't be the case when we support // tracks like MUSI (ie midi) etc. ati = (AudioTrakInfo) trakInfo; // System.out.println("audio: chunk length is " + length); /// For STSC (sample per chunk): START int samplesPerChunk = (length / ati.frameSizeInBytes) * ati.samplesPerBlock; // System.out.println("samplesPerChunk is " + samplesPerChunk); ati.numSamples += samplesPerChunk; if (ati.previousSamplesPerChunk != samplesPerChunk) { // Another samplesPerChunk entry int samplesPerChunkIndex = ati.samplesPerChunkIndex; int numSamplesPerChunkArraysUsed = ati.numSamplesPerChunkArraysUsed; ati.samplesPerChunkArray[ numSamplesPerChunkArraysUsed-1 ][samplesPerChunkIndex] = trakInfo.totalFrames + 1; // Chunk numbers start from 1 samplesPerChunkIndex++; ati.samplesPerChunkArray[ numSamplesPerChunkArraysUsed-1 ][samplesPerChunkIndex] = samplesPerChunk; samplesPerChunkIndex++; ati.samplesPerChunkIndex = samplesPerChunkIndex; ati.previousSamplesPerChunk = samplesPerChunk; if (++samplesPerChunkIndex >= ati.MAX_SAMPLESPERCHUNK_ARRAYSIZE) { ati.samplesPerChunkIndex = 0; ati.samplesPerChunkArray[numSamplesPerChunkArraysUsed] = new int[ati.MAX_SAMPLESPERCHUNK_ARRAYSIZE]; ati.numSamplesPerChunkArraysUsed++; if (++numSamplesPerChunkArraysUsed >= ati.MAX_SAMPLESPERCHUNK_NUMARRAYS) { System.err.println("Cannot create quicktime file with more than " + (ati.MAX_SAMPLESPERCHUNK_NUMARRAYS * ati.MAX_SAMPLESPERCHUNK_ARRAYSIZE) + " chunks "); //close(); return BUFFER_PROCESSED_FAILED; } } } /// For STSC (sample per chunk): END } // if (length > suggestedBufferSizes[streamNumber]) // suggestedBufferSizes[streamNumber] = length; // //} trakInfo.totalFrames++; // if (isVideoFormat) // totalVideoFrames++; // totalFrames++; // // Handle this case properly. Internal error? // if ( totalFrames >= MAX_FRAMES_STORED ) { // System.err.println("Cannot store more than " + MAX_FRAMES_STORED + // " frames"); // close(); // return; // } // // System.out.println("Frame " + totalFrames); return BUFFER_PROCESSED_OK; } protected void writeHeader() { mdatOffset = filePointer; bufClear(); bufWriteInt(0); bufWriteBytes("mdat"); bufFlush(); dataSize = 0; } public boolean requireTwoPass() { return requireTwoPass; } protected void writeFooter() { moovOffset = filePointer; seek((int)mdatOffset); bufClear(); mdatLength = 8 + dataSize; bufWriteInt(mdatLength); bufFlush(); seek((int)moovOffset); writeMOOV(); int maxTrackDuration = -1; for (int i = 0; i < numTracks; i++) { if (!trakInfoArray[i].supported) continue; // System.out.println("Duration of track 1 " + // trakInfoArray[i].duration); writeSize(trakInfoArray[i].tkhdDurationOffset, trakInfoArray[i].duration); if (trakInfoArray[i].type.equals(VIDEO)) { writeSize(trakInfoArray[i].mdhdDurationOffset, trakInfoArray[i].duration); } if (trakInfoArray[i].duration > maxTrackDuration) { maxTrackDuration = trakInfoArray[i].duration; } } // update duartion at mvhdDurationOffset writeSize(mvhdDurationOffset, maxTrackDuration); RandomAccess st; if (requireTwoPass && (sth != null) && (sth instanceof RandomAccess) ) { // See if there is space to write the streamable quicktime file. if ( (st = (RandomAccess) sth).write(-1, moovLength + mdatLength)) { updateSTCO(); // At this point the non-streamable file cannot be played as the // updateSTCO updates the offsets so as to work with the streamable // file. But if the 2 st.write calls don't succeed then you will // have neither streamable nor non-streamable file. // But this shouldn't happen due to lack of disk space because // the 'if' statement succeeds only if a dummy file of the // required size could be created. write(null, 0, -1); // EOS. I don't think this is necessary as it is nop $$$ // TODO: $$$ If these 2 writes don't succeed, then throw appropriate // Exception. "Error creating Quicktime file. st.write(moovOffset, moovLength); st.write(mdatOffset, mdatLength); // Done writing moov and mdat chunks to streamable file. } else { System.err.println("No space to write streamable file"); } st.write(-1, -1); } } private int writeMOOV() { long offset = filePointer; bufClear(); bufWriteInt(0); // This is filled in later bufWriteBytes("moov"); bufFlush(); int size = 8; size += writeMVHD(); for (int i = 0; i < numTracks; i++) { if (!trakInfoArray[i].supported) continue; size += writeTRAK(i, trakInfoArray[i].type); } moovLength = size; return writeSize(offset, size); } private int writeMVHD() { bufClear(); bufWriteInt(MVHD_ATOM_SIZE + 8); bufWriteBytes("mvhd"); bufWriteInt(0); // Skip version(1), flags(3) bufWriteInt(0); // create time bufWriteInt(0); // mod time bufWriteInt(movieTimeScale); // time scale mvhdDurationOffset = filePointer; bufWriteInt(0); // duration. Will be filled at the end bufWriteInt(65536); // preferredRate bufWriteShort((short)0xff); // preferredVolume // 10 reserved bytes bufWriteInt(0); // reserved bufWriteInt(0); // reserved bufWriteShort((short)0); // reserved ///////////////////////// bufFlush(); writeMatrix(); bufClear(); bufWriteInt(0); // previewTime bufWriteInt(0); // previewDuration bufWriteInt(0); // posterTime bufWriteInt(0); // selectionTime bufWriteInt(0); // selectionDuration bufWriteInt(0); // currentTime bufWriteInt(numberOfSupportedTracks + 1); // nextTrackID bufFlush(); return MVHD_ATOM_SIZE + 8; } // TODO: Choose a good name private int writeSize(long offset, int size) { long currentOffset = filePointer; seek((int)offset); bufClear(); bufWriteInt(size); bufFlush(); seek((int)currentOffset); return size; } /** * Required atoms are tkhd and mdia */ private int writeTRAK(int streamNumber, String type) { long offset = filePointer; bufClear(); bufWriteInt(0); bufWriteBytes("trak"); bufFlush(); int size = 8; size += writeTKHD(streamNumber, type); size += writeMDIA(streamNumber, type); return writeSize(offset, size); } private int writeTKHD(int streamNumber, String type) { int width = 0; int height = 0; int duration = 0; int volume = 0; // For TKHD, the duration for all the tracks is computed // based on movieTimeScale if (type.equals(VIDEO)) { VideoTrakInfo videoTrakInfo = (VideoTrakInfo) trakInfoArray[streamNumber]; Dimension size = null; VideoFormat vf = videoTrakInfo.videoFormat; if (vf != null) { size = vf.getSize(); } if (size != null) { width = size.width; height = size.height; } else { // TODO: throw internal error } // System.out.println("tkhd: width, height " + width + " : " + height); // videoTrakInfo.duration = videoTrakInfo.frameDuration * videoTrakInfo.totalFrames; duration = videoTrakInfo.duration; } else { AudioTrakInfo audioTrakInfo = (AudioTrakInfo) trakInfoArray[streamNumber]; float sampleRate = (int) audioTrakInfo.audioFormat.getSampleRate(); float epsilon = 0.01F; // TODO: CHECK TO SEE IF THIS EPSILON MAKES SENSE duration = (int) ( (audioTrakInfo.numSamples / sampleRate) * movieTimeScale + epsilon ); audioTrakInfo.duration = duration; // System.out.println("duration of audio track " + (streamNumber+1) + // " is " + audioTrakInfo.duration); volume = 255; } bufClear(); bufWriteInt(TKHD_ATOM_SIZE + 8); bufWriteBytes("tkhd"); bufWriteInt(TRAK_ENABLED | TRAK_IN_MOVIE); // version + flag [track is enabled] bufWriteInt(0); // create time bufWriteInt(0); // mod time bufWriteInt(streamNumber+1); bufWriteInt(0); // reserved trakInfoArray[streamNumber].tkhdDurationOffset = filePointer; bufWriteInt(duration); // duration. Will be updated later with computed duration bufWriteInt(0); // reserved bufWriteInt(0); // reserved bufWriteShort((short)0); // layer TODO bufWriteShort((short)0); // alternate group TODO bufWriteShort((short)volume); bufWriteShort((short)0); // reserved bufFlush(); writeMatrix(); bufClear(); bufWriteInt(width * 65536); // Track Width, fixed point bufWriteInt(height * 65536); // Track Height, fixed point bufFlush(); return TKHD_ATOM_SIZE + 8; } /** * Required atoms are mdhd */ private int writeMDIA(int streamNumber, String type) { long offset = filePointer; bufClear(); bufWriteInt(0); bufWriteBytes("mdia"); bufFlush(); int size = 8; size += writeMDHD(streamNumber, type); size += writeMhlrHdlr(streamNumber, type); size += writeMINF(streamNumber, type); return writeSize(offset, size); } private void writeMatrix() { // Matrix bufClear(); bufWriteInt(65536); bufWriteInt(0); bufWriteInt(0); bufWriteInt(0); bufWriteInt(65536); bufWriteInt(0); bufWriteInt(0); bufWriteInt(0); bufWriteInt(1073741824); bufFlush(); } private int writeMDHD(int streamNumber, String type) { int timeScale = 0; int duration = 0; if (type.equals(VIDEO)) { // For video, the timescale is typically the movie time // scale. So we can have the same duration value that // we had in TKHD for this track timeScale = movieTimeScale; VideoTrakInfo videoTrakInfo = (VideoTrakInfo) trakInfoArray[streamNumber]; duration = videoTrakInfo.duration; // Computed by writeTKHD earlier // System.out.println("duration of track " + (streamNumber+1) + // " is " + videoTrakInfo.duration); } else { // For audio, the timeScale is typically the sampleRate // audio duration should be computed based on this timeScale // Due to this different timeScale, the duration recorded in // MDHD will have a different value from that in the // corresponding TKHD AudioTrakInfo audioTrakInfo = (AudioTrakInfo) trakInfoArray[streamNumber]; timeScale = (int) audioTrakInfo.audioFormat.getSampleRate(); duration = audioTrakInfo.numSamples; //? TODO CHECK } bufClear(); bufWriteInt(MDHD_ATOM_SIZE + 8); bufWriteBytes("mdhd"); bufWriteInt(1); // version + flag [track is enabled] bufWriteInt(0); // create time bufWriteInt(0); // mod time bufWriteInt(timeScale); trakInfoArray[streamNumber].mdhdDurationOffset = (int) filePointer; bufWriteInt(duration); // duration. Will be updated later for video with computed duration bufWriteShort((short)0); // language bufWriteShort((short)0); // quality bufFlush(); return MDHD_ATOM_SIZE + 8; } /** * Required atoms: vmhd or smhd and hdlr */ private int writeMINF(int streamNumber, String type) { long offset = filePointer; bufClear(); bufWriteInt(0); // Size bufWriteBytes("minf"); bufFlush(); int size = 8; if (type.equals(VIDEO)) { size += writeVMHD(streamNumber, type); } else { size += writeSMHD(streamNumber, type); } size += writeDHlrHdlr(streamNumber, type); ///??? Data Handler size += writeDINF(streamNumber, type); size += writeSTBL(streamNumber, type); return writeSize(offset, size); } private int writeVMHD(int streamNumber, String type) { bufClear(); bufWriteInt(8 + 12); bufWriteBytes("vmhd"); bufWriteInt(1); // Version + Flags bufWriteShort((short)64); // Graphics Mode bufWriteShort((short)32768); // Opcolor: Red bufWriteShort((short)32768); // Opcolor: Green bufWriteShort((short)32768); // Opcolor: Blue bufFlush(); return (8 + 12); } private int writeSMHD(int streamNumber, String type) { bufClear(); bufWriteInt(8 + 8); bufWriteBytes("smhd"); bufWriteInt(0); // Version + Flags bufWriteShort((short)0); // Balance bufWriteShort((short)0); // Reserved bufFlush(); return (8 + 8); } private int writeMhlrHdlr(int streamNumber, String type) { bufClear(); bufWriteInt(8 + 28); bufWriteBytes("hdlr"); bufWriteInt(0); // Version + Flags bufWriteBytes("mhlr"); bufWriteBytes(type); bufWriteBytes(" "); // Component Manufacturer ?? bufWriteInt(0); // flags ?? bufWriteInt(0); // flags mask ?? bufWriteBytes(" "); // Component Name ?? bufFlush(); return (8 + 28); } private int writeDHlrHdlr(int streamNumber, String type) { bufClear(); bufWriteInt(8 + 28); bufWriteBytes("hdlr"); bufWriteInt(0); // Version + Flags bufWriteBytes("dhlr"); bufWriteBytes("alis"); bufWriteBytes(" "); // Component Manufacturer ?? bufWriteInt(0); // flags ?? bufWriteInt(0); // flags mask ?? bufWriteBytes(" "); // Component Name ?? bufFlush(); return (8 + 28); } /** * Structure of dinf * size: 36 [4] * dinf [4] * size: 28 [4] * dref [4] * version+flags [4] * num_entries [4] * size 12 [4] * type alis [4] * version+flags 1 <self_contained> [4] * data 0 [0] */ private int writeDINF(int streamNumber, String type) { bufClear(); bufWriteInt(36); bufWriteBytes("dinf"); bufWriteInt(28); bufWriteBytes("dref"); bufWriteInt(0); // Version + Flags bufWriteInt(1); // Number of entries bufWriteInt(12); bufWriteBytes("alis"); bufWriteInt(DATA_SELF_REFERENCE_FLAG); // Version + Flags bufFlush(); return 36; } private int writeSTBL(int streamNumber, String type) { long offset = filePointer; bufClear(); bufWriteInt(0); bufWriteBytes("stbl"); bufFlush(); int size = 8; size += writeSTSD(streamNumber, type); size += writeSTTS(streamNumber, type); size += writeSTSS(streamNumber, type); size += writeSTSC(streamNumber, type); size += writeSTSZ(streamNumber, type); size += writeSTCO(streamNumber, type); return writeSize(offset, size); } private int writeSTSD(int streamNumber, String type) { long offset = filePointer; bufClear(); bufWriteInt(0); bufWriteBytes("stsd"); int size = 8; bufWriteInt(0); // Version + Flags bufWriteInt(1); // Number of entries bufFlush(); size += 8; if (type.equals(VIDEO)) { size += writeVideoSampleDescription(streamNumber, type); } else { // Will be Audio size += writeAudioSampleDescription(streamNumber, type); } return writeSize(offset, size); } private int writeVideoSampleDescription(int streamNumber, String type) { VideoTrakInfo videoTrakInfo = (VideoTrakInfo) trakInfoArray[streamNumber]; int width = videoTrakInfo.videoFormat.getSize().width; int height = videoTrakInfo.videoFormat.getSize().height; long offset = filePointer; // Sample Description Table bufClear(); bufWriteInt(0); // Size int size = 4; String encoding = videoTrakInfo.encoding; String fourcc = null; int bitsPerPixel; if (encoding.equalsIgnoreCase(VideoFormat.RGB)) { RGBFormat rgbFormat = (RGBFormat) videoTrakInfo.format; bitsPerPixel = rgbFormat.getBitsPerPixel(); fourcc = "raw "; } else { fourcc = (String) videoFourccMapper.get(encoding.toLowerCase()); // Check: For all non-RGB formats, set bitsPerPixel, ie depth // to 24. The stsd chunk has a depth field that is set to this value bitsPerPixel = 24; } bufWriteBytes(fourcc); size += 4; // 6 [4 + 2] reserved bytes bufWriteInt(0); // reserved bufWriteShort((short)0); // reserved size += 6; /** Data Reference Index: * A 16-bit integer that contains the index of the data * reference to use to retrieve data associated with samples * that use this sample description. Data references are * stored in data reference atoms. */ bufWriteShort((short)1); // Data Reference index. size += 2; // This section takes up 70 bytes bufWriteShort((short)0); // Version # of compressed data bufWriteShort((short)0); // Revision level bufWriteBytes("appl"); // Vendor bufWriteInt(1023); // $$ TODO: ASK: Temporal Quality [No info to set this value] bufWriteInt(1023); // $$ TODO: ASK Spatial Quality [No info to set this value] bufWriteShort((short)width); bufWriteShort((short)height); // $$ Horizontal Resolution in pixels/inch [No info to set this value] bufWriteInt(4718592); // 72 pixels/inch * 65536 // $$ Vertical Resolution in pixels/inch [No info to set this value] bufWriteInt(4718592); // 72 pixels/inch * 65536 bufWriteInt(0); // Data size [always set to 0] // A 16-bit integer that indicates how many frames of compressed data // are stored in each sample. Usually set to 1. bufWriteShort((short)1); // Compressor name takes up 32 bytes, fill the rest with space bufWriteBytes(fourcc); // TODO: use full name, e.g Cinepak instead of cvid bufWriteBytes(" "); // 28 spaces bufWriteShort((short)bitsPerPixel); bufWriteShort((short)-1); // Use Default color table. bufFlush(); size += 70; return writeSize(offset, size); } private int writeAudioSampleDescription(int streamNumber, String type) { AudioTrakInfo audioTrakInfo = (AudioTrakInfo) trakInfoArray[streamNumber]; AudioFormat audioFormat = audioTrakInfo.audioFormat; int channels = audioFormat.getChannels(); int sampleSizeInBits = audioFormat.getSampleSizeInBits(); int sampleRate = (int) audioFormat.getSampleRate(); long offset = filePointer; // Sample Description Table bufClear(); bufWriteInt(0); // Size int size = 4; String encoding = audioTrakInfo.encoding; String fourcc; if (encoding.equalsIgnoreCase(AudioFormat.LINEAR)) { // for 16 bit fourcc will always be 'twos' // for 8 bit signed fourcc is 'twos' // for 8 bit unsigned fourcc is 'raw ' if ( (sampleSizeInBits == 8) && (audioFormat.getSigned() == AudioFormat.UNSIGNED) ) { fourcc = "raw "; } else { fourcc = "twos"; } } else { fourcc = (String) audioFourccMapper.get(encoding.toLowerCase()); } bufWriteBytes(fourcc); size += 4; // 6 [4 + 2] reserved bytes bufWriteInt(0); // reserved bufWriteShort((short)0); // reserved size += 6; /** Data Reference Index: * A 16-bit integer that contains the index of the data * reference to use to retrieve data associated with samples * that use this sample description. Data references are * stored in data reference atoms. */ bufWriteShort((short)1); // Data Reference index. size += 2; // This section takes up 20 bytes bufWriteShort((short)0); // Version: Must be set to 0 bufWriteShort((short)0); // Revision Level: Must be set to 0 bufWriteInt(0); // Vendor: Must be set to 0 bufWriteShort((short)channels); bufWriteShort((short)sampleSizeInBits); bufWriteShort((short)0); // Compression ID: Must be set to 0 bufWriteShort((short)0); // Packet Size: Must be set to 0 bufWriteInt(sampleRate * 65536); // Unsigned Fixed Point bufFlush(); size += 20; return writeSize(offset, size); } private int writeSTTS(int streamNumber, String type) { long offset = filePointer; bufClear(); bufWriteInt(0); bufWriteBytes("stts"); int size = 8; bufWriteInt(0); // Version + Flags size += 4; if (type.equals(VIDEO)) { VideoTrakInfo vti = (VideoTrakInfo) trakInfoArray[streamNumber]; if (ALWAYS_USE_ONE_ENTRY_FOR_STTS || (vti.minDuration <= -1) /* $$ should it be <= 0? */ || ( (vti.maxDuration - vti.minDuration) < EPSILON_DURATION ) ) { bufWriteInt(1); // Number of entries size += 4; bufWriteInt(vti.totalFrames); bufWriteInt(vti.frameDuration); vti.duration = vti.totalFrames * vti.frameDuration; size += 8; } else { // TODO: Collapse entries with the same duration so that // to make this table small // System.out.println("numFrames is " + vti.totalFrames); bufWriteInt(vti.totalFrames); // Number of entries size += 4; vti.duration = 0; long[][] timeStamps = vti.timeStamps; int indexi = 0; int indexj = 0; long timeStamp = 0; int numEntries = vti.totalFrames-1; int bytesPerLoop = 2* 4; int actualBufSize = ( (maxBufSize - 200) / bytesPerLoop ) * bytesPerLoop; int requiredSize = numEntries * bytesPerLoop; int numLoops; int entriesPerLoop; if (requiredSize <= actualBufSize) { numLoops = 1; entriesPerLoop = numEntries; } else { numLoops = requiredSize / actualBufSize; if ( ( (float) requiredSize / actualBufSize ) > numLoops ) numLoops++; entriesPerLoop = actualBufSize / bytesPerLoop; } for (int ii = 0; ii < numLoops; ii++) { for (int jj = 0; jj < entriesPerLoop; jj++) { bufWriteInt(1); // Num frames with this duration timeStamp = timeStamps[indexi][indexj++]; // duration int dur = (int) ( 0.5 + (timeStamp / 1000000000.) * movieTimeScale); bufWriteInt(dur); vti.duration += dur; size += 8; if (indexj >= vti.MAX_TIMESTAMP_ARRAYSIZE) { indexi++; indexj = 0; } } bufFlush(); bufClear(); if (ii == numLoops -2) { entriesPerLoop = numEntries - (numLoops -1) * entriesPerLoop; } } // Last frame gets same duration as previous frame if (vti.totalFrames > 1) { bufWriteInt(1); // Num frames with this duration int dur = (int) ( (timeStamp / 1000000000.) * movieTimeScale); bufWriteInt(dur); size += 8; vti.duration += dur; // System.out.println("vti.duration final is " + vti.duration); } } for (int i = 0; i < vti.numTimeStampArraysUsed; i++) { vti.timeStamps[i] = null; // Can be garbage collected } } else { // Will be Audio AudioTrakInfo ati = (AudioTrakInfo) trakInfoArray[streamNumber]; bufWriteInt(1); // Number of entries size += 4; bufWriteInt(ati.numSamples); bufWriteInt(1); // Sample duration size += 8; } if (bufLength > 0) { bufFlush(); } return writeSize(offset, size); } /** * Sync Sample Atom */ private int writeSTSS(int streamNumber, String type) { if ( !type.equals(VIDEO) ) { // sync sample atom applies only to video tracks return 0; } VideoTrakInfo vti = (VideoTrakInfo) trakInfoArray[streamNumber]; int numKeyFrameArraysUsed = vti.numKeyFrameArraysUsed; int numKeyFrames = (numKeyFrameArraysUsed - 1) * vti.MAX_KEYFRAME_ARRAYSIZE + vti.keyFrameIndex; // System.out.println("stts:numKeyFrames is " + numKeyFrames); if ( numKeyFrames == 0) { com.sun.media.Log.warning("Error: There should be atleast 1 keyframe in the track. All frames are now treated as keyframes"); return 0; } if (numKeyFrames == vti.totalFrames) { // No sync sample atom as all frames are key frames for (int i = 0; i < numKeyFrameArraysUsed; i++) { vti.keyFrames[i] = null; // Can be garbage collected } return 0; } long offset = filePointer; bufClear(); bufWriteInt(0); bufWriteBytes("stss"); int size = 8; bufWriteInt(0); // Version + Flags size += 4; int[][] keyFrames = vti.keyFrames; bufWriteInt(numKeyFrames); // Number of entries size += 4; int numEntries = numKeyFrames; int bytesPerLoop = 1 * 4; int actualBufSize = ( (maxBufSize - 200) / bytesPerLoop ) * bytesPerLoop; int requiredSize = numEntries * bytesPerLoop; int numLoops; int entriesPerLoop; if (requiredSize <= actualBufSize) { numLoops = 1; entriesPerLoop = numEntries; } else { numLoops = requiredSize / actualBufSize; if ( ( (float) requiredSize / actualBufSize ) > numLoops ) numLoops++; entriesPerLoop = actualBufSize / bytesPerLoop; } int indexi = 0; int indexj = 0; for (int ii = 0; ii < numLoops; ii++) { for (int jj = 0; jj < entriesPerLoop; jj++) { bufWriteInt(keyFrames[indexi][indexj++]); if (indexj >= vti.MAX_KEYFRAME_ARRAYSIZE) { indexi++; indexj = 0; } } bufFlush(); bufClear(); if (ii == numLoops -2) { entriesPerLoop = numEntries - (numLoops -1) * entriesPerLoop; } } size += (numKeyFrames * 4); return writeSize(offset, size); } private int writeSTSC(int streamNumber, String type) { long offset = filePointer; bufClear(); bufWriteInt(0); bufWriteBytes("stsc"); int size = 8; bufWriteInt(0); // Version + Flags size += 4; if (type.equals(VIDEO)) { // All chunks have only one sample, ie video frame VideoTrakInfo vti = (VideoTrakInfo) trakInfoArray[streamNumber]; bufWriteInt(1); // Number of entries size += 4; bufWriteInt(1); // First Chunk bufWriteInt(1); // Samples/Chunk bufWriteInt(1); // Sample Description size += 12; } else { // Will be Audio AudioTrakInfo ati = (AudioTrakInfo) trakInfoArray[streamNumber]; int numberOfEntries = ( (ati.numSamplesPerChunkArraysUsed - 1) * ati.MAX_SAMPLESPERCHUNK_ARRAYSIZE + ati.samplesPerChunkIndex) / 2; bufWriteInt(numberOfEntries); // Number of entries size += 4; int numEntries = numberOfEntries; int bytesPerLoop = 3 * 4; int actualBufSize = ( (maxBufSize - 200) / bytesPerLoop ) * bytesPerLoop; int requiredSize = numEntries * bytesPerLoop; int numLoops; int entriesPerLoop; if (requiredSize <= actualBufSize) { numLoops = 1; entriesPerLoop = numEntries; } else { numLoops = requiredSize / actualBufSize; if ( ( (float) requiredSize / actualBufSize ) > numLoops ) numLoops++; entriesPerLoop = actualBufSize / bytesPerLoop; } int indexi = 0; int indexj = 0; int samplesPerChunkArray[][] = ati.samplesPerChunkArray; for (int ii = 0; ii < numLoops; ii++) { for (int jj = 0; jj < entriesPerLoop; jj++) { bufWriteInt(samplesPerChunkArray[indexi][indexj++]); // First Chunk bufWriteInt(samplesPerChunkArray[indexi][indexj++]); // Samples/Chunk bufWriteInt(1); // Sample Description ID if (indexj >= ati.MAX_SAMPLESPERCHUNK_ARRAYSIZE) { indexi++; indexj = 0; } } bufFlush(); bufClear(); if (ii == numLoops -2) { entriesPerLoop = numEntries - (numLoops -1) * entriesPerLoop; } } size += (numberOfEntries * 12); } if (bufLength > 0) { bufFlush(); } return writeSize(offset, size); } private int writeSTSZ(int streamNumber, String type) { long offset = filePointer; bufClear(); bufWriteInt(0); bufWriteBytes("stsz"); int size = 8; bufWriteInt(0); // Version + Flags size += 4; TrakInfo trakInfo = trakInfoArray[streamNumber]; if (type.equals(AUDIO)) { // All Samples have the same sample size bufWriteInt( 1 ); // sampleSize bufWriteInt( ((AudioTrakInfo)trakInfo).numSamples); size += 8; } else if (type.equals(VIDEO)) { VideoTrakInfo vti = (VideoTrakInfo) trakInfo; int numSampleSizeArraysUsed = vti.numSampleSizeArraysUsed; int numberOfEntries = trakInfo.totalFrames; if (trakInfo.constantSampleSize) { // All Samples have the same sample size // This is always the case for audio but sometimes for // video, for e.g uncompressed video. int sampleSize = vti.sampleSize[0][0]; bufWriteInt(sampleSize); // sampleSize bufWriteInt(numberOfEntries); size += 8; } else { int[][] sampleSize = vti.sampleSize; bufWriteInt(0); // implies that sampleSize is not constant bufWriteInt(numberOfEntries); // Number of Entries size += 8; int numEntries = numberOfEntries; int bytesPerLoop = 1 * 4; int actualBufSize = ( (maxBufSize - 200) / bytesPerLoop ) * bytesPerLoop; int requiredSize = numEntries * bytesPerLoop; int numLoops; int entriesPerLoop; if (requiredSize <= actualBufSize) { numLoops = 1; entriesPerLoop = numEntries; } else { numLoops = requiredSize / actualBufSize; if ( ( (float) requiredSize / actualBufSize ) > numLoops ) numLoops++; entriesPerLoop = actualBufSize / bytesPerLoop; } int indexi = 0; int indexj = 0; for (int ii = 0; ii < numLoops; ii++) { for (int jj = 0; jj < entriesPerLoop; jj++) { bufWriteInt(sampleSize[indexi][indexj++]); if (indexj >= vti.MAX_SAMPLE_SIZE_ARRAYSIZE) { indexi++; indexj = 0; } } bufFlush(); bufClear(); if (ii == numLoops -2) { entriesPerLoop = numEntries - (numLoops -1) * entriesPerLoop; } } size += (numberOfEntries * 4); } // Is this garbage collection necessary? for (int i = 0; i < numSampleSizeArraysUsed; i++) { vti.sampleSize[i] = null; // Can be garbage collected. } } if (bufLength > 0) { bufFlush(); } return writeSize(offset, size); } private int writeSTCO(int streamNumber, String type) { long offset = filePointer; bufClear(); bufWriteInt(0); bufWriteBytes("stco"); int size = 8; bufWriteInt(0); // Version + Flags size += 4; TrakInfo trakInfo = trakInfoArray[streamNumber]; int numChunkOffsetsArraysUsed = trakInfo.numChunkOffsetsArraysUsed; int[][] chunkOffsetsArray = trakInfo.chunkOffsetsArray; bufWriteInt(trakInfo.totalFrames); // Number of Entries size += 4; int numEntries = trakInfo.totalFrames; int bytesPerLoop = 1 * 4; int actualBufSize = ( (maxBufSize - 200) / bytesPerLoop ) * bytesPerLoop; int requiredSize = numEntries * bytesPerLoop; int numLoops; int entriesPerLoop; if (requiredSize <= actualBufSize) { numLoops = 1; entriesPerLoop = numEntries; } else { numLoops = requiredSize / actualBufSize; if ( ( (float) requiredSize / actualBufSize ) > numLoops ) numLoops++; entriesPerLoop = actualBufSize / bytesPerLoop; } int indexi = 0; int indexj = 0; trakInfo.chunkOffsetOffset = filePointer; int off; for (int ii = 0; ii < numLoops; ii++) { for (int jj = 0; jj < entriesPerLoop; jj++) { off = (int) (chunkOffsetsArray[indexi][indexj++] - mdatOffset); bufWriteInt(off); if (indexj >= trakInfo.MAX_CHUNKOFFSETS_ARRAYSIZE) { indexi++; indexj = 0; } } bufFlush(); bufClear(); if (ii == numLoops -2) { entriesPerLoop = numEntries - (numLoops -1) * entriesPerLoop; } } size += (trakInfo.totalFrames * 4); return writeSize(offset, size); } // Update the chunkoffset values. This needs to be done because // the MOOV chunk will be moved in front of the MDAT chunk to // create a streamable file private void updateSTCO() { for (int streamNumber = 0; streamNumber < trakInfoArray.length; streamNumber++) { TrakInfo trakInfo = trakInfoArray[streamNumber]; int numChunkOffsetsArraysUsed = trakInfo.numChunkOffsetsArraysUsed; int[][] chunkOffsetsArray = trakInfo.chunkOffsetsArray; int chunkOffsetOffset = trakInfo.chunkOffsetOffset; seek(chunkOffsetOffset); bufClear(); int numEntries = trakInfo.totalFrames; int bytesPerLoop = 1 * 4; int actualBufSize = ( (maxBufSize - 200) / bytesPerLoop ) * bytesPerLoop; int requiredSize = numEntries * bytesPerLoop; int numLoops; int entriesPerLoop; if (requiredSize <= actualBufSize) { numLoops = 1; entriesPerLoop = numEntries; } else { numLoops = requiredSize / actualBufSize; if ( ( (float) requiredSize / actualBufSize ) > numLoops ) numLoops++; entriesPerLoop = actualBufSize / bytesPerLoop; } int indexi = 0; int indexj = 0; int off; for (int ii = 0; ii < numLoops; ii++) { for (int jj = 0; jj < entriesPerLoop; jj++) { off = (int) (chunkOffsetsArray[indexi][indexj++] + moovLength); bufWriteInt(off); if (indexj >= trakInfo.MAX_CHUNKOFFSETS_ARRAYSIZE) { indexi++; indexj = 0; } } bufFlush(); bufClear(); if (ii == numLoops -2) { entriesPerLoop = numEntries - (numLoops -1) * entriesPerLoop; } } } } /*********************************************************************** * INNER CLASSES ***********************************************************************/ private class TrakInfo { boolean initFormat = false; boolean supported = false; String type; String encoding; Format format; long tkhdDurationOffset = -1; long mdhdDurationOffset = -1; int totalFrames = 0; // TODO changeName?: this is actually total # of chunks int duration; // duration of track // TODO: if constantSampleSize is always true for audio, // move it to VideoTrakInfo class. boolean constantSampleSize = true; final int MAX_CHUNKOFFSETS_NUMARRAYS = 1000; final int MAX_CHUNKOFFSETS_ARRAYSIZE = 1000; int numChunkOffsetsArraysUsed = 1; int chunkOffsetsIndex = 0; int chunkOffsetsArray[][]; int chunkOffsetOffset; public TrakInfo() { chunkOffsetsArray = new int[MAX_CHUNKOFFSETS_NUMARRAYS][]; chunkOffsetsArray[0] = new int[MAX_CHUNKOFFSETS_ARRAYSIZE]; } public String toString() { if (!supported) { System.out.println("No support for format " + format); } return (type + ": " + encoding + " : totalFrames " + totalFrames); } } private class VideoTrakInfo extends TrakInfo { VideoFormat videoFormat; float frameRate; int frameDuration; // TODO?: AT THIS POINT I DON"T THINK I NEED A sampleSize // array for audio. Because all samples will be of the same // size. If not the stsz atom for audio will be huge as we // need to list the size for every sample // In the extremely unlikely case that this is not true, // we need to move the next // 5 lines to the base class. If that is done, for audio // usr rle so that we don't need large arrays, also we need // to update constantSampleSize boolean and set it to false // if all samples are not of the same sample size final int MAX_SAMPLE_SIZE_NUMARRAYS = 1000; final int MAX_SAMPLE_SIZE_ARRAYSIZE = 1000*2; // Should be even [for rle audio] int numSampleSizeArraysUsed = 1; int sampleSizeIndex = 0; int sampleSize[][]; final int MAX_KEYFRAME_NUMARRAYS = 1000; final int MAX_KEYFRAME_ARRAYSIZE = 1000; int numKeyFrameArraysUsed = 1; int keyFrameIndex = 0; int keyFrames[][]; final int MAX_TIMESTAMP_NUMARRAYS = 1000; final int MAX_TIMESTAMP_ARRAYSIZE = 1000; int numTimeStampArraysUsed = 1; int timeStampIndex = 0; long timeStamps[][]; long minDuration = Long.MAX_VALUE; long maxDuration = -1; long previousTimeStamp; public VideoTrakInfo() { sampleSize = new int[MAX_SAMPLE_SIZE_NUMARRAYS][]; sampleSize[0] = new int[MAX_SAMPLE_SIZE_ARRAYSIZE]; keyFrames = new int[MAX_KEYFRAME_NUMARRAYS][]; keyFrames[0] = new int[MAX_KEYFRAME_ARRAYSIZE]; timeStamps = new long[MAX_TIMESTAMP_NUMARRAYS][]; timeStamps[0] = new long[MAX_TIMESTAMP_ARRAYSIZE]; } public String toString() { return (super.toString() + (" \n frameRate " + frameRate + " : frameDuration " + frameDuration)); } } private class AudioTrakInfo extends TrakInfo { AudioFormat audioFormat; final int IMA4_SAMPLES_PER_BLOCK = 64; final int GSM_SAMPLES_PER_BLOCK = 160; final int MAC3_SAMPLES_PER_BLOCK = 6; final int MAC6_SAMPLES_PER_BLOCK = 6; int samplesPerBlock = 1; int numSamples = 0; int frameSizeInBytes; // TODO: see if you need this // Note: the reason we don't have this array for video also // is that in the JMF architecture there will be one video // frame per Buffer (Chunk). That is one sample (frame) per chunk. // Note that the Sample Description ID will always be 1 final int MAX_SAMPLESPERCHUNK_NUMARRAYS = 1000; // Should be even as we will have <chunk#, samples/chunk> pairs final int MAX_SAMPLESPERCHUNK_ARRAYSIZE = 500*2; int numSamplesPerChunkArraysUsed = 1; int samplesPerChunkIndex = 0; int samplesPerChunkArray[][]; int previousSamplesPerChunk = -1; public AudioTrakInfo() { samplesPerChunkArray = new int[MAX_SAMPLESPERCHUNK_NUMARRAYS][]; samplesPerChunkArray[0] = new int[MAX_SAMPLESPERCHUNK_ARRAYSIZE]; samplesPerChunkArray[0][0] = 1; // first chunk samplesPerChunkArray[0][1] = -1; // sample/chunk not initialized } } }