/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Processing project - http://processing.org Copyright (c) 2004-09 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.lang.reflect.*; import quicktime.*; import quicktime.qd.*; import quicktime.std.*; import quicktime.std.sg.*; import quicktime.util.RawEncodedImage; /** * Watchin' shit on the telly. */ @SuppressWarnings("deprecation") public class Capture extends PImage implements Runnable { // there are more, but these are all we'll provide for now // The useful ref page for <a href="http://developer.apple.com/documentation/Java/Reference/1.4.1/Java141API_QTJ/constant-values.html">quicktime constants</a> static public final int COMPOSITE = StdQTConstants.compositeIn; // 0 static public final int SVIDEO = StdQTConstants.sVideoIn; // 1 static public final int COMPONENT = StdQTConstants.rgbComponentIn; // 2 static public final int TUNER = StdQTConstants.tvTunerIn; // 6 static public final int NTSC = StdQTConstants.ntscIn; static public final int PAL = StdQTConstants.palIn; static public final int SECAM = StdQTConstants.secamIn; // no longer needed because parent field added to PImage //PApplet parent; Method captureEventMethod; String name; // keep track for error messages (unused) Thread runner; boolean available = false; /** Temporary storage for the raw image data read directly from the capture device */ public int data[]; public int dataWidth; public int dataHeight; public int dataRowBytes; /** True if this image is currently being cropped */ public boolean crop; public int cropX; public int cropY; public int cropW; public int cropH; public int frameRate; public RawEncodedImage raw; public SequenceGrabber capture; /** the guy who's doing all the work */ public SGVideoChannel channel; /** boundary of image at the requested size */ protected QDRect qdrect; /* static { try { QTSession.open(); } catch (QTException e) { e.printStackTrace(); } // this doesn't appear to do jack QTRuntimeException.registerHandler(new QTRuntimeHandler() { public void exceptionOccurred(QTRuntimeException e, Object obj, String s, boolean flag) { System.err.println("Problem inside Capture"); e.printStackTrace(); } }); } */ public Capture(PApplet parent, int requestWidth, int requestHeight) { this(parent, requestWidth, requestHeight, null, 30); } public Capture(PApplet parent, int reqWidth, int reqHeight, int frameRate) { this(parent, reqWidth, reqHeight, null, frameRate); } public Capture(PApplet parent, int reqWidth, int reqHeight, String name) { this(parent, reqWidth, reqHeight, name, 30); } /** * If 'name' is null or the empty string, it won't set a specific * device, which means that QuickTime will use that last device * used by a QuickTime application. * <P/> * Unfortunately, Apple's QuickTime API uses the name to select devices, * and in some cases there might be cameras with the same name on a machine. * If you ask for a camera of the same name in sequence, you might see if it * just does the right thing and grabs each separate camera in succession. * If that doesn't work, you might try calling settings() which will * bring up the prompt where you can select a capture device. * <P/> * If the following function: * <PRE>public void captureEvent(Capture c)</PRE> * is defined in the host PApplet, then it will be called every * time a new frame is available from the capture device. */ public Capture(final PApplet parent, final int requestWidth, final int requestHeight, final String name, final int frameRate) { // Running on EDT because of weird hang on OS X // http://dev.processing.org/bugs/show_bug.cgi?id=882 // QTSession.open() is hanging, not sure why, but it seems to prefer // being run from the EDT. Not sure if that's a mistaken expectation in // QTJava (we hadn't had trouble in the past because we did everything // on the EDT) or if something broken in more recent QTJ. Or (maybe most // likely) we're simply hitting some other threading strangeness, and // using invokeLater() isolates us from that. Which is a nice way of // saying that it's a hack. //SwingUtilities.invokeLater(new Runnable() { // public void run() { init(parent, requestWidth, requestHeight, name, frameRate); //} //}); } public void init(PApplet parent, int requestWidth, int requestHeight, String name, int frameRate) { this.parent = parent; this.name = name; this.frameRate = frameRate; try { QTSession.open(); } catch (QTException e) { e.printStackTrace(System.out); return; } try { qdrect = new QDRect(requestWidth, requestHeight); // workaround for bug with the intel macs QDGraphics qdgraphics = null; //new QDGraphics(qdrect); if (quicktime.util.EndianOrder.isNativeLittleEndian()) { qdgraphics = new QDGraphics(QDConstants.k32BGRAPixelFormat, qdrect); } else { qdgraphics = new QDGraphics(QDGraphics.kDefaultPixelFormat, qdrect); } capture = new SequenceGrabber(); capture.setGWorld(qdgraphics, null); channel = new SGVideoChannel(capture); channel.setBounds(qdrect); channel.setUsage(2); // what is this usage number? capture.startPreview(); // maybe this comes later? PixMap pixmap = qdgraphics.getPixMap(); raw = pixmap.getPixelData(); /* if (name == null) { channel.settingsDialog(); } else if (name.length() > 0) { channel.setDevice(name); } */ if ((name != null) && (name.length() > 0)) { channel.setDevice(name); } dataRowBytes = raw.getRowBytes(); dataWidth = dataRowBytes / 4; dataHeight = raw.getSize() / dataRowBytes; if (dataWidth != requestWidth) { crop = true; cropX = 0; cropY = 0; cropW = requestWidth; cropH = requestHeight; } // initialize my PImage self super.init(requestWidth, requestHeight, RGB); parent.registerDispose(this); try { captureEventMethod = parent.getClass().getMethod("captureEvent", new Class[] { Capture.class }); } catch (Exception e) { // no such method, or an error.. which is fine, just ignore } runner = new Thread(this); runner.start(); } catch (QTException qte) { //} catch (StdQTException qte) { //qte.printStackTrace(); int errorCode = qte.errorCode(); if (errorCode == Errors.couldntGetRequiredComponent) { // this can happen when the capture device isn't available // or wasn't shut down properly parent.die("No capture could be found, " + "or the VDIG is not installed correctly.", qte); } else { parent.die("Error while setting up Capture", qte); } } catch (Exception e) { parent.die("Error while setting up Capture", e); } } /** * True if a frame is ready to be read. * <PRE> * // put this somewhere inside draw * if (capture.available()) capture.read(); * </PRE> * Alternatively, you can use captureEvent(Capture c) to notify you * whenever available() is set to true. In which case, things might * look like this: * <PRE> * public void captureEvent(Capture c) { * c.read(); * // do something exciting now that c has been updated * } * </PRE> */ public boolean available() { return available; } /** * Set the video to crop from its original. * <P> * It seems common that captures add lines to the top or bottom * of an image, so this can be useful for removing them. * Internally, the pixel buffer size returned from QuickTime is * often a different size than requested, so crop will be set * more often than not. */ public void crop(int x, int y, int w, int h) { /* if (imageMode == CORNERS) { w -= x; // w was actually x2 h -= y; // h was actually y2 } */ crop = true; cropX = Math.max(0, x); cropY = Math.max(0, y); cropW = Math.min(w, dataWidth); cropH = Math.min(dataHeight, y + h) - cropY; // if size has changed, re-init this image if ((cropW != width) || (cropH != height)) { init(w, h, RGB); } } /** * Remove the cropping (if any) of the image. * <P> * By default, cropping is often enabled to trim out black pixels. * But if you'd rather deal with them yourself (so as to avoid * an extra lag while the data is moved around) you can shut it off. */ public void noCrop() { crop = false; } public void read() { //try { //synchronized (capture) { loadPixels(); synchronized (pixels) { //System.out.println("read1"); if (crop) { //System.out.println("read2a"); // f#$)(#$ing quicktime / jni is so g-d slow, calling copyToArray // for the invidual rows is literally 100x slower. instead, first // copy the entire buffer to a separate array (i didn't need that // memory anyway), and do an arraycopy for each row. if (data == null) { data = new int[dataWidth * dataHeight]; } raw.copyToArray(0, data, 0, dataWidth * dataHeight); int sourceOffset = cropX + cropY*dataWidth; int destOffset = 0; for (int y = 0; y < cropH; y++) { System.arraycopy(data, sourceOffset, pixels, destOffset, cropW); sourceOffset += dataWidth; destOffset += width; } } else { // no crop, just copy directly //System.out.println("read2b"); raw.copyToArray(0, pixels, 0, width * height); } //System.out.println("read3"); available = false; // mark this image as modified so that PGraphicsJava2D and // PGraphicsOpenGL will properly re-blit and draw this guy updatePixels(); //System.out.println("read4"); } } public void run() { while ((Thread.currentThread() == runner) && (capture != null)) { try { synchronized (capture) { capture.idle(); //read(); available = true; if (captureEventMethod != null) { try { captureEventMethod.invoke(parent, new Object[] { this }); } catch (Exception e) { System.err.println("Disabling captureEvent() for " + name + " because of an error."); e.printStackTrace(); captureEventMethod = null; } } } } catch (QTException e) { errorMessage("run", e); } try { Thread.sleep(1000 / frameRate); } catch (InterruptedException e) { } } } /** * Set the frameRate for how quickly new frames are read * from the capture device. */ public void frameRate(int iframeRate) { if (iframeRate <= 0) { System.err.println("Capture: ignoring bad frameRate of " + iframeRate + " fps."); return; } frameRate = iframeRate; } /** * Called by applets to stop capturing video. */ public void stop() { if (capture != null) { try { capture.stop(); // stop the "preview" } catch (StdQTException e) { e.printStackTrace(); } capture = null; } runner = null; // unwind the thread } /** * Called by PApplet to shut down video so that QuickTime * can be used later by another applet. */ public void dispose() { stop(); //System.out.println("calling dispose"); // this is important so that the next app can do video QTSession.close(); } /** * General error reporting, all corraled here just in case * I think of something slightly more intelligent to do. */ protected void errorMessage(String where, Exception e) { parent.die("Error inside Capture." + where + "()", e); } /** * Set the format to ask for from the video digitizer: * TUNER, COMPOSITE, SVIDEO, or COMPONENT. * <P> * The constants are just aliases to the constants returned from * QuickTime's getInputFormat() function, so any valid constant from * that will work just fine. */ public void source(int which) { try { VideoDigitizer digitizer = channel.getDigitizerComponent(); int count = digitizer.getNumberOfInputs(); for (int i = 0; i < count; i++) { //System.out.println("format " + digitizer.getInputFormat(i)); if (digitizer.getInputFormat(i) == which) { digitizer.setInput(i); return; } } throw new RuntimeException("The specified source() is not available."); } catch (StdQTException e) { e.printStackTrace(); throw new RuntimeException("Could not set the video input source."); } } /** * Set the video format standard to use on the * video digitizer: NTSC, PAL, or SECAM. * <P> * The constants are just aliases to the constants used for * QuickTime's setInputStandard() function, so any valid * constant from that will work just fine. */ public void format(int which) { try { VideoDigitizer digitizer = channel.getDigitizerComponent(); digitizer.setInputStandard(which); } catch (StdQTException e) { e.printStackTrace(); //throw new RuntimeException("Could not set the video input format"); } } /** * Show the settings dialog for this input device. */ public void settings() { try { // fix for crash here submitted by hansi (stop/startPreview lines) capture.stop(); // Whenever settingsDialog() is called, the boundries change, // causing the image to be cropped. Fix for Bug #366 // http://dev.processing.org/bugs/show_bug.cgi?id=366 channel.setBounds(qdrect); // Open the settings dialog (throws an Exception if canceled) channel.settingsDialog(); } catch (StdQTException qte) { int errorCode = qte.errorCode(); if (errorCode == Errors.userCanceledErr) { // User only canceled the settings dialog, continue as we were } else { qte.printStackTrace(); throw new RuntimeException("Error inside Capture.settings()"); } } try { // Start the preview again (unreachable if newly thrown exception) capture.startPreview(); } catch (StdQTException qte) { qte.printStackTrace(); } } /** * Get a list of all available captures as a String array. * i.e. println(Capture.list()) will show you the goodies. */ static public String[] list() { try { QTSession.open(); SequenceGrabber grabber = new SequenceGrabber(); SGVideoChannel channel = new SGVideoChannel(grabber); SGDeviceList deviceList = channel.getDeviceList(0); // flags is 0 String listing[] = new String[deviceList.getCount()]; for (int i = 0; i < deviceList.getCount(); i++) { listing[i] = deviceList.getDeviceName(i).getName(); } // properly shut down the channel so the app can use it again grabber.disposeChannel(channel); QTSession.close(); return listing; } catch (QTException qte) { int errorCode = qte.errorCode(); if (errorCode == Errors.couldntGetRequiredComponent) { throw new RuntimeException("Couldn't find any capture devices, " + "read the video reference for more info."); } else { qte.printStackTrace(); throw new RuntimeException("Problem listing capture devices, " + "read the video reference for more info."); } } //return null; } }