/**
* @(#)QuickTimeWriter.java 1.3.3 2011-01-17
*
* Copyright (c) 2010-2011 Werner Randelshofer, Immensee, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package ch.randelshofer.media.quicktime;
import ch.randelshofer.io.ImageOutputStreamAdapter;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferUShort;
import java.awt.image.IndexColorModel;
import java.awt.image.WritableRaster;
import java.io.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.zip.DeflaterOutputStream;
import javax.imageio.*;
import javax.imageio.stream.*;
import javax.sound.sampled.AudioFormat;
/**
* Supports writing of time-based video and audio data into a QuickTime movie
* file (.MOV) without the need of native code.
*
* {@code QuickTimeWriter} works with tracks and samples. After creating a
* {@code QuickTimeWriter} one or more video and audio tracks can be added to
* it. Then samples can be written into the track(s). A sample is a single
* element in a sequence of time-ordered data. For video data a sample typically
* consists of a single video frame, for uncompressed stereo audio data a sample
* contains one PCM impulse per channel. Samples of compressed media data may encompass larger time units.
*
* Tracks support edit lists. An edit list specifies when to play which portion
* of the media data at what speed. An empty edit can be used to insert an empty
* time span, for example to offset a track from the start of the movie. Edits
* can also be used to play the same portion of media data multiple times
* without having it to store it more than once in the track.
* Moreover edit lists are useful for lossless cutting of media data at non-sync
* frames. For example, MP3 layer III audio data can not be cut at arbitrary
* frames, because audio data can be 'borrowed' from previous frames. An edit
* list can be used to select the desired portion of the audio data, while the
* track stores the media starting from the nearest sync frame.
*
* Samples are stored in a QuickTime file in the same sequence as they are written.
* In order to get optimal movie playback, the samples from different tracks
* should be interleaved from time to time. An interleave should occur about twice
* per second. Furthermore, to overcome any latencies in sound playback, at
* least one second of sound data needs to be placed at the beginning of the
* movie. So that the sound and video data is offset from each other in the file
* by one second.
*
* For convenience, this class has built-in encoders for video frames in the following
* formats: RAW, RLE, JPEG and PNG. Media data in other formats, including all audio
* data, must be encoded before it can be written with {@code QuickTimeWriter}.
*
* Example: Writing 10 seconds of a movie with 640x480 pixel, 30 fps,
* PNG-encoded video and 16-bit stereo, 44100 Hz, PCM-encoded audio.
*
*
* QuickTimeWriter w = new QuickTimeWriter(new File("mymovie.mov"));
* w.addAudioTrack(new AudioFormat(AudioFormat.Encoding.PCM_SIGNED), 44100, 2, 16, 2, 44100, true)); // audio in track 0
* w.addVideoTrack(QuickTimeWriter.VideoFormat.PNG, 30, 640, 480); // video in track 1
*
* // calculate total movie duration in media time units for each track
* long atmax = w.getMediaTimeScale(0) * 10;
* long vtmax = w.getMediaTimeScale(1) * 10;
*
* // duration of a single sample
* long asduration = 1;
* long vsduration = 1;
*
* // half a second in media time units (we interleave twice per second)
* long atstep = w.getMediaTimeScale(0) / 2;
* long vtstep = w.getMediaTimeScale(1) / 2;
*
* // the time when the next interleave occurs in media time units
* long atnext = w.getMediaTimeScale(0); // offset audio by 1 second
* long vtnext = 0;
*
* // the current time in media time units
* long atime = 0;
* long vtime = 0;
*
* // create buffers
* int asamplesize = 2 * 2; // 16-bit stereo * 2 channels
* byte[] audio=new byte[atstep * asamplesize];
* BufferedImage img=new BufferedImage(640, 480, BufferedImage.TYPE_INT_RGB);
*
* // main loop
* while (atime < atmax || vtime < vtmax) {
* atnext = Math.min(atmax, atnext + atstep); // advance audio to next interleave time
* while (atime < atnext) { // catch up with audio time
* int duration = (int) Math.min(audio.length / asamplesize, atmax - atime);
* ...fill in audio data for time "atime" and duration "duration" here...
* w.writeSamples(0, duration, audio, 0, duration * asamplesize, asduration);
* atime += duration;
* }
* vtnext = Math.min(vtmax, vtnext + vtstep); // advance video to next interleave time
* while (vtime < vtnext) { // catch up with video time
* int duration = (int) Math.min(1, vtmax - vtime);
* ...fill in image data for time "vtime" and duration "duration" here...
* w.writeFrame(1, img, vsduration);
* vtime += duration;
* }
* }
* w.close();
*
*
* For information about the QuickTime file format see the
* "QuickTime File Format Specification", Apple Inc. 2010-08-03. (qtff)
*
* http://developer.apple.com/library/mac/documentation/QuickTime/QTFF/qtff.pdf
*
*
* @author Werner Randelshofer
* @version 1.3.3 2011-01-17 Improves writing of compressed movie headers.
*
1.3.2 2011-01-17 Fixes out of bounds exception when writing
* sub-images with RLE encoder. Fixes writing of compressed movie headers.
*
1.3.1 2011-01-09 Fixes broken RAW encoder.
*
1.3 2011-01-07 Improves robustness of API.
* Adds method toWebOptimizedMovie().
*
1.2.2 2011-01-07 Reduces file seeking with "RLE" encoder.
*
1.2.1 2011-01-07 Fixed default syncInterval for "RLE" video.
*
1.2 2011-01-05 Adds support for "RLE" encoded video.
*
1.1 2011-01-04 Adds "depth" parameter to addVideoTrack method.
*
1.0 2011-01-02 Adds support for edit lists. Adds support for MP3
* audio format.
*
0.1.1 2010-12-05 Updates the link to the QuickTime file format
* specification.
*
0.1 2010-09-30 Created.
*/
@SuppressWarnings("resource")
public class QuickTimeWriter {
/** An {@code Edit} define the portions of the media that are to be used to
* build up a track for a movie. The edits themselves are stored in an edit
* list table, which consists of time offset and duration values for each
* segment.
*
* 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.
*/
public static class Edit {
/** A 32-bit integer that specifies the duration of this edit
* segment in units of the movie's time scale.
*/
public int trackDuration;
/** A 32-bit integer containing the start time within the media
* of this edit segment (in media time scale units). If this field is
* set to -1, it is an empty edit. The last edit in a track should never
* be an empty edit. Any differece between the movie's duration and
* the track's duration is expressed as an implicit empty edit.
*/
public int mediaTime;
/** A 32-bit fixed-point number (16.16) that specifies the relative rate
* at which to play the media corresponding to this edit segment. This
* rate value cannot be 0 or negative.
*/
public int mediaRate;
/**
* Creates an edit.
*
* @param trackDuration Duration of this edit in the movie's time scale.
* @param mediaTime Start time of this edit in the media's time scale.
* Specify -1 for an empty edit. The last edit in
* a track should never be an empty edit.
* @param mediaRate The relative rate at which to play this edit.
*/
public Edit(int trackDuration, int mediaTime, double mediaRate) {
if (trackDuration < 0) {
throw new IllegalArgumentException("trackDuration must not be < 0:" + trackDuration);
}
if (mediaTime < -1) {
throw new IllegalArgumentException("mediaTime must not be < -1:" + mediaTime);
}
if (mediaRate <= 0) {
throw new IllegalArgumentException("mediaRate must not be <= 0:" + mediaRate);
}
this.trackDuration = trackDuration;
this.mediaTime = mediaTime;
this.mediaRate = (int) (mediaRate * (1 << 16));
}
/**
* Creates an edit.
*
* Use this constructor only if you want to compute the fixed point
* media rate by yourself.
*
* @param trackDuration Duration of this edit in the movie's time scale.
* @param mediaTime Start time of this edit in the media's time scale.
* Specify -1 for an empty edit. The last edit in
* a track should never be an empty edit.
* @param mediaRate The relative rate at which to play this edit given
* as a 16.16 fixed point value.
*/
public Edit(int trackDuration, int mediaTime, int mediaRate) {
if (trackDuration < 0) {
throw new IllegalArgumentException("trackDuration must not be < 0:" + trackDuration);
}
if (mediaTime < -1) {
throw new IllegalArgumentException("mediaTime must not be < -1:" + mediaTime);
}
if (mediaRate <= 0) {
throw new IllegalArgumentException("mediaRate must not be <= 0:" + mediaRate);
}
this.trackDuration = trackDuration;
this.mediaTime = mediaTime;
this.mediaRate = mediaRate;
}
}
/**
* Underlying output stream.
*/
private ImageOutputStream out;
/** The offset of the QuickTime stream in the underlying ImageOutputStream.
* Normally this is 0 unless the underlying stream already contained data
* when it was passed to the constructor.
*/
private long streamOffset;
/**
* Built-in video formats.
*/
public static enum VideoFormat {
/** Raw video frames (lossless and uncompressed). */
RAW("raw ", "None", true),
/** Photo-JPEG encoded video frames (lossy). */
JPG("jpeg", "Photo - JPEG", true),
/** PNG encoded video frames (lossless). */
PNG("png ", "PNG", true),
/** Apple Animation (RLE) encoded video frames (lossless). */
RLE("rle ", "Animation", false),;
private String compressionType;
private String compressorName;
private boolean allSamplesAreSyncSamples;
VideoFormat(String compressionType, String compressorName, boolean allSamplesAreSyncSamples) {
this.compressionType = compressionType;
this.compressorName = compressorName;
this.allSamplesAreSyncSamples = allSamplesAreSyncSamples;
}
}
/**
* Supported track types.
*/
private static enum MediaType {
VIDEO, AUDIO;
}
/**
* Creation time of the movie output stream.
*/
private Date creationTime;
/**
* The timeScale of the movie.
* 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.
*/
private long movieTimeScale = 600;
/** The list of tracks in this movie file. */
private ArrayList