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 - */