diff --git a/trunk/lib/avi.jar b/trunk/lib/avi.jar
new file mode 100644
index 000000000..5409c7678
Binary files /dev/null and b/trunk/lib/avi.jar differ
diff --git a/trunk/lib/gif.jar b/trunk/lib/gif.jar
new file mode 100644
index 000000000..05ad57001
Binary files /dev/null and b/trunk/lib/gif.jar differ
diff --git a/trunk/libsrc/avi/build.xml b/trunk/libsrc/avi/build.xml
new file mode 100644
index 000000000..f35d65806
--- /dev/null
+++ b/trunk/libsrc/avi/build.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+ Builds, tests, and runs the project avi.
+
+
+
diff --git a/trunk/libsrc/avi/nbproject/build-impl.xml b/trunk/libsrc/avi/nbproject/build-impl.xml
new file mode 100644
index 000000000..675a2b235
--- /dev/null
+++ b/trunk/libsrc/avi/nbproject/build-impl.xml
@@ -0,0 +1,1407 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must set src.dir
+ Must set test.src.dir
+ Must set build.dir
+ Must set dist.dir
+ Must set build.classes.dir
+ Must set dist.javadoc.dir
+ Must set build.test.classes.dir
+ Must set build.test.results.dir
+ Must set build.classes.excludes
+ Must set dist.jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must set javac.includes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No tests executed.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must set JVM to use for profiling in profiler.info.jvm
+ Must set profiler agent JVM arguments in profiler.info.jvmargs.agent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select some files in the IDE or set javac.includes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ To run this application from the command line without Ant, try:
+
+ java -jar "${dist.jar.resolved}"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set debug.class
+
+
+
+
+ Must select one file in the IDE or set debug.class
+
+
+
+
+ Must set fix.includes
+
+
+
+
+
+
+
+
+
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set profile.class
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+
+
+
+
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+
+
+ Must select some files in the IDE or set test.includes
+
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+
+ Must select one file in the IDE or set applet.url
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select some files in the IDE or set javac.includes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Some tests failed; see details above.
+
+
+
+
+
+
+
+
+ Must select some files in the IDE or set test.includes
+
+
+
+ Some tests failed; see details above.
+
+
+
+ Must select some files in the IDE or set test.class
+ Must select some method in the IDE or set test.method
+
+
+
+ Some tests failed; see details above.
+
+
+
+
+ Must select one file in the IDE or set test.class
+
+
+
+ Must select one file in the IDE or set test.class
+ Must select some method in the IDE or set test.method
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set applet.url
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set applet.url
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/trunk/libsrc/avi/nbproject/genfiles.properties b/trunk/libsrc/avi/nbproject/genfiles.properties
new file mode 100644
index 000000000..4b63314ae
--- /dev/null
+++ b/trunk/libsrc/avi/nbproject/genfiles.properties
@@ -0,0 +1,8 @@
+build.xml.data.CRC32=505ab359
+build.xml.script.CRC32=fa68b8cb
+build.xml.stylesheet.CRC32=8064a381@1.68.1.46
+# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
+# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
+nbproject/build-impl.xml.data.CRC32=505ab359
+nbproject/build-impl.xml.script.CRC32=b5283dd3
+nbproject/build-impl.xml.stylesheet.CRC32=5a01deb7@1.68.1.46
diff --git a/trunk/libsrc/avi/nbproject/project.properties b/trunk/libsrc/avi/nbproject/project.properties
new file mode 100644
index 000000000..b26bc8e4c
--- /dev/null
+++ b/trunk/libsrc/avi/nbproject/project.properties
@@ -0,0 +1,72 @@
+annotation.processing.enabled=true
+annotation.processing.enabled.in.editor=false
+annotation.processing.processor.options=
+annotation.processing.processors.list=
+annotation.processing.run.all.processors=true
+annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
+build.classes.dir=${build.dir}/classes
+build.classes.excludes=**/*.java,**/*.form
+# This directory is removed when the project is cleaned:
+build.dir=build
+build.generated.dir=${build.dir}/generated
+build.generated.sources.dir=${build.dir}/generated-sources
+# Only compile against the classpath explicitly listed here:
+build.sysclasspath=ignore
+build.test.classes.dir=${build.dir}/test/classes
+build.test.results.dir=${build.dir}/test/results
+# Uncomment to specify the preferred debugger connection transport:
+#debug.transport=dt_socket
+debug.classpath=\
+ ${run.classpath}
+debug.test.classpath=\
+ ${run.test.classpath}
+# Files in build.classes.dir which should be excluded from distribution jar
+dist.archive.excludes=
+# This directory is removed when the project is cleaned:
+dist.dir=dist
+dist.jar=${dist.dir}/avi.jar
+dist.javadoc.dir=${dist.dir}/javadoc
+excludes=
+includes=**
+jar.compress=false
+javac.classpath=
+# Space-separated list of extra javac options
+javac.compilerargs=
+javac.deprecation=false
+javac.processorpath=\
+ ${javac.classpath}
+javac.source=1.7
+javac.target=1.7
+javac.test.classpath=\
+ ${javac.classpath}:\
+ ${build.classes.dir}
+javac.test.processorpath=\
+ ${javac.test.classpath}
+javadoc.additionalparam=
+javadoc.author=false
+javadoc.encoding=${source.encoding}
+javadoc.noindex=false
+javadoc.nonavbar=false
+javadoc.notree=false
+javadoc.private=false
+javadoc.splitindex=true
+javadoc.use=true
+javadoc.version=false
+javadoc.windowtitle=
+main.class=org.monte.media.math.IntMath
+meta.inf.dir=${src.dir}/META-INF
+mkdist.disabled=true
+platform.active=default_platform
+run.classpath=\
+ ${javac.classpath}:\
+ ${build.classes.dir}
+# Space-separated list of JVM arguments used when running the project.
+# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
+# To set system properties for unit tests define test-sys-prop.name=value:
+run.jvmargs=
+run.test.classpath=\
+ ${javac.test.classpath}:\
+ ${build.test.classes.dir}
+source.encoding=UTF-8
+src.dir=src
+test.src.dir=test
diff --git a/trunk/libsrc/avi/nbproject/project.xml b/trunk/libsrc/avi/nbproject/project.xml
new file mode 100644
index 000000000..53a131c45
--- /dev/null
+++ b/trunk/libsrc/avi/nbproject/project.xml
@@ -0,0 +1,15 @@
+
+
+ org.netbeans.modules.java.j2seproject
+
+
+ avi
+
+
+
+
+
+
+
+
+
diff --git a/trunk/libsrc/avi/src/org/monte/media/AbortException.java b/trunk/libsrc/avi/src/org/monte/media/AbortException.java
new file mode 100644
index 000000000..cd20a6ecf
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/AbortException.java
@@ -0,0 +1,39 @@
+/*
+ * @(#)AbortException.java
+ *
+ * Copyright (c) 1999-2012 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;
+
+/**
+ * This exception is thrown when the production of an image
+ * has been aborted.
+ *
+ * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland
+ *
+ * @version $Id: AbortException.java 299 2013-01-03 07:40:18Z werner $
+ */
+public class AbortException extends Exception {
+
+ public static final long serialVersionUID = 1L;
+
+ /**
+ Creates a new exception.
+ */
+ public AbortException() {
+ super();
+ }
+
+ /**
+ Creates a new exception.
+
+ */
+ public AbortException(String message) {
+ super(message);
+ }
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/AbstractCodec.java b/trunk/libsrc/avi/src/org/monte/media/AbstractCodec.java
new file mode 100644
index 000000000..6a2a5e816
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/AbstractCodec.java
@@ -0,0 +1,99 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.monte.media;
+
+import java.util.ArrayList;
+/**
+ * {@code AbstractCodec}.
+ *
+ * @author Werner Randelshofer
+ * @version 1.0 2011-03-12 Created.
+ */
+public abstract class AbstractCodec implements Codec {
+
+ protected Format[] inputFormats;
+ protected Format[] outputFormats;
+ protected Format inputFormat;
+ protected Format outputFormat;
+ protected String name="unnamed codec";
+
+ public AbstractCodec(Format[] supportedInputFormats, Format[] supportedOutputFormats) {
+ this.inputFormats = supportedInputFormats;
+ this.outputFormats = supportedOutputFormats;
+ }
+ public AbstractCodec(Format[] supportedInputOutputFormats) {
+ this.inputFormats = supportedInputOutputFormats;
+ this.outputFormats = supportedInputOutputFormats;
+ }
+
+ @Override
+ public Format[] getInputFormats() {
+ return inputFormats.clone();
+ }
+
+ @Override
+ public Format[] getOutputFormats(Format input) {
+ ArrayListof=new ArrayList(outputFormats.length);
+ for (Format f:outputFormats) {
+ of.add(input==null?f:f.append(input));
+ }
+ return of.toArray(new Format[of.size()]);
+ }
+
+ @Override
+ public Format setInputFormat(Format f) {
+ if (f!=null)
+ for (Format sf : getInputFormats()) {
+ if (sf.matches(f)) {
+ this.inputFormat = sf.append(f);
+ return inputFormat;
+ }
+ }
+ this.inputFormat=null;
+ return null;
+ }
+
+ @Override
+ public Format setOutputFormat(Format f) {
+ for (Format sf : getOutputFormats(f)) {
+ if (sf.matches(f)) {
+ this.outputFormat = f;
+ return sf;
+ }
+ }
+ this.outputFormat=null;
+ return null;
+ }
+
+ @Override
+ public Format getInputFormat() {
+ return inputFormat;
+ }
+
+ @Override
+ public Format getOutputFormat() {
+ return outputFormat;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ /** Empty implementation of the reset method. Don't call super. */
+ @Override
+ public void reset() {
+ // empty
+ }
+
+ @Override
+ public String toString() {
+ String className=getClass().getName();
+ int p=className.lastIndexOf('.');
+ return className.substring(p+1)+"{" + "inputFormat=" + inputFormat + ", outputFormat=" + outputFormat+'}';
+ }
+
+
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/AbstractVideoCodec.java b/trunk/libsrc/avi/src/org/monte/media/AbstractVideoCodec.java
new file mode 100644
index 000000000..f180e7233
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/AbstractVideoCodec.java
@@ -0,0 +1,226 @@
+/*
+ * @(#)AbstractVideoCodec.java
+ *
+ * Copyright (c) 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;
+
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBufferByte;
+import java.awt.image.DataBufferInt;
+import java.awt.image.DataBufferShort;
+import java.awt.image.DataBufferUShort;
+import java.awt.image.DirectColorModel;
+import java.awt.image.WritableRaster;
+import java.io.IOException;
+import javax.imageio.stream.ImageOutputStream;
+import static org.monte.media.VideoFormatKeys.*;
+
+/**
+ * {@code AbstractVideoCodec}.
+ *
+ * @author Werner Randelshofer
+ * @version $Id: AbstractVideoCodec.java 299 2013-01-03 07:40:18Z werner $
+ */
+public abstract class AbstractVideoCodec extends AbstractCodec {
+
+ private BufferedImage imgConverter;
+
+ public AbstractVideoCodec(Format[] supportedInputFormats, Format[] supportedOutputFormats) {
+ super(supportedInputFormats, supportedOutputFormats);
+ }
+
+ /** Gets 8-bit indexed pixels from a buffer. Returns null if conversion failed. */
+ protected byte[] getIndexed8(Buffer buf) {
+ if (buf.data instanceof byte[]) {
+ return (byte[]) buf.data;
+ }
+ if (buf.data instanceof BufferedImage) {
+ BufferedImage image = (BufferedImage) buf.data;
+ if (image.getRaster().getDataBuffer() instanceof DataBufferByte) {
+ return ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
+ }
+ }
+ return null;
+ }
+
+ /** Gets 15-bit RGB pixels from a buffer. Returns null if conversion failed. */
+ protected short[] getRGB15(Buffer buf) {
+ if (buf.data instanceof int[]) {
+ return (short[]) buf.data;
+ }
+ if (buf.data instanceof BufferedImage) {
+ BufferedImage image = (BufferedImage) buf.data;
+ if (image.getColorModel() instanceof DirectColorModel) {
+ DirectColorModel dcm = (DirectColorModel) image.getColorModel();
+ if (image.getRaster().getDataBuffer() instanceof DataBufferShort) {
+ // FIXME - Implement additional checks
+ return ((DataBufferShort) image.getRaster().getDataBuffer()).getData();
+ } else if (image.getRaster().getDataBuffer() instanceof DataBufferUShort) {
+ // FIXME - Implement additional checks
+ return ((DataBufferUShort) image.getRaster().getDataBuffer()).getData();
+ }
+ }
+ if (imgConverter == null) {
+ int width = outputFormat.get(WidthKey);
+ int height = outputFormat.get(HeightKey);
+ imgConverter = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_555_RGB);
+ }
+ Graphics2D g = imgConverter.createGraphics();
+ g.drawImage(image, 0, 0, null);
+ g.dispose();
+ return ((DataBufferUShort) imgConverter.getRaster().getDataBuffer()).getData();
+ }
+ return null;
+ }
+ /** Gets 16-bit RGB-5-6-5 pixels from a buffer. Returns null if conversion failed. */
+ protected short[] getRGB16(Buffer buf) {
+ if (buf.data instanceof int[]) {
+ return (short[]) buf.data;
+ }
+ if (buf.data instanceof BufferedImage) {
+ BufferedImage image = (BufferedImage) buf.data;
+ if (image.getColorModel() instanceof DirectColorModel) {
+ DirectColorModel dcm = (DirectColorModel) image.getColorModel();
+ if (image.getRaster().getDataBuffer() instanceof DataBufferShort) {
+ // FIXME - Implement additional checks
+ return ((DataBufferShort) image.getRaster().getDataBuffer()).getData();
+ } else if (image.getRaster().getDataBuffer() instanceof DataBufferUShort) {
+ // FIXME - Implement additional checks
+ return ((DataBufferUShort) image.getRaster().getDataBuffer()).getData();
+ }
+ }
+ if (imgConverter == null) {
+ int width = outputFormat.get(WidthKey);
+ int height = outputFormat.get(HeightKey);
+ imgConverter = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_565_RGB);
+ }
+ Graphics2D g = imgConverter.createGraphics();
+ g.drawImage(image, 0, 0, null);
+ g.dispose();
+ return ((DataBufferUShort) imgConverter.getRaster().getDataBuffer()).getData();
+ }
+ return null;
+ }
+
+
+ /** Gets 24-bit RGB pixels from a buffer. Returns null if conversion failed. */
+ protected int[] getRGB24(Buffer buf) {
+ if (buf.data instanceof int[]) {
+ return (int[]) buf.data;
+ }
+ if (buf.data instanceof BufferedImage) {
+ BufferedImage image = (BufferedImage) buf.data;
+ if (image.getColorModel() instanceof DirectColorModel) {
+ DirectColorModel dcm = (DirectColorModel) image.getColorModel();
+ if (dcm.getBlueMask() == 0xff && dcm.getGreenMask() == 0xff00 && dcm.getRedMask() == 0xff0000) {
+ if (image.getRaster().getDataBuffer() instanceof DataBufferInt) {
+ return ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
+ }
+ }
+ }
+ return image.getRGB(0, 0, //
+ outputFormat.get(WidthKey), outputFormat.get(HeightKey), //
+ null, 0, outputFormat.get(WidthKey));
+ }
+ return null;
+ }
+
+ /** Gets 32-bit ARGB pixels from a buffer. Returns null if conversion failed. */
+ protected int[] getARGB32(Buffer buf) {
+ if (buf.data instanceof int[]) {
+ return (int[]) buf.data;
+ }
+ if (buf.data instanceof BufferedImage) {
+ BufferedImage image = (BufferedImage) buf.data;
+ if (image.getColorModel() instanceof DirectColorModel) {
+ DirectColorModel dcm = (DirectColorModel) image.getColorModel();
+ if (dcm.getBlueMask() == 0xff && dcm.getGreenMask() == 0xff00 && dcm.getRedMask() == 0xff0000) {
+ if (image.getRaster().getDataBuffer() instanceof DataBufferInt) {
+ return ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
+ }
+ }
+ }
+ return image.getRGB(0, 0, //
+ outputFormat.get(WidthKey), outputFormat.get(HeightKey), //
+ null, 0, outputFormat.get(WidthKey));
+ }
+ return null;
+ }
+
+ /** Gets a buffered image from a buffer. Returns null if conversion failed. */
+ protected BufferedImage getBufferedImage(Buffer buf) {
+ if (buf.data instanceof BufferedImage) {
+ return (BufferedImage) buf.data;
+ }
+ return null;
+ }
+ private byte[] byteBuf = new byte[4];
+
+ protected void writeInt24(ImageOutputStream out, int v) throws IOException {
+ byteBuf[0] = (byte) (v >>> 16);
+ byteBuf[1] = (byte) (v >>> 8);
+ byteBuf[2] = (byte) (v >>> 0);
+ out.write(byteBuf, 0, 3);
+ }
+
+ protected void writeInt24LE(ImageOutputStream out, int v) throws IOException {
+ byteBuf[2] = (byte) (v >>> 16);
+ byteBuf[1] = (byte) (v >>> 8);
+ byteBuf[0] = (byte) (v >>> 0);
+ out.write(byteBuf, 0, 3);
+ }
+
+ protected void writeInts24(ImageOutputStream out, int[] i, int off, int len) throws IOException {
+ // Fix 4430357 - if off + len < 0, overflow occurred
+ if (off < 0 || len < 0 || off + len > i.length || off + len < 0) {
+ throw new IndexOutOfBoundsException("off < 0 || len < 0 || off + len > i.length!");
+ }
+
+ byte[] b = new byte[len * 3];
+ int boff = 0;
+ for (int j = 0; j < len; j++) {
+ int v = i[off + j];
+ //b[boff++] = (byte)(v >>> 24);
+ b[boff++] = (byte) (v >>> 16);
+ b[boff++] = (byte) (v >>> 8);
+ b[boff++] = (byte) (v >>> 0);
+ }
+
+ out.write(b, 0, len * 3);
+ }
+
+ protected void writeInts24LE(ImageOutputStream out, int[] i, int off, int len) throws IOException {
+ // Fix 4430357 - if off + len < 0, overflow occurred
+ if (off < 0 || len < 0 || off + len > i.length || off + len < 0) {
+ throw new IndexOutOfBoundsException("off < 0 || len < 0 || off + len > i.length!");
+ }
+
+ byte[] b = new byte[len * 3];
+ int boff = 0;
+ for (int j = 0; j < len; j++) {
+ int v = i[off + j];
+ b[boff++] = (byte) (v >>> 0);
+ b[boff++] = (byte) (v >>> 8);
+ b[boff++] = (byte) (v >>> 16);
+ //b[boff++] = (byte)(v >>> 24);
+ }
+
+ out.write(b, 0, len * 3);
+ }
+
+ /** Copies a buffered image. */
+ protected static BufferedImage copyImage(BufferedImage img) {
+ ColorModel cm = img.getColorModel();
+ boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
+ WritableRaster raster = img.copyData(null);
+ return new BufferedImage(cm, raster, isAlphaPremultiplied, null);
+ }
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/AudioFormatKeys.java b/trunk/libsrc/avi/src/org/monte/media/AudioFormatKeys.java
new file mode 100644
index 000000000..4d5e2558c
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/AudioFormatKeys.java
@@ -0,0 +1,126 @@
+/*
+ * @(#)AudioFormatKeys.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;
+
+import org.monte.media.math.Rational;
+import java.nio.ByteOrder;
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioFormat.Encoding;
+
+/**
+ * Defines common format keys for audio media.
+ *
+ * @author Werner Randelshofer
+ * @version $Id: AudioFormatKeys.java 299 2013-01-03 07:40:18Z werner $
+ */
+public class AudioFormatKeys extends FormatKeys {
+ // Standard video EncodingKey strings for use onlyWith FormatKey.Encoding.
+
+ /**
+ * Specifies SignedKey, linear PCM data.
+ */
+ public static final String ENCODING_PCM_SIGNED = javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED.toString();
+ /**
+ * Specifies unsigned, linear PCM data.
+ */
+ public static final String ENCODING_PCM_UNSIGNED = javax.sound.sampled.AudioFormat.Encoding.PCM_UNSIGNED.toString();
+ /**
+ * Specifies u-law encoded data.
+ */
+ public static final String ENCODING_ULAW = javax.sound.sampled.AudioFormat.Encoding.ULAW.toString();
+ /**
+ * Specifies a-law encoded data.
+ */
+ public static final String ENCODING_ALAW = javax.sound.sampled.AudioFormat.Encoding.ALAW.toString();
+ /**
+ * AVI PCM encoding.
+ */
+ public static final String ENCODING_AVI_PCM = "\u0000\u0000\u0000\u0001";
+ /**
+ * QuickTime 16-bit big endian signed PCM encoding.
+ */
+ public static final String ENCODING_QUICKTIME_TWOS_PCM = "twos";
+ /**
+ * QuickTime 16-bit little endian signed PCM encoding.
+ */
+ public static final String ENCODING_QUICKTIME_SOWT_PCM = "sowt";
+ /**
+ * QuickTime 24-bit big endian signed PCM encoding.
+ */
+ public static final String ENCODING_QUICKTIME_IN24_PCM = "in24";
+ /**
+ * QuickTime 32-bit big endian signed PCM encoding.
+ */
+ public static final String ENCODING_QUICKTIME_IN32_PCM = "in32";
+ /**
+ * QuickTime 8-bit unsigned PCM encoding.
+ */
+ public static final String ENCODING_QUICKTIME_RAW_PCM = "raw ";
+ /**
+ * Specifies MP3 encoded data.
+ */
+ public static final String ENCODING_MP3 = "MP3";
+ /**
+ * The sample size in bits.
+ */
+ public final static FormatKey SampleSizeInBitsKey = new FormatKey("sampleSizeInBits", Integer.class);
+ /**
+ * The numer of ChannelsKey.
+ */
+ public final static FormatKey ChannelsKey = new FormatKey("channels", Integer.class);
+ /**
+ * The size of a frame.
+ */
+ public final static FormatKey FrameSizeKey = new FormatKey("frameSize", Integer.class);
+ /**
+ * The compressor name.
+ */
+ public final static FormatKey ByteOrderKey = new FormatKey("byteOrder", ByteOrder.class);
+ /**
+ * The number of frames per second.
+ */
+ public final static FormatKey SampleRateKey = new FormatKey("sampleRate", Rational.class);
+ /**
+ * Whether values are signed.
+ */
+ public final static FormatKey SignedKey = new FormatKey("signed", Boolean.class);
+ /**
+ * Whether silence is encoded as -128 instead of 0.
+ */
+ public final static FormatKey SilenceBugKey = new FormatKey("silenceBug", Boolean.class);
+
+ public static Format fromAudioFormat(javax.sound.sampled.AudioFormat fmt) {
+ return new Format(
+ MediaTypeKey, MediaType.AUDIO,
+ EncodingKey, fmt.getEncoding().toString(),
+ SampleRateKey, Rational.valueOf(fmt.getSampleRate()),
+ SampleSizeInBitsKey, fmt.getSampleSizeInBits(),
+ ChannelsKey, fmt.getChannels(),
+ FrameSizeKey, fmt.getFrameSize(),
+ FrameRateKey, Rational.valueOf(fmt.getFrameRate()),
+ ByteOrderKey, fmt.isBigEndian() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN,
+ SignedKey, AudioFormat.Encoding.PCM_SIGNED.equals(fmt.getEncoding())//,
+ //
+ );
+ }
+
+ public static javax.sound.sampled.AudioFormat toAudioFormat(Format fmt) {
+ // We always use PCM_SIGNED or PCM_UNSIGNED
+ return new javax.sound.sampled.AudioFormat(
+ !fmt.containsKey(SignedKey) || fmt.get(SignedKey) ? Encoding.PCM_SIGNED : Encoding.PCM_UNSIGNED,
+ fmt.get(SampleRateKey).floatValue(),
+ fmt.get(SampleSizeInBitsKey, 16),
+ fmt.get(ChannelsKey, 1),
+ fmt.containsKey(FrameSizeKey) ? fmt.get(FrameSizeKey) : (fmt.get(SampleSizeInBitsKey, 16) + 7) / 8 * fmt.get(ChannelsKey, 1),
+ fmt.containsKey(FrameRateKey) ? fmt.get(FrameRateKey).floatValue() : fmt.get(SampleRateKey).floatValue(),
+ fmt.containsKey(ByteOrderKey) ? fmt.get(ByteOrderKey) == ByteOrder.BIG_ENDIAN : true);
+ }
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/Buffer.java b/trunk/libsrc/avi/src/org/monte/media/Buffer.java
new file mode 100644
index 000000000..94c6da20e
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/Buffer.java
@@ -0,0 +1,164 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.monte.media;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.WritableRaster;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.monte.media.math.Rational;
+import org.monte.media.util.Methods;
+
+/**
+ * A {@code Buffer} carries media data from one media processing unit to another.
+ *
+ * @author Werner Randelshofer
+ * @version 1.0 2011-03-12 Created.
+ */
+public class Buffer {
+
+ /** A flag mask that describes the boolean attributes for this buffer.
+ */
+ public EnumSet flags = EnumSet.noneOf(BufferFlag.class);
+ /** Values which are not specified must have this value. */
+ public static final int NOT_SPECIFIED = -1;
+ /** The track number.
+ * This can be set to NOT_SPECIFIED or to a number >= 0.
+ */
+ public int track;
+ /** Header information, such as RTP header for this chunk. */
+ public Object header;
+ /** The media data. */
+ public Object data;
+ /** The data offset. This field is only used if {@code data} is an array. */
+ public int offset;
+ /** The data length. This field is only used if {@code data} is an array. */
+ public int length;
+ /** Duration of a sample in seconds.
+ * Multiply this with {@code sampleCount} to get the buffer duration.
+ */
+ public Rational sampleDuration;
+ /** The time stamp of this buffer in seconds. */
+ public Rational timeStamp;
+ /** The format of the data in this buffer. */
+ public Format format;
+ /** The number of samples in the data field. */
+ public int sampleCount = 1;
+
+ /** Sequence number of the buffer. This can be used for debugging. */
+ public long sequenceNumber;
+
+ /** Sets all variables of this buffer to that buffer except for {@code data},
+ * {@code offset}, {@code length} and {@code header}.
+ */
+ public void setMetaTo(Buffer that) {
+ this.flags = EnumSet.copyOf(that.flags);
+ //this.data=that.data;
+ //this.offset=that.offset;
+ //this.length=that.length;
+ //this.header=that.header;
+ this.track = that.track;
+ this.sampleDuration = that.sampleDuration;
+ this.timeStamp = that.timeStamp;
+ this.format = that.format;
+ this.sampleCount = that.sampleCount;
+ this.format = that.format;
+ this.sequenceNumber=that.sequenceNumber;
+ }
+
+ /** Sets {@code data}, {@code offset}, {@code length} and {@code header}
+ * of this buffer to that buffer.
+ * Note that this method creates copies of the {@code data} and
+ * {@code header}, so that these fields in that buffer can be discarded
+ * without affecting the contents of this buffer.
+ *
+ * FIXME - This method does not always create a copy!!
+ */
+ public void setDataTo(Buffer that) {
+ this.offset = that.offset;
+ this.length = that.length;
+ this.data = copy(that.data, this.data);
+ this.header = copy(that.header, this.header);
+
+ }
+
+ private Object copy(Object from, Object into) {
+ if (from instanceof byte[]) {
+ byte[] b=(byte[])from;
+ if (!(into instanceof byte[]) || ((byte[]) into).length < b.length) {
+ into = new byte[b.length];
+ }
+ System.arraycopy(b, 0, (byte[])into, 0, b.length);
+ } else if (from instanceof BufferedImage) {
+ // FIXME - Try to reuse BufferedImage in output!
+ BufferedImage img = (BufferedImage) from;
+ ColorModel cm = img.getColorModel();
+ boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
+ WritableRaster raster = img.copyData(null);
+ into = new BufferedImage(cm, raster, isAlphaPremultiplied, null);
+ } else if (from instanceof Cloneable) {
+ try {
+ into=Methods.invoke(from, "clone");
+ } catch (NoSuchMethodException ex) {
+ into=from;
+ }
+ } else {
+ // FIXME - This is very fragile, since we do not know, if the
+ // input data stays valid until the output data is processed!
+ into = from;
+ }
+
+ return into;
+ }
+
+ /** Returns true if the specified flag is set. */
+ public boolean isFlag(BufferFlag flag) {
+ return flags.contains(flag);
+ }
+
+ /** Convenience method for setting a flag. */
+ public void setFlag(BufferFlag flag) {
+ setFlag(flag, true);
+ }
+
+ /** Convenience method for clearing a flag. */
+ public void clearFlag(BufferFlag flag) {
+ setFlag(flag, false);
+ }
+
+ /** Sets or clears the specified flag. */
+ public void setFlag(BufferFlag flag, boolean value) {
+ if (value) {
+ flags.add(flag);
+ } else {
+ flags.remove(flag);
+ }
+ }
+
+ /** Clears all flags, and then sets the specified flag. */
+ public void setFlagsTo(BufferFlag... flags) {
+ if (flags.length == 0) {
+ this.flags = EnumSet.noneOf(BufferFlag.class);
+ } else {
+ this.flags = EnumSet.copyOf(Arrays.asList(flags));
+ }
+ }
+
+ /** Clears all flags, and then sets the specified flag. */
+ public void setFlagsTo(EnumSet flags) {
+ if (flags == null) {
+ this.flags = EnumSet.noneOf(BufferFlag.class);
+ } else {
+ this.flags = EnumSet.copyOf(flags);
+ }
+ }
+
+ public void clearFlags() {
+ flags.clear();
+ }
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/BufferFlag.java b/trunk/libsrc/avi/src/org/monte/media/BufferFlag.java
new file mode 100644
index 000000000..92c1e730d
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/BufferFlag.java
@@ -0,0 +1,42 @@
+/*
+ * @(#)BufferFlag.java
+ *
+ * Copyright (c) 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;
+
+/**
+ * {@code BufferFlag}.
+ *
+ * @author Werner Randelshofer
+ * @version $Id: BufferFlag.java 299 2013-01-03 07:40:18Z werner $
+ */
+public enum BufferFlag {
+
+ /** Indicates that the data in this buffer should be ignored. */
+ DISCARD,
+ /** Indicates that this Buffer holds an intra-coded picture, which can be
+ * decoded independently. */
+ KEYFRAME,
+ /** Indicates that the data in this buffer is at the end of the media. */
+ END_OF_MEDIA,
+ /** Indicates that the data in this buffer is used for initializing the
+ * decoding queue.
+ *
+ * This flag is used when the media time of a track is set to a non-keyframe
+ * sample. Thus decoding must start at a keyframe at an earlier time.
+ *
+ * Decoders should decode the buffer.
+ * Encoders and Multiplexers should discard the buffer.
+ */
+ PREFETCH,
+ /** Indicates that this buffer is known to have the same data as the
+ * previous buffer. This may improve encoding performance.
+ */
+ SAME_DATA;
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/Codec.java b/trunk/libsrc/avi/src/org/monte/media/Codec.java
new file mode 100644
index 000000000..908707a8e
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/Codec.java
@@ -0,0 +1,73 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.monte.media;
+
+/**
+ * A {@code Codec} processes a {@code Buffer} and stores the result in another
+ * {@code Buffer}.
+ *
+ * @author Werner Randelshofer
+ * @version 1.0 2011-03-12 Created.
+ */
+public interface Codec {
+
+ /** The codec successfully converted the input to output. */
+ public final static int CODEC_OK = 0;
+ /** The codec could not handle the input. */
+ public final static int CODEC_FAILED = 1;
+ /** The codec did not fully consume the input buffer.
+ * The codec has updated the input buffer to
+ * reflect the amount of data that it has processed.
+ * The codec must be called again with the same input buffer.
+ */
+ public final static int CODEC_INPUT_NOT_CONSUMED = 2;
+ /** The codec did not fully fill the output buffer.
+ * The codec has updated the output buffer to
+ * reflect the amount of data that it has processed.
+ * The codec must be called again with the same output buffer.
+ */
+ public final static int CODEC_OUTPUT_NOT_FILLED = 4;
+
+ /** Lists all of the input formats that this codec accepts. */
+ public Format[] getInputFormats();
+
+ /** Lists all of the output formats that this codec can generate
+ * with the provided input format. If the input format is null, returns
+ * all supported output formats.
+ */
+ public Format[] getOutputFormats(Format input);
+
+ /** Sets the input format.
+ * Returns the format that was actually set. This is the closest format
+ * that the Codec supports. Returns null if the specified format is not
+ * supported and no reasonable match could be found.
+ */
+ public Format setInputFormat(Format input);
+
+ public Format getInputFormat();
+
+ /** Sets the output format.
+ * Returns the format that was actually set. This is the closest format
+ * that the Codec supports. Returns null if the specified format is not
+ * supported and no reasonable match could be found.
+ */
+ public Format setOutputFormat(Format output);
+
+ public Format getOutputFormat();
+
+ /** Performs the media processing defined by this codec.
+ *
+ * Copies the data from the input buffer into the output buffer.
+ *
+ * @return A combination of processing flags.
+ */
+ public int process(Buffer in, Buffer out);
+
+ /** Returns a human readable name of the codec. */
+ public String getName();
+
+ /** Resets the state of the codec. */
+ public void reset();
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/DefaultRegistry.java b/trunk/libsrc/avi/src/org/monte/media/DefaultRegistry.java
new file mode 100644
index 000000000..16496597b
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/DefaultRegistry.java
@@ -0,0 +1,395 @@
+/*
+ * @(#)DefaultRegistry.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;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import static org.monte.media.VideoFormatKeys.*;
+import static org.monte.media.AudioFormatKeys.*;
+
+/**
+ * {@code DefaultRegistry}.
+ *
+ * FIXME - The registry should be read from a file.
+ *
+ * @author Werner Randelshofer
+ * @version $Id: DefaultRegistry.java 299 2013-01-03 07:40:18Z werner $
+ */
+public class DefaultRegistry extends Registry {
+
+ private HashMap> codecMap;
+ private HashMap> readerMap;
+ private HashMap> writerMap;
+ private HashMap fileFormatMap;
+
+ @Override
+ public Format[] getReaderFormats() {
+ return getFileFormats();
+ }
+
+ @Override
+ public Format[] getWriterFormats() {
+ return getFileFormats();
+ }
+
+ @Override
+ public Format[] getFileFormats() {
+ return fileFormatMap.values().toArray(new Format[fileFormatMap.size()]);
+ }
+
+ private static class RegistryEntry {
+
+ Format inputFormat;
+ Format outputFormat;
+ String className;
+
+ public RegistryEntry(Format inputFormat, Format outputFormat, String className) {
+ this.inputFormat = inputFormat;
+ this.outputFormat = outputFormat;
+ this.className = className;
+ }
+ }
+
+ public DefaultRegistry() {
+ }
+
+ @Override
+ protected void init() {
+ codecMap = new HashMap>();
+ readerMap = new HashMap>();
+ writerMap = new HashMap>();
+ fileFormatMap = new HashMap();
+
+ // IFF ANIM
+ // --------
+ putCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_ANIM, EncodingKey, ENCODING_BITMAP_IMAGE),
+ "org.monte.media.anim.BitmapCodec");
+
+ // AVI
+ // --------
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_DIB),
+ "org.monte.media.avi.DIBCodec");
+
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_MJPG),
+ "org.monte.media.jpeg.JPEGCodec");
+
+ putCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_PNG),
+ "org.monte.media.png.PNGCodec");
+
+ putCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_RLE),
+ "org.monte.media.avi.RunLengthCodec");
+
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ "org.monte.media.avi.TechSmithCodec");
+
+ putCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_DOSBOX_SCREEN_CAPTURE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ "org.monte.media.avi.ZMBVCodec");
+
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_PCM),
+ "org.monte.media.avi.AVIPCMAudioCodec");
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_PCM),
+ "org.monte.media.avi.AVIPCMAudioCodec");
+
+ // QuickTime
+ // --------
+ putCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_RAW),
+ "org.monte.media.quicktime.RawCodec");
+
+ putCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_ANIMATION),
+ "org.monte.media.quicktime.AnimationCodec");
+
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_JPEG),
+ "org.monte.media.jpeg.JPEGCodec");
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_MJPG),
+ "org.monte.media.jpeg.JPEGCodec");
+
+ putCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_PNG),
+ "org.monte.media.png.PNGCodec");
+
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME,
+ EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE),
+ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE),
+ "org.monte.media.avi.TechSmithCodec");
+
+ putCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_TWOS_PCM),
+ "org.monte.media.quicktime.QuickTimePCMAudioCodec");
+ putCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_TWOS_PCM),
+ "org.monte.media.quicktime.QuickTimePCMAudioCodec");
+
+ putCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_SOWT_PCM),
+ "org.monte.media.quicktime.QuickTimePCMAudioCodec");
+ putCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_SOWT_PCM),
+ "org.monte.media.quicktime.QuickTimePCMAudioCodec");
+
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN24_PCM),
+ "org.monte.media.quicktime.QuickTimePCMAudioCodec");
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN24_PCM),
+ "org.monte.media.quicktime.QuickTimePCMAudioCodec");
+
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN32_PCM),
+ "org.monte.media.quicktime.QuickTimePCMAudioCodec");
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN32_PCM),
+ "org.monte.media.quicktime.QuickTimePCMAudioCodec");
+
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_RAW_PCM),
+ "org.monte.media.quicktime.QuickTimePCMAudioCodec");
+ putBidiCodec(
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED),
+ new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_RAW_PCM),
+ "org.monte.media.quicktime.QuickTimePCMAudioCodec");
+
+
+ putReader(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI), "org.monte.media.avi.AVIReader");
+ putReader(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_QUICKTIME), "org.monte.media.quicktime.QuickTimeReader");
+ putReader(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_ANIM), "org.monte.media.anim.ANIMReader");
+ putWriter(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI), "org.monte.media.avi.AVIWriter");
+ putWriter(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_QUICKTIME), "org.monte.media.quicktime.QuickTimeWriter");
+ putWriter(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_ANIM), "org.monte.media.anim.ANIMWriter");
+
+ putFileFormat("avi", new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI));
+ putFileFormat("mov", new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_QUICKTIME));
+ putFileFormat("anim", new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_ANIM));
+ }
+
+ /**
+ *
+ * @param inputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}.
+ * @param outputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}.
+ * @param codecClass
+ */
+ public void putBidiCodec(Format inputFormat, Format outputFormat, String codecClass) {
+ putCodec(inputFormat, outputFormat, codecClass);
+ putCodec(outputFormat, inputFormat, codecClass);
+ }
+
+ /**
+ *
+ * @param inputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}.
+ * @param outputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}.
+ * @param codecClass
+ */
+ @Override
+ public void putCodec(Format inputFormat, Format outputFormat, String codecClass) {
+ RegistryEntry entry = new RegistryEntry(inputFormat, outputFormat, codecClass);
+ addCodecEntry(inputFormat.get(EncodingKey), entry);
+ addCodecEntry(outputFormat.get(EncodingKey), entry);
+ }
+
+ private void addCodecEntry(String key, RegistryEntry entry) {
+ LinkedList list = codecMap.get(key);
+ if (list == null) {
+ list = new LinkedList();
+ codecMap.put(key, list);
+ }
+ list.add(entry);
+ }
+
+ /**
+ *
+ * @param fileFormat Must have {@code MediaTypeKey}, {@code MimeTypeKey}.
+ * @param readerClass
+ */
+ @Override
+ public void putReader(Format fileFormat, String readerClass) {
+ RegistryEntry entry = new RegistryEntry(null, fileFormat, readerClass);
+ String key = fileFormat.get(MimeTypeKey);
+ LinkedList list = readerMap.get(key);
+ if (list == null) {
+ list = new LinkedList();
+ readerMap.put(key, list);
+ }
+ list.add(entry);
+ }
+
+ /**
+ *
+ * @param fileFormat Must have {@code MediaTypeKey}, {@code MimeTypeKey}.
+ * @param writerClass
+ */
+ @Override
+ public void putWriter(Format fileFormat, String writerClass) {
+ RegistryEntry entry = new RegistryEntry(fileFormat, null, writerClass);
+ String key = fileFormat.get(MimeTypeKey);
+ LinkedList list = writerMap.get(key);
+ if (list == null) {
+ list = new LinkedList();
+ writerMap.put(key, list);
+ }
+ list.add(entry);
+ }
+
+ @Override
+ public String[] getCodecClasses(Format inputFormat, Format outputFormat) {
+ HashSet classNames = new HashSet();
+ HashSet entries = new HashSet();
+ if (inputFormat != null) {
+ LinkedList re;
+ if (inputFormat.get(EncodingKey) == null) {
+ re = new LinkedList();
+ for (Map.Entry> i : codecMap.entrySet()) {
+ for (RegistryEntry j : i.getValue()) {
+ if (inputFormat.matches(j.inputFormat)) {
+ re.add(j);
+ }
+ }
+ }
+ } else {
+ re = codecMap.get(inputFormat.get(EncodingKey));
+ }
+ if (re != null) {
+ entries.addAll(re);
+ }
+ }
+ if (outputFormat != null) {
+ LinkedList re;
+ if (outputFormat.get(EncodingKey) == null) {
+ re = new LinkedList();
+ for (Map.Entry> i : codecMap.entrySet()) {
+ for (RegistryEntry j : i.getValue()) {
+ if (outputFormat.matches(j.outputFormat)) {
+ re.add(j);
+ }
+ }
+ }
+ } else {
+ re = codecMap.get(outputFormat.get(EncodingKey));
+ }
+ if (re != null) {
+ entries.addAll(re);
+ }
+ }
+ for (RegistryEntry e : entries) {
+ if ((inputFormat == null || e.inputFormat == null || inputFormat.matches(e.inputFormat))
+ && (outputFormat == null || e.outputFormat == null || outputFormat.matches(e.outputFormat))) {
+ classNames.add(e.className);
+ }
+ }
+ return classNames.toArray(new String[classNames.size()]);
+ }
+
+ @Override
+ public String[] getReaderClasses(Format fileFormat) {
+ LinkedList rr = readerMap.get(fileFormat.get(MimeTypeKey));
+ String[] names = new String[rr == null ? 0 : rr.size()];
+ if (rr != null) {
+ int i = 0;
+ for (RegistryEntry e : rr) {
+ names[i++] = e.className;
+ }
+ }
+ return names;
+ }
+
+ @Override
+ public Format getFileFormat(File file) {
+ String ext = file.getName();
+ int p = ext.lastIndexOf('.');
+ if (p != -1) {
+ ext = ext.substring(p + 1);
+ }
+ ext = ext.toLowerCase();
+ return fileFormatMap.get(ext);
+ }
+
+ @Override
+ public String[] getWriterClasses(Format fileFormat) {
+ LinkedList rr = writerMap.get(fileFormat.get(MimeTypeKey));
+ String[] names = new String[rr == null ? 0 : rr.size()];
+ if (rr != null) {
+ int i = 0;
+ for (RegistryEntry e : rr) {
+ names[i++] = e.className;
+ }
+ }
+ return names;
+ }
+
+ @Override
+ public void putFileFormat(String extension, Format format) {
+ fileFormatMap.put(extension.toLowerCase(), format);
+ }
+
+ @Override
+ public String getExtension(Format ff) {
+ for (Map.Entry e : fileFormatMap.entrySet()) {
+ if (e.getValue().get(MimeTypeKey).equals(ff.get(MimeTypeKey))) {
+ return e.getKey();
+ }
+ }
+ return "";
+ }
+
+ @Override
+ public void unregisterCodec(String codecClass) {
+ for (Map.Entry> i:codecMap.entrySet()) {
+ LinkedList ll=i.getValue();
+ for (Iterator j=ll.iterator();j.hasNext();) {
+ RegistryEntry e=j.next();
+ if (e.className.equals(codecClass)) {
+ j.remove();
+ }
+ }
+ }
+ }
+
+
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/Format.java b/trunk/libsrc/avi/src/org/monte/media/Format.java
new file mode 100644
index 000000000..edabcfc04
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/Format.java
@@ -0,0 +1,325 @@
+/*
+ * @(#)Format.java
+ *
+ * Copyright (c) 2011-2012 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;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * Specifies the format of a media, for example of audio and video.
+ *
+ * @author Werner Randelshofer
+ * @version $Id: Format.java 299 2013-01-03 07:40:18Z werner $
+ */
+public class Format {
+
+ /**
+ * Holds the properties of the format.
+ */
+ private HashMap properties;
+
+ /**
+ * Creates a new format onlyWith the specified properties.
+ */
+ public Format(Map properties) {
+ this(properties, true);
+ }
+
+ /**
+ * Creates a new format onlyWith the specified properties.
+ */
+ private Format(Map properties, boolean copy) {
+ if (copy || ! (properties instanceof HashMap)) {
+ for (Map.Entry e : properties.entrySet()) {
+ if (!e.getKey().isAssignable(e.getValue())) {
+ throw new ClassCastException(e.getValue() + " must be of type " + e.getKey().getValueClass());
+ }
+ }
+ this.properties = new HashMap< FormatKey, Object>(properties);
+ } else {
+ this.properties = (HashMap< FormatKey, Object>) properties;
+ }
+ }
+
+ /**
+ * Creates a new format onlyWith the specified properties. The properties
+ * must be given as key value pairs.
+ */
+ public Format(Object... p) {
+ this.properties = new HashMap< FormatKey, Object>();
+ for (int i = 0; i < p.length; i += 2) {
+ FormatKey key = (FormatKey) p[i];
+ if (!key.isAssignable(p[i + 1])) {
+ throw new ClassCastException(key + ": " + p[i + 1] + " must be of type " + key.getValueClass());
+ }
+ this.properties.put(key, p[i + 1]);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public T get(FormatKey key) {
+ return (T) properties.get(key);
+ }
+
+ @SuppressWarnings("unchecked")
+ public T get(FormatKey key, T defaultValue) {
+ return (properties.containsKey(key)) ? (T) properties.get(key) : defaultValue;
+ }
+
+ public boolean containsKey(FormatKey key) {
+ return properties.containsKey(key);
+ }
+
+ /**
+ * Gets the properties of the format as an unmodifiable map.
+ */
+ public Map getProperties() {
+ return Collections.unmodifiableMap(properties);
+ }
+
+ /**
+ * Gets the keys of the format as an unmodifiable set.
+ */
+ public Set getKeys() {
+ return Collections.unmodifiableSet(properties.keySet());
+ }
+
+ /**
+ * Returns true if that format matches this format. That is iff all
+ * properties defined in both format objects are identical. Properties which
+ * are only defined in one of the format objects are not considered.
+ *
+ * @param that Another format.
+ * @return True if the other format matches this format.
+ */
+ public boolean matches(Format that) {
+ for (Map.Entry e : properties.entrySet()) {
+ if (!e.getKey().isComment()) {
+ if (that.properties.containsKey(e.getKey())) {
+ Object a = e.getValue();
+ Object b = that.properties.get(e.getKey());
+ if (a != b && a == null || !a.equals(b)) {
+ return false;
+ }
+
+ }
+ }
+ }
+ return true;
+ }
+
+ public boolean matchesWithout(Format that, FormatKey... without) {
+ OuterLoop:
+ for (Map.Entry e : properties.entrySet()) {
+ FormatKey k = e.getKey();
+ if (!e.getKey().isComment()) {
+ if (that.properties.containsKey(k)) {
+ for (int i = 0; i < without.length; i++) {
+ if (without[i] == k) {
+ continue OuterLoop;
+ }
+ }
+ Object a = e.getValue();
+ Object b = that.properties.get(k);
+ if (a != b && a == null || !a.equals(b)) {
+ return false;
+ }
+
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Creates a new format which contains all properties from this format and
+ * additional properties from that format.
If a property is specified in
+ * both formats, then the property value from this format is used. It
+ * overwrites that format.
If one of the format has more properties than
+ * the other, then the new format is more specific than this format.
+ *
+ * @param that
+ * @return That format with properties overwritten by this format.
+ */
+ public Format append(Format that) {
+ HashMap m = new HashMap(this.properties);
+ for (Map.Entry e : that.properties.entrySet()) {
+ if (!m.containsKey(e.getKey())) {
+ m.put(e.getKey(), e.getValue());
+ }
+ }
+ return new Format(m,false);
+ }
+
+ /**
+ * Creates a new format which contains all properties from this format and
+ * additional properties listed.
If a property is specified in both
+ * formats, then the property value from this format is used. It overwrites
+ * that format.
If one of the format has more properties than the other,
+ * then the new format is more specific than this format.
+ *
+ * @param p The properties must be given as key value pairs.
+ * @return That format with properties overwritten by this format.
+ */
+ public Format append(Object... p) {
+ HashMap m = new HashMap(this.properties);
+ for (int i = 0; i < p.length; i += 2) {
+ FormatKey key = (FormatKey) p[i];
+ if (!key.isAssignable(p[i + 1])) {
+ throw new ClassCastException(key + ": " + p[i + 1] + " must be of type " + key.getValueClass());
+ }
+ m.put(key, p[i + 1]);
+ }
+ return new Format(m,false);
+ }
+
+ /**
+ * Creates a new format which contains all properties from the specified
+ * format and additional properties from this format.
+ *
If a property is specified in both formats, then the property value
+ * from this format is used. It overwrites that format.
+ *
If one of the format has more properties than the other, then the new
+ * format is more specific than this format.
+ *
+ * @param that
+ * @return That format with properties overwritten by this format.
+ */
+ public Format prepend(Format that) {
+ HashMap m = new HashMap(that.properties);
+ for (Map.Entry e : this.properties.entrySet()) {
+ if (!m.containsKey(e.getKey())) {
+ m.put(e.getKey(), e.getValue());
+ }
+ }
+ return new Format(m,false);
+ }
+
+ /**
+ * Creates a new format which contains all specified properties and
+ * additional properties from this format.
+ *
If a property is specified in both formats, then the property value
+ * from this format is used. It overwrites that format.
+ *
If one of the format has more properties than the other, then the new
+ * format is more specific than this format.
+ *
+ * @param p The properties must be given as key value pairs.
+ * @return That format with properties overwritten by this format.
+ */
+ public Format prepend(Object... p) {
+ HashMap m = new HashMap();
+ for (int i = 0; i < p.length; i += 2) {
+ FormatKey key = (FormatKey) p[i];
+ if (!key.isAssignable(p[i + 1])) {
+ throw new ClassCastException(key + ": " + p[i + 1] + " must be of type " + key.getValueClass());
+ }
+ m.put(key, p[i + 1]);
+ }
+ for (Map.Entry e : this.properties.entrySet()) {
+ if (!m.containsKey(e.getKey())) {
+ m.put(e.getKey(), e.getValue());
+ }
+ }
+ return new Format(m,false);
+ }
+ /**
+ * Creates a new format which only has the specified keys (or less).
If
+ * the keys are reduced, then the new format is less specific than this
+ * format.
+ */
+ public Format intersectKeys(FormatKey... keys) {
+ HashMap m = new HashMap();
+ for (FormatKey k : keys) {
+ if (properties.containsKey(k)) {
+ m.put(k, properties.get(k));
+ }
+ }
+ return new Format(m,false);
+ }
+
+ /**
+ * Creates a new format without the specified keys.
If the keys are
+ * reduced, then the new format is less specific than this format.
+ */
+ public Format removeKeys(FormatKey... keys) {
+ boolean needsRemoval = false;
+ for (FormatKey k : keys) {
+ if (properties.containsKey(k)) {
+ needsRemoval = true;
+ break;
+ }
+ }
+ if (!needsRemoval) {
+ return this;
+ }
+
+ HashMap m = new HashMap(properties);
+ for (FormatKey k : keys) {
+ m.remove(k);
+ }
+ return new Format(m,false);
+ }
+
+ /**
+ * Returns true if the format has the specified keys.
+ */
+ public Format containsKeys(FormatKey... keys) {
+ HashMap m = new HashMap(properties);
+ for (FormatKey k : keys) {
+ m.remove(k);
+ }
+ return new Format(m,false);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder("Format{");
+ boolean isFirst = true;
+ for (Map.Entry e : properties.entrySet()) {
+ if (isFirst) {
+ isFirst = false;
+ } else {
+ buf.append(',');
+ }
+ buf.append(e.getKey().toString());
+ buf.append(':');
+ appendStuffedString(e.getValue(), buf);
+ }
+ buf.append('}');
+ return buf.toString();
+ }
+
+ /**
+ * This method is used by #toString.
+ */
+ private static void appendStuffedString(Object value, StringBuilder stuffed) {
+ if (value == null) {
+ stuffed.append("null");
+ }
+ value = value.toString();
+ if (value instanceof String) {
+ for (char ch : ((String) value).toCharArray()) {
+ if (ch >= ' ') {
+ stuffed.append(ch);
+ } else {
+ String hex = Integer.toHexString(ch);
+ stuffed.append("\\u");
+ for (int i = hex.length(); i < 4; i++) {
+ stuffed.append('0');
+ }
+ stuffed.append(hex);
+ }
+ }
+ }
+ }
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/FormatKey.java b/trunk/libsrc/avi/src/org/monte/media/FormatKey.java
new file mode 100644
index 000000000..374d76ca0
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/FormatKey.java
@@ -0,0 +1,112 @@
+/*
+ * @(#)FormatKey.java
+ *
+ * Copyright (c) 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;
+
+import java.io.Serializable;
+
+/**
+ * A FormatKey provides type-safe access to an attribute of
+ * a {@link Format}.
+ *
+ * A format key has a name, a type and a value.
+ *
+ * @author Werner Randelshofer
+ * @version $Id: FormatKey.java 299 2013-01-03 07:40:18Z werner $
+ */
+public class FormatKey implements Serializable, Comparable {
+
+ public static final long serialVersionUID = 1L;
+ /**
+ * Holds a String representation of the attribute key.
+ */
+ private String key;
+ /**
+ * Holds a pretty name. This can be null, if the value is self-explaining.
+ */
+ private String name;
+ /** This variable is used as a "type token" so that we can check for
+ * assignability of attribute values at runtime.
+ */
+ private Class clazz;
+
+ /** Comment keys are ignored when matching two media formats with each other. */
+ private boolean comment;
+
+ /** Creates a new instance with the specified attribute key, type token class,
+ * default value null, and allowing null values. */
+ public FormatKey(String key, Class clazz) {
+ this(key, key, clazz);
+ }
+
+ /** Creates a new instance with the specified attribute key, type token class,
+ * default value null, and allowing null values. */
+ public FormatKey(String key, String name, Class clazz) {
+ this(key,name,clazz,false);
+ }
+ /** Creates a new instance with the specified attribute key, type token class,
+ * default value null, and allowing null values. */
+ public FormatKey(String key, String name, Class clazz, boolean comment) {
+ this.key = key;
+ this.name = name;
+ this.clazz = clazz;
+ this.comment=comment;
+ }
+
+ /**
+ * Returns the key string.
+ * @return key string.
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Returns the pretty name string.
+ * @return name string.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /** Returns the key string. */
+ @Override
+ public String toString() {
+ return key;
+ }
+
+ /**
+ * Returns true if the specified value is assignable with this key.
+ *
+ * @param value
+ * @return True if assignable.
+ */
+ public boolean isAssignable(Object value) {
+ return clazz.isInstance(value);
+ }
+
+ public boolean isComment() {
+ return comment;
+ }
+
+
+ public Class getValueClass() {
+ return clazz;
+ }
+
+ @Override
+ public int compareTo(Object o) {
+ return compareTo((FormatKey) o);
+ }
+
+ public int compareTo(FormatKey that) {
+ return this.key.compareTo(that.key);
+ }
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/FormatKeys.java b/trunk/libsrc/avi/src/org/monte/media/FormatKeys.java
new file mode 100644
index 000000000..71dff637f
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/FormatKeys.java
@@ -0,0 +1,61 @@
+/*
+ * @(#)FormatKeys.java
+ *
+ * Copyright (c) 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;
+
+import org.monte.media.math.Rational;
+
+/**
+ * Defines common {@code FormatKey}'s.
+ *
+ * @author Werner Randelshofer
+ * @version $Id: FormatKeys.java 299 2013-01-03 07:40:18Z werner $
+ */
+public class FormatKeys {
+ public static enum MediaType {
+ AUDIO,
+ VIDEO,
+ MIDI,
+ TEXT,
+ META,
+ FILE
+ }
+ /**
+ * The media MediaTypeKey.
+ */
+ public final static FormatKey MediaTypeKey = new FormatKey("mediaType", MediaType.class);
+ /**
+ * The EncodingKey.
+ */
+ public final static FormatKey EncodingKey = new FormatKey("encoding", String.class);
+
+ //
+ public final static String MIME_AVI = "video/avi";
+ public final static String MIME_QUICKTIME = "video/quicktime";
+ public final static String MIME_MP4 = "video/mp4";
+ public final static String MIME_JAVA = "Java";
+ public final static String MIME_ANIM = "x-iff/anim";
+ public final static String MIME_IMAGE_SEQUENCE = "ImageSequence";
+ /**
+ * The mime type.
+ */
+ public final static FormatKey MimeTypeKey = new FormatKey("mimeType", String.class);
+ /**
+ * The number of frames per second.
+ */
+ public final static FormatKey FrameRateKey = new FormatKey("frameRate", Rational.class);
+
+ /**
+ * The interval between key frames.
+ * If this value is not specified, most codecs will use {@code FrameRateKey}
+ * as a hint and try to produce one key frame per second.
+ */
+ public final static FormatKey KeyFrameIntervalKey = new FormatKey("keyFrameInterval", Integer.class);
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/MovieReader.java b/trunk/libsrc/avi/src/org/monte/media/MovieReader.java
new file mode 100644
index 000000000..0370eea0c
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/MovieReader.java
@@ -0,0 +1,93 @@
+/*
+ * @(#)MovieReader.java
+ *
+ * Copyright (c) 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;
+
+import org.monte.media.math.Rational;
+import java.io.IOException;
+
+/**
+ * A simple API for reading movie data (audio and video) from a file.
+ *
+ *
+ * FIXME - MovieReader should extend Demultiplexer
+ *
+ * @author Werner Randelshofer
+ * @version $Id: MovieReader.java 299 2013-01-03 07:40:18Z werner $
+ */
+public interface MovieReader {
+ /** Returns the number of tracks. */
+ public int getTrackCount() throws IOException;
+
+ /** Finds a track with the specified format.
+ *
+ * @param fromTrack the start track number.
+ * @param format A format specification.
+ * @return The track number >= fromTrack or -1 if no track has been found.
+ */
+ public int findTrack(int fromTrack, Format format) throws IOException;
+
+ /** Returns the total duration of the movie . */
+ public Rational getDuration() throws IOException;
+ /** Returns the duration of the specified track. */
+ public Rational getDuration(int track) throws IOException;
+
+ /** Returns the sample number for the specified time. */
+ public long timeToSample(int track, Rational seconds) throws IOException;
+ /** Returns the time for the specified sample number. */
+ public Rational sampleToTime(int track, long sample) throws IOException;
+
+ /** Returns the file format. */
+ public Format getFileFormat() throws IOException;
+
+ /** Returns the media format of the specified track.
+ *
+ * @param track Track number.
+ * @return The media format of the track.
+ */
+ public Format getFormat(int track) throws IOException;
+
+ /** Returns the number of media data chunks in the specified track.
+ * A chunk contains one or more samples.
+ */
+ public long getChunkCount(int track) throws IOException;
+
+ /** Reads the next sample chunk from the specified track.
+ *
+ * @param track Track number.
+ * @param buffer The buffer into which to store the sample data.
+ */
+ public void read(int track, Buffer buffer) throws IOException;
+ /** Reads the next sample chunk from the next track in playback sequence.
+ * The variable buffer.track contains the track number.
+ *
+ * @param buf The buffer into which to store the sample data.
+ */
+ //public void read(Buffer buffer) throws IOException;
+
+ /** Returns the index of the next track in playback sequence.
+ *
+ * @return Index of next track or -1 if end of media reached.
+ */
+ public int nextTrack() throws IOException;
+
+ public void close() throws IOException;
+
+ /** Sets the read time of all tracks to the closest sync sample before or
+ * at the specified time.
+ *
+ * @param newValue Time in seconds.
+ */
+ public void setMovieReadTime(Rational newValue) throws IOException;
+
+ /** Returns the current time of the track. */
+ public Rational getReadTime(int track) throws IOException;
+
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/MovieWriter.java b/trunk/libsrc/avi/src/org/monte/media/MovieWriter.java
new file mode 100644
index 000000000..e4fd559cf
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/MovieWriter.java
@@ -0,0 +1,82 @@
+/*
+ * @(#)MovieWriter.java
+ *
+ * Copyright (c) 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;
+
+import org.monte.media.math.Rational;
+import java.io.IOException;
+
+/**
+ * A simple API for writing movie data (audio and video) into a file.
+ *
+ * @author Werner Randelshofer
+ * @version $Id: MovieWriter.java 299 2013-01-03 07:40:18Z werner $
+ */
+public interface MovieWriter extends Multiplexer {
+ /** Returns the file format. */
+ public Format getFileFormat() throws IOException;
+
+ /** Adds a track to the writer for a suggested input format.
+ *
+ * The format should at least specify the desired {@link FormatKeys.MediaType}.
+ * The actual input format is a refined version of the suggested format. For
+ * example, if a MovieWriter only supports fixed frame rate video, then the
+ * MovieWriter will extend the format with that information.
+ *
+ * If the suggested input format is not compatible, then an IOException is
+ * thrown. For example, if a MovieWriter only supports fixed frame rate video,
+ * but a format with variable frame rate was requested.
+ *
+ * @param format The desired input format of the track. The actual input
+ * format may be a refined version of the specified format.
+ * @return The track number.
+ */
+ public int addTrack(Format format) throws IOException;
+
+ /** Returns the media format of the specified track.
+ * This is a refined version of the format that was requested when the
+ * track was added. See {@link #addTrack}.
+ *
+ * @param track Track number.
+ * @return The media format of the track.
+ */
+ public Format getFormat(int track);
+
+ /** Returns the number of tracks. */
+ public int getTrackCount();
+
+ /** Writes a sample 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 the sample data.
+ */
+ @Override
+ public void write(int track, Buffer buf) throws IOException;
+
+ /** Closes the writer. */
+ @Override
+ public void close() throws IOException;
+
+ /** Returns true if the limit for media data has been reached.
+ * If this limit is reached, no more samples should be added to the movie.
+ *
+ * This limit is imposed by data structures of the movie file
+ * which will overflow if more samples are added to the movie.
+ *
+ * FIXME - Maybe replace by getCapacity():long.
+ */
+ public boolean isDataLimitReached();
+
+ /** Returns the duration of the track in seconds. */
+ public Rational getDuration(int track);
+ /** Returns true if the specified track has no samples. */
+ public boolean isEmpty(int track);
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/Multiplexer.java b/trunk/libsrc/avi/src/org/monte/media/Multiplexer.java
new file mode 100644
index 000000000..04311c5d9
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/Multiplexer.java
@@ -0,0 +1,34 @@
+/*
+ * @(#)Multiplexer.java 1.0 2011-02-19
+ *
+ * Copyright (c) 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;
+
+import java.io.IOException;
+
+/**
+ * A {@code Multiplexer} can write multiple media tracks into a
+ * single output stream.
+ *
+ * @author Werner Randelshofer
+ * @version 1.0 2011-02-19 Created.
+ */
+public interface Multiplexer {
+ /** Writes a sample.
+ * Does nothing if the discard-flag in the buffer is set to true.
+ *
+ * @param track The track number.
+ * @param buf The buffer containing the sample data.
+ */
+ public void write(int track, Buffer buf) throws IOException;
+
+ /** Closes the Multiplexer. */
+ public void close() throws IOException;
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/ParseException.java b/trunk/libsrc/avi/src/org/monte/media/ParseException.java
new file mode 100644
index 000000000..7aee64949
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/ParseException.java
@@ -0,0 +1,30 @@
+/*
+ * @(#)ParseException.java
+ *
+ * Copyright (c) 1999-2012 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;
+
+/**
+ * Exception thrown by IFFParse.
+ *
+ * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland
+ * @version $Id: ParseException.java 299 2013-01-03 07:40:18Z werner $
+ */
+public class ParseException extends Exception {
+
+ public static final long serialVersionUID = 1L;
+
+ public ParseException(String message) {
+ super(message);
+ }
+
+ public ParseException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/Registry.java b/trunk/libsrc/avi/src/org/monte/media/Registry.java
new file mode 100644
index 000000000..e0f660a93
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/Registry.java
@@ -0,0 +1,310 @@
+/*
+ * @(#)Registry.java
+ *
+ * Copyright (c) 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;
+
+import java.io.File;
+import java.util.ArrayList;
+import static org.monte.media.FormatKeys.*;
+
+/**
+ * The {@code Registry} for audio and video codecs.
+ *
+ * @author Werner Randelshofer
+ * @version $Id: Registry.java 299 2013-01-03 07:40:18Z werner $
+ */
+public abstract class Registry {
+
+ private static Registry instance;
+
+ public static Registry getInstance() {
+ if (instance == null) {
+ instance = new DefaultRegistry();
+ instance.init();
+ }
+ return instance;
+ }
+
+ /**
+ * Initializes the registry.
+ */
+ protected abstract void init();
+
+ /**
+ * Puts a codec into the registry.
+ *
+ * @param inputFormat The input format. Must not be null.
+ * @param outputFormat The output format. Must not be null.
+ * @param codecClass The codec class name. Must not be null.
+ */
+ public abstract void putCodec(Format inputFormat, Format outputFormat, String codecClass);
+
+ /**
+ * Gets all codecs which can decode the specified format.
+ *
+ * @param format The format.
+ * @return An array of codec class names. If no codec was found, an empty
+ * array is returned.
+ */
+ public final String[] getDecoderClasses(Format format) {
+ return getCodecClasses(format, null);
+ }
+
+ /**
+ * Gets all codecs which can decode the specified format.
+ *
+ * @param format The format.
+ * @return An array of codec class names. If no codec was found, an empty
+ * array is returned.
+ */
+ public final String[] getEncoderClasses(Format format) {
+ return getCodecClasses(null, format);
+ }
+
+ /**
+ * Gets all codecs which can transcode from the specified input format to
+ * the specified output format.
+ *
+ * @param inputFormat The input format.
+ * @param outputFormat The output format.
+ * @return An array of codec class names. If no codec was found, an empty
+ * array is returned.
+ */
+ public abstract String[] getCodecClasses(//
+ Format inputFormat,
+ Format outputFormat);
+
+ /**
+ * Gets all codecs which can decode the specified format.
+ *
+ * @param inputFormat The input format.
+ * @return An array of codec class names. If no codec was found, an empty
+ * array is returned.
+ */
+ public final Codec[] getDecoders(Format inputFormat) {
+ return getCodecs(inputFormat, null);
+ }
+
+ /**
+ * Gets the first codec which can decode the specified format.
+ *
+ * @param inputFormat The output format.
+ * @return A codec. Returns null if no codec was found.
+ */
+ public Codec getDecoder(Format inputFormat) {
+ return getCodec(inputFormat, null);
+ }
+
+ /**
+ * Gets all codecs which can encode the specified format.
+ *
+ * @param outputFormat The output format.
+ * @return An array of codecs. If no codec was found, an empty array is
+ * returned.
+ */
+ public final Codec[] getEncoders(Format outputFormat) {
+ return getCodecs(null, outputFormat);
+ }
+
+ /**
+ * Gets the first codec which can encode the specified foramt.
+ *
+ * @param outputFormat The output format.
+ * @return A codec. Returns null if no codec was found.
+ */
+ public Codec getEncoder(Format outputFormat) {
+ return getCodec(null, outputFormat);
+ }
+
+ /**
+ * Gets all codecs which can transcode from the specified input format to
+ * the specified output format.
+ *
+ * @param inputFormat The input format.
+ * @param outputFormat The output format.
+ * @return An array of codec class names. If no codec was found, an empty
+ * array is returned.
+ */
+ public Codec[] getCodecs(Format inputFormat, Format outputFormat) {
+ String[] clazz = getCodecClasses(inputFormat, outputFormat);
+ ArrayList codecs = new ArrayList(clazz.length);
+ for (int i = 0; i < clazz.length; i++) {
+ try {
+ codecs.add((Codec) Class.forName(clazz[i]).newInstance());
+ } catch (Exception ex) {
+ //ex.printStackTrace();
+ System.err.println("Monte Registry. Codec class not found: " + clazz[i]);
+ unregisterCodec(clazz[i]);
+ }
+ }
+ return codecs.toArray(new Codec[codecs.size()]);
+ }
+
+ /**
+ * Gets a codec which can transcode from the specified input format to the
+ * specified output format.
+ *
+ * @param inputFormat The input format.
+ * @param outputFormat The output format.
+ * @return A codec or null.
+ */
+ public Codec getCodec(Format inputFormat, Format outputFormat) {
+ String[] clazz = getCodecClasses(inputFormat, outputFormat);
+ for (int i = 0; i < clazz.length; i++) {
+ try {
+ Codec codec = ((Codec) Class.forName(clazz[i]).newInstance());
+ codec.setInputFormat(inputFormat);
+ if (outputFormat != null) {
+ codec.setOutputFormat(outputFormat);
+ }
+ return codec;
+ } catch (Exception ex) {
+ //ex.printStackTrace();
+ System.err.println("Monte Registry. Codec class not found: " + clazz[i]);
+ unregisterCodec(clazz[i]);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Puts a reader into the registry.
+ *
+ * @param fileFormat The file format, e.g."video/avi", "video/quicktime".
+ * Use "Java" for formats which are not tied to a file format. Must not be
+ * null.
+ * @param readerClass The reader class name. Must not be null.
+ */
+ public abstract void putReader(Format fileFormat, String readerClass);
+
+ /**
+ * Puts a writer into the registry.
+ *
+ * @param fileFormat The file format, e.g."video/avi", "video/quicktime".
+ * Use "Java" for formats which are not tied to a file format. Must not be
+ * null.
+ * @param writerClass The writer class name. Must not be null.
+ */
+ public abstract void putWriter(Format fileFormat, String writerClass);
+
+ /**
+ * Gets all reader class names from the registry for the specified file
+ * format.
+ *
+ * @param fileFormat The file format, e.g."AVI", "QuickTime".
+ * @return The reader class names.
+ */
+ public abstract String[] getReaderClasses(Format fileFormat);
+
+ /**
+ * Gets all writer class names from the registry for the specified file
+ * format.
+ *
+ * @param fileFormat The file format, e.g."AVI", "QuickTime".
+ * @return The writer class names.
+ */
+ public abstract String[] getWriterClasses(Format fileFormat);
+
+ public MovieReader getReader(Format fileFormat, File file) {
+ String[] clazz = getReaderClasses(fileFormat);
+ for (int i = 0; i < clazz.length; i++) {
+ try {
+ return ((MovieReader) Class.forName(clazz[i]).getConstructor(File.class).newInstance(file));
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ public MovieWriter getWriter(File file) {
+ Format format = getFileFormat(file);
+ return format == null ? null : getWriter(format, file);
+ }
+
+ public MovieWriter getWriter(Format fileFormat, File file) {
+ String[] clazz = getWriterClasses(fileFormat);
+ for (int i = 0; i < clazz.length; i++) {
+ try {
+ return ((MovieWriter) Class.forName(clazz[i]).getConstructor(File.class).newInstance(file));
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ public MovieReader getReader(File file) {
+ Format format = getFileFormat(file);
+ return format == null ? null : getReader(format, file);
+ }
+
+ public abstract void putFileFormat(String extension, Format format);
+
+ public abstract Format getFileFormat(File file);
+
+ public abstract Format[] getReaderFormats();
+
+ public abstract Format[] getWriterFormats();
+
+ public abstract Format[] getFileFormats();
+
+ public abstract String getExtension(Format ff);
+
+ /**
+ * Suggests output formats for the given input media format and specified
+ * file format.
+ *
+ * @param inputMediaFormat
+ * @param outputFileFormat
+ * @return List of output media formats.
+ */
+ public ArrayList suggestOutputFormats(Format inputMediaFormat, Format outputFileFormat) {
+ ArrayList formats = new ArrayList();
+ Format matchFormat = new Format(//
+ MimeTypeKey, outputFileFormat.get(MimeTypeKey),//
+ MediaTypeKey, inputMediaFormat.get(MediaTypeKey));
+ Codec[] codecs = getEncoders(matchFormat);
+ int matchingCount = 0;
+ for (Codec c : codecs) {
+ for (Format mf : c.getOutputFormats(null)) {
+ if (mf.matches(matchFormat)) {
+ if (inputMediaFormat.matchesWithout(mf, MimeTypeKey)) {
+ // add matching formats first
+ formats.add(0, mf.append(inputMediaFormat));
+ matchingCount++;
+ } else if (inputMediaFormat.matchesWithout(mf, MimeTypeKey, EncodingKey)) {
+ // add formats which match everything but the encoding second
+ formats.add(matchingCount, mf.append(inputMediaFormat));
+ } else {
+ // add remaining formats last
+ formats.add(mf.append(inputMediaFormat));
+ }
+ }
+ }
+ }
+
+ // remove duplicates
+ for (int i = formats.size() - 1; i >= 0; i--) {
+ Format fi = formats.get(i);
+ for (int j = i - 1; j >= 0; j--) {
+ Format fj = formats.get(j);
+ if (fi.matches(fj)) {
+ formats.remove(i);
+ break;
+ }
+ }
+ }
+
+ return formats;
+ }
+
+ public abstract void unregisterCodec(String codecClass);
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/VideoFormatKeys.java b/trunk/libsrc/avi/src/org/monte/media/VideoFormatKeys.java
new file mode 100644
index 000000000..2bb943c3b
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/VideoFormatKeys.java
@@ -0,0 +1,77 @@
+/*
+ * @(#)VideoFormatKeys.java
+ *
+ * Copyright (c) 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;
+
+import org.monte.media.math.Rational;
+
+/**
+ * Defines common format keys for video media.
+ *
+ * @author Werner Randelshofer
+ * @version $Id: VideoFormatKeys.java 299 2013-01-03 07:40:18Z werner $
+ */
+public class VideoFormatKeys extends FormatKeys {
+ // Standard video ENCODING strings for use with FormatKey.Encoding.
+ public static final String ENCODING_BUFFERED_IMAGE = "image";
+ /** Cinepak format. */
+ public static final String ENCODING_QUICKTIME_CINEPAK = "cvid";
+ public static final String COMPRESSOR_NAME_QUICKTIME_CINEPAK = "Cinepak";
+ /** JPEG format. */
+ public static final String ENCODING_QUICKTIME_JPEG = "jpeg";
+ public static final String COMPRESSOR_NAME_QUICKTIME_JPEG = "Photo - JPEG";
+ /** PNG format. */
+ public static final String ENCODING_QUICKTIME_PNG = "png ";
+ public static final String COMPRESSOR_NAME_QUICKTIME_PNG = "PNG";
+ /** Animation format. */
+ public static final String ENCODING_QUICKTIME_ANIMATION = "rle ";
+ public static final String COMPRESSOR_NAME_QUICKTIME_ANIMATION = "Animation";
+ /** Raw format. */
+ public static final String ENCODING_QUICKTIME_RAW = "raw ";
+ public static final String COMPRESSOR_NAME_QUICKTIME_RAW = "NONE";
+ // AVI Formats
+ /** Microsoft Device Independent Bitmap (DIB) format. */
+ public static final String ENCODING_AVI_DIB = "DIB ";
+ /** Microsoft Run Length format. */
+ public static final String ENCODING_AVI_RLE = "RLE ";
+ /** Techsmith Screen Capture format. */
+ public static final String ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE = "tscc";
+ public static final String COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE = "Techsmith Screen Capture";
+ /** DosBox Screen Capture format. */
+ public static final String ENCODING_AVI_DOSBOX_SCREEN_CAPTURE = "ZMBV";
+ /** JPEG format. */
+ public static final String ENCODING_AVI_MJPG = "MJPG";
+ /** PNG format. */
+ public static final String ENCODING_AVI_PNG = "png ";
+ /** Interleaved planar bitmap format. */
+ public static final String ENCODING_BITMAP_IMAGE = "ILBM";
+
+ //
+
+ /** The WidthKey of a video frame. */
+ public final static FormatKey WidthKey = new FormatKey("dimX","width", Integer.class);
+ /** The HeightKey of a video frame. */
+ public final static FormatKey HeightKey = new FormatKey("dimY","height", Integer.class);
+ /** The number of bits per pixel. */
+ public final static FormatKey DepthKey = new FormatKey("dimZ","depth", Integer.class);
+ /** The data class. */
+ public final static FormatKey DataClassKey = new FormatKey("dataClass", Class.class);
+ /** The compressor name. */
+ public final static FormatKey CompressorNameKey = new FormatKey("compressorName", "compressorName",String.class, true);
+ /** The pixel aspect ratio WidthKey : HeightKey;
+ */
+ public final static FormatKey PixelAspectRatioKey = new FormatKey("pixelAspectRatio", Rational.class);
+ /** Whether the frame rate must be fixed. False means variable frame rate. */
+ public final static FormatKey FixedFrameRateKey = new FormatKey("fixedFrameRate", Boolean.class);
+ /** Whether the video is interlaced. */
+ public final static FormatKey InterlaceKey = new FormatKey("interlace", Boolean.class);
+ /** Encoding quality. Value between 0 and 1. */
+ public final static FormatKey QualityKey = new FormatKey("quality", Float.class);
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/AVIBMPDIB.java b/trunk/libsrc/avi/src/org/monte/media/avi/AVIBMPDIB.java
new file mode 100644
index 000000000..4f9a68bee
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/avi/AVIBMPDIB.java
@@ -0,0 +1,106 @@
+/*
+ * @(#)AVIBMPDIB.java 1.0 2009-12-30
+ *
+ * Copyright (c) 2009 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.ImageInputStreamAdapter;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.SequenceInputStream;
+import java.util.*;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.MemoryCacheImageInputStream;
+
+/**
+ * This class defines the JPEG Huffman table, which is omitted in AVI MJPEG
+ * files.
+ *
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.
For detailed information about the AVI 1.0 file format
+ * see: msdn.microsoft.com
+ * AVI RIFF www.microsoft.com
+ * FOURCC for Video Compression www.saettler.com
+ * RIFF
+ *
+ * @author Werner Randelshofer
+ * @version $Id: AVIOutputStream.java 306 2013-01-04 16:19:29Z werner $
+ */
+public class AVIOutputStream extends AbstractAVIStream {
+
+ /**
+ * The states of the movie output stream.
+ */
+ protected static enum States {
+
+ STARTED, FINISHED, CLOSED;
+ }
+ /**
+ * The current state of the movie output stream.
+ */
+ protected States state = States.FINISHED;
+ /**
+ * This chunk holds the whole AVI content.
+ */
+ protected CompositeChunk aviChunk;
+ /**
+ * This chunk holds the movie frames.
+ */
+ protected CompositeChunk moviChunk;
+ /**
+ * This chunk holds the AVI Main Header.
+ */
+ protected FixedSizeDataChunk avihChunk;
+ ArrayList idx1 = new ArrayList();
+
+ /**
+ * Creates a new instance.
+ *
+ * @param file the output file
+ */
+ public AVIOutputStream(File file) throws IOException {
+ if (file.exists()) {
+ file.delete();
+ }
+ this.out = new FileImageOutputStream(file);
+ out.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ this.streamOffset = 0;
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param out the output stream.
+ */
+ public AVIOutputStream(ImageOutputStream out) throws IOException {
+ this.out = out;
+ this.streamOffset = out.getStreamPosition();
+ out.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ /**
+ * Adds a video track.
+ *
+ * @param fccHandler The 4-character code of the format.
+ * @param scale The numerator of the sample rate.
+ * @param rate The denominator of the sample rate.
+ * @param width The width of a video image. Must be greater than 0.
+ * @param height The height of a video image. Must be greater than 0.
+ * @param depth The number of bits per pixel. Must be greater than 0.
+ * @param syncInterval Interval for sync-samples. 0=automatic. 1=all frames
+ * are keyframes. Values larger than 1 specify that for every n-th frame is
+ * a keyframe.
+ *
+ * @return Returns the track index.
+ *
+ * @throws IllegalArgumentException if the width or the height is smaller
+ * than 1.
+ */
+ public int addVideoTrack(String fccHandler, long scale, long rate, int width, int height, int depth, int syncInterval) throws IOException {
+ ensureFinished();
+ if (fccHandler == null || fccHandler.length() != 4) {
+ throw new IllegalArgumentException("fccHandler must be 4 characters long:" + fccHandler);
+ }
+ VideoTrack vt = new VideoTrack(tracks.size(), typeToInt(fccHandler),//
+ new Format(MediaTypeKey, MediaType.VIDEO,
+ MimeTypeKey, MIME_AVI,
+ EncodingKey, fccHandler,
+ DataClassKey, byte[].class,
+ WidthKey, width, HeightKey, height, DepthKey, depth,
+ FixedFrameRateKey, true,
+ FrameRateKey, new Rational(rate, scale)));
+ vt.scale = scale;
+ vt.rate = rate;
+ vt.syncInterval = syncInterval;
+ vt.frameLeft = 0;
+ vt.frameTop = 0;
+ vt.frameRight = width;
+ vt.frameBottom = height;
+ vt.bitCount = depth;
+ vt.planes = 1; // must be 1
+
+ if (depth == 4) {
+ byte[] gray = new byte[16];
+ for (int i = 0; i < gray.length; i++) {
+ gray[i] = (byte) ((i << 4) | i);
+ }
+ vt.palette = new IndexColorModel(4, 16, gray, gray, gray);
+ } else if (depth == 8) {
+ byte[] gray = new byte[256];
+ for (int i = 0; i < gray.length; i++) {
+ gray[i] = (byte) i;
+ }
+ vt.palette = new IndexColorModel(8, 256, gray, gray, gray);
+ }
+
+ tracks.add(vt);
+ return tracks.size() - 1;
+ }
+
+ /**
+ * Adds an audio track.
+ *
+ * @param waveFormatTag The format of the audio stream given in MMREG.H, for
+ * example 0x0001 for WAVE_FORMAT_PCM.
+ * @param scale The numerator of the sample rate.
+ * @param rate The denominator of the sample rate.
+ * @param numberOfChannels The number of channels: 1 for mono, 2 for stereo.
+ * @param sampleSizeInBits The number of bits in a sample: 8 or 16.
+ * @param isCompressed Whether the sound is compressed.
+ * @param frameDuration The frame duration, expressed in the media’s
+ * timescale, where the timescale is equal to the sample rate. For
+ * uncompressed formats, this field is always 1.
+ * @param frameSize For uncompressed audio, the number of bytes in a sample
+ * for a single channel (sampleSize divided by 8). For compressed audio, the
+ * number of bytes in a frame.
+ *
+ * @throws IllegalArgumentException if the format is not 4 characters long,
+ * if the time scale is not between 1 and 2^32, if the integer portion of
+ * the sampleRate is not equal to the scale, if numberOfChannels is not 1 or
+ * 2.
+ * @return Returns the track index.
+ */
+ public int addAudioTrack(int waveFormatTag, //
+ long scale, long rate, //
+ int numberOfChannels, int sampleSizeInBits, //
+ boolean isCompressed, //
+ int frameDuration, int frameSize) throws IOException {
+ ensureFinished();
+
+ if (scale < 1 || scale > (2L << 32)) {
+ throw new IllegalArgumentException("timeScale must be between 1 and 2^32:" + scale);
+ }
+ if (numberOfChannels != 1 && numberOfChannels != 2) {
+ throw new IllegalArgumentException("numberOfChannels must be 1 or 2: " + numberOfChannels);
+ }
+ if (sampleSizeInBits != 8 && sampleSizeInBits != 16) {
+ throw new IllegalArgumentException("sampleSize must be 8 or 16: " + numberOfChannels);
+ }
+
+ AudioTrack t = new AudioTrack(tracks.size(), typeToInt("\u0000\u0000\u0000\u0000"));
+ t.wFormatTag = waveFormatTag;
+
+ float afSampleRate = (float) rate / (float) scale;
+
+ t.format = new Format(MediaTypeKey, MediaType.AUDIO,
+ MimeTypeKey, MIME_AVI,
+ EncodingKey, RIFFParser.idToString(waveFormatTag),
+ SampleRateKey, Rational.valueOf(afSampleRate),
+ SampleSizeInBitsKey, sampleSizeInBits,
+ ChannelsKey, numberOfChannels,
+ FrameSizeKey, frameSize,
+ FrameRateKey, Rational.valueOf(afSampleRate),
+ SignedKey, sampleSizeInBits != 8,
+ ByteOrderKey, ByteOrder.LITTLE_ENDIAN);
+
+ t.scale = scale;
+ t.rate = rate;
+ t.samplesPerSec = rate / scale;
+ t.channels = numberOfChannels;
+ t.avgBytesPerSec = t.samplesPerSec * frameSize;
+ t.blockAlign = t.channels * sampleSizeInBits / 8;
+ t.bitsPerSample = sampleSizeInBits;
+ tracks.add(t);
+ return tracks.size() - 1;
+ }
+
+ /**
+ * Sets the global color palette.
+ */
+ public void setPalette(int track, ColorModel palette) {
+ if (palette instanceof IndexColorModel) {
+ ((VideoTrack) tracks.get(track)).palette = (IndexColorModel) palette;
+ }
+ }
+
+ /**
+ * Gets the dimension of a track.
+ */
+ public Dimension getVideoDimension(int track) {
+ Track tr = tracks.get(track);
+ if (tr instanceof VideoTrack) {
+ VideoTrack vt = (VideoTrack) tr;
+ Format fmt = vt.format;
+ return new Dimension(fmt.get(WidthKey), fmt.get(HeightKey));
+ } else {
+ return new Dimension(0, 0);
+ }
+ }
+
+ /**
+ * Returns the contents of the extra track header. Returns null if the
+ * header is not present.
Note: this method can only be performed before
+ * media data has been written into the tracks.
+ *
+ * @param track
+ * @param fourcc
+ * @param data the extra header as a byte array
+ * @throws IOException
+ */
+ public void putExtraHeader(int track, String fourcc, byte[] data) throws IOException {
+ if (state == States.STARTED) {
+ throw new IllegalStateException("Stream headers have already been written!");
+ }
+ Track tr = tracks.get(track);
+ int id = RIFFParser.stringToID(fourcc);
+ // Remove duplicate entries
+ for (int i = tr.extraHeaders.size() - 1; i >= 0; i--) {
+ if (tr.extraHeaders.get(i).getID() == id) {
+ tr.extraHeaders.remove(i);
+ }
+ }
+
+ // Add new entry
+ RIFFChunk chunk = new RIFFChunk(STRH_ID, id, data.length, -1);
+ chunk.setData(data);
+ tr.extraHeaders.add(chunk);
+ }
+
+ /**
+ * Returns the fourcc's of all extra stream headers.
+ *
+ * @param track
+ * @return An array of fourcc's of all extra stream headers.
+ * @throws IOException
+ */
+ public String[] getExtraHeaderFourCCs(int track) throws IOException {
+ Track tr = tracks.get(track);
+ String[] fourccs = new String[tr.extraHeaders.size()];
+ for (int i = 0; i < fourccs.length; i++) {
+ fourccs[i] = RIFFParser.idToString(tr.extraHeaders.get(i).getID());
+ }
+ return fourccs;
+ }
+
+ public void setName(int track, String name) {
+ tracks.get(track).name = name;
+ }
+
+ /**
+ * Sets the compression quality of a track.
A value of 0 stands for
+ * "high compression is important" a value of 1 for "high image quality is
+ * important".
Changing this value affects the encoding of video frames
+ * which are subsequently written into the track. Frames which have already
+ * been written are not changed.
This value has no effect on videos
+ * encoded with lossless encoders such as the PNG format.
The default
+ * value is 0.97.
+ *
+ * @param newValue
+ */
+ public void setCompressionQuality(int track, float newValue) {
+ VideoTrack vt = (VideoTrack) tracks.get(track);
+ vt.videoQuality = newValue;
+ }
+
+ /**
+ * Returns the compression quality of a track.
+ *
+ * @return compression quality
+ */
+ public float getCompressionQuality(int track) {
+ return ((VideoTrack) tracks.get(track)).videoQuality;
+ } /**
+ * Sets the state of the QuickTimeOutpuStream to started.
If the state
+ * is changed by this method, the prolog is written.
+ */
+ protected void ensureStarted() throws IOException {
+ if (state != States.STARTED) {
+ writeProlog();
+ state = States.STARTED;
+ }
+ }
+
+ /**
+ * Sets the state of the QuickTimeOutpuStream to finished.
If the state
+ * is changed by this method, the prolog is written.
+ */
+ protected void ensureFinished() throws IOException {
+ if (state != States.FINISHED) {
+ throw new IllegalStateException("Writer is in illegal state for this operation.");
+ }
+ }
+
+ /**
+ * Writes an already encoded palette change into the specified track.
If
+ * a track contains palette changes, then all key frames must be immediately
+ * preceeded by a palette change chunk which also is a key frame. If a key
+ * frame is not preceeded by a key frame palette change chunk, it will be
+ * downgraded to a delta frame.
+ *
+ * @throws IllegalArgumentException if the track is not a video track.
+ */
+ public void writePalette(int track, byte[] data, int off, int len, boolean isKeyframe) throws IOException {
+ Track tr = tracks.get(track);
+ if (!(tr instanceof VideoTrack)) {
+ throw new IllegalArgumentException("Error: track " + track + " is not a video track.");
+ }
+ if (!isKeyframe && tr.samples.isEmpty()) {
+ throw new IllegalStateException("The first sample in a track must be a keyframe.");
+ }
+
+ VideoTrack vt = (VideoTrack) tr;
+ tr.flags |= STRH_FLAG_VIDEO_PALETTE_CHANGES;
+
+ DataChunk paletteChangeChunk = new DataChunk(vt.twoCC | PC_ID);
+ long offset = getRelativeStreamPosition();
+ ImageOutputStream pOut = paletteChangeChunk.getOutputStream();
+ pOut.write(data, off, len);
+ moviChunk.add(paletteChangeChunk);
+ paletteChangeChunk.finish();
+ long length = getRelativeStreamPosition() - offset;
+ Sample s = new Sample(paletteChangeChunk.chunkType, 0, offset, length, isKeyframe);
+ tr.addSample(s);
+ idx1.add(s);
+ //tr.length+=0; Length is not affected by this chunk!
+ offset = getRelativeStreamPosition();
+ }
+
+ /**
+ * Writes an already encoded sample from a file to the specified track.
+ * This method does not inspect the contents of the file. For example, Its
+ * your responsibility to only append JPG files if you have chosen the JPEG
+ * video format.
If you append 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 sample data.
+ *
+ * @throws IllegalStateException if the duration is less than 1.
+ * @throws IOException if writing the sample data failed.
+ */
+ public void writeSample(int track, File file, boolean isKeyframe) throws IOException {
+ FileInputStream in = null;
+ try {
+ in = new FileInputStream(file);
+ writeSample(track, in, isKeyframe);
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ }
+
+ /**
+ * Writes an already encoded sample from an input stream to the specified
+ * track.
This method does not inspect the contents of the file. For
+ * example, its your responsibility to only append JPG files if you have
+ * chosen the JPEG video format.
If you append 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 track The track number.
+ * @param in The input stream which holds the sample data.
+ * @param isKeyframe True if the sample is a key frame.
+ *
+ * @throws IllegalArgumentException if the duration is less than 1.
+ * @throws IOException if writing the sample data failed.
+ */
+ public void writeSample(int track, InputStream in, boolean isKeyframe) throws IOException {
+ ensureStarted();
+
+ Track tr = tracks.get(track);
+
+ if (!isKeyframe && tr.samples.isEmpty()) {
+ throw new IllegalStateException("The first sample in a track must be a keyframe.");
+ }
+
+ // If a stream has palette changes, then only palette change samples can
+ // be marked as keyframe.
+ if (isKeyframe && 0 != (tr.flags & STRH_FLAG_VIDEO_PALETTE_CHANGES)) {
+ // If a keyframe sample is immediately preceeded by a palette change
+ // we can raise the palette change to a keyframe.
+ if (tr.samples.size() > 0) {
+ Sample s = tr.samples.get(tr.samples.size() - 1);
+ if ((s.chunkType & 0xffff) == PC_ID) {
+ s.isKeyframe = true;
+ }
+ }
+ isKeyframe = false;
+ }
+
+
+ DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe));
+ moviChunk.add(dc);
+ ImageOutputStream mdatOut = dc.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;
+ dc.finish();
+ Sample s = new Sample(dc.chunkType, 1, offset, length, isKeyframe);
+ tr.addSample(s);
+ idx1.add(s);
+ tr.length++;
+ if (getRelativeStreamPosition() > 1L << 32) {
+ throw new IOException("AVI file is larger than 4 GB");
+ }
+ }
+
+ /**
+ * Writes an already encoded sample from a byte array into a track.
This
+ * method does not inspect the contents of the samples. The contents has to
+ * match the format and dimensions of the media in this track.
If a
+ * track contains palette changes, then all key frames must be immediately
+ * preceeded by a palette change chunk. If a key frame is not preceeded by a
+ * palette change chunk, it will be downgraded to a delta frame.
+ *
+ * @param track The track index.
+ * @param data The encoded sample data.
+ * @param off The startTime offset in the data.
+ * @param len The number of bytes to write.
+ * @param isKeyframe Whether the sample is a sync sample (keyframe).
+ *
+ * @throws IllegalArgumentException if the duration is less than 1.
+ * @throws IOException if writing the sample data failed.
+ */
+ public void writeSample(int track, byte[] data, int off, int len, boolean isKeyframe) throws IOException {
+ ensureStarted();
+ Track tr = tracks.get(track);
+
+ // The first sample in a track is always a key frame
+ if (!isKeyframe && tr.samples.isEmpty()) {
+ throw new IllegalStateException("The first sample in a track must be a keyframe.\nTrack="+track+", "+tr.format);
+ }
+
+ // If a stream has palette changes, then only palette change samples can
+ // be marked as keyframe.
+ if (isKeyframe && 0 != (tr.flags & STRH_FLAG_VIDEO_PALETTE_CHANGES)) {
+ throw new IllegalStateException("Only palette changes can be marked as keyframe.\nTrack="+track+", "+tr.format);
+ }
+
+ DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe), len);
+ moviChunk.add(dc);
+ ImageOutputStream mdatOut = dc.getOutputStream();
+ long offset = getRelativeStreamPosition();
+ mdatOut.write(data, off, len);
+ long length = getRelativeStreamPosition() - offset;
+ dc.finish();
+ Sample s = new Sample(dc.chunkType, 1, offset, length, isKeyframe);
+ tr.addSample(s);
+ idx1.add(s);
+ if (getRelativeStreamPosition() > 1L << 32) {
+ throw new IOException("AVI file is larger than 4 GB");
+ }
+ }
+
+ /**
+ * Writes multiple already encoded samples from a byte array into a track.
+ *
This method does not inspect the contents of the data. The contents
+ * has to match the format and dimensions of the media in this track.
+ *
+ * @param track The track index.
+ * @param sampleCount The number of samples.
+ * @param data The encoded sample data.
+ * @param off The startTime offset in the data.
+ * @param len The number of bytes to write. Must be dividable by
+ * sampleCount.
+ * @param isKeyframe Whether the samples are sync samples. All samples must
+ * either be sync samples or non-sync samples.
+ *
+ * @throws IllegalArgumentException if the duration is less than 1.
+ * @throws IOException if writing the sample data failed.
+ */
+ public void writeSamples(int track, int sampleCount, byte[] data, int off, int len, boolean isKeyframe) throws IOException {
+ ensureStarted();
+ Track tr = tracks.get(track);
+ if (tr.mediaType == AVIMediaType.AUDIO) {
+ DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe), len);
+ moviChunk.add(dc);
+ ImageOutputStream mdatOut = dc.getOutputStream();
+ long offset = getRelativeStreamPosition();
+ mdatOut.write(data, off, len);
+ long length = getRelativeStreamPosition() - offset;
+ dc.finish();
+ Sample s = new Sample(dc.chunkType, sampleCount, offset, length, isKeyframe | tr.samples.isEmpty());
+ tr.addSample(s);
+ idx1.add(s);
+ tr.length += sampleCount;
+ if (getRelativeStreamPosition() > 1L << 32) {
+ throw new IOException("AVI file is larger than 4 GB");
+ }
+ } else {
+ for (int i = 0; i < sampleCount; i++) {
+ writeSample(track, data, off, len / sampleCount, isKeyframe);
+ off += len / sampleCount;
+ }
+ }
+ }
+
+ /**
+ * Returns the duration of the track in media time scale.
+ */
+ public long getMediaDuration(int track) {
+ Track tr = tracks.get(track);
+ long duration = tr.startTime;
+ if (!tr.samples.isEmpty()) {
+ Sample s = tr.samples.get(tr.samples.size() - 1);
+ duration += s.timeStamp + s.duration;
+ }
+ return duration;
+ }
+
+ /**
+ * Closes the stream.
+ *
+ * @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) {
+ moviChunk.finish();
+ writeEpilog();
+ state = States.FINISHED;
+ }
+ }
+
+ /**
+ * 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");
+ }
+ }
+
+ /**
+ * Returns true if the limit for media samples has been reached. If this
+ * limit is reached, no more samples should be added to the movie.
AVI
+ * 1.0 files have a file size limit of 2 GB. This method returns true if a
+ * file size of 1.8 GB has been reached.
+ */
+ public boolean isDataLimitReached() {
+ try {
+ return getRelativeStreamPosition() > (long) (1.8 * 1024 * 1024 * 1024);
+ } catch (IOException ex) {
+ return true;
+ }
+ }
+
+ private void writeProlog() throws IOException {
+ // The file has the following structure:
+ //
+ // .RIFF AVI
+ // ..avih (AVI Header Chunk)
+ // ..LIST strl (for each track)
+ // ...strh (Stream Header Chunk)
+ // ...strf (Stream Format Chunk)
+ // ...**** (Extra Stream Header Chunks)
+ // ...strn (Stream Name 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_ID, AVI_ID);
+ CompositeChunk hdrlChunk = new CompositeChunk(LIST_ID, HDRL_ID);
+
+ // Write empty AVI Main Header Chunk - we fill the data in later
+ aviChunk.add(hdrlChunk);
+ avihChunk = new FixedSizeDataChunk(AVIH_ID, 56);
+ avihChunk.seekToEndOfChunk();
+ hdrlChunk.add(avihChunk);
+
+ // Write empty AVI Stream Header Chunk - we fill the data in later
+ for (Track tr : tracks) {
+
+ CompositeChunk strlChunk = new CompositeChunk(LIST_ID, STRL_ID);
+ hdrlChunk.add(strlChunk);
+
+ tr.strhChunk = new FixedSizeDataChunk(STRH_ID, 56);
+ tr.strhChunk.seekToEndOfChunk();
+ strlChunk.add(tr.strhChunk);
+
+ tr.strfChunk = new FixedSizeDataChunk(STRF_ID, tr.getSTRFChunkSize());
+ tr.strfChunk.seekToEndOfChunk();
+ strlChunk.add(tr.strfChunk);
+
+ for (RIFFChunk c : tr.extraHeaders) {
+ DataChunk d = new DataChunk(c.getID(),
+ c.getSize());
+ ImageOutputStream dout = d.getOutputStream();
+ dout.write(c.getData());
+ d.finish();
+ strlChunk.add(d);
+ }
+
+ if (tr.name != null) {
+ byte[] data = (tr.name + "\u0000").getBytes("ASCII");
+ DataChunk d = new DataChunk(STRN_ID,
+ data.length);
+ ImageOutputStream dout = d.getOutputStream();
+ dout.write(data);
+ d.finish();
+ strlChunk.add(d);
+ }
+ }
+
+ moviChunk = new CompositeChunk(LIST_ID, MOVI_ID);
+ aviChunk.add(moviChunk);
+
+
+ }
+
+ private void writeEpilog() throws IOException {
+
+ ImageOutputStream d;
+
+ /* Create Idx1 Chunk and write data
+ * -------------
+ typedef struct _avioldindex {
+ FOURCC fcc;
+ DWORD cb;
+ struct _avioldindex_entry {
+ DWORD dwChunkId;
+ DWORD flags;
+ DWORD dwOffset;
+ DWORD dwSize;
+ } aIndex[];
+ } AVIOLDINDEX;
+ */
+ {
+ DataChunk idx1Chunk = new DataChunk(IDX1_ID);
+ aviChunk.add(idx1Chunk);
+ d = idx1Chunk.getOutputStream();
+ long moviListOffset = moviChunk.offset + 8 + 8;
+
+ {
+ double movieTime = 0;
+ int nTracks = tracks.size();
+ int[] trackSampleIndex = new int[nTracks];
+ long[] trackSampleCount = new long[nTracks];
+ for (Sample s : idx1) {
+ d.setByteOrder(ByteOrder.BIG_ENDIAN);
+ d.writeInt(s.chunkType); // dwChunkId
+ d.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ // 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
+ // header Palette change
+ // wb Audio data
+
+ d.writeInt(((s.chunkType & 0xffff) == PC_ID ? 0x100 : 0x0)//
+ | (s.isKeyframe ? 0x10 : 0x0)); // flags
+ // 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.writeInt((int) (s.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 startTime of the
+ // 'movi' list; however, in some AVI files it is given as an offset from
+ // the startTime of the file.
+
+ d.writeInt((int) (s.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 flags;
+ DWORD dwTotalFrames;
+ DWORD initialFrames;
+ DWORD dwStreams;
+ DWORD dwSuggestedBufferSize;
+ DWORD dwWidth;
+ DWORD dwHeight;
+ DWORD dwReserved[4];
+ } AVIMAINHEADER; */
+ {
+ avihChunk.seekToStartOfData();
+ d = avihChunk.getOutputStream();
+
+ // compute largest buffer size
+ long largestBufferSize = 0;
+ long duration = 0;
+ for (Track tr : tracks) {
+ long trackDuration = 0;
+ for (Sample s : tr.samples) {
+ trackDuration += s.duration;
+ }
+ duration = max(duration, trackDuration);
+ for (Sample s : tr.samples) {
+ if (s.length > largestBufferSize) {
+ largestBufferSize = s.length;
+ }
+ }
+ }
+
+
+
+ // FIXME compute dwMicroSecPerFrame properly!
+ Track tt = tracks.get(0);
+
+ d.writeInt((int) ((1000000L * tt.scale) / tt.rate)); // dwMicroSecPerFrame
+ // Specifies the number of microseconds between frames.
+ // This value indicates the overall timing for the file.
+
+ d.writeInt((int)largestBufferSize); // 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.writeInt(0); // dwPaddingGranularity
+ // Specifies the alignment for data, in bytes. Pad the data to multiples
+ // of this value.
+
+ d.writeInt(0x10|0x100|0x800); // flags
+ // 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.
+ // 0x800 AVIF_TRUST_CK_TYPE ???
+ // 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.
+
+ /*long dwTotalFrames = 0;
+ for (Track t : tracks) {
+ dwTotalFrames += t.samples.size();
+ }*/
+ d.writeInt(tt.samples.size()); // dwTotalFrames
+ // Specifies the total number of frames of data in the file.
+
+ d.writeInt(0); // initialFrames
+ // 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 initialFrames
+ // member of the AVISTREAMHEADER structure in the audio stream header
+
+ d.writeInt(tracks.size()); // dwStreams
+ // Specifies the number of streams in the file. For example, a file with
+ // audio and video has two streams.
+
+ d.writeInt((int) largestBufferSize); // 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.
+ {
+ VideoTrack vt = null;
+ int width = 0, height = 0;
+ // FIXME - Maybe we should support a global video dimension property
+ for (Track tr : tracks) {
+ width = max(width, max(tr.frameLeft, tr.frameRight));
+ height = max(height, max(tr.frameTop, tr.frameBottom));
+ }
+ d.writeInt(width); // dwWidth
+ // Specifies the width of the AVI file in pixels.
+
+ d.writeInt(height); // dwHeight
+ // Specifies the height of the AVI file in pixels.
+ }
+ d.writeInt(0); // dwReserved[0]
+ d.writeInt(0); // dwReserved[1]
+ d.writeInt(0); // dwReserved[2]
+ d.writeInt(0); // dwReserved[3]
+ // Reserved. Set this array to zero.
+ }
+
+ for (Track tr : tracks) {
+ /* 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 flags;
+ WORD priority;
+ WORD language;
+ DWORD initialFrames;
+ DWORD scale;
+ DWORD rate;
+ DWORD startTime;
+ DWORD dwLength;
+ DWORD dwSuggestedBufferSize;
+ DWORD quality;
+ DWORD dwSampleSize;
+ struct {
+ short int left;
+ short int top;
+ short int right;
+ short int bottom;
+ } rcFrame;
+ } AVISTREAMHEADER;
+ */
+ tr.strhChunk.seekToStartOfData();
+ d = tr.strhChunk.getOutputStream();
+ d.setByteOrder(ByteOrder.BIG_ENDIAN);
+ d.writeInt(typeToInt(tr.mediaType.fccType)); // fccType: "vids" for video stream
+ d.writeInt(tr.fccHandler); // fccHandler: specifies the codec
+ d.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+
+ d.writeInt(tr.flags);
+ // 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.writeShort(tr.priority); // priority: highest priority denotes default stream
+ d.writeShort(tr.language); // language: language code (?)
+ d.writeInt((int) tr.initialFrames); // initialFrames: how far audio data is ahead of the video frames
+ d.writeInt((int) tr.scale); // scale: time scale
+ d.writeInt((int) tr.rate); // rate: sample rate in scale units
+ d.writeInt((int) tr.startTime); // startTime: starting time of stream
+ d.writeInt((int) tr.length); // dwLength: length of stream ! WRONG
+
+ long dwSuggestedBufferSize = 0;
+ long dwSampleSize = -1; // => -1 indicates unknown
+ for (Sample s : tr.samples) {
+ if (s.length > dwSuggestedBufferSize) {
+ dwSuggestedBufferSize = s.length;
+ }
+ if (dwSampleSize == -1) {
+ dwSampleSize = s.length;
+ } else if (dwSampleSize != s.length) {
+ dwSampleSize = 0;
+ }
+ }
+ if (dwSampleSize == -1) {
+ dwSampleSize = 0;
+ }
+
+ d.writeInt((int) dwSuggestedBufferSize); // 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(tr.quality); // quality
+ // 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.writeInt(tr instanceof AudioTrack ? ((AudioTrack) tr).blockAlign : (int) dwSampleSize); // 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 blockAlign member of the WAVEFORMATEX structure describing the
+ // audio.
+
+ d.writeShort(tr.frameLeft); // rcFrame.left
+ d.writeShort(tr.frameTop); // rcFrame.top
+ d.writeShort(tr.frameRight); // rcFrame.right
+ d.writeShort(tr.frameBottom); // 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.
+
+ if (tr instanceof VideoTrack) {
+ VideoTrack vt = (VideoTrack) tr;
+ Format vf = tr.format;
+
+ /* 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 width;
+ LONG height;
+ WORD planes;
+ WORD bitCount;
+ DWORD compression;
+ DWORD sizeImage;
+ LONG xPelsPerMeter;
+ LONG yPelsPerMeter;
+ DWORD clrUsed;
+ DWORD clrImportant;
+ } BITMAPINFOHEADER;
+ */
+ tr.strfChunk.seekToStartOfData();
+ d = tr.strfChunk.getOutputStream();
+ d.writeInt(40); // biSize: number of bytes required by the structure.
+ d.writeInt(vf.get(WidthKey)); // width
+ d.writeInt(vf.get(HeightKey)); // height
+ d.writeShort(1); // planes
+ d.writeShort(vf.get(DepthKey)); // bitCount
+
+ String enc = vf.get(EncodingKey);
+ if (enc.equals(ENCODING_AVI_DIB)) {
+ d.writeInt(0); // compression - BI_RGB for uncompressed RGB
+ } else if (enc.equals(ENCODING_AVI_RLE)) {
+ if (vf.get(DepthKey) == 8) {
+ d.writeInt(1); // compression - BI_RLE8
+ } else if (vf.get(DepthKey) == 4) {
+ d.writeInt(2); // compression - BI_RLE4
+ } else {
+ throw new UnsupportedOperationException("RLE only supports 4-bit and 8-bit images");
+ }
+ } else {
+ d.setByteOrder(ByteOrder.BIG_ENDIAN);
+ d.writeInt(typeToInt(vt.format.get(EncodingKey))); // compression
+ d.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ if (enc.equals(ENCODING_AVI_DIB)) {
+ d.writeInt(0); // sizeImage
+ } else {
+ if (vf.get(DepthKey) == 4) {
+ d.writeInt(vf.get(WidthKey) * vf.get(HeightKey) / 2); // sizeImage
+ } else {
+ int bytesPerPixel = Math.max(1, vf.get(DepthKey) / 8);
+ d.writeInt(vf.get(WidthKey) * vf.get(HeightKey) * bytesPerPixel); // sizeImage
+ }
+ }
+
+ d.writeInt(0); // xPelsPerMeter
+ d.writeInt(0); // yPelsPerMeter
+
+ d.writeInt(vt.palette == null ? 0 : vt.palette.getMapSize()); // clrUsed
+
+ d.writeInt(0); // clrImportant
+
+ if (vt.palette != null) {
+ for (int i = 0, n = vt.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(vt.palette.getBlue(i));
+ d.write(vt.palette.getGreen(i));
+ d.write(vt.palette.getRed(i));
+ d.write(0);
+ }
+ }
+ } else if (tr instanceof AudioTrack) {
+ AudioTrack at = (AudioTrack) tr;
+
+ /* Write WAVEFORMATEX Data into AVI Stream Format Chunk
+ /* -------------
+ * see http://msdn.microsoft.com/en-us/library/dd757720(v=vs.85).aspx
+ typedef struct {
+ WORD wFormatTag;
+ WORD channels;
+ DWORD samplesPerSec;
+ DWORD avgBytesPerSec;
+ WORD blockAlign;
+ WORD bitsPerSample;
+ WORD cbSize;
+ } WAVEFORMATEX;
+ */
+ tr.strfChunk.seekToStartOfData();
+ d = tr.strfChunk.getOutputStream();
+
+ d.writeShort(at.wFormatTag); // wFormatTag: WAVE_FORMAT_PCM=0x0001
+ d.writeShort(at.channels); // channels
+ d.writeInt((int) at.samplesPerSec);// samplesPerSec
+ d.writeInt((int) at.avgBytesPerSec); // avgBytesPerSec
+ d.writeShort(at.blockAlign); // blockAlign
+ d.writeShort(at.bitsPerSample); // bitsPerSample
+
+ d.writeShort(0); //cbSize
+ // cbSize: Size, in bytes, of extra format information appended
+ // to the end of the WAVEFORMATEX structure. This information
+ // can be used by non-PCM formats to store extra attributes for
+ // the wFormatTag. If no extra information is required by the
+ // wFormatTag, this member must be set to zero. If this value is
+ // 22, the format is most likely described using the
+ // WAVEFORMATEXTENSIBLE structure, of which WAVEFORMATEX is the
+ // first member.
+ }
+ }
+
+ // -----------------
+ aviChunk.finish();
+ }
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/AVIWriter.java b/trunk/libsrc/avi/src/org/monte/media/avi/AVIWriter.java
new file mode 100644
index 000000000..6a41f0c7d
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/avi/AVIWriter.java
@@ -0,0 +1,517 @@
+/**
+ * @(#)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 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();
+ }
+}
diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/AbstractAVIStream.java b/trunk/libsrc/avi/src/org/monte/media/avi/AbstractAVIStream.java
new file mode 100644
index 000000000..8e2a66df6
--- /dev/null
+++ b/trunk/libsrc/avi/src/org/monte/media/avi/AbstractAVIStream.java
@@ -0,0 +1,1734 @@
+/*
+ * @(#)AbstractAVIStream.java
+ *
+ * Copyright (c) 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.riff.RIFFChunk;
+import java.util.Map;
+import org.monte.media.Buffer;
+import org.monte.media.Codec;
+import org.monte.media.Format;
+import org.monte.media.io.SubImageOutputStream;
+import java.awt.Dimension;
+import java.awt.image.IndexColorModel;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import javax.imageio.stream.ImageOutputStream;
+import static org.monte.media.VideoFormatKeys.*;
+
+/**
+ * This is the base class for low-level AVI stream IO.
+ *
+ * @author Werner Randelshofer
+ * @version $Id: AbstractAVIStream.java 299 2013-01-03 07:40:18Z werner $
+ */
+public abstract class AbstractAVIStream {
+
+ /**
+ * Chunk IDs.
+ */
+ /*
+ protected final static int RIFF_ID = 0x46464952;//0x52494646;// "RIFF"
+ protected final static int AVI_ID = 0x20495641; //0x41564920;// "AVI "
+ protected final static int LIST_ID = 0x5453494c;//0x4c495354;// "LIST"
+ protected final static int MOVI_ID = 0x69766f6d;//0x6d6f7669;// "movi"
+ protected final static int HDRL_ID = 0x6c726468;//0x6864726c;// "hdrl"
+ protected final static int AVIH_ID = 0x68697661;//0x61766968;// "avih"
+ protected final static int STRL_ID = 0x6c727473;//0x7374726c;// "strl"
+ protected final static int STRH_ID = 0x68727473;//0x73747268;// "strh"
+ protected final static int STRN_ID = 0x6e727473;//0x7374726e;// "strn"
+ protected final static int STRF_ID = 0x66727473;//0x73747266;// "strf"
+ protected final static int STRD_ID = 0x64727473;//0x73747264;// "strd"
+ protected final static int IDX1_ID = 0x31786469;//0x69647831;// "idx1"
+ protected final static int REC_ID = 0x20636572;//0x72656320;// "rec "
+ protected final static int PC_ID = 0x63700000;//0x00007063;// "??pc"
+ protected final static int DB_ID = 0x62640000;//0x00006462;// "??db"
+ protected final static int DC_ID = 0x63640000;//0x00006463;// "??dc"
+ protected final static int WB_ID = 0x62770000;//0x00007762;// "??wb"
+ */
+ protected final static int RIFF_ID =0x52494646;// "RIFF"
+ protected final static int AVI_ID = 0x41564920;// "AVI "
+ protected final static int AVIX_ID = 0x41564958;// "AVIX"
+ protected final static int LIST_ID = 0x4c495354;// "LIST"
+ protected final static int MOVI_ID = 0x6d6f7669;// "movi"
+ protected final static int HDRL_ID = 0x6864726c;// "hdrl"
+ protected final static int AVIH_ID = 0x61766968;// "avih"
+ protected final static int STRL_ID = 0x7374726c;// "strl"
+ protected final static int STRH_ID = 0x73747268;// "strh"
+ protected final static int STRN_ID = 0x7374726e;// "strn"
+ protected final static int STRF_ID = 0x73747266;// "strf"
+ protected final static int STRD_ID = 0x73747264;// "strd"
+ protected final static int IDX1_ID = 0x69647831;// "idx1"
+ protected final static int REC_ID = 0x72656320;// "rec "
+ protected final static int CHUNK_SUBTYPE_MASK = 0xffff;// "??xx"
+ protected final static int PC_ID = 0x00007063;// "??pc"
+ protected final static int DB_ID = 0x00006462;// "??db"
+ protected final static int DC_ID = 0x00006463;// "??dc"
+ protected final static int WB_ID = 0x00007762;// "??wb"
+
+ /**
+ * Indicates the AVI file has an index.
+ */
+ public final static int AVIH_FLAG_HAS_INDEX = 0x00000010;
+ /**
+ * 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.
+ */
+ public final static int AVIH_FLAG_MUST_USE_INDEX = 0x00000020;
+ /**
+ * Indicates the AVI file is interleaved.
+ */
+ public final static int AVIH_FLAG_IS_INTERLEAVED = 0x00000100;
+ /**
+ * ??
+ */
+ public final static int AVIH_FLAG_TRUST_CK_TYPE = 0x00000800;
+ /**
+ * // 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.
+ */
+ public final static int AVIH_FLAG_WAS_CAPTURE_FILE = 0x00010000;
+ /* Indicates the AVI file contains copyrighted data and
+ * software. When this flag is used, software should not
+ * permit the data to be duplicated. */
+ public final static int AVIH_FLAG_COPYRIGHTED = 0x00020000;
+ /**
+ * Indicates this stream should not be enabled by default.
+ */
+ public final static int STRH_FLAG_DISABLED = 0x00000001;
+ /**
+ * Indicates this video stream contains palette changes. This flag warns the
+ * playback software that it will need to animate the palette.
+ */
+ public final static int STRH_FLAG_VIDEO_PALETTE_CHANGES = 0x00010000;
+ /**
+ * Underlying output stream.
+ */
+ protected 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.
+ */
+ protected long streamOffset;
+
+ /**
+ * Supported media types.
+ */
+ public static enum AVIMediaType {
+
+ AUDIO("auds"),//
+ MIDI("mids"),//
+ TEXT("txts"),//
+ VIDEO("vids")//
+ ;
+ protected final String fccType;
+
+ @Override
+ public String toString() {
+ return fccType;
+ }
+
+ AVIMediaType(String fourCC) {
+ this.fccType = fourCC;
+ }
+ }
+ /**
+ * The list of tracks in the file.
+ */
+ protected ArrayList