All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ru.sbtqa.monte.media.quicktime.QuickTimeOutputStream Maven / Gradle / Ivy

There is a newer version: 1.1.0-JAVA7
Show newest version
/* @(#)QuickTimeOutputStream.java
 * Copyright © 2011 Werner Randelshofer, Switzerland. 
 * You may only use this software in accordance with the license terms.
 */
package ru.sbtqa.monte.media.quicktime;

import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.io.*;
import static java.lang.Math.*;
import static java.lang.System.arraycopy;
import static java.lang.System.err;
import java.nio.ByteOrder;
import java.util.Date;
import java.util.zip.DeflaterOutputStream;
import javax.imageio.stream.*;
import static ru.sbtqa.monte.media.AudioFormatKeys.*;
import ru.sbtqa.monte.media.Format;
import static ru.sbtqa.monte.media.FormatKeys.MediaType.AUDIO;
import static ru.sbtqa.monte.media.FormatKeys.MediaType.VIDEO;
import static ru.sbtqa.monte.media.VideoFormatKeys.*;
import static ru.sbtqa.monte.media.io.IOStreams.copy;
import ru.sbtqa.monte.media.io.ImageOutputStreamAdapter;
import ru.sbtqa.monte.media.math.Rational;
import static ru.sbtqa.monte.media.math.Rational.valueOf;
import static ru.sbtqa.monte.media.quicktime.AbstractQuickTimeStream.States.CLOSED;
import static ru.sbtqa.monte.media.quicktime.AbstractQuickTimeStream.States.FINISHED;
import static ru.sbtqa.monte.media.quicktime.AbstractQuickTimeStream.States.STARTED;

/**
 * This class provides low-level support for writing already encoded audio and
 * video samples into a QuickTime file.
 *
 * @author Werner Randelshofer
 * @version 1.0 2011-08-19 Created.
 */
public class QuickTimeOutputStream extends AbstractQuickTimeStream {

    /**
     * Creates a new instance.
     *
     * @param file the output file
     * @throws java.io.IOException TODO
     */
    public QuickTimeOutputStream(File file) throws IOException {
        if (file.exists()) {
            file.delete();
        }
        this.out = new FileImageOutputStream(file);
        this.streamOffset = 0;
        init();
    }

    /**
     * Creates a new instance.
     *
     * @param out the output stream.
     * @throws java.io.IOException TODO
     */
    public QuickTimeOutputStream(ImageOutputStream out) throws IOException {
        this.out = out;
        this.streamOffset = out.getStreamPosition();
        init();
    }

    private void init() {
        creationTime = new Date();
        modificationTime = new Date();
    }

    /**
     * Sets the time scale for this movie, that is, the number of time units
     * that pass per second in its time coordinate system.
     * 
     * The default value is 600.
     *
     * @param timeScale TODO
     */
    public void setMovieTimeScale(long timeScale) {
        if (timeScale < 1 || timeScale > (2L << 32)) {
            throw new IllegalArgumentException("timeScale must be between 1 and 2^32:" + timeScale);
        }
        this.movieTimeScale = timeScale;
    }

    /**
     * Returns the time scale of the movie.
     *
     * @return time scale
     * @see #setMovieTimeScale(long)
     */
    public long getMovieTimeScale() {
        return movieTimeScale;
    }

    /**
     * Returns the time scale of the media in a track.
     *
     * @param track Track index.
     * @return time scale
     * @see #setMovieTimeScale(long)
     */
    public long getMediaTimeScale(int track) {
        return tracks.get(track).mediaTimeScale;
    }

    /**
     * Returns the media duration of a track in the media's time scale.
     *
     * @param track Track index.
     * @return media duration
     */
    public long getMediaDuration(int track) {
        return tracks.get(track).mediaDuration;
    }

    /**
     * Returns the track duration in the movie's time scale without taking the
     * edit list into account.
     * 
     * The returned value is the media duration of the track in the movies's
     * time scale.
     *
     * @param track Track index.
     * @return unedited track duration
     */
    public long getUneditedTrackDuration(int track) {
        Track t = tracks.get(track);
        return t.mediaDuration * t.mediaTimeScale / movieTimeScale;
    }

    /**
     * Returns the track duration in the movie's time scale.
     * 
     * If the track has an edit-list, the track duration is the sum of all edit
     * durations.
     * 
     * If the track does not have an edit-list, then this method returns the
     * media duration of the track in the movie's time scale.
     *
     * @param track Track index.
     * @return track duration
     */
    public long getTrackDuration(int track) {
        return tracks.get(track).getTrackDuration(movieTimeScale);
    }

    /**
     * Returns the total duration of the movie in the movie's time scale.
     *
     * @return media duration
     */
    public long getMovieDuration() {
        long duration = 0;
        for (Track t : tracks) {
            duration = max(duration, t.getTrackDuration(movieTimeScale));
        }
        return duration;
    }

    /**
     * Sets the color table for videos with indexed color models.
     *
     * @param track The track number.
     * @param icm IndexColorModel. Specify null to use the standard Macintosh
     * color table.
     */
    public void setVideoColorTable(int track, ColorModel icm) {
        if (icm instanceof IndexColorModel) {
            VideoTrack t = (VideoTrack) tracks.get(track);
            t.videoColorTable = (IndexColorModel) icm;
        }
    }

    /**
     * Gets the preferred color table for displaying the movie on devices that
     * support only 256 colors.
     *
     * @param track The track number.
     * @return The color table or null, if the video uses the standard Macintosh
     * color table.
     */
    public IndexColorModel getVideoColorTable(int track) {
        VideoTrack t = (VideoTrack) tracks.get(track);
        return t.videoColorTable;
    }

    /**
     * Sets the edit list for the specified track.
     * 
     * In the absence of an edit list, the presentation of the track starts
     * immediately. An empty edit is used to offset the start time of a track.
     * 
     *
     * @param track TODO
     * @param editList TODO
     * @throws IllegalArgumentException If the edit list ends with an empty
     * edit.
     */
    public void setEditList(int track, Edit[] editList) {
        if (editList != null && editList.length > 0 && editList[editList.length - 1].mediaTime == -1) {
            throw new IllegalArgumentException("Edit list must not end with empty edit.");
        }
        tracks.get(track).editList = editList;
    }

    /**
     * Adds a video track.
     *
     * @param compressionType The QuickTime "image compression format"
     * 4-Character code. A list of supported 4-Character codes is given in qtff,
     * table 3-1, page 96.
     * @param compressorName The QuickTime compressor name. Can be up to 32
     * characters long.
     * @param timeScale The media time scale between 1 and 2^32.
     * @param width The width of a video frame.
     * @param height The height of a video frame.
     * @param depth The number of bits per pixel.
     * @param syncInterval Interval for sync-samples. 0=automatic. 1=all frames
     * are keyframes. Values larger than 1 specify that for every n-th frame is
     * a keyframe. Apple's QuickTime will not work properly if there is not at
     * least one keyframe every second.
     *
     * @return Returns the track index.
     * @throws java.io.IOException TODO
     *
     * @throws IllegalArgumentException if {@code width} or {@code height} is
     * smaller than 1, if the length of {@code compressionType} is not equal to
     * 4, if the length of the {@code compressorName} is not between 1 and 32,
     * if the tiimeScale is not between 1 and 2^32.
     */
    public int addVideoTrack(String compressionType, String compressorName, long timeScale, int width, int height, int depth, int syncInterval) throws IOException {
        ensureStarted();
        if (compressionType == null || compressionType.length() != 4) {
            throw new IllegalArgumentException("compressionType must be 4 characters long:" + compressionType);
        }
        if (compressorName == null || compressorName.length() < 1 || compressorName.length() > 32) {
            throw new IllegalArgumentException("compressorName must be between 1 and 32 characters long:" + (compressorName == null ? "null" : "\"" + compressorName + "\""));
        }
        if (timeScale < 1 || timeScale > (2L << 32)) {
            throw new IllegalArgumentException("timeScale must be between 1 and 2^32:" + timeScale);
        }
        if (width < 1 || height < 1) {
            throw new IllegalArgumentException("Width and height must be greater than 0, width:" + width + " height:" + height);
        }

        VideoTrack t = new VideoTrack();
        t.mediaCompressionType = compressionType;
        t.mediaCompressorName = compressorName;
        t.mediaTimeScale = timeScale;
        t.width = width;
        t.height = height;
        t.videoDepth = depth;
        t.syncInterval = syncInterval;
        t.format = new Format(
              MediaTypeKey, VIDEO,
              MimeTypeKey, MIME_QUICKTIME,
              EncodingKey, compressionType,
              CompressorNameKey, compressorName,
              DataClassKey, byte[].class,
              WidthKey, width, HeightKey, height, DepthKey, depth,
              FrameRateKey, new Rational(timeScale, 1));
        tracks.add(t);
        return tracks.size() - 1;
    }

    /**
     * Adds an audio track.
     *
     * @param compressionType The QuickTime 4-character code. A list of
     * supported 4-Character codes is given in qtff, table 3-7, page 113.
     * @param timeScale The media time scale between 1 and 2^32.
     * @param sampleRate The sample rate. The integer portion must match the
     * {@code timeScale}.
     * @param numberOfChannels The number of channels: 1 for mono, 2 for stereo.
     * @param sampleSizeInBits The number of bits in a sample: 8 or 16.
     * @param isCompressed Whether the sound is compressed.
     * @param frameDuration The frame duration, expressed in the media’s
     * timescale, where the timescale is equal to the sample rate. For
     * uncompressed formats, this field is always 1.
     * @param frameSize For uncompressed audio, the number of bytes in a sample
     * for a single channel (sampleSize divided by 8). For compressed audio, the
     * number of bytes in a frame.
     * @param signed TODO
     * @param byteOrder TODO
     * @throws java.io.IOException TODO
     *
     * @throws IllegalArgumentException if the audioFormat is not 4 characters
     * long, if the time scale is not between 1 and 2^32, if the integer portion
     * of the sampleRate is not equal to the timeScale, if numberOfChannels is
     * not 1 or 2.
     * @return Returns the track index.
     */
    public int addAudioTrack(String compressionType, //
          long timeScale, double sampleRate, //
          int numberOfChannels, int sampleSizeInBits, //
          boolean isCompressed, //
          int frameDuration, int frameSize, boolean signed, ByteOrder byteOrder) throws IOException {
        ensureStarted();
        if (compressionType == null || compressionType.length() != 4) {
            throw new IllegalArgumentException("audioFormat must be 4 characters long:" + compressionType);
        }
        if (timeScale < 1 || timeScale > (2L << 32)) {
            throw new IllegalArgumentException("timeScale must be between 1 and 2^32:" + timeScale);
        }
        if (timeScale != (int) floor(sampleRate)) {
            throw new IllegalArgumentException("timeScale: " + timeScale + " must match integer portion of sampleRate: " + sampleRate);
        }
        if (numberOfChannels != 1 && numberOfChannels != 2) {
            throw new IllegalArgumentException("numberOfChannels must be 1 or 2: " + numberOfChannels);
        }
        if (sampleSizeInBits != 8 && sampleSizeInBits != 16) {
            throw new IllegalArgumentException("sampleSize must be 8 or 16: " + numberOfChannels);
        }

        AudioTrack t = new AudioTrack();
        t.mediaCompressionType = compressionType;
        t.mediaTimeScale = timeScale;
        t.soundSampleRate = sampleRate;
        t.soundCompressionId = isCompressed ? -2 : -1;
        t.soundNumberOfChannels = numberOfChannels;
        t.soundSampleSize = sampleSizeInBits;
        t.soundSamplesPerPacket = frameDuration;
        if (isCompressed) {
            t.soundBytesPerPacket = frameSize;
            t.soundBytesPerFrame = frameSize * numberOfChannels;
        } else {
            t.soundBytesPerPacket = frameSize / numberOfChannels;
            t.soundBytesPerFrame = frameSize;
        }
        t.soundBytesPerSample = sampleSizeInBits / 8;

        t.format = new Format(
              MediaTypeKey, AUDIO,
              MimeTypeKey, MIME_QUICKTIME,
              EncodingKey, compressionType,
              SampleRateKey, valueOf(sampleRate),
              SampleSizeInBitsKey, sampleSizeInBits,
              ChannelsKey, numberOfChannels,
              FrameSizeKey, frameSize,
              SampleRateKey, valueOf(sampleRate),
              SignedKey, signed,
              ByteOrderKey, byteOrder);
        tracks.add(t);
        return tracks.size() - 1;
    }

    /**
     * Sets the compression quality of a track.
     * 
     * A value of 0 stands for "high compression is important" a value of 1 for
     * "high image quality is important".
     * 
     * Changing this value affects the encoding of video frames which are
     * subsequently written into the track. Frames which have already been
     * written are not changed.
     * 
     * This value has no effect on videos encoded with lossless encoders such as
     * the PNG format.
     * 
     * The default value is 0.97.
     *
     * @param track TODO
     * @param newValue TODO
     */
    public void setCompressionQuality(int track, float newValue) {
        VideoTrack vt = (VideoTrack) tracks.get(track);
        vt.videoQuality = newValue;
    }

    /**
     * Returns the compression quality of a track.
     *
     * @param track TODO
     * @return compression quality
     */
    public float getCompressionQuality(int track) {
        return ((VideoTrack) tracks.get(track)).videoQuality;
    }

    /**
     * Sets the sync interval for the specified video track.
     *
     * @param track The track number.
     * @param i Interval between sync samples (keyframes). 0 = automatic. 1 =
     * write all samples as sync samples. n = sync every n-th sample.
     */
    public void setSyncInterval(int track, int i) {
        ((VideoTrack) tracks.get(track)).syncInterval = i;
    }

    /**
     * Gets the sync interval from the specified video track.
     *
     * @param track TODO
     * @return TODO
     */
    public int getSyncInterval(int track) {
        return ((VideoTrack) tracks.get(track)).syncInterval;
    }

    /**
     * Sets the creation time of the movie.
     *
     * @param creationTime TODO
     */
    public void setCreationTime(Date creationTime) {
        this.creationTime = creationTime;
    }

    /**
     * Gets the creation time of the movie.
     *
     * @return TODO
     */
    public Date getCreationTime() {
        return creationTime;
    }

    /**
     * Sets the modification time of the movie.
     *
     * @param modificationTime TODO
     */
    public void setModificationTime(Date modificationTime) {
        this.modificationTime = modificationTime;
    }

    /**
     * Gets the modification time of the movie.
     *
     * @return TODO
     */
    public Date getModificationTime() {
        return modificationTime;
    }

    /**
     * Gets the preferred rate at which to play this movie. A value of 1.0
     * indicates normal rate.
     *
     * @return TODO
     */
    public double getPreferredRate() {
        return preferredRate;
    }

    /**
     * Sets the preferred rate at which to play this movie. A value of 1.0
     * indicates normal rate.
     *
     * @param preferredRate TODO
     */
    public void setPreferredRate(double preferredRate) {
        this.preferredRate = preferredRate;
    }

    /**
     * Gets the preferred volume of this movie’s sound. A value of 1.0 indicates
     * full volume.
     *
     * @return TODO
     */
    public double getPreferredVolume() {
        return preferredVolume;
    }

    /**
     * Sets the preferred volume of this movie’s sound. A value of 1.0 indicates
     * full volume.
     *
     * @param preferredVolume TODO
     */
    public void setPreferredVolume(double preferredVolume) {
        this.preferredVolume = preferredVolume;
    }

    /**
     * Gets the time value for current time position within the movie.
     *
     * @return TODO
     */
    public long getCurrentTime() {
        return currentTime;
    }

    /**
     * Sets the time value for current time position within the movie.
     *
     * @param currentTime TODO
     */
    public void setCurrentTime(long currentTime) {
        this.currentTime = currentTime;
    }

    /**
     * Gets the time value of the time of the movie poster.
     *
     * @return TODO
     */
    public long getPosterTime() {
        return posterTime;
    }

    /**
     * Sets the time value of the time of the movie poster.
     *
     * @param posterTime TODO
     */
    public void setPosterTime(long posterTime) {
        this.posterTime = posterTime;
    }

    /**
     * Gets the duration of the movie preview in movie time scale units.
     *
     * @return TODO
     */
    public long getPreviewDuration() {
        return previewDuration;
    }

    /**
     * Gets the duration of the movie preview in movie time scale units.
     *
     * @param previewDuration TODO
     */
    public void setPreviewDuration(long previewDuration) {
        this.previewDuration = previewDuration;
    }

    /**
     * Gets the time value in the movie at which the preview begins.
     *
     * @return TODO
     */
    public long getPreviewTime() {
        return previewTime;
    }

    /**
     * The time value in the movie at which the preview begins.
     *
     * @param previewTime TODO
     */
    public void setPreviewTime(long previewTime) {
        this.previewTime = previewTime;
    }

    /**
     * The duration of the current selection in movie time scale units.
     *
     * @return TODO
     */
    public long getSelectionDuration() {
        return selectionDuration;
    }

    /**
     * The duration of the current selection in movie time scale units.
     *
     * @param selectionDuration TODO
     */
    public void setSelectionDuration(long selectionDuration) {
        this.selectionDuration = selectionDuration;
    }

    /**
     * The time value for the start time of the current selection.
     *
     * @return TODO
     */
    public long getSelectionTime() {
        return selectionTime;
    }

    /**
     * The time value for the start time of the current selection.
     *
     * @param selectionTime TODO
     */
    public void setSelectionTime(long selectionTime) {
        this.selectionTime = selectionTime;
    }

    /**
     * Sets the transformation matrix of the entire movie.
     * 
     * {a, b, u,
     *  c, d, v,
     *  tx,ty,w} // X- and Y-Translation
     *
     *           [ a  b  u
     * [x y 1] *   c  d  v   = [x' y' 1]
     *  tx ty w ]
     *
     *
     * @param matrix The transformation matrix.
     */
    public void setMovieTransformationMatrix(double[] matrix) {
        if (matrix.length != 9) {
            throw new IllegalArgumentException("matrix must have 9 elements, matrix.length=" + matrix.length);
        }

        arraycopy(matrix, 0, movieMatrix, 0, 9);
    }

    /**
     * Gets the transformation matrix of the entire movie.
     *
     * @return The transformation matrix.
     */
    public double[] getMovieTransformationMatrix() {
        return movieMatrix.clone();
    }

    /**
     * Sets the transformation matrix of the specified track.
     * 
     * {a, b, u,
     *  c, d, v,
     *  tx,ty,w} // X- and Y-Translation
     *
     *           [ a  b  u
     * [x y 1] *   c  d  v   = [x' y' 1]
     *  tx ty w ]
     *
     *
     * @param track The track number.
     * @param matrix The transformation matrix.
     */
    public void setTransformationMatrix(int track, double[] matrix) {
        if (matrix.length != 9) {
            throw new IllegalArgumentException("matrix must have 9 elements, matrix.length=" + matrix.length);
        }

        arraycopy(matrix, 0, tracks.get(track).matrix, 0, 9);
    }

    /**
     * Gets the transformation matrix of the specified track.
     *
     * @param track The track number.
     * @return The transformation matrix.
     */
    public double[] getTransformationMatrix(int track) {
        return tracks.get(track).matrix.clone();
    }

    /**
     * Sets the state of the QuickTimeWriter to started.
     * 
     * If the state is changed by this method, the prolog is written.
     *
     * @throws java.io.IOException TODO
     */
    protected void ensureStarted() throws IOException {
        ensureOpen();
        if (state == FINISHED) {
            throw new IOException("Can not write into finished movie.");
        }
        if (state != STARTED) {
            writeProlog();
            mdatAtom = new WideDataAtom("mdat");
            state = STARTED;
        }
    }

    /**
     * Writes an already encoded sample from a file into a track.
     * 
     * This method does not inspect the contents of the samples. The contents
     * has to match the format and dimensions of the media in this track.
     *
     * @param track The track index.
     * @param file The file which holds the encoded data sample.
     * @param duration The duration of the sample in media time scale units.
     * @param isSync whether the sample is a sync sample (key frame).
     * @throws IllegalArgumentException if the track does not support video, if
     * the duration is less than 1, or if the dimension of the frame does not
     * match the dimension of the video.
     * @throws IOException if writing the sample data failed.
     */
    public void writeSample(int track, File file, long duration, boolean isSync) throws IOException {
        ensureStarted();
        try (FileInputStream in = new FileInputStream(file)) {
            writeSample(track, in, duration, isSync);
        }
    }

    /**
     * Writes an already encoded sample from an input stream into a track.
     * 
     * This method does not inspect the contents of the samples. The contents
     * has to match the format and dimensions of the media in this track.
     *
     * @param track The track index.
     * @param in The input stream which holds the encoded sample data.
     * @param duration The duration of the video frame in media time scale
     * units.
     * @param isSync Whether the sample is a sync sample (keyframe).
     *
     * @throws IllegalArgumentException if the duration is less than 1.
     * @throws IOException if writing the sample data failed.
     */
    public void writeSample(int track, InputStream in, long duration, boolean isSync) throws IOException {
        ensureStarted();
        if (duration <= 0) {
            throw new IllegalArgumentException("duration must be greater 0");
        }
        Track t = tracks.get(track); // throws index out of bounds exception if illegal track index
        ensureOpen();
        ensureStarted();
        long offset = getRelativeStreamPosition();
        OutputStream mdatOut = mdatAtom.getOutputStream();
        copy(in, mdatOut);
        long length = getRelativeStreamPosition() - offset;
        t.addSample(new Sample(duration, offset, length), 1, isSync);
    }

    /**
     * Writes an already encoded sample from a byte array into a track.
     * 
     * This method does not inspect the contents of the samples. The contents
     * has to match the format and dimensions of the media in this track.
     *
     * @param track The track index.
     * @param data The encoded sample data.
     * @param duration The duration of the sample in media time scale units.
     * @param isSync Whether the sample is a sync sample.
     *
     * @throws IllegalArgumentException if the duration is less than 1.
     * @throws IOException if writing the sample data failed.
     */
    public void writeSample(int track, byte[] data, long duration, boolean isSync) throws IOException {
        writeSample(track, data, 0, data.length, duration, isSync);
    }

    /**
     * Writes an already encoded sample from a byte array into a track.
     * 
     * This method does not inspect the contents of the samples. The contents
     * has to match the format and dimensions of the media in this track.
     *
     * @param track The track index.
     * @param data The encoded sample data.
     * @param off The start offset in the data.
     * @param len The number of bytes to write.
     * @param duration The duration of the sample in media time scale units.
     * @param isSync Whether the sample is a sync sample (keyframe).
     *
     * @throws IllegalArgumentException if the duration is less than 1.
     * @throws IOException if writing the sample data failed.
     */
    public void writeSample(int track, byte[] data, int off, int len, long duration, boolean isSync) throws IOException {
        ensureStarted();
        if (duration <= 0) {
            throw new IllegalArgumentException("duration must be greater 0");
        }
        Track t = tracks.get(track); // throws index out of bounds exception if illegal track index
        ensureOpen();
        ensureStarted();
        long offset = getRelativeStreamPosition();
        OutputStream mdatOut = mdatAtom.getOutputStream();
        mdatOut.write(data, off, len);
        t.addSample(new Sample(duration, offset, len), 1, isSync);
    }

    /**
     * Writes multiple sync samples from a byte array into a track.
     * 
     * This method does not inspect the contents of the samples. The contents
     * has to match the format and dimensions of the media in this track.
     *
     * @param track The track index.
     * @param sampleCount The number of samples.
     * @param data The encoded sample data. The length of data must be dividable
     * by sampleCount.
     * @param sampleDuration The duration of a sample. All samples must have the
     * same duration.
     * @param isSync TODO
     *
     * @throws IllegalArgumentException if {@code sampleDuration} is less than 1
     * or if the length of {@code data} is not dividable by {@code sampleCount}.
     * @throws IOException if writing the chunk failed.
     */
    public void writeSamples(int track, int sampleCount, byte[] data, long sampleDuration, boolean isSync) throws IOException {
        writeSamples(track, sampleCount, data, 0, data.length, sampleDuration, isSync);
    }

    /**
     * Writes multiple sync samples from a byte array into a track.
     * 
     * This method does not inspect the contents of the samples. The contents
     * has to match the format and dimensions of the media in this track.
     *
     * @param track The track index.
     * @param sampleCount The number of samples.
     * @param data The encoded sample data.
     * @param off The start offset in the data.
     * @param len The number of bytes to write. Must be dividable by
     * sampleCount.
     * @param sampleDuration The duration of a sample. All samples must have the
     * same duration.
     *
     * @throws IllegalArgumentException if the duration is less than 1.
     * @throws IOException if writing the sample data failed.
     */
    public void writeSamples(int track, int sampleCount, byte[] data, int off, int len, long sampleDuration) throws IOException {
        writeSamples(track, sampleCount, data, off, len, sampleDuration, true);
    }

    /**
     * Writes multiple samples from a byte array into a track.
     * 
     * This method does not inspect the contents of the data. The contents has
     * to match the format and dimensions of the media in this track.
     *
     * @param track The track index.
     * @param sampleCount The number of samples.
     * @param data The encoded sample data.
     * @param off The start offset in the data.
     * @param len The number of bytes to write. Must be dividable by
     * sampleCount.
     * @param sampleDuration The duration of a sample. All samples must have the
     * same duration.
     * @param isSync Whether the samples are sync samples. All samples must
     * either be sync samples or non-sync samples.
     *
     * @throws IllegalArgumentException if the duration is less than 1.
     * @throws IOException if writing the sample data failed.
     */
    public void writeSamples(int track, int sampleCount, byte[] data, int off, int len, long sampleDuration, boolean isSync) throws IOException {
        ensureStarted();
        if (sampleDuration <= 0) {
            throw new IllegalArgumentException("sampleDuration must be greater 0, sampleDuration=" + sampleDuration + " track=" + track);
        }
        if (sampleCount <= 0) {
            throw new IllegalArgumentException("sampleCount must be greater 0, sampleCount=" + sampleCount + " track=" + track);
        }
        if (len % sampleCount != 0) {
            throw new IllegalArgumentException("len must be divisable by sampleCount len=" + len + " sampleCount=" + sampleCount + " track=" + track);
        }
        Track t = tracks.get(track); // throws index out of bounds exception if illegal track index
        ensureOpen();
        ensureStarted();
        long offset = getRelativeStreamPosition();
        OutputStream mdatOut = mdatAtom.getOutputStream();
        mdatOut.write(data, off, len);

        int sampleLength = len / sampleCount;
        Sample first = new Sample(sampleDuration, offset, sampleLength);
        Sample last = new Sample(sampleDuration, offset + sampleLength * (sampleCount - 1), sampleLength);
        t.addChunk(new Chunk(first, last, sampleCount, 1), isSync);
    }

    /**
     * Returns true if the limit for media samples has been reached. If this
     * limit is reached, no more samples should be added to the movie.
     * 
     * QuickTime files can be up to 64 TB long, but there are other values that
     * may overflow before this size is reached. This method returns true when
     * the files size exceeds 2^60 or when the media duration value of a track
     * exceeds 2^61.
     *
     * @return TODO
     */
    public boolean isDataLimitReached() {
        try {
            long maxMediaDuration = 0;
            for (Track t : tracks) {
                maxMediaDuration = max(t.mediaDuration, maxMediaDuration);
            }

            return getRelativeStreamPosition() > (1L << 61) //
                  || maxMediaDuration > 1L << 61;
        } catch (IOException ex) {
            return true;
        }
    }

    /**
     * Closes the movie file as well as the stream being filtered.
     *
     * @exception IOException if an I/O error has occurred
     */
    public void close() throws IOException {
        try {
            if (state == STARTED) {
                finish();
            }
        } finally {
            if (state != CLOSED) {
                out.close();
                state = CLOSED;
            }
        }
    }

    /**
     * Finishes writing the contents of the QuickTime output stream without
     * closing the underlying stream. Use this method when applying multiple
     * filters in succession to the same output stream.
     *
     * @exception IllegalStateException if the dimension of the video track has
     * not been specified or determined yet.
     * @exception IOException if an I/O exception has occurred
     */
    public void finish() throws IOException {
        ensureOpen();
        if (state != FINISHED) {
            for (int i = 0, n = tracks.size(); i < n; i++) {
            }
            mdatAtom.finish();
            writeEpilog();
            state = FINISHED;
            /*
             for (int i = 0, n = tracks.size(); i < n; i++) {
             if (tracks.get(i) instanceof VideoTrack) {
             VideoTrack t = (VideoTrack) tracks.get(i);
             t.videoWidth = t.videoHeight = -1;
             }
             }*/
        }
    }

    /**
     * Check to make sure that this stream has not been closed
     *
     * @throws java.io.IOException TODO
     */
    protected void ensureOpen() throws IOException {
        if (state == CLOSED) {
            throw new IOException("Stream closed");
        }
    }

    /**
     * Writes the stream prolog.
     */
    private void writeProlog() throws IOException {
        /* File type atom
         *
         typedef struct {
         magic brand;
         bcd4 versionYear;
         bcd2 versionMonth;
         bcd2 versionMinor;
         magic[4] compatibleBrands;
         } ftypAtom;
         */
        DataAtom ftypAtom = new DataAtom("ftyp");
        DataAtomOutputStream d = ftypAtom.getOutputStream();
        d.writeType("qt  "); // brand
        d.writeBCD4(2005); // versionYear
        d.writeBCD2(3); // versionMonth
        d.writeBCD2(0); // versionMinor
        d.writeType("qt  "); // compatibleBrands
        d.writeInt(0); // compatibleBrands (0 is used to denote no value)
        d.writeInt(0); // compatibleBrands (0 is used to denote no value)
        d.writeInt(0); // compatibleBrands (0 is used to denote no value)
        ftypAtom.finish();
    }

    private void writeEpilog() throws IOException {
        long duration = getMovieDuration();

        DataAtom leaf;

        /* Movie Atom ========= */
        moovAtom = new CompositeAtom("moov");

        /* Movie Header Atom -------------
         * The data contained in this atom defines characteristics of the entire
         * QuickTime movie, such as time scale and duration. It has an atom type
         * value of 'mvhd'.
         *
         * typedef struct {
         byte version;
         byte[3] flags;
         mactimestamp creationTime;
         mactimestamp modificationTime;
         int timeScale;
         int duration;
         fixed16d16 preferredRate;
         fixed8d8 preferredVolume;
         byte[10] reserved;
         fixed16d16 matrixA;
         fixed16d16 matrixB;
         fixed2d30 matrixU;
         fixed16d16 matrixC;
         fixed16d16 matrixD;
         fixed2d30 matrixV;
         fixed16d16 matrixX;
         fixed16d16 matrixY;
         fixed2d30 matrixW;
         int previewTime;
         int previewDuration;
         int posterTime;
         int selectionTime;
         int selectionDuration;
         int currentTime;
         int nextTrackId;
         } movieHeaderAtom;
         */
        leaf = new DataAtom("mvhd");
        moovAtom.add(leaf);
        DataAtomOutputStream d = leaf.getOutputStream();
        d.writeByte(0); // version
        // A 1-byte specification of the version of this movie header atom.

        d.writeByte(0); // flags[0]
        d.writeByte(0); // flags[1]
        d.writeByte(0); // flags[2]
        // Three bytes of space for future movie header flags.

        d.writeMacTimestamp(creationTime); // creationTime
        // A 32-bit integer that specifies the calendar date and time (in
        // seconds since midnight, January 1, 1904) when the movie atom was
        // created. It is strongly recommended that this value should be
        // specified using coordinated universal time (UTC).

        d.writeMacTimestamp(modificationTime); // modificationTime
        // A 32-bit integer that specifies the calendar date and time (in
        // seconds since midnight, January 1, 1904) when the movie atom was
        // changed. BooleanIt is strongly recommended that this value should be
        // specified using coordinated universal time (UTC).

        d.writeUInt(movieTimeScale); // timeScale
        // A time value that indicates the time scale for this movie—that is,
        // the number of time units that pass per second in its time coordinate
        // system. A time coordinate system that measures time in sixtieths of a
        // second, for example, has a time scale of 60.

        d.writeUInt(duration); // duration
        // A time value that indicates the duration of the movie in time scale
        // units. Note that this property is derived from the movie’s tracks.
        // The value of this field corresponds to the duration of the longest
        // track in the movie.

        d.writeFixed16D16(preferredRate); // preferredRate
        // A 32-bit fixed-point number that specifies the rate at which to play
        // this movie. A value of 1.0 indicates normal rate.

        d.writeFixed8D8(preferredVolume); // preferredVolume
        // A 16-bit fixed-point number that specifies how loud to play this
        // movie’s sound. A value of 1.0 indicates full volume.

        d.write(new byte[10]); // reserved;
        // Ten bytes reserved for use by Apple. Set to 0.

        d.writeFixed16D16(movieMatrix[0]); // matrix[0]
        d.writeFixed16D16(movieMatrix[1]); // matrix[1]
        d.writeFixed2D30(movieMatrix[2]); // matrix[2]
        d.writeFixed16D16(movieMatrix[3]); // matrix[3]
        d.writeFixed16D16(movieMatrix[4]); // matrix[4]
        d.writeFixed2D30(movieMatrix[5]); // matrix[5]
        d.writeFixed16D16(movieMatrix[6]); // matrix[6]
        d.writeFixed16D16(movieMatrix[7]); // matrix[7]
        d.writeFixed2D30(movieMatrix[8]); // matrix[8]

        // The matrix structure associated with this movie. A matrix shows how
        // to map points from one coordinate space into another. See “Matrices”
        // for a discussion of how display matrices are used in QuickTime:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_4.html#//apple_ref/doc/uid/TP40000939-CH206-18737
        d.writeUInt(previewTime); // previewTime
        // The time value in the movie at which the preview begins.

        d.writeUInt(previewDuration); // previewDuration
        // The duration of the movie preview in movie time scale units.

        d.writeUInt(posterTime); // posterTime
        // The time value of the time of the movie poster.

        d.writeUInt(selectionTime); // selectionTime
        // The time value for the start time of the current selection.

        d.writeUInt(selectionDuration); // selectionDuration
        // The duration of the current selection in movie time scale units.

        d.writeUInt(currentTime); // currentTime;
        // The time value for current time position within the movie.

        d.writeUInt(tracks.size() + 1); // nextTrackId
        // A 32-bit integer that indicates a value to use for the track ID
        // number of the next track added to this movie. Note that 0 is not a
        // valid track ID value.

        for (int i = 0, n = tracks.size(); i < n; i++) {
            /* Track Atom ======== */
            writeTrackAtoms(i, moovAtom, modificationTime);
        }
        //
        moovAtom.finish();
    }

    protected void writeTrackAtoms(int trackIndex, CompositeAtom moovAtom, Date modificationTime) throws IOException {
        Track t = tracks.get(trackIndex);

        DataAtom leaf;
        DataAtomOutputStream d;

        /* Track Atom ======== */
        CompositeAtom trakAtom = new CompositeAtom("trak");
        moovAtom.add(trakAtom);

        /* Track Header Atom -----------
         * The track header atom specifies the characteristics of a single track
         * within a movie. A track header atom contains a size field that
         * specifies the number of bytes and a type field that indicates the
         * format of the data (defined by the atom type 'tkhd').
         *
         typedef struct {
         byte version;
         byte flag0;
         byte flag1;
         byte set TrackHeaderFlags flag2;
         mactimestamp creationTime;
         mactimestamp modificationTime;
         int trackId;
         byte[4] reserved;
         int duration;
         byte[8] reserved;
         short layer;
         short alternateGroup;
         short volume;
         byte[2] reserved;
         int[9] matrix;
         int trackWidth;
         int trackHeight;
         } trackHeaderAtom;     */
        leaf = new DataAtom("tkhd");
        trakAtom.add(leaf);
        d = leaf.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this track header.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(t.headerFlags); // flag[2]
        // Three bytes that are reserved for the track header flags. These flags
        // indicate how the track is used in the movie. The following flags are
        // valid (all flags are enabled 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.

        d.writeMacTimestamp(creationTime); // creationTime
        // A 32-bit integer that indicates the calendar date and time (expressed
        // in seconds since midnight, January 1, 1904) when the track header was
        // created. It is strongly recommended that this value should be
        // specified using coordinated universal time (UTC).

        d.writeMacTimestamp(modificationTime); // modificationTime
        // A 32-bit integer that indicates the calendar date and time (expressed
        // in seconds since midnight, January 1, 1904) when the track header was
        // changed. It is strongly recommended that this value should be
        // specified using coordinated universal time (UTC).

        d.writeInt(trackIndex + 1); // trackId
        // A 32-bit integer that uniquely identifies the track. The value 0
        // cannot be used.

        d.writeInt(0); // reserved;
        // A 32-bit integer that is reserved for use by Apple. Set this field to 0.

        d.writeUInt(t.getTrackDuration(movieTimeScale)); // 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. If there is no edit
        // list, then the duration is the sum of the sample durations, converted
        // into the movie timescale.

        d.writeLong(0); // reserved
        // An 8-byte value that is reserved for use by Apple. Set this field to 0.

        d.writeShort(0); // layer;
        // A 16-bit integer that indicates this track’s spatial priority in its
        // movie. The QuickTime Movie Toolbox uses this value to determine how
        // tracks overlay one another. Tracks with lower layer values are
        // displayed in front of tracks with higher layer values.

        d.writeShort(0); // alternate group
        // A 16-bit integer that specifies a collection of movie tracks that
        // contain alternate 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, language, or the
        // capabilities of the computer.

        d.writeFixed8D8(t.mediaType == AUDIO ? 1 : 0); // volume
        // A 16-bit fixed-point value that indicates how loudly this track’s
        // sound is to be played. A value of 1.0 indicates normal volume.

        d.writeShort(0); // reserved
        // A 16-bit integer that is reserved for use by Apple. Set this field to 0.

        double[] m = t.matrix;
        d.writeFixed16D16(m[0]); // matrix[0]
        d.writeFixed16D16(m[1]); // matrix[1]
        d.writeFixed2D30(m[2]); // matrix[2]
        d.writeFixed16D16(m[3]); // matrix[3]
        d.writeFixed16D16(m[4]); // matrix[4]
        d.writeFixed2D30(m[5]); // matrix[5]
        d.writeFixed16D16(m[6]); // matrix[6]
        d.writeFixed16D16(m[7]); // matrix[7]
        d.writeFixed2D30(m[8]); // matrix[8]
        // The matrix structure associated with this track.
        // See Figure 2-8 for an illustration of a matrix structure:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_3.html#//apple_ref/doc/uid/TP40000939-CH204-32967

        d.writeFixed16D16(t.mediaType == VIDEO ? ((VideoTrack) t).width : 0); // width
        // A 32-bit fixed-point number that specifies the width of this track in pixels.

        d.writeFixed16D16(t.mediaType == VIDEO ? ((VideoTrack) t).height : 0); // height
        // A 32-bit fixed-point number that indicates the height of this track in pixels.

        /* Edit Atom ========= */
        CompositeAtom edtsAtom = new CompositeAtom("edts");
        trakAtom.add(edtsAtom);

        /* Edit List atom ------- */
 /*
         typedef struct {
         byte version;
         byte[3] flags;
         int numberOfEntries;
         editListTable editListTable[numberOfEntries];
         } editListAtom;
            
         typedef struct {
         int trackDuration;
         int mediaTime;
         fixed16d16 mediaRate;
         } editListTable;
         */
        leaf = new DataAtom("elst");
        edtsAtom.add(leaf);
        d = leaf.getOutputStream();

        d.write(0); // version
        // One byte that specifies the version of this header atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]

        Edit[] elist = t.editList;
        if (elist == null || elist.length == 0) {
            d.writeUInt(1); // numberOfEntries
            d.writeUInt(t.getTrackDuration(movieTimeScale)); // trackDuration
            d.writeUInt(0); // mediaTime
            d.writeFixed16D16(1); // mediaRate
        } else {
            d.writeUInt(elist.length); // numberOfEntries
            for (int i = 0; i < elist.length; ++i) {
                d.writeUInt(elist[i].trackDuration); // trackDuration
                d.writeUInt(elist[i].mediaTime); // mediaTime
                d.writeUInt(elist[i].mediaRate); // mediaRate
            }
        }


        /* Media Atom ========= */
        CompositeAtom mdiaAtom = new CompositeAtom("mdia");
        trakAtom.add(mdiaAtom);

        /* Media Header atom -------
         typedef struct {
         byte version;
         byte[3] flags;
         mactimestamp creationTime;
         mactimestamp modificationTime;
         int timeScale;
         int duration;
         short language;
         short quality;
         } mediaHeaderAtom;*/
        leaf = new DataAtom("mdhd");
        mdiaAtom.add(leaf);
        d = leaf.getOutputStream();
        d.write(0); // version
        // One byte that specifies the version of this header atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // Three bytes of space for media header flags. Set this field to 0.

        d.writeMacTimestamp(creationTime); // creationTime
        // A 32-bit integer that specifies (in seconds since midnight, January
        // 1, 1904) when the media atom was created. It is strongly recommended
        // that this value should be specified using coordinated universal time
        // (UTC).

        d.writeMacTimestamp(modificationTime); // modificationTime
        // A 32-bit integer that specifies (in seconds since midnight, January
        // 1, 1904) when the media atom was changed. It is strongly recommended
        // that this value should be specified using coordinated universal time
        // (UTC).

        d.writeUInt(t.mediaTimeScale); // timeScale
        // A time value that indicates the time scale for this media—that is,
        // the number of time units that pass per second in its time coordinate
        // system.

        d.writeUInt(t.mediaDuration); // duration
        // The duration of this media in units of its time scale.

        d.writeShort(0); // language;
        // A 16-bit integer that specifies the language code for this media.
        // See “Language Code Values” for valid language codes:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_2.html#//apple_ref/doc/uid/TP40000939-CH206-27005

        d.writeShort(0); // quality
        // A 16-bit integer that specifies the media’s playback quality—that is,
        // its suitability for playback in a given environment.

        /**
         * Media Handler Reference Atom -------
         */
        leaf = new DataAtom("hdlr");
        mdiaAtom.add(leaf);
        /*typedef struct {
         byte version;
         byte[3] flags;
         magic componentType;
         magic componentSubtype;
         magic componentManufacturer;
         int componentFlags;
         int componentFlagsMask;
         pstring componentName;
         } handlerReferenceAtom;
         */
        d = leaf.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this handler information.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for handler information flags. Set this field to 0.

        d.writeType("mhlr"); // componentType
        // A four-character code that identifies the type of the handler. Only
        // two values are valid for this field: 'mhlr' for media handlers and
        // 'dhlr' for data handlers.

        d.writeType(t.mediaType == VIDEO ? "vide" : "soun"); // componentSubtype
        // A four-character code that identifies the type of the media handler
        // or data handler. For media handlers, this field defines the type of
        // data—for example, 'vide' for video data or 'soun' for sound data.
        //
        // For data handlers, this field defines the data reference type—for
        // example, a component subtype value of 'alis' identifies a file alias.

        if (t.mediaType == AUDIO) {
            d.writeType("appl");
        } else {
            d.writeUInt(0);
        }
        // componentManufacturer
        // Reserved. Set to 0.

        d.writeUInt(t.mediaType == AUDIO ? 268435456L : 0); // componentFlags
        // Reserved. Set to 0.

        d.writeUInt(t.mediaType == AUDIO ? 65941 : 0); // componentFlagsMask
        // Reserved. Set to 0.

        d.writePString(t.mediaType == AUDIO ? "Apple Sound Media Handler" : ""); // componentName (empty string)
        // A (counted) string that specifies the name of the component—that is,
        // the media handler used when this media was created. This field may
        // contain a zero-length (empty) string.

        /* Media Information atom ========= */
        writeMediaInformationAtoms(trackIndex, mdiaAtom);
    }

    protected void writeMediaInformationAtoms(int trackIndex, CompositeAtom mdiaAtom) throws IOException {
        Track t = tracks.get(trackIndex);
        DataAtom leaf;
        DataAtomOutputStream d;
        /* Media Information atom ========= */
        CompositeAtom minfAtom = new CompositeAtom("minf");
        mdiaAtom.add(minfAtom);

        /* Video or Audio media information atom -------- */
        switch (t.mediaType) {
            case VIDEO:
                writeVideoMediaInformationHeaderAtom(trackIndex, minfAtom);
                break;
            case AUDIO:
                writeSoundMediaInformationHeaderAtom(trackIndex, minfAtom);
                break;
            default:
                throw new UnsupportedOperationException("Media type " + t.mediaType + " not supported yet.");
        }


        /* Data Handler Reference Atom -------- */
        // The handler reference atom specifies the media handler component that
        // is to be used to interpret the media’s data. The handler reference
        // atom has an atom type value of 'hdlr'.
        leaf = new DataAtom("hdlr");
        minfAtom.add(leaf);
        /*typedef struct {
         byte version;
         byte[3] flags;
         magic componentType;
         magic componentSubtype;
         magic componentManufacturer;
         int componentFlags;
         int componentFlagsMask;
         pstring componentName;
         ubyte[] extraData;
         } handlerReferenceAtom;
         */
        d = leaf.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this handler information.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for handler information flags. Set this field to 0.

        d.writeType("dhlr"); // componentType
        // A four-character code that identifies the type of the handler. Only
        // two values are valid for this field: 'mhlr' for media handlers and
        // 'dhlr' for data handlers.

        d.writeType("alis"); // componentSubtype
        // A four-character code that identifies the type of the media handler
        // or data handler. For media handlers, this field defines the type of
        // data—for example, 'vide' for video data or 'soun' for sound data.
        // For data handlers, this field defines the data reference type—for
        // example, a component subtype value of 'alis' identifies a file alias.

        if (t.mediaType == AUDIO) {
            d.writeType("appl");
        } else {
            d.writeUInt(0);
        }
        // componentManufacturer
        // Reserved. Set to 0.

        d.writeUInt(t.mediaType == AUDIO ? 268435457L : 0); // componentFlags
        // Reserved. Set to 0.

        d.writeInt(t.mediaType == AUDIO ? 65967 : 0); // componentFlagsMask
        // Reserved. Set to 0.

        d.writePString("Apple Alias Data Handler"); // componentName (empty string)
        // A (counted) string that specifies the name of the component—that is,
        // the media handler used when this media was created. This field may
        // contain a zero-length (empty) string.

        /* Data information atom ===== */
        CompositeAtom dinfAtom = new CompositeAtom("dinf");
        minfAtom.add(dinfAtom);

        /* Data reference atom ----- */
        // Data reference atoms contain tabular data that instructs the data
        // handler component how to access the media’s data.
        leaf = new DataAtom("dref");
        dinfAtom.add(leaf);
        /*typedef struct {
         ubyte version;
         ubyte[3] flags;
         int numberOfEntries;
         dataReferenceEntry dataReference[numberOfEntries];
         } dataReferenceAtom;
            
         set {
         dataRefSelfReference=1 // I am not shure if this is the correct value for this flag
         } drefEntryFlags;
            
         typedef struct {
         int size;
         magic type;
         byte version;
         ubyte flag1;
         ubyte flag2;
         ubyte set drefEntryFlags flag3;
         byte[size - 12] data;
         } dataReferenceEntry;
         */
        d = leaf.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this data reference atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for data reference flags. Set this field to 0.

        d.writeInt(1); // numberOfEntries
        // A 32-bit integer containing the count of data references that follow.

        d.writeInt(12); // dataReference.size
        // A 32-bit integer that specifies the number of bytes in the data
        // reference.

        d.writeType("alis"); // dataReference.type
        // A 32-bit integer that specifies the type of the data in the data
        // reference. Table 2-4 lists valid type values:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_4.html#//apple_ref/doc/uid/TP40000939-CH204-38840

        d.write(0); // dataReference.version
        // A 1-byte specification of the version of the data reference.

        d.write(0); // dataReference.flag1
        d.write(0); // dataReference.flag2
        d.write(0x1); // dataReference.flag3
        // 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.


        /* Sample Table atom ========= */
        writeSampleTableAtoms(trackIndex, minfAtom);
    }

    protected void writeVideoMediaInformationHeaderAtom(int trackIndex, CompositeAtom minfAtom) throws IOException {
        DataAtom leaf;
        DataAtomOutputStream d;

        /* Video media information atom -------- */
        leaf = new DataAtom("vmhd");
        minfAtom.add(leaf);
        /*typedef struct {
             byte version;
             byte flag1;
             byte flag2;
             byte set vmhdFlags flag3;
             short graphicsMode;
             ushort[3] opcolor;
             } videoMediaInformationHeaderAtom;*/
        d = leaf.getOutputStream();
        d.write(0); // version
        // One byte that specifies the version of this header atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0x1); // flag[2]
        // Three bytes of space for media header flags.
        // This is a compatibility flag that allows QuickTime to distinguish
        // between movies created with QuickTime 1.0 and newer movies. You
        // should always set this flag to 1, unless you are creating a movie
        // intended for playback using version 1.0 of QuickTime. This flag’s
        // value is 0x0001.

        d.writeShort(0x40); // graphicsMode (0x40 = DitherCopy)
        // A 16-bit integer that specifies the transfer mode. The transfer mode
        // specifies which Boolean operation QuickDraw should perform when
        // drawing or transferring an image from one location to another.
        // See “Graphics Modes” for a list of graphics modes supported by
        // QuickTime:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_5.html#//apple_ref/doc/uid/TP40000939-CH206-18741

        d.writeUShort(0); // opcolor[0]
        d.writeUShort(0); // opcolor[1]
        d.writeUShort(0); // opcolor[2]
        // Three 16-bit values that specify the red, green, and blue colors for
        // the transfer mode operation indicated in the graphics mode field.
    }

    protected void writeSoundMediaInformationHeaderAtom(int trackIndex, CompositeAtom minfAtom) throws IOException {
        DataAtom leaf;
        DataAtomOutputStream d;

        /* Sound media information header atom -------- */
        leaf = new DataAtom("smhd");
        minfAtom.add(leaf);
        /*typedef struct {
             ubyte version;
             ubyte[3] flags;
             short balance;
             short reserved;
             } soundMediaInformationHeaderAtom;*/
        d = leaf.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this sound media information header atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for sound media information flags. Set this field to 0.

        d.writeFixed8D8(0); // balance
        // A 16-bit integer that specifies the sound balance of this
        // sound media. Sound balance is the setting that controls
        // the mix of sound between the two speakers of a computer.
        // This field is normally set to 0.
        // Balance values are represented as 16-bit, fixed-point
        // numbers that range from -1.0 to +1.0. The high-order 8
        // bits contain the integer portion of the value; the
        // low-order 8 bits contain the fractional part. Negative
        // values weight the balance toward the left speaker;
        // positive values emphasize the right channel. Setting the
        // balance to 0 corresponds to a neutral setting.

        d.writeUShort(0); // reserved
        // Reserved for use by Apple. Set this field to 0.

    }

    protected void writeSampleTableAtoms(int trackIndex, CompositeAtom minfAtom) throws IOException {
        Track t = tracks.get(trackIndex);
        DataAtom leaf;
        DataAtomOutputStream d;

        /* Sample Table atom ========= */
        CompositeAtom stblAtom = new CompositeAtom("stbl");
        minfAtom.add(stblAtom);

        /* Sample Description atom ------- */
        t.writeSampleDescriptionAtom(stblAtom);


        /* Time to Sample atom ---- */
        // Time-to-sample atoms store duration information for a media’s
        // samples, providing a mapping from a time in a media to the
        // corresponding data sample. The time-to-sample atom has an atom type
        // of 'stts'.
        leaf = new DataAtom("stts");
        stblAtom.add(leaf);
        /*
         typedef struct {
         byte version;
         byte[3] flags;
         int numberOfEntries;
         timeToSampleTable timeToSampleTable[numberOfEntries];
         } timeToSampleAtom;
            
         typedef struct {
         int sampleCount;
         int sampleDuration;
         } timeToSampleTable;
         */
        d = leaf.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this time-to-sample atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for time-to-sample flags. Set this field to 0.

        d.writeUInt(t.timeToSamples.size()); // numberOfEntries
        // A 32-bit integer containing the count of entries in the
        // time-to-sample table.

        for (TimeToSampleGroup tts : t.timeToSamples) {
            d.writeUInt(tts.getSampleCount()); // timeToSampleTable[0].sampleCount
            // A 32-bit integer that specifies the number of consecutive
            // samples that have the same duration.

            d.writeUInt(tts.getSampleDuration()); // timeToSampleTable[0].sampleDuration
            // A 32-bit integer that specifies the duration of each
            // sample.
        }
        /* sample to chunk atom -------- */
        // The sample-to-chunk atom contains a table that maps samples to chunks
        // in the media data stream. By examining the sample-to-chunk atom, you
        // can determine the chunk that contains a specific sample.
        leaf = new DataAtom("stsc");
        stblAtom.add(leaf);
        /*
         typedef struct {
         byte version;
         byte[3] flags;
         int numberOfEntries;
         sampleToChunkTable sampleToChunkTable[numberOfEntries];
         } sampleToChunkAtom;
            
         typedef struct {
         int firstChunk;
         int samplesPerChunk;
         int sampleDescription;
         } sampleToChunkTable;
         */
        d = leaf.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this time-to-sample atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for time-to-sample flags. Set this field to 0.

        int entryCount = 0;
        long previousSampleCount = -1;
        long previousSampleDescriptionId = -1;
        for (Chunk c : t.chunks) {
            if (c.sampleCount != previousSampleCount//
                  || c.sampleDescriptionId != previousSampleDescriptionId) {
                previousSampleCount = c.sampleCount;
                previousSampleDescriptionId = c.sampleDescriptionId;
                entryCount++;
            }
        }

        d.writeInt(entryCount); // number of entries
        // A 32-bit integer containing the count of entries in the sample-to-chunk table.

        int firstChunk = 1;
        previousSampleCount = -1;
        previousSampleDescriptionId = -1;
        for (Chunk c : t.chunks) {
            if (c.sampleCount != previousSampleCount//
                  || c.sampleDescriptionId != previousSampleDescriptionId) {
                previousSampleCount = c.sampleCount;
                previousSampleDescriptionId = c.sampleDescriptionId;

                d.writeUInt(firstChunk); // first chunk
                // The first chunk number using this table entry.

                d.writeUInt(c.sampleCount); // samples per chunk
                // The number of samples in each chunk.

                d.writeInt(c.sampleDescriptionId); // sample description

                // The identification number associated with the sample description for
                // the sample. For details on sample description atoms, see “Sample
                // Description Atoms.”:
                // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_5.html#//apple_ref/doc/uid/TP40000939-CH204-25691
            }
            firstChunk++;
        }
        //
        /* sync sample atom -------- */
        if (t.syncSamples != null) {
            leaf = new DataAtom("stss");
            stblAtom.add(leaf);
            /*
             typedef struct {
             byte version;
             byte[3] flags;
             int numberOfEntries;
             syncSampleTable syncSampleTable[numberOfEntries];
             } syncSampleAtom;
                
             typedef struct {
             int number;
             } syncSampleTable;
             */
            d = leaf.getOutputStream();
            d.write(0); // version
            // A 1-byte specification of the version of this time-to-sample atom.

            d.write(0); // flag[0]
            d.write(0); // flag[1]
            d.write(0); // flag[2]
            // A 3-byte space for time-to-sample flags. Set this field to 0.

            d.writeUInt(t.syncSamples.size());
            // Number of entries
            //A 32-bit integer containing the count of entries in the sync sample table.

            for (Long number : t.syncSamples) {
                d.writeUInt(number);
                // Sync sample table A table of sample numbers; each sample
                // number corresponds to a key frame.
            }
        }


        /* sample size atom -------- */
        // The sample size atom contains the sample count and a table giving the
        // size of each sample. This allows the media data itself to be
        // unframed. The total number of samples in the media is always
        // indicated in the sample count. If the default size is indicated, then
        // no table follows.
        leaf = new DataAtom("stsz");
        stblAtom.add(leaf);
        /*
         typedef struct {
         byte version;
         byte[3] flags;
         int sampleSize;
         int numberOfEntries;
         sampleSizeTable sampleSizeTable[numberOfEntries];
         } sampleSizeAtom;
            
         typedef struct {
         int size;
         } sampleSizeTable;
         */
        d = leaf.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this time-to-sample atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for time-to-sample flags. Set this field to 0.

        int sampleUnit = t.mediaType == AUDIO //

              && ((AudioTrack) t).soundCompressionId != -2 //
                    ? ((AudioTrack) t).soundSampleSize / 8 * ((AudioTrack) t).soundNumberOfChannels//
                    : 1;
        if (t.sampleSizes.size() == 1) {
            d.writeUInt(t.sampleSizes.get(0).getSampleLength() / sampleUnit); // sample size
            // A 32-bit integer specifying the sample size. If all the samples are
            // the same size, this field contains that size value. If this field is
            // set to 0, then the samples have different sizes, and those sizes are
            // stored in the sample size table.

            d.writeUInt(t.sampleSizes.get(0).getSampleCount()); // number of entries
            // A 32-bit integer containing the count of entries in the sample size
            // table.

        } else {
            d.writeUInt(0); // sample size
            // A 32-bit integer specifying the sample size. If all the samples are
            // the same size, this field contains that size value. If this field is
            // set to 0, then the samples have different sizes, and those sizes are
            // stored in the sample size table.

            long count = 0;
            for (SampleSizeGroup s : t.sampleSizes) {
                count += s.sampleCount;
            }
            d.writeUInt(count); // number of entries
            // A 32-bit integer containing the count of entries in the sample size
            // table.

            for (SampleSizeGroup s : t.sampleSizes) {
                long sampleSize = s.getSampleLength() / sampleUnit;
                for (int i = 0; i < s.sampleCount; i++) {
                    d.writeUInt(sampleSize); // sample size
                    // The size field contains the size, in bytes, of the sample in
                    // question. The table is indexed by sample number—the first entry
                    // corresponds to the first sample, the second entry is for the
                    // second sample, and so on.
                }
            }
        }
        //
        /* chunk offset atom -------- */
        // The chunk-offset table gives the index of each chunk into the
        // QuickTime Stream. There are two variants, permitting the use of
        // 32-bit or 64-bit offsets. The latter is useful when managing very
        // large movies. Only one of these variants occurs in any single
        // instance of a sample table atom.
        if (t.chunks.isEmpty() || t.chunks.get(t.chunks.size() - 1).getChunkOffset() <= 0xffffffffL) {
            /* 32-bit chunk offset atom -------- */
            leaf = new DataAtom("stco");
            stblAtom.add(leaf);
            /*
             typedef struct {
             byte version;
             byte[3] flags;
             int numberOfEntries;
             chunkOffsetTable chunkOffsetTable[numberOfEntries];
             } chunkOffsetAtom;
                
             typedef struct {
             int offset;
             } chunkOffsetTable;
             */
            d = leaf.getOutputStream();
            d.write(0); // version
            // A 1-byte specification of the version of this time-to-sample atom.

            d.write(0); // flag[0]
            d.write(0); // flag[1]
            d.write(0); // flag[2]
            // A 3-byte space for time-to-sample flags. Set this field to 0.

            d.writeUInt(t.chunks.size()); // number of entries
            // A 32-bit integer containing the count of entries in the chunk
            // offset table.
            for (Chunk c : t.chunks) {
                d.writeUInt(c.getChunkOffset() + mdatOffset); // offset
                // The offset contains the byte offset from the beginning of the
                // data stream to the chunk. The table is indexed by chunk
                // number—the first table entry corresponds to the first chunk,
                // the second table entry is for the second chunk, and so on.
            }
        } else {
            /* 64-bit chunk offset atom -------- */
            leaf = new DataAtom("co64");
            stblAtom.add(leaf);
            /*
             typedef struct {
             byte version;
             byte[3] flags;
             int numberOfEntries;
             chunkOffsetTable chunkOffset64Table[numberOfEntries];
             } chunkOffset64Atom;
                
             typedef struct {
             long offset;
             } chunkOffset64Table;
             */
            d = leaf.getOutputStream();
            d.write(0); // version
            // A 1-byte specification of the version of this time-to-sample atom.

            d.write(0); // flag[0]
            d.write(0); // flag[1]
            d.write(0); // flag[2]
            // A 3-byte space for time-to-sample flags. Set this field to 0.

            d.writeUInt(t.chunks.size()); // number of entries
            // A 32-bit integer containing the count of entries in the chunk
            // offset table.

            for (Chunk c : t.chunks) {
                d.writeLong(c.getChunkOffset()); // offset
                // The offset contains the byte offset from the beginning of the
                // data stream to the chunk. The table is indexed by chunk
                // number—the first table entry corresponds to the first chunk,
                // the second table entry is for the second chunk, and so on.
            }
        }
    }

    /**
     * Writes a version of the movie which is optimized for the web into the
     * specified output file.
     * 
     * This method finishes the movie and then copies its content into the
     * specified file. The web-optimized file starts with the movie header.
     *
     * @param outputFile The output file
     * @param compressHeader Whether the movie header shall be compressed.
     * @throws java.io.IOException TODO
     */
    public void toWebOptimizedMovie(File outputFile, boolean compressHeader) throws IOException {
        finish();
        long originalMdatOffset = mdatAtom.getOffset();
        CompositeAtom originalMoovAtom = moovAtom;
        mdatOffset = 0;

        ImageOutputStream originalOut = out;
        try {
            out = null;

            if (compressHeader) {
                ByteArrayOutputStream buf = new ByteArrayOutputStream();
                int maxIteration = 5;
                long compressionHeadersSize = 40 + 8;
                long headerSize = 0;
                long freeSize = 0;
                while (true) {
                    mdatOffset = compressionHeadersSize + headerSize + freeSize;
                    buf.reset();
                    try (DeflaterOutputStream deflater = new DeflaterOutputStream(buf)) {
                        out = new MemoryCacheImageOutputStream(deflater);
                        writeEpilog();
                        out.close();
                    }

                    if (buf.size() > headerSize + freeSize && --maxIteration > 0) {
                        if (headerSize != 0) {
                            freeSize = max(freeSize, buf.size() - headerSize - freeSize);
                        }
                        headerSize = buf.size();
                    } else {
                        freeSize = headerSize + freeSize - buf.size();
                        headerSize = buf.size();
                        break;
                    }
                }

                if (maxIteration < 0 || buf.size() == 0) {
                    compressHeader = false;
                    err.println("WARNING QuickTimeWriter failed to compress header.");
                } else {
                    out = new FileImageOutputStream(outputFile);
                    writeProlog();

                    // 40 bytes compression headers
                    DataAtomOutputStream daos = new DataAtomOutputStream(new ImageOutputStreamAdapter(out));
                    daos.writeUInt(headerSize + 40);
                    daos.writeType("moov");

                    daos.writeUInt(headerSize + 32);
                    daos.writeType("cmov");

                    daos.writeUInt(12);
                    daos.writeType("dcom");
                    daos.writeType("zlib");

                    daos.writeUInt(headerSize + 12);
                    daos.writeType("cmvd");
                    daos.writeUInt(originalMoovAtom.size());

                    daos.write(buf.toByteArray());

                    // 8 bytes "free" atom + free data
                    daos.writeUInt(freeSize + 8);
                    daos.writeType("free");
                    for (int i = 0; i < freeSize; i++) {
                        daos.write(0);
                    }
                }

            }
            if (!compressHeader) {
                out = new FileImageOutputStream(outputFile);
                mdatOffset = moovAtom.size();
                writeProlog();
                writeEpilog();
            }

            byte[] buf = new byte[4096];
            originalOut.seek((originalMdatOffset));
            for (long count = 0, n = mdatAtom.size(); count < n;) {
                int read = originalOut.read(buf, 0, (int) min(buf.length, n - count));
                out.write(buf, 0, read);
                count += read;
            }
            out.close();
        } finally {
            mdatOffset = 0;
            moovAtom = originalMoovAtom;
            out = originalOut;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy