bytedeco / javacv

Java interface to OpenCV, FFmpeg, and more
Other
7.41k stars 1.57k forks source link

Mouse events kill key events in CanvasFrame #2123

Closed gareth-edwards closed 3 weeks ago

gareth-edwards commented 8 months ago

First: thanks for the amazing library!

I'm trying to make a video player with controls from both keyboard and mouse with spacebar or mouse click. This works with the spacebar or mouse click to stop/start the video. You can use the spacebar as many times as you like at first, but after the first mouse click the spacebar no longer works.

I have tried various ways to attach/re-attach the key and mouse event listeners but it is beyond me! Can you help? Here is the code to replicate:

public class MouseEventsKillKeyEventsPlayerTest {

    private static class PlayerThread extends Thread {

        private PlayerCanvasFrame _playerCanvasFrame;
        private boolean _running;
        private double _frameRate;

        public boolean getRunning() { return _running; }

        public PlayerThread(PlayerCanvasFrame sessionFrame) {
            // retain the session frame
            _playerCanvasFrame = sessionFrame;
            // mark running
            _running = true;
            // start with frameRate from video frameRate from grabber
            _frameRate = _playerCanvasFrame.getFrameGrabber().getFrameRate();
            start();

        }

        @Override
        public void run() {

            // record milliseconds for update image to determine any sleep to match frame rate
            long startTime = 0;
            long finishTime = System.currentTimeMillis();
            long sleepTime = 0;

            while (_running) {
                // get the time now
                startTime = finishTime;
                // update the frame, passing in the sleep time for skipping when fast
                _playerCanvasFrame.updateImage(sleepTime);
                // get the finish time
                finishTime = System.currentTimeMillis();
                // derive any sleep time. Positive times are sleep times, negative times are skip. Never more than 2 seconds
                sleepTime = Math.min(2000, (long) (1000 / _frameRate - (finishTime - startTime)));
                // check sleep if need be, or skip showing until
                if (sleepTime > 0) {

                    // do any sleep
                    try {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException e) {}
                }
            }
        }

        @Override
        public void interrupt() {
            _running = false;
            super.interrupt();
        }

    }

    public static class PlayerCanvasFrame extends CanvasFrame  {

        private File _file;
        private FFmpegFrameGrabber _grabber;
        private PlayerThread _playerThread;

        public PlayerCanvasFrame(File file) {

            super("Viewer");

            PlayerCanvasFrame playerCanvasFrame = this;

            // a key listener on this frame to stop/start, and quit
            KeyListener keyListener = new KeyListener() {

                @Override
                public void keyPressed(KeyEvent ev) {

                    log("Key event " + ev);

                    int keyCode = ev.getKeyCode();

                    if (keyCode == KeyEvent.VK_SPACE) {
                        // stop or start the player
                        togglePlayer();
                    } else if (keyCode == KeyEvent.VK_X || keyCode == KeyEvent.VK_Q || keyCode == KeyEvent.VK_ESCAPE) {
                        quit();
                    }

                }

                @Override
                public void keyReleased(KeyEvent ev) {}

                @Override
                public void keyTyped(KeyEvent ev) {}

            };

            // add the keyListener to the frame. Mouse events turn it off for some reason, hope was having it like this we could add it back
            playerCanvasFrame.addKeyListener(keyListener);

            // I've read the mouse listener goes on the canvas
            getCanvas().addMouseListener(new MouseListener() {

            // But we also get mouse events and loss of key events after a click with it on the frame?
            //addMouseListener(new MouseListener() {

                @Override
                public void mousePressed(MouseEvent ev) {
                    log("Mouse event " + ev);
                }

                @Override
                public void mouseReleased(MouseEvent ev) {
                    log("Mouse event " + ev);
                }

                @Override
                public void mouseClicked(MouseEvent ev) {
                    log("Mouse event " + ev);
                    // stop or start the player
                    togglePlayer();
                    // this fires last (after the two above), after which keyListener gets no more events. Try adding keyListener back?
                    playerCanvasFrame.addKeyListener(keyListener);
                }

                @Override
                public void mouseEntered(MouseEvent ev) {
                    // this does not kill the keyListener...
                    log("Mouse event " + ev);
                }

                @Override
                public void mouseExited(MouseEvent ev) {
                    // this does not kill the keyListener...
                    log("Mouse event " + ev);
                }

            });

            // set up and show frame
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            setFocusable(true);
            setVisible(true);

            // load the file!
            loadFile(file);
        }

        public FrameGrabber getFrameGrabber() {
            return _grabber;
        }

        public void togglePlayer() {
            if (_playerThread == null || !_playerThread.getRunning()) {
                _playerThread = new PlayerThread(this);
            } else {
                stopPlayer();
            }
        }

        public void stopPlayer() {
            if (_playerThread != null) {
                _playerThread.interrupt();
                _playerThread = null;
            }
        }

        public void quit() {
            stopPlayer();
            if (_grabber != null)
                try {
                    _grabber.stop();
                } catch (org.bytedeco.javacv.FFmpegFrameGrabber.Exception e) {
                    log("Error stopping grabber : " + e.getMessage());
                }
            // dispose the frame
            dispose();
            // kill process
            System.exit(0);
        }

        public void updateImage(long sleepTime) {
            if (_file != null) {
                try {
                    // set title to the name
                    String title = _file.getName();
                    // if we have an open video reader
                    if (_grabber != null) {
                        // if already opened (we want to reuse it)
                        if (_grabber.hasVideo()) {
                            // grab the image frame (no audio)
                            Frame frame = _grabber.grabImage();
                            // if we got a frame
                            if (frame == null) {
                                // stop any player
                                stopPlayer();
                            } else {
                                // show the frame, if there was 0 or some sleep (skip when negative)
                                if (sleepTime >= 0)
                                    showImage(frame);
                            }
                        }
                        // add the frame to the title for the video
                        title += " : " + _grabber.getFrameNumber();
                    }
                    // update title
                    setTitle(title);
                } catch (java.lang.Exception ex) {
                    log("Failed to update image : " + ex.getMessage());
                }
            }
        }

        // overload to the above with no sleep
        public void updateImage() {
            updateImage(0);
        }

        // load file, create and start a grabber for it, show the first frame
        public void loadFile(File file) {
            log("Creating grabber for " + file);
            // retain file
            _file = file;
            // open a new grabber for this file
            _grabber = new FFmpegFrameGrabber(_file);
            try {
                log("Starting grabber...");
                // start the grabber!
                _grabber.start();
                // set the image size (just once)
                setSize(_grabber.getImageWidth(), _grabber.getImageHeight());
            } catch (org.bytedeco.javacv.FFmpegFrameGrabber.Exception e) {
                log("Error starting grabber : " + e.getMessage());
            }
            // update the image
            updateImage();
        }
    }

    // very simple logging!
    public static void log(Object message) {
        System.out.println(message);
    }

    public static void main(String[] args) {
        log("Starting viewer...");
        // Schedule a job for the event-dispatching thread creating and showing this application's GUI.
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                log("Creating frame...");
                // full path the video file we want to watch
                File file = new File("/path/to/movie.mp4");
                // Create our player frame, it will set itself up
                    new PlayerCanvasFrame(file);
            }
        });
    }

}

Many thanks!

gareth-edwards commented 8 months ago

I've gotten mouse and key events to play together by adding this after the getCanvas().addMouseListener(new MouseListener() { ... });

            // this gives the focus back to the frame when the mouse events lose it so that key events are available again
            getCanvas().addFocusListener(new FocusListener() {

                @Override
                public void focusGained(FocusEvent arg0) {
                    // request focus back on the frame to restore key listener
                    playerCanvasFrame.requestFocus();
                }

                @Override
                public void focusLost(FocusEvent arg0) {}

            });

But I'm not sure if this the intended/best solution!