diff --git a/org.monte.demo.aviwriter/src/main/java/org.monte.demo.aviwriter/org/monte/demo/aviwriter/Main.java b/org.monte.demo.aviwriter/src/main/java/org.monte.demo.aviwriter/org/monte/demo/aviwriter/Main.java index 21f32d6..2974112 100755 --- a/org.monte.demo.aviwriter/src/main/java/org.monte.demo.aviwriter/org/monte/demo/aviwriter/Main.java +++ b/org.monte.demo.aviwriter/src/main/java/org.monte.demo.aviwriter/org/monte/demo/aviwriter/Main.java @@ -18,7 +18,7 @@ import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Stroke; -import java.awt.geom.Line2D; +import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.IndexColorModel; import java.io.File; @@ -84,18 +84,16 @@ private static void drawAnimationFrame(BufferedImage img, Graphics2D g, double s g.drawString("Frame " + (frameIndex + 1) + " of " + frameCount, 473, 24); } - private static void drawClock(Graphics2D g, int cx, int cy, int radius, double seconds) { + private static void drawClock(Graphics2D g, int cx, int cy, int radius, double timeInSeconds) { g.setPaint(Color.WHITE); g.fillOval(cx - radius, cy - radius, radius * 2, radius * 2); - - double minutes = seconds / 60.0; - double hours = minutes / 60.0; - drawClockHand(g, cx, cy, -10, radius / 2, new BasicStroke(20, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL), Color.BLACK, (hours) * (Math.PI * 2.0 / 12.0)); - drawClockHand(g, cx, cy, -10, radius - 20, new BasicStroke(20, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL), new Color(0x1a1a1a), (minutes) * (Math.PI * 2.0 / 60.0)); - drawClockHand(g, cx, cy, -64, radius - 1, new BasicStroke(6, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL), Color.RED, (seconds) * (Math.PI * 2.0 / +60.0)); - drawClockHand(g, cx, cy, -64, radius - 1, new BasicStroke(20, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL, 4f, new float[]{20f, radius * 2}, 0f), Color.RED, (seconds) * (Math.PI * 2.0 / +60.0)); - + double timeInMinutes = timeInSeconds / 60.0; + double timeInHours = timeInMinutes / 60.0; + drawClockHand(g, cx, cy, -10, radius / 2, new BasicStroke(20, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL), Color.BLACK, (timeInHours) * (Math.PI * 2.0 / 12.0)); + drawClockHand(g, cx, cy, -10, radius - 20, new BasicStroke(20, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL), new Color(0x1a1a1a), (timeInMinutes) * (Math.PI * 2.0 / 60.0)); + drawClockHand(g, cx, cy, -64, radius - 1, new BasicStroke(6, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL), Color.RED, (timeInSeconds) * (Math.PI * 2.0 / +60.0)); + drawClockHand(g, cx, cy, -64, -24, new BasicStroke(20, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL), Color.RED, (timeInSeconds) * (Math.PI * 2.0 / +60.0)); // Draw plug int plugRadius = 12; g.setPaint(Color.WHITE); @@ -105,13 +103,15 @@ private static void drawClock(Graphics2D g, int cx, int cy, int radius, double s g.drawOval(cx - plugRadius, cy - plugRadius, plugRadius * 2, plugRadius * 2); } - private static void drawClockHand(Graphics2D g, int cx, int cy, int radius1, int radius2, Stroke stroke, Color color, double angle) { - angle = angle % (Math.PI * 2); - double sin = Math.sin(angle); - double cos = Math.cos(angle); - g.setPaint(color); + private static void drawClockHand(Graphics2D g, int cx, int cy, int radius1, int radius2, Stroke stroke, Color color, double theta) { + AffineTransform tx = new AffineTransform(); + tx.setToRotation(theta % (Math.PI * 2), cx, cy); + g.setTransform(tx); + g.setColor(color); g.setStroke(stroke); - g.draw(new Line2D.Double(cx + radius1 * sin, cy - radius1 * cos, cx + radius2 * sin, cy - radius2 * cos)); + g.drawLine(cx, cy - radius1, cx, cy - radius2); + tx.setToIdentity(); + g.setTransform(tx); } /** diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/Main.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/Main.java index e04ff5e..ce57148 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/Main.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/Main.java @@ -27,9 +27,13 @@ public void start(Stage stage) throws Exception { MainWindowController controller = loader.getController(); - stage.titleProperty().bind(controller.fileProperty().map(f -> - labels.getString("application.name") + (f == null ? "" : ": " + f.getName()) - )); + controller.fileProperty().addListener((o, oldv, f) -> + stage.setTitle((f == null ? labels.getString("file.noFile") : f.getName())) + ); + stage.setOnHidden(event -> controller.close(null)); + + + stage.setTitle(labels.getString("file.noFile")); Scene scene = new Scene(root); scene.getStylesheets().add(getClass().getResource("controls.css").toString()); stage.setScene(scene); diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/MainWindowController.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/MainWindowController.java index 37b5609..5e85539 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/MainWindowController.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/MainWindowController.java @@ -16,6 +16,7 @@ import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.Label; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; @@ -49,12 +50,10 @@ enum Mode { @FXML // URL location of the FXML file that was given to the FXMLLoader private URL location; - @FXML - private StackPane stackPane; // Value injected by FXMLLoader - - @FXML // fx:id="rootPane" private BorderPane rootPane; // Value injected by FXMLLoader + @FXML // fx:id="rootPane" + private StackPane stackPane; // Value injected by FXMLLoader private FileChooser fileChooser; @FXML @@ -71,6 +70,12 @@ void about(ActionEvent event) { @FXML void close(ActionEvent event) { + MediaPlayerInterface p = player.get(); + if (p != null) { + p.dispose(); + player.set(null); + stackPane.getChildren().clear(); + } getStage().close(); } @@ -87,6 +92,51 @@ private Stage getStage() { return scene == null ? null : (Stage) scene.getWindow(); } + @FXML + void zoomIn(ActionEvent event) { + zoomTo((Math.round(getZoomPower() + 1))); + } + + @FXML + void zoomOut(ActionEvent event) { + zoomTo((Math.round(getZoomPower() - 1))); + } + + @FXML + void zoomToActualSize(ActionEvent event) { + zoomTo(0); + } + + void zoomTo(double power) { + MediaInterface media = getPlayer() instanceof MediaPlayerInterface p ? p.getMedia() : null; + if (media == null) { + return; + } + double factor = Math.pow(2, power); + stackPane.setPrefWidth(media.getWidth() * factor); + stackPane.setPrefHeight(media.getHeight() * factor); + getStage().sizeToScene(); + } + + private MediaPlayerInterface getPlayer() { + return player.get(); + } + + private double getZoomPower() { + MediaInterface media = getPlayer() instanceof MediaPlayerInterface p ? p.getMedia() : null; + if (media == null) { + return 1; + } + double factor = stackPane.getWidth() / media.getWidth(); + double power = Math.log(factor) / Math.log(2); + return power; + } + + @FXML + void zoomToFit(ActionEvent event) { + zoomTo(getZoomPower()); + } + @FXML // This method is called by the FXMLLoader when initialization is complete void initialize() { @@ -148,13 +198,14 @@ private void createMoviePlayer(int retries) { }; if (player == null || player.getError() != null) { - retryCreateMoviePlayer(retries); + retryCreateMoviePlayer(retries, player == null ? null : player.getError()); } else { - player.setOnError(() -> retryCreateMoviePlayer(retries)); + this.player.set(player); + player.setOnError(() -> retryCreateMoviePlayer(retries, player.getError())); } } - private void retryCreateMoviePlayer(int retries) { + private void retryCreateMoviePlayer(int retries, Throwable error) { Mode[] values = Mode.values(); if (retries < values.length) { mode.set(values[(mode.get().ordinal() + 1) % values.length]); @@ -162,7 +213,8 @@ private void retryCreateMoviePlayer(int retries) { } else { ObservableList c = stackPane.getChildren(); c.clear(); - c.add(new Label(resources.getString("error.creatingPlayer"))); + c.add(new Label(resources.getString("error.creatingPlayer") + "\n" + error)); + error.printStackTrace(); } } @@ -174,9 +226,9 @@ private MediaPlayerInterface createMonteMediaPlayer() { } MonteMedia movie = new MonteMedia(mediaFile); MonteMediaPlayer player = new MonteMediaPlayer(movie); - MonteMediaView monteMediaView = MonteMediaView.newVideoView(); + MonteMediaView monteMediaView = MonteMediaView.newMonteMediaView(); monteMediaView.setMedia(movie); - ControlsController playerController = createPlayerController(); + PlayerControlsController playerController = createPlayerController(); playerController.setPlayer(player); showPlayer(monteMediaView.getRoot(), player, movie, playerController); return player; @@ -201,15 +253,22 @@ private MediaPlayerInterface createFXMoviePlayer() { mediaPlayer.dispose(); return null; } - ControlsController playerController = createPlayerController(); + PlayerControlsController playerController = createPlayerController(); FXMediaPlayer p = new FXMediaPlayer(mediaPlayer); player.set(p); playerController.setPlayer(p); mediaPlayer.setAutoPlay(true); mediaView = new MediaView(mediaPlayer); + + mediaView.fitWidthProperty().bind(stackPane.widthProperty()); + mediaView.fitHeightProperty().bind(stackPane.heightProperty()); + mediaView.setManaged(false); + + mediaView.setPreserveRatio(false); + + showPlayer(mediaView, p, new FXMedia(media), playerController); - //mediaView.setOnError(t -> leftStatusLabel.setText(resources.getString("error") + t.toString())); return p; } catch (Exception mediaException) { @@ -218,19 +277,21 @@ private MediaPlayerInterface createFXMoviePlayer() { return null; } - - private void showPlayer(Node mediaView, MediaPlayerInterface mediaPlayer, MediaInterface media, ControlsController playerController) { + private void showPlayer(Node mediaView, MediaPlayerInterface mediaPlayer, MediaInterface media, PlayerControlsController playerController) { mediaPlayer.setOnReady(() -> { + stackPane.setPrefWidth(media.getWidth()); + stackPane.setPrefHeight(media.getHeight()); sizeStageToScene(); }); stackPane.getChildren().addAll(mediaView, playerController.getRoot()); } - private ControlsController createPlayerController() { + private PlayerControlsController createPlayerController() { try { - FXMLLoader loader = new FXMLLoader(getClass().getResource("Controls.fxml")); + FXMLLoader loader = new FXMLLoader(getClass().getResource("PlayerControls.fxml")); ResourceBundle labels = ResourceBundle.getBundle("org.monte.demo.javafx.movieplayer.Labels"); + loader.setRoot(new AnchorPane()); loader.setResources(labels); loader.load(); return loader.getController(); diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/ControlsController.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/PlayerControlsController.java similarity index 79% rename from org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/ControlsController.java rename to org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/PlayerControlsController.java index b564f9f..7a1bfe2 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/ControlsController.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/PlayerControlsController.java @@ -1,5 +1,5 @@ /** - * Sample Skeleton for 'Controls.fxml' Controller Class + * Sample Skeleton for 'PlayerControls.fxml' Controller Class */ package org.monte.demo.javafx.movieplayer; @@ -13,7 +13,6 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.Node; -import javafx.scene.Parent; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Slider; @@ -21,7 +20,6 @@ import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; -import javafx.scene.layout.Pane; import javafx.scene.media.MediaPlayer; import javafx.util.Duration; import org.monte.demo.javafx.movieplayer.model.MediaPlayerInterface; @@ -33,7 +31,7 @@ import java.util.Timer; import java.util.TimerTask; -public class ControlsController { +public class PlayerControlsController extends AnchorPane { @FXML // ResourceBundle that was given to the FXMLLoader private ResourceBundle resources; @@ -74,19 +72,31 @@ public class ControlsController { @FXML - void goBackward(ActionEvent event) { + void seekBackward(ActionEvent event) { MediaPlayerInterface p = getPlayer(); if (p == null) return; - - p.seek(p.getCurrentTime().subtract( - p.getStatus() == MediaPlayer.Status.PLAYING ? Duration.minutes(1) : Duration.seconds(1))); + Duration seekTime; + Duration currentTime = p.getCurrentTime(); + if (p.getStatus() == MediaPlayer.Status.PLAYING) { + seekTime = currentTime.subtract(Duration.minutes(1)); + } else { + seekTime = p.getFrameBefore(currentTime); + } + p.seek(seekTime); } @FXML - void goForward(ActionEvent event) { + void seekForward(ActionEvent event) { MediaPlayerInterface p = getPlayer(); if (p == null) return; - p.seek(p.getCurrentTime().add(p.getStatus() == MediaPlayer.Status.PLAYING ? Duration.minutes(1) : Duration.seconds(1))); + Duration seekTime; + Duration currentTime = p.getCurrentTime(); + if (p.getStatus() == MediaPlayer.Status.PLAYING) { + seekTime = currentTime.add(Duration.minutes(1)); + } else { + seekTime = p.getFrameAfter(currentTime); + } + p.seek(seekTime); } public MediaPlayerInterface getPlayer() { @@ -122,16 +132,16 @@ void togglePlayPause(ActionEvent event) { @FXML // This method is called by the FXMLLoader when initialization is complete void initialize() { - assert backwardButton != null : "fx:id=\"backwardButton\" was not injected: check your FXML file 'Controls.fxml'."; - assert controllerPane != null : "fx:id=\"controllerPane\" was not injected: check your FXML file 'Controls.fxml'."; - assert durationLabel != null : "fx:id=\"durationLabel\" was not injected: check your FXML file 'Controls.fxml'."; - assert forwardButton != null : "fx:id=\"forwardButton\" was not injected: check your FXML file 'Controls.fxml'."; - assert muteButton != null : "fx:id=\"muteButton\" was not injected: check your FXML file 'Controls.fxml'."; - assert playButton != null : "fx:id=\"playButton\" was not injected: check your FXML file 'Controls.fxml'."; - assert rootPane != null : "fx:id=\"rootPane\" was not injected: check your FXML file 'Controls.fxml'."; - assert timeLabel != null : "fx:id=\"timeLabel\" was not injected: check your FXML file 'Controls.fxml'."; - assert timeSlider != null : "fx:id=\"timeSlider\" was not injected: check your FXML file 'Controls.fxml'."; - assert volumeSlider != null : "fx:id=\"volumeSlider\" was not injected: check your FXML file 'Controls.fxml'."; + assert backwardButton != null : "fx:id=\"backwardButton\" was not injected: check your FXML file 'PlayerControls.fxml'."; + assert controllerPane != null : "fx:id=\"controllerPane\" was not injected: check your FXML file 'PlayerControls.fxml'."; + assert durationLabel != null : "fx:id=\"durationLabel\" was not injected: check your FXML file 'PlayerControls.fxml'."; + assert forwardButton != null : "fx:id=\"forwardButton\" was not injected: check your FXML file 'PlayerControls.fxml'."; + assert muteButton != null : "fx:id=\"muteButton\" was not injected: check your FXML file 'PlayerControls.fxml'."; + assert playButton != null : "fx:id=\"playButton\" was not injected: check your FXML file 'PlayerControls.fxml'."; + assert rootPane != null : "fx:id=\"rootPane\" was not injected: check your FXML file 'PlayerControls.fxml'."; + assert timeLabel != null : "fx:id=\"timeLabel\" was not injected: check your FXML file 'PlayerControls.fxml'."; + assert timeSlider != null : "fx:id=\"timeSlider\" was not injected: check your FXML file 'PlayerControls.fxml'."; + assert volumeSlider != null : "fx:id=\"volumeSlider\" was not injected: check your FXML file 'PlayerControls.fxml'."; ControllerPaneMouseDraggedHandler dh = new ControllerPaneMouseDraggedHandler(this); @@ -262,7 +272,7 @@ private String toTotalDurationString(Duration duration) { } private class ControllerPaneVisibleHandler { - private Timer timer = new Timer(); + private Timer timer; private class ShowHideTask extends TimerTask { private final boolean show; @@ -282,7 +292,11 @@ public boolean cancel() { public void run() { Platform.runLater(() -> { if (!cancelled) { - controllerPane.setVisible(show); + if (show) { + show(); + } else { + controllerPane.setVisible(show); + } } }); } @@ -290,12 +304,20 @@ public void run() { private ShowHideTask currentTask; - public ControllerPaneVisibleHandler(ControlsController controlsController) { + public ControllerPaneVisibleHandler(PlayerControlsController controlsController) { rootPane.setOnMouseEntered(this::rootPaneEntered); rootPane.setOnMouseExited(this::rootPaneExited); controllerPane.setOnMouseEntered(this::controllerPaneEntered); rootPane.setOnMouseMoved(this::rootPaneMouseMoved); controllerPane.setOnMouseMoved(this::controllerPaneMouseMoved); + + rootPane.sceneProperty().addListener((o, oldv, newv) -> { + if (timer != null) { + timer.cancel(); + timer = null; + } + }); + } private void controllerPaneMouseMoved(MouseEvent mouseEvent) { @@ -304,10 +326,20 @@ private void controllerPaneMouseMoved(MouseEvent mouseEvent) { private void rootPaneMouseMoved(MouseEvent mouseEvent) { mouseEvent.consume(); - controllerPane.setVisible(true); + show(); schedule(false); } + private void show() { + var b = controllerPane.getLayoutBounds(); + double rw = rootPane.getWidth(); + double rh = rootPane.getHeight(); + if (b.getMinX() < 0 || b.getMinY() < 0 || b.getMaxX() > rh || b.getMaxY() > rw) { + controllerPane.relocate((rw - b.getWidth()) * 0.5, (rh - b.getHeight()) * 0.75); + } + controllerPane.setVisible(true); + } + private void cancelScheduled() { if (currentTask != null) { currentTask.cancel(); @@ -317,7 +349,11 @@ private void cancelScheduled() { private void schedule(boolean show) { cancelScheduled(); - timer.schedule(currentTask = new ShowHideTask(show), 2000); + getOrCreateTimer().schedule(currentTask = new ShowHideTask(show), 2000); + } + + private Timer getOrCreateTimer() { + return timer == null ? timer = new Timer(PlayerControlsController.this + "-timer") : timer; } private void controllerPaneEntered(MouseEvent mouseEvent) { @@ -333,7 +369,7 @@ private void rootPaneExited(MouseEvent mouseEvent) { private void rootPaneEntered(MouseEvent mouseEvent) { mouseEvent.consume(); cancelScheduled(); - controllerPane.setVisible(true); + show(); } } @@ -341,7 +377,7 @@ private class ControllerPaneMouseDraggedHandler { private double prevMouseX, prevMouseY; - public ControllerPaneMouseDraggedHandler(ControlsController controlsController) { + public ControllerPaneMouseDraggedHandler(PlayerControlsController controlsController) { controllerPane.setOnMouseDragged(this::mouseDragged); controllerPane.setOnMousePressed(this::mousePressed); @@ -359,19 +395,16 @@ private void mouseDragged(MouseEvent event) { double dx = sceneX - prevMouseX; double sceneY = event.getSceneY(); double dy = sceneY - prevMouseY; - Parent parent = controllerPane.getParent(); double width = controllerPane.getWidth(); double height = controllerPane.getHeight(); - double parentWidth, parentHeight; - if (parent instanceof Pane p) { - parentWidth = Math.max(p.getWidth(), width); - parentHeight = Math.max(p.getHeight(), height); - } else { - parentHeight = height; - parentWidth = width; - } - controllerPane.setLayoutX(Math.clamp(controllerPane.getLayoutX() + dx, 0, parentWidth - width)); - controllerPane.setLayoutY(Math.clamp(controllerPane.getLayoutY() + dy, 0, parentHeight - height)); + double parentWidth = rootPane.getWidth(); + double parentHeight = rootPane.getHeight(); + + int minimumAmountVisible = 10; + double newX = Math.clamp(controllerPane.getLayoutX() + dx, 0 - width + minimumAmountVisible, parentWidth - minimumAmountVisible); + double newY = Math.clamp(controllerPane.getLayoutY() + dy, 0 - height + minimumAmountVisible, parentHeight - minimumAmountVisible); + AnchorPane.setRightAnchor(controllerPane, rootPane.getWidth() - width - newX); + AnchorPane.setBottomAnchor(controllerPane, rootPane.getHeight() - height - newY); prevMouseX = sceneX; prevMouseY = sceneY; } diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/fxplayer/FXMediaPlayer.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/fxplayer/FXMediaPlayer.java index c9ed733..3367e15 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/fxplayer/FXMediaPlayer.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/fxplayer/FXMediaPlayer.java @@ -194,6 +194,16 @@ public void stop() { player.stop(); } + @Override + public Duration getFrameAfter(Duration timestamp) { + return timestamp.add(Duration.seconds(10)); + } + + @Override + public Duration getFrameBefore(Duration timestamp) { + return timestamp.add(Duration.seconds(10)); + } + @Override public ObjectProperty onReadyProperty() { return player.onReadyProperty(); diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/model/MediaPlayerInterface.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/model/MediaPlayerInterface.java index 549601c..1e10ad1 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/model/MediaPlayerInterface.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/model/MediaPlayerInterface.java @@ -306,5 +306,7 @@ default void setVolume(double newValue) { void stop(); + Duration getFrameAfter(Duration timestamp); + Duration getFrameBefore(Duration timestamp); } diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteAudioTrack.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteAudioTrack.java index af3c8a0..da0154d 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteAudioTrack.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteAudioTrack.java @@ -5,13 +5,117 @@ package org.monte.demo.javafx.movieplayer.monteplayer; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.scene.image.WritableImage; import org.monte.demo.javafx.movieplayer.model.AbstractAudioTrack; +import org.monte.media.av.Buffer; +import org.monte.media.av.Codec; +import org.monte.media.av.Format; +import org.monte.media.math.Rational; +import javax.sound.sampled.SourceDataLine; import java.util.Locale; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; -public class MonteAudioTrack extends AbstractAudioTrack { +public class MonteAudioTrack extends AbstractAudioTrack implements MonteTrackInterface { + protected long renderTimeValidUntilNanoTime; + private SourceDataLine sourceDataLine; + /** + * The dispatcher. + */ + protected ExecutorService dispatcher = Executors.newSingleThreadExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, MonteAudioTrack.this + "-worker"); + } + }); public MonteAudioTrack(Locale locale, long trackId, String name, Map metadata) { super(locale, trackId, name, metadata); } + + protected Buffer inBuffer = new Buffer(); + protected Buffer outBufferA = new Buffer(); + protected Buffer outBufferB = new Buffer(); + private Codec codec; + protected final ReadOnlyObjectWrapper videoImage = new ReadOnlyObjectWrapper<>(); + private Rational renderedStartTime = Rational.ZERO; + private Rational renderedEndTime = Rational.ZERO; + private Format format; + + + public Codec getCodec() { + return codec; + } + + @Override + public Buffer getInBuffer() { + return inBuffer; + } + + @Override + public Buffer getOutBufferA() { + return outBufferA; + } + + @Override + public Buffer getOutBufferB() { + return outBufferB; + } + + @Override + public Format getFormat() { + return format; + } + + public void setCodec(Codec codec) { + this.codec = codec; + } + + public void setFormat(Format format) { + this.format = format; + } + + public void setSourceDataLine(SourceDataLine sourceDataLine) { + this.sourceDataLine = sourceDataLine; + } + + + @Override + public Buffer swapOutBuffers() { + var swap = outBufferA; + outBufferA = outBufferB; + outBufferB = swap; + return outBufferA; + } + + public SourceDataLine getSourceDataLine() { + return sourceDataLine; + } + + public Rational getRenderedEndTime() { + return renderedEndTime; + } + + public void setRenderedEndTime(Rational seconds) { + this.renderedEndTime = seconds; + } + + public Rational getRenderedStartTime() { + return renderedStartTime; + } + + public void setRenderedStartTime(Rational seconds) { + this.renderedStartTime = seconds; + } + + @Override + public void dispose() { + if (sourceDataLine != null) { + sourceDataLine.close(); + sourceDataLine = null; + } + } } diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMedia.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMedia.java index e90c754..58bfbc6 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMedia.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMedia.java @@ -5,16 +5,14 @@ package org.monte.demo.javafx.movieplayer.monteplayer; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.property.ReadOnlyProperty; -import javafx.scene.image.WritableImage; import javafx.util.Duration; import org.monte.demo.javafx.movieplayer.model.AbstractMedia; +import org.monte.media.av.Format; import java.io.File; public class MonteMedia extends AbstractMedia { - protected final ReadOnlyObjectWrapper videoImage = new ReadOnlyObjectWrapper<>(); + private Format format; public MonteMedia(String source) { super(source); @@ -24,6 +22,14 @@ public MonteMedia(File source) { super(source.toURI().toString()); } + public void dispose() { + for (var tr : tracks) { + if (tr instanceof MonteTrackInterface mtr) { + mtr.dispose(); + } + } + } + /** * Package private method for {@link PlayerEngine} * @@ -42,6 +48,10 @@ void setDuration(Duration newValue) { duration.set(newValue); } + public void setFormat(Format fileFormat) { + this.format = fileFormat; + } + void setWidth(int newValue) { width.set(newValue); } @@ -50,16 +60,4 @@ void setHeight(int newValue) { height.set(newValue); } - public WritableImage getVideoImage() { - return videoImage.get(); - } - - public ReadOnlyProperty videoImageProperty() { - return videoImage.getReadOnlyProperty(); - } - - void setVideoImage(WritableImage newValue) { - videoImage.set(newValue); - } - } diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMediaPlayer.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMediaPlayer.java index ccfc6e0..2e24d78 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMediaPlayer.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMediaPlayer.java @@ -31,6 +31,7 @@ public MonteMedia getMedia() { @Override public void dispose() { engine.close(); + media.dispose(); } @Override @@ -53,6 +54,16 @@ public void stop() { engine.stop(); } + @Override + public Duration getFrameAfter(Duration timestamp) { + return Duration.seconds(engine.getFrameAfter(Rational.valueOf(timestamp.toSeconds())).doubleValue()); + } + + @Override + public Duration getFrameBefore(Duration timestamp) { + return Duration.seconds(engine.getFrameBefore(Rational.valueOf(timestamp.toSeconds())).doubleValue()); + } + /** * Package private method for {@link PlayerEngine} * diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMediaView.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMediaView.java index 713999a..73cf92e 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMediaView.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteMediaView.java @@ -4,14 +4,30 @@ package org.monte.demo.javafx.movieplayer.monteplayer; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.ObjectBinding; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; +import javafx.scene.Group; +import javafx.scene.Node; import javafx.scene.image.ImageView; +import javafx.scene.layout.Pane; +import javafx.scene.transform.Affine; +import javafx.scene.transform.MatrixType; +import javafx.scene.transform.Scale; +import org.monte.demo.javafx.movieplayer.model.TrackInterface; +import org.monte.media.av.Format; +import org.monte.media.av.codec.video.AffineTransform; +import org.monte.media.av.codec.video.VideoFormatKeys; import java.io.IOException; import java.net.URL; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.ResourceBundle; public class MonteMediaView { @@ -22,26 +38,89 @@ public class MonteMediaView { @FXML // URL location of the FXML file that was given to the FXMLLoader private URL location; - @FXML // fx:id="imageView" - private ImageView imageView; // Value injected by FXMLLoader + @FXML // fx:id="rootPane" + private Pane rootPane; // Value injected by FXMLLoader + @FXML // fx:id="group" + private Group group; // Value injected by FXMLLoader + + private ObjectBinding scaleGroupBinding; @FXML // This method is called by the FXMLLoader when initialization is complete void initialize() { - assert imageView != null : "fx:id=\"imageView\" was not injected: check your FXML file 'MonteMediaView.fxml'."; + assert rootPane != null : "fx:id=\"rootPane\" was not injected: check your FXML file 'MonteMediaView.fxml'."; + assert group != null : "fx:id=\"rootPane\" was not injected: check your FXML file 'MonteMediaView.fxml'."; + + group.getTransforms().add(new Scale(1, 1, 0, 0)); media.addListener((o, oldv, newv) -> { if (oldv != null) { - imageView.imageProperty().unbind(); + ObservableList tracks = oldv.getTracks(); + tracks.removeListener(trackHandler); + group.getChildren().clear(); + if (scaleGroupBinding != null) { + scaleGroupBinding.dispose(); + scaleGroupBinding = null; + } } if (newv != null) { - imageView.imageProperty().bind(newv.videoImageProperty()); - imageView.fitWidthProperty().bind(newv.widthProperty()); - imageView.fitHeightProperty().bind(newv.heightProperty()); + ObservableList tracks = newv.getTracks(); + tracks.addListener(trackHandler); + for (TrackInterface tr : tracks) { + addTrack(tr); + } + scaleGroupBinding = Bindings.createObjectBinding(() -> { + return new Scale(rootPane.getWidth() / newv.getWidth(), rootPane.getHeight() / newv.getHeight(), 0, 0); + }, rootPane.layoutBoundsProperty(), newv.widthProperty(), newv.heightProperty() + + ); + scaleGroupBinding.addListener((oo, oldvv, newvv) -> { + group.getTransforms().set(0, newvv); + }); } }); } + private void addTrack(TrackInterface tr) { + if (tr instanceof MonteVideoTrack vt) { + ImageView imageView = new ImageView(); + imageView.imageProperty().bind(vt.videoImageProperty()); + Format format = vt.getFormat(); + if (format != null) { + AffineTransform transform = format.getOrDefault(VideoFormatKeys.TransformKey); + if (!transform.isIdentity()) { + Affine affine = new Affine(transform.getFlatMatrix(), MatrixType.MT_2D_2x3, 0); + imageView.getTransforms().add(affine); + } + } + group.getChildren().add(imageView); + trackMap.put(tr, imageView); + } + } + + private final ListChangeListener trackHandler = new ListChangeListener() { + @Override + public void onChanged(Change c) { + while (c.next()) { + for (TrackInterface remitem : c.getRemoved()) { + removeTrack(remitem); + } + for (TrackInterface additem : c.getAddedSubList()) { + addTrack(additem); + } + } + } + }; + private Map trackMap = new LinkedHashMap<>(); + + private void removeTrack(TrackInterface remitem) { + Node remove = trackMap.remove(remitem); + group.getChildren().remove(remove); + if (remove instanceof ImageView imageView) { + imageView.imageProperty().unbind(); + } + } + public MonteMedia getMedia() { return media.get(); } @@ -54,11 +133,11 @@ public ObjectProperty mediaProperty() { return media; } - public ImageView getRoot() { - return imageView; + public Pane getRoot() { + return rootPane; } - public static MonteMediaView newVideoView() { + public static MonteMediaView newMonteMediaView() { FXMLLoader loader = new FXMLLoader(MonteMediaView.class.getResource("MonteMediaView.fxml")); ResourceBundle labels = ResourceBundle.getBundle("org.monte.demo.javafx.movieplayer.Labels"); loader.setResources(labels); diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteTrackInterface.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteTrackInterface.java new file mode 100644 index 0000000..c462ddf --- /dev/null +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteTrackInterface.java @@ -0,0 +1,43 @@ +/* + * @(#)MonteTrackInterface.java + * Copyright © 2024 Werner Randelshofer, Switzerland. MIT License. + */ + +package org.monte.demo.javafx.movieplayer.monteplayer; + +import org.monte.demo.javafx.movieplayer.model.TrackInterface; +import org.monte.media.av.Buffer; +import org.monte.media.av.Codec; +import org.monte.media.av.Format; +import org.monte.media.math.Rational; + +public interface MonteTrackInterface extends TrackInterface { + Buffer getInBuffer(); + + Buffer getOutBufferA(); + + Buffer getOutBufferB(); + + Format getFormat(); + + /** + * Swaps the output buffers and returns outBufferA. + * + * @return outBufferA + */ + Buffer swapOutBuffers(); + + Codec getCodec(); + + void setCodec(Codec newValue); + + public Rational getRenderedEndTime(); + + public void setRenderedEndTime(Rational seconds); + + public Rational getRenderedStartTime(); + + public void setRenderedStartTime(Rational seconds); + + void dispose(); +} diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteTrack.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteUnsupportedTrack.java similarity index 59% rename from org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteTrack.java rename to org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteUnsupportedTrack.java index cb9693b..8ed9097 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteTrack.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteUnsupportedTrack.java @@ -1,5 +1,5 @@ /* - * @(#)MonteTrack.java + * @(#)MonteUnsupportedTrack.java * Copyright © 2024 Werner Randelshofer, Switzerland. MIT License. */ @@ -10,8 +10,8 @@ import java.util.Locale; import java.util.Map; -public class MonteTrack extends AbstractTrack { - public MonteTrack(Locale locale, long trackId, String name, Map metadata) { +public class MonteUnsupportedTrack extends AbstractTrack { + public MonteUnsupportedTrack(Locale locale, long trackId, String name, Map metadata) { super(locale, trackId, name, metadata); } } diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteVideoTrack.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteVideoTrack.java index 3f8281a..175f4ed 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteVideoTrack.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/MonteVideoTrack.java @@ -11,33 +11,63 @@ import org.monte.demo.javafx.movieplayer.model.AbstractVideoTrack; import org.monte.media.av.Buffer; import org.monte.media.av.Codec; +import org.monte.media.av.Format; import org.monte.media.math.Rational; import java.util.Locale; import java.util.Map; -public class MonteVideoTrack extends AbstractVideoTrack { +public class MonteVideoTrack extends AbstractVideoTrack implements MonteTrackInterface { protected Buffer inBuffer = new Buffer(); - protected Buffer[] outBuffer = {new Buffer(), new Buffer()}; - protected int outBufferIndex = 0; + protected Buffer outBufferA = new Buffer(); + protected Buffer outBufferB = new Buffer(); private Codec codec; protected final ReadOnlyObjectWrapper videoImage = new ReadOnlyObjectWrapper<>(); - private Rational currentStartTime = Rational.ZERO; - private Rational currentEndTime = Rational.ZERO; + private Rational renderedStartTIme = Rational.ZERO; + private Rational renderedEndTime = Rational.ZERO; + private Format format; public MonteVideoTrack(Locale locale, long trackId, String name, Map metadata) { super(locale, trackId, name, metadata); } - Codec getCodec() { + public Codec getCodec() { return codec; } - void setCodec(Codec newValue) { + @Override + public Buffer getInBuffer() { + return inBuffer; + } + + @Override + public Buffer getOutBufferA() { + return outBufferA; + } + + @Override + public Buffer getOutBufferB() { + return outBufferB; + } + + @Override + public Format getFormat() { + return format; + } + + + @Override + public void setCodec(Codec newValue) { codec = newValue; } + void setFormat(Format newValue) { + format = newValue; + + } + + void setWidth(int newValue) { width = newValue; } @@ -46,6 +76,14 @@ WritableImage getVideoImage() { return videoImage.get(); } + @Override + public Buffer swapOutBuffers() { + var swap = outBufferA; + outBufferA = outBufferB; + outBufferB = swap; + return outBufferA; + } + ReadOnlyProperty videoImageProperty() { return videoImage.getReadOnlyProperty(); } @@ -58,19 +96,24 @@ void setHeight(int newValue) { height = newValue; } - public Rational getCurrentEndTime() { - return currentEndTime; + public Rational getRenderedEndTime() { + return renderedEndTime; + } + + public void setRenderedEndTime(Rational seconds) { + this.renderedEndTime = seconds; } - public void setCurrentEndTime(Rational currentEndTime) { - this.currentEndTime = currentEndTime; + public Rational getRenderedStartTime() { + return renderedStartTIme; } - public Rational getCurrentStartTime() { - return currentStartTime; + public void setRenderedStartTime(Rational seconds) { + this.renderedStartTIme = seconds; } - public void setCurrentStartTime(Rational currentStartTime) { - this.currentStartTime = currentStartTime; + @Override + public void dispose() { + } } diff --git a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/PlayerEngine.java b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/PlayerEngine.java index 1fca033..0088fe4 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/PlayerEngine.java +++ b/org.monte.demo.javafx.movieplayer/src/main/java/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/monteplayer/PlayerEngine.java @@ -11,6 +11,7 @@ import javafx.scene.media.MediaPlayer; import javafx.util.Duration; import org.monte.demo.javafx.movieplayer.model.TrackInterface; +import org.monte.media.av.AbstractPlayer; import org.monte.media.av.Buffer; import org.monte.media.av.BufferFlag; import org.monte.media.av.Codec; @@ -19,13 +20,19 @@ import org.monte.media.av.FormatKeys; import org.monte.media.av.MovieReader; import org.monte.media.av.Registry; +import org.monte.media.av.codec.audio.AudioFormatKeys; import org.monte.media.av.codec.video.VideoFormatKeys; import org.monte.media.math.Rational; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.net.URI; +import java.nio.ByteOrder; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -33,16 +40,27 @@ import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; import static org.monte.media.av.FormatKeys.EncodingKey; +import static org.monte.media.av.FormatKeys.MIME_JAVA; import static org.monte.media.av.FormatKeys.MediaTypeKey; +import static org.monte.media.av.FormatKeys.MimeTypeKey; +import static org.monte.media.av.codec.audio.AudioFormatKeys.ENCODING_PCM_SIGNED; +import static org.monte.media.av.codec.audio.AudioFormatKeys.SignedKey; import static org.monte.media.av.codec.video.VideoFormatKeys.DataClassKey; -class PlayerEngine extends org.monte.media.av.AbstractPlayer { +class PlayerEngine extends AbstractPlayer { private final MonteMediaPlayer player; private final MonteMedia media; private MovieReader reader; - private volatile Rational targetTime; + /** + * Method {@link #seek(Rational)} sets the seek time to a non-null value. + *

+ * Methods {@link #doPrefetched()} and {@link #doStarted()} retrieve the value (using {@code seekTime.getAndSet(null)}). + * And seek to the desired time. + */ + private final AtomicReference seekTime = new AtomicReference<>(); private MonteVideoTrack vTrack; public PlayerEngine(MonteMediaPlayer player) { @@ -53,13 +71,14 @@ public PlayerEngine(MonteMediaPlayer player) { @Override protected void fireStateChanged(int oldState, int newState) { Platform.runLater(() -> { - var status = switch (newState) { - case REALIZED -> MediaPlayer.Status.READY; - case PREFETCHING, PREFETCHED -> MediaPlayer.Status.PAUSED; - case STARTED -> MediaPlayer.Status.PLAYING; - case CLOSED -> MediaPlayer.Status.DISPOSED; - default -> MediaPlayer.Status.UNKNOWN; - }; + player.setStatus( + switch (newState) { + case REALIZED -> MediaPlayer.Status.READY; + case PREFETCHING, PREFETCHED -> MediaPlayer.Status.PAUSED; + case STARTED -> MediaPlayer.Status.PLAYING; + case CLOSED -> MediaPlayer.Status.DISPOSED; + default -> MediaPlayer.Status.UNKNOWN; + }); }); } @@ -91,70 +110,40 @@ protected void doRealizing() throws Exception { reader = null; tmpReader.close(); } + reader = Registry.getInstance().getReader(new File(new URI(media.getSource()))); List tracks = new ArrayList<>(); - int width = 0, height = 0; + int mediaWidth = 0, mediaHeight = 0; + Format fileFormat = reader.getFileFormat(); for (int i = 0, n = reader.getTrackCount(); i < n; i++) { - Format fileFormat = reader.getFileFormat(); - Format format = reader.getFormat(i); + mediaWidth = (fileFormat.get(VideoFormatKeys.WidthKey, 0)); + mediaHeight = (fileFormat.get(VideoFormatKeys.HeightKey, 0)); + Format trackFormat = reader.getFormat(i); + Format format = trackFormat; final Map metadata = new LinkedHashMap<>(); format.getProperties().entrySet().iterator().forEachRemaining(e -> metadata.put(e.getKey().getName(), e.getValue())); tracks.add(switch (format.get(MediaTypeKey)) { case FormatKeys.MediaType.VIDEO -> { - //FIXME apply transform to video - //AffineTransform transform = format.getOrDefault(VideoFormatKeys.TransformKey); - width = Math.max(width, format.get(VideoFormatKeys.WidthKey)); - height = Math.max(height, format.get(VideoFormatKeys.HeightKey)); - vTrack = new MonteVideoTrack(Locale.ENGLISH, i, i + "", metadata); - vTrack.setWidth(format.get(VideoFormatKeys.WidthKey)); - vTrack.setHeight(format.get(VideoFormatKeys.HeightKey)); - - Codec codec1 = Registry.getInstance().getCodec(format, new Format(DataClassKey, BufferedImage.class)); - if (codec1 == null) { - throw new IOException("Could not find a codec for the video track."); - } - Codec codec2 = Registry.getInstance().getCodec(codec1.getOutputFormat(), new Format(EncodingKey, VideoFormatKeys.ENCODING_WRITABLE_IMAGE, DataClassKey, WritableImage.class)); - if (codec2 == null) { - throw new IOException("Could not find a codec for JavaFX WritableImage."); - } - CodecChain codec = new CodecChain(codec1, codec2); - vTrack.setCodec(codec); - Buffer inBuf = vTrack.inBuffer; - vTrack.outBufferIndex = (vTrack.outBufferIndex + 1) % vTrack.outBuffer.length; - Buffer outBuf = vTrack.outBuffer[vTrack.outBufferIndex]; - reader.read(i, inBuf); - int status; - do { - status = codec.process(inBuf, outBuf); - } while (status == Codec.CODEC_OUTPUT_NOT_FILLED); - if (!outBuf.isFlag(BufferFlag.DISCARD) && outBuf.data instanceof WritableImage wImg) { - vTrack.setVideoImage(wImg); - vTrack.setCurrentStartTime(outBuf.timeStamp); - vTrack.setCurrentEndTime(outBuf.timeStamp.add(outBuf.sampleDuration)); - } else { - throw new IOException("Could not decode the video track."); - } + realizeVideoTrack(i, metadata, format, trackFormat); yield vTrack; } - case FormatKeys.MediaType.AUDIO -> new MonteAudioTrack(Locale.ENGLISH, i, i + "", metadata); - case FormatKeys.MediaType.TEXT -> new MonteSubtitleTrack(Locale.ENGLISH, i, i + "", metadata); - default -> new MonteTrack(Locale.ENGLISH, i, i + "", metadata); + case FormatKeys.MediaType.AUDIO -> { + MonteAudioTrack audioTrack = realizeAudioTrack(i, metadata, trackFormat, format); + yield audioTrack; + } + case FormatKeys.MediaType.TEXT -> realizeSubtitleTrack(i, metadata); + default -> new MonteUnsupportedTrack(Locale.ENGLISH, i, i + "", metadata); }); } - int finalWidth = width; - int finalHeight = height; + int finalWidth = mediaWidth; + int finalHeight = mediaHeight; runAndWait(() -> { + media.setFormat(fileFormat); media.getTracks().addAll(tracks); media.setDuration(Duration.seconds(reader.getDuration().doubleValue())); media.setWidth(finalWidth); media.setHeight(finalHeight); - for (TrackInterface track : tracks) { - if (track instanceof MonteVideoTrack mvt - && !media.videoImage.isBound()) { - media.videoImage.bind(mvt.videoImage); - } - } - player.setCurrentTime(Duration.seconds(vTrack.getCurrentStartTime().doubleValue())); + player.setCurrentTime(Duration.seconds(vTrack.getRenderedStartTime().doubleValue())); player.setCurrentCount(0); player.setCurrentRate(0.0); return null; @@ -162,6 +151,101 @@ protected void doRealizing() throws Exception { } + private static MonteSubtitleTrack realizeSubtitleTrack(int i, Map metadata) { + return new MonteSubtitleTrack(Locale.ENGLISH, i, i + "", metadata); + } + + private MonteAudioTrack realizeAudioTrack(int i, Map metadata, Format trackFormat, Format format) throws LineUnavailableException, IOException { + MonteAudioTrack audioTrack = new MonteAudioTrack(Locale.ENGLISH, i, i + "", metadata); + audioTrack.setFormat(trackFormat); + Format desiredOutputFormat = new Format(MediaTypeKey, FormatKeys.MediaType.AUDIO,// + EncodingKey, ENCODING_PCM_SIGNED,// + MimeTypeKey, MIME_JAVA,// + SignedKey, true); + Codec codec1 = Registry.getInstance().getCodec(format, desiredOutputFormat); + if (codec1 != null) { + codec1.setInputFormat(format); + codec1.setOutputFormat(desiredOutputFormat); + audioTrack.setCodec(codec1); + + Format actualOutputFormat = codec1.getOutputFormat(); + AudioFormat audioFormat = new AudioFormat( + actualOutputFormat.get(AudioFormatKeys.SampleRateKey).floatValue(), + actualOutputFormat.get(AudioFormatKeys.SampleSizeInBitsKey), + actualOutputFormat.get(AudioFormatKeys.ChannelsKey), + actualOutputFormat.get(SignedKey), actualOutputFormat.get(AudioFormatKeys.ByteOrderKey) == ByteOrder.BIG_ENDIAN); + SourceDataLine sourceDataLine = AudioSystem.getSourceDataLine(audioFormat); + audioTrack.setSourceDataLine(sourceDataLine); + sourceDataLine.open(audioFormat); + sourceDataLine.start(); + + audioTrack.setFormat(trackFormat); + Buffer inBuf = audioTrack.inBuffer; + Buffer outBuf = audioTrack.outBufferA; + reader.read(i, inBuf); + int status; + do { + status = codec1.process(inBuf, outBuf); + } while (status == Codec.CODEC_OUTPUT_NOT_FILLED); + + } + return audioTrack; + } + + private void realizeVideoTrack(int i, Map metadata, Format format, Format trackFormat) throws IOException { + vTrack = new MonteVideoTrack(Locale.ENGLISH, i, i + "", metadata); + vTrack.setWidth(format.get(VideoFormatKeys.WidthKey)); + vTrack.setHeight(format.get(VideoFormatKeys.HeightKey)); + CodecChain codecChain = null; + Codec codec1 = Registry.getInstance().getCodec(format, new Format(DataClassKey, BufferedImage.class)); + if (codec1 != null) { + Codec codec2 = Registry.getInstance().getCodec(codec1.getOutputFormat(), new Format(EncodingKey, VideoFormatKeys.ENCODING_WRITABLE_IMAGE, DataClassKey, WritableImage.class)); + if (codec2 != null) { + codecChain = new CodecChain(codec1, codec2); + } + vTrack.setCodec(codecChain); + + vTrack.setFormat(trackFormat); + Buffer inBuf = vTrack.inBuffer; + Buffer outBuf = vTrack.outBufferA; + reader.read(i, inBuf); + int status; + do { + status = codecChain.process(inBuf, outBuf); + } while (status == Codec.CODEC_OUTPUT_NOT_FILLED); + if (!outBuf.isFlag(BufferFlag.DISCARD) && outBuf.data instanceof WritableImage wImg) { + vTrack.setVideoImage(wImg); + vTrack.setRenderedStartTime(outBuf.timeStamp); + vTrack.setRenderedEndTime(outBuf.timeStamp.add(outBuf.sampleDuration)); + } else { + throw new IOException("Could not decode the video track."); + } + } + } + + public Rational getFrameAfter(Rational seconds) { + int trackID = (int) vTrack.getTrackID(); + try { + long sample = reader.timeToSample(trackID, seconds); + Rational time = reader.sampleToTime(trackID, sample); + Rational duration = reader.getDuration(trackID, sample); + return time.add(duration); + } catch (IOException e) { + return Rational.ZERO; + } + } + + public Rational getFrameBefore(Rational seconds) { + int trackID = (int) vTrack.getTrackID(); + try { + long sample = reader.timeToSample(trackID, seconds); + Rational time = reader.sampleToTime(trackID, Math.max(0, sample - 1)); + return time; + } catch (IOException e) { + return Rational.ZERO; + } + } + protected T runAndWait(Callable r) throws Exception { CompletableFuture future = new CompletableFuture<>(); Platform.runLater(() -> { @@ -185,60 +269,172 @@ protected void doPrefetching() throws Exception { @Override protected void doPrefetched() throws Exception { - Rational requestedTime = targetTime; - if (requestedTime == null || vTrack == null) { + Rational playTime = seekTime.getAndSet(null); + if (playTime == null || vTrack == null) { return; } - int trackId = (int) vTrack.getTrackID(); - requestedTime = Rational.min(reader.getDuration(trackId), requestedTime); - if (vTrack.getCurrentStartTime().compareTo(requestedTime) <= 0 && - requestedTime.compareTo(vTrack.getCurrentEndTime()) <= 0) { + reader.setMovieReadTime(playTime); + updateBuffers(playTime); + renderBuffers(playTime, false); + } + + private Rational frameRate = Rational.valueOf(1, 60); + + @Override + protected void doStarted() throws Exception { + int trackId = (int) vTrack.getTrackID(); + Rational playTime = vTrack.getRenderedEndTime() == null ? Rational.valueOf(player.getCurrentTime().toSeconds()) : vTrack.getRenderedEndTime(); + if (playTime == null || vTrack == null) { return; } + // Start from beginning if we are at the end of the movie + Rational playEndTime = reader.getDuration(trackId); + if (playTime.compareTo(playEndTime) >= 0) { + playTime = Rational.ZERO; + } + + reader.setMovieReadTime(playTime); + Rational playStartTime = playTime; + long startNanoTime = System.nanoTime(); + while (playTime.compareTo(playEndTime) <= 0 && getTargetState() == PlayerEngine.STARTED) { + + updateBuffers(playTime); + + int elapsedMovieMillis = playTime.subtract(playStartTime).multiply(1000).intValue(); + int elapsedSystemMillis = (int) ((System.nanoTime() - startNanoTime) / 1_000_000L); + int sleepMillis = (elapsedMovieMillis - elapsedSystemMillis); + if (sleepMillis > 0) { + Thread.sleep(sleepMillis); + } + + renderBuffers(playTime, true); + + // Compute the next play time + Rational newTargetTime = seekTime.getAndSet(null); + if (newTargetTime != null) { + newTargetTime = Rational.clamp(newTargetTime, Rational.ZERO, playEndTime); + reader.setMovieReadTime(newTargetTime); + playStartTime = playTime = newTargetTime; + startNanoTime = System.nanoTime(); + } else { + playTime = playTime.add(frameRate); + } + } + + stopAudio(); + } + + private void stopAudio() { + for (var t : media.getTracks()) { + if (t instanceof MonteAudioTrack mat) { + SourceDataLine sourceDataLine = mat.getSourceDataLine(); + if (sourceDataLine != null) { + sourceDataLine.flush(); + } + } + } + } + + private void updateBuffers(Rational playTime) throws IOException { + for (var track : media.getTracks()) { + if (!(track instanceof MonteTrackInterface tr) + || tr.getCodec() == null) { + // we cannot decode this track + continue; + } + Buffer outBuf = tr.getOutBufferA(); + if (outBuf.timeStamp.compareTo(playTime) <= 0 && + playTime.compareTo(outBuf.getBufferEndTimestamp()) < 0) { + // outBufA contains the sample for this playTime + continue; + } - reader.setMovieReadTime(requestedTime); - var codec = vTrack.getCodec(); - Buffer inBuf = vTrack.inBuffer; - vTrack.outBufferIndex = (vTrack.outBufferIndex + 1) % vTrack.outBuffer.length; - Buffer outBuf = vTrack.outBuffer[vTrack.outBufferIndex]; - int status; - Rational startTime = Rational.ZERO; - Rational endTime = Rational.ZERO; - do { - reader.read(trackId, inBuf); - /* - if(inBuf.isFlag(BufferFlag.DISCARD)&&inBuf.isFlag(BufferFlag.END_OF_MEDIA)){ - break; - }*/ + outBuf = tr.swapOutBuffers(); + Buffer inBuf = tr.getInBuffer(); + var codec = tr.getCodec(); + int trackId = (int) tr.getTrackID(); + int status; do { - status = codec.process(inBuf, outBuf); - } while (status == Codec.CODEC_OUTPUT_NOT_FILLED); - startTime = outBuf.timeStamp; - endTime = startTime.add(outBuf.sampleDuration); - } while (status == Codec.CODEC_OK && endTime.compareTo(requestedTime) < 0 && !outBuf.isFlag(BufferFlag.END_OF_MEDIA)); - - final Rational finalStartTime = startTime; - final Rational finalEndTime = endTime; - vTrack.setCurrentStartTime(finalStartTime); - vTrack.setCurrentEndTime(finalEndTime); - if (!outBuf.isFlag(BufferFlag.DISCARD) && outBuf.data instanceof WritableImage wImg) { - runAndWait(() -> { - vTrack.setVideoImage(wImg); - player.setCurrentTime(Duration.seconds(finalStartTime.doubleValue())); - player.setCurrentRate(0.0); - return null; - }); + reader.read(trackId, inBuf); + do { + status = codec.process(inBuf, outBuf); + } while (status == Codec.CODEC_OUTPUT_NOT_FILLED); + } while (status == Codec.CODEC_OK + && outBuf.getBufferEndTimestamp().compareTo(playTime) < 0 + && !outBuf.isFlag(BufferFlag.END_OF_MEDIA)); } } - @Override - protected void doStarted() { + private void renderBuffers(Rational renderTime, boolean playAudio) throws IOException { + Platform.runLater(() -> { + boolean muted = player.muteProperty().get(); + player.setCurrentTime(Duration.seconds(renderTime.doubleValue())); + for (var track : media.getTracks()) { + if (!(track instanceof MonteTrackInterface tr) + || tr.getCodec() == null) { + // we cannot decode this track + continue; + } + Buffer outBuf = tr.getOutBufferA(); + if (!outBuf.isFlag(BufferFlag.DISCARD)) { + // outBufA contains the sample for this playTime + Rational bufferStartTime = outBuf.timeStamp; + Rational bufferEndTime = outBuf.getBufferEndTimestamp(); + boolean previouslyRenderedBufferTimeIntersectsPlayTime = tr.getRenderedStartTime().compareTo(renderTime) <= 0 && + renderTime.compareTo(tr.getRenderedEndTime()) < 0; + boolean bufferTimeIntersectsPlayTime = bufferStartTime.compareTo(renderTime) <= 0 && + renderTime.compareTo(bufferEndTime) < 0; + if (bufferTimeIntersectsPlayTime) { + switch (tr) { + case MonteVideoTrack mvt -> { + if (outBuf.data instanceof WritableImage img) { + mvt.setVideoImage(img); + } + } + case MonteAudioTrack mat -> { + if (mat.getSourceDataLine() != null && outBuf.data instanceof byte[] byteArray) { + if (playAudio & !muted) { + long currentNanoTime = System.nanoTime(); + boolean isRenderTimeValid = mat.renderTimeValidUntilNanoTime > currentNanoTime; + int skipSamples; + if (isRenderTimeValid && tr.getRenderedStartTime().compareTo(renderTime) < 0 + && renderTime.compareTo(tr.getRenderedEndTime()) < 0) { + skipSamples = (bufferStartTime.compareTo(tr.getRenderedEndTime()) < 0) ? + tr.getRenderedEndTime().subtract(bufferStartTime).divide(outBuf.sampleDuration).intValue() : 0; + } else { + skipSamples = 0; + } + if (skipSamples < outBuf.sampleCount) { + int sampleSize = outBuf.length / outBuf.sampleCount; + byte[] clippedSamples = new byte[sampleSize * (outBuf.sampleCount - skipSamples)]; + System.arraycopy(byteArray, outBuf.offset + skipSamples * sampleSize, clippedSamples, 0, clippedSamples.length); + mat.dispatcher.execute(() -> { + mat.getSourceDataLine().write(clippedSamples, 0, clippedSamples.length); + }); + Rational clippedBufferDuration = outBuf.sampleDuration.multiply(outBuf.sampleCount - skipSamples); + mat.renderTimeValidUntilNanoTime = (long) (currentNanoTime + clippedBufferDuration.doubleValue() * 1e9); + + } + } + } + } + default -> { + // unsupported track type + } + } + } + tr.setRenderedStartTime(bufferStartTime); + tr.setRenderedEndTime(bufferEndTime); + } + } + }); } + public void seek(Rational seconds) { - targetTime = seconds; + seekTime.set(seconds); prefetch(); } } diff --git a/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/Labels.properties b/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/Labels.properties index a9beabd..a396f77 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/Labels.properties +++ b/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/Labels.properties @@ -4,6 +4,7 @@ # application.name=MonteMoviePlayer application.copyright=Copyright © 2024 Werner Randelshofer, Switzerland. MIT License. +file.noFile=No File file.menu=File edit.menu=Edit view.menu=View @@ -14,4 +15,8 @@ file.open=Open... error=Error: duration.indefinite=Indefinite duration.unknown=Unknown -error.creatingPlayer=Can not play this file \ No newline at end of file +error.creatingPlayer=Can not play this file +view.zoomToActualSize=Zoom to Actual Size +view.zoomToFit=Zoom to Fit +view.zoomIn=Zoom in +view.zoomOut=Zoom out \ No newline at end of file diff --git a/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/MainWindow.fxml b/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/MainWindow.fxml index 8ec204f..1cc72b3 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/MainWindow.fxml +++ b/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/MainWindow.fxml @@ -3,6 +3,7 @@ +

- - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -27,6 +59,6 @@
- +
diff --git a/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/Controls.fxml b/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/PlayerControls.fxml similarity index 85% rename from org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/Controls.fxml rename to org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/PlayerControls.fxml index 02479e6..4038439 100644 --- a/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/Controls.fxml +++ b/org.monte.demo.javafx.movieplayer/src/main/resources/org.monte.demo.javafx.movieplayer/org/monte/demo/javafx/movieplayer/PlayerControls.fxml @@ -1,6 +1,5 @@ - @@ -10,11 +9,13 @@ - + + maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" styleClass="controls-panel" vgap="4.0" + AnchorPane.bottomAnchor="60.0" AnchorPane.rightAnchor="60.0"> @@ -31,13 +32,13 @@