diff --git a/README.md b/README.md
index 4ab0a8a..523b8ea 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,6 @@ Video engine support for Open Source Physics Java programs including Tracker and
This code requires the Open Source Physics Core Library available in the OpenSourcePhysics/osp repository.
-Xuggle video engine: compiling the xuggle package requires the libraries "xuggle-xuggler.jar", "logback-classic.jar", "logback-core.jar" and "slf4j-api.jar" in video-engines/libraries.
-
QuickTime video engine: compiling the quicktime package requires the library "QTJava.zip" in video-engines/libraries.
diff --git a/libraries/QTJava.zip b/libraries/QTJava.zip
deleted file mode 100644
index 86b9934..0000000
Binary files a/libraries/QTJava.zip and /dev/null differ
diff --git a/libraries/logback-classic.jar b/libraries/logback-classic.jar
deleted file mode 100644
index 0a650a3..0000000
Binary files a/libraries/logback-classic.jar and /dev/null differ
diff --git a/libraries/logback-core.jar b/libraries/logback-core.jar
deleted file mode 100644
index 5245cb0..0000000
Binary files a/libraries/logback-core.jar and /dev/null differ
diff --git a/libraries/slf4j-api.jar b/libraries/slf4j-api.jar
deleted file mode 100644
index 4d23f41..0000000
Binary files a/libraries/slf4j-api.jar and /dev/null differ
diff --git a/libraries/xuggle-xuggler.jar b/libraries/xuggle-xuggler.jar
deleted file mode 100644
index 4ea5de4..0000000
Binary files a/libraries/xuggle-xuggler.jar and /dev/null differ
diff --git a/src/org/opensourcephysics/media/ffmpeg/BgrConverter.java b/src/org/opensourcephysics/media/ffmpeg/BgrConverter.java
new file mode 100644
index 0000000..f3cde6f
--- /dev/null
+++ b/src/org/opensourcephysics/media/ffmpeg/BgrConverter.java
@@ -0,0 +1,127 @@
+package org.opensourcephysics.media.ffmpeg;
+
+import static org.ffmpeg.avutil.AvutilLibrary.av_freep;
+import static org.ffmpeg.avutil.AvutilLibrary.av_image_alloc;
+
+import java.awt.color.ColorSpace;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.ComponentColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.awt.image.PixelInterleavedSampleModel;
+import java.awt.image.Raster;
+import java.awt.image.SampleModel;
+import java.awt.image.WritableRaster;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.bridj.IntValuedEnum;
+import org.bridj.Pointer;
+import org.ffmpeg.avcodec.AVPicture;
+import org.ffmpeg.avutil.AvutilLibrary.AVPixelFormat;
+import org.ffmpeg.swscale.SwscaleLibrary;
+import org.ffmpeg.swscale.SwscaleLibrary.SwsContext;
+import org.opensourcephysics.controls.OSPLog;
+
+/**
+ * A converter to translate {@link AVPicture}s to and from {@link BufferedImage}
+ * s of type {@link BufferedImage#TYPE_3BYTE_BGR}.
+ */
+
+public class BgrConverter {
+ // band offsets requried by the sample model
+
+ private static final int[] mBandOffsets = { 2, 1, 0 };
+
+ // color space for this converter
+
+ private static final ColorSpace mColorSpace = ColorSpace
+ .getInstance(ColorSpace.CS_sRGB);
+
+ private SampleModel sm;
+ private ColorModel colorModel;
+
+ // input picture pixel format
+ IntValuedEnum pixfmt;
+ // data structure needed for resampling
+ Pointer resampler;
+ Pointer> rpicture;
+ Pointer rpicture_linesize;
+ int rpicture_bufsize;
+
+ public BgrConverter(IntValuedEnum pixfmt, int w, int h)
+ throws IOException {
+ sm = new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, w, h, 3,
+ 3 * w, mBandOffsets);
+ colorModel = new ComponentColorModel(mColorSpace, false, false,
+ ColorModel.OPAQUE, DataBuffer.TYPE_BYTE);
+ this.pixfmt = pixfmt;
+ if (pixfmt != AVPixelFormat.AV_PIX_FMT_BGR24) {
+ resampler = SwscaleLibrary.sws_getContext(w, h, pixfmt, w, h,
+ AVPixelFormat.AV_PIX_FMT_BGR24,
+ SwscaleLibrary.SWS_BILINEAR, null, null, null);
+ if (resampler == null) {
+ OSPLog.warning("Could not create color space resampler"); //$NON-NLS-1$
+ throw new IOException("Could not create color space resampler.");
+ }
+ rpicture = Pointer.allocatePointers(Byte.class, 4);
+ rpicture_linesize = Pointer.allocateInts(4);
+ rpicture_bufsize = av_image_alloc(rpicture, rpicture_linesize, w,
+ h, AVPixelFormat.AV_PIX_FMT_BGR24, 1);
+ if (rpicture_bufsize < 0) {
+ OSPLog.warning("Could not allocate BGR24 picture memory");
+ throw new IOException(
+ "Could not allocate BGR24 picture memory.");
+ }
+ } else {
+ resampler = null;
+ rpicture = null;
+ rpicture_linesize = null;
+ rpicture_bufsize = 0;
+ }
+ }
+
+ public BufferedImage toImage(Pointer> picture,
+ Pointer picture_linesize, int size) {
+ if (resampler != null) {
+ if (SwscaleLibrary.sws_scale(resampler, picture, picture_linesize,
+ 0, sm.getHeight(), rpicture, rpicture_linesize) < 0) {
+ OSPLog.warning("Could not encode video as BGR24"); //$NON-NLS-1$
+ return null;
+ }
+ }
+ // make a copy of the raw bytes int a DataBufferByte which the
+ // writable raster can operate on
+
+ final ByteBuffer byteBuf = ByteBuffer.wrap(rpicture == null ? picture
+ .get().getBytes(size) : rpicture.get().getBytes(
+ rpicture_bufsize));
+ final byte[] bytes = new byte[rpicture == null ? size : rpicture_bufsize];
+ byteBuf.get(bytes, 0, bytes.length);
+
+ // create the data buffer from the bytes
+ final DataBufferByte db = new DataBufferByte(bytes, bytes.length);
+
+ // create an a sample model which matches the byte layout of the
+ // image data and raster which contains the data which now can be
+ // properly interpreted
+ final WritableRaster wr = Raster.createWritableRaster(sm, db, null);
+
+ // return a new image created from the color model and raster
+ return new BufferedImage(colorModel, wr, false, null);
+ }
+
+ public void dispose() {
+ if (rpicture != null) {
+ if(rpicture.getValidElements() > 0)
+ av_freep(rpicture);
+ rpicture = null;
+ }
+ rpicture_linesize = null;
+ if (resampler != null) {
+ SwscaleLibrary.sws_freeContext(resampler);
+ resampler = null;
+ }
+ }
+}
diff --git a/src/org/opensourcephysics/media/ffmpeg/FFMPegAnalyzer.java b/src/org/opensourcephysics/media/ffmpeg/FFMPegAnalyzer.java
new file mode 100644
index 0000000..f26d815
--- /dev/null
+++ b/src/org/opensourcephysics/media/ffmpeg/FFMPegAnalyzer.java
@@ -0,0 +1,359 @@
+package org.opensourcephysics.media.ffmpeg;
+
+import static org.ffmpeg.avcodec.AvcodecLibrary.AV_PKT_FLAG_KEY;
+import static org.ffmpeg.avcodec.AvcodecLibrary.av_free_packet;
+import static org.ffmpeg.avcodec.AvcodecLibrary.av_init_packet;
+import static org.ffmpeg.avcodec.AvcodecLibrary.avcodec_decode_video2;
+import static org.ffmpeg.avcodec.AvcodecLibrary.avcodec_find_decoder;
+import static org.ffmpeg.avcodec.AvcodecLibrary.avcodec_open2;
+import static org.ffmpeg.avformat.AvformatLibrary.av_find_best_stream;
+import static org.ffmpeg.avformat.AvformatLibrary.av_read_frame;
+import static org.ffmpeg.avformat.AvformatLibrary.av_register_all;
+import static org.ffmpeg.avformat.AvformatLibrary.avformat_close_input;
+import static org.ffmpeg.avformat.AvformatLibrary.avformat_find_stream_info;
+import static org.ffmpeg.avformat.AvformatLibrary.avformat_open_input;
+import static org.ffmpeg.avutil.AvutilLibrary.AV_NOPTS_VALUE;
+import static org.ffmpeg.avutil.AvutilLibrary.av_frame_get_best_effort_timestamp;
+import static org.ffmpeg.avutil.AvutilLibrary.av_image_copy;
+
+import java.awt.image.BufferedImage;
+import java.beans.PropertyChangeSupport;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.bridj.Pointer;
+import org.ffmpeg.avcodec.AVCodec;
+import org.ffmpeg.avcodec.AVCodecContext;
+import org.ffmpeg.avcodec.AVPacket;
+import org.ffmpeg.avcodec.AvcodecLibrary;
+import org.ffmpeg.avformat.AVFormatContext;
+import org.ffmpeg.avformat.AVStream;
+import org.ffmpeg.avutil.AVFrame;
+import org.ffmpeg.avutil.AVRational;
+import org.ffmpeg.avutil.AvutilLibrary;
+import org.ffmpeg.avutil.AvutilLibrary.AVMediaType;
+import org.opensourcephysics.media.core.VideoIO;
+
+public class FFMPegAnalyzer {
+
+ // path to the video file
+ String path;
+ // PropertyChangeSupport object to notify
+ PropertyChangeSupport support;
+ // timebase
+ AVRational timebase;
+
+ // video stream index or -1 if none
+ private int streamIndex;
+ // maps frame number to timestamp of displayed packet (last packet loaded)
+ private Map frameTimeStamps;
+ // maps frame number to timestamp of key packet (first packet loaded)
+ private Map keyTimeStamps;
+ // seconds array to calculate startTimes
+ private ArrayList seconds;
+
+ // create thumbnail image
+ private Pointer> picture;
+ private Pointer picture_linesize;
+ int picture_bufsize;
+ BgrConverter converter;
+ private BufferedImage thumbnail;
+
+ private boolean createThumbnail;
+ private int targetFrameNumber;
+ private boolean analyzed;
+
+ public FFMPegAnalyzer(String path, PropertyChangeSupport support) {
+ frameTimeStamps = new HashMap();
+ keyTimeStamps = new HashMap();
+ createThumbnail = false;
+ targetFrameNumber = 15;
+ analyzed = false;
+ thumbnail = null;
+ this.path = path;
+ this.streamIndex = -1;
+ timebase = null;
+ this.support = support;
+ seconds = new ArrayList();
+ picture = null;
+ picture_linesize = null;
+ picture_bufsize = 0;
+ converter = null;
+ }
+
+ public FFMPegAnalyzer(String path, boolean createThumbnail,
+ int targetFrameNumber) {
+ this(path, null);
+ this.createThumbnail = createThumbnail;
+ this.targetFrameNumber = targetFrameNumber;
+ if (createThumbnail) {
+ picture = Pointer.allocatePointers(Byte.class, 4);
+ picture_linesize = Pointer.allocateInts(4);
+ }
+ }
+
+ public int getVideoStreamIndex() throws IOException {
+ if (!analyzed)
+ analyze();
+ return streamIndex;
+ }
+
+ public Map getFrameTimeStamps() throws IOException {
+ if (!analyzed)
+ analyze();
+ return frameTimeStamps;
+ }
+
+ public Map getKeyTimeStamps() throws IOException {
+ if (!analyzed)
+ analyze();
+ return keyTimeStamps;
+ }
+
+ public double[] getStartTimes() throws IOException {
+ if (!analyzed)
+ analyze();
+ double[] startTimes = new double[frameTimeStamps.size()];
+ if(startTimes.length < 1)
+ return startTimes;
+ startTimes[0] = 0;
+ for (int i = 1; i < startTimes.length; i++) {
+ startTimes[i] = seconds.get(i) * 1000;
+ }
+ return startTimes;
+ }
+
+ public BufferedImage getThumbnail() throws IOException {
+ if (!analyzed)
+ analyze();
+ return thumbnail;
+ }
+
+ private long getTimeStamp(Pointer pFrame) {
+ long pts = av_frame_get_best_effort_timestamp(pFrame);
+ if( pts == AV_NOPTS_VALUE)
+ pts = 0;
+ return pts;
+ }
+
+ public void analyze() throws IOException {
+ Pointer context = null;
+ Pointer stream = null;
+ Pointer cContext = null;
+ Pointer codec = null;
+ Pointer packet = null, orig_packet = null;
+ Pointer frame = null;
+ Pointer got_frame = null;
+ try {
+ av_register_all();
+ // set up frame data using temporary container
+ Pointer> pfmt_ctx = Pointer
+ .allocatePointer(AVFormatContext.class);
+ if (avformat_open_input(pfmt_ctx, Pointer.pointerToCString(path),
+ null, null) < 0) {
+ throw new IOException("unable to open " + path); //$NON-NLS-1$
+ }
+ context = pfmt_ctx.get();
+ /* retrieve stream information */
+ if (avformat_find_stream_info(context, null) < 0) {
+ throw new IOException("unable to find stream info in " + path); //$NON-NLS-1$
+ }
+
+ // find the first video stream in the container
+ int ret = av_find_best_stream(context,
+ AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, null, 0);
+ streamIndex = -1;
+ if (ret < 0) {
+ throw new IOException("unable to find video stream in " + path); //$NON-NLS-1$
+ }
+ streamIndex = ret;
+ if (streamIndex < 0) {
+ throw new IOException("unable to find video stream in " + path); //$NON-NLS-1$
+ }
+ stream = context.get().streams().get(streamIndex);
+ timebase = copy(stream.get().time_base());
+
+ cContext = stream.get().codec();
+ codec = avcodec_find_decoder(cContext.get().codec_id());
+ if (codec == null) {
+ throw new IOException(
+ "unable to find codec video stream in " + path); //$NON-NLS-1$
+ }
+ // check that coder opens
+ if (avcodec_open2(cContext, codec, null) < 0) {
+ throw new IOException(
+ "unable to open video decoder for " + path); //$NON-NLS-1$
+ }
+
+ packet = Pointer.allocate(AVPacket.class);
+ av_init_packet(packet);
+ packet.get().data(null);
+ packet.get().size(0);
+
+ long keyTimeStamp = Long.MIN_VALUE;
+ long startTimeStamp = Long.MIN_VALUE;
+ long pts;
+ seconds = new ArrayList();
+ if (support != null)
+ support.firePropertyChange("progress", path, 0); //$NON-NLS-1$
+ int frameNr = 0;
+
+ // step thru container and find all video frames
+ /* allocate image where the decoded image will be put */
+ if (createThumbnail) {
+ picture_bufsize = AvutilLibrary.av_image_alloc(picture,
+ picture_linesize, cContext.get().width(), cContext
+ .get().height(), cContext.get().pix_fmt(), 1);
+ if (picture_bufsize < 0) {
+ throw new IOException(
+ "unable to allocate raw memory buffer for " + path); //$NON-NLS-1$
+ }
+ converter = new BgrConverter(cContext.get().pix_fmt(), cContext
+ .get().width(), cContext.get().height());
+ }
+ frame = Pointer.allocate(AVFrame.class);
+ got_frame = Pointer.allocateInt();
+ while (av_read_frame(context, packet) >= 0) {
+ if (VideoIO.isCanceled()) {
+ if (support != null)
+ support.firePropertyChange("progress", path, null); //$NON-NLS-1$
+ throw new IOException("Canceled by user"); //$NON-NLS-1$
+ }
+ if (isVideoPacket(packet, streamIndex)) {
+ orig_packet = packet;
+ long ptr = packet.get().data().getPeer();
+ int bytesDecoded;
+ do {
+ /* decode video frame */
+ bytesDecoded = avcodec_decode_video2(cContext, frame,
+ got_frame, packet);
+ // check for errors
+ if (bytesDecoded < 0)
+ break;
+ if (got_frame.get() != 0) {
+ pts = getTimeStamp(frame);
+ if (keyTimeStamp == Long.MIN_VALUE
+ || isKeyFrame(frame)) {
+ keyTimeStamp = pts;
+ }
+ if (startTimeStamp == Long.MIN_VALUE) {
+ startTimeStamp = pts;
+ }
+ frameTimeStamps.put(frameNr, pts);
+ seconds.add((double) ((pts - startTimeStamp) * value(timebase)));
+ keyTimeStamps.put(frameNr, keyTimeStamp);
+ if (support != null)
+ support.firePropertyChange(
+ "progress", path, frameNr); //$NON-NLS-1$
+ if (createThumbnail) {
+ /*
+ * copy decoded frame to destination buffer: this is
+ * required since rawvideo expects non aligned data
+ */
+ av_image_copy(picture, picture_linesize, frame
+ .get().data(), frame.get().linesize(),
+ cContext.get().pix_fmt(), cContext.get()
+ .width(), cContext.get().height());
+ thumbnail = converter.toImage(picture, picture_linesize, picture_bufsize);
+ }
+ frameNr++;
+ }
+ ptr+=bytesDecoded;
+ packet.get().data((Pointer)Pointer.pointerToAddress(ptr));
+ packet.get().size(packet.get().size()-bytesDecoded);
+ } while(packet.get().size() > 0);
+ }
+ av_free_packet(packet);
+ if (createThumbnail
+ && (thumbnail != null || frameNr >= targetFrameNumber))
+ break;
+ }
+ /* flush cached frames */
+ packet.get().data(null);
+ packet.get().size(0);
+
+ do {
+ if (createThumbnail
+ && (thumbnail != null || frameNr >= targetFrameNumber))
+ break;
+ /* decode video frame */
+ avcodec_decode_video2(cContext, frame, got_frame, packet);
+ if (got_frame.get() != 0) {
+ pts = getTimeStamp(frame);
+ if (keyTimeStamp == Long.MIN_VALUE || isKeyFrame(frame)) {
+ keyTimeStamp = pts;
+ }
+ if (startTimeStamp == Long.MIN_VALUE) {
+ startTimeStamp = pts;
+ }
+ frameTimeStamps.put(frameNr, pts);
+ seconds.add((double) ((pts - startTimeStamp) * value(timebase)));
+ keyTimeStamps.put(frameNr, keyTimeStamp);
+ if (support != null)
+ support.firePropertyChange("progress", path, frameNr); //$NON-NLS-1$
+ frameNr++;
+ }
+ } while (got_frame.get() != 0);
+ } finally {
+ // clean up temporary objects
+ AvcodecLibrary.avcodec_close(cContext);
+ cContext = null;
+ stream = null;
+ packet = null;
+ frame = null;
+ if (context != null) {
+ avformat_close_input(context.getReference());
+ context = null;
+ }
+ if (converter != null) {
+ converter.dispose();
+ converter = null;
+ }
+ }
+ analyzed = true;
+ }
+
+ /**
+ * Determines if a frame is a key frame.
+ *
+ * @param packet
+ * the frame
+ * @return true if frame is a key in the video stream
+ */
+ public static boolean isKeyFrame(Pointer frame) {
+ if ((frame.get().flags() & AV_PKT_FLAG_KEY) != 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Determines if a packet is a video packet.
+ *
+ * @param packet
+ * the packet
+ * @return true if packet is in the video stream
+ */
+ public static boolean isVideoPacket(Pointer packet,
+ int videoStreamIndex) {
+ if (packet.get().stream_index() == videoStreamIndex) {
+ return true;
+ }
+ return false;
+ }
+
+ public static AVRational copy(AVRational rat) {
+ AVRational ret = new AVRational();
+ ret.den(rat.den());
+ ret.num(rat.num());
+ return ret;
+ }
+
+ public static double value(AVRational rat) {
+ double ret = 1.0 * rat.num() / rat.den();
+ return ret;
+ }
+
+}
diff --git a/src/org/opensourcephysics/media/ffmpeg/FFMPegIO.java b/src/org/opensourcephysics/media/ffmpeg/FFMPegIO.java
new file mode 100644
index 0000000..146b926
--- /dev/null
+++ b/src/org/opensourcephysics/media/ffmpeg/FFMPegIO.java
@@ -0,0 +1,101 @@
+package org.opensourcephysics.media.ffmpeg;
+
+import static org.ffmpeg.avformat.AvformatLibrary.av_register_all;
+
+import org.opensourcephysics.controls.OSPLog;
+import org.opensourcephysics.media.core.VideoFileFilter;
+import org.opensourcephysics.media.core.VideoIO;
+import org.opensourcephysics.media.ffmpeg.FFMPegVideoType;
+import org.opensourcephysics.tools.ResourceLoader;
+
+/**
+ * This registers FFMPeg with VideoIO so it can be used to open and record
+ * videos.
+ *
+ * @author Frank Schütte
+ * @version 1.0
+ */
+public class FFMPegIO extends VideoIO {
+ /**
+ * Registers FFMPeg video types with VideoIO class.
+ */
+ static public void registerWithVideoIO() { // add FFMPeg video types, if
+ // available
+ try {
+ VideoIO.addVideoEngine(new FFMPegVideoType());
+ // register all supported audio/video types with ffmpeg
+ av_register_all();
+ // add common video types shared with QuickTime
+ for (String ext : VideoIO.VIDEO_EXTENSIONS) { // {"mov", "avi",
+ // "mp4"}
+ VideoFileFilter filter = new VideoFileFilter(ext,
+ new String[] { ext });
+ FFMPegVideoType ffmpegType = new FFMPegVideoType(filter);
+ VideoIO.addVideoType(ffmpegType);
+ ResourceLoader.addExtractExtension(ext);
+ }
+ // add additional ffmpeg types
+ // FLV
+ VideoFileFilter filter = new VideoFileFilter(
+ "flv", new String[] { "flv" }); //$NON-NLS-1$ //$NON-NLS-2$
+ VideoIO.addVideoType(new FFMPegVideoType(filter));
+ ResourceLoader.addExtractExtension("flv"); //$NON-NLS-1$
+ // 3GP
+ filter = new VideoFileFilter("3gp", new String[] { "3gp" }); //$NON-NLS-1$ //$NON-NLS-2$
+ FFMPegVideoType vidType = new FFMPegVideoType(filter);
+ vidType.setRecordable(false);
+ VideoIO.addVideoType(vidType);
+ ResourceLoader.addExtractExtension("3gp"); //$NON-NLS-1$
+ // WMV
+ filter = new VideoFileFilter("asf", new String[] { "wmv" }); //$NON-NLS-1$ //$NON-NLS-2$
+ VideoIO.addVideoType(new FFMPegVideoType(filter));
+ ResourceLoader.addExtractExtension("wmv"); //$NON-NLS-1$
+ // DV
+ filter = new VideoFileFilter("dv", new String[] { "dv" }); //$NON-NLS-1$ //$NON-NLS-2$
+ vidType = new FFMPegVideoType(filter);
+ vidType.setRecordable(false);
+ VideoIO.addVideoType(vidType);
+ ResourceLoader.addExtractExtension("dv"); //$NON-NLS-1$
+ // MTS
+ filter = new VideoFileFilter("mts", new String[] { "mts" }); //$NON-NLS-1$ //$NON-NLS-2$
+ vidType = new FFMPegVideoType(filter);
+ vidType.setRecordable(false);
+ VideoIO.addVideoType(vidType);
+ ResourceLoader.addExtractExtension("mts"); //$NON-NLS-1$
+ // M2TS
+ filter = new VideoFileFilter("m2ts", new String[] { "m2ts" }); //$NON-NLS-1$ //$NON-NLS-2$
+ vidType = new FFMPegVideoType(filter);
+ vidType.setRecordable(false);
+ VideoIO.addVideoType(vidType);
+ ResourceLoader.addExtractExtension("m2ts"); //$NON-NLS-1$
+ // MPG
+ filter = new VideoFileFilter("mpg", new String[] { "mpg" }); //$NON-NLS-1$ //$NON-NLS-2$
+ vidType = new FFMPegVideoType(filter);
+ vidType.setRecordable(false);
+ VideoIO.addVideoType(vidType);
+ ResourceLoader.addExtractExtension("mpg"); //$NON-NLS-1$
+ // MOD
+ filter = new VideoFileFilter("mod", new String[] { "mod" }); //$NON-NLS-1$ //$NON-NLS-2$
+ vidType = new FFMPegVideoType(filter);
+ vidType.setRecordable(false);
+ VideoIO.addVideoType(vidType);
+ ResourceLoader.addExtractExtension("mod"); //$NON-NLS-1$
+ // OGG
+ filter = new VideoFileFilter("ogg", new String[] { "ogg", "ogv" }); //$NON-NLS-1$ //$NON-NLS-2$
+ vidType = new FFMPegVideoType(filter);
+ vidType.setRecordable(false);
+ VideoIO.addVideoType(vidType);
+ ResourceLoader.addExtractExtension("mod"); //$NON-NLS-1$
+ filter = new VideoFileFilter("webm", new String[] {"webm"}); //$NON-NLS-1$ //$NON-NLS-2$
+ vidType = new FFMPegVideoType(filter);
+ vidType.setRecordable(false);
+ VideoIO.addVideoType(vidType);
+ ResourceLoader.addExtractExtension("webm"); //$NON-NLS-1$
+ } catch (Exception ex) { // ffmpeg not working
+ OSPLog.config("ffmpeg exception: " + ex.toString()); //$NON-NLS-1$
+ } catch (Error er) { // ffmpeg not working
+ OSPLog.config("ffmpeg error: " + er.toString()); //$NON-NLS-1$
+ }
+ }
+
+}
diff --git a/src/org/opensourcephysics/media/xuggle/XuggleThumbnailTool.java b/src/org/opensourcephysics/media/ffmpeg/FFMPegThumbnailTool.java
similarity index 72%
rename from src/org/opensourcephysics/media/xuggle/XuggleThumbnailTool.java
rename to src/org/opensourcephysics/media/ffmpeg/FFMPegThumbnailTool.java
index cbaf1bb..d8498ff 100644
--- a/src/org/opensourcephysics/media/xuggle/XuggleThumbnailTool.java
+++ b/src/org/opensourcephysics/media/ffmpeg/FFMPegThumbnailTool.java
@@ -1,5 +1,5 @@
/*
- * The org.opensourcephysics.media.xuggle package provides Xuggle
+ * The org.opensourcephysics.media.ffmpeg package provides FFMPeg
* services including implementations of the Video and VideoRecorder interfaces.
*
* Copyright (c) 2017 Douglas Brown and Wolfgang Christian.
@@ -22,7 +22,7 @@
* For additional information and documentation on Open Source Physics,
* please see .
*/
-package org.opensourcephysics.media.xuggle;
+package org.opensourcephysics.media.ffmpeg;
import java.awt.AlphaComposite;
import java.awt.Dimension;
@@ -31,21 +31,20 @@
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+
+import javax.imageio.ImageIO;
import org.opensourcephysics.media.core.VideoIO;
import org.opensourcephysics.tools.ResourceLoader;
-import com.xuggle.mediatool.IMediaReader;
-import com.xuggle.mediatool.MediaToolAdapter;
-import com.xuggle.mediatool.ToolFactory;
-import com.xuggle.mediatool.event.IVideoPictureEvent;
-
/**
* A class to create thumbnail images of videos.
*/
-public class XuggleThumbnailTool extends MediaToolAdapter {
+public class FFMPegThumbnailTool {
- private static final XuggleThumbnailTool THUMBNAIL_TOOL = new XuggleThumbnailTool();
+ private static final FFMPegThumbnailTool THUMBNAIL_TOOL = new FFMPegThumbnailTool();
private static final int TARGET_FRAME_NUMBER = 15;
private BufferedImage thumbnail;
@@ -56,7 +55,7 @@ public class XuggleThumbnailTool extends MediaToolAdapter {
private Dimension dim;
/**
- * "Starts" this tool--called by XuggleVideoType so minijar will include it
+ * "Starts" this tool--called by FFMPegVideoType so minijar will include it
*/
public static void start() {}
@@ -69,11 +68,12 @@ public static void start() {}
public static synchronized BufferedImage createThumbnailImage(Dimension dim, String pathToVideo) {
THUMBNAIL_TOOL.initialize(dim);
String path = pathToVideo.startsWith("http")? ResourceLoader.getURIPath(pathToVideo): pathToVideo; //$NON-NLS-1$
- IMediaReader mediaReader = ToolFactory.makeReader(path);
- mediaReader.setBufferedImageTypeToGenerate(BufferedImage.TYPE_3BYTE_BGR);
- mediaReader.addListener(THUMBNAIL_TOOL);
- while (!THUMBNAIL_TOOL.isFinished() && mediaReader.readPacket()==null); // reads video until a thumbnail is created
- mediaReader.close();
+ THUMBNAIL_TOOL.finished = false;
+ FFMPegAnalyzer analyzer = null;
+ try {
+ analyzer = new FFMPegAnalyzer(path, true, TARGET_FRAME_NUMBER);
+ THUMBNAIL_TOOL.thumbnailFromPicture(analyzer.getThumbnail());
+ } catch (IOException e) { }
return THUMBNAIL_TOOL.thumbnail;
}
@@ -90,13 +90,11 @@ public static synchronized File createThumbnailFile(Dimension dim, String pathTo
}
/**
- * Creates a thumbnail image from the video image passed in by an IMediaReader.
- * @param event the IVideoPictureEvent from the mediaReader
+ * Creates a thumbnail image from the video image passed in from a file.
+ * @param BufferedImage from the file
*/
- @Override
- public void onVideoPicture(IVideoPictureEvent event) {
+ private void thumbnailFromPicture(BufferedImage image) {
if (!isFinished()) {
- BufferedImage image = event.getImage();
double widthFactor = dim.getWidth()/image.getWidth();
double heightFactor = dim.getHeight()/image.getHeight();
@@ -113,7 +111,7 @@ public void onVideoPicture(IVideoPictureEvent event) {
g.drawImage(image, 0, 0, null);
if (overlay!=null) {
- g.scale(1/factor, 1/factor); // draw overlay at full scale
+ g.scale(1/factor, 1/factor); // draw overlay at full scale
// determine the inset and translate the image
Rectangle2D bounds = new Rectangle2D.Float(0, 0, overlay.getWidth(), overlay.getHeight());
@@ -127,24 +125,21 @@ public void onVideoPicture(IVideoPictureEvent event) {
frameNumber++;
finished = frameNumber>=TARGET_FRAME_NUMBER;
}
-
- // call parent which will pass the video onto next tool in chain
- super.onVideoPicture(event);
-
}
private void initialize(Dimension dimension) {
dim = dimension;
finished = false;
frameNumber = 0;
-// try {
-// String imageFile = "C:/Program Files (x86)/Tracker/tracker_icon.png";
-// overlay = ImageIO.read(new File(imageFile));
-// }
-// catch (IOException e) {
-// e.printStackTrace();
-// throw new RuntimeException("Could not open file");
-// }
+ try {
+ URL imageURL = FFMPegThumbnailTool.class.getResource("../../resources/media/images/tracker_icon.png");
+ overlay = ImageIO.read(imageURL);
+ }
+ catch (IllegalArgumentException e) { }
+ catch (IOException e) {
+ e.printStackTrace();
+ throw new RuntimeException("Could not open file");
+ }
}
@@ -152,4 +147,10 @@ private boolean isFinished() {
return finished;
}
+ public static void main(String[] args) {
+ if(args == null || args.length != 2) {
+ System.err.println("usage: FFMPegThumbnailTool ");
+ }
+ createThumbnailFile(new Dimension(640,480),args[0], args[1]);
+ }
}
diff --git a/src/org/opensourcephysics/media/ffmpeg/FFMPegVideo.java b/src/org/opensourcephysics/media/ffmpeg/FFMPegVideo.java
new file mode 100644
index 0000000..e316146
--- /dev/null
+++ b/src/org/opensourcephysics/media/ffmpeg/FFMPegVideo.java
@@ -0,0 +1,989 @@
+package org.opensourcephysics.media.ffmpeg;
+
+import static org.ffmpeg.avcodec.AvcodecLibrary.av_free_packet;
+import static org.ffmpeg.avcodec.AvcodecLibrary.av_init_packet;
+import static org.ffmpeg.avcodec.AvcodecLibrary.avcodec_decode_video2;
+import static org.ffmpeg.avcodec.AvcodecLibrary.avcodec_find_decoder;
+import static org.ffmpeg.avcodec.AvcodecLibrary.avcodec_open2;
+import static org.ffmpeg.avcodec.AvcodecLibrary.avcodec_flush_buffers;
+import static org.ffmpeg.avformat.AvformatLibrary.av_read_frame;
+import static org.ffmpeg.avformat.AvformatLibrary.avformat_find_stream_info;
+import static org.ffmpeg.avformat.AvformatLibrary.avformat_open_input;
+import static org.ffmpeg.avutil.AvutilLibrary.AV_NOPTS_VALUE;
+import static org.ffmpeg.avutil.AvutilLibrary.av_frame_get_best_effort_timestamp;
+import static org.ffmpeg.avutil.AvutilLibrary.av_freep;
+import static org.ffmpeg.avutil.AvutilLibrary.av_image_copy;
+import static org.ffmpeg.avutil.AVUtil.av_q2d;
+import static org.opensourcephysics.media.ffmpeg.FFMPegAnalyzer.copy;
+import static org.opensourcephysics.media.ffmpeg.FFMPegAnalyzer.isKeyFrame;
+import static org.opensourcephysics.media.ffmpeg.FFMPegAnalyzer.isVideoPacket;
+
+import java.awt.Dimension;
+import java.awt.Frame;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.image.BufferedImage;
+import java.beans.PropertyChangeListener;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+
+import org.bridj.Pointer;
+import org.ffmpeg.avcodec.AVCodec;
+import org.ffmpeg.avcodec.AVCodecContext;
+import org.ffmpeg.avcodec.AVPacket;
+import org.ffmpeg.avcodec.AvcodecLibrary;
+import org.ffmpeg.avformat.AVFormatContext;
+import org.ffmpeg.avformat.AVStream;
+import org.ffmpeg.avformat.AvformatLibrary;
+import org.ffmpeg.avutil.AVFrame;
+import org.ffmpeg.avutil.AVRational;
+import org.ffmpeg.avutil.AvutilLibrary;
+import org.opensourcephysics.controls.OSPLog;
+import org.opensourcephysics.controls.XML;
+import org.opensourcephysics.controls.XMLControl;
+import org.opensourcephysics.media.core.DoubleArray;
+import org.opensourcephysics.media.core.Filter;
+import org.opensourcephysics.media.core.ImageCoordSystem;
+import org.opensourcephysics.media.core.VideoAdapter;
+import org.opensourcephysics.media.core.VideoIO;
+import org.opensourcephysics.media.core.VideoType;
+import org.opensourcephysics.tools.Resource;
+import org.opensourcephysics.tools.ResourceLoader;
+
+/**
+ * A class to display videos using the ffmpeg library.
+ */
+public class FFMPegVideo extends VideoAdapter {
+
+ Pointer context;
+ int streamIndex = -1;
+ Pointer cContext;
+ Pointer codec;
+ Pointer frame;
+ Pointer packet;
+ Pointer> picture = Pointer.allocatePointers(Byte.class, 4);
+ Pointer picture_linesize = Pointer.allocateInts(4);
+ int picture_bufsize;
+ Pointer stream;
+ AVRational timebase;
+ BgrConverter converter;
+ // maps frame number to timestamp of displayed frame (last frame loaded)
+ Map frameTimeStamps = new HashMap();
+ // maps frame number to timestamp of key frame (first frame loaded)
+ Map keyTimeStamps = new HashMap();
+ // array of frame start times in milliseconds
+ private double[] startTimes;
+ private long systemStartPlayTime;
+ private double frameStartPlayTime;
+ private boolean playSmoothly = true;
+ private int frameNr, prevFrameNr;
+ private Timer failDetectTimer;
+
+ /**
+ * Creates a FFMPegVideo and loads a video file specified by name
+ *
+ * @param fileName
+ * the name of the video file
+ * @throws IOException
+ */
+ public FFMPegVideo(final String fileName) throws IOException {
+ Frame[] frames = Frame.getFrames();
+ for (int i = 0, n = frames.length; i < n; i++) {
+ if (frames[i].getName().equals("Tracker")) { //$NON-NLS-1$
+ addPropertyChangeListener(
+ "progress", (PropertyChangeListener) frames[i]); //$NON-NLS-1$
+ addPropertyChangeListener(
+ "stalled", (PropertyChangeListener) frames[i]); //$NON-NLS-1$
+ break;
+ }
+ }
+ // timer to detect failures
+ failDetectTimer = new Timer(6000, new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ if (frameNr == prevFrameNr) {
+ firePropertyChange("stalled", null, fileName); //$NON-NLS-1$
+ failDetectTimer.stop();
+ }
+ prevFrameNr = frameNr;
+ }
+ });
+ failDetectTimer.setRepeats(true);
+ load(fileName);
+ }
+
+ /**
+ * Plays the video at the current rate. Overrides VideoAdapter method.
+ */
+ public void play() {
+ if (getFrameCount() == 1) {
+ return;
+ }
+ int n = getFrameNumber() + 1;
+ playing = true;
+ support.firePropertyChange("playing", null, new Boolean(true)); //$NON-NLS-1$
+ startPlayingAtFrame(n);
+ }
+
+ /**
+ * Stops the video.
+ */
+ public void stop() {
+ playing = false;
+ support.firePropertyChange("playing", null, new Boolean(false)); //$NON-NLS-1$
+ }
+
+ /**
+ * Sets the frame number. Overrides VideoAdapter setFrameNumber method.
+ *
+ * @param n
+ * the desired frame number
+ */
+ public void setFrameNumber(int n) {
+ if (n == getFrameNumber())
+ return;
+ super.setFrameNumber(n);
+ BufferedImage bi = getImage(getFrameNumber());
+ if (bi != null) {
+ rawImage = bi;
+ isValidImage = false;
+ isValidFilteredImage = false;
+ firePropertyChange(
+ "framenumber", null, new Integer(getFrameNumber())); //$NON-NLS-1$
+ if (isPlaying()) {
+ Runnable runner = new Runnable() {
+ public void run() {
+ continuePlaying();
+ }
+ };
+ SwingUtilities.invokeLater(runner);
+ }
+ }
+ }
+
+ /**
+ * Gets the start time of the specified frame in milliseconds.
+ *
+ * @param n
+ * the frame number
+ * @return the start time of the frame in milliseconds, or -1 if not known
+ */
+ public double getFrameTime(int n) {
+ if ((n >= startTimes.length) || (n < 0)) {
+ return -1;
+ }
+ return startTimes[n];
+ }
+
+ /**
+ * Gets the current frame time in milliseconds.
+ *
+ * @return the current time in milliseconds, or -1 if not known
+ */
+ public double getTime() {
+ return getFrameTime(getFrameNumber());
+ }
+
+ /**
+ * Sets the frame number to (nearly) a desired time in milliseconds.
+ *
+ * @param millis
+ * the desired time in milliseconds
+ */
+ public void setTime(double millis) {
+ millis = Math.abs(millis);
+ for (int i = 0; i < startTimes.length; i++) {
+ double t = startTimes[i];
+ if (millis < t) { // find first frame with later start time
+ setFrameNumber(i - 1);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Gets the start frame time in milliseconds.
+ *
+ * @return the start time in milliseconds, or -1 if not known
+ */
+ public double getStartTime() {
+ return getFrameTime(getStartFrameNumber());
+ }
+
+ /**
+ * Sets the start frame to (nearly) a desired time in milliseconds.
+ *
+ * @param millis
+ * the desired start time in milliseconds
+ */
+ public void setStartTime(double millis) {
+ millis = Math.abs(millis);
+ for (int i = 0; i < startTimes.length; i++) {
+ double t = startTimes[i];
+ if (millis < t) { // find first frame with later start time
+ setStartFrameNumber(i - 1);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Gets the end frame time in milliseconds.
+ *
+ * @return the end time in milliseconds, or -1 if not known
+ */
+ public double getEndTime() {
+ int n = getEndFrameNumber();
+ if (n < getFrameCount() - 1)
+ return getFrameTime(n + 1);
+ return getDuration();
+ }
+
+ /**
+ * Sets the end frame to (nearly) a desired time in milliseconds.
+ *
+ * @param millis
+ * the desired end time in milliseconds
+ */
+ public void setEndTime(double millis) {
+ millis = Math.abs(millis);
+ millis = Math.min(getDuration(), millis);
+ for (int i = 0; i < startTimes.length; i++) {
+ double t = startTimes[i];
+ if (millis < t) { // find first frame with later start time
+ setEndFrameNumber(i - 1);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Gets the duration of the video.
+ *
+ * @return the duration of the video in milliseconds, or -1 if not known
+ */
+ public double getDuration() {
+ int n = getFrameCount() - 1;
+ if (n == 0)
+ return 100; // arbitrary duration for single-frame video!
+ // assume last and next-to-last frames have same duration
+ double delta = getFrameTime(n) - getFrameTime(n - 1);
+ return getFrameTime(n) + delta;
+ }
+
+ /**
+ * Sets the relative play rate. Overrides VideoAdapter method.
+ *
+ * @param rate
+ * the relative play rate.
+ */
+ public void setRate(double rate) {
+ super.setRate(rate);
+ if (isPlaying()) {
+ startPlayingAtFrame(getFrameNumber());
+ }
+ }
+
+ private long getTimeStamp(Pointer frame) {
+ long pts = av_frame_get_best_effort_timestamp(frame);
+ if( pts == AV_NOPTS_VALUE)
+ pts = 0;
+ return pts;
+ }
+
+ /**
+ * Disposes of this video.
+ */
+ public void dispose() {
+ super.dispose();
+ if(converter != null) {
+ converter.dispose();
+ }
+ if (cContext != null) {
+ AvcodecLibrary.avcodec_close(cContext);
+ cContext = null;
+ }
+ if (stream != null) {
+ AvcodecLibrary.avcodec_close(stream.get().codec());
+ stream = null;
+ }
+ if (picture != null) {
+ if (picture.getValidElements() > 0)
+ av_freep(picture);
+ picture = null;
+ }
+ packet = null;
+ frame = null;
+ if (context != null) {
+ AvformatLibrary.avformat_close_input(context.getReference());
+ context = null;
+ }
+ }
+
+ /**
+ * Sets the playSmoothly flag.
+ *
+ * @param smooth
+ * true to play smoothly
+ */
+ public void setSmoothPlay(boolean smooth) {
+ playSmoothly = smooth;
+ }
+
+ /**
+ * Gets the playSmoothly flag.
+ *
+ * @return true if playing smoothly
+ */
+ public boolean isSmoothPlay() {
+ return playSmoothly;
+ }
+
+ // ______________________________ private methods _________________________
+
+ /**
+ * Sets the system and frame start times.
+ *
+ * @param frameNumber
+ * the frame number at which playing will start
+ */
+ private void startPlayingAtFrame(int frameNumber) {
+ // systemStartPlayTime is the system time when play starts
+ systemStartPlayTime = System.currentTimeMillis();
+ // frameStartPlayTime is the frame time where play starts
+ frameStartPlayTime = getFrameTime(frameNumber);
+ setFrameNumber(frameNumber);
+ }
+
+ /**
+ * Plays the next time-appropriate frame at the current rate.
+ */
+ private void continuePlaying() {
+ int n = getFrameNumber();
+ if (n < getEndFrameNumber()) {
+ long elapsedTime = System.currentTimeMillis() - systemStartPlayTime;
+ double frameTime = frameStartPlayTime + getRate() * elapsedTime;
+ int frameToPlay = getFrameNumberBefore(frameTime);
+ while (frameToPlay > -1 && frameToPlay <= n) {
+ elapsedTime = System.currentTimeMillis() - systemStartPlayTime;
+ frameTime = frameStartPlayTime + getRate() * elapsedTime;
+ frameToPlay = getFrameNumberBefore(frameTime);
+ }
+ if (frameToPlay == -1)
+ frameToPlay = getEndFrameNumber();
+ // startPlayingAtFrame(frameToPlay);
+ setFrameNumber(frameToPlay);
+ } else if (looping) {
+ startPlayingAtFrame(getStartFrameNumber());
+ } else {
+ stop();
+ }
+ }
+
+ /**
+ * Gets the number of the last frame before the specified time.
+ *
+ * @param time
+ * the time in milliseconds
+ * @return the frame number, or -1 if not found
+ */
+ private int getFrameNumberBefore(double time) {
+ for (int i = 0; i < startTimes.length; i++) {
+ if (time < startTimes[i])
+ return i - 1;
+ }
+ // if not found, see if specified time falls in last frame
+ int n = startTimes.length - 1;
+ // assume last and next-to-last frames have same duration
+ double endTime = 2 * startTimes[n] - startTimes[n - 1];
+ if (time < endTime)
+ return n;
+ return -1;
+ }
+
+ /**
+ * Loads a video specified by name.
+ *
+ * @param fileName
+ * the video file name
+ * @throws IOException
+ */
+ private void load(String fileName) throws IOException {
+ Resource res = ResourceLoader.getResource(fileName);
+ if (res == null) {
+ throw new IOException("unable to create resource for " + fileName); //$NON-NLS-1$
+ }
+ // create and open a FFMPeg container
+ URL url = res.getURL();
+ boolean isLocal = url.getProtocol().toLowerCase().indexOf("file") > -1; //$NON-NLS-1$
+ String path = isLocal ? res.getAbsolutePath() : url.toExternalForm();
+ OSPLog.finest("FFMPeg video loading " + path + " local?: " + isLocal); //$NON-NLS-1$ //$NON-NLS-2$
+ Pointer> pfmt_ctx = Pointer
+ .allocatePointer(AVFormatContext.class);
+ if (avformat_open_input(pfmt_ctx, Pointer.pointerToCString(path), null,
+ null) < 0) {
+ dispose();
+ throw new IOException("unable to open " + fileName); //$NON-NLS-1$
+ }
+ context = pfmt_ctx.get();
+ /* retrieve stream information */
+ if (avformat_find_stream_info(context, null) < 0) {
+ dispose();
+ throw new IOException("unable to find stream info in " + fileName); //$NON-NLS-1$
+ }
+
+ // set up frame data using FFMPegAnalyzer object
+ FFMPegAnalyzer analyzer = null;
+ failDetectTimer.start();
+ frameNr = prevFrameNr = 0;
+ try {
+ analyzer = new FFMPegAnalyzer(path, support);
+ streamIndex = analyzer.getVideoStreamIndex();
+ frameTimeStamps = analyzer.getFrameTimeStamps();
+ keyTimeStamps = analyzer.getKeyTimeStamps();
+
+ // set initial video clip properties
+ frameCount = frameTimeStamps.size();
+ startFrameNumber = 0;
+ endFrameNumber = frameCount - 1;
+
+ // create startTimes array
+ startTimes = analyzer.getStartTimes();
+ } catch(IOException e) {
+ failDetectTimer.stop();
+ dispose();
+ throw new IOException(e.getLocalizedMessage());
+ }
+
+ stream = context.get().streams().get(streamIndex);
+ /* find decoder for the stream */
+ cContext = stream.get().codec();
+ codec = avcodec_find_decoder(cContext.get().codec_id());
+ if (codec == null) {
+ dispose();
+ throw new IOException(
+ "unable to find codec video stream in " + fileName); //$NON-NLS-1$
+ }
+
+ // check that coder opens
+ if (avcodec_open2(cContext, codec, null) < 0) {
+ dispose();
+ throw new IOException(
+ "unable to open video decoder for " + fileName); //$NON-NLS-1$
+ }
+ timebase = copy(stream.get().time_base());
+
+ // throw IOException if no frames were loaded
+ if (frameTimeStamps.size() == 0) {
+ firePropertyChange("progress", fileName, null); //$NON-NLS-1$
+ failDetectTimer.stop();
+ dispose();
+ // VideoIO.setCanceled(true);
+ throw new IOException("packets loaded but no complete picture"); //$NON-NLS-1$
+ }
+
+ // set properties
+ setProperty("name", XML.getName(fileName)); //$NON-NLS-1$
+ setProperty("absolutePath", res.getAbsolutePath()); //$NON-NLS-1$
+ if (fileName.indexOf(":") == -1) { //$NON-NLS-1$
+ // if name is relative, path is name
+ setProperty("path", XML.forwardSlash(fileName)); //$NON-NLS-1$
+ } else {
+ // else path is relative to user directory
+ setProperty("path", XML.getRelativePath(fileName)); //$NON-NLS-1$
+ }
+
+ // initialize frame, packet, picture and image
+ /* allocate image where the decoded image will be put */
+ picture_bufsize = AvutilLibrary.av_image_alloc(picture,
+ picture_linesize, cContext.get().width(), cContext.get()
+ .height(), cContext.get().pix_fmt(), 1);
+ if (picture_bufsize < 0) {
+ dispose();
+ throw new IOException(
+ "unable to allocate raw memory buffer for " + fileName); //$NON-NLS-1$
+ }
+ packet = Pointer.allocate(AVPacket.class);
+ av_init_packet(packet);
+ packet.get().data(null);
+ packet.get().size(0);
+ frame = Pointer.allocate(AVFrame.class);
+ loadNextFrame();
+ BufferedImage img = getImage(0);
+ if (img == null) {
+ for (int i = 1; i < frameTimeStamps.size(); i++) {
+ img = getImage(i);
+ if (img != null)
+ break;
+ }
+ }
+ firePropertyChange("progress", fileName, null); //$NON-NLS-1$
+ failDetectTimer.stop();
+ if (img == null) {
+ dispose();
+ throw new IOException("No images"); //$NON-NLS-1$
+ }
+ setImage(img);
+ }
+
+ /**
+ * Reloads the current video.
+ *
+ * @throws IOException
+ */
+ private void reload() throws IOException {
+ String url = context.get().filename().getCString();
+ if (context != null) {
+ AvformatLibrary.avformat_close_input(context.getReference());
+ context = null;
+ }
+ if (cContext != null) {
+ AvcodecLibrary.avcodec_close(cContext);
+ cContext = null;
+ }
+ stream = null;
+ boolean isLocal = url.toLowerCase().indexOf("file:") > -1; //$NON-NLS-1$
+ String path = isLocal ? ResourceLoader.getNonURIPath(url) : url;
+ Pointer> pfmt_ctx = Pointer
+ .allocatePointer(AVFormatContext.class);
+ if (AvformatLibrary.avformat_open_input(pfmt_ctx,
+ Pointer.pointerToCString(path), null, null) < 0) {
+ dispose();
+ throw new IOException("unable to open " + path); //$NON-NLS-1$
+ }
+ context = pfmt_ctx.get();
+ stream = context.get().streams().get(streamIndex);
+ cContext = stream.get().codec();
+ codec = avcodec_find_decoder(cContext.get().codec_id());
+ if (codec == null) {
+ dispose();
+ throw new IOException(
+ "unable to find codec video stream in " + path); //$NON-NLS-1$
+ }
+
+ // check that coder opens
+ if (avcodec_open2(cContext, codec, null) < 0) {
+ dispose();
+ throw new IOException("unable to open video decoder for " + path); //$NON-NLS-1$
+ }
+ timebase = copy(stream.get().time_base());
+ }
+
+ /**
+ * Sets the initial image.
+ *
+ * @param image
+ * the image
+ */
+ private void setImage(BufferedImage image) {
+ rawImage = image;
+ size = new Dimension(image.getWidth(), image.getHeight());
+ refreshBufferedImage();
+ // create coordinate system and relativeAspects
+ coords = new ImageCoordSystem(frameCount);
+ coords.addPropertyChangeListener(this);
+ aspects = new DoubleArray(frameCount, 1);
+ }
+
+ /**
+ * Returns the key frame with the specified timestamp.
+ *
+ * @param timestamp
+ * the timestamp in stream timebase units
+ * @return true if frame found and loaded
+ */
+ private boolean loadKeyFrame(long timestamp) {
+ // compare requested timestamp with current frame
+ long delta = timestamp - getTimeStamp(frame);
+ long currenttimestamp = Integer.MIN_VALUE;
+ // if delta is zero, return frame
+ if (delta == 0) {
+ return true;
+ }
+ // if delta is positive and short, step forward
+ AVRational timebase = null;
+ timebase = copy(stream.get().time_base());
+ int shortTime = timebase != null ? (int)(1.0/av_q2d(timebase)) : 1; // one second
+ if (delta > 0 && delta < shortTime) {
+ while (loadNextFrame()) {
+ currenttimestamp = getTimeStamp(frame);
+ if (isKeyFrame(frame) && currenttimestamp == timestamp) {
+ return true;
+ }
+ if (currenttimestamp > timestamp) {
+ delta = timestamp - currenttimestamp;
+ break;
+ }
+ }
+ }
+ // if delta is positive and long, seek forward
+ if (delta > 0
+ && AvformatLibrary.av_seek_frame(context, streamIndex,
+ timestamp, 0) >= 0) {
+ avcodec_flush_buffers(cContext);
+ while (loadNextFrame()) {
+ currenttimestamp = getTimeStamp(frame);
+ if (isKeyFrame(frame) && currenttimestamp == timestamp) {
+ return true;
+ }
+ if (currenttimestamp > timestamp) {
+ delta = timestamp - currenttimestamp;
+ break;
+ }
+ }
+ }
+ // if delta is negative, seek backward
+ if (getFrameNumber(timestamp) == 0) {
+ resetContainer();
+ return true;
+ }
+ if (delta < 0
+ && AvformatLibrary.av_seek_frame(context, streamIndex,
+ timestamp, AvformatLibrary.AVSEEK_FLAG_BACKWARD) >= 0) {
+ avcodec_flush_buffers(cContext);
+ while (loadNextFrame()) {
+ currenttimestamp = getTimeStamp(frame);
+ if (isKeyFrame(frame) && currenttimestamp == timestamp) {
+ return true;
+ }
+ if (currenttimestamp > timestamp) {
+ delta = timestamp - currenttimestamp;
+ break;
+ }
+ }
+ }
+
+ // if all else fails, reopen container and step forward
+ resetContainer();
+ while (loadNextFrame()) {
+ currenttimestamp = getTimeStamp(frame);
+ if (isKeyFrame(frame) && currenttimestamp == timestamp) {
+ return true;
+ }
+ if (currenttimestamp > timestamp) {
+ break;
+ }
+ }
+
+ // if still not found, return false
+ return false;
+ }
+
+ /**
+ * Gets the key frame needed to display a specified frame.
+ *
+ * @param frameNumber
+ * the frame number
+ * @return true, if frame found
+ */
+ private boolean loadKeyFrameForFrame(int frameNumber) {
+ long keyTimeStamp = keyTimeStamps.get(frameNumber);
+ return loadKeyFrame(keyTimeStamp);
+ }
+
+ /**
+ * Loads the FFMPeg picture with all data needed to display a specified
+ * frame.
+ *
+ * @param frameNumber
+ * the frame number to load
+ * @return true if loaded successfully
+ */
+ private boolean loadPicture(int frameNumber) {
+ // check to see if seek is needed
+ long currentTS = getTimeStamp(frame);
+ long targetTS = getTimeStamp(frameNumber);
+ long keyTS = keyTimeStamps.get(frameNumber);
+ if (currentTS == targetTS) {
+ // frame is already loaded
+ return true;
+ }
+ if (currentTS >= keyTS && currentTS < targetTS) {
+ // no need to seek--just step forward
+ if (loadNextFrame()) {
+ int n = getFrameNumber(frame);
+ while (n > -2 && n < frameNumber) {
+ if (loadNextFrame()) {
+ n = getFrameNumber(frame);
+ } else
+ return false;
+ }
+ } else
+ return false;
+ } else if (loadKeyFrameForFrame(frameNumber)) {
+ int n = getFrameNumber(frame);
+ while (n > -2 && n < frameNumber) {
+ if (loadNextFrame()) {
+ n = getFrameNumber(frame);
+ } else
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Gets the timestamp for a specified frame.
+ *
+ * @param frameNumber
+ * the frame number
+ * @return the timestamp in stream timebase units
+ */
+ private long getTimeStamp(int frameNumber) {
+ return frameTimeStamps.get(frameNumber);
+ }
+
+ /**
+ * Gets the frame number for a specified timestamp.
+ *
+ * @param timeStamp
+ * the timestamp in stream timebase units
+ * @return the frame number, or -1 if not found
+ */
+ private int getFrameNumber(long timeStamp) {
+ for (int i = 0; i < frameTimeStamps.size(); i++) {
+ long ts = frameTimeStamps.get(i);
+ if (ts == timeStamp)
+ return i;
+ }
+ return -1;
+ }
+
+ /**
+ * Gets the frame number for a specified frame.
+ *
+ * @param packet
+ * the packet
+ * @return the frame number, or -2 if null
+ */
+ private int getFrameNumber(Pointer frame) {
+ if (frame == null)
+ return -2;
+ return getFrameNumber(getTimeStamp(frame));
+ }
+
+ /**
+ * Gets the BufferedImage for a specified frame.
+ *
+ * @param frameNumber
+ * the frame number
+ * @return the image, or null if failed to load
+ */
+ private BufferedImage getImage(int frameNumber) {
+ if (frameNumber < 0 || frameNumber >= frameTimeStamps.size()) {
+ return null;
+ }
+ if (loadPicture(frameNumber)) {
+ // convert picture to buffered image and display
+ return getBufferedImageFromPicture();
+ }
+ return null;
+ }
+
+ /**
+ * Gets the BufferedImage for a specified ffmpeg picture.
+ *
+ * @param picture
+ * the picture
+ * @return the image, or null if unable to resample
+ */
+ private BufferedImage getBufferedImageFromPicture() {
+ // use BgrConverter to convert picture to buffered image
+ try {
+ if (converter == null) {
+ converter = new BgrConverter(cContext.get().pix_fmt(), cContext.get().width(), cContext.get()
+ .height());
+ }
+ } catch(IOException e) {
+ return null;
+ }
+
+ BufferedImage image = null;
+ image = converter.toImage(picture, picture_linesize, picture_bufsize);
+ // garbage collect to play smoothly--but slows down playback speed
+ // significantly!
+ if (playSmoothly)
+ System.gc();
+ return image;
+ }
+
+ /**
+ * Loads the next video frame in the container into the current FFMPeg
+ * picture.
+ *
+ * @return true if successfully loaded
+ */
+ private boolean loadNextFrame() {
+ while (av_read_frame(context, packet) >= 0) {
+ Pointer origPacket = packet;
+ try {
+ if (isVideoPacket(packet, streamIndex)) {
+ // long timeStamp = packet.getTimeStamp();
+ // System.out.println("loading next packet at "+timeStamp+": "+packet.getSize());
+ if( loadFrame() )
+ return true;
+ }
+ } finally {
+ if (origPacket != null) {
+ av_free_packet(origPacket);
+ }
+ }
+ }
+ /* load cached frame */
+ packet.get().data(null);
+ packet.get().size(0);
+ if ( loadFrame() )
+ return true;
+ return false;
+ }
+
+ /**
+ * Loads a video frame into the current ffmpeg picture.
+ *
+ * @return true if successfully loaded, false if no more
+ * frames or no frame loaded.
+ */
+ private boolean loadFrame() {
+ if (frame == null || packet == null)
+ return false;
+ int bytesDecoded;
+ long ptr = 0;
+ do {
+ if(packet.get().size() > 0)
+ ptr = packet.get().data().getPeer();
+ Pointer got_frame = Pointer.allocateInt();
+ // decode the frame into the picture
+ bytesDecoded = avcodec_decode_video2(
+ cContext, frame, got_frame, packet);
+ // check for errors
+ if (bytesDecoded < 0)
+ return false;
+
+ if (got_frame.getInt() == 1) {
+ copyToPicture(frame);
+ return true;
+ }
+ if(packet.get().size() > 0) {
+ ptr+=bytesDecoded;
+ packet.get().data((Pointer)Pointer.pointerToAddress(ptr));
+ packet.get().size(packet.get().size()-bytesDecoded);
+ }
+ } while (packet.get().size() > 0);
+ return false;
+ }
+
+ private void copyToPicture(Pointer frame) {
+ /*
+ * copy decoded frame to destination buffer: this is required since
+ * rawvideo expects non aligned data
+ */
+ av_image_copy(picture, picture_linesize, frame.get().data(), frame
+ .get().linesize(), cContext.get().pix_fmt(), cContext.get()
+ .width(), cContext.get().height());
+ }
+
+ /**
+ * Resets the container to the beginning.
+ */
+ private void resetContainer() {
+ // seek backwards--this will fail for streamed web videos
+ if (AvformatLibrary.av_seek_frame(context, streamIndex, getTimeStamp(0),
+ AvformatLibrary.AVSEEK_FLAG_BACKWARD) >= 0) {
+ avcodec_flush_buffers(cContext);
+ loadNextFrame();
+ } else {
+ try {
+ reload();
+ loadNextFrame();
+ } catch (IOException e) {
+ OSPLog.warning("Container could not be reset"); //$NON-NLS-1$
+ }
+ }
+ }
+
+ /**
+ * Returns an XML.ObjectLoader to save and load FFMPegVideo data.
+ *
+ * @return the object loader
+ */
+ public static XML.ObjectLoader getLoader() {
+ return new Loader();
+ }
+
+ /**
+ * A class to save and load FFMPegVideo data.
+ */
+ static class Loader implements XML.ObjectLoader {
+ /**
+ * Saves FFMPegVideo data to an XMLControl.
+ *
+ * @param control
+ * the control to save to
+ * @param obj
+ * the FFMPegVideo object to save
+ */
+ public void saveObject(XMLControl control, Object obj) {
+ FFMPegVideo video = (FFMPegVideo) obj;
+ String base = (String) video.getProperty("base"); //$NON-NLS-1$
+ String absPath = (String) video.getProperty("absolutePath"); //$NON-NLS-1$
+ control.setValue("path", XML.getPathRelativeTo(absPath, base)); //$NON-NLS-1$
+ if (!video.getFilterStack().isEmpty()) {
+ control.setValue("filters", video.getFilterStack().getFilters()); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Creates a new FFMPegVideo.
+ *
+ * @param control
+ * the control
+ * @return the new FFMPegVideo
+ */
+ public Object createObject(XMLControl control) {
+ try {
+ String path = control.getString("path"); //$NON-NLS-1$
+ String ext = XML.getExtension(path);
+ FFMPegVideo video = new FFMPegVideo(path);
+ VideoType ffmpegType = VideoIO.getVideoType(
+ VideoIO.ENGINE_FFMPEG, ext);
+ if (ffmpegType != null)
+ video.setProperty("video_type", ffmpegType); //$NON-NLS-1$
+ return video;
+ } catch (IOException ex) {
+ OSPLog.fine(ex.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Loads properties into a FFMPegVideo.
+ *
+ * @param control
+ * the control
+ * @param obj
+ * the FFMPegVideo object
+ * @return the loaded object
+ */
+ public Object loadObject(XMLControl control, Object obj) {
+ FFMPegVideo video = (FFMPegVideo) obj;
+ Collection> filters = (Collection>) control
+ .getObject("filters"); //$NON-NLS-1$
+ if (filters != null) {
+ video.getFilterStack().clear();
+ Iterator> it = filters.iterator();
+ while (it.hasNext()) {
+ Filter filter = (Filter) it.next();
+ video.getFilterStack().addFilter(filter);
+ }
+ }
+ return obj;
+ }
+ }
+
+}
diff --git a/src/org/opensourcephysics/media/ffmpeg/FFMPegVideoRecorder.java b/src/org/opensourcephysics/media/ffmpeg/FFMPegVideoRecorder.java
new file mode 100644
index 0000000..81f98c9
--- /dev/null
+++ b/src/org/opensourcephysics/media/ffmpeg/FFMPegVideoRecorder.java
@@ -0,0 +1,39 @@
+package org.opensourcephysics.media.ffmpeg;
+
+import java.awt.Image;
+import java.io.IOException;
+
+import org.opensourcephysics.media.core.ScratchVideoRecorder;
+import org.opensourcephysics.media.ffmpeg.FFMPegVideoType;
+
+public class FFMPegVideoRecorder extends ScratchVideoRecorder {
+
+ /**
+ * Constructs a FFMPegVideoRecorder object.
+ *
+ * @param type
+ * the video type
+ */
+ public FFMPegVideoRecorder(FFMPegVideoType type) {
+ super(type);
+ }
+
+ @Override
+ protected void saveScratch() throws IOException {
+ // TODO Automatisch generierter Methodenstub
+
+ }
+
+ @Override
+ protected boolean startRecording() {
+ // TODO Automatisch generierter Methodenstub
+ return false;
+ }
+
+ @Override
+ protected boolean append(Image image) {
+ // TODO Automatisch generierter Methodenstub
+ return false;
+ }
+
+}
diff --git a/src/org/opensourcephysics/media/ffmpeg/FFMPegVideoType.java b/src/org/opensourcephysics/media/ffmpeg/FFMPegVideoType.java
new file mode 100644
index 0000000..41cc080
--- /dev/null
+++ b/src/org/opensourcephysics/media/ffmpeg/FFMPegVideoType.java
@@ -0,0 +1,178 @@
+package org.opensourcephysics.media.ffmpeg;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.io.File;
+import java.io.IOException;
+import java.util.TreeSet;
+
+import org.opensourcephysics.controls.OSPLog;
+import org.opensourcephysics.media.core.MediaRes;
+import org.opensourcephysics.media.core.Video;
+import org.opensourcephysics.media.core.VideoFileFilter;
+import org.opensourcephysics.media.core.VideoRecorder;
+import org.opensourcephysics.media.core.VideoType;
+
+/**
+ * This implements the VideoType interface with a ffmpeg type.
+ *
+ * @author Frank Schütte
+ * @version 1.0
+ */
+public class FFMPegVideoType implements VideoType {
+
+ protected static TreeSet ffmpegFileFilters = new TreeSet();
+ protected static String ffmpegClass = "org.ffmpeg.FFMPeg"; //$NON-NLS-1$
+ protected static PropertyChangeListener errorListener;
+ protected static boolean isffmpegAvailable = true;
+ protected boolean recordable = true;
+
+ static {
+ errorListener = new PropertyChangeListener() {
+ public void propertyChange(PropertyChangeEvent e) {
+ if (e.getPropertyName().equals("ffmpeg_error")) { //$NON-NLS-1$
+ isffmpegAvailable = false;
+ }
+ }
+ };
+ OSPLog.getOSPLog().addPropertyChangeListener(errorListener);
+ FFMPegThumbnailTool.start();
+ }
+
+ private VideoFileFilter singleTypeFilter; // null for general type
+
+ /**
+ * Constructor attempts to load a ffmpeg class the first time used. This
+ * will throw an error if ffmpeg is not available.
+ */
+ public FFMPegVideoType() {
+ if (!isffmpegAvailable)
+ throw new Error("ffmpeg unavailable"); //$NON-NLS-1$
+ boolean logConsole = OSPLog.isConsoleMessagesLogged();
+ try {
+ OSPLog.setConsoleMessagesLogged(false);
+ Class.forName(ffmpegClass);
+ OSPLog.setConsoleMessagesLogged(logConsole);
+ } catch (Exception ex) {
+ OSPLog.setConsoleMessagesLogged(logConsole);
+ throw new Error("ffmpeg unavailable"); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Constructor with a file filter for a specific container type.
+ *
+ * @param filter
+ * the file filter
+ */
+ public FFMPegVideoType(VideoFileFilter filter) {
+ this();
+ if (filter != null) {
+ singleTypeFilter = filter;
+ ffmpegFileFilters.add(filter);
+ }
+ }
+
+ /**
+ * Opens a named video as a FFMPegVideo.
+ *
+ * @param name the name of the video
+ * @return a new FFMPeg video
+ */
+ public Video getVideo(String name) {
+ try {
+ Video video = new FFMPegVideo(name);
+ video.setProperty("video_type", this); //$NON-NLS-1$
+ return video;
+ } catch(IOException ex) {
+ OSPLog.fine(this.getDescription()+": "+ex.getMessage()); //$NON-NLS-1$
+ return null;
+ }
+ }
+
+ /**
+ * Reports whether this ffmpeg type can record videos
+ *
+ * @return true by default (set recordable to change)
+ */
+ public boolean canRecord() {
+ return recordable;
+ }
+
+ /**
+ * Sets the recordable property
+ *
+ * @param record true if recordable
+ */
+ public void setRecordable(boolean record) {
+ recordable = record;
+ }
+
+ /**
+ * Gets a ffmpeg video recorder.
+ *
+ * @return the video recorder
+ */
+ public VideoRecorder getRecorder() {
+ return new FFMPegVideoRecorder(this);
+ }
+
+ /**
+ * Gets the file filters for this type.
+ *
+ * @return an array of file filters
+ */
+ public VideoFileFilter[] getFileFilters() {
+ if (singleTypeFilter!=null)
+ return new VideoFileFilter[] {singleTypeFilter};
+ return ffmpegFileFilters.toArray(new VideoFileFilter[0]);
+ }
+
+ /**
+ * Gets the default file filter for this type. May return null.
+ *
+ * @return the default file filter
+ */
+ public VideoFileFilter getDefaultFileFilter() {
+ if (singleTypeFilter!=null)
+ return singleTypeFilter;
+ return null;
+ }
+
+ /**
+ * Return true if the specified video is this type.
+ *
+ * @param video the video
+ * @return true if the video is this type
+ */
+ public boolean isType(Video video) {
+ if (!video.getClass().equals(FFMPegVideo.class)) return false;
+ if (singleTypeFilter==null) return true;
+ String name = (String)video.getProperty("name"); //$NON-NLS-1$
+ return singleTypeFilter.accept(new File(name));
+ }
+
+ /**
+ * Gets the name and/or description of this type.
+ *
+ * @return a description
+ */
+ public String getDescription() {
+ if (singleTypeFilter!=null)
+ return singleTypeFilter.getDescription();
+ return MediaRes.getString("FFMPegVideoType.Description"); //$NON-NLS-1$
+ }
+
+ /**
+ * Gets the default extension for this type.
+ *
+ * @return an extension
+ */
+ public String getDefaultExtension() {
+ if (singleTypeFilter!=null) {
+ return singleTypeFilter.getDefaultExtension();
+ }
+ return null;
+ }
+
+}
diff --git a/src/org/opensourcephysics/media/ffmpeg/FFMPegWriterVideoRecorder.java b/src/org/opensourcephysics/media/ffmpeg/FFMPegWriterVideoRecorder.java
new file mode 100644
index 0000000..e97b160
--- /dev/null
+++ b/src/org/opensourcephysics/media/ffmpeg/FFMPegWriterVideoRecorder.java
@@ -0,0 +1,198 @@
+package org.opensourcephysics.media.ffmpeg;
+
+import java.awt.Dimension;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.imageio.ImageIO;
+import javax.swing.filechooser.FileFilter;
+
+import org.opensourcephysics.controls.XML;
+import org.opensourcephysics.media.core.ScratchVideoRecorder;
+import org.opensourcephysics.media.core.VideoFileFilter;
+import org.opensourcephysics.tools.ResourceLoader;
+
+/**
+ * A class to record videos using a FFMPeg IMediaWriter.
+ */
+public class FFMPegWriterVideoRecorder extends ScratchVideoRecorder {
+
+// private IRational timebase = IRational.make(1, 9000);
+ private String tempFileBasePath;
+ private String tempFileType = "png"; //$NON-NLS-1$
+
+ /**
+ * Constructs a FFMPegVideoRecorder object.
+ * @param type the video type
+ */
+ public FFMPegWriterVideoRecorder(FFMPegVideoType type) {
+ super(type);
+ }
+
+ /**
+ * Discards the current video and resets the recorder to a ready state.
+ */
+ @Override
+ public void reset() {
+ deleteTempFiles();
+ super.reset();
+ scratchFile = null;
+ }
+
+ /**
+ * Called by the garbage collector when this recorder is no longer in use.
+ */
+ @Override
+ protected void finalize() {
+ reset();
+ }
+
+ /**
+ * Appends a frame to the current video by saving the image in a tempFile.
+ *
+ * @param image the image to append
+ * @return true if image successfully appended
+ */
+ @Override
+ protected boolean append(Image image) {
+ int w = image.getWidth(null);
+ int h = image.getHeight(null);
+ if (dim==null || (!hasContent && (dim.width!=w || dim.height!=h))) {
+ dim = new Dimension(w, h);
+ }
+ // resize and/or convert to BufferedImage if needed
+ if (dim.width!=w || dim.height!=h || !(image instanceof BufferedImage)) {
+ BufferedImage img = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_RGB);
+ int x = (dim.width-w)/2;
+ int y = (dim.height-h)/2;
+ img.getGraphics().drawImage(image, x, y, null);
+ image = img;
+ }
+ BufferedImage source = (BufferedImage)image;
+ String fileName = tempFileBasePath+"_"+tempFiles.size()+".tmp"; //$NON-NLS-1$ //$NON-NLS-2$
+ try {
+ ImageIO.write(source, tempFileType, new BufferedOutputStream(
+ new FileOutputStream(fileName)));
+ } catch (Exception e) {
+ return false;
+ }
+ File imageFile = new File(fileName);
+ if (imageFile.exists()) {
+ synchronized (tempFiles) {
+ tempFiles.add(imageFile);
+ }
+ imageFile.deleteOnExit();
+ }
+ return true;
+ }
+
+ /**
+ * Saves the video to the scratchFile.
+ *
+ * @throws IOException
+ */
+ @Override
+ protected void saveScratch() throws IOException {
+ FileFilter fileFilter = videoType.getDefaultFileFilter();
+ if (!hasContent || !(fileFilter instanceof VideoFileFilter))
+ return;
+ VideoFileFilter videoFilter = (VideoFileFilter)fileFilter;
+
+ // set up the container format
+// TODO IContainerFormat format = IContainerFormat.make();
+// format.setOutputFormat(videoFilter.getContainerType(), null, null);
+//
+// // get an appropriate codec for this format
+// ICodec codec = ICodec.guessEncodingCodec(format, null, null, null, ICodec.Type.CODEC_TYPE_VIDEO);
+// if (codec == null)
+// throw new UnsupportedOperationException("could not guess video codec");
+//
+// // set scratch file extension to video type
+// String s = XML.stripExtension(scratchFile.getAbsolutePath())+"."+videoFilter.getDefaultExtension();
+// scratchFile = new File(s);
+//
+// // define frame rate
+//// IRational frameRate = IRational.make(1000/frameDuration);
+// IRational frameRate = IRational.make(1000/33);
+//
+// System.out.println("frame rate "+frameRate);
+// // create mediaWriter and add a video stream with id 0, position 0, and fixed frame rate
+// IMediaWriter writer = ToolFactory.makeWriter(scratchFile.getAbsolutePath());
+// writer.addVideoStream(0, 0, codec, frameRate, dim.width, dim.height);
+
+ // open temp images and encode
+ long timeStamp = 0;
+ int n=0;
+ synchronized (tempFiles) {
+ for (File imageFile: tempFiles) {
+
+ if (!imageFile.exists())
+ throw new IOException("temp image file not found"); //$NON-NLS-1$
+
+ BufferedImage image = ResourceLoader.getBufferedImage(imageFile.getAbsolutePath(), BufferedImage.TYPE_3BYTE_BGR);
+ if (image==null || image.getType()!=BufferedImage.TYPE_3BYTE_BGR) {
+ throw new IOException("unable to load temp image file"); //$NON-NLS-1$
+ }
+
+ System.out.println("adding frame "+n+" at "+timeStamp);
+
+// writer.encodeVideo(0, image, timeStamp, TimeUnit.NANOSECONDS);
+
+ n++;
+ timeStamp = Math.round(n*frameDuration*1000000); // frameDuration in ms, timestamp in microsec
+ }
+ }
+// writer.close();
+ deleteTempFiles();
+ hasContent = false;
+ canRecord = false;
+ }
+
+ /**
+ * Starts the video recording process.
+ *
+ * @return true if video recording successfully started
+ */
+ @Override
+ protected boolean startRecording() {
+ try {
+ tempFileBasePath = XML.stripExtension(scratchFile.getAbsolutePath());
+ } catch (Exception e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Given the short name of a container, prints out information about
+ * it, including which codecs ffmpeg can write (mux) into that container.
+ *
+ * @param name the short name of the format (e.g. "flv")
+ */
+// TODO public static void getSupportedCodecs(String name) {
+// IContainerFormat format = IContainerFormat.make();
+// format.setOutputFormat(name, null, null);
+//
+// List codecs = format.getOutputCodecsSupported();
+// if (codecs.isEmpty())
+// System.out.println("no supported codecs for "+name); //$NON-NLS-1$
+// else {
+// System.out.println(name+" ("+format+") supports following codecs:"); //$NON-NLS-1$ //$NON-NLS-2$
+// for(ID id : codecs) {
+// if (id != null) {
+// ICodec codec = ICodec.findEncodingCodec(id);
+// if (codec != null) {
+// System.out.println(codec);
+// }
+// }
+// }
+// }
+// }
+
+}
diff --git a/src/org/opensourcephysics/media/xuggle/XuggleIO.java b/src/org/opensourcephysics/media/xuggle/XuggleIO.java
deleted file mode 100644
index 0208f3c..0000000
--- a/src/org/opensourcephysics/media/xuggle/XuggleIO.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * The org.opensourcephysics.media.xuggle package provides Xuggle
- * services including implementations of the Video and VideoRecorder interfaces.
- *
- * Copyright (c) 2017 Douglas Brown and Wolfgang Christian.
- *
- * This is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
- * or view the license online at http://www.gnu.org/copyleft/gpl.html
- *
- * For additional information and documentation on Open Source Physics,
- * please see .
- */
-package org.opensourcephysics.media.xuggle;
-
-import org.opensourcephysics.controls.OSPLog;
-import org.opensourcephysics.media.core.VideoFileFilter;
-import org.opensourcephysics.media.core.VideoIO;
-import org.opensourcephysics.tools.ResourceLoader;
-
-/**
- * This registers Xuggle with VideoIO so it can be used to open and record videos.
- *
- * @author Wolfgang Christian, Douglas Brown
- * @version 1.0
- */
-public class XuggleIO {
-
- /**
- * Registers Xuggle video types with VideoIO class.
- */
- static public void registerWithVideoIO(){ // add Xuggle video types, if available
- String xugglehome = System.getenv("XUGGLE_HOME"); //$NON-NLS-1$
- if (xugglehome!=null) {
- try {
- VideoIO.addVideoEngine(new XuggleVideoType());
-
- // add common video types
- for (String ext: VideoIO.VIDEO_EXTENSIONS) { // {"mov", "avi", "mp4"}
- VideoFileFilter filter = new VideoFileFilter(ext, new String[] {ext});
- XuggleVideoType xuggleType = new XuggleVideoType(filter);
- // avi not recordable with xuggle
- if (ext.equals("avi")) { //$NON-NLS-1$
- xuggleType.setRecordable(false);
- }
- VideoIO.addVideoType(xuggleType);
- ResourceLoader.addExtractExtension(ext);
- }
- // add additional xuggle types
- // FLV
- VideoFileFilter filter = new VideoFileFilter("flv", new String[] {"flv"}); //$NON-NLS-1$ //$NON-NLS-2$
- VideoIO.addVideoType(new XuggleVideoType(filter));
- ResourceLoader.addExtractExtension("flv"); //$NON-NLS-1$
- // WMV
- filter = new VideoFileFilter("asf", new String[] {"wmv"}); //$NON-NLS-1$ //$NON-NLS-2$
- VideoIO.addVideoType(new XuggleVideoType(filter));
- ResourceLoader.addExtractExtension("wmv"); //$NON-NLS-1$
- // DV
- filter = new VideoFileFilter("dv", new String[] {"dv"}); //$NON-NLS-1$ //$NON-NLS-2$
- XuggleVideoType vidType = new XuggleVideoType(filter);
- vidType.setRecordable(false);
- VideoIO.addVideoType(vidType);
- ResourceLoader.addExtractExtension("dv"); //$NON-NLS-1$
- // MTS
- filter = new VideoFileFilter("mts", new String[] {"mts"}); //$NON-NLS-1$ //$NON-NLS-2$
- vidType = new XuggleVideoType(filter);
- vidType.setRecordable(false);
- VideoIO.addVideoType(vidType);
- ResourceLoader.addExtractExtension("mts"); //$NON-NLS-1$
- // M2TS
- filter = new VideoFileFilter("m2ts", new String[] {"m2ts"}); //$NON-NLS-1$ //$NON-NLS-2$
- vidType = new XuggleVideoType(filter);
- vidType.setRecordable(false);
- VideoIO.addVideoType(vidType);
- ResourceLoader.addExtractExtension("m2ts"); //$NON-NLS-1$
- // MPG
- filter = new VideoFileFilter("mpg", new String[] {"mpg"}); //$NON-NLS-1$ //$NON-NLS-2$
- vidType = new XuggleVideoType(filter);
- vidType.setRecordable(false);
- VideoIO.addVideoType(vidType);
- ResourceLoader.addExtractExtension("mpg"); //$NON-NLS-1$
- // MOD
- filter = new VideoFileFilter("mod", new String[] {"mod"}); //$NON-NLS-1$ //$NON-NLS-2$
- vidType = new XuggleVideoType(filter);
- vidType.setRecordable(false);
- VideoIO.addVideoType(vidType);
- ResourceLoader.addExtractExtension("mod"); //$NON-NLS-1$
- // OGG
- filter = new VideoFileFilter("ogg", new String[] {"ogg"}); //$NON-NLS-1$ //$NON-NLS-2$
- vidType = new XuggleVideoType(filter);
- vidType.setRecordable(false);
- VideoIO.addVideoType(vidType);
- ResourceLoader.addExtractExtension("ogg"); //$NON-NLS-1$
- ResourceLoader.addExtractExtension("mod"); //$NON-NLS-1$
- // WEBM unsupported by Xuggle
- }
- catch (Exception ex) { // Xuggle not working
- OSPLog.config("Xuggle exception: "+ex.toString()); //$NON-NLS-1$
- }
- catch (Error er) { // Xuggle not working
- OSPLog.config("Xuggle error: "+er.toString()); //$NON-NLS-1$
- }
- }
- else {
- OSPLog.config("Xuggle not installed? (XUGGLE_HOME not found)"); //$NON-NLS-1$
- }
- }
-
-}
diff --git a/src/org/opensourcephysics/media/xuggle/XuggleVideo.java b/src/org/opensourcephysics/media/xuggle/XuggleVideo.java
deleted file mode 100644
index 2f6ca0f..0000000
--- a/src/org/opensourcephysics/media/xuggle/XuggleVideo.java
+++ /dev/null
@@ -1,1002 +0,0 @@
-/*
- * The org.opensourcephysics.media.xuggle package provides Xuggle
- * services including implementations of the Video and VideoRecorder interfaces.
- *
- * Copyright (c) 2017 Douglas Brown and Wolfgang Christian.
- *
- * This is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
- * or view the license online at http://www.gnu.org/copyleft/gpl.html
- *
- * For additional information and documentation on Open Source Physics,
- * please see .
- */
-package org.opensourcephysics.media.xuggle;
-
-import java.awt.Dimension;
-import java.awt.Frame;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.image.BufferedImage;
-import java.beans.PropertyChangeListener;
-import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-import javax.swing.SwingUtilities;
-import javax.swing.Timer;
-
-import org.opensourcephysics.controls.OSPLog;
-import org.opensourcephysics.controls.XML;
-import org.opensourcephysics.controls.XMLControl;
-import org.opensourcephysics.media.core.DoubleArray;
-import org.opensourcephysics.media.core.Filter;
-import org.opensourcephysics.media.core.ImageCoordSystem;
-import org.opensourcephysics.media.core.VideoAdapter;
-import org.opensourcephysics.media.core.VideoIO;
-import org.opensourcephysics.media.core.VideoType;
-import org.opensourcephysics.tools.Resource;
-import org.opensourcephysics.tools.ResourceLoader;
-
-import com.xuggle.xuggler.ICodec;
-import com.xuggle.xuggler.IContainer;
-import com.xuggle.xuggler.IPacket;
-import com.xuggle.xuggler.IPixelFormat;
-import com.xuggle.xuggler.IRational;
-import com.xuggle.xuggler.IStream;
-import com.xuggle.xuggler.IStreamCoder;
-import com.xuggle.xuggler.IVideoPicture;
-import com.xuggle.xuggler.IVideoResampler;
-import com.xuggle.xuggler.video.ConverterFactory;
-import com.xuggle.xuggler.video.IConverter;
-
-/**
- * A class to display videos using the Xuggle library. Xuggle in turn
- * uses FFMpeg as its video engine.
- */
-public class XuggleVideo extends VideoAdapter {
-
- IContainer container;
- int streamIndex = -1;
- IStreamCoder videoCoder;
- IVideoResampler resampler;
- IPacket packet;
- IVideoPicture picture;
- IStream stream;
- IRational timebase;
- IConverter converter;
- // maps frame number to timestamp of displayed packet (last packet loaded)
- Map frameTimeStamps = new HashMap();
- // maps frame number to timestamp of key packet (first packet loaded)
- Map keyTimeStamps = new HashMap();
- // array of frame start times in milliseconds
- private double[] startTimes;
- private long systemStartPlayTime;
- private double frameStartPlayTime;
- private boolean playSmoothly = true;
- private int frame, prevFrame;
- private Timer failDetectTimer;
-
- /**
- * Creates a XuggleVideo and loads a video file specified by name
- *
- * @param fileName the name of the video file
- * @throws IOException
- */
- public XuggleVideo(final String fileName) throws IOException {
- Frame[] frames = Frame.getFrames();
- for(int i = 0, n = frames.length; i=startTimes.length)||(n<0)) {
- return -1;
- }
- return startTimes[n];
- }
-
- /**
- * Gets the current frame time in milliseconds.
- *
- * @return the current time in milliseconds, or -1 if not known
- */
- public double getTime() {
- return getFrameTime(getFrameNumber());
- }
-
- /**
- * Sets the frame number to (nearly) a desired time in milliseconds.
- *
- * @param millis the desired time in milliseconds
- */
- public void setTime(double millis) {
- millis = Math.abs(millis);
- for(int i = 0; i-1 && frameToPlay<=n) {
- elapsedTime = System.currentTimeMillis()-systemStartPlayTime;
- frameTime = frameStartPlayTime+getRate()*elapsedTime;
- frameToPlay = getFrameNumberBefore(frameTime);
- }
- if (frameToPlay==-1) {
- frameToPlay = getEndFrameNumber();
- }
- setFrameNumber(frameToPlay);
- }
- else if(looping) {
- startPlayingAtFrame(getStartFrameNumber());
- }
- else {
- stop();
- }
- }
-
- /**
- * Gets the number of the last frame before the specified time.
- *
- * @param time the time in milliseconds
- * @return the frame number, or -1 if not found
- */
- private int getFrameNumberBefore(double time) {
- for (int i = 0; i < startTimes.length; i++) {
- if (time < startTimes[i])
- return i-1;
- }
- // if not found, see if specified time falls in last frame
- int n = startTimes.length-1;
- // assume last and next-to-last frames have same duration
- double endTime = 2*startTimes[n]-startTimes[n-1];
- if (time < endTime)
- return n;
- return -1;
- }
-
- /**
- * Loads a video specified by name.
- *
- * @param fileName the video file name
- * @throws IOException
- */
- @SuppressWarnings("deprecation")
- private void load(String fileName) throws IOException {
- Resource res = ResourceLoader.getResource(fileName);
- if (res==null) {
- throw new IOException("unable to create resource for "+fileName); //$NON-NLS-1$
- }
- // create and open a Xuggle container
- URL url = res.getURL();
- boolean isLocal = url.getProtocol().toLowerCase().indexOf("file")>-1; //$NON-NLS-1$
- String path = isLocal? res.getAbsolutePath(): url.toExternalForm();
- OSPLog.finest("Xuggle video loading "+path+" local?: "+isLocal); //$NON-NLS-1$ //$NON-NLS-2$
- container = IContainer.make();
- if (isLocal) { // random access file handles non-ascii unicode characters
- RandomAccessFile raf = new RandomAccessFile(path, "r"); //$NON-NLS-1$
- if (container.open(raf, IContainer.Type.READ, null) < 0) {
- dispose();
- throw new IOException("unable to open "+fileName); //$NON-NLS-1$
- }
- }
- else if (container.open(path, IContainer.Type.READ, null) < 0) {
- dispose();
- throw new IOException("unable to open "+fileName); //$NON-NLS-1$
- }
-
- // find the first video stream in the container
- for (int i = 0; i < container.getNumStreams(); i++) {
- IStream nextStream = container.getStream(i);
- // get the pre-configured decoder that can decode this stream
- IStreamCoder coder = nextStream.getStreamCoder();
- // get the type of stream from the coder's codec type
- if (coder.getCodecType().equals(ICodec.Type.CODEC_TYPE_VIDEO)) {
- stream = nextStream;
- streamIndex = i;
- videoCoder = coder;
- timebase = stream.getTimeBase().copy();
- break;
- }
- }
-
- // check that a video stream was found
- if (streamIndex == -1) {
- dispose();
- throw new IOException("no video stream found in "+fileName); //$NON-NLS-1$
- }
-
- // check that coder opens
- if (videoCoder.open() < 0) {
- dispose();
- throw new IOException("unable to open video decoder for "+fileName); //$NON-NLS-1$
- }
-
- // set properties
- setProperty("name", XML.getName(fileName)); //$NON-NLS-1$
- setProperty("absolutePath", res.getAbsolutePath()); //$NON-NLS-1$
- if(fileName.indexOf(":")==-1) { //$NON-NLS-1$
- // if name is relative, path is name
- setProperty("path", XML.forwardSlash(fileName)); //$NON-NLS-1$
- } else {
- // else path is relative to user directory
- setProperty("path", XML.getRelativePath(fileName)); //$NON-NLS-1$
- }
-
- // set up frame data using temporary container
- IContainer tempContainer = IContainer.make();
- if (isLocal) {
- RandomAccessFile tempRaf = new RandomAccessFile(path, "r"); //$NON-NLS-1$
- tempContainer.open(tempRaf, IContainer.Type.READ, null);
- }
- else {
- tempContainer.open(container.getURL(), IContainer.Type.READ, null);
- }
- IStream tempStream = tempContainer.getStream(streamIndex);
- IStreamCoder tempCoder = tempStream.getStreamCoder();
- tempCoder.open();
-
- IVideoPicture tempPicture = IVideoPicture.make(tempCoder.getPixelType(),
- tempCoder.getWidth(), tempCoder.getHeight());
- IPacket tempPacket = IPacket.make();
- long keyTimeStamp = Long.MIN_VALUE;
- long startTimeStamp = Long.MIN_VALUE;
- ArrayList seconds = new ArrayList();
- firePropertyChange("progress", fileName, 0); //$NON-NLS-1$
- frame = prevFrame = 0;
- failDetectTimer.start();
- // step thru container and find all video frames
- while (tempContainer.readNextPacket(tempPacket)>=0) {
- if (VideoIO.isCanceled()) {
- failDetectTimer.stop();
- firePropertyChange("progress", fileName, null); //$NON-NLS-1$
- // clean up temporary objects
- tempCoder.close();
- tempCoder.delete();
- tempStream.delete();
- tempPicture.delete();
- tempPacket.delete();
- tempContainer.close();
- tempContainer.delete();
- dispose();
- throw new IOException("Canceled by user"); //$NON-NLS-1$
- }
- if (isVideoPacket(tempPacket)) {
- if (keyTimeStamp == Long.MIN_VALUE || tempPacket.isKeyPacket()) {
- keyTimeStamp = tempPacket.getTimeStamp();
- }
- int offset = 0;
- while(offset < tempPacket.getSize()) {
- // decode the packet into the picture
- int bytesDecoded = tempCoder.decodeVideo(tempPicture, tempPacket, offset);
- // check for errors
- if (bytesDecoded < 0)
- break;
- offset += bytesDecoded;
- if (tempPicture.isComplete()) {
- if (startTimeStamp == Long.MIN_VALUE) {
- startTimeStamp = tempPacket.getTimeStamp();
- }
- frameTimeStamps.put(frame, tempPacket.getTimeStamp());
- seconds.add((tempPacket.getTimeStamp()-startTimeStamp)*timebase.getValue());
- keyTimeStamps.put(frame, keyTimeStamp);
- firePropertyChange("progress", fileName, frame); //$NON-NLS-1$
- frame++;
- }
- }
- }
- }
- // clean up temporary objects
- tempCoder.close();
- tempCoder.delete();
- tempStream.delete();
- tempPicture.delete();
- tempPacket.delete();
- tempContainer.close();
- tempContainer.delete();
-
- // throw IOException if no frames were loaded
- if (frameTimeStamps.size()==0) {
- firePropertyChange("progress", fileName, null); //$NON-NLS-1$
- failDetectTimer.stop();
- dispose();
- throw new IOException("packets loaded but no complete picture"); //$NON-NLS-1$
- }
-
- // set initial video clip properties
- frameCount = frameTimeStamps.size();
- startFrameNumber = 0;
- endFrameNumber = frameCount-1;
- // create startTimes array
- startTimes = new double[frameCount];
- startTimes[0] = 0;
- for(int i = 1; i-1; //$NON-NLS-1$
- String path = isLocal? ResourceLoader.getNonURIPath(url): url;
- container = IContainer.make();
- if (isLocal) {
- RandomAccessFile raf = new RandomAccessFile(path, "r"); //$NON-NLS-1$
- container.open(raf, IContainer.Type.READ, null);
- }
- else {
- container.open(path, IContainer.Type.READ, null);
- }
- stream = container.getStream(streamIndex);
- videoCoder = stream.getStreamCoder();
- videoCoder.open();
- }
-
- /**
- * Sets the initial image.
- *
- * @param image the image
- */
- private void setImage(BufferedImage image) {
- rawImage = image;
- size = new Dimension(image.getWidth(), image.getHeight());
- refreshBufferedImage();
- // create coordinate system and relativeAspects
- coords = new ImageCoordSystem(frameCount);
- coords.addPropertyChangeListener(this);
- aspects = new DoubleArray(frameCount, 1);
- }
-
- /**
- * Determines if a packet is a key packet.
- *
- * @param packet the packet
- * @return true if packet is a key in the video stream
- */
- private boolean isKeyPacket(IPacket packet) {
- if (isVideoPacket(packet)
- && packet.isKeyPacket()) {
- return true;
- }
- return false;
- }
-
- /**
- * Determines if a packet is a video packet.
- *
- * @param packet the packet
- * @return true if packet is in the video stream
- */
- private boolean isVideoPacket(IPacket packet) {
- if (packet.getStreamIndex() == streamIndex) {
- return true;
- }
- return false;
- }
-
- /**
- * Returns the key packet with the specified timestamp.
- *
- * @param timestamp the timestamp in stream timebase units
- * @return the packet, or null if none found
- */
- private IPacket getKeyPacket(long timestamp) {
- // compare requested timestamp with current packet
- long delta = timestamp-packet.getTimeStamp();
- // if delta is zero, return packet
- if (delta==0) {
- return packet;
- }
- // if delta is positive and short, step forward
- IRational timebase = packet.getTimeBase();
- int shortTime = timebase.getDenominator(); // one second
- if (delta>0 && delta=0) {
- if (isKeyPacket(packet)
- && packet.getTimeStamp() == timestamp) {
- return packet;
- }
- if (isVideoPacket(packet) && packet.getTimeStamp()>timestamp) {
- delta = timestamp-packet.getTimeStamp();
- break;
- }
- }
- }
- // if delta is positive and long, seek forward
- if (delta>0 && container.seekKeyFrame(streamIndex,
- timestamp, timestamp, timestamp, 0)>=0) {
- while (container.readNextPacket(packet)>=0) {
- if (isKeyPacket(packet)
- && packet.getTimeStamp() == timestamp) {
- return packet;
- }
- if (isVideoPacket(packet) && packet.getTimeStamp()>timestamp) {
- delta = timestamp-packet.getTimeStamp();
- break;
- }
- }
- }
- // if delta is negative, seek backward
- if (getFrameNumber(timestamp)==0) {
- resetContainer();
- return packet;
- }
- if (delta<0 && container.seekKeyFrame(streamIndex,
- timestamp, timestamp, timestamp, IContainer.SEEK_FLAG_BACKWARDS)>=0) {
- while (container.readNextPacket(packet)>=0) {
- if (isKeyPacket(packet) && isVideoPacket(packet)
- && packet.getTimeStamp() == timestamp) {
- return packet;
- }
- if (isVideoPacket(packet) && packet.getTimeStamp()>timestamp) {
- delta = timestamp-packet.getTimeStamp();
- break;
- }
- }
- }
-
- // if all else fails, reopen container and step forward
- resetContainer();
- while (container.readNextPacket(packet)>=0) {
- if (isKeyPacket(packet)
- && packet.getTimeStamp() == timestamp) {
- return packet;
- }
- if (isVideoPacket(packet) && packet.getTimeStamp()>timestamp) {
- break;
- }
- }
-
- // if still not found, return null
- return null;
- }
-
- /**
- * Gets the key packet needed to display a specified frame.
- *
- * @param frameNumber the frame number
- * @return the packet, or null if none found
- */
- private IPacket getKeyPacketForFrame(int frameNumber) {
- long keyTimeStamp = keyTimeStamps.get(frameNumber);
- return getKeyPacket(keyTimeStamp);
- }
-
- /**
- * Loads the Xuggle picture with all data needed to display a specified frame.
- *
- * @param frameNumber the frame number to load
- * @return true if loaded successfully
- */
- private boolean loadPicture(int frameNumber) {
- // check to see if seek is needed
- long currentTS = packet.getTimeStamp();
- long targetTS = getTimeStamp(frameNumber);
- long keyTS = keyTimeStamps.get(frameNumber);
- if (currentTS==targetTS && isVideoPacket(packet)) {
- // frame is already loaded
- return picture.isComplete();
- }
- if (currentTS>=keyTS && currentTS -2 && n < frameNumber) {
- if (loadNextPacket()) {
- n = getFrameNumber(packet);
- }
- else return false;
- }
- }
- else return false;
- }
- else if (getKeyPacketForFrame(frameNumber)!=null) {
- if (loadPacket(packet)) {
- int n = getFrameNumber(packet);
- while (n > -2 && n < frameNumber) {
- if (loadNextPacket()) {
- n = getFrameNumber(packet);
- }
- else return false;
- }
- }
- else return false;
- }
- return picture.isComplete();
- }
-
- /**
- * Gets the timestamp for a specified frame.
- *
- * @param frameNumber the frame number
- * @return the timestamp in stream timebase units
- */
- private long getTimeStamp(int frameNumber) {
- return frameTimeStamps.get(frameNumber);
- }
-
- /**
- * Gets the frame number for a specified timestamp.
- *
- * @param timeStamp the timestamp in stream timebase units
- * @return the frame number, or -1 if not found
- */
- private int getFrameNumber(long timeStamp) {
- for (int i = 0; i < frameTimeStamps.size(); i++) {
- long ts = frameTimeStamps.get(i);
- if (ts == timeStamp)
- return i;
- }
- return -1;
- }
-
- /**
- * Gets the frame number for a specified packet.
- *
- * @param packet the packet
- * @return the frame number, or -2 if not a video packet
- */
- private int getFrameNumber(IPacket packet) {
- if (packet.getStreamIndex() != streamIndex)
- return -2;
- return getFrameNumber(packet.getTimeStamp());
- }
-
- /**
- * Gets the BufferedImage for a specified frame.
- *
- * @param frameNumber the frame number
- * @return the image, or null if failed to load
- */
- private BufferedImage getImage(int frameNumber) {
- if (frameNumber<0 || frameNumber>=frameTimeStamps.size()) {
- return null;
- }
- if (loadPicture(frameNumber)) {
- // convert picture to buffered image and display
- return getBufferedImage(picture);
- }
- return null;
- }
-
- /**
- * Gets the BufferedImage for a specified Xuggle picture.
- *
- * @param picture the picture
- * @return the image, or null if unable to resample
- */
- private BufferedImage getBufferedImage(IVideoPicture picture) {
- // if needed, convert picture into BGR24 format
- if (picture.getPixelType() != IPixelFormat.Type.BGR24) {
- if (resampler == null) {
- resampler = IVideoResampler.make(
- picture.getWidth(), picture.getHeight(), IPixelFormat.Type.BGR24,
- picture.getWidth(), picture.getHeight(), picture.getPixelType());
- if (resampler == null) {
- OSPLog.warning("Could not create color space resampler"); //$NON-NLS-1$
- return null;
- }
- }
- IVideoPicture newPic = IVideoPicture.make(resampler.getOutputPixelFormat(),
- picture.getWidth(), picture.getHeight());
- if (resampler.resample(newPic, picture) < 0
- || newPic.getPixelType() != IPixelFormat.Type.BGR24) {
- OSPLog.warning("Could not encode video as BGR24"); //$NON-NLS-1$
- return null;
- }
- picture = newPic;
- }
-
- // use IConverter to convert picture to buffered image
- if (converter==null) {
- ConverterFactory.Type type = ConverterFactory.findRegisteredConverter(
- ConverterFactory.XUGGLER_BGR_24);
- converter = ConverterFactory.createConverter(type.getDescriptor(), picture);
- }
- BufferedImage image = converter.toImage(picture);
- // garbage collect to play smoothly--but slows down playback speed significantly!
- if (playSmoothly)
- System.gc();
- return image;
- }
-
- /**
- * Loads the next video packet in the container into the current Xuggle picture.
- *
- * @return true if successfully loaded
- */
- private boolean loadNextPacket() {
- while (container.readNextPacket(packet)>=0) {
- if (isVideoPacket(packet)) {
-// long timeStamp = packet.getTimeStamp();
-// System.out.println("loading next packet at "+timeStamp+": "+packet.getSize());
- return loadPacket(packet);
- }
- }
- return false;
- }
-
- /**
- * Loads a video packet into the current Xuggle picture.
- *
- * @param packet the packet
- * @return true if successfully loaded
- */
- private boolean loadPacket(IPacket packet) {
- int offset = 0;
- int size = packet.getSize();
- while(offset < size) {
- // decode the packet into the picture
- int bytesDecoded = videoCoder.decodeVideo(picture, packet, offset);
- // check for errors
- if (bytesDecoded < 0)
- return false;
-
- offset += bytesDecoded;
- if (picture.isComplete()) {
- return true;
- }
- }
- return true;
- }
-
- /**
- * Resets the container to the beginning.
- */
- private void resetContainer() {
- // seek backwards--this will fail for streamed web videos
- if (container.seekKeyFrame(-1, // stream index -1 ==> seek to microseconds
- Long.MIN_VALUE, 0, Long.MAX_VALUE,
- IContainer.SEEK_FLAG_BACKWARDS)>=0) {
- loadNextPacket();
- }
- else {
- try {
- reload();
- loadNextPacket();
- } catch (IOException e) {
- OSPLog.warning("Container could not be reset"); //$NON-NLS-1$
- }
- }
- }
-
- /**
- * Returns an XML.ObjectLoader to save and load XuggleVideo data.
- *
- * @return the object loader
- */
- public static XML.ObjectLoader getLoader() {
- return new Loader();
- }
-
- /**
- * A class to save and load XuggleVideo data.
- */
- static class Loader implements XML.ObjectLoader {
- /**
- * Saves XuggleVideo data to an XMLControl.
- *
- * @param control the control to save to
- * @param obj the XuggleVideo object to save
- */
- public void saveObject(XMLControl control, Object obj) {
- XuggleVideo video = (XuggleVideo) obj;
- String base = (String) video.getProperty("base"); //$NON-NLS-1$
- String absPath = (String) video.getProperty("absolutePath"); //$NON-NLS-1$
- control.setValue("path", XML.getPathRelativeTo(absPath, base)); //$NON-NLS-1$
- if(!video.getFilterStack().isEmpty()) {
- control.setValue("filters", video.getFilterStack().getFilters()); //$NON-NLS-1$
- }
- }
-
- /**
- * Creates a new XuggleVideo.
- *
- * @param control the control
- * @return the new XuggleVideo
- */
- public Object createObject(XMLControl control) {
- try {
- String path = control.getString("path"); //$NON-NLS-1$
- String ext = XML.getExtension(path);
- XuggleVideo video = new XuggleVideo(path);
- VideoType xuggleType = VideoIO.getVideoType(VideoIO.ENGINE_XUGGLE, ext);
- if (xuggleType!=null)
- video.setProperty("video_type", xuggleType); //$NON-NLS-1$
- return video;
- } catch(IOException ex) {
- OSPLog.fine(ex.getMessage());
- return null;
- }
- }
-
- /**
- * Loads properties into a XuggleVideo.
- *
- * @param control the control
- * @param obj the XuggleVideo object
- * @return the loaded object
- */
- public Object loadObject(XMLControl control, Object obj) {
- XuggleVideo video = (XuggleVideo) obj;
- Collection> filters = (Collection>) control.getObject("filters"); //$NON-NLS-1$
- if(filters!=null) {
- video.getFilterStack().clear();
- Iterator> it = filters.iterator();
- while(it.hasNext()) {
- Filter filter = (Filter) it.next();
- video.getFilterStack().addFilter(filter);
- }
- }
- return obj;
- }
- }
-
-}
diff --git a/src/org/opensourcephysics/media/xuggle/XuggleVideoRecorder.java b/src/org/opensourcephysics/media/xuggle/XuggleVideoRecorder.java
deleted file mode 100644
index ae42ea1..0000000
--- a/src/org/opensourcephysics/media/xuggle/XuggleVideoRecorder.java
+++ /dev/null
@@ -1,394 +0,0 @@
-/*
- * The org.opensourcephysics.media.xuggle package provides Xuggle
- * services including implementations of the Video and VideoRecorder interfaces.
- *
- * Copyright (c) 2017 Douglas Brown and Wolfgang Christian.
- *
- * This is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
- * or view the license online at http://www.gnu.org/copyleft/gpl.html
- *
- * For additional information and documentation on Open Source Physics,
- * please see .
- */
-package org.opensourcephysics.media.xuggle;
-
-import java.awt.Dimension;
-import java.awt.Image;
-import java.awt.image.BufferedImage;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.List;
-
-import javax.imageio.ImageIO;
-import javax.swing.filechooser.FileFilter;
-
-import org.opensourcephysics.controls.OSPLog;
-import org.opensourcephysics.controls.XML;
-import org.opensourcephysics.media.core.ScratchVideoRecorder;
-import org.opensourcephysics.media.core.VideoFileFilter;
-import org.opensourcephysics.tools.ResourceLoader;
-
-import com.xuggle.xuggler.ICodec;
-import com.xuggle.xuggler.IContainer;
-import com.xuggle.xuggler.IContainerFormat;
-//import com.xuggle.xuggler.IMetaData;
-import com.xuggle.xuggler.IPacket;
-import com.xuggle.xuggler.IPixelFormat;
-import com.xuggle.xuggler.IRational;
-import com.xuggle.xuggler.IStream;
-import com.xuggle.xuggler.IStreamCoder;
-import com.xuggle.xuggler.IVideoPicture;
-import com.xuggle.xuggler.ICodec.ID;
-import com.xuggle.xuggler.video.ConverterFactory;
-import com.xuggle.xuggler.video.IConverter;
-
-/**
- * A class to record videos using the Xuggle video engine.
- */
-public class XuggleVideoRecorder extends ScratchVideoRecorder {
-
- private IContainer outContainer;
- private IStream outStream;
- private IStreamCoder outStreamCoder;
- private IConverter outConverter;
-// private IRational timebase = IRational.make(1, 9000);
- private Dimension converterDim;
-
- /**
- * Constructs a XuggleVideoRecorder object.
- * @param type the video type
- */
- public XuggleVideoRecorder(XuggleVideoType type) {
- super(type);
- }
-
- /**
- * Discards the current video and resets the recorder to a ready state.
- */
- @Override
- public void reset() {
- try {
- closeStream();
- } catch (IOException e) {}
- if (outConverter!=null) {
- outConverter.delete();
- outConverter = null;
- }
- deleteTempFiles();
- super.reset();
- scratchFile = null;
- }
-
- /**
- * Called by the garbage collector when this recorder is no longer in use.
- */
- @Override
- protected void finalize() {
- reset();
- }
-
- /**
- * Appends a frame to the current video by saving the image in a tempFile.
- *
- * @param image the image to append
- * @return true if image successfully saved
- */
- @Override
- protected boolean append(Image image) {
- int w = image.getWidth(null);
- int h = image.getHeight(null);
- if (dim==null || (!hasContent && (dim.width!=w || dim.height!=h))) {
- dim = new Dimension(w, h);
- }
- // resize and/or convert to BufferedImage if needed
- if (dim.width!=w || dim.height!=h || !(image instanceof BufferedImage)) {
- BufferedImage img = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_RGB);
- int x = (dim.width-w)/2;
- int y = (dim.height-h)/2;
- img.getGraphics().drawImage(image, x, y, null);
- image = img;
- }
- BufferedImage source = (BufferedImage)image;
- String fileName = tempFileBasePath+"_"+tempFiles.size()+".tmp"; //$NON-NLS-1$ //$NON-NLS-2$
- try {
- ImageIO.write(source, tempFileType, new BufferedOutputStream(
- new FileOutputStream(fileName)));
- } catch (Exception e) {
- return false;
- }
- File imageFile = new File(fileName);
- if (imageFile.exists()) {
- synchronized (tempFiles) {
- tempFiles.add(imageFile);
- }
- imageFile.deleteOnExit();
- }
- return true;
- }
-
- /**
- * Saves the video to the current scratchFile.
- *
- * @throws IOException
- */
- @Override
- protected void saveScratch() throws IOException {
- FileFilter fileFilter = videoType.getDefaultFileFilter();
- if (!hasContent || !(fileFilter instanceof VideoFileFilter))
- return;
-
- // set container format
- IContainerFormat format = IContainerFormat.make();
- VideoFileFilter xuggleFilter = (VideoFileFilter)fileFilter;
- format.setOutputFormat(xuggleFilter.getContainerType(), null, null);
-
- // set the pixel type--may depend on selected fileFilter?
- IPixelFormat.Type pixelType = IPixelFormat.Type.YUV420P;
-
- // open the output stream, write the images, close the stream
- openStream(format, pixelType);
-
- // open temp images and encode
- long timeStamp = 0;
- int n=0;
- synchronized (tempFiles) {
- for (File imageFile: tempFiles) {
- if (!imageFile.exists())
- throw new IOException("temp image file not found"); //$NON-NLS-1$
- BufferedImage image = ResourceLoader.getBufferedImage(imageFile.getAbsolutePath(), BufferedImage.TYPE_3BYTE_BGR);
- if (image==null || image.getType()!=BufferedImage.TYPE_3BYTE_BGR) {
- throw new IOException("unable to load temp image file"); //$NON-NLS-1$
- }
-
- encodeImage(image, pixelType, timeStamp);
- n++;
- timeStamp = Math.round(n*frameDuration*1000); // frameDuration in ms, timestamp in microsec
- }
- }
- closeStream();
- deleteTempFiles();
- hasContent = false;
- canRecord = false;
- }
-
- /**
- * Starts the video recording process.
- *
- * @return true if video recording successfully started
- */
- @Override
- protected boolean startRecording() {
- try {
- tempFileBasePath = XML.stripExtension(scratchFile.getAbsolutePath());
- } catch (Exception e) {
- return false;
- }
- return true;
- }
-
- /**
- * Opens/initializes the output stream using a specified Xuggle format.
- *
- * @param format the format
- * @param pixelType the pixel type
- * @throws IOException
- */
- @SuppressWarnings("deprecation")
- private boolean openStream(IContainerFormat format, IPixelFormat.Type pixelType)
- throws IOException {
- outContainer = IContainer.make();
- if (outContainer.open(scratchFile.getAbsolutePath(), IContainer.Type.WRITE, format)<0) {
- OSPLog.finer("Xuggle could not open output file"); //$NON-NLS-1$
- return false;
- }
- String typicalName = "typical."+videoType.getDefaultExtension(); //$NON-NLS-1$
- ICodec codec = ICodec.guessEncodingCodec(format, null, typicalName, null, ICodec.Type.CODEC_TYPE_VIDEO);
- outStream = outContainer.addNewStream(0);
-
- outStreamCoder = outStream.getStreamCoder();
- outStreamCoder.setNumPicturesInGroupOfPictures(10);
- outStreamCoder.setCodec(codec);
- outStreamCoder.setBitRate(250000);
-// outStreamCoder.setBitRateTolerance(9000);
- outStreamCoder.setPixelType(pixelType);
- if(dim==null && frameImage!=null) {
- dim = new Dimension(frameImage.getWidth(null), frameImage.getHeight(null));
- }
- if(dim!=null) {
- outStreamCoder.setHeight(dim.height);
- outStreamCoder.setWidth(dim.width);
- }
-// outStreamCoder.setFlag(IStreamCoder.Flags.FLAG_QSCALE, true);
-// outStreamCoder.setGlobalQuality(0);
-
- IRational frameRate = IRational.make(1000/frameDuration);
- boolean hasTimeBaseLimit = typicalName.endsWith(".avi") || typicalName.endsWith(".mpg"); //$NON-NLS-1$ //$NON-NLS-2$
- if (hasTimeBaseLimit && frameRate.getDenominator()>65535) { // maximum timebase = 2^16 - 1
- double fps = 1000/frameDuration;
- int denom = 63000; // 7 x 9000
- int numer = Math.round(Math.round(fps*denom));
- frameRate = IRational.make(numer, denom);
- }
-
-
- outStreamCoder.setFrameRate(frameRate);
- // set time base to inverse of frame rate
- outStreamCoder.setTimeBase(IRational.make(frameRate.getDenominator(),
- frameRate.getNumerator()));
-// // some codecs require experimental mode to be set? (and all accept it??)
-// if (outStreamCoder.setStandardsCompliance(IStreamCoder.CodecStandardsCompliance.COMPLIANCE_EXPERIMENTAL) < 0) {
-// OSPLog.finer("Xuggle could not set compliance mode to experimental"); //$NON-NLS-1$
-// return false;
-// }
-
- if (outStreamCoder.open()<0) {
- OSPLog.finer("Xuggle could not open stream encoder"); //$NON-NLS-1$
- return false;
- }
-
- if (outContainer.writeHeader()<0) {
- OSPLog.finer("Xuggle could not write file header"); //$NON-NLS-1$
- return false;
- }
- return true;
- }
-
- /**
- * Encodes an image and writes it to the output stream.
- *
- * @param image the image to encode (must be BufferedImage.TYPE_3BYTE_BGR)
- * @param pixelType the pixel type
- * @param timeStamp the time stamp in microseconds
- * @throws IOException
- */
- private boolean encodeImage(BufferedImage image, IPixelFormat.Type pixelType, long timeStamp)
- throws IOException {
- // convert image to xuggle picture
- IVideoPicture picture = getPicture(image, pixelType, timeStamp);
- if (picture==null)
- throw new RuntimeException("could not convert to picture"); //$NON-NLS-1$
- // make a packet
- IPacket packet = IPacket.make();
- if (outStreamCoder.encodeVideo(packet, picture, 0) < 0) {
- throw new RuntimeException("could not encode video"); //$NON-NLS-1$
- }
- if (packet.isComplete()) {
- boolean forceInterleave = true;
- if (outContainer.writePacket(packet, forceInterleave) < 0) {
- throw new RuntimeException("could not save packet to container"); //$NON-NLS-1$
- }
- return true;
- }
- return false;
- }
-
- /**
- * Closes the output stream.
- *
- * @throws IOException
- */
- private void closeStream() throws IOException {
- if (outContainer!=null) {
- if (outContainer.writeTrailer() < 0) {
- throw new RuntimeException("could not write trailer to output file"); //$NON-NLS-1$
- }
- outStreamCoder.close();
- outContainer.close();
-// outStreamCoder.delete();
-// outStream.delete();
-// outContainer.delete();
- outContainer = null;
- outStreamCoder = null;
- outStream = null;
- }
- }
-
- /**
- * Converts a bgr source image to a xuggle picture.
- *
- * @param bgrImage the source image (must be type TYPE_3BYTE_BGR)
- * @param pixelType the pixel type
- * @param timeStamp the timestamp in microseconds
- * @return the xuggle picture
- */
- private IVideoPicture getPicture(BufferedImage bgrImage, IPixelFormat.Type pixelType, long timeStamp) {
-
- IVideoPicture picture = null;
- try {
- IConverter converter = getConverter(bgrImage, pixelType);
- picture = converter.toPicture(bgrImage, timeStamp);
- picture.setQuality(0);
- } catch (Exception ex) {
- ex.printStackTrace();
- } catch (Error err) {
- err.printStackTrace();
- }
- return picture;
- }
-
- /**
- * Gets the converter for converting images to pictures.
- *
- * @param bgrImage the source image (must be type TYPE_3BYTE_BGR)
- * @param pixelType the desired pixel type
- */
- private IConverter getConverter(BufferedImage bgrImage, IPixelFormat.Type pixelType){
- int w = bgrImage.getWidth();
- int h = bgrImage.getHeight();
- if (converterDim==null) {
- converterDim = new Dimension(w, h);
- }
- if (outConverter==null || w!=converterDim.width || h!=converterDim.height
- || outConverter.getPictureType()!= pixelType) {
- try {
- outConverter = ConverterFactory.createConverter(bgrImage, pixelType);
- converterDim = new Dimension(w, h);
- } catch(UnsupportedOperationException e){
- System.err.println(e.getMessage());
- e.printStackTrace();
- }
- }
- return outConverter;
- }
-
- /**
- * Given the short name of a container, prints out information about
- * it, including which codecs Xuggler can write (mux) into that container.
- *
- * @param name the short name of the format (e.g. "flv")
- */
- public static void getSupportedCodecs(String name) {
- IContainerFormat format = IContainerFormat.make();
- format.setOutputFormat(name, null, null);
-
- List codecs = format.getOutputCodecsSupported();
- if (codecs.isEmpty())
- System.out.println("no supported codecs for "+name); //$NON-NLS-1$
- else {
- System.out.println(name+" ("+format+") supports following codecs:"); //$NON-NLS-1$ //$NON-NLS-2$
- for(ID id : codecs) {
- if (id != null) {
- ICodec codec = ICodec.findEncodingCodec(id);
- if (codec != null) {
- System.out.println(codec);
- }
- }
- }
- }
- }
-
-}
diff --git a/src/org/opensourcephysics/media/xuggle/XuggleVideoType.java b/src/org/opensourcephysics/media/xuggle/XuggleVideoType.java
deleted file mode 100644
index 346a157..0000000
--- a/src/org/opensourcephysics/media/xuggle/XuggleVideoType.java
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * The org.opensourcephysics.media.xuggle package provides Xuggle
- * services including implementations of the Video and VideoRecorder interfaces.
- *
- * Copyright (c) 2017 Douglas Brown and Wolfgang Christian.
- *
- * This is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
- * or view the license online at http://www.gnu.org/copyleft/gpl.html
- *
- * For additional information and documentation on Open Source Physics,
- * please see .
- */
-package org.opensourcephysics.media.xuggle;
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-import java.io.File;
-import java.io.IOException;
-import java.util.TreeSet;
-
-import org.opensourcephysics.controls.OSPLog;
-import org.opensourcephysics.media.core.MediaRes;
-import org.opensourcephysics.media.core.VideoFileFilter;
-import org.opensourcephysics.media.core.Video;
-import org.opensourcephysics.media.core.VideoRecorder;
-import org.opensourcephysics.media.core.VideoType;
-
-/**
- * This implements the VideoType interface with a Xuggle type.
- *
- * @author Douglas Brown
- * @version 1.0
- */
-public class XuggleVideoType implements VideoType {
-
- protected static TreeSet xuggleFileFilters
- = new TreeSet();
- protected static String xuggleClass = "com.xuggle.xuggler.IContainer"; //$NON-NLS-1$
- protected static PropertyChangeListener errorListener;
- protected static boolean isXuggleAvailable = true;
- protected boolean recordable = true;
-
- static {
- errorListener = new PropertyChangeListener() {
- public void propertyChange(PropertyChangeEvent e) {
- if (e.getPropertyName().equals("xuggle_error")) { //$NON-NLS-1$
- isXuggleAvailable = false;
- }
- }
- };
- OSPLog.getOSPLog().addPropertyChangeListener(errorListener);
- XuggleThumbnailTool.start();
- }
-
- private VideoFileFilter singleTypeFilter; // null for general type
-
- /**
- * Constructor attempts to load a xuggle class the first time used.
- * This will throw an error if xuggle is not available.
- */
- public XuggleVideoType() {
- if (!isXuggleAvailable)
- throw new Error("Xuggle unavailable"); //$NON-NLS-1$
- boolean logConsole = OSPLog.isConsoleMessagesLogged();
- try {
- OSPLog.setConsoleMessagesLogged(false);
- Class.forName(xuggleClass);
- OSPLog.setConsoleMessagesLogged(logConsole);
- } catch (Exception ex) {
- OSPLog.setConsoleMessagesLogged(logConsole);
- throw new Error("Xuggle unavailable"); //$NON-NLS-1$
- }
- }
-
- /**
- * Constructor with a file filter for a specific container type.
- *
- * @param filter the file filter
- */
- public XuggleVideoType(VideoFileFilter filter) {
- this();
- if (filter!=null) {
- singleTypeFilter = filter;
- xuggleFileFilters.add(filter);
- }
- }
-
- /**
- * Opens a named video as a XuggleVideo.
- *
- * @param name the name of the video
- * @return a new Xuggle video
- */
- public Video getVideo(String name) {
- try {
- Video video = new XuggleVideo(name);
- video.setProperty("video_type", this); //$NON-NLS-1$
- return video;
- } catch(IOException ex) {
- OSPLog.fine(this.getDescription()+": "+ex.getMessage()); //$NON-NLS-1$
- return null;
- }
- }
-
- /**
- * Reports whether this xuggle type can record videos
- *
- * @return true by default (set recordable to change)
- */
- public boolean canRecord() {
- return recordable;
- }
-
- /**
- * Sets the recordable property
- *
- * @param record true if recordable
- */
- public void setRecordable(boolean record) {
- recordable = record;
- }
-
- /**
- * Gets a Xuggle video recorder.
- *
- * @return the video recorder
- */
- public VideoRecorder getRecorder() {
- return new XuggleVideoRecorder(this);
- }
-
- /**
- * Gets the file filters for this type.
- *
- * @return an array of file filters
- */
- public VideoFileFilter[] getFileFilters() {
- if (singleTypeFilter!=null)
- return new VideoFileFilter[] {singleTypeFilter};
- return xuggleFileFilters.toArray(new VideoFileFilter[0]);
- }
-
- /**
- * Gets the default file filter for this type. May return null.
- *
- * @return the default file filter
- */
- public VideoFileFilter getDefaultFileFilter() {
- if (singleTypeFilter!=null)
- return singleTypeFilter;
- return null;
- }
-
- /**
- * Return true if the specified video is this type.
- *
- * @param video the video
- * @return true if the video is this type
- */
- public boolean isType(Video video) {
- if (!video.getClass().equals(XuggleVideo.class)) return false;
- if (singleTypeFilter==null) return true;
- String name = (String)video.getProperty("name"); //$NON-NLS-1$
- return singleTypeFilter.accept(new File(name));
- }
-
- /**
- * Gets the name and/or description of this type.
- *
- * @return a description
- */
- public String getDescription() {
- if (singleTypeFilter!=null)
- return singleTypeFilter.getDescription();
- return MediaRes.getString("XuggleVideoType.Description"); //$NON-NLS-1$
- }
-
- /**
- * Gets the default extension for this type.
- *
- * @return an extension
- */
- public String getDefaultExtension() {
- if (singleTypeFilter!=null) {
- return singleTypeFilter.getDefaultExtension();
- }
- return null;
- }
-
-}
-
-/*
- * Open Source Physics software is free software; you can redistribute
- * it and/or modify it under the terms of the GNU General Public License (GPL) as
- * published by the Free Software Foundation; either version 2 of the License,
- * or(at your option) any later version.
-
- * Code that uses any portion of the code in the org.opensourcephysics package
- * or any subpackage (subdirectory) of this package must must also be be released
- * under the GNU GPL license.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
- * or view the license online at http://www.gnu.org/copyleft/gpl.html
- *
- * Copyright (c) 2017 The Open Source Physics project
- * https://www.compadre.org/osp
- */