javacv icon indicating copy to clipboard operation
javacv copied to clipboard

Mouse events kill key events in CanvasFrame

Open gareth-edwards opened this issue 9 months ago • 1 comments

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 avatar Nov 07 '23 07:11 gareth-edwards

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!

gareth-edwards avatar Nov 08 '23 12:11 gareth-edwards