/* * jPSXdec: Playstation 1 Media Decoder/Converter in Java * Copyright (C) 2007-2008 Michael Sabin * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ /* * Modified by Masayuki Igawa * Original source code is available at * http://code.google.com/p/jpsxdec/source/browse/#svn/trunk/src/jpsxdec/util */ /* * AviWriter.java */ package org.orzlabs.java.media; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import javax.imageio.ImageIO; import java.io.IOException; import java.io.RandomAccessFile; import java.io.File; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Iterator; import javax.imageio.IIOImage; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.MemoryCacheImageOutputStream; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import org.orzlabs.java.media.AVIOLDINDEX.AVIOLDINDEXENTRY; /** * AVI encoder to write uncompressed, RGB DIB video, or compressed MJPG video, * along with uncompressed PCM audio. The resulting MJPG AVI seems playable on * vanilla Windows XP systems, and of course VLC. * <p> * This code is originally based on (but now hardly resembles) the ImageJ * package at http://rsb.info.nih.gov/ij * <blockquote> * ImageJ is being developed at the National Institutes of Health by an * employee of the Federal Government in the course of his official duties. * Pursuant to Title 17, Section 105 of the United States Code, this software * is not subject to copyright protection and is in the public domain. * ImageJ is an experimental system. NIH assumes no responsibility whatsoever * for its use by other parties, and makes no guarantees, expressed or implied, * about its quality, reliability, or any other characteristic. * </blockquote> * The ImageJ AviWriter class was based on the FileAvi class written by * William Gandler. That FileAvi class is part of Matthew J. McAuliffe's * MIPAV program, available from http://mipav.cit.nih.gov/, which also * appears to be in the public domain. * <p> * I owe my MJPG understanding to the jpegtoavi program. * http://sourceforge.net/projects/jpegtoavi/ * <p> * Random list of codecs * http://www.oltenia.ro/download/pub/windows/media/video/tools/GSpot/gspot22/GSpot22.dat * <p> * http://www.alexander-noe.com/video/documentation/avi.pdf * <p> * Works with Java 1.5 or higher. */ public class AviWriter { /** Hold's true if system can write "jpeg" images. */ private final static boolean CAN_ENCODE_JPEG; static { // check if the system can write "jpeg" images boolean bln = false; for (String s : ImageIO.getReaderFormatNames()) { if (s.equals("jpeg")) { bln = true; break; } } CAN_ENCODE_JPEG = bln; } // ------------------------------------------------------------------------- // -- Fields --------------------------------------------------------------- // ------------------------------------------------------------------------- /** Width of the frame in pixels. */ private int m_iWidth = -1; /** Height of the frame in pixels. */ private int m_iHeight = -1; /** Numerator of the frames/second fraction. */ private int m_iFrames = -1; /** Denominator of the frames/second fraction. */ private int m_iPerSecond = -1; /** Size of the frame data in bytes. Only applicable to DIB AVI. * Each DIB frame submitted is compared to this value to ensure * proper data. */ private int m_iFrameByteSize = -1; /** Number of frames written. */ private int m_iFrameCount = 0; /** Number of audio channels. 0 for no audio, 1 for mono, 2 for stereo. */ private int m_iChannels; /** Number of bytes per sample. */ private final int m_iBytesPerSample = 2; /** Sample rate of the audio. */ private int m_iSamplesPerSecond = -1; /** Number of audio samples submitted. */ private double m_dblSampleCount = 0; /** The image writer used to convert the BufferedImages to BMP or JPEG. */ private final ImageWriter m_oImgWriter; /** Only used for MJPG when not using default quality level. */ private final ImageWriteParam m_oWriteParams; /** True if writing avi with MJPG codec, false if writing DIB codec. */ private final boolean m_blnMJPG; // ------------------------------------------------------------------------- // -- Properties ----------------------------------------------------------- // ------------------------------------------------------------------------- public void setSamplesPerSecond(int i) { if (i < 1 ) throw new IllegalArgumentException("Samples/Second must be greater than 0"); m_iSamplesPerSecond = i; } public int getSamplesPerSecond() { return m_iSamplesPerSecond; } public void setFramesPerSecond(int i, int j) { if (i < 1 || j < 1) throw new IllegalArgumentException("frames/sec must be greater than 0"); m_iFrames = i; m_iPerSecond = j; } public int getFramesPerSecNum() { return m_iFrames; } public int getFramesperSecDenom() { return m_iPerSecond; } public void setDimensions(int i, int j) { if (i < 1 || j < 1) throw new IllegalArgumentException("Dimensions must be greater than 0"); if (m_iWidth < 0) m_iWidth = i; else throw new IllegalArgumentException("Width has already been set."); if (m_iHeight < 0) m_iHeight = j; else throw new IllegalArgumentException("Height has already been set."); } public int getWidth() { return m_iWidth; } public int getHeight() { return m_iHeight; } // ------------------------------------------------------------------------- // -- AVI Structure Fields ------------------------------------------------- // ------------------------------------------------------------------------- private RandomAccessFile raFile; private Chunk RIFF_chunk; private Chunk LIST_hdr1; private AVIMAINHEADER avih; private Chunk LIST_strl_vid; private Chunk strf_vid; private AVISTREAMHEADER strh_vid; private BITMAPINFOHEADER bif; //strf_vid //LIST_strl_vid private Chunk LIST_strl_aud; private Chunk strf_aud; private AVISTREAMHEADER strh_aud; private WAVEFORMATEX wavfmt; //strf_aud //LIST_strl_aud //LIST_hdr1 private Chunk LIST_movi; /* image and audio chunk data */ //LIST_movi private AVIOLDINDEX avioldidx; //RIFF_chunk /** Holds the 'idx' section index data. */ private ArrayList<AVIOLDINDEXENTRY> indexList; // ------------------------------------------------------------------------- // -- Constructors --------------------------------------------------------- // ------------------------------------------------------------------------- /** Write uncompressed RGB device-independent bitmap (DIB) frames. */ public AviWriter(final File oOutputfile, final int iAudChannels) throws IOException { this(oOutputfile, iAudChannels, false); } /** Write MJPG encoded frames with default quality, * or uncompressed RGB device-independent bitmap (DIB) frames. */ public AviWriter(final File oOutputfile, final int iAudChannels, final boolean blnEncodeMjpg) throws IOException { Iterator oIter; if (blnEncodeMjpg) { if (!CAN_ENCODE_JPEG) throw new UnsupportedOperationException("Unable to encode 'jpeg' on this platform."); oIter = ImageIO.getImageWritersByFormatName("jpeg"); } else { oIter = ImageIO.getImageWritersByFormatName("bmp"); } m_oImgWriter = (ImageWriter)oIter.next(); m_blnMJPG = blnEncodeMjpg; m_oWriteParams = null; InitAVIWriter(oOutputfile, iAudChannels); } /** Write MJPG encoded frames with specified quality. * @param fltMjpgQuality quality from 0 (lowest) to 1 (highest). */ public AviWriter(final File oOutputfile, final int iAudChannels, final float fltMjpgQuality) throws IOException { if (!CAN_ENCODE_JPEG) throw new UnsupportedOperationException("Unable to encode 'jpeg' on this platform."); Iterator oIter = ImageIO.getImageWritersByFormatName("jpeg"); m_oImgWriter = (ImageWriter)oIter.next(); m_oWriteParams = m_oImgWriter.getDefaultWriteParam(); m_oWriteParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); m_oWriteParams.setCompressionQuality(fltMjpgQuality); // 0 for lowest qulaity, 1 for highest m_blnMJPG = true; InitAVIWriter(oOutputfile, iAudChannels); } // ------------------------------------------------------------------------- /** Opens and prepares an AVI file for writing video/audio data. */ private void InitAVIWriter(final File oOutputfile, final int iAudChannels) throws IOException { if (iAudChannels < 0 || iAudChannels > 2) throw new IllegalArgumentException("Channels must be 0, 1 or 2"); m_iChannels = iAudChannels; raFile = new RandomAccessFile(oOutputfile, "rw"); raFile.setLength(0); // trim the file to 0 //---------------------------------------------------------------------- // Setup the header structure. // Actual values will be filled in when avi is closed. RIFF_chunk = new Chunk(raFile, "RIFF", "AVI "); LIST_hdr1 = new Chunk(raFile, "LIST", "hdrl"); avih = new AVIMAINHEADER(); avih.makePlaceholder(raFile); LIST_strl_vid = new Chunk(raFile, "LIST", "strl"); strh_vid = new AVISTREAMHEADER(); strh_vid.makePlaceholder(raFile); strf_vid = new Chunk(raFile, "strf"); bif = new BITMAPINFOHEADER(); bif.makePlaceholder(raFile); strf_vid.endChunk(raFile); LIST_strl_vid.endChunk(raFile); if (m_iChannels > 0) { // if there is audio LIST_strl_aud = new Chunk(raFile, "LIST", "strl"); strh_aud = new AVISTREAMHEADER(); strh_aud.makePlaceholder(raFile); strf_aud = new Chunk(raFile, "strf"); wavfmt = new WAVEFORMATEX(); wavfmt.makePlaceholder(raFile); strf_aud.endChunk(raFile); LIST_strl_aud.endChunk(raFile); } LIST_hdr1.endChunk(raFile); LIST_movi = new Chunk(raFile, "LIST", "movi"); // now we're ready to start accepting video/audio data // generate an index as we write 'movi' section indexList = new ArrayList<AVIOLDINDEXENTRY>(); } // ------------------------------------------------------------------------- // -- Public functions ----------------------------------------------------- // ------------------------------------------------------------------------- /** Assumes width/height is correct. * abData should either be DIB data * (BGR rows inverted and widths padded to 4 byte boundaries), * or JPEG data with the 'JFIF' header text changed to 'AVI1'. */ public void writeFrame(byte[] abData) throws IOException { if (raFile == null) throw new IOException("Avi file is closed"); // only keep track of frame data size if it's DIB. They should all be the same if (!m_blnMJPG) { if (m_iFrameByteSize < 0) m_iFrameByteSize = abData.length; else if (m_iFrameByteSize != abData.length) throw new IllegalArgumentException("Frame data size is not consistent"); } writeStreamDataChunk(abData, true); } /** Converts a BufferedImage to proper avi format and writes it. */ public void writeFrame(BufferedImage bi) throws IOException { if (raFile == null) throw new IOException("Avi file is closed"); if (m_iWidth < 0) m_iWidth = bi.getWidth(); else if (m_iWidth != bi.getWidth()) throw new IllegalArgumentException("AviWriter: Frame width is inconsistent" + " (was " + m_iWidth + ", now " + bi.getWidth() + ")."); if (m_iHeight < 0) m_iHeight = bi.getHeight(); else if (m_iHeight != bi.getHeight()) throw new IllegalArgumentException("AviWriter: Frame height is inconsistent" + " (was " + m_iHeight + ", now " + bi.getHeight() + ")."); if (m_blnMJPG) { writeFrame(Image2MJPEG(bi)); } else { writeFrame(Image2DIB(bi, m_iFrameByteSize)); } } public void writeAudio(byte[] abData) throws IOException { if (raFile == null) throw new IOException("Avi file is closed"); writeStreamDataChunk(abData, false); } public void writeAudio(AudioInputStream oData) throws IOException { if (raFile == null) throw new IOException("Avi file is closed"); AudioFormat fmt = oData.getFormat(); if (m_iBytesPerSample < 0) {}//m_iBytesPerSample = fmt.getFrameSize(); else if (fmt.getSampleSizeInBits() != m_iBytesPerSample * 8) throw new IOException("The bytes per sample don't match."); if (m_iChannels != fmt.getChannels()) throw new IOException("The number of audio channels don't match."); if (m_iSamplesPerSecond < 0) m_iSamplesPerSecond = (int)fmt.getSampleRate(); else if (m_iSamplesPerSecond != (int)fmt.getSampleRate()) throw new IOException("The sample rate doesn't match."); Chunk data_size; AVIOLDINDEXENTRY idxentry = new AVIOLDINDEXENTRY(); idxentry.dwOffset = (int)(raFile.getFilePointer() - (LIST_movi.getStart() + 4)); idxentry.dwChunkId = AVIstruct.string2int("01wb"); idxentry.dwFlags = 0; data_size = new Chunk(raFile, "01wb"); // write the data, padded to 4 byte boundary byte[] b = new byte[1024]; int i; while ((i = oData.read(b)) > 0) { m_dblSampleCount += (double)i / m_iBytesPerSample / m_iChannels; raFile.write(b, 0, i); } int remaint = (int)(4 - ( (raFile.getFilePointer() - (data_size.getStart()+4) ) % 4)) % 4; while (remaint > 0) { raFile.write(0); remaint--; } // end the chunk data_size.endChunk(raFile); // add this item to the index idxentry.dwSize = (int)data_size.getSize(); indexList.add(idxentry); } private void writeStreamDataChunk(byte[] abData, boolean blnIsVideo) throws IOException { Chunk data_size; AVIOLDINDEXENTRY idxentry = new AVIOLDINDEXENTRY(); idxentry.dwOffset = (int)(raFile.getFilePointer() - (LIST_movi.getStart() + 4)); if (blnIsVideo) { // if video String sChunkId; if (m_blnMJPG) sChunkId = "00dc"; // dc for compressed frame else sChunkId = "00db"; // db for uncompressed frame idxentry.dwChunkId = AVIstruct.string2int(sChunkId); idxentry.dwFlags = AVIOLDINDEX.AVIIF_KEYFRAME; // Write the flags - select AVIIF_KEYFRAME // AVIIF_KEYFRAME 0x00000010L // The flag indicates key frames in the video sequence. m_iFrameCount++; data_size = new Chunk(raFile, sChunkId); } else { // if audio // TODO: Maybe have better handling if half a sample is provided if (abData.length % m_iBytesPerSample != 0 || abData.length % m_iChannels != 0) throw new IllegalArgumentException("Half an audio sample can't be processed."); idxentry.dwChunkId = AVIstruct.string2int("01wb"); idxentry.dwFlags = 0; m_dblSampleCount += (double)abData.length / m_iBytesPerSample / m_iChannels; data_size = new Chunk(raFile, "01wb"); } // write the data, padded to 4 byte boundary int remaint = (4 - (abData.length % 4)) % 4; raFile.write(abData); while (remaint > 0) { raFile.write(0); remaint--; } // end the chunk data_size.endChunk(raFile); // add the index to the list idxentry.dwSize = (int)data_size.getSize(); indexList.add(idxentry); } public void close() throws IOException { if (raFile == null) throw new IOException("Avi file is closed"); //////////////////////////////////////////////////////////////////////// if (m_iFrames < 1 || m_iPerSecond < 1) throw new IllegalStateException("Must set frames/second before closing avi"); if (m_iChannels > 0 && m_iSamplesPerSecond < 1) throw new IllegalStateException("Must set samples/second before closing avi"); if (m_iWidth < 0 || m_iHeight < 0) throw new IllegalStateException("Must set dimentions before closing avi"); //////////////////////////////////////////////////////////////////////// LIST_movi.endChunk(raFile); // write idx avioldidx = new AVIOLDINDEX(indexList.toArray(new AVIOLDINDEXENTRY[0])); avioldidx.write(raFile); // /write idx RIFF_chunk.endChunk(raFile); //###################################################################### //## Fill the headers fields ########################################### //###################################################################### //avih.fcc = 'avih'; // the avih sub-CHUNK //avih.cb = 0x38; // the length of the avih sub-CHUNK (38H) not including the // the first 8 bytes for avihSignature and the length avih.dwMicroSecPerFrame = (int)((m_iPerSecond/(double)m_iFrames)*1.0e6); avih.dwMaxBytesPerSec = 0; // (maximum data rate of the file in bytes per second) avih.dwPaddingGranularity = 0; avih.dwFlags = AVIMAINHEADER.AVIF_HASINDEX | AVIMAINHEADER.AVIF_ISINTERLEAVED; // just set the bit for AVIF_HASINDEX // 10H AVIF_HASINDEX: The AVI file has an idx1 chunk containing // an index at the end of the file. For good performance, all // AVI files should contain an index. avih.dwTotalFrames = m_iFrameCount; // total frame number avih.dwInitialFrames = 0; // Initial frame for interleaved files. // Noninterleaved files should specify 0. if (m_iChannels > 0) avih.dwStreams = 2; // number of streams in the file - here 1 video and zero audio. else avih.dwStreams = 1; // number of streams in the file - here 1 video and zero audio. avih.dwSuggestedBufferSize = 0; // Suggested buffer size for reading the file. // Generally, this size should be large enough to contain the largest // chunk in the file. // dwSuggestedBufferSize - Suggested buffer size for reading the file. avih.dwWidth = m_iWidth; // image width in pixels avih.dwHeight = m_iHeight; // image height in pixels //avih.dwReserved1 = 0; // Microsoft says to set the following 4 values to 0. //avih.dwReserved2 = 0; // //avih.dwReserved3 = 0; // //avih.dwReserved4 = 0; // //###################################################################### // AVISTREAMHEADER for video //strh_vid.fcc = 'strh'; // strh sub-CHUNK //strh_vid.cb = 56; // the length of the strh sub-CHUNK strh_vid.fccType = AVIstruct.string2int("vids"); // the type of data stream - here vids for video stream // Write DIB for Microsoft Device Independent Bitmap. Note: Unfortunately, // at least 3 other four character codes are sometimes used for uncompressed // AVI videos: 'RGB ', 'RAW ', 0x00000000 if (m_blnMJPG) strh_vid.fccHandler = AVIstruct.string2int("MJPG"); else strh_vid.fccHandler = AVIstruct.string2int("DIB "); strh_vid.dwFlags = 0; strh_vid.wPriority = 0; strh_vid.wLanguage = 0; strh_vid.dwInitialFrames = 0; strh_vid.dwScale = m_iPerSecond; strh_vid.dwRate = m_iFrames; // frame rate for video streams strh_vid.dwStart = 0; // this field is usually set to zero strh_vid.dwLength = m_iFrameCount; // playing time of AVI file as defined by scale and rate // Set equal to the number of frames // TODO: Add a sugested buffer size strh_vid.dwSuggestedBufferSize = 0; // Suggested buffer size for reading the stream. // Typically, this contains a value corresponding to the largest chunk // in a stream. strh_vid.dwQuality = -1; // encoding quality given by an integer between // 0 and 10,000. If set to -1, drivers use the default // quality value. strh_vid.dwSampleSize = 0; strh_vid.left = 0; strh_vid.top = 0; strh_vid.right = (short)m_iWidth; // virtualdub uses width strh_vid.bottom = (short)m_iHeight; // virtualdub uses height //###################################################################### // BITMAPINFOHEADER //bif.biSize = 40; // Write header size of BITMAPINFO header structure // Applications should use this size to determine which BITMAPINFO header structure is // being used. This size includes this biSize field. bif.biWidth = m_iWidth; // BITMAP width in pixels bif.biHeight = m_iHeight; // image height in pixels. If height is positive, // the bitmap is a bottom up DIB and its origin is in the lower left corner. If // height is negative, the bitmap is a top-down DIB and its origin is the upper // left corner. This negative sign feature is supported by the Windows Media Player, but it is not // supported by PowerPoint. //bif.biPlanes = 1; // biPlanes - number of color planes in which the data is stored // This must be set to 1. bif.biBitCount = 24; // biBitCount - number of bits per pixel # if (m_blnMJPG) // 0L for BI_RGB, uncompressed data as bitmap bif.biCompression = AVIstruct.string2int("MJPG"); else // type of compression used bif.biCompression = BITMAPINFOHEADER.BI_RGB; bif.biSizeImage = 0; bif.biXPelsPerMeter = 0; // horizontal resolution in pixels bif.biYPelsPerMeter = 0; // vertical resolution in pixels // per meter bif.biClrUsed = 0; // bif.biClrImportant = 0; // biClrImportant - specifies that the first x colors of the color table // are important to the DIB. If the rest of the colors are not available, // the image still retains its meaning in an acceptable manner. When this // field is set to zero, all the colors are important, or, rather, their // relative importance has not been computed. //###################################################################### // AVISTREAMHEADER for audio if (m_iChannels > 0) { //strh.fcc = 'strh'; // strh sub-CHUNK //strh.cb = 56; // length of the strh sub-CHUNK strh_aud.fccType = AVIstruct.string2int("auds"); // Write the type of data stream - here auds for audio stream strh_aud.fccHandler = 0; // no fccHandler for wav strh_aud.dwFlags = 0; strh_aud.wPriority = 0; strh_aud.wLanguage = 0; strh_aud.dwInitialFrames = 1; // virtualdub uses 1 strh_aud.dwScale = 1; strh_aud.dwRate = m_iSamplesPerSecond; // sample rate for audio streams strh_aud.dwStart = 0; // this field is usually set to zero // FIXME: for some reason virtualdub has a different dwLength value strh_aud.dwLength = (int)m_dblSampleCount; // playing time of AVI file as defined by scale and rate // Set equal to the number of audio samples in file? // TODO: Add suggested audio buffer size strh_aud.dwSuggestedBufferSize = 0; // Suggested buffer size for reading the stream. // Typically, this contains a value corresponding to the largest chunk // in a stream. strh_aud.dwQuality = -1; // encoding quality given by an integer between // 0 and 10,000. If set to -1, drivers use the default // quality value. strh_aud.dwSampleSize = m_iBytesPerSample * m_iChannels; strh_aud.left = 0; strh_aud.top = 0; strh_aud.right = 0; strh_aud.bottom = 0; //###################################################################### // WAVEFORMATEX wavfmt.wFormatTag = WAVEFORMATEX.WAVE_FORMAT_PCM; wavfmt.nChannels = (short)m_iChannels; wavfmt.nSamplesPerSec = m_iSamplesPerSecond; wavfmt.nAvgBytesPerSec = m_iBytesPerSample * m_iSamplesPerSecond * m_iChannels; wavfmt.nBlockAlign = (short)(m_iBytesPerSample * m_iChannels); wavfmt.wBitsPerSample = m_iBytesPerSample * 8; //wavfmt.cbSize = 0; // not written } //###################################################################### //###################################################################### //###################################################################### // go back and write the headers avih.goBackAndWrite(raFile); strh_vid.goBackAndWrite(raFile); bif.goBackAndWrite(raFile); if (m_iChannels > 0) { strh_aud.goBackAndWrite(raFile); wavfmt.goBackAndWrite(raFile); } // and we're done raFile.close(); raFile = null; RIFF_chunk = null; LIST_hdr1 = null; avih = null; LIST_strl_vid = null; strf_vid = null; strh_vid = null; bif = null; LIST_strl_aud = null; strf_aud = null; strh_aud = null; wavfmt = null; LIST_movi = null; avioldidx = null; } // ------------------------------------------------------------------------- // -- Private functions ---------------------------------------------------- // ------------------------------------------------------------------------- private final static void writeString(RandomAccessFile raFile, String s) throws IOException { byte[] bytes = s.getBytes("UTF8"); raFile.write(bytes); } private final static void write32LE(RandomAccessFile raFile, int v) throws IOException { raFile.write(v & 0xFF); raFile.write((v >>> 8) & 0xFF); raFile.write((v >>> 16) & 0xFF); raFile.write((v >>> 24) & 0xFF); } //////////////////////////////////////////////////////////////////////////// private byte[] Image2DIB(BufferedImage bmp, int iSize) throws IOException { // first make sure this is a 24 bit RGB image ColorModel cm = bmp.getColorModel(); if (bmp.getType() != BufferedImage.TYPE_3BYTE_BGR) { // if not, convert it BufferedImage buffer = new BufferedImage( bmp.getWidth(), bmp.getHeight(), BufferedImage.TYPE_3BYTE_BGR); Graphics2D g = buffer.createGraphics(); g.drawImage(bmp, 0, 0, null); g.dispose(); bmp = buffer; } byte[] abDIB; // use a ByteArrayOutputStream with the same // initial size as the last frame (saves time and memory re-allocation) // TODO: Use a ByteArrayOutputStream sub-class to expose the internal buffer so a double-copy isn't necessary below if (iSize <= 32) abDIB = WriteImageToBytes(bmp, new ByteArrayOutputStream()); else abDIB = WriteImageToBytes(bmp, new ByteArrayOutputStream(iSize + 54)); // get the 'bfOffBits' value, which says where the // image data actually starts (should be 54) int iDataStart = read32LE(abDIB, 10); // return the data from that byte onward byte[] abDIBcpy = new byte[abDIB.length - iDataStart]; System.arraycopy(abDIB, iDataStart, abDIBcpy, 0, abDIBcpy.length); return abDIBcpy; } /** Read a 32 little-endian value from a position in an array. */ private static int read32LE(byte[] ab, int iPos) { return (ab[iPos+0]) | (ab[iPos+1] << 8 ) | (ab[iPos+2] << 16) | (ab[iPos+3] << 24); } /** Converts a BufferedImage into a frame to be written into a MJPG avi. */ private byte[] Image2MJPEG(BufferedImage img) throws IOException { byte[] abJpg = WriteImageToBytes(img, new ByteArrayOutputStream()); //IO.writeFile("test.bin", abJpg); // debug JPEG2MJPEG(abJpg); return abJpg; } private byte[] WriteImageToBytes(BufferedImage img, ByteArrayOutputStream oOut) throws IOException { // wrap the ByteArrayOutputStream with a MemoryCacheImageOutputStream MemoryCacheImageOutputStream oMemOut = new MemoryCacheImageOutputStream(oOut); // set our image writer's output stream m_oImgWriter.setOutput(oMemOut); // wrap the BufferedImage with a IIOImage IIOImage oImgIO = new IIOImage(img, null, null); // finally write the buffered image to the output stream // using our parameters (if any) m_oImgWriter.write(null, oImgIO, m_oWriteParams); // don't forget to flush oMemOut.flush(); oMemOut.close(); // clear image writer's output stream m_oImgWriter.setOutput(null); // return the result return oOut.toByteArray(); } /** Converts JPEG file data to be used in an MJPG AVI. */ private static void JPEG2MJPEG(byte [] ab) throws IOException { if (ab[6] != 'J' || ab[7] != 'F' || ab[8] != 'I' || ab[9] != 'F') throw new IOException("JFIF header not found in jpeg data, unable to write frame to AVI."); // http://cekirdek.pardus.org.tr/~ismail/ffmpeg-docs/mjpegdec_8c-source.html#l00869 // ffmpeg treats the JFIF and AVI1 header differently. It's probably // safer to stick with standard JFIF header since that's what JPEG uses. /* ab[6] = 'A'; ab[7] = 'V'; ab[8] = 'I'; ab[9] = '1'; */ } /** Represents an AVI 'chunk'. When created, it saves the current * position in the AVI RandomAccessFile. When endChunk() is called, * it temporarily jumps back to the start of the chunk and records how * many bytes have been written. */ private static class Chunk { final private long m_lngPos; private long m_lngSize = -1; Chunk(RandomAccessFile oRAF, String sChunkName) throws IOException { writeString(oRAF, sChunkName); m_lngPos = oRAF.getFilePointer(); oRAF.writeInt(0); } Chunk(RandomAccessFile oRAF, String sChunkName, String sSubChunkName) throws IOException { this(oRAF, sChunkName); writeString(oRAF, sSubChunkName); } /** Jumps back to saved position in the RandomAccessFile and writes * how many bytes have passed since the position was saved, then * returns to the current position again. */ public void endChunk(RandomAccessFile oRAF) throws IOException { long lngCurPos = oRAF.getFilePointer(); // save this pos oRAF.seek(m_lngPos); // go back to where the header is m_lngSize = (lngCurPos - (m_lngPos + 4)); // save number of bytes since start of chunk write32LE(oRAF, (int)m_lngSize); // write the header size oRAF.seek(lngCurPos); // return to current position } /** After endChunk() has been called, returns the size that was * written. */ private long getSize() { return m_lngSize; } /** Returns the position where the size will be written when * endChunk() is called. */ private long getStart() { return m_lngPos; } } }