/* * Open Source Physics software is free software as described near the bottom of this code file. * * For additional information and documentation on Open Source Physics please see: * <http://www.opensourcephysics.org/> */ /* * The org.opensourcephysics.media.core package defines the Open Source Physics * media framework for working with video and other media. * * Copyright (c) 2014 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 <http://www.opensourcephysics.org/>. */ package org.opensourcephysics.media.core; import java.awt.Frame; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import javax.swing.JCheckBox; import javax.swing.JOptionPane; import javax.swing.event.SwingPropertyChangeSupport; import org.opensourcephysics.controls.OSPLog; import org.opensourcephysics.controls.XML; import org.opensourcephysics.controls.XMLControl; import org.opensourcephysics.tools.ResourceLoader; /** * This defines a subset of video frames called steps. * * @author Douglas Brown * @version 1.0 */ public class VideoClip { // instance fields public boolean changeEngine; // signals that user wishes to change preferred video engine private int startFrame = 0; private int stepSize = 1; private int stepCount = 10; // default stepCount is 10 if video is null private int frameCount = stepCount; // default frameCount same as stepCount private int maxFrameCount = 300000; // approx 2h45m at 30fps private int frameShift = 0; // for shifted video frame numbers private double startTime = 0; // start time in milliseconds protected boolean isDefaultStartTime = true; protected Video video = null; private int[] stepFrames; ClipInspector inspector; private PropertyChangeSupport support; private boolean playAllSteps = false; private boolean isDefaultState; private boolean isAdjusting = false; private int endFrame; protected String readoutType; protected String videoPath; protected double savedStartTime; // used when DataTrack sets start time protected boolean startTimeIsSaved = false; // used when DataTrack sets start time protected int extraFrames = 0; // extends clip length past video end /** * Constructs a VideoClip. * * @param video the video */ public VideoClip(Video video) { support = new SwingPropertyChangeSupport(this); this.video = video; if(video!=null) { video.setProperty("videoclip", this); //$NON-NLS-1$ setStartFrameNumber(video.getStartFrameNumber()); if(video.getFrameCount()>1) { setStepCount(video.getEndFrameNumber()-startFrame+1); } } updateArray(); isDefaultState = true; } /** * Gets the video. * * @return the video */ public Video getVideo() { return video; } /** * Gets the video path. * * @return the video path */ public String getVideoPath() { if (video!=null) return (String)video.getProperty("absolutePath"); //$NON-NLS-1$ return videoPath; } /** * Sets the start frame number. * * @param start the desired start frame number * @return true if changed */ public boolean setStartFrameNumber(int start) { int maxEndFrame = getLastFrameNumber(); return setStartFrameNumber(start, maxEndFrame); } /** * Sets the start frame number. * * @param start the desired start frame number * @param maxStart start frame number that cannot be exceeded * @return true if changed */ public boolean setStartFrameNumber(int start, int maxStart) { int prevStart = getStartFrameNumber(); int prevEnd = getEndFrameNumber(); // can't start before first frame number or after max end frame start = Math.max(start, getFirstFrameNumber()); start = Math.min(start, maxStart); if(video!=null && video.getFrameCount()>1) { // set video end frame to last video frame (temporary) video.setEndFrameNumber(video.getFrameCount()-1); // set video start frame number to desired start frame int vidStart = Math.max(0, start+frameShift); video.setStartFrameNumber(vidStart); // set start frame to frame number of first video frame startFrame = Math.max(0, video.getStartFrameNumber()-frameShift); } else { // no video or single-frame video startFrame = start; updateArray(); } start = getStartFrameNumber(); // reset end frame setEndFrameNumber(prevEnd); if(prevStart!=start) { isDefaultState = false; support.firePropertyChange("startframe", null, new Integer(start)); //$NON-NLS-1$ } return prevStart!=start; } /** * Gets the start frame number. * * @return the start frame number */ public int getStartFrameNumber() { return startFrame; } /** * Sets the step size. * * @param size the desired step size * @return true if changed */ public boolean setStepSize(int size) { isDefaultState = false; if(size==0) { return false; } size = Math.abs(size); if (video!=null && video.getFrameCount()>1) { int maxSize = Math.max(video.getFrameCount()-startFrame-1+extraFrames, 1); size = Math.min(size, maxSize); } if(stepSize==size) { return false; } // get current end frame int endFrame = getEndFrameNumber(); stepSize = size; // set stepCount to near value stepCount = 1+(endFrame-getStartFrameNumber())/stepSize; updateArray(); support.firePropertyChange("stepsize", null, new Integer(size)); //$NON-NLS-1$ // reset end frame setEndFrameNumber(endFrame); trimFrameCount(); return true; } /** * Gets the step size. * * @return the step size */ public int getStepSize() { return stepSize; } /** * Sets the step count. * * @param count the desired number of steps */ public void setStepCount(int count) { if(count==0) { return; } count = Math.abs(count); if (video!=null) { if(video.getFrameCount()>1) { int end = video.getFrameCount()-1-frameShift+extraFrames; int maxCount = 1+(int) ((end-startFrame)/(1.0*stepSize)); count = Math.min(count, maxCount); } int end = startFrame+(count-1)*stepSize+frameShift; if (end!=video.getEndFrameNumber()) { video.setEndFrameNumber(end); } } else { count = Math.min(count, frameToStep(maxFrameCount-1)+1); } count = Math.max(count, 1); if (stepCount==count) { updateArray(); return; } Integer prev = new Integer(stepCount); stepCount = count; updateArray(); support.firePropertyChange("stepcount", prev, new Integer(stepCount)); //$NON-NLS-1$ } /** * Gets the step count. * * @return the number of steps */ public int getStepCount() { return stepCount; } /** * Sets the frame shift. * * @param n the desired frame shift * @return the new frame shift */ public int setFrameShift(int n) { int start = getStartFrameNumber(); int steps = getStepCount(); return setFrameShift(n, start, steps); } /** * Sets the frame shift. * DISABLED Jan 2016--too many potential bugs in frame shifting, and very little need. * * @param n the desired frame shift * @param start the desired start frame * @param stepCount the desired step count * @return the new frame shift */ protected int setFrameShift(int n, int start, int stepCount) { // // frameshift cannot be greater than highest video frame number--no frames would be visible! // if (video!=null) // n = Math.min(n, video.getFrameCount()-1); // frameShift = n; // support.firePropertyChange("frameshift", null, frameShift); //$NON-NLS-1$ // setStartFrameNumber(start); // setStepCount(stepCount); return frameShift; } /** * Gets the frame shift. * * @return frame shift */ public int getFrameShift() { return frameShift; } /** * Sets the extra frame count. Extra frames are blank frames after the last frame of a video. * * @param extras the number of extra frames to display */ public void setExtraFrames(int extras) { int prev = extraFrames; extraFrames = Math.max(extras, 0); if (prev!=extraFrames) { OSPLog.finest("set extra frames to "+extraFrames); //$NON-NLS-1$ setStepCount(stepCount); } } /** * Gets the extra frame count. * * @return the extra frames */ public int getExtraFrames() { return extraFrames; } /** * Gets the frame count. * * @return the number of frames */ public int getFrameCount() { if(video!=null && video.getFrameCount()>1) { int n = video.getFrameCount() + extraFrames; n = Math.min(n, n-frameShift); n = Math.max(1, n); return n; } int frames = getEndFrameNumber()+1; frameCount = Math.max(frameCount, frames); frameCount = Math.min(frameCount, maxFrameCount); return frameCount; } /** * Sets the start time. * * @param t0 the start time in milliseconds */ public void setStartTime(double t0) { isDefaultState = false; if(startTime==t0 || (isDefaultStartTime && Double.isNaN(t0))) { return; } isDefaultStartTime = Double.isNaN(t0); startTime = Double.isNaN(t0) ? 0.0 : t0; support.firePropertyChange("starttime", null, new Double(startTime)); //$NON-NLS-1$ } /** * Gets the start time. * * @return the start time in milliseconds */ public double getStartTime() { return startTime; } /** * Gets the end frame number. * * @return the end frame */ public int getEndFrameNumber() { endFrame = startFrame+stepSize*(stepCount-1); return endFrame; } /** * Sets the end frame number. * * @param end the desired end frame * @return true if the end frame number was changed */ public boolean setEndFrameNumber(int end) { return setEndFrameNumber(end, maxFrameCount-1-frameShift, true); } /** * Sets the end frame number after adding extra frames if needed. * * @param end the desired end frame * @return true if the end frame number was extended */ public boolean extendEndFrameNumber(int end) { if (video!=null && getFrameCount()<=end) { setExtraFrames(end+1-getFrameCount()+extraFrames); } return setEndFrameNumber(end); } /** * Sets the end frame number. * * @param end the desired end frame * @param max the maximum allowed * @return true if the end frame number was changed */ private boolean setEndFrameNumber(int end, int max, boolean onlyIfChanged) { int prev = getEndFrameNumber(); if (prev==end && onlyIfChanged) return false; isDefaultState = false; end = Math.max(end, startFrame); // end can't be less than start // determine step count needed for desired end frame int rem = (end-startFrame)%stepSize; int count = (end-startFrame)/stepSize; if(rem*1.0/stepSize>0.5) { count++; } while (stepToFrame(count) > max) { count--; } // set step count setStepCount(count+1); end = getEndFrameNumber(); // determine maximum step size and adjust step size if needed if (end!=startFrame) { int maxStepSize = Math.max(end-startFrame, 1); if(maxStepSize<stepSize) { stepSize = maxStepSize; } } return prev!=end; } /** * Converts step number to frame number. * * @param stepNumber the step number * @return the frame number */ public int stepToFrame(int stepNumber) { return startFrame+stepNumber*stepSize; } /** * Converts frame number to step number. A frame number that falls * between two steps maps to the previous step. * * @param n the frame number * @return the step number */ public int frameToStep(int n) { return(int) ((n-startFrame)/(1.0*stepSize)); } /** * Determines whether the specified frame is a step frame. * * @param n the frame number * @return <code>true</code> if the frame is a step frame */ public boolean includesFrame(int n) { for(int i = 0; i<stepCount; i++) { if(stepFrames[i]==n) { return true; } } return false; } /** * Gets the clip inspector. * * @return the clip inspector */ public ClipInspector getClipInspector() { return inspector; } /** * Gets the clip inspector with access to the specified ClipControl. * * @param control the clip control * @param frame the owner of the inspector * @return the clip inspector */ public ClipInspector getClipInspector(ClipControl control, Frame frame) { if(inspector==null) { inspector = new ClipInspector(this, control, frame); } return inspector; } /** * Hides the clip inspector. */ public void hideClipInspector() { if(inspector!=null) { inspector.setVisible(false); } } /** * Returns true if no properties have been set or reviewed by the user. * * @return true if in a default state */ public boolean isDefaultState() { return isDefaultState && inspector==null; } /** * Sets the adjusting flag. * * @param adjusting true if adjusting */ public void setAdjusting(boolean adjusting) { if (isAdjusting==adjusting) return; isAdjusting = adjusting; support.firePropertyChange("adjusting", null, adjusting); //$NON-NLS-1$ } /** * Gets the adjusting flag. * * @return true if adjusting */ public boolean isAdjusting() { return isAdjusting; } /** * Sets the playAllSteps flag. * * @param all true to play all steps */ public void setPlayAllSteps(boolean all) { playAllSteps = all; } /** * Gets the playAllSteps flag. * * @return true if playing all steps */ public boolean isPlayAllSteps() { return playAllSteps; } /** * Adds a PropertyChangeListener to this video clip. * * @param listener the object requesting property change notification */ public void addPropertyChangeListener(PropertyChangeListener listener) { support.addPropertyChangeListener(listener); } /** * Adds a PropertyChangeListener to this video clip. * * @param property the name of the property of interest to the listener * @param listener the object requesting property change notification */ public void addPropertyChangeListener(String property, PropertyChangeListener listener) { support.addPropertyChangeListener(property, listener); } /** * Removes a PropertyChangeListener from this video clip. * * @param listener the listener requesting removal */ public void removePropertyChangeListener(PropertyChangeListener listener) { support.removePropertyChangeListener(listener); } /** * Removes a PropertyChangeListener for a specified property. * * @param property the name of the property * @param listener the listener to remove */ public void removePropertyChangeListener(String property, PropertyChangeListener listener) { support.removePropertyChangeListener(property, listener); } /** * Trims unneeded frames after end frame (null videos only). */ protected void trimFrameCount() { if(video==null || video.getFrameCount()==1) { frameCount = getEndFrameNumber()+1; support.firePropertyChange("framecount", null, new Integer(frameCount)); //$NON-NLS-1$ } } /** * Updates the list of step frames. */ private void updateArray() { stepFrames = new int[stepCount]; for(int i = 0; i<stepCount; i++) { stepFrames[i] = stepToFrame(i); } } /** * Gets the first frame number. * @return the frame number */ public int getFirstFrameNumber() { if (video==null) return 0; // frameShift changes frame number--but never less than zero return Math.max(0, -frameShift); } /** * Gets the last frame number. * @return the frame number */ public int getLastFrameNumber() { if (video==null || video.getFrameCount()==1) { return getEndFrameNumber(); } int finalVideoFrame = video.getFrameCount()-1+extraFrames; // frameShift changes frame number--but never less than zero return Math.max(0, finalVideoFrame-frameShift); } /** * Returns an XML.ObjectLoader to save and load data for this class. * * @return the object loader */ public static XML.ObjectLoader getLoader() { return new Loader(); } /** * A class to save and load data for this class. */ static class Loader implements XML.ObjectLoader { /** * Saves object data in an XMLControl. * * @param control the control to save to * @param obj the object to save */ public void saveObject(XMLControl control, Object obj) { VideoClip clip = (VideoClip) obj; Video video = clip.getVideo(); if(video!=null) { if(video instanceof ImageVideo) { ImageVideo vid = (ImageVideo) video; if(vid.isFileBased()) { control.setValue("video", video); //$NON-NLS-1$ } } else { control.setValue("video", video); //$NON-NLS-1$ } control.setValue("video_framecount", video.getFrameCount()); //$NON-NLS-1$ } control.setValue("startframe", clip.getStartFrameNumber()); //$NON-NLS-1$ control.setValue("stepsize", clip.getStepSize()); //$NON-NLS-1$ control.setValue("stepcount", clip.getStepCount()); //$NON-NLS-1$ control.setValue("starttime", clip.startTimeIsSaved? //$NON-NLS-1$ clip.savedStartTime: clip.getStartTime()); // control.setValue("frameshift", clip.getFrameShift()); //$NON-NLS-1$ control.setValue("readout", clip.readoutType); //$NON-NLS-1$ control.setValue("playallsteps", clip.playAllSteps); //$NON-NLS-1$ } /** * Creates a new object. * * @param control the XMLControl with the object data * @return the newly created object */ public Object createObject(XMLControl control) { // load the video and return a new clip boolean hasVideo = control.getPropertyNames().contains("video"); //$NON-NLS-1$ if(!hasVideo) { return new VideoClip(null); } ResourceLoader.addSearchPath(control.getString("basepath")); //$NON-NLS-1$ XMLControl child = control.getChildControl("video"); //$NON-NLS-1$ String path = child.getString("path"); //$NON-NLS-1$ Video video = VideoIO.getVideo(path, null); boolean engineChange = false; if (video==null && path!=null && !VideoIO.isCanceled()) { if (ResourceLoader.getResource(path)!=null) { // resource exists but not loaded OSPLog.info("\""+path+"\" could not be opened"); //$NON-NLS-1$ //$NON-NLS-2$ // determine if other engines are available for the video extension ArrayList<VideoType> otherEngines = new ArrayList<VideoType>(); String engine = VideoIO.getEngine(); String ext = XML.getExtension(path); if (!engine.equals(VideoIO.ENGINE_XUGGLE)) { VideoType xuggleType = VideoIO.getVideoType("Xuggle", ext); //$NON-NLS-1$ if (xuggleType!=null) otherEngines.add(xuggleType); } if (!engine.equals(VideoIO.ENGINE_QUICKTIME)) { VideoType qtType = VideoIO.getVideoType("QT", ext); //$NON-NLS-1$ if (qtType!=null) otherEngines.add(qtType); } if (otherEngines.isEmpty()) { JOptionPane.showMessageDialog(null, MediaRes.getString("VideoIO.Dialog.BadVideo.Message")+"\n\n"+path, //$NON-NLS-1$ //$NON-NLS-2$ MediaRes.getString("VideoClip.Dialog.BadVideo.Title"), //$NON-NLS-1$ JOptionPane.WARNING_MESSAGE); } else { // provide immediate way to open with other engines JCheckBox changePreferredEngine = new JCheckBox(MediaRes.getString("VideoIO.Dialog.TryDifferentEngine.Checkbox")); //$NON-NLS-1$ video = VideoIO.getVideo(path, otherEngines, changePreferredEngine, null); engineChange = changePreferredEngine.isSelected(); if (video!=null && changePreferredEngine.isSelected()) { String typeName = video.getClass().getSimpleName(); String newEngine = typeName.indexOf("Xuggle")>-1? VideoIO.ENGINE_XUGGLE: //$NON-NLS-1$ typeName.indexOf("QT")>-1? VideoIO.ENGINE_QUICKTIME: //$NON-NLS-1$ VideoIO.ENGINE_NONE; VideoIO.setEngine(newEngine); } } } else { int response = JOptionPane.showConfirmDialog(null, "\""+path+"\" " //$NON-NLS-1$ //$NON-NLS-2$ +MediaRes.getString("VideoClip.Dialog.VideoNotFound.Message"), //$NON-NLS-1$ MediaRes.getString("VideoClip.Dialog.VideoNotFound.Title"), //$NON-NLS-1$ JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if(response==JOptionPane.YES_OPTION) { VideoIO.getChooser().setAccessory(VideoIO.videoEnginePanel); VideoIO.videoEnginePanel.reset(); VideoIO.getChooser().setSelectedFile(new File(path)); java.io.File[] files = VideoIO.getChooserFiles("open video"); //$NON-NLS-1$ if(files!=null && files.length>0) { VideoType selectedType = VideoIO.videoEnginePanel.getSelectedVideoType(); path = XML.getAbsolutePath(files[0]); video = VideoIO.getVideo(path, selectedType); } } } } if (video!=null) { Collection<?> filters = (Collection<?>) child.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); } } if (video instanceof ImageVideo) { double dt = child.getDouble("delta_t"); //$NON-NLS-1$ if (!Double.isNaN(dt)) { ((ImageVideo)video).setFrameDuration(dt); } } } VideoClip clip = new VideoClip(video); clip.changeEngine = engineChange; if (path!=null) { if (!path.startsWith("/") && path.indexOf(":")==-1) { //$NON-NLS-1$ //$NON-NLS-2$ // convert path to absolute String base = control.getString("basepath"); //$NON-NLS-1$ path = XML.getResolvedPath(path, base); } clip.videoPath = path; } return clip; } /** * Loads a VideoClip with data from an XMLControl. * * @param control the XMLControl * @param obj the object * @return the loaded object */ public Object loadObject(XMLControl control, Object obj) { VideoClip clip = (VideoClip) obj; // set total frame count first so start frame will not be limited int start = control.getInt("startframe"); //$NON-NLS-1$ int stepSize = control.getInt("stepsize"); //$NON-NLS-1$ int stepCount = control.getInt("stepcount"); //$NON-NLS-1$ int frameCount = clip.getFrameCount(); if (control.getPropertyNames().contains("video_framecount")) { //$NON-NLS-1$ frameCount = control.getInt("video_framecount"); //$NON-NLS-1$ } else if (start!=Integer.MIN_VALUE && stepSize!=Integer.MIN_VALUE && stepCount!=Integer.MIN_VALUE) { frameCount = start + stepCount*stepSize; } clip.setStepCount(frameCount); // this should equal or exceed the actual frameCount // // set frame shift // int shift = control.getInt("frameshift"); //$NON-NLS-1$ // if(shift!=Integer.MIN_VALUE) { // clip.setFrameShift(shift); // } // set start frame if(start!=Integer.MIN_VALUE) { clip.setStartFrameNumber(start); } // set step size if(stepSize!=Integer.MIN_VALUE) { clip.setStepSize(stepSize); } // set step count if(stepCount!=Integer.MIN_VALUE) { clip.setStepCount(stepCount); } // set start time double t = control.getDouble("starttime"); //$NON-NLS-1$ if(!Double.isNaN(t)) { clip.startTime = t; } clip.readoutType = control.getString("readout"); //$NON-NLS-1$ clip.playAllSteps = true; // by default if (control.getPropertyNames().contains("playallsteps")) { //$NON-NLS-1$ clip.playAllSteps = control.getBoolean("playallsteps"); //$NON-NLS-1$ } return obj; } } } /* * 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) 2007 The Open Source Physics project * http://www.opensourcephysics.org */