You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1027 lines
26 KiB
Java

/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2004-12 Ben Fry and Casey Reas
The previous version of this code was developed by Hernando Barragan
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General
Public License along with this library; if not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330,
Boston, MA 02111-1307 USA
*/
package processing.video;
import processing.core.*;
import java.awt.Dimension;
import java.io.*;
import java.net.URI;
import java.nio.*;
import java.util.concurrent.TimeUnit;
import java.lang.reflect.*;
import org.gstreamer.*;
import org.gstreamer.Buffer;
import org.gstreamer.elements.*;
/**
* ( begin auto-generated from Movie.xml )
*
* Datatype for storing and playing movies in Apple's QuickTime format.
* Movies must be located in the sketch's data directory or an accessible
* place on the network to load without an error.
*
* ( end auto-generated )
*
* @webref video
* @usage application
*/
public class Movie extends PImage implements PConstants {
public static String[] supportedProtocols = { "http" };
public float frameRate;
public String filename;
public PlayBin2 playbin;
protected boolean playing = false;
protected boolean paused = false;
protected boolean repeat = false;
protected float rate;
protected int bufWidth;
protected int bufHeight;
protected float volume;
protected Method movieEventMethod;
protected Object eventHandler;
protected boolean available;
protected boolean sinkReady;
protected boolean newFrame;
protected RGBDataAppSink rgbSink = null;
protected int[] copyPixels = null;
protected boolean firstFrame = true;
protected boolean seeking = false;
protected boolean useBufferSink = false;
protected boolean outdatedPixels = true;
protected Object bufferSink;
protected Method sinkCopyMethod;
protected Method sinkSetMethod;
protected Method sinkDisposeMethod;
protected Method sinkGetMethod;
protected String copyMask;
protected Buffer natBuffer = null;
protected BufferDataAppSink natSink = null;
/**
* Creates an instance of GSMovie loading the movie from filename.
*
* @param parent PApplet
* @param filename String
*/
public Movie(PApplet parent, String filename) {
super(0, 0, RGB);
initGStreamer(parent, filename);
}
/**
* Disposes all the native resources associated to this movie.
*
* NOTE: This is not official API and may/will be removed at any time.
*/
public void dispose() {
if (playbin != null) {
try {
if (playbin.isPlaying()) {
playbin.stop();
playbin.getState();
}
} catch (Exception e) {
e.printStackTrace();
}
pixels = null;
copyPixels = null;
if (rgbSink != null) {
rgbSink.removeListener();
rgbSink.dispose();
rgbSink = null;
}
natBuffer = null;
if (natSink != null) {
natSink.removeListener();
natSink.dispose();
natSink = null;
}
playbin.dispose();
playbin = null;
parent.g.removeCache(this);
parent.unregisterMethod("dispose", this);
parent.unregisterMethod("post", this);
}
}
/**
* Finalizer of the class.
*/
protected void finalize() throws Throwable {
try {
dispose();
} finally {
super.finalize();
}
}
/**
* ( begin auto-generated from Movie_frameRate.xml )
*
* Sets how often frames are read from the movie. Setting the <b>fps</b>
* parameter to 4, for example, will cause 4 frames to be read per second.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @param ifps speed of the movie in frames per second
* @brief Sets the target frame rate
*/
public void frameRate(float ifps) {
if (seeking) return;
// We calculate the target ratio in the case both the
// current and target framerates are valid (greater than
// zero), otherwise we leave it as 1.
float f = (0 < ifps && 0 < frameRate) ? ifps / frameRate : 1;
if (playing) {
playbin.pause();
playbin.getState();
}
long t = playbin.queryPosition(TimeUnit.NANOSECONDS);
boolean res;
long start, stop;
if (rate > 0) {
start = t;
stop = -1;
} else {
start = 0;
stop = t;
}
res = playbin.seek(rate * f, Format.TIME, SeekFlags.FLUSH,
SeekType.SET, start, SeekType.SET, stop);
playbin.getState();
if (!res) {
PGraphics.showWarning("Seek operation failed.");
}
if (playing) {
playbin.play();
}
frameRate = ifps;
// getState() will wait until any async state change
// (like seek in this case) has completed
seeking = true;
playbin.getState();
seeking = false;
}
/**
* ( begin auto-generated from Movie_speed.xml )
*
* Sets the relative playback speed of the movie. The <b>rate</b>
* parameters sets the speed where 2.0 will play the movie twice as fast,
* 0.5 will play at half the speed, and -1 will play the movie in normal
* speed in reverse.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @param irate speed multiplier for movie playback
* @brief Sets the relative playback speed
*/
public void speed(float irate) {
// If the frameRate() method is called continuously with very similar
// rate values, playback might become sluggish. This condition attempts
// to take care of that.
if (PApplet.abs(rate - irate) > 0.1) {
rate = irate;
frameRate(frameRate); // The framerate is the same, but the rate (speed) could be different.
}
}
/**
* ( begin auto-generated from Movie_duration.xml )
*
* Returns the length of the movie in seconds. If the movie is 1 minute and
* 20 seconds long the value returned will be 80.0.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @brief Returns length of movie in seconds
*/
public float duration() {
float sec = playbin.queryDuration().toSeconds();
float nanosec = playbin.queryDuration().getNanoSeconds();
return sec + Video.nanoSecToSecFrac(nanosec);
}
/**
* ( begin auto-generated from Movie_time.xml )
*
* Returns the location of the playback head in seconds. For example, if
* the movie has been playing for 4 seconds, the number 4.0 will be returned.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @brief Returns location of playback head in units of seconds
*/
public float time() {
float sec = playbin.queryPosition().toSeconds();
float nanosec = playbin.queryPosition().getNanoSeconds();
return sec + Video.nanoSecToSecFrac(nanosec);
}
/**
* ( begin auto-generated from Movie_jump.xml )
*
* Jumps to a specific location within a movie. The parameter <b>where</b>
* is in terms of seconds. For example, if the movie is 12.2 seconds long,
* calling <b>jump(6.1)</b> would go to the middle of the movie.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @param where position to jump to specified in seconds
* @brief Jumps to a specific location
*/
public void jump(float where) {
if (seeking) return;
if (!sinkReady) {
initSink();
}
// Round the time to a multiple of the source framerate, in
// order to eliminate stutter. Suggested by Daniel Shiffman
float fps = getSourceFrameRate();
int frame = (int)(where * fps);
where = frame / fps;
boolean res;
long pos = Video.secToNanoLong(where);
res = playbin.seek(rate, Format.TIME, SeekFlags.FLUSH,
SeekType.SET, pos, SeekType.NONE, -1);
if (!res) {
PGraphics.showWarning("Seek operation failed.");
}
// getState() will wait until any async state change
// (like seek in this case) has completed
seeking = true;
playbin.getState();
seeking = false;
/*
if (seeking) return; // don't seek again until the current seek operation is done.
if (!sinkReady) {
initSink();
}
// Round the time to a multiple of the source framerate, in
// order to eliminate stutter. Suggested by Daniel Shiffman
float fps = getSourceFrameRate();
int frame = (int)(where * fps);
final float seconds = frame / fps;
// Put the seek operation inside a thread to avoid blocking the main
// animation thread
Thread seeker = new Thread() {
@Override
public void run() {
long pos = Video.secToNanoLong(seconds);
boolean res = playbin.seek(rate, Format.TIME, SeekFlags.FLUSH,
SeekType.SET, pos, SeekType.NONE, -1);
if (!res) {
PGraphics.showWarning("Seek operation failed.");
}
// getState() will wait until any async state change
// (like seek in this case) has completed
seeking = true;
playbin.getState();
seeking = false;
}
};
seeker.start();
*/
}
/**
* ( begin auto-generated from Movie_available.xml )
*
* Returns "true" when a new movie frame is available to read.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @brief Returns "true" when a new movie frame is available to read.
*/
public boolean available() {
return available;
}
/**
* ( begin auto-generated from Movie_play.xml )
*
* Plays a movie one time and stops at the last frame.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @brief Plays movie one time and stops at the last frame
*/
public void play() {
if (seeking) return;
if (!sinkReady) {
initSink();
}
playing = true;
paused = false;
playbin.play();
playbin.getState();
}
/**
* ( begin auto-generated from Movie_loop.xml )
*
* Plays a movie continuously, restarting it when it's over.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @brief Plays a movie continuously, restarting it when it's over.
*/
public void loop() {
if (seeking) return;
repeat = true;
play();
}
/**
* ( begin auto-generated from Movie_noLoop.xml )
*
* If a movie is looping, calling noLoop() will cause it to play until the
* end and then stop on the last frame.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @brief Stops the movie from looping
*/
public void noLoop() {
if (seeking) return;
if (!sinkReady) {
initSink();
}
repeat = false;
}
/**
* ( begin auto-generated from Movie_pause.xml )
*
* Pauses a movie during playback. If a movie is started again with play(),
* it will continue from where it was paused.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @brief Pauses the movie
*/
public void pause() {
if (seeking) return;
if (!sinkReady) {
initSink();
}
playing = false;
paused = true;
playbin.pause();
playbin.getState();
}
/**
* ( begin auto-generated from Movie_stop.xml )
*
* Stops a movie from continuing. The playback returns to the beginning so
* when a movie is played, it will begin from the beginning.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @brief Stops the movie
*/
public void stop() {
if (seeking) return;
if (!sinkReady) {
initSink();
}
if (playing) {
jump(0);
playing = false;
}
paused = false;
playbin.stop();
playbin.getState();
}
/**
* ( begin auto-generated from Movie_read.xml )
*
* Reads the current frame of the movie.
*
* ( end auto-generated )
*
* @webref movie
* @usage web_application
* @brief Reads the current frame
*/
public synchronized void read() {
if (frameRate < 0) {
// Framerate not set yet, so we obtain from stream,
// which is already playing since we are in read().
frameRate = getSourceFrameRate();
}
if (volume < 0) {
// Idem for volume
volume = (float)playbin.getVolume();
}
if (useBufferSink) { // The native buffer from gstreamer is copied to the buffer sink.
outdatedPixels = true;
if (natBuffer == null) {
return;
}
if (firstFrame) {
super.init(bufWidth, bufHeight, ARGB, 1);
firstFrame = false;
}
if (bufferSink == null) {
Object cache = parent.g.getCache(this);
if (cache == null) {
return;
}
setBufferSink(cache);
getSinkMethods();
}
ByteBuffer byteBuffer = natBuffer.getByteBuffer();
try {
sinkCopyMethod.invoke(bufferSink, new Object[] { natBuffer, byteBuffer, bufWidth, bufHeight });
} catch (Exception e) {
e.printStackTrace();
}
natBuffer = null;
} else { // The pixels just read from gstreamer are copied to the pixels array.
if (copyPixels == null) {
return;
}
if (firstFrame) {
super.init(bufWidth, bufHeight, RGB, 1);
firstFrame = false;
}
int[] temp = pixels;
pixels = copyPixels;
updatePixels();
copyPixels = temp;
}
available = false;
newFrame = true;
}
/**
* Change the volume. Values are from 0 to 1.
*
* @param float v
*/
public void volume(float v) {
if (playing && PApplet.abs(volume - v) > 0.001f) {
playbin.setVolume(v);
volume = v;
}
}
public synchronized void loadPixels() {
super.loadPixels();
if (useBufferSink) {
if (natBuffer != null) {
// This means that the OpenGL texture hasn't been created so far (the
// video frame not drawn using image()), but the user wants to use the
// pixel array, which we can just get from natBuffer.
IntBuffer buf = natBuffer.getByteBuffer().asIntBuffer();
buf.rewind();
buf.get(pixels);
Video.convertToARGB(pixels, width, height);
} else if (sinkGetMethod != null) {
try {
// sinkGetMethod will copy the latest buffer to the pixels array,
// and the pixels will be copied to the texture when the OpenGL
// renderer needs to draw it.
sinkGetMethod.invoke(bufferSink, new Object[] { pixels });
} catch (Exception e) {
e.printStackTrace();
}
}
outdatedPixels = false;
}
}
public int get(int x, int y) {
if (outdatedPixels) loadPixels();
return super.get(x, y);
}
protected void getImpl(int sourceX, int sourceY,
int sourceWidth, int sourceHeight,
PImage target, int targetX, int targetY) {
if (outdatedPixels) loadPixels();
super.getImpl(sourceX, sourceY, sourceWidth, sourceHeight,
target, targetX, targetY);
}
////////////////////////////////////////////////////////////
// Initialization methods.
protected void initGStreamer(PApplet parent, String filename) {
this.parent = parent;
playbin = null;
File file;
Video.init();
// first check to see if this can be read locally from a file.
try {
try {
// first try a local file using the dataPath. usually this will
// work ok, but sometimes the dataPath is inside a jar file,
// which is less fun, so this will crap out.
file = new File(parent.dataPath(filename));
if (file.exists()) {
playbin = new PlayBin2("Movie Player");
playbin.setInputFile(file);
}
} catch (Exception e) {
} // ignored
// read from a file just hanging out in the local folder.
// this might happen when the video library is used with some
// other application, or the person enters a full path name
if (playbin == null) {
try {
file = new File(filename);
if (file.exists()) {
playbin = new PlayBin2("Movie Player");
playbin.setInputFile(file);
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (playbin == null) {
// Try network read...
for (int i = 0; i < supportedProtocols.length; i++) {
if (filename.startsWith(supportedProtocols[i] + "://")) {
try {
playbin = new PlayBin2("Movie Player");
playbin.setURI(URI.create(filename));
break;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
} catch (SecurityException se) {
// online, whups. catch the security exception out here rather than
// doing it three times (or whatever) for each of the cases above.
}
if (playbin == null) {
parent.die("Could not load movie file " + filename, null);
}
// we've got a valid movie! let's rock.
try {
this.filename = filename; // for error messages
// register methods
parent.registerMethod("dispose", this);
parent.registerMethod("post", this);
setEventHandlerObject(parent);
rate = 1.0f;
frameRate = -1;
volume = -1;
sinkReady = false;
bufWidth = bufHeight = 0;
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Uses a generic object as handler of the movie. This object should have a
* movieEvent method that receives a GSMovie argument. This method will
* be called upon a new frame read event.
*
*/
protected void setEventHandlerObject(Object obj) {
eventHandler = obj;
try {
movieEventMethod = eventHandler.getClass().getMethod("movieEvent", Movie.class);
return;
} catch (Exception e) {
// no such method, or an error... which is fine, just ignore
}
// movieEvent can alternatively be defined as receiving an Object, to allow
// Processing mode implementors to support the video library without linking
// to it at build-time.
try {
movieEventMethod = eventHandler.getClass().getMethod("movieEvent", Object.class);
} catch (Exception e) {
// no such method, or an error... which is fine, just ignore
}
}
protected void initSink() {
if (bufferSink != null || (Video.useGLBufferSink && parent.g.isGL())) {
useBufferSink = true;
if (bufferSink != null) {
getSinkMethods();
}
if (copyMask == null || copyMask.equals("")) {
initCopyMask();
}
natSink = new BufferDataAppSink("nat", copyMask,
new BufferDataAppSink.Listener() {
public void bufferFrame(int w, int h, Buffer buffer) {
invokeEvent(w, h, buffer);
}
});
natSink.setAutoDisposeBuffer(false);
playbin.setVideoSink(natSink);
// The setVideoSink() method sets the videoSink as a property of the
// PlayBin, which increments the refcount of the videoSink element.
// Disposing here once to decrement the refcount.
natSink.dispose();
} else {
rgbSink = new RGBDataAppSink("rgb",
new RGBDataAppSink.Listener() {
public void rgbFrame(int w, int h, IntBuffer buffer) {
invokeEvent(w, h, buffer);
}
});
// Setting direct buffer passing in the video sink.
rgbSink.setPassDirectBuffer(Video.passDirectBuffer);
playbin.setVideoSink(rgbSink);
// The setVideoSink() method sets the videoSink as a property of the
// PlayBin, which increments the refcount of the videoSink element.
// Disposing here once to decrement the refcount.
rgbSink.dispose();
}
// Creating bus to handle end-of-stream event.
Bus bus = playbin.getBus();
bus.connect(new Bus.EOS() {
public void endOfStream(GstObject element) {
eosEvent();
}
});
sinkReady = true;
newFrame = false;
}
////////////////////////////////////////////////////////////
// Stream event handling.
protected synchronized void invokeEvent(int w, int h, IntBuffer buffer) {
available = true;
bufWidth = w;
bufHeight = h;
if (copyPixels == null) {
copyPixels = new int[w * h];
}
buffer.rewind();
try {
buffer.get(copyPixels);
} catch (BufferUnderflowException e) {
e.printStackTrace();
copyPixels = null;
return;
}
if (playing) {
fireMovieEvent();
}
}
protected synchronized void invokeEvent(int w, int h, Buffer buffer) {
available = true;
bufWidth = w;
bufHeight = h;
if (natBuffer != null) {
// To handle the situation where read() is not called in the sketch, so
// that the native buffers are not being sent to the sinke, and therefore, not disposed
// by it.
natBuffer.dispose();
}
natBuffer = buffer;
if (playing) {
fireMovieEvent();
}
}
private void fireMovieEvent() {
// Creates a movieEvent.
if (movieEventMethod != null) {
try {
movieEventMethod.invoke(eventHandler, this);
} catch (Exception e) {
System.err.println("error, disabling movieEvent() for " + filename);
e.printStackTrace();
movieEventMethod = null;
}
}
}
protected void eosEvent() {
if (repeat) {
if (0 < rate) {
// Playing forward, so we return to the beginning
jump(0);
} else {
// Playing backwards, so we go to the end.
jump(duration());
}
// The rate is set automatically to 1 when restarting the
// stream, so we need to call frameRate in order to reset
// to the latest fps rate.
frameRate(frameRate);
} else {
playing = false;
}
}
////////////////////////////////////////////////////////////
// Stream query methods.
/**
* Get the height of the source video. Note: calling this method repeatedly
* can slow down playback performance.
*
* @return int
*/
protected int getSourceHeight() {
Dimension dim = playbin.getVideoSize();
if (dim != null) {
return dim.height;
} else {
return 0;
}
}
/**
* Get the original framerate of the source video. Note: calling this method
* repeatedly can slow down playback performance.
*
* @return float
*/
protected float getSourceFrameRate() {
return (float)playbin.getVideoSinkFrameRate();
}
/**
* Get the width of the source video. Note: calling this method repeatedly
* can slow down playback performance.
*
* @return int
*/
protected int getSourceWidth() {
Dimension dim = playbin.getVideoSize();
if (dim != null) {
return dim.width;
} else {
return 0;
}
}
////////////////////////////////////////////////////////////
// Buffer source interface.
/**
* Sets the object to use as destination for the frames read from the stream.
* The color conversion mask is automatically set to the one required to
* copy the frames to OpenGL.
*
* NOTE: This is not official API and may/will be removed at any time.
*
* @param Object dest
*/
public void setBufferSink(Object sink) {
bufferSink = sink;
initCopyMask();
}
/**
* Sets the object to use as destination for the frames read from the stream.
*
* NOTE: This is not official API and may/will be removed at any time.
*
* @param Object dest
* @param String mask
*/
public void setBufferSink(Object sink, String mask) {
bufferSink = sink;
copyMask = mask;
}
/**
* NOTE: This is not official API and may/will be removed at any time.
*/
public boolean hasBufferSink() {
return bufferSink != null;
}
/**
* NOTE: This is not official API and may/will be removed at any time.
*/
public synchronized void disposeBuffer(Object buf) {
((Buffer)buf).dispose();
}
protected void getSinkMethods() {
try {
sinkCopyMethod = bufferSink.getClass().getMethod("copyBufferFromSource",
new Class[] { Object.class, ByteBuffer.class, int.class, int.class });
} catch (Exception e) {
throw new RuntimeException("Movie: provided sink object doesn't have a " +
"copyBufferFromSource method.");
}
try {
sinkSetMethod = bufferSink.getClass().getMethod("setBufferSource",
new Class[] { Object.class });
sinkSetMethod.invoke(bufferSink, new Object[] { this });
} catch (Exception e) {
throw new RuntimeException("Movie: provided sink object doesn't have a " +
"setBufferSource method.");
}
try {
sinkDisposeMethod = bufferSink.getClass().getMethod("disposeSourceBuffer",
new Class[] { });
} catch (Exception e) {
throw new RuntimeException("Movie: provided sink object doesn't have " +
"a disposeSourceBuffer method.");
}
try {
sinkGetMethod = bufferSink.getClass().getMethod("getBufferPixels",
new Class[] { int[].class });
} catch (Exception e) {
throw new RuntimeException("Movie: provided sink object doesn't have " +
"a getBufferPixels method.");
}
}
protected void initCopyMask() {
if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) {
copyMask = "red_mask=(int)0xFF000000, green_mask=(int)0xFF0000, blue_mask=(int)0xFF00";
} else {
copyMask = "red_mask=(int)0xFF, green_mask=(int)0xFF00, blue_mask=(int)0xFF0000";
}
}
public synchronized void post() {
if (useBufferSink && sinkDisposeMethod != null) {
try {
sinkDisposeMethod.invoke(bufferSink, new Object[] {});
} catch (Exception e) {
e.printStackTrace();
}
}
}
}