
org.monte.media.avi.AVIWriter Maven / Gradle / Ivy
/**
* @(#)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();
}
}