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

org.monte.media.avi.AVIWriter Maven / Gradle / Ivy

There is a newer version: 1.1
Show newest version
/**
 * @(#)AVIWriter.java
 *
 * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. All rights
 * reserved.
 *
 * You may not use, copy or modify this file, except in compliance onlyWith the
 * license agreement you entered into onlyWith Werner Randelshofer. For details
 * see accompanying license terms.
 */
package org.monte.media.avi;

import java.util.EnumSet;
import org.monte.media.math.Rational;
import org.monte.media.Format;
import org.monte.media.Codec;
import org.monte.media.Buffer;
import org.monte.media.MovieWriter;
import org.monte.media.Registry;
import org.monte.media.io.ByteArrayImageOutputStream;
import org.monte.media.riff.RIFFParser;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.io.*;
import java.nio.ByteOrder;
import java.util.Arrays;
import javax.imageio.stream.*;
import static org.monte.media.AudioFormatKeys.*;
import static org.monte.media.VideoFormatKeys.*;
import org.monte.media.BufferFlag;
import static org.monte.media.BufferFlag.*;

/**
 * Provides high-level support for encoding and writing audio and video samples
 * into an AVI 1.0 file.
 *
 * @author Werner Randelshofer
 * @version $Id: AVIWriter.java 306 2013-01-04 16:19:29Z werner $
 */
public class AVIWriter extends AVIOutputStream implements MovieWriter {

    public final static Format AVI = new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI);
    public final static Format VIDEO_RAW = new Format(
            MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI,
            EncodingKey, ENCODING_AVI_DIB, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW);
    public final static Format VIDEO_JPEG = new Format(
            MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI,
            EncodingKey, ENCODING_AVI_MJPG, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW);
    public final static Format VIDEO_PNG = new Format(
            MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI,
            EncodingKey, ENCODING_AVI_PNG, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW);
    public final static Format VIDEO_RLE = new Format(
            MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI,
            EncodingKey, ENCODING_AVI_RLE, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW);
    public final static Format VIDEO_SCREEN_CAPTURE = new Format(
            MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI,
            EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW);

    /**
     * Creates a new AVI writer.
     *
     * @param file the output file
     */
    public AVIWriter(File file) throws IOException {
        super(file);
    }

    /**
     * Creates a new AVI writer.
     *
     * @param out the output stream.
     */
    public AVIWriter(ImageOutputStream out) throws IOException {
        super(out);
    }

    @Override
    public Format getFileFormat() throws IOException {
        return AVI;
    }

    @Override
    public Format getFormat(int track) {
        return tracks.get(track).format;
    }

    /**
     * Returns the media duration of the track in seconds.
     */
    @Override
    public Rational getDuration(int track) {
        Track tr = tracks.get(track);
        long duration = getMediaDuration(track);
        return new Rational(duration * tr.scale, tr.rate);
    }

    /**
     * Adds a track.
     *
     * @param format The format of the track.
     * @return The track number.
     */
    @Override
    public int addTrack(Format format) throws IOException {
        if (format.get(MediaTypeKey) == MediaType.VIDEO) {
            return addVideoTrack(format);
        } else {
            return addAudioTrack(format);
        }
    }

    /**
     * Adds a video track.
     *
     * @param format The format of the track.
     * @return The track number.
     */
    private int addVideoTrack(Format vf) throws IOException {
        if (!vf.containsKey(EncodingKey)) {
            throw new IllegalArgumentException("EncodingKey missing in " + vf);
        }
        if (!vf.containsKey(FrameRateKey)) {
            throw new IllegalArgumentException("FrameRateKey missing in " + vf);
        }
        if (!vf.containsKey(WidthKey)) {
            throw new IllegalArgumentException("WidthKey missing in " + vf);
        }
        if (!vf.containsKey(HeightKey)) {
            throw new IllegalArgumentException("HeightKey missing in " + vf);
        }
        if (!vf.containsKey(DepthKey)) {
            throw new IllegalArgumentException("DepthKey missing in " + vf);
        }
        int tr = addVideoTrack(vf.get(EncodingKey),
                vf.get(FrameRateKey).getDenominator(), vf.get(FrameRateKey).getNumerator(),
                vf.get(WidthKey), vf.get(HeightKey), vf.get(DepthKey),
                vf.get(FrameRateKey).floor(1).intValue());
        setCompressionQuality(tr, vf.get(QualityKey, 1.0f));
        return tr;
    }

    /**
     * Adds an audio track.
     *
     * @param format The format of the track.
     * @return The track number.
     */
    private int addAudioTrack(Format format) throws IOException {
        int waveFormatTag = 0x0001; // WAVE_FORMAT_PCM


        long timeScale = 1;
        long sampleRate = format.get(SampleRateKey, new Rational(41000, 0)).longValue();
        int numberOfChannels = format.get(ChannelsKey, 1);
        int sampleSizeInBits = format.get(SampleSizeInBitsKey, 16); //
        boolean isCompressed = false; // FIXME
        int frameDuration = 1;
        int frameSize = format.get(FrameSizeKey, (sampleSizeInBits + 7) / 8 * numberOfChannels);


        String enc = format.get(EncodingKey);
        if (enc == null) {
            waveFormatTag = 0x0001; // WAVE_FORMAT_PCM
        } else if (enc.equals(ENCODING_ALAW)) {
            waveFormatTag = 0x0001; // WAVE_FORMAT_PCM
        } else if (enc.equals(ENCODING_PCM_SIGNED)) {
            waveFormatTag = 0x0001; // WAVE_FORMAT_PCM
        } else if (enc.equals(ENCODING_PCM_UNSIGNED)) {
            waveFormatTag = 0x0001; // WAVE_FORMAT_PCM
        } else if (enc.equals(ENCODING_ULAW)) {
            waveFormatTag = 0x0001; // WAVE_FORMAT_PCM
        } else if (enc.equals(ENCODING_MP3)) {
            waveFormatTag = 0x0001; // WAVE_FORMAT_PCM - FIXME
        } else {
            waveFormatTag = RIFFParser.stringToID(format.get(EncodingKey)) & 0xffff;
        }

        return addAudioTrack(waveFormatTag, //
                timeScale, sampleRate, //
                numberOfChannels, sampleSizeInBits, //
                isCompressed, //
                frameDuration, frameSize);
    }

    /**
     * Returns the codec of the specified track.
     */
    public Codec getCodec(int track) {
        return tracks.get(track).codec;
    }

    /**
     * Sets the codec for the specified track.
     */
    public void setCodec(int track, Codec codec) {
        tracks.get(track).codec = codec;
    }

    @Override
    public int getTrackCount() {
        return tracks.size();
    }

    /**
     * Encodes the provided image and writes its sample data into the specified
     * track.
     *
     * @param track The track index.
     * @param image The image of the video frame.
     * @param duration Duration given in media time units.
     *
     * @throws IndexOutofBoundsException if the track index is out of bounds.
     * @throws if the duration is less than 1, or if the dimension of the frame
     * does not match the dimension of the video.
     * @throws UnsupportedOperationException if the {@code MovieWriter} does not
     * have a built-in encoder for this video format.
     * @throws java.io.IOException if writing the sample data failed.
     */
    public void write(int track, BufferedImage image, long duration) throws IOException {
        ensureStarted();

        VideoTrack vt = (VideoTrack) tracks.get(track);
        if (vt.codec == null) {
            createCodec(track);
        }
        if (vt.codec == null) {
            throw new UnsupportedOperationException("No codec for this format: " + vt.format);
        }

        // The dimension of the image must match the dimension of the video track
        Format fmt = vt.format;
        if (fmt.get(WidthKey) != image.getWidth() || fmt.get(HeightKey) != image.getHeight()) {
            throw new IllegalArgumentException("Dimensions of image[" + vt.samples.size()
                    + "] (width=" + image.getWidth() + ", height=" + image.getHeight()
                    + ") differs from video format of track: " + fmt);
        }

        // Encode pixel data
        {
            if (vt.outputBuffer == null) {
                vt.outputBuffer = new Buffer();
            }

            boolean isKeyframe = vt.syncInterval == 0 ? false : vt.samples.size() % vt.syncInterval == 0;

            Buffer inputBuffer = new Buffer();
            inputBuffer.flags = (isKeyframe) ? EnumSet.of(KEYFRAME) : EnumSet.noneOf(BufferFlag.class);
            inputBuffer.data = image;
            vt.codec.process(inputBuffer, vt.outputBuffer);
            if (vt.outputBuffer.flags.contains(DISCARD)) {
                return;
            }

            // Encode palette data
            isKeyframe = vt.outputBuffer.flags.contains(KEYFRAME);
            boolean paletteChange = writePalette(track, image, isKeyframe);
            writeSample(track, (byte[]) vt.outputBuffer.data, vt.outputBuffer.offset, vt.outputBuffer.length, isKeyframe && !paletteChange);
            /*
             long offset = getRelativeStreamPosition();

             DataChunk videoFrameChunk = new DataChunk(vt.getSampleChunkFourCC(isKeyframe));
             moviChunk.add(videoFrameChunk);
             videoFrameChunk.getOutputStream().write((byte[]) vt.outputBuffer.data, vt.outputBuffer.offset, vt.outputBuffer.length);
             videoFrameChunk.finish();
             long length = getRelativeStreamPosition() - offset;

             Sample s=new Sample(videoFrameChunk.chunkType, 1, offset, length, isKeyframe&&!paletteChange);
             vt.addSample(s);
             idx1.add(s);
            
             if (getRelativeStreamPosition() > 1L << 32) {
             throw new IOException("AVI file is larger than 4 GB");
             }*/
        }
    }

    /**
     * Encodes the data provided in the buffer and then writes it into the
     * specified track. 

Does nothing if the discard-flag in the buffer is * set to true. * * @param track The track number. * @param buf The buffer containing a data sample. */ @Override public void write(int track, Buffer buf) throws IOException { ensureStarted(); if (buf.flags.contains(DISCARD)) { return; } Track tr = tracks.get(track); boolean isKeyframe = buf.flags.contains(KEYFRAME); if (buf.data instanceof BufferedImage) { if (tr.syncInterval != 0) { isKeyframe = buf.flags.contains(KEYFRAME) | (tr.samples.size() % tr.syncInterval == 0); } } // Encode palette data boolean paletteChange = false; if (buf.data instanceof BufferedImage && tr instanceof VideoTrack) { paletteChange = writePalette(track, (BufferedImage) buf.data, isKeyframe); } else if (buf.header instanceof IndexColorModel) { paletteChange = writePalette(track, (IndexColorModel) buf.header, isKeyframe); } // Encode sample data { if (buf.format == null) { throw new IllegalArgumentException("Buffer.format must not be null"); } if (buf.format.matchesWithout(tr.format, FrameRateKey) && buf.data instanceof byte[]) { writeSamples(track, buf.sampleCount, (byte[]) buf.data, buf.offset, buf.length, buf.isFlag(KEYFRAME) && !paletteChange); return; } // We got here, because the buffer format does not match the track // format. Lets see if we can create a codec which can perform the // encoding for us. if (tr.codec == null) { createCodec(track); if (tr.codec == null) { throw new UnsupportedOperationException("No codec for this format " + tr.format); } } if (tr.outputBuffer == null) { tr.outputBuffer = new Buffer(); } Buffer outBuf = tr.outputBuffer; if (tr.codec.process(buf, outBuf) != Codec.CODEC_OK) { throw new IOException("Codec failed or could not encode the sample in a single step."); } if (outBuf.isFlag(DISCARD)) { return; } writeSamples(track, outBuf.sampleCount, (byte[]) outBuf.data, outBuf.offset, outBuf.length, isKeyframe && !paletteChange); } } private boolean writePalette(int track, BufferedImage image, boolean isKeyframe) throws IOException { if ((image.getColorModel() instanceof IndexColorModel)) { return writePalette(track, (IndexColorModel) image.getColorModel(), isKeyframe); } return false; } private boolean writePalette(int track, IndexColorModel imgPalette, boolean isKeyframe) throws IOException { ensureStarted(); VideoTrack vt = (VideoTrack) tracks.get(track); int imgDepth = vt.bitCount; ByteArrayImageOutputStream tmp = null; boolean paletteChange = false; switch (imgDepth) { case 4: { //IndexColorModel imgPalette = (IndexColorModel) image.getColorModel(); int[] imgRGBs = new int[16]; imgPalette.getRGBs(imgRGBs); int[] previousRGBs = new int[16]; if (vt.previousPalette == null) { vt.previousPalette = vt.palette; } vt.previousPalette.getRGBs(previousRGBs); if (isKeyframe || !Arrays.equals(imgRGBs, previousRGBs)) { paletteChange = true; vt.previousPalette = imgPalette; /* 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; */ tmp = new ByteArrayImageOutputStream(ByteOrder.LITTLE_ENDIAN); tmp.writeByte(first);//bFirstEntry tmp.writeByte(last - first + 1);//bNumEntries tmp.writeShort(0);//wFlags for (int i = first; i <= last; i++) { tmp.writeByte((imgRGBs[i] >>> 16) & 0xff); // red tmp.writeByte((imgRGBs[i] >>> 8) & 0xff); // green tmp.writeByte(imgRGBs[i] & 0xff); // blue tmp.writeByte(0); // reserved*/ } } break; } case 8: { //IndexColorModel imgPalette = (IndexColorModel) image.getColorModel(); int[] imgRGBs = new int[256]; imgPalette.getRGBs(imgRGBs); int[] previousRGBs = new int[256]; if (vt.previousPalette != null) { vt.previousPalette.getRGBs(previousRGBs); } if (isKeyframe || !Arrays.equals(imgRGBs, previousRGBs)) { paletteChange = true; vt.previousPalette = imgPalette; /* 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; */ tmp = new ByteArrayImageOutputStream(ByteOrder.LITTLE_ENDIAN); tmp.writeByte(first);//bFirstEntry tmp.writeByte(last - first + 1);//bNumEntries tmp.writeShort(0);//wFlags for (int i = first; i <= last; i++) { tmp.writeByte((imgRGBs[i] >>> 16) & 0xff); // red tmp.writeByte((imgRGBs[i] >>> 8) & 0xff); // green tmp.writeByte(imgRGBs[i] & 0xff); // blue tmp.writeByte(0); // reserved*/ } } break; } } if (tmp != null) { tmp.close(); writePalette(track, tmp.toByteArray(), 0, (int) tmp.length(), isKeyframe); } return paletteChange; } private Codec createCodec(Format fmt) { return Registry.getInstance().getEncoder(fmt.prepend(MimeTypeKey, MIME_AVI)); } private void createCodec(int track) { Track tr = tracks.get(track); Format fmt = tr.format; tr.codec = createCodec(fmt); String enc = fmt.get(EncodingKey); if (tr.codec != null) { if (fmt.get(MediaTypeKey) == MediaType.VIDEO) { tr.codec.setInputFormat(fmt.prepend( EncodingKey, ENCODING_BUFFERED_IMAGE, DataClassKey, BufferedImage.class)); if (null == tr.codec.setOutputFormat( fmt.prepend(FixedFrameRateKey, true, QualityKey, getCompressionQuality(track), MimeTypeKey, MIME_AVI, DataClassKey, byte[].class))) { throw new UnsupportedOperationException("Track " + tr + " codec does not support format " + fmt + ". codec=" + tr.codec); } } else { tr.codec.setInputFormat(null); if (null == tr.codec.setOutputFormat( fmt.prepend(FixedFrameRateKey, true, QualityKey, getCompressionQuality(track), MimeTypeKey, MIME_AVI, DataClassKey, byte[].class))) { throw new UnsupportedOperationException("Track " + tr + " codec " + tr.codec + " does not support format. " + fmt); } } } } public boolean isVFRSupported() { return false; } @Override public boolean isEmpty(int track) { return tracks.get(track).samples.isEmpty(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy