/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Processing project - http://processing.org Copyright (c) 2012-22 The Processing Foundation Copyright (c) 2004-12 Ben Fry and Casey Reas GStreamer implementation ported from GSVideo library by Andres Colubri 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.nio.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.EnumSet; import java.util.List; import java.lang.reflect.*; import org.freedesktop.gstreamer.*; import org.freedesktop.gstreamer.Buffer; import org.freedesktop.gstreamer.device.*; import org.freedesktop.gstreamer.elements.*; import org.freedesktop.gstreamer.event.SeekFlags; import org.freedesktop.gstreamer.event.SeekType; /** * Datatype for storing and manipulating video frames from an attached * capture device such as a camera. Use Capture.list() to show * the names of any attached devices. Using the version of the constructor * without name will attempt to use the last device used by a * QuickTime program. * * @webref capture * @webBrief Datatype for storing and manipulating video frames from an * attached capture device such as a camera. * @usage application */ public class Capture extends PImage implements PConstants { public Pipeline pipeline; // The source resolution and framerate of the device public int sourceWidth; public int sourceHeight; public float sourceFrameRate; public float frameRate; protected float rate; protected boolean capturing = false; protected Method captureEventMethod; protected Object eventHandler; protected boolean available; protected boolean ready; protected boolean newFrame; protected AppSink rgbSink = null; protected int[] copyPixels = null; protected boolean firstFrame = true; 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 device; protected static List devices; // we're caching this list for speed reasons NewSampleListener newSampleListener; NewPrerollListener newPrerollListener; private final Lock bufferLock = new ReentrantLock(); /** * Open the default capture device * @param parent PApplet, typically "this" */ public Capture(PApplet parent) { // Attempt to use a default resolution this(parent, 640, 480, null, 0); } /** * Open a specific capture device * @param device device name * @see Capture#list() * @see Capture#listRawNames() */ public Capture(PApplet parent, String device) { // Attempt to use a default resolution this(parent, 640, 480, device, 0); } /** * Open the default capture device with a given resolution * @param width width in pixels * @param height height in pixels */ public Capture(PApplet parent, int width, int height) { this(parent, width, height, null, 0); } /** * Open the default capture device with a given resolution and framerate * @param fps frames per second */ public Capture(PApplet parent, int width, int height, float fps) { this(parent, width, height, null, fps); } /** * Open a specific capture device with a given resolution * @see Capture#list() */ public Capture(PApplet parent, int width, int height, String device) { this(parent, width, height, device, 0); } /** * Open a specific capture device with a given resolution and framerate * @see Capture#list() */ public Capture(PApplet parent, int width, int height, String device, float fps) { super(width, height, RGB); this.device = device; this.frameRate = fps; initGStreamer(parent); } /** * Disposes all the native resources associated to this capture device. * * NOTE: This is not official API and may/will be removed at any time. */ public void dispose() { if (pipeline != null) { try { if (pipeline.isPlaying()) { pipeline.stop(); pipeline.getState(); } } catch (Exception e) { } pixels = null; rgbSink.disconnect(newSampleListener); rgbSink.disconnect(newPrerollListener); rgbSink.dispose(); pipeline.setState(org.freedesktop.gstreamer.State.NULL); pipeline.getState(); pipeline.getBus().dispose(); pipeline.dispose(); 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(); } } /** * Sets how often frames are read from the capture device. Setting the fps * parameter to 4, for example, will cause 4 frames to be read per second. * * @webref capture * @webBrief Sets how often frames are read from the capture device. * @usage web_application * @param ifps speed of the capture device in frames per second * @brief Sets the target frame rate */ public void frameRate(float ifps) { float f = (0 < ifps && 0 < frameRate) ? ifps / frameRate : 1; long t = pipeline.queryPosition(TimeUnit.NANOSECONDS); long start, stop; if (rate > 0) { start = t; stop = -1; } else { start = 0; stop = t; } seek(rate * f, start, stop); frameRate = ifps; } /** * Returns "true" when a new frame from the device is available to read. * * @webref capture * @webBrief Returns "true" when a new frame from the device is available to read. * @usage web_application * @brief Returns "true" when a new frame is available to read. */ public boolean available() { return available; } /** * Starts capturing frames from the selected device. * * @webref capture * @webBrief Starts capturing frames from an attached device. * @usage web_application * @brief Starts video capture */ public void start() { setReady(); pipeline.play(); pipeline.getState(); capturing = true; } /** * Stops capturing frames from an attached device. * * @webref capture * @webBrief Stops capturing frames from an attached device. * @usage web_application * @brief Stops video capture */ public void stop() { setReady(); pipeline.stop(); pipeline.getState(); capturing = false; } /** * Reads the current frame of the device. * * @webref capture * @webBrief Reads the current frame of the device. * @usage web_application * @brief Reads the current frame */ public synchronized void read() { if (firstFrame) { super.init(sourceWidth, sourceHeight, RGB, 1); firstFrame = false; } if (useBufferSink) { if (bufferSink == null) { Object cache = parent.g.getCache(Capture.this); if (cache != null) { setBufferSink(cache); getSinkMethods(); } } } else { int[] temp = pixels; pixels = copyPixels; updatePixels(); copyPixels = temp; } available = false; newFrame = true; } /** * Loads the pixel data for the image into its pixels[] array. */ @Override public synchronized void loadPixels() { super.loadPixels(); if (useBufferSink && bufferSink != 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; } } /** * Reads the color of any pixel or grabs a section of an image. */ @Override public int get(int x, int y) { if (outdatedPixels) loadPixels(); return super.get(x, y); } /** * @param w width of pixel rectangle to get * @param h height of pixel rectangle to get */ public PImage get(int x, int y, int w, int h) { if (outdatedPixels) loadPixels(); return super.get(x, y, w, h); } @Override public PImage copy() { if (outdatedPixels) loadPixels(); return super.copy(); } 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); } /** * Check if this device object is currently capturing. */ public boolean isCapturing() { return capturing; } //////////////////////////////////////////////////////////// // Initialization methods. protected void initGStreamer(PApplet parent) { this.parent = parent; pipeline = null; Video.init(); if(device == null) { String[] devices = list(); if(devices != null && devices.length > 0) { device = devices[0]; } else { throw new IllegalStateException("Could not find any devices"); } } device = device.trim(); int p = device.indexOf("pipeline:"); if (p == 0) { initCustomPipeline(device.substring(9)); } else { initDevicePipeline(); } try { // Register methods parent.registerMethod("dispose", this); parent.registerMethod("post", this); setEventHandlerObject(parent); sourceWidth = sourceHeight = 0; sourceFrameRate = -1; frameRate = -1; rate = 1.0f; ready = false; } catch (Exception e) { e.printStackTrace(); } } public static String fpsToFramerate(float fps) { String formatted = Float.toString(fps); // This presumes the delimitter is always a dot int i = formatted.indexOf('.'); if (Math.floor(fps) != fps) { int denom = (int)Math.pow(10, formatted.length()-i-1); int num = (int)(fps * denom); return num + "/" + denom; } else { return (int)fps + "/1"; } } protected void initCustomPipeline(String pstr) { String PIPELINE_END = " ! videorate ! videoscale ! videoconvert ! appsink name=sink"; pipeline = (Pipeline) Gst.parseLaunch(pstr + PIPELINE_END); String caps = ", width=" + width + ", height=" + height; if (frameRate != 0.0) { caps += ", framerate=" + fpsToFramerate(frameRate); } rgbSink = (AppSink) pipeline.getElementByName("sink"); rgbSink.set("emit-signals", true); newSampleListener = new NewSampleListener(); newPrerollListener = new NewPrerollListener(); rgbSink.connect(newSampleListener); rgbSink.connect(newPrerollListener); useBufferSink = Video.useGLBufferSink && parent.g.isGL(); if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { if (useBufferSink) { rgbSink.setCaps(Caps.fromString("video/x-raw, format=RGBx" + caps)); } else { rgbSink.setCaps(Caps.fromString("video/x-raw, format=BGRx" + caps)); } } else { rgbSink.setCaps(Caps.fromString("video/x-raw, format=xRGB" + caps)); } makeBusConnections(pipeline.getBus()); } protected void initDevicePipeline() { Element srcElement = null; if (device == null) { // Use the default device from GStreamer srcElement = ElementFactory.make("autovideosrc", null); } else { // Look for device if (devices == null) { DeviceMonitor monitor = new DeviceMonitor(); monitor.addFilter("Video/Source", null); devices = monitor.getDevices(); monitor.close(); } for (int i=0; i < devices.size(); i++) { String deviceName = assignDisplayName(devices.get(i), i); if (devices.get(i).getDisplayName().equals(device) || devices.get(i).getName().equals(device) || deviceName.equals(device)) { // Found device srcElement = devices.get(i).createElement(null); break; } } // Error out if we got passed an invalid device name if (srcElement == null) { throw new RuntimeException("Could not find device " + device); } } pipeline = new Pipeline(); Element videoscale = ElementFactory.make("videoscale", null); Element videoconvert = ElementFactory.make("videoconvert", null); Element capsfilter = ElementFactory.make("capsfilter", null); String frameRateString; if (frameRate != 0.0) { frameRateString = ", framerate=" + fpsToFramerate(frameRate); } else { frameRateString = ""; } capsfilter.set("caps", Caps.fromString("video/x-raw, width=" + width + ", height=" + height + frameRateString)); initSink(); pipeline.addMany(srcElement, videoscale, videoconvert, capsfilter, rgbSink); Element.linkMany(srcElement, videoscale, videoconvert, capsfilter, rgbSink); makeBusConnections(pipeline.getBus()); } /** * Uses a generic object as handler of the capture. This object should have a * captureEvent method that receives a Capture argument. This method will * be called upon a new frame read event. * */ protected void setEventHandlerObject(Object obj) { eventHandler = obj; try { captureEventMethod = eventHandler.getClass().getMethod("captureEvent", Capture.class); return; } catch (Exception e) { // no such method, or an error... which is fine, just ignore } // captureEvent 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 { captureEventMethod = eventHandler.getClass().getMethod("captureEvent", Object.class); } catch (Exception e) { // no such method, or an error... which is fine, just ignore } } protected void initSink() { rgbSink = new AppSink("capture sink"); rgbSink.set("emit-signals", true); newSampleListener = new NewSampleListener(); newPrerollListener = new NewPrerollListener(); rgbSink.connect(newSampleListener); rgbSink.connect(newPrerollListener); useBufferSink = Video.useGLBufferSink && parent.g.isGL(); if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { if (useBufferSink) rgbSink.setCaps(Caps.fromString("video/x-raw, format=RGBx")); else rgbSink.setCaps(Caps.fromString("video/x-raw, format=BGRx")); } else { rgbSink.setCaps(Caps.fromString("video/x-raw, format=xRGB")); } } protected void setReady() { if (!ready) { pipeline.setState(org.freedesktop.gstreamer.State.READY); newFrame = false; ready = true; } } private void makeBusConnections(Bus bus) { bus.connect(new Bus.ERROR() { public void errorMessage(GstObject arg0, int arg1, String arg2) { System.err.println(arg0 + " : " + arg2); } }); bus.connect(new Bus.EOS() { public void endOfStream(GstObject arg0) { try { stop(); } catch (Exception ex) { ex.printStackTrace(); } } }); } //////////////////////////////////////////////////////////// // Stream event handling. private void seek(double rate, long start, long stop) { Gst.invokeLater(new Runnable() { public void run() { boolean res = pipeline.seek(rate, Format.TIME, EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), SeekType.SET, start, SeekType.SET, stop); if (!res) { PGraphics.showWarning("Seek operation failed."); } } }); } private void fireCaptureEvent() { if (captureEventMethod != null) { try { captureEventMethod.invoke(eventHandler, this); } catch (Exception e) { System.err.println("error, disabling captureEvent()"); e.printStackTrace(); captureEventMethod = null; } } } //////////////////////////////////////////////////////////// // 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; } /** * 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("Capture: 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("Capture: provided sink object doesn't have a " + "setBufferSource method."); } try { sinkDisposeMethod = bufferSink.getClass().getMethod("disposeSourceBuffer", new Class[] { }); } catch (Exception e) { throw new RuntimeException("Capture: 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("Capture: provided sink object doesn't have " + "a getBufferPixels method."); } } public synchronized void post() { if (useBufferSink && sinkDisposeMethod != null) { try { sinkDisposeMethod.invoke(bufferSink, new Object[] {}); } catch (Exception e) { e.printStackTrace(); } } } /** * Returns a list of all capture devices, using the device's pretty display name. * Multiple devices can have identical display names, appending ' #n' to devices * with duplicate display names. * @return array of device names */ static public String[] list() { Video.init(); String[] out; DeviceMonitor monitor = new DeviceMonitor(); monitor.addFilter("Video/Source", null); devices = monitor.getDevices(); monitor.close(); out = new String[devices.size()]; for (int i = 0; i < devices.size(); i++) { Device dev = devices.get(i); out[i] = checkCameraDuplicates(dev) > 1 ? assignDisplayName(dev, i) : dev.getDisplayName(); } return out; } static private String assignDisplayName(Device d, int pos) { String s = ""; int count = 1; for(int i = 0; i < devices.size(); i++) { if (devices.get(i).getDisplayName().equals(d.getDisplayName())){ if (i == pos) { s = d.getDisplayName() + " #" + Integer.toString(count); } count++; } } return s; } static private int checkCameraDuplicates(Device d) { int count = 0; for (int i = 0; i < devices.size(); i++) { if (devices.get(i).getDisplayName().equals(d.getDisplayName())) { count++; } } return count; } private class NewSampleListener implements AppSink.NEW_SAMPLE { @Override public FlowReturn newSample(AppSink sink) { Sample sample = sink.pullSample(); // Pull out metadata from caps Structure capsStruct = sample.getCaps().getStructure(0); sourceWidth = capsStruct.getInteger("width"); sourceHeight = capsStruct.getInteger("height"); Fraction fps = capsStruct.getFraction("framerate"); sourceFrameRate = (float)fps.numerator / fps.denominator; // Set the playback rate to the file's native framerate // unless the user has already set a custom one if (frameRate == -1.0) { frameRate = sourceFrameRate; } Buffer buffer = sample.getBuffer(); ByteBuffer bb = buffer.map(false); if (bb != null) { // If the EDT is still copying data from the buffer, just drop this frame if (!bufferLock.tryLock()) { return FlowReturn.OK; } available = true; if (useBufferSink && bufferSink != null) { // The native buffer from GStreamer is copied to the buffer sink. try { sinkCopyMethod.invoke(bufferSink, new Object[] { buffer, bb, sourceWidth, sourceHeight }); if (capturing) { fireCaptureEvent(); } } catch (Exception e) { e.printStackTrace(); } finally { bufferLock.unlock(); } } else { IntBuffer rgb = bb.asIntBuffer(); if (copyPixels == null) { copyPixels = new int[sourceWidth * sourceHeight]; } try { rgb.get(copyPixels, 0, width * height); if (capturing) { fireCaptureEvent(); } } finally { bufferLock.unlock(); } } buffer.unmap(); } sample.dispose(); return FlowReturn.OK; } } private class NewPrerollListener implements AppSink.NEW_PREROLL { @Override public FlowReturn newPreroll(AppSink sink) { Sample sample = sink.pullPreroll(); // Pull out metadata from caps Structure capsStruct = sample.getCaps().getStructure(0); sourceWidth = capsStruct.getInteger("width"); sourceHeight = capsStruct.getInteger("height"); Fraction fps = capsStruct.getFraction("framerate"); sourceFrameRate = (float)fps.numerator / fps.denominator; // Set the playback rate to the file's native framerate // unless the user has already set a custom one if (frameRate == -1.0) { frameRate = sourceFrameRate; } sample.dispose(); return FlowReturn.OK; } } }