0

I'm using Webcam Capture API in java to access my webcam. Webcam Capture API is built on Swing, I know that, however I want to combine the Webcam Swing class with my JavaFX class. The JavaFX class displays a rectangle on the screen. My goal is: I run my JavaFX class which displays the rectangle on the screen. At some point (e.g. mouse click) I want to start the Webcam. The Webcam is setup to look at the screen and should then do certain things with the images of the rectangle.

JavaFX class:

public class JavaFXDisplay extends Application {

    @Override
    public void start(Stage primaryStage) {
        WebcamCapture wc = new WebcamCapture();

        StackPane root = new StackPane();

        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(500);
        rectangle.setHeight(500);

        Scene scene = new Scene(root, 1000, 1000);
        root.getChildren().addAll(rectangle);

        primaryStage.setScene(scene);
        primaryStage.show();

        scene.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent mouseEvent) {
                wc.doSomething();
            }
        });
   }

   public static void main(String[] args) {
       launch(args);
   }
}

Swing class:

public class WebcamCapture extends JFrame implements Runnable, ThreadFactory {

    private static final long serialVersionUID = 6441489157408381878L;

    private Executor executor = Executors.newSingleThreadExecutor(this);

    private Webcam webcam = null;
    private WebcamPanel panel = null;
    private JTextArea textarea = null;

    public WebcamCapture() {
        super();

        setLayout(new FlowLayout());
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        Dimension size = WebcamResolution.QVGA.getSize();

        webcam = Webcam.getWebcams().get(0);
        webcam.setViewSize(size);

        panel = new WebcamPanel(webcam);
        panel.setPreferredSize(size);

        textarea = new JTextArea();
        textarea.setEditable(false);
        textarea.setPreferredSize(size);

        add(panel);
        add(textarea);

        pack();
        setVisible(true);
    }

    public void doSomething() {
        executor.execute(this);
    }

    @Override
    public void run() {
        do {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            BufferedImage image = null;

            if (webcam.isOpen()) {
                if ((image = webcam.getImage()) == null) {
                    continue;
                }

                doSomeStuff;
            }
        } while (true);
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, "example-runner");
        t.setDaemon(true);
        return t;
    }

    public static void main(String[] args) {
        new WebcamCapture();
    }
}

However my JavaFX class is not starting/displaying. What is wrong with my code?

  • I'm not familiar with the webcam capture API you're using; however if you mix Swing and JavaFX you need to manage the threads correctly. Swing UI can only be manipulated on the AWT event dispatch thread; JavaFX UI can only be manipulated on the JavaFX Application Thread. See, e.g, [`SwingNode`](http://docs.oracle.com/javase/8/javafx/api/javafx/embed/swing/SwingNode.html) for mixing the two. (You *might* be able to embed your web cam directly in JavaFX with a `SwingNode`, if the components are lightweight.) – James_D Mar 15 '17 at 18:02
  • You are probably right, implementing the Swing part in JavaFX may be the best solution, however I'm not familiar enough with JavaFX and the Webcam Capture API to do it. – Danielle Woods Mar 15 '17 at 18:16
  • So if you move everything to the correct thread, does it work? – James_D Mar 15 '17 at 18:16
  • Isn't everything already in the correct thread? At least I think it is. – Danielle Woods Mar 15 '17 at 18:25
  • No: you are doing all your Swing work on the FX Application Thread. – James_D Mar 15 '17 at 18:26
  • Also (forgive the "is it turned on" question): both your classes have `main(...)` methods. You are executing the main method in `JavaFXDisplay`, right...? – James_D Mar 15 '17 at 18:29
  • Yes exactly I am executing the main method in `JavaFXDisplay`. I even see that the webcam is started, it is just both UIs are missing. But I need to somehow launch the Swing class from the JavaFX class, so I need to start if from the FX Application thread. – Danielle Woods Mar 15 '17 at 18:40
  • So, just to update, testing on my Mac it appears you cannot touch the `Webcam` instances at all on the FX Application Thread: it just hangs. I guess there is some deadlock introduced in the native graphics pipeline. You probably should be doing all that work off that thread anyway, but you have to be careful... Anyway, I put some sample code up as a gist at https://gist.github.com/james-d/f826c9f38d53628114124a56fb7c4557. That just provides some wrappers for the webcam using the JavaFX concurrency API (basically a service). Also an `ImageView`-based view of the service. Not production quality – James_D Mar 16 '17 at 01:43
  • And where do I put my original `JavaFXDisplay` in this example? Do I just combine it with the `FXCamTest`? Because otherwise again I have to call the `FXCamTest` in a separate Thread I guess? – Danielle Woods Mar 16 '17 at 08:44
  • No, `FXCamTest` is just an example of using the classes I wrote. So your application code would replace that and use those classes in the same way. You only ever have one `Application` subclass in a JavaFX application. – James_D Mar 16 '17 at 09:04
  • Ok, so here is my conclusion: Your new solution also works perfectly, however it is not suited for my scenario because I need the basic function to stay the same as in my original `WebcamCapture` class. That means I need the window to open and display the webcam when I call it (i.e. I create the class instance) and with a method call (`run()` method) to capture the images and start some behavior. Thank you very much for all the effort, you really helped me a lot. I upvoted your answer it is just not visible for the public. – Danielle Woods Mar 16 '17 at 11:00
  • Or is there a way to move the `cam.open()` and `cam.close()` from the `WebCamService` class to the `FXCamTest`. When I do that, the camera is somehow not starting. – Danielle Woods Mar 16 '17 at 12:01
  • As far as I can tell, empirically, the threading rules seem to be: 1. You must retrieve the list of cameras *before the FX Application starts*. (I think that requirement is unintentional, i.e. a bug in the library/driver code.) So it works in `init()`, but basically nowhere else. 2. `cam.open()` and `cam.close()` *must* be called on a background thread. (That seems more reasonable.) – James_D Mar 16 '17 at 12:10
  • Alright so it definitely won't work with my desired behavior (included in my conclusion). Thank you again very much, the other solution with Swing works with it. – Danielle Woods Mar 16 '17 at 12:46

1 Answers1

0

I'm not familiar with the web cam API you're using (so I don't know if this is the only thing wrong), but you do need to create your Swing content on the AWT event dispatch thread. Currently you are creating it on the FX Application Thread.

You can use the following idiom:

public class JavaFXDisplay extends Application {

    private WebcamCapture wc ;

    @Override
    public void init() throws Exception {
        super.init();
        FutureTask<WebcamCapture> launchWebcam = new FutureTask<>(WebcamCapture::new) ;
        SwingUtilities.invokeLater(launchWebcam);

        // block until webcam is started:
        wc = launchWebcam.get();
    }

    @Override
    public void start(Stage primaryStage) throws Exception {

        StackPane root = new StackPane();

        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(500);
        rectangle.setHeight(500);

        Scene scene = new Scene(root, 1000, 1000);
        root.getChildren().addAll(rectangle);

        primaryStage.setScene(scene);
        primaryStage.show();

        scene.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent mouseEvent) {
                wc.doSomething();
            }
        });
   }

   public static void main(String[] args) {
       launch(args);
   }
}

For reference, here is a JavaFX-only webcam viewer. I tested this on a MacBookPro running OS X Sierra (10.12.2), with a 2011 27" Thunderbolt display with camera.

import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamResolution;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class FXWebCamViewer extends Application {

    private BlockingQueue<Image> imageQueue = new ArrayBlockingQueue<>(5);

    private Executor exec = Executors.newCachedThreadPool(runnable -> {
        Thread t = new Thread(runnable);
        t.setDaemon(true);
        return t ;
    });

    private Webcam webcam;

    @Override
    public void init() {
        webcam = Webcam.getWebcams().get(0);
        Dimension viewSize = WebcamResolution.QVGA.getSize();
        webcam.setViewSize(viewSize);
        webcam.open();
    }

    @Override
    public void start(Stage primaryStage) {
        ImageView imageView = new ImageView();

        StackPane root = new StackPane(imageView);
        imageView.fitWidthProperty().bind(root.widthProperty());
        imageView.fitHeightProperty().bind(root.heightProperty());
        imageView.setPreserveRatio(true);

        AnimationTimer updateImage = new AnimationTimer() {
            @Override
            public void handle(long timestamp) {
                Image image = imageQueue.poll();
                if (image != null) {
                    imageView.setImage(image);
                }
            }
        };
        updateImage.start();

        exec.execute(this::generateImages);

        Scene scene = new Scene(root, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void generateImages() {
        while (! Thread.interrupted()) {
            try {
                if (webcam.isOpen() && webcam.isImageNew()) {
                    BufferedImage bimg = webcam.getImage();
                    if (bimg != null) {
                        imageQueue.put(SwingFXUtils.toFXImage(bimg, null));
                    }
                } else {
                    Thread.sleep(250);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Sadly my problem still persists. Just to make it clear, the Swing class with the Webcam Capture API stuff is from the examples of the creator of the library and works (Link: https://github.com/sarxos/webcam-capture/tree/master/webcam-capture-examples/webcam-capture-qrcode) However what makes me sceptic is that the JavaFX example of the author (Link: https://github.com/sarxos/webcam-capture/tree/master/webcam-capture-examples/webcam-capture-javafx) also doesn't work. Could maybe someone check for me if it works? Maybe it has something to do with my machine. – Danielle Woods Mar 15 '17 at 19:04
  • @DanielleWoods See update. That works for me with version 0.3.10 of the sarxos library. Not quite sure why the initialization has to move to `init()`... – James_D Mar 15 '17 at 19:06
  • I can confirm that your first solution works indeed. I will have a look at your second solution now. However what seems strange to me is that you are calling `webcam.getImage()` in the `init()` method. Is there no way to call it in the `start()` method since I thought `init()` is just for initialization and not for continuous stuff? – Danielle Woods Mar 15 '17 at 20:00
  • I'm not really calling it in the `init()` method: I'm calling it in a background thread (which is really what you want to be doing). I'm just creating and launching the background thread in the `init()` method. You could certainly move the creation of the runnable to the `start()` method, if you prefer. Updated answer along those lines. – James_D Mar 15 '17 at 20:03
  • One more question, is it also possible to create a `FutureTask` for a class that has instance variables that get instantiated in the constructor of the class? – Danielle Woods Mar 15 '17 at 20:45
  • @DanielleWoods Yes, should not cause any problem. Not quite sure what you are asking... – James_D Mar 15 '17 at 20:46
  • For the syntax. Where do I have to add the variables here `new FutureTask<>(WebcamCapture::new)`. I changed the constructor of `WebcamCapture` to `public WebcamCapture(int firstParam, int secondParam)` – Danielle Woods Mar 15 '17 at 20:54
  • The just `new FutureTask<>(()->new WebcamCapture(firstParam, secondParam));`. – James_D Mar 15 '17 at 20:56
  • So I got everything to work with your first solution, thank you very much. It may not be very beautiful but it works. So everyone who is looking for something like this try it out. However I'm trying now with your second solution. What would actually be the way to use it. Should I combine my original `JavaFXViewer` class and your `FXWebCamViewer` class into one single class or should I call it again with a `FutureTask`? – Danielle Woods Mar 15 '17 at 21:57
  • Added a gist at https://gist.github.com/james-d/f826c9f38d53628114124a56fb7c4557 which makes it relatively easy to decouple it all from the rest of your application. BTW you should mark this answer as correct if it's working for you. – James_D Mar 16 '17 at 01:44