package com.github.xsavikx.androidscreencast.api.recording; import com.github.xsavikx.androidscreencast.api.recording.atom.AtomType; import com.github.xsavikx.androidscreencast.api.recording.atom.CompositeAtom; import com.github.xsavikx.androidscreencast.api.recording.atom.DataAtom; import com.github.xsavikx.androidscreencast.api.recording.atom.WideDataAtom; import com.github.xsavikx.androidscreencast.api.recording.exception.OutputStreamAlreadyClosedException; import com.github.xsavikx.androidscreencast.exception.IORuntimeException; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.FileImageOutputStream; import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.MemoryCacheImageOutputStream; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.io.*; import java.nio.file.Files; import java.util.Date; import java.util.LinkedList; import static com.github.xsavikx.androidscreencast.api.recording.atom.AtomType.*; import static com.google.common.base.Preconditions.*; import static com.google.common.collect.Lists.newLinkedList; /** * Implementation of QuickTime video encoder. * For more information see <a href="developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFPreface/qtffPreface.html">QuickTime File Format Specification</a> */ public class QuickTimeOutputStream { private static final int UNSPECIFIED = -1; private static final float DEFAULT_QUALITY = 0.9f; private static final int DEFAULT_TIME_SCALE = 600; /** * Output stream of the QuickTimeOutputStream. */ private final ImageOutputStream out; /** * Current video format. */ private final VideoFormat videoFormat; /** * Quality of JPEG encoded video frames. */ private float quality = DEFAULT_QUALITY; /** * Creation time of the movie output stream. */ private Date creationTime; /** * Width of the video frames. All frames must have the same width. The value -1 is used to mark unspecified width. */ private int imgWidth = UNSPECIFIED; /** * Height of the video frames. All frames must have the same height. The value -1 is used to mark unspecified height. */ private int imgHeight = UNSPECIFIED; /** * 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 int timeScale = DEFAULT_TIME_SCALE; /** * The current state of the movie output stream. */ private States state = States.FINISHED; /** * List of video frames. */ private LinkedList<Sample> videoFrames; /** * This atom holds the movie frames. */ private WideDataAtom mdatAtom; /** * Creates a new output stream with the specified image videoFormat and framerate. * * @param file the output file * @param format Selects an encoder for the video format "JPG" or "PNG". * @throws IllegalArgumentException if videoFormat is null or if framerate is <= 0 */ public QuickTimeOutputStream(File file, VideoFormat format) throws IOException { checkNotNull(file, "Result file should not be null."); checkNotNull(format, "Video format must not be null."); Files.deleteIfExists(file.toPath()); out = new FileImageOutputStream(file); this.videoFormat = format; this.videoFrames = newLinkedList(); } /** * Closes the movie file as well as the stream being filtered. */ public void close() { if (state == States.STARTED) { finish(); } if (state != States.CLOSED) { try { out.close(); } catch (IOException e) { throw new IORuntimeException(e); } state = States.CLOSED; } } /** * Check to make sure that this stream has not been closed */ private void ensureOpen() { if (state == States.CLOSED) { throw new OutputStreamAlreadyClosedException(); } } /** * Sets the state of the QuickTimeOutpuStream to started. * <p> * If the state is changed by this method, the prolog is written. */ private void ensureStarted() { if (state != States.STARTED) { creationTime = new Date(); writeProlog(); mdatAtom = new WideDataAtom(MEDIA_DATA, out); state = States.STARTED; } } /** * 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. * * @throws IllegalStateException if the dimension of the video track has not been specified or determined yet. */ public void finish() { ensureOpen(); if (state != States.FINISHED) { checkState(imgWidth != UNSPECIFIED && imgHeight != UNSPECIFIED, "Image width and height must be specified, but were: width=%s, height=%s", imgWidth, imgHeight); mdatAtom.finish(); writeEpilog(); state = States.FINISHED; imgWidth = imgHeight = UNSPECIFIED; } } /** * Returns the time scale of this media. * * @return time scale */ public int getTimeScale() { return timeScale; } /** * Sets the time scale for this media, that is, the number of time units that pass per second in its time coordinate system. * <p> * The default value is 600. * * @param timeScale */ public void setTimeScale(int timeScale) { checkArgument(timeScale > 0, "timeScale must be greater 0, but was %s", timeScale); this.timeScale = timeScale; } /** * Returns the video compression quality. * * @return video compression quality */ public float getVideoCompressionQuality() { return quality; } /** * Sets the compression quality of the video track. A value of 0 stands for "high compression is important" a value of 1 for * "high image quality is important". * <p> * Changing this value affects frames which are subsequently written to the QuickTimeOutputStream. Frames which have already been written are not * changed. * <p> * This value has no effect on videos encoded with the PNG format. * <p> * The default value is 0.9. * * @param quality */ public void setVideoCompressionQuality(float quality) { checkArgument(quality >= 0f && quality <= 1f, "Compression quality should be between 0 and 1, but was %s", quality); this.quality = quality; } /** * Sets the dimension of the video track. * <p> * You need to explicitly set the dimension, if you add all frames from files or input streams. * <p> * If you add frames from buffered images, then QuickTimeOutputStream can determine the video dimension from the image width and height. * * @param width * @param height */ public void setVideoDimension(int width, int height) { checkArgument(width > 1 && height > 1, "width and height must be greater than 0, but were: width=%s, height=%s", width, height); this.imgWidth = width; this.imgHeight = height; } private void writeEpilog() { try { Date modificationTime = new Date(); int duration = 0; for (Sample s : videoFrames) { duration += s.duration; } /* Movie Atom */ CompositeAtom movieAtom = new CompositeAtom(MOVIE, out); movieAtom.add(createMovieHeaderAtom(modificationTime, duration, out)); movieAtom.add(createTrackAtom(modificationTime, duration, out)); movieAtom.finish(); } catch (IOException e) { throw new IORuntimeException(e); } } /** * Track Atom * * @param modificationTime calendar date and time of last modification * @param duration time value that indicates duration of video * @param out ImageOutputStream for this data atom * @return filled Track Atom * @throws IOException if any write operation fails */ private CompositeAtom createTrackAtom(Date modificationTime, int duration, ImageOutputStream out) throws IOException { CompositeAtom trackAtom = new CompositeAtom(TRACK, out); trackAtom.add(createTrackHeaderAtom(modificationTime, duration, out)); trackAtom.add(createMediaAtom(modificationTime, duration, out)); return trackAtom; } /** * 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'). * <p> * 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; * * @param modificationTime calendar date and time of last modification * @param duration time value that indicates duration of video * @param out ImageOutputStream for this data atom * @return filled Track Header Atom * @throws IOException if any write operation fails */ private DataAtom createTrackHeaderAtom(Date modificationTime, int duration, ImageOutputStream out) throws IOException { DataAtom leaf = new DataAtom(TRACK_HEADER, out); DataAtomOutputStream 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(0xf); // 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(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.writeInt(duration); // 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.writeShort(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. d.writeFixed16D16(1f); // matrix[0] d.writeFixed16D16(0f); // matrix[1] d.writeFixed2D30(0f); // matrix[2] d.writeFixed16D16(0f); // matrix[3] d.writeFixed16D16(1f); // matrix[4] d.writeFixed2D30(0); // matrix[5] d.writeFixed16D16(0); // matrix[6] d.writeFixed16D16(0); // matrix[7] d.writeFixed2D30(1f); // 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(imgWidth); // width // A 32-bit fixed-point number that specifies the width of this track in // pixels. d.writeFixed16D16(imgHeight); // height // A 32-bit fixed-point number that indicates the height of this track // in pixels. return leaf; } /** * Media Atom * * @param modificationTime calendar date and time of last modification * @param duration time value that indicates duration of video * @param out ImageOutputStream for this data atom * @return filled Media Atom * @throws IOException if any write operation fails */ private CompositeAtom createMediaAtom(Date modificationTime, int duration, ImageOutputStream out) throws IOException { CompositeAtom mediaAtom = new CompositeAtom(MEDIA, out); mediaAtom.add(createMediaHeaderAtom(modificationTime, duration, out)); mediaAtom.add(createMediaHandlerAtom(out)); mediaAtom.add(createMediaInformationAtom(out)); return mediaAtom; } /** * Media Header atom * typedef struct { byte version; byte[3] flags; mactimestamp creationTime; mactimestamp modificationTime; int * timeScale; int duration; short language; short quality; } mediaHeaderAtom; * * @param modificationTime calendar date and time of last modification * @param duration time value that indicates duration of video * @param out ImageOutputStream for this data atom * @return filled Media Header Atom * @throws IOException if any write operation fails */ private DataAtom createMediaHeaderAtom(Date modificationTime, int duration, ImageOutputStream out) throws IOException { DataAtom mediaHangler = new DataAtom(MEDIA_HEADER, out); DataAtomOutputStream d = mediaHangler.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.writeInt(timeScale); // 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.writeInt(duration); // 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. return mediaHangler; } /** * Create Handler Atom * * @param out ImageOutputStream for this data atom * @param componentType 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. * @return filled Handler Atom * @throws IOException if any write operation fails */ private DataAtom createHandlerAtom(ImageOutputStream out, AtomType componentType) throws IOException { DataAtom handlerAtom = new DataAtom(HANDLER, out); /* * typedef struct { byte version; byte[3] flags; magic componentType; magic componentSubtype; magic componentManufacturer; int componentFlags; int * componentFlagsMask; cstring componentName; } handlerReferenceAtom; */ DataAtomOutputStream d = handlerAtom.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(componentType); // 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(VIDEO); // 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. d.writeInt(0); // componentManufacturer // Reserved. Set to 0. d.writeInt(0); // componentFlags // Reserved. Set to 0. d.writeInt(0); // componentFlagsMask // Reserved. Set to 0. d.write(0); // 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. return handlerAtom; } /** * Media Handler Atom * * @param out ImageOutputStream for this data atom * @return filled Media Handler Atom * @throws IOException if any write operation fails */ private DataAtom createMediaHandlerAtom(ImageOutputStream out) throws IOException { return createHandlerAtom(out, MEDIA_HANDLER); } /** * Media Information atom * * @param out ImageOutputStream for this data atom * @return filled Media Information Atom * @throws IOException if any write operation fails */ private CompositeAtom createMediaInformationAtom(ImageOutputStream out) throws IOException { CompositeAtom mediaInformationAtom = new CompositeAtom(MEDIA_INFORMATION, out); mediaInformationAtom.add(createVideoMediaInformationAtom(out)); mediaInformationAtom.add(createHandleReferenceAtom(out)); mediaInformationAtom.add(createDataInformationAtom(out)); mediaInformationAtom.add(createSampleTableAtom(out)); return mediaInformationAtom; } /** * Video media information atom * * @param out ImageOutputStream for this data atom * @return filled Video Media Information Atom * @throws IOException if any write operation fails */ private DataAtom createVideoMediaInformationAtom(ImageOutputStream out) throws IOException { DataAtom videoMediaInformationAtom = new DataAtom(VIDEO_MEDIA_INFORMATION, out); /* * typedef struct { byte version; byte flag1; byte flag2; byte set vmhdFlags flag3; short graphicsMode; ushort[3] opcolor; } * videoMediaInformationHeaderAtom; */ DataAtomOutputStream d = videoMediaInformationAtom.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. return videoMediaInformationAtom; } /** * Handle 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'. * * @param out ImageOutputStream for this data atom * @return filled Handle Reference Atom * @throws IOException if any write operation fails */ private DataAtom createHandleReferenceAtom(ImageOutputStream out) throws IOException { return createHandlerAtom(out, DATA_HANDLER); } /** * Data Information Atom * * @param out ImageOutputStream for this data atom * @return filled Data Information Atom * @throws IOException if any write operation fails */ private CompositeAtom createDataInformationAtom(ImageOutputStream out) throws IOException { CompositeAtom dataInformationAtom = new CompositeAtom(DATA_INFORMATION, out); dataInformationAtom.add(createDataReferenceAtom(out)); return dataInformationAtom; } /** * Data reference atom * Data reference atoms contain tabular data that instructs the data * handler component how to access the media's data. * * @param out ImageOutputStream for this data atom * @return filled Data Reference Atom * @throws IOException if any write operation fails */ private DataAtom createDataReferenceAtom(ImageOutputStream out) throws IOException { DataAtom leaf = new DataAtom(DATA_REFERENCE, out); /* * 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; */ DataAtomOutputStream 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(FILE_ALIAS); // 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. return leaf; } /** * Sample Table Atom * * @param out ImageOutputStream for this data atom * @return filled Sample Table Atom * @throws IOException if any write operation fails */ private CompositeAtom createSampleTableAtom(ImageOutputStream out) throws IOException { CompositeAtom sampleTableAtom = new CompositeAtom(SAMPLE_TABLE, out); sampleTableAtom.add(createSampleDescriptionAtom(out)); sampleTableAtom.add(createTimeToSampleAtom(out)); sampleTableAtom.add(createSamplesToChunksMappingAtom(out)); sampleTableAtom.add(createSamplesSizeAtom(out)); sampleTableAtom.add(createChunkOffsetTableAtom(out)); return sampleTableAtom; } /** * Sample Description atom * The sample description atom stores information that allows you to * decode samples in the media. The data stored in the sample * description varies, depending on the media type. For example, in the * case of video media, the sample descriptions are image description * structures. The sample description information for each media type is * explained in "Media Data Atom Types": * <p> * http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_1.html#//apple_ref/doc/uid/TP40000939-CH205-SW1 * * @param out ImageOutputStream for this data atom * @return filled Sample Description Atom * @throws IOException if any write operation fails */ private DataAtom createSampleDescriptionAtom(ImageOutputStream out) throws IOException { DataAtom sampleDescriptionAtom = new DataAtom(SAMPLE_DESCRIPTION, out); /* * typedef struct { byte version; byte[3] flags; int numberOfEntries; sampleDescriptionEntry sampleDescriptionTable[numberOfEntries]; } * sampleDescriptionAtom; * * typedef struct { int size; magic type; byte[6] reserved; // six bytes that must be zero short dataReferenceIndex; // 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. byte[size - 16] data; } sampleDescriptionEntry; */ DataAtomOutputStream d = sampleDescriptionAtom.getOutputStream(); d.write(0); // version // A 1-byte specification of the version of this sample description // atom. d.write(0); // flag[0] d.write(0); // flag[1] d.write(0); // flag[2] // A 3-byte space for sample description flags. Set this field to 0. d.writeInt(1); // number of Entries // A 32-bit integer containing the number of sample descriptions that // follow. // A 32-bit integer indicating the number of bytes in the sample // description. switch (videoFormat) { case RAW: { writeRawSampleDescriptionAtomData(d); break; } case JPG: { writeJpgSampleDescriptionAtomData(d); break; } case PNG: { writePngSampleDescriptionAtomData(d); break; } default: throw new IllegalStateException("Such video format is not supported: " + videoFormat); } return sampleDescriptionAtom; } private void writeRawSampleDescriptionAtomData(DataAtomOutputStream d) throws IOException { d.writeInt(86); // sampleDescriptionTable[0].size d.writeType(RAW); // sampleDescriptionTable[0].type // A 32-bit integer indicating the format of the stored data. // This depends on the media type, but is usually either the // compression format or the media type. d.write(new byte[6]); // sampleDescriptionTable[0].reserved // Six bytes that must be set to 0. d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex // 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. // Video Sample Description // ------------------------ // The format of the following fields is described here: // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version // A 16-bit integer indicating the version number of the // compressed data. This is set to 0, unless a compressor has // changed its data format. d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel // A 16-bit integer that must be set to 0. d.writeType(JAVA); // sampleDescriptionTable.videoSampleDescription.manufacturer // A 32-bit integer that specifies the developer of the // compressor that generated the compressed data. Often this // field contains 'appl' to indicate Apple Computer, Inc. d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality // A 32-bit integer containing a value from 0 to 1023 indicating // the degree of temporal compression. d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality // A 32-bit integer containing a value from 0 to 1024 indicating // the degree of spatial compression. d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width // A 16-bit integer that specifies the width of the source image // in pixels. d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height // A 16-bit integer that specifies the height of the source image in // pixels. d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution // A 32-bit fixed-point number containing the horizontal // resolution of the image in pixels per inch. d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution // A 32-bit fixed-point number containing the vertical // resolution of the image in pixels per inch. d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize // A 32-bit integer that must be set to 0. d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount // A 16-bit integer that indicates how many frames of compressed // data are stored in each sample. Usually set to 1. d.writePString("None", 32); // sampleDescriptionTable.videoSampleDescription.compressorName // A 32-byte Pascal string containing the name of the compressor // that created the image, such as "jpeg". d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth // A 16-bit integer that indicates the pixel depth of the // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32 // indicate the depth of color images. The value 32 should be // used only if the image contains an alpha channel. Values of // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale, // respectively, for grayscale images. d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID // A 16-bit integer that identifies which color table to use. // If this field is set to -1, the default color table should be // used for the specified depth. For all depths below 16 bits // per pixel, this indicates a standard Macintosh color table // for the specified depth. Depths of 16, 24, and 32 have no // color table. } private void writeJpgSampleDescriptionAtomData(DataAtomOutputStream d) throws IOException { d.writeInt(86); // sampleDescriptionTable[0].size d.writeType(JPEG); // sampleDescriptionTable[0].type // A 32-bit integer indicating the format of the stored data. // This depends on the media type, but is usually either the // compression format or the media type. d.write(new byte[6]); // sampleDescriptionTable[0].reserved // Six bytes that must be set to 0. d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex // 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. // Video Sample Description // ------------------------ // The format of the following fields is described here: // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version // A 16-bit integer indicating the version number of the // compressed data. This is set to 0, unless a compressor has // changed its data format. d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel // A 16-bit integer that must be set to 0. d.writeType(JAVA); // sampleDescriptionTable.videoSampleDescription.manufacturer // A 32-bit integer that specifies the developer of the // compressor that generated the compressed data. Often this // field contains 'appl' to indicate Apple Computer, Inc. d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality // A 32-bit integer containing a value from 0 to 1023 indicating // the degree of temporal compression. d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality // A 32-bit integer containing a value from 0 to 1024 indicating // the degree of spatial compression. d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width // A 16-bit integer that specifies the width of the source image // in pixels. d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height // A 16-bit integer that specifies the height of the source image in // pixels. d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution // A 32-bit fixed-point number containing the horizontal // resolution of the image in pixels per inch. d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution // A 32-bit fixed-point number containing the vertical // resolution of the image in pixels per inch. d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize // A 32-bit integer that must be set to 0. d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount // A 16-bit integer that indicates how many frames of compressed // data are stored in each sample. Usually set to 1. d.writePString("Photo - JPEG", 32); // sampleDescriptionTable.videoSampleDescription.compressorName // A 32-byte Pascal string containing the name of the compressor // that created the image, such as "jpeg". d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth // A 16-bit integer that indicates the pixel depth of the // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32 // indicate the depth of color images. The value 32 should be // used only if the image contains an alpha channel. Values of // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale, // respectively, for grayscale images. d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID // A 16-bit integer that identifies which color table to use. // If this field is set to -1, the default color table should be // used for the specified depth. For all depths below 16 bits // per pixel, this indicates a standard Macintosh color table // for the specified depth. Depths of 16, 24, and 32 have no // color table. } private void writePngSampleDescriptionAtomData(DataAtomOutputStream d) throws IOException { d.writeInt(86); // sampleDescriptionTable[0].size d.writeType(PNG); // sampleDescriptionTable[0].type // A 32-bit integer indicating the format of the stored data. // This depends on the media type, but is usually either the // compression format or the media type. d.write(new byte[6]); // sampleDescriptionTable[0].reserved // Six bytes that must be set to 0. d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex // 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. // Video Sample Description // ------------------------ // The format of the following fields is described here: // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version // A 16-bit integer indicating the version number of the // compressed data. This is set to 0, unless a compressor has // changed its data format. d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel // A 16-bit integer that must be set to 0. d.writeType(JAVA); // sampleDescriptionTable.videoSampleDescription.manufacturer // A 32-bit integer that specifies the developer of the // compressor that generated the compressed data. Often this // field contains 'appl' to indicate Apple Computer, Inc. d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality // A 32-bit integer containing a value from 0 to 1023 indicating // the degree of temporal compression. d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality // A 32-bit integer containing a value from 0 to 1024 indicating // the degree of spatial compression. d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width // A 16-bit integer that specifies the width of the source image // in pixels. d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height // A 16-bit integer that specifies the height of the source image in // pixels. d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution // A 32-bit fixed-point number containing the horizontal // resolution of the image in pixels per inch. d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution // A 32-bit fixed-point number containing the vertical // resolution of the image in pixels per inch. d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize // A 32-bit integer that must be set to 0. d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount // A 16-bit integer that indicates how many frames of compressed // data are stored in each sample. Usually set to 1. d.writePString("PNG", 32); // sampleDescriptionTable.videoSampleDescription.compressorName // A 32-byte Pascal string containing the name of the compressor // that created the image, such as "jpeg". d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth // A 16-bit integer that indicates the pixel depth of the // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32 // indicate the depth of color images. The value 32 should be // used only if the image contains an alpha channel. Values of // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale, // respectively, for grayscale images. d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID // A 16-bit integer that identifies which color table to use. // If this field is set to -1, the default color table should be // used for the specified depth. For all depths below 16 bits // per pixel, this indicates a standard Macintosh color table // for the specified depth. Depths of 16, 24, and 32 have no // color table. } /** * 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. * * @param out ImageOutputStream for this data atom * @return filled Sample Size Atom * @throws IOException if any write operation fails */ private DataAtom createSamplesSizeAtom(ImageOutputStream out) throws IOException { DataAtom samplesSizeAtom = new DataAtom(SAMPLE_SIZE, out); /* * typedef struct { byte version; byte[3] flags; int sampleSize; int numberOfEntries; sampleSizeTable sampleSizeTable[numberOfEntries]; } * sampleSizeAtom; * * typedef struct { int size; } sampleSizeTable; */ DataAtomOutputStream d = samplesSizeAtom.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(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. d.writeUInt(videoFrames.size()); // number of entries // A 32-bit integer containing the count of entries in the sample size // table. for (Sample s : videoFrames) { d.writeUInt(s.length); // 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. } return samplesSizeAtom; } /** * chunk offset table atom * The chunk-offset table gives the index of each chunk into the * containing file. 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. * * @param out ImageOutputStream for this data atom * @return filled Chunk Offset Atom * @throws IOException if any write operation fails */ private DataAtom createChunkOffsetTableAtom(ImageOutputStream out) throws IOException { if (videoFrames.size() == 0 || videoFrames.getLast().offset <= 0xffffffffL) { return create32BitChunkOffsetTableAtom(out); } return create64BitChunkOffsetTableAtom(out); } /** * 32-bit chunk offset table atom * * @param out ImageOutputStream for this data atom * @return filled 32-bit Chunk Offset Table Atom * @throws IOException if any write operation fails */ private DataAtom create32BitChunkOffsetTableAtom(ImageOutputStream out) throws IOException { DataAtom chunkOffsetAtom = new DataAtom(STANDARD_CHUNK_OFFSET_TABLE, out); /* * typedef struct { byte version; byte[3] flags; int numberOfEntries; chunkOffsetTable chunkOffsetTable[numberOfEntries]; } chunkOffsetAtom; * * typedef struct { int offset; } chunkOffsetTable; */ DataAtomOutputStream d = chunkOffsetAtom.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(videoFrames.size()); // number of entries // A 32-bit integer containing the count of entries in the chunk // offset table. for (Sample s : videoFrames) { d.writeUInt(s.offset); // 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. } return chunkOffsetAtom; } /** * 64-bit chunk offset table atom * * @param out ImageOutputStream for this data atom * @return filled 64-bit Chunk Offset Table Atom * @throws IOException if any write operation fails */ private DataAtom create64BitChunkOffsetTableAtom(ImageOutputStream out) throws IOException { DataAtom chunkOffsetAtom = new DataAtom(WIDE_CHUNK_OFFSET_TABLE, out); /* * typedef struct { byte version; byte[3] flags; int numberOfEntries; chunkOffsetTable chunkOffset64Table[numberOfEntries]; } chunkOffset64Atom; * * typedef struct { long offset; } chunkOffset64Table; */ DataAtomOutputStream d = chunkOffsetAtom.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(videoFrames.size()); // number of entries // A 32-bit integer containing the count of entries in the chunk // offset table. for (Sample s : videoFrames) { d.writeLong(s.offset); // 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. } return chunkOffsetAtom; } /** * 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. * * @param out ImageOutputStream for this data atom * @return filled Sample To Chunk Atom * @throws IOException if any write operation fails */ private DataAtom createSamplesToChunksMappingAtom(ImageOutputStream out) throws IOException { DataAtom samplesToChunksMappingAtom = new DataAtom(SAMPLE_TO_CHUNK_MAPPING, out); /* * typedef struct { byte version; byte[3] flags; int numberOfEntries; sampleToChunkTable sampleToChunkTable[numberOfEntries]; } sampleToChunkAtom; * * typedef struct { int firstChunk; int samplesPerChunk; int sampleDescription; } sampleToChunkTable; */ DataAtomOutputStream d = samplesToChunksMappingAtom.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.writeInt(1); // number of entries // A 32-bit integer containing the count of entries in the // sample-to-chunk table. d.writeInt(1); // first chunk // The first chunk number using this table entry. d.writeInt(1); // samples per chunk // The number of samples in each chunk. d.writeInt(1); // 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 return samplesToChunksMappingAtom; } /** * 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'. * * @param out ImageOutputStream for this data atom * @return filled Time to Sample Atom * @throws IOException if any write operation fails */ private DataAtom createTimeToSampleAtom(ImageOutputStream out) throws IOException { DataAtom timeToSampleAtom = new DataAtom(TIME_TO_SAMPLE_MAPPING, out); /* * typedef struct { byte version; byte[3] flags; int numberOfEntries; timeToSampleTable timeToSampleTable[numberOfEntries]; } timeToSampleAtom; * * typedef struct { int sampleCount; int sampleDuration; } timeToSampleTable; */ DataAtomOutputStream d = timeToSampleAtom.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. // count runs of video frame durations int runCount = 1; int prevDuration = videoFrames.size() == 0 ? 0 : videoFrames.get(0).duration; for (Sample s : videoFrames) { if (s.duration != prevDuration) { runCount++; prevDuration = s.duration; } } d.writeInt(runCount); // numberOfEntries // A 32-bit integer containing the count of entries in the // time-to-sample table. int runLength = 0; prevDuration = videoFrames.size() == 0 ? 0 : videoFrames.get(0).duration; for (Sample s : videoFrames) { if (s.duration != prevDuration) { if (runLength > 0) { d.writeInt(runLength); // timeToSampleTable[0].sampleCount // A 32-bit integer that specifies the number of consecutive // samples that have the same duration. d.writeInt(prevDuration); // timeToSampleTable[0].sampleDuration // A 32-bit integer that specifies the duration of each // sample. } prevDuration = s.duration; runLength = 1; } else { runLength++; } } if (runLength > 0) { d.writeInt(runLength); // timeToSampleTable[0].sampleCount // A 32-bit integer that specifies the number of consecutive // samples that have the same duration. d.writeInt(prevDuration); // timeToSampleTable[0].sampleDuration // A 32-bit integer that specifies the duration of each // sample. } return timeToSampleAtom; } /** * 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'. * <p> * typedef struct { byte version; byte[3] flags; mactimestamp creationTime; mactimestamp modificationTime; int timeScale; int duration; int * preferredRate; short preferredVolume; byte[10] reserved; int[9] matrix; int previewTime; int previewDuration; int posterTime; int * selectionTime; int selectionDuration; int currentTime; int nextTrackId; } movieHeaderAtom; * * @param duration time value that indicates duration of video * @param modificationTime calendar date and time of last modification * @param out ImageOutputStream for this data atom * @return filled Movie Header Atom * @throws IOException if any write operation fails */ private DataAtom createMovieHeaderAtom(Date modificationTime, int duration, ImageOutputStream out) throws IOException { DataAtom movieHeader = new DataAtom(MOVIE_HEADER, out); DataAtomOutputStream d = movieHeader.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.writeInt(timeScale); // 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.writeInt(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(1d); // 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.writeShort(256); // 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(1f); // matrix[0] d.writeFixed16D16(0f); // matrix[1] d.writeFixed2D30(0f); // matrix[2] d.writeFixed16D16(0f); // matrix[3] d.writeFixed16D16(1f); // matrix[4] d.writeFixed2D30(0); // matrix[5] d.writeFixed16D16(0); // matrix[6] d.writeFixed16D16(0); // matrix[7] d.writeFixed2D30(1f); // 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.writeInt(0); // previewTime // The time value in the movie at which the preview begins. d.writeInt(0); // previewDuration // The duration of the movie preview in movie time scale units. d.writeInt(0); // posterTime // The time value of the time of the movie poster. d.writeInt(0); // selectionTime // The time value for the start time of the current selection. d.writeInt(0); // selectionDuration // The duration of the current selection in movie time scale units. d.writeInt(0); // currentTime; // The time value for current time position within the movie. d.writeInt(2); // 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. return movieHeader; } /** * Writes a frame to the video track. * <p> * If the dimension of the video track has not been specified yet, it is derived from the first buffered image added to the QuickTimeOutputStream. * * @param image The frame image. * @param duration The duration of the frame in time scale units. * @throws IllegalArgumentException if the duration is less than 1, or if the dimension of the frame does not match the dimension of the video track. * @throws IORuntimeException if writing the image failed. */ public void writeFrame(BufferedImage image, int duration) { checkArgument(duration >= 0, "duration should be greater than 0, but was %s", duration); ensureOpen(); ensureStarted(); // Get the dimensions of the first image if (imgWidth == UNSPECIFIED) { imgWidth = image.getWidth(); imgHeight = image.getHeight(); } else { // The dimension of the image must match the dimension of the video // track checkState(imgWidth == image.getWidth() && imgHeight == image.getHeight(), "Dimensions of image[%s] (width=%s, height=%s) differs from image[0] (width=%s, height=%s", videoFrames.size(), image.getWidth(), image.getHeight(), imgWidth, imgHeight); } try { long offset = out.getStreamPosition(); switch (videoFormat) { case RAW: { WritableRaster raster = image.getRaster(); int[] raw = new int[imgWidth * 3]; // holds a scanline of raw image // data with 3 channels of 32 // bit data byte[] bytes = new byte[imgWidth * 3]; // holds a scanline of raw // image data with 3 // channels of 8 bit data int n = imgWidth * 3; for (int y = 0; y < imgHeight; y++) { raster.getPixels(0, y, imgWidth, 1, raw); for (int k = 0; k < n; k++) { bytes[k] = (byte) raw[k]; } mdatAtom.getOutputStream().write(bytes); } break; } case JPG: { ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/jpeg").next(); ImageWriteParam iwParam = iw.getDefaultWriteParam(); iwParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); iwParam.setCompressionQuality(quality); MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(mdatAtom.getOutputStream()); iw.setOutput(imgOut); IIOImage img = new IIOImage(image, null, null); iw.write(null, img, iwParam); iw.dispose(); break; } case PNG: default: { ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/png").next(); ImageWriteParam iwParam = iw.getDefaultWriteParam(); MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(mdatAtom.getOutputStream()); iw.setOutput(imgOut); IIOImage img = new IIOImage(image, null, null); iw.write(null, img, iwParam); iw.dispose(); break; } } long length = out.getStreamPosition() - offset; videoFrames.add(new Sample(duration, offset, length)); } catch (IOException e) { throw new IORuntimeException(e); } } /** * Writes a frame from a file to the video track. * <p> * This method does not inspect the contents of the file. The contents has to match the video format. For example, it is your responsibility to only * add JPG files if you have chosen the JPEG video format. * <p> * If you add all frames from files or from input streams, then you have to explicitly set the dimension of the video track before you call finish() * or stop(). * * @param file The file which holds the image data. * @param duration The duration of the frame in time scale units. * @throws IllegalStateException if the duration is less than 1. * @throws IORuntimeException if writing the image failed. */ public void writeFrame(File file, int duration) { try (FileInputStream in = new FileInputStream(file)) { writeFrame(in, duration); } catch (IOException e) { throw new IORuntimeException(e); } } /** * Writes a frame to the video track. * <p> * This method does not inspect the contents of the input stream. The contents has to match the video format. For example, it is your responsibility * to only add JPG files if you have chosen the JPEG video format. * <p> * If you add all frames from files or from input streams, then you have to explicitly set the dimension of the video track before you call finish() * or stop(). * * @param in The input stream which holds the image data. * @param duration The duration of the frame in time scale units. * @throws IllegalArgumentException if the duration is less than 1. * @throws IORuntimeException if writing the image failed. */ public void writeFrame(InputStream in, int duration) { checkArgument(duration >= 0, "duration should be greater than 0, but was %s", duration); ensureOpen(); ensureStarted(); try { long offset = out.getStreamPosition(); try (OutputStream mdatOut = mdatAtom.getOutputStream()) { byte[] buf = new byte[512]; int len; while ((len = in.read(buf)) != -1) { mdatOut.write(buf, 0, len); } long length = out.getStreamPosition() - offset; videoFrames.add(new Sample(duration, offset, length)); } } catch (IOException e) { throw new IORuntimeException(e); } } private void writeProlog() { try { /* * File type atom * * typedef struct { magic brand; bcd4 versionYear; bcd2 versionMonth; bcd2 versionMinor; magic[4] compatibleBrands; } ftypAtom; */ DataAtom ftypAtom = new DataAtom(FILE_TYPE, out); DataAtomOutputStream d = ftypAtom.getOutputStream(); d.writeType(QUICK_TIME); // brand d.writeBCD4(2005); // versionYear d.writeBCD2(3); // versionMonth d.writeBCD2(0); // versionMinor d.writeType(QUICK_TIME); // 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(); } catch (IOException e) { throw new IORuntimeException(e); } } /** * The states of the movie output stream. */ private enum States { STARTED, FINISHED, CLOSED } /** * Supported video formats. */ public enum VideoFormat { RAW, JPG, PNG } /** * QuickTime stores media data in samples. A sample is a single element in a sequence of time-ordered data. Samples are stored in the mdat atom. */ private static class Sample { /** * Offset of the sample relative to the start of the QuickTime file. */ private long offset; /** * Data length of the sample. */ private long length; /** * The duration of the sample in time scale units. */ private int duration; /** * Creates a new sample. * * @param duration The duration of the sample in time scale units. * @param offset Offset of the sample relative to the start of the QuickTime file. * @param length Data length of the sample. */ public Sample(int duration, long offset, long length) { this.duration = duration; this.offset = offset; this.length = length; } } }