/** * @(#)AVIOutputStreamOLD.java 1.6 2011-03-12 * * Copyright (c) 2008-2011 Werner Randelshofer, Goldau, 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 org.monte.media.avi; import org.monte.media.io.ImageOutputStreamAdapter; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.awt.image.IndexColorModel; import java.awt.image.WritableRaster; import java.io.*; import java.util.Arrays; import java.util.Date; import java.util.LinkedList; import javax.imageio.*; import javax.imageio.stream.*; /** * This class supports writing of images into an AVI 1.0 video file. * <p> * The images are written as video frames. * <p> * Video frames can be encoded with one of the following formats: * <ul> * <li>JPEG</li> * <li>PNG</li> * <li>RAW</li> * <li>RLE</li> * </ul> * All frames must have the same format. * When JPG is used each frame can have an individual encoding quality. * <p> * All frames in an AVI file must have the same duration. The duration can * be set by setting an appropriate pair of values using methods * {@link #setFrameRate} and {@link #setTimeScale}. * <p> * The length of an AVI 1.0 file is limited to 1 GB. * This class supports lengths of up to 4 GB, but such files may not work on * all players. * <p> * For detailed information about the AVI RIFF file format see:<br> * <a href="http://msdn.microsoft.com/en-us/library/ms779636.aspx">msdn.microsoft.com AVI RIFF</a><br> * <a href="http://www.microsoft.com/whdc/archive/fourcc.mspx">www.microsoft.com FOURCC for Video Compression</a><br> * <a href="http://www.saettler.com/RIFFMCI/riffmci.html">www.saettler.com RIFF</a><br> * * @deprecated Use {@link AVIWriter} instead of this class. * * @author Werner Randelshofer * @version 1.6 2011-03-12 Deprecated. * <br>1.5.1 2011-01-17 Fixes unintended closing of output stream.. * <br>1.5 2011-01-06 Adds support for RLE 8-bit video format. * <br>1.4 2011-01-04 Adds support for RAW 4-bit and 8-bit video format. Fixes offsets * in "idx1" chunk. * <br>1.3.2 2010-12-27 File size limit is 1 GB. * <br>1.3.1 2010-07-19 Fixes seeking and calculation of offsets. * <br>1.3 2010-07-08 Adds constructor with ImageOutputStream. * Added method getVideoDimension(). * <br>1.2 2009-08-29 Adds support for RAW video format. * <br>1.1 2008-08-27 Fixes computation of dwMicroSecPerFrame in avih * chunk. Changed the API to reflect that AVI works with frame rates instead of * with frame durations. * <br>1.0.1 2008-08-13 Uses FourCC "MJPG" instead of "jpg " for JPG * encoded video. * <br>1.0 2008-08-11 Created. */ public class AVIOutputStreamOLD { /** * Underlying output stream. */ private ImageOutputStream out; /** The offset 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; /** Previous frame for delta compression. */ private Object previousData; /** * Supported video formats. */ public static enum AVIVideoFormat { RAW, RLE, JPG, PNG; } /** * Current video formats. */ private AVIVideoFormat videoFormat; /** * Quality of JPEG encoded video frames. */ private float quality = 0.9f; /** * 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 = -1; /** * Height of the video frames. All frames must have the same height. * The value -1 is used to mark unspecified height. */ private int imgHeight = -1; /** Number of bits per pixel. */ private int imgDepth = 24; /** Index color model for RAW_RGB4 and RAW_RGB8 formats. */ private IndexColorModel palette; private IndexColorModel previousPalette; /** Video encoder. */ private RunLengthCodec encoder; /** * The timeScale of the movie. * <p> * Used with frameRate to specify the time scale that this stream will use. * Dividing frameRate by timeScale gives the number of samples per second. * For video streams, this is the frame rate. For audio streams, this rate * corresponds to the time needed to play nBlockAlign bytes of audio, which * for PCM audio is the just the sample rate. */ private int timeScale = 1; /** * The frameRate of the movie in timeScale units. * <p> * @see timeScale */ private int frameRate = 30; /** Interval between keyframes. */ private int syncInterval = 30; /** * The states of the movie output stream. */ private static enum States { STARTED, FINISHED, CLOSED; } /** * The current state of the movie output stream. */ private States state = States.FINISHED; /** * AVI stores media data in samples. * A sample is a single element in a sequence of time-ordered data. */ private static class Sample { String chunkType; /** Offset of the sample relative to the start of the AVI file. */ long offset; /** Data length of the sample. */ long length; /** * The duration of the sample in time scale units. */ int duration; /** Whether the sample is a sync-sample. */ boolean isSync; /** * Creates a new sample. * @param duration * @param offset * @param length */ public Sample(String chunkId, int duration, long offset, long length, boolean isSync) { this.chunkType = chunkId; this.duration = duration; this.offset = offset; this.length = length; this.isSync = isSync; } } /** * List of video frames. */ private LinkedList<Sample> videoFrames; /** * This chunk holds the whole AVI content. */ private CompositeChunk aviChunk; /** * This chunk holds the movie frames. */ private CompositeChunk moviChunk; /** * This chunk holds the AVI Main Header. */ FixedSizeDataChunk avihChunk; /** * This chunk holds the AVI Stream Header. */ FixedSizeDataChunk strhChunk; /** * This chunk holds the AVI Stream Format Header. */ FixedSizeDataChunk strfChunk; /** * Chunk base class. */ private abstract class Chunk { /** * The chunkType of the chunk. A String with the length of 4 characters. */ protected String chunkType; /** * The offset of the chunk relative to the start of the * ImageOutputStream. */ protected long offset; /** * Creates a new Chunk at the current position of the ImageOutputStream. * @param chunkType The chunkType of the chunk. A string with a length of 4 characters. */ public Chunk(String chunkType) throws IOException { this.chunkType = chunkType; offset = getRelativeStreamPosition(); } /** * Writes the chunk to the ImageOutputStream and disposes it. */ public abstract void finish() throws IOException; /** * Returns the size of the chunk including the size of the chunk header. * @return The size of the chunk. */ public abstract long size(); } /** * A CompositeChunk contains an ordered list of Chunks. */ private class CompositeChunk extends Chunk { /** * The type of the composite. A String with the length of 4 characters. */ protected String compositeType; private LinkedList<Chunk> children; private boolean finished; /** * Creates a new CompositeChunk at the current position of the * ImageOutputStream. * @param compositeType The type of the composite. * @param chunkType The type of the chunk. */ public CompositeChunk(String compositeType, String chunkType) throws IOException { super(chunkType); this.compositeType = compositeType; //out.write out.writeLong(0); // make room for the chunk header out.writeInt(0); // make room for the chunk header children = new LinkedList<Chunk>(); } public void add(Chunk child) throws IOException { if (children.size() > 0) { children.getLast().finish(); } children.add(child); } /** * Writes the chunk and all its children to the ImageOutputStream * and disposes of all resources held by the chunk. * @throws java.io.IOException */ @Override public void finish() throws IOException { if (!finished) { if (size() > 0xffffffffL) { throw new IOException("CompositeChunk \"" + chunkType + "\" is too large: " + size()); } long pointer = getRelativeStreamPosition(); seekRelative(offset); DataChunkOutputStream headerData = new DataChunkOutputStream(new ImageOutputStreamAdapter(out),false); headerData.writeType(compositeType); headerData.writeUInt(size() - 8); headerData.writeType(chunkType); for (Chunk child : children) { child.finish(); } seekRelative(pointer); if (size() % 2 == 1) { out.writeByte(0); // write pad byte } finished = true; } } @Override public long size() { long length = 12; for (Chunk child : children) { length += child.size() + child.size() % 2; } return length; } } /** * Data Chunk. */ private class DataChunk extends Chunk { private DataChunkOutputStream data; private boolean finished; /** * Creates a new DataChunk at the current position of the * ImageOutputStream. * @param chunkType The chunkType of the chunk. */ public DataChunk(String name) throws IOException { super(name); out.writeLong(0); // make room for the chunk header data = new DataChunkOutputStream(new ImageOutputStreamAdapter(out), false); } public DataChunkOutputStream getOutputStream() { if (finished) { throw new IllegalStateException("DataChunk is finished"); } return data; } /** * Returns the offset of this chunk to the beginning of the random access file * @return */ public long getOffset() { return offset; } @Override public void finish() throws IOException { if (!finished) { long sizeBefore = size(); System.out.println("sizeBefore:"+sizeBefore); if (size() > 0xffffffffL) { throw new IOException("DataChunk \"" + chunkType + "\" is too large: " + size()); } long pointer = getRelativeStreamPosition(); seekRelative(offset); DataChunkOutputStream headerData = new DataChunkOutputStream(new ImageOutputStreamAdapter(out),false); headerData.writeType(chunkType); headerData.writeUInt(size() - 8); seekRelative(pointer); if (size() % 2 == 1) { out.writeByte(0); // write pad byte } finished = true; long sizeAfter = size(); if (sizeBefore != sizeAfter) { System.err.println("size mismatch " + sizeBefore + ".." + sizeAfter); } } } @Override public long size() { return 8 + data.size(); } } /** * A DataChunk with a fixed size. */ private class FixedSizeDataChunk extends Chunk { private DataChunkOutputStream data; private boolean finished; private long fixedSize; /** * Creates a new DataChunk at the current position of the * ImageOutputStream. * @param chunkType The chunkType of the chunk. */ public FixedSizeDataChunk(String chunkType, long fixedSize) throws IOException { super(chunkType); this.fixedSize = fixedSize; data = new DataChunkOutputStream(new ImageOutputStreamAdapter(out),false); data.writeType(chunkType); data.writeUInt(fixedSize); data.clearCount(); // Fill fixed size with nulls byte[] buf = new byte[(int) Math.min(512, fixedSize)]; long written = 0; while (written < fixedSize) { data.write(buf, 0, (int) Math.min(buf.length, fixedSize - written)); written += Math.min(buf.length, fixedSize - written); } if (fixedSize % 2 == 1) { out.writeByte(0); // write pad byte } seekToStartOfData(); } public DataChunkOutputStream getOutputStream() { /*if (finished) { throw new IllegalStateException("DataChunk is finished"); }*/ return data; } /** * Returns the offset of this chunk to the beginning of the random access file * @return */ public long getOffset() { return offset; } public void seekToStartOfData() throws IOException { seekRelative(offset + 8); data.clearCount(); } public void seekToEndOfChunk() throws IOException { seekRelative(offset + 8 + fixedSize + fixedSize % 2); } @Override public void finish() throws IOException { if (!finished) { finished = true; } } @Override public long size() { return 8 + fixedSize; } } /** * Creates a new AVI file with the specified video format and * frame rate. The video has 24 bits per pixel. * * @param file the output file * @param format Specifies the video format. * @exception IllegalArgumentException if videoFormat is null or if * frame rate is <= 0 */ public AVIOutputStreamOLD(File file, AVIVideoFormat format) throws IOException { this(file,format,24); } /** * Creates a new AVI file with the specified video format and * frame rate. * * @param file the output file * @param format Selects an encoder for the video format. * @param bitsPerPixel the number of bits per pixel. * @exception IllegalArgumentException if videoFormat is null or if * frame rate is <= 0 */ public AVIOutputStreamOLD(File file, AVIVideoFormat format, int bitsPerPixel) throws IOException { if (format == null) { throw new IllegalArgumentException("format must not be null"); } if (file.exists()) { file.delete(); } this.out = new FileImageOutputStream(file); this.streamOffset = 0; this.videoFormat = format; this.videoFrames = new LinkedList<Sample>(); this.imgDepth = bitsPerPixel; if (imgDepth == 4) { byte[] gray = new byte[16]; for (int i = 0; i < gray.length; i++) { gray[i] = (byte) ((i << 4) | i); } palette = new IndexColorModel(4, 16, gray, gray, gray); } else if (imgDepth == 8) { byte[] gray = new byte[256]; for (int i = 0; i < gray.length; i++) { gray[i] = (byte) i; } palette = new IndexColorModel(8, 256, gray, gray, gray); } } /** * Creates a new AVI output stream with the specified video format and * framerate. * * @param out the underlying output stream * @param format Selects an encoder for the video format. * @exception IllegalArgumentException if videoFormat is null or if * framerate is <= 0 */ public AVIOutputStreamOLD(ImageOutputStream out, AVIVideoFormat format) throws IOException { if (format == null) { throw new IllegalArgumentException("format must not be null"); } this.out = out; this.streamOffset = out.getStreamPosition(); this.videoFormat = format; this.videoFrames = new LinkedList<Sample>(); } /** * Used with frameRate to specify the time scale that this stream will use. * Dividing frameRate by timeScale gives the number of samples per second. * For video streams, this is the frame rate. For audio streams, this rate * corresponds to the time needed to play nBlockAlign bytes of audio, which * for PCM audio is the just the sample rate. * <p> * The default value is 1. * * @param newValue */ public void setTimeScale(int newValue) { if (newValue <= 0) { throw new IllegalArgumentException("timeScale must be greater 0"); } this.timeScale = newValue; } /** * Returns the time scale of this media. * * @return time scale */ public int getTimeScale() { return timeScale; } /** * Sets the rate of video frames in time scale units. * <p> * The default value is 30. Together with the default value 1 of timeScale * this results in 30 frames pers second. * * @param newValue */ public void setFrameRate(int newValue) { if (newValue <= 0) { throw new IllegalArgumentException("frameDuration must be greater 0"); } if (state == States.STARTED) { throw new IllegalStateException("frameDuration must be set before the first frame is written"); } this.frameRate = newValue; } /** * Returns the frame rate of this media. * * @return frame rate */ public int getFrameRate() { return frameRate; } /** Sets the global color palette. */ public void setPalette(IndexColorModel palette) { this.palette = palette; } /** * 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 AVIOutputStreamOLD. Frames which have already been written * are not changed. * <p> * This value has only effect on videos encoded with JPG format. * <p> * The default value is 0.9. * * @param newValue */ public void setVideoCompressionQuality(float newValue) { this.quality = newValue; } /** * Returns the video compression quality. * * @return video compression quality */ public float getVideoCompressionQuality() { return 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 AVIOutputStreamOLD * can determine the video dimension from the image width and height. * * @param width Must be greater than 0. * @param height Must be greater than 0. */ public void setVideoDimension(int width, int height) { if (width < 1 || height < 1) { throw new IllegalArgumentException("width and height must be greater zero."); } this.imgWidth = width; this.imgHeight = height; } /** * Gets the dimension of the video track. * <p> * Returns null if the dimension is not known. */ public Dimension getVideoDimension() { if (imgWidth < 1 || imgHeight < 1) { return null; } return new Dimension(imgWidth, imgHeight); } /** * Sets the state of the QuickTimeOutpuStream to started. * <p> * If the state is changed by this method, the prolog is * written. */ private void ensureStarted() throws IOException { if (state != States.STARTED) { creationTime = new Date(); writeProlog(); state = States.STARTED; } } /** * 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 AVIOutputStreamOLD. * * @param image The frame image. * * @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 IOException if writing the image failed. */ public void writeFrame(BufferedImage image) throws IOException { ensureOpen(); ensureStarted(); // Get the dimensions of the first image if (imgWidth == -1) { imgWidth = image.getWidth(); imgHeight = image.getHeight(); } else { // The dimension of the image must match the dimension of the video track if (imgWidth != image.getWidth() || imgHeight != image.getHeight()) { throw new IllegalArgumentException("Dimensions of image[" + videoFrames.size() + "] (width=" + image.getWidth() + ", height=" + image.getHeight() + ") differs from image[0] (width=" + imgWidth + ", height=" + imgHeight); } } DataChunk videoFrameChunk; long offset = getRelativeStreamPosition(); boolean isSync = true; switch (videoFormat) { case RAW: { switch (imgDepth) { case 4: { IndexColorModel imgPalette = (IndexColorModel) image.getColorModel(); int[] imgRGBs = new int[16]; imgPalette.getRGBs(imgRGBs); int[] previousRGBs = new int[16]; if (previousPalette == null) { previousPalette = palette; } previousPalette.getRGBs(previousRGBs); if (!Arrays.equals(imgRGBs, previousRGBs)) { previousPalette = imgPalette; DataChunk paletteChangeChunk = new DataChunk("00pc"); /* int first = imgPalette.getMapSize(); int last = -1; for (int i = 0; i < 16; i++) { if (previousRGBs[i] != imgRGBs[i] && i < first) { first = i; } if (previousRGBs[i] != imgRGBs[i] && i > last) { last = i; } }*/ int first = 0; int last = imgPalette.getMapSize() - 1; /* * typedef struct { BYTE bFirstEntry; BYTE bNumEntries; WORD wFlags; PALETTEENTRY peNew[]; } AVIPALCHANGE; * * typedef struct tagPALETTEENTRY { BYTE peRed; BYTE peGreen; BYTE peBlue; BYTE peFlags; } PALETTEENTRY; */ DataChunkOutputStream pOut = paletteChangeChunk.getOutputStream(); pOut.writeByte(first);//bFirstEntry pOut.writeByte(last - first + 1);//bNumEntries pOut.writeShort(0);//wFlags for (int i = first; i <= last; i++) { pOut.writeByte((imgRGBs[i] >>> 16) & 0xff); // red pOut.writeByte((imgRGBs[i] >>> 8) & 0xff); // green pOut.writeByte(imgRGBs[i] & 0xff); // blue pOut.writeByte(0); // reserved*/ } moviChunk.add(paletteChangeChunk); paletteChangeChunk.finish(); long length = getRelativeStreamPosition() - offset; videoFrames.add(new Sample(paletteChangeChunk.chunkType, 0, offset, length - 8, false)); offset = getRelativeStreamPosition(); } videoFrameChunk = new DataChunk("00db"); byte[] rgb8 = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); byte[] rgb4 = new byte[imgWidth / 2]; for (int y = (imgHeight - 1) * imgWidth; y >= 0; y -= imgWidth) { // Upside down for (int x = 0, xx = 0, n = imgWidth; x < n; x += 2, ++xx) { rgb4[xx] = (byte) (((rgb8[y + x] & 0xf) << 4) | (rgb8[y + x + 1] & 0xf)); } videoFrameChunk.getOutputStream().write(rgb4); } break; } case 8: { IndexColorModel imgPalette = (IndexColorModel) image.getColorModel(); int[] imgRGBs = new int[256]; imgPalette.getRGBs(imgRGBs); int[] previousRGBs = new int[256]; if (previousPalette == null) { previousPalette = palette; } previousPalette.getRGBs(previousRGBs); if (!Arrays.equals(imgRGBs, previousRGBs)) { previousPalette = imgPalette; DataChunk paletteChangeChunk = new DataChunk("00pc"); /* int first = imgPalette.getMapSize(); int last = -1; for (int i = 0; i < 16; i++) { if (previousRGBs[i] != imgRGBs[i] && i < first) { first = i; } if (previousRGBs[i] != imgRGBs[i] && i > last) { last = i; } }*/ int first = 0; int last = imgPalette.getMapSize() - 1; /* * typedef struct { BYTE bFirstEntry; BYTE bNumEntries; WORD wFlags; PALETTEENTRY peNew[]; } AVIPALCHANGE; * * typedef struct tagPALETTEENTRY { BYTE peRed; BYTE peGreen; BYTE peBlue; BYTE peFlags; } PALETTEENTRY; */ DataChunkOutputStream pOut = paletteChangeChunk.getOutputStream(); pOut.writeByte(first);//bFirstEntry pOut.writeByte(last - first + 1);//bNumEntries pOut.writeShort(0);//wFlags for (int i = first; i <= last; i++) { pOut.writeByte((imgRGBs[i] >>> 16) & 0xff); // red pOut.writeByte((imgRGBs[i] >>> 8) & 0xff); // green pOut.writeByte(imgRGBs[i] & 0xff); // blue pOut.writeByte(0); // reserved*/ } moviChunk.add(paletteChangeChunk); paletteChangeChunk.finish(); long length = getRelativeStreamPosition() - offset; videoFrames.add(new Sample(paletteChangeChunk.chunkType, 0, offset, length - 8, false)); offset = getRelativeStreamPosition(); } videoFrameChunk = new DataChunk("00db"); byte[] rgb8 = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); for (int y = (imgHeight - 1) * imgWidth; y >= 0; y -= imgWidth) { // Upside down videoFrameChunk.getOutputStream().write(rgb8, y, imgWidth); } break; } default: { videoFrameChunk = new DataChunk("00db"); 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 for (int y = imgHeight - 1; y >= 0; --y) { // Upside down raster.getPixels(0, y, imgWidth, 1, raw); for (int x = 0, n = imgWidth * 3; x < n; x += 3) { bytes[x + 2] = (byte) raw[x]; // Blue bytes[x + 1] = (byte) raw[x + 1]; // Green bytes[x] = (byte) raw[x + 2]; // Red } videoFrameChunk.getOutputStream().write(bytes); } break; } } break; } case RLE: { if (encoder == null) { encoder = new RunLengthCodec(); } isSync = videoFrames.size() % syncInterval == 0; switch (imgDepth) { case 4: { throw new UnsupportedOperationException("RLE 4-bit not implemented."); } case 8: { IndexColorModel imgPalette = (IndexColorModel) image.getColorModel(); int[] imgRGBs = new int[256]; imgPalette.getRGBs(imgRGBs); int[] previousRGBs = new int[256]; if (previousPalette == null) { previousPalette = palette; } previousPalette.getRGBs(previousRGBs); if (!Arrays.equals(imgRGBs, previousRGBs)) { isSync = true; // palette change requires key frame previousPalette = imgPalette; DataChunk paletteChangeChunk = new DataChunk("00pc"); /* int first = imgPalette.getMapSize(); int last = -1; for (int i = 0; i < 16; i++) { if (previousRGBs[i] != imgRGBs[i] && i < first) { first = i; } if (previousRGBs[i] != imgRGBs[i] && i > last) { last = i; } }*/ int first = 0; int last = imgPalette.getMapSize() - 1; /* * typedef struct { BYTE bFirstEntry; BYTE bNumEntries; WORD wFlags; PALETTEENTRY peNew[]; } AVIPALCHANGE; * * typedef struct tagPALETTEENTRY { BYTE peRed; BYTE peGreen; BYTE peBlue; BYTE peFlags; } PALETTEENTRY; */ DataChunkOutputStream pOut = paletteChangeChunk.getOutputStream(); pOut.writeByte(first);//bFirstEntry pOut.writeByte(last - first + 1);//bNumEntries pOut.writeShort(0);//wFlags for (int i = first; i <= last; i++) { pOut.writeByte((imgRGBs[i] >>> 16) & 0xff); // red pOut.writeByte((imgRGBs[i] >>> 8) & 0xff); // green pOut.writeByte(imgRGBs[i] & 0xff); // blue pOut.writeByte(0); // reserved*/ } moviChunk.add(paletteChangeChunk); paletteChangeChunk.finish(); long length = getRelativeStreamPosition() - offset; videoFrames.add(new Sample(paletteChangeChunk.chunkType, 0, offset, length - 8, false)); offset = getRelativeStreamPosition(); } videoFrameChunk = new DataChunk("00dc"); byte[] rgb8 = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); if (isSync) { encoder.writeKey8(videoFrameChunk.getOutputStream(), rgb8, 0, imgWidth, imgWidth, imgHeight); } else { encoder.writeDelta8(videoFrameChunk.getOutputStream(), rgb8, (byte[]) previousData, 0, imgWidth, imgWidth, imgHeight); } if (previousData == null) { previousData = new byte[rgb8.length]; } System.arraycopy(rgb8, 0, previousData, 0, rgb8.length); break; } default: { throw new UnsupportedOperationException("RLE only supports 4-bit and 8-bit video."); } } break; } case JPG: { videoFrameChunk = new DataChunk("00dc"); ImageWriter iw = (ImageWriter) ImageIO.getImageWritersByMIMEType("image/jpeg").next(); ImageWriteParam iwParam = iw.getDefaultWriteParam(); iwParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); iwParam.setCompressionQuality(quality); MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(videoFrameChunk.getOutputStream()); iw.setOutput(imgOut); IIOImage img = new IIOImage(image, null, null); iw.write(null, img, iwParam); iw.dispose(); break; } case PNG: default: { videoFrameChunk = new DataChunk("00dc"); ImageWriter iw = (ImageWriter) ImageIO.getImageWritersByMIMEType("image/png").next(); ImageWriteParam iwParam = iw.getDefaultWriteParam(); MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(videoFrameChunk.getOutputStream()); iw.setOutput(imgOut); IIOImage img = new IIOImage(image, null, null); iw.write(null, img, iwParam); iw.dispose(); break; } } long length = getRelativeStreamPosition() - offset; moviChunk.add(videoFrameChunk); videoFrameChunk.finish(); videoFrames.add(new Sample(videoFrameChunk.chunkType, frameRate, offset, length - 8, isSync)); if (getRelativeStreamPosition() > 1L << 32) { throw new IOException("AVI file is larger than 4 GB"); } } /** * Writes a frame from a file to the video track. * <p> * This method does not inspect the contents of the file. * For example, Its 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 close(). * * @param file The file which holds the image data. * * @throws IllegalStateException if the duration is less than 1. * @throws IOException if writing the image failed. */ public void writeFrame(File file) throws IOException { FileInputStream in = null; try { in = new FileInputStream(file); writeFrame(in); } finally { if (in != null) { in.close(); } } } /** * Writes a frame to the video track. * <p> * This method does not inspect the contents of the file. * For example, its 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 close(). * * @param in The input stream which holds the image data. * * @throws IllegalArgumentException if the duration is less than 1. * @throws IOException if writing the image failed. */ public void writeFrame(InputStream in) throws IOException { ensureOpen(); ensureStarted(); DataChunk videoFrameChunk = new DataChunk( videoFormat == AVIVideoFormat.RAW ? "00db" : "00dc"); moviChunk.add(videoFrameChunk); OutputStream mdatOut = videoFrameChunk.getOutputStream(); long offset = getRelativeStreamPosition(); byte[] buf = new byte[512]; int len; while ((len = in.read(buf)) != -1) { mdatOut.write(buf, 0, len); } long length = getRelativeStreamPosition() - offset; videoFrameChunk.finish(); videoFrames.add(new Sample(videoFrameChunk.chunkType, frameRate, offset, length - 8, true)); if (getRelativeStreamPosition() > 1L << 32) { throw new IOException("AVI file is larger than 4 GB"); } } /** * 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 { if (state == States.STARTED) { finish(); } if (state != States.CLOSED) { out.close(); state = States.CLOSED; } } /** * Finishes writing the contents of the AVI 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 != States.FINISHED) { if (imgWidth == -1 || imgHeight == -1) { throw new IllegalStateException("image width and height must be specified"); } moviChunk.finish(); writeEpilog(); state = States.FINISHED; imgWidth = imgHeight = -1; } } /** * Check to make sure that this stream has not been closed */ private void ensureOpen() throws IOException { if (state == States.CLOSED) { throw new IOException("Stream closed"); } } /** Gets the position relative to the beginning of the QuickTime stream. * <p> * Usually this value is equal to the stream position of the underlying * ImageOutputStream, but can be larger if the underlying stream already * contained data. * * @return The relative stream position. * @throws IOException */ private long getRelativeStreamPosition() throws IOException { return out.getStreamPosition() - streamOffset; } /** Seeks relative to the beginning of the QuickTime stream. * <p> * Usually this equal to seeking in the underlying ImageOutputStream, but * can be different if the underlying stream already contained data. * */ private void seekRelative(long newPosition) throws IOException { out.seek(newPosition + streamOffset); } private void writeProlog() throws IOException { // The file has the following structure: // // .RIFF AVI // ..avih (AVI Header Chunk) // ..LIST strl // ...strh (Stream Header Chunk) // ...strf (Stream Format Chunk) // ..LIST movi // ...00dc (Compressed video data chunk in Track 00, repeated for each frame) // ..idx1 (List of video data chunks and their location in the file) // The RIFF AVI Chunk holds the complete movie aviChunk = new CompositeChunk("RIFF", "AVI "); CompositeChunk hdrlChunk = new CompositeChunk("LIST", "hdrl"); // Write empty AVI Main Header Chunk - we fill the data in later aviChunk.add(hdrlChunk); avihChunk = new FixedSizeDataChunk("avih", 56); avihChunk.seekToEndOfChunk(); hdrlChunk.add(avihChunk); CompositeChunk strlChunk = new CompositeChunk("LIST", "strl"); hdrlChunk.add(strlChunk); // Write empty AVI Stream Header Chunk - we fill the data in later strhChunk = new FixedSizeDataChunk("strh", 56); strhChunk.seekToEndOfChunk(); strlChunk.add(strhChunk); strfChunk = new FixedSizeDataChunk("strf", palette == null ? 40 : 40 + palette.getMapSize() * 4); strfChunk.seekToEndOfChunk(); strlChunk.add(strfChunk); moviChunk = new CompositeChunk("LIST", "movi"); aviChunk.add(moviChunk); } private void writeEpilog() throws IOException { // Compute values int duration = 0; for (Sample s : videoFrames) { duration += s.duration; } long bufferSize = 0; for (Sample s : videoFrames) { if (s.length > bufferSize) { bufferSize = s.length; } } DataChunkOutputStream d; /* Create Idx1 Chunk and write data * ------------- typedef struct _avioldindex { FOURCC fcc; DWORD cb; struct _avioldindex_entry { DWORD dwChunkId; DWORD dwFlags; DWORD dwOffset; DWORD dwSize; } aIndex[]; } AVIOLDINDEX; */ DataChunk idx1Chunk = new DataChunk("idx1"); aviChunk.add(idx1Chunk); d = idx1Chunk.getOutputStream(); long moviListOffset = moviChunk.offset + 8; //moviListOffset = 0; for (Sample f : videoFrames) { d.writeType(f.chunkType); // dwChunkId // Specifies a FOURCC that identifies a stream in the AVI file. The // FOURCC must have the form 'xxyy' where xx is the stream number and yy // is a two-character code that identifies the contents of the stream: // // Two-character code Description // db Uncompressed video frame // dc Compressed video frame // pc Palette change // wb Audio data d.writeUInt((f.chunkType.endsWith("pc") ? 0x100 : 0x0)// | (f.isSync ? 0x10 : 0x0)); // dwFlags // Specifies a bitwise combination of zero or more of the following // flags: // // Value Name Description // 0x10 AVIIF_KEYFRAME The data chunk is a key frame. // 0x1 AVIIF_LIST The data chunk is a 'rec ' list. // 0x100 AVIIF_NO_TIME The data chunk does not affect the timing of the // stream. For example, this flag should be set for // palette changes. d.writeUInt(f.offset - moviListOffset); // dwOffset // Specifies the location of the data chunk in the file. The value // should be specified as an offset, in bytes, from the start of the // 'movi' list; however, in some AVI files it is given as an offset from // the start of the file. d.writeUInt(f.length); // dwSize // Specifies the size of the data chunk, in bytes. } idx1Chunk.finish(); /* Write Data into AVI Main Header Chunk * ------------- * The AVIMAINHEADER structure defines global information in an AVI file. * see http://msdn.microsoft.com/en-us/library/ms779632(VS.85).aspx typedef struct _avimainheader { FOURCC fcc; DWORD cb; DWORD dwMicroSecPerFrame; DWORD dwMaxBytesPerSec; DWORD dwPaddingGranularity; DWORD dwFlags; DWORD dwTotalFrames; DWORD dwInitialFrames; DWORD dwStreams; DWORD dwSuggestedBufferSize; DWORD dwWidth; DWORD dwHeight; DWORD dwReserved[4]; } AVIMAINHEADER; */ avihChunk.seekToStartOfData(); d = avihChunk.getOutputStream(); d.writeUInt((1000000L * (long) timeScale) / (long) frameRate); // dwMicroSecPerFrame // Specifies the number of microseconds between frames. // This value indicates the overall timing for the file. d.writeUInt(0); // dwMaxBytesPerSec // Specifies the approximate maximum data rate of the file. // This value indicates the number of bytes per second the system // must handle to present an AVI sequence as specified by the other // parameters contained in the main header and stream header chunks. d.writeUInt(0); // dwPaddingGranularity // Specifies the alignment for data, in bytes. Pad the data to multiples // of this value. d.writeUInt(0x10); // dwFlags (0x10 == hasIndex) // Contains a bitwise combination of zero or more of the following // flags: // // Value Name Description // 0x10 AVIF_HASINDEX Indicates the AVI file has an index. // 0x20 AVIF_MUSTUSEINDEX Indicates that application should use the // index, rather than the physical ordering of the // chunks in the file, to determine the order of // presentation of the data. For example, this flag // could be used to create a list of frames for // editing. // 0x100 AVIF_ISINTERLEAVED Indicates the AVI file is interleaved. // 0x1000 AVIF_WASCAPTUREFILE Indicates the AVI file is a specially // allocated file used for capturing real-time // video. Applications should warn the user before // writing over a file with this flag set because // the user probably defragmented this file. // 0x20000 AVIF_COPYRIGHTED Indicates the AVI file contains copyrighted // data and software. When this flag is used, // software should not permit the data to be // duplicated. d.writeUInt(videoFrames.size()); // dwTotalFrames // Specifies the total number of frames of data in the file. d.writeUInt(0); // dwInitialFrames // Specifies the initial frame for interleaved files. Noninterleaved // files should specify zero. If you are creating interleaved files, // specify the number of frames in the file prior to the initial frame // of the AVI sequence in this member. // To give the audio driver enough audio to work with, the audio data in // an interleaved file must be skewed from the video data. Typically, // the audio data should be moved forward enough frames to allow // approximately 0.75 seconds of audio data to be preloaded. The // dwInitialRecords member should be set to the number of frames the // audio is skewed. Also set the same value for the dwInitialFrames // member of the AVISTREAMHEADER structure in the audio stream header d.writeUInt(1); // dwStreams // Specifies the number of streams in the file. For example, a file with // audio and video has two streams. d.writeUInt(bufferSize); // dwSuggestedBufferSize // Specifies the suggested buffer size for reading the file. Generally, // this size should be large enough to contain the largest chunk in the // file. If set to zero, or if it is too small, the playback software // will have to reallocate memory during playback, which will reduce // performance. For an interleaved file, the buffer size should be large // enough to read an entire record, and not just a chunk. d.writeUInt(imgWidth); // dwWidth // Specifies the width of the AVI file in pixels. d.writeUInt(imgHeight); // dwHeight // Specifies the height of the AVI file in pixels. d.writeUInt(0); // dwReserved[0] d.writeUInt(0); // dwReserved[1] d.writeUInt(0); // dwReserved[2] d.writeUInt(0); // dwReserved[3] // Reserved. Set this array to zero. /* Write Data into AVI Stream Header Chunk * ------------- * The AVISTREAMHEADER structure contains information about one stream * in an AVI file. * see http://msdn.microsoft.com/en-us/library/ms779638(VS.85).aspx typedef struct _avistreamheader { FOURCC fcc; DWORD cb; FOURCC fccType; FOURCC fccHandler; DWORD dwFlags; WORD wPriority; WORD wLanguage; DWORD dwInitialFrames; DWORD dwScale; DWORD dwRate; DWORD dwStart; DWORD dwLength; DWORD dwSuggestedBufferSize; DWORD dwQuality; DWORD dwSampleSize; struct { short int left; short int top; short int right; short int bottom; } rcFrame; } AVISTREAMHEADER; */ strhChunk.seekToStartOfData(); d = strhChunk.getOutputStream(); d.writeType("vids"); // fccType - vids for video stream // Contains a FOURCC that specifies the type of the data contained in // the stream. The following standard AVI values for video and audio are // defined: // // FOURCC Description // 'auds' Audio stream // 'mids' MIDI stream // 'txts' Text stream // 'vids' Video stream switch (videoFormat) { case RAW: d.writeType("DIB "); // fccHandler - DIB for Raw RGB break; case RLE: d.writeType("RLE "); // fccHandler - Microsoft RLE break; case JPG: d.writeType("MJPG"); // fccHandler - MJPG for Motion JPEG break; case PNG: default: d.writeType("png "); // fccHandler - png for PNG break; } // Optionally, contains a FOURCC that identifies a specific data // handler. The data handler is the preferred handler for the stream. // For audio and video streams, this specifies the codec for decoding // the stream. if (imgDepth <= 8) { d.writeUInt(0x00010000); // dwFlags - AVISF_VIDEO_PALCHANGES } else { d.writeUInt(0); // dwFlags } // Contains any flags for the data stream. The bits in the high-order // word of these flags are specific to the type of data contained in the // stream. The following standard flags are defined: // // Value Name Description // AVISF_DISABLED 0x00000001 Indicates this stream should not // be enabled by default. // AVISF_VIDEO_PALCHANGES 0x00010000 // Indicates this video stream contains // palette changes. This flag warns the playback // software that it will need to animate the // palette. d.writeUShort(0); // wPriority // Specifies priority of a stream type. For example, in a file with // multiple audio streams, the one with the highest priority might be // the default stream. d.writeUShort(0); // wLanguage // Language tag. d.writeUInt(0); // dwInitialFrames // Specifies how far audio data is skewed ahead of the video frames in // interleaved files. Typically, this is about 0.75 seconds. If you are // creating interleaved files, specify the number of frames in the file // prior to the initial frame of the AVI sequence in this member. For // more information, see the remarks for the dwInitialFrames member of // the AVIMAINHEADER structure. d.writeUInt(timeScale); // dwScale // Used with dwRate to specify the time scale that this stream will use. // Dividing dwRate by dwScale gives the number of samples per second. // For video streams, this is the frame rate. For audio streams, this // rate corresponds to the time needed to play nBlockAlign bytes of // audio, which for PCM audio is the just the sample rate. d.writeUInt(frameRate); // dwRate // See dwScale. d.writeUInt(0); // dwStart // Specifies the starting time for this stream. The units are defined by // the dwRate and dwScale members in the main file header. Usually, this // is zero, but it can specify a delay time for a stream that does not // start concurrently with the file. d.writeUInt(videoFrames.size()); // dwLength // Specifies the length of this stream. The units are defined by the // dwRate and dwScale members of the stream's header. d.writeUInt(bufferSize); // dwSuggestedBufferSize // Specifies how large a buffer should be used to read this stream. // Typically, this contains a value corresponding to the largest chunk // present in the stream. Using the correct buffer size makes playback // more efficient. Use zero if you do not know the correct buffer size. d.writeInt(-1); // dwQuality // Specifies an indicator of the quality of the data in the stream. // Quality is represented as a number between 0 and 10,000. // For compressed data, this typically represents the value of the // quality parameter passed to the compression software. If set to –1, // drivers use the default quality value. d.writeUInt(0); // dwSampleSize // Specifies the size of a single sample of data. This is set to zero // if the samples can vary in size. If this number is nonzero, then // multiple samples of data can be grouped into a single chunk within // the file. If it is zero, each sample of data (such as a video frame) // must be in a separate chunk. For video streams, this number is // typically zero, although it can be nonzero if all video frames are // the same size. For audio streams, this number should be the same as // the nBlockAlign member of the WAVEFORMATEX structure describing the // audio. d.writeUShort(0); // rcFrame.left d.writeUShort(0); // rcFrame.top d.writeUShort(imgWidth); // rcFrame.right d.writeUShort(imgHeight); // rcFrame.bottom // Specifies the destination rectangle for a text or video stream within // the movie rectangle specified by the dwWidth and dwHeight members of // the AVI main header structure. The rcFrame member is typically used // in support of multiple video streams. Set this rectangle to the // coordinates corresponding to the movie rectangle to update the whole // movie rectangle. Units for this member are pixels. The upper-left // corner of the destination rectangle is relative to the upper-left // corner of the movie rectangle. /* Write BITMAPINFOHEADR Data into AVI Stream Format Chunk /* ------------- * see http://msdn.microsoft.com/en-us/library/ms779712(VS.85).aspx typedef struct tagBITMAPINFOHEADER { DWORD biSize; LONG biWidth; LONG biHeight; WORD biPlanes; WORD biBitCount; DWORD biCompression; DWORD biSizeImage; LONG biXPelsPerMeter; LONG biYPelsPerMeter; DWORD biClrUsed; DWORD biClrImportant; } BITMAPINFOHEADER; */ strfChunk.seekToStartOfData(); d = strfChunk.getOutputStream(); d.writeUInt(40); // biSize // Specifies the number of bytes required by the structure. This value // does not include the size of the color table or the size of the color // masks, if they are appended to the end of structure. d.writeInt(imgWidth); // biWidth // Specifies the width of the bitmap, in pixels. d.writeInt(imgHeight); // biHeight // Specifies the height of the bitmap, in pixels. // // For uncompressed RGB bitmaps, if biHeight is positive, the bitmap is // a bottom-up DIB with the origin at the lower left corner. If biHeight // is negative, the bitmap is a top-down DIB with the origin at the // upper left corner. // For YUV bitmaps, the bitmap is always top-down, regardless of the // sign of biHeight. Decoders should offer YUV formats with postive // biHeight, but for backward compatibility they should accept YUV // formats with either positive or negative biHeight. // For compressed formats, biHeight must be positive, regardless of // image orientation. d.writeShort(1); // biPlanes // Specifies the number of planes for the target device. This value must // be set to 1. d.writeShort(imgDepth); // biBitCount // Specifies the number of bits per pixel (bpp). For uncompressed // formats, this value is the average number of bits per pixel. For // compressed formats, this value is the implied bit depth of the // uncompressed image, after the image has been decoded. switch (videoFormat) { case RAW: default: d.writeInt(0); // biCompression - BI_RGB for uncompressed RGB break; case RLE: if (imgDepth == 8) { d.writeInt(1); // biCompression - BI_RLE8 } else if (imgDepth == 4) { d.writeInt(2); // biCompression - BI_RLE4 } else { throw new UnsupportedOperationException("RLE only supports 4-bit and 8-bit images"); } break; case JPG: d.writeType("MJPG"); // biCompression - MJPG for Motion JPEG break; case PNG: d.writeType("png "); // biCompression - png for PNG break; } // For compressed video and YUV formats, this member is a FOURCC code, // specified as a DWORD in little-endian order. For example, YUYV video // has the FOURCC 'VYUY' or 0x56595559. For more information, see FOURCC // Codes. // // For uncompressed RGB formats, the following values are possible: // // Value Description // BI_RGB 0x00000000 Uncompressed RGB. // BI_BITFIELDS 0x00000003 Uncompressed RGB with color masks. // Valid for 16-bpp and 32-bpp bitmaps. // // Note that BI_JPG and BI_PNG are not valid video formats. // // For 16-bpp bitmaps, if biCompression equals BI_RGB, the format is // always RGB 555. If biCompression equals BI_BITFIELDS, the format is // either RGB 555 or RGB 565. Use the subtype GUID in the AM_MEDIA_TYPE // structure to determine the specific RGB type. switch (videoFormat) { case RAW: d.writeInt(0); // biSizeImage break; case RLE: case JPG: case PNG: default: if (imgDepth == 4) { d.writeInt(imgWidth * imgHeight / 2); // biSizeImage } else { int bytesPerPixel = Math.max(1, imgDepth / 8); d.writeInt(imgWidth * imgHeight * bytesPerPixel); // biSizeImage } break; } // Specifies the size, in bytes, of the image. This can be set to 0 for // uncompressed RGB bitmaps. d.writeInt(0); // biXPelsPerMeter // Specifies the horizontal resolution, in pixels per meter, of the // target device for the bitmap. d.writeInt(0); // biYPelsPerMeter // Specifies the vertical resolution, in pixels per meter, of the target // device for the bitmap. d.writeInt(palette == null ? 0 : palette.getMapSize()); // biClrUsed // Specifies the number of color indices in the color table that are // actually used by the bitmap. d.writeInt(0); // biClrImportant // Specifies the number of color indices that are considered important // for displaying the bitmap. If this value is zero, all colors are // important. if (palette != null) { for (int i = 0, n = palette.getMapSize(); i < n; ++i) { /* * typedef struct tagRGBQUAD { BYTE rgbBlue; BYTE rgbGreen; BYTE rgbRed; BYTE rgbReserved; // This member is reserved and must be zero. } RGBQUAD; */ d.write(palette.getBlue(i)); d.write(palette.getGreen(i)); d.write(palette.getRed(i)); d.write(0); } } // ----------------- aviChunk.finish(); } }