JavaFX setOnEndOfMedia on next MediaPlayer - javafx

I have List<MediaPlayer> that is populating by
private List<MediaPlayer> players() {
for (String file : path.list((dir1, name) -> {
if (name.endsWith(SUPPORTED_VIDEO_FILE_EXTENSIONS)){
return true;
}
return false;
})) {
players.add(createPlayer(Paths.get(path + "/" + file).toUri().toString()));
System.out.println(Paths.get(path + "/" + file).toUri().toString());
}
return players;
}
In my case size of List<MediaPlayer> is 3. Than i use it there
if (currentNumOfVideo == -1) {
currentNumOfVideo = 0;
}
mv = new MediaView();
MediaPlayer currentPlayer = players.get(currentNumOfVideo);
mv.setMediaPlayer(currentPlayer);
currentPlayer.play();
players.get(currentNumOfVideo).setOnEndOfMedia(() -> {
players.get(currentNumOfVideo).stop();
if (currentNumOfVideo < players.size()) {
currentNumOfVideo += 1;
mv.setMediaPlayer(players.get(currentNumOfVideo));
players.get(currentNumOfVideo).play();
} else {
currentNumOfVideo = 0;
mv.setMediaPlayer(players.get(currentNumOfVideo));
players.get(currentNumOfVideo).play();
}
});
First video playing, when it ends second video starts. After second video MediaPlayer stops and didn't play third video.
I understand that because of my setOnEndOfMedia that is only on first MediaPlayer. When the second video starts it doesn't have setOnEndOfMedia. How can I setOnEndOfMedia on every video in my List<MediaPlayer>.

Personally, I would not create all the MediaPlayer instances ahead-of-time. Instead, get a list of Media objects or at least the URIs pointing to the media. Then create a method which is responsible for playing the next video when the current video ends. That method will dispose the old MediaPlayer, create the next MediaPlayer, and configure it to call the same method upon completion. For example:
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.stage.Stage;
public class Main extends Application {
private MediaView mediaView;
private List<Media> media;
private int curIndex;
#Override
public void start(Stage primaryStage) throws Exception {
media = ...; // get media from somewhere
mediaView = new MediaView();
primaryStage.setScene(new Scene(new StackPane(mediaView), 720, 480));
primaryStage.show();
playNextVideo();
}
private void playNextVideo() {
disposePlayer();
if (curIndex == media.size()) {
return; // no more videos to play
}
MediaPlayer player = new MediaPlayer(media.get(curIndex++));
player.setAutoPlay(true); // play ASAP
player.setOnEndOfMedia(this::playNextVideo); // play next video when this one ends
mediaView.setMediaPlayer(player);
}
private void disposePlayer() {
MediaPlayer player = mediaView.getMediaPlayer();
if (player != null) {
player.dispose(); // release resources
}
}
}
This may cause a pause between videos. If that's not acceptable you could create the next MediaPlayer ahead-of-time, either at the same time as the current MediaPlayer or when the current player reaches a certain timestamp (e.g. 10 seconds before the end). But I still wouldn't create all the MediaPlayer instances ahead-of-time.

Related

MediaPlayer silently fails to play H264/MPEG4 video

I would like to use MediaView to play video streams and video files generated by the Shinobi media server but MediaView does not seem to be able to handle any stream or file generated by Shinobi.
I am using Java 18 and JavaFX 19 (I have tried older versions as well).
I have one sample file generated by Shinobi here.
It plays fine in VLC, which shows that the file has the following attributes:
Codec: H264 - MPEG-4 AVC (part 10) (avc1)
Video resolution: 1280x720
Decoded format: Planar 4:2:2 YUV full scale
Chroma location: left
I have added error handlers to the MediaPlayer, MediaView and Media objects but there are no errors when I attempt to play the file.
Does anybody have any ideas as to why the player wouldn't like the file above?
Has anybody had success playing files from Shinobi (it uses FFMPEG under the covers.)?
It does play other files like:
https://coderslegacy.com/wp-content/uploads/2020/07/Pygame_Platformer-1.mp4";
I expect the video to play without errors or for the player to tell me why it can not play the video.
Here is my code:
package com.example.videotester;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaErrorEvent;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.stage.Stage;
import java.io.File;
public class HelloApplication extends Application {
#Override
public void start(Stage stage) throws Exception {
Button button1 = new Button("Play");
Button button2 = new Button("Pause");
Button button3 = new Button("Stop");
String path = "C:/Users/Rob/Desktop/rlrO5DVBJS-2022-10-26T20-52-34.mp4";
File f = new File(path);
path = f.toURI().toString();
// path="http://192.168.1.239:8080/532046fecc8da376f3f32f5518bad33b/videos/NUW6mXm9CF/rlrO5DVBJS/2022-10-26T14-32-07.mp4";
// path="http://192.168.1.239:8080/2921f1ca7204e640734709e29fc2033f/hls/NUW6mXm9CF/rlrO5DVBJS/s.m3u8";
// path="http://192.168.1.239:8080/2921f1ca7204e640734709e29fc2033f/h265/NUW6mXm9CF/rlrO5DVBJS/s.hevc";
// path="http://192.168.1.239:8080/2921f1ca7204e640734709e29fc2033f/mp4/NUW6mXm9CF/rlrO5DVBJS/s.mp4";
// path="http://192.168.1.239:8080/5ee9ca532fd17f860e3cef43a288b951/mjpeg/NUW6mXm9CF/rlrO5DVBJS"; //mjpeg
// path="http://192.168.1.239:8080/f7fb8d581d5aab4ebb8732de13b61337/videos/NUW6mXm9CF/rlrO5DVBJS/2022-10-26T15-57-19.mp4";
// path="http://192.168.1.239:8080/5ee9ca532fd17f860e3cef43a288b951/videos/NUW6mXm9CF/rlrO5DVBJS/2022-10-27T19-49-31.mp4";
// path="http://192.168.1.239:8080/c6c8a86382548433c505d9e7cf7c2085/videos/NUW6mXm9CF/rlrO5DVBJS/2022-10-26T04-05-00.mp4";
// path="https://coderslegacy.com/wp-content/uploads/2020/07/Pygame_Platformer-1.mp4";
// path="https://www.dropbox.com/s/h1ky0he5dvclhkt/rlrO5DVBJS-2022-10-30T20-53-29.mp4?dl=0";
//Instantiating Media class
// Media media = new Media(new File(path).toURI().toString());
//URL url = new URL(path);
final Media media;
final MediaPlayer mediaPlayer;
MediaView mediaView = null;
try {
media = new Media(path);
if (media.getError() == null) {
media.setOnError(() -> System.out.println("media player error : " + media.getError()));
try {
mediaPlayer = new MediaPlayer(media);
mediaPlayer.setAutoPlay(true);
button1.setOnAction(e -> mediaPlayer.play());
button2.setOnAction(e -> mediaPlayer.pause());
button3.setOnAction(e -> mediaPlayer.stop());
mediaPlayer.setOnReady(() -> System.out.println("Video player ready"));
if (mediaPlayer.getError() == null) {
mediaPlayer.setOnError(() -> System.out.println("media player error : " + mediaPlayer.getError()));
mediaView = new MediaView(mediaPlayer);
mediaView.setOnError(mee -> System.out.println("media view error : " + t));
} else
System.out.println("Error in media player: " + mediaPlayer.getError());
} catch (Exception mediaPlayerException) {
System.out.println("media player exception " + mediaPlayerException);
}
} else
System.out.println("Error media creating media " + media.getError());
} catch (Exception mediaException) {
// Handle exception in Media constructor.
System.out.println("Handle exception " + mediaException);
System.exit(1);
}
GridPane layout = new GridPane();
layout.setHgap(10);
layout.setVgap(10);
layout.add(button1, 0, 0);
layout.add(button2, 1, 0);
layout.add(button3, 2, 0);
layout.add(mediaView, 0, 1, 4, 1);
Scene scene = new Scene(layout, 300, 200);
stage.setTitle("Video Player Tester");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
Rob - your file in
https://www.dropbox.com/s/h1ky0he5dvclhkt/rlrO5DVBJS-2022-10-30T20-53-29.mp4?dl=0
has a color subsampling of 4:2:2 (color information scaled to 50%) most consumer video is 4:2:0 (color information scaled to 25%).
The file at https://coderslegacy.com/wp-content/uploads/2020/07/Pygame_Platformer-1.mp4 is 4:2:0.
I am guessing that the Java media player is passing the video to the operating system and the OS's default decoder may not support the higher color resolution of 4:2:2.

JavaFX detect computer sleep or hibernate

I have a JavaFX application that gets CPU and Memory problems after the computer is going to sleep or hibernate.
In the application, I use a Canvas that is painted two times a second. This may cause issues. I am wondering if it's possible to detect when the computer is sleeping, and not repaint it. Maybe the canvas.isVisible() is already checking this?
You could only paint if your app has focus.
This can be achieved by pausing the animation when your application's main window no longer has focus.
You can monitor the stage's focusedProperty() to find out when the stage has focus.
I ran some tests on a Mac (OS X 12.3) with JavaFX 18 and found that when the computer is put to sleep (click the apple icon in the menu bar and select Sleep), the focus is removed from the application, which allows the animation for the application to be paused while it does not have focus.
Example application
import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class SleepyApp extends Application {
private long start;
private Counter continuousCounter = new Counter();
private Counter focusedCounter = new Counter();
#Override
public void start(Stage stage) throws Exception {
VBox layout = new VBox(10,
focusedCounter.getCounterLabel(),
continuousCounter.getCounterLabel()
);
layout.setPrefSize(80, 80);
layout.setAlignment(Pos.CENTER);
stage.setScene(new Scene(layout));
stage.show();
stage.focusedProperty().addListener((observable, wasFocused, isFocused) -> {
if (isFocused) {
focusedCounter.play();
} else {
focusedCounter.pause();
}
});
continuousCounter.play();
focusedCounter.play();
start = System.currentTimeMillis();
}
#Override
public void stop() throws Exception {
long stop = System.currentTimeMillis();
continuousCounter.stop();
focusedCounter.stop();
long elapsed = (stop - start) / 1_000;
System.out.println("Elapsed: " + elapsed + " seconds.");
System.out.println("Continuous Counter: " + continuousCounter.getCount() + " seconds.");
System.out.println("Focused Counter: " + focusedCounter.getCount() + " seconds.");
}
public static void main(String[] args) {
launch(args);
}
class Counter {
private final Timeline timeline;
private final IntegerProperty count;
private final Label counterLabel;
public Counter() {
count = new SimpleIntegerProperty(0);
counterLabel = new Label(count.asString().get());
counterLabel.textProperty().bind(
count.asString()
);
counterLabel.setStyle("-fx-font-size: 20px");
timeline = new Timeline(
new KeyFrame(
Duration.seconds(1),
e -> count.set(count.get() + 1)
)
);
timeline.setCycleCount(Timeline.INDEFINITE);
}
public int getCount() {
return count.get();
}
public Label getCounterLabel() {
return counterLabel;
}
public void play() {
timeline.play();
}
public void pause() {
timeline.pause();
}
public void stop() {
timeline.stop();
}
}
}
Output
In this case, the computer was put to sleep for 16 seconds.
The first number is the number of one per second animation frames rendered only when the application has focus.
The second number is the number of one per second animation frames rendered regardless of whether the application has focus (and sleep state).
Elapsed: 30 seconds.
Continuous Counter: 30 seconds.
Focused Counter: 14 seconds.
On isVisible()
node.isVisible() is not applicable for this case.
This is a description of the isVisible method:
Specifies whether this Node and any subnodes should be rendered as part of the scene graph. A node may be visible and yet not be shown in the rendered scene if, for instance, it is off the screen or obscured by another Node. Invisible nodes never receive mouse events or keyboard focus and never maintain keyboard focus when they become invisible.
As the documentation indicates, the node may be visible but not shown in the rendered scene, which will be the case when the computer is sleeping.

Delegate mouse events to all children in a JavaFX StackPane

I'm trying to come up with a solution to allow multiple Pane nodes handle mouse events independently when assembled into a StackPane
StackPane
Pane 1
Pane 2
Pane 3
I'd like to be able to handle mouse events in each child, and the first child calling consume() stops the event going to the next child.
I'm also aware of setPickOnBounds(false), but this does not solve all cases as some of the overlays will be pixel based with Canvas, i.e. not involving the scene graph.
I've tried various experiments with Node.fireEvent(). However these always lead to recursion ending in stack overflow. This is because the event is propagated from the root scene and triggers the same handler again.
What I'm looking for is some method to trigger the event handlers on the child panes individually without the event travelling through its normal path.
My best workaround so far is to capture the event with a filter and manually invoke the handler. I'd need to repeat this for MouseMoved etc
parent.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
for (Node each : parent.getChildren()) {
if (!event.isConsumed()) {
each.getOnMouseClicked().handle(event);
}
}
event.consume();
});
However this only triggers listeners added with setOnMouseClicked, not addEventHandler, and only on that node, not child nodes.
Another sort of solution is just to accept JavaFX doesn't work like this, and restructure the panes like this, this will allow normal event propagation to take place.
Pane 1
Pane 2
Pane 3
Example
import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class EventsInStackPane extends Application {
public static void main(String[] args) {
launch(args);
}
private static class DebugPane extends Pane {
public DebugPane(Color color, String name) {
setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY)));
setOnMouseClicked(event -> {
System.out.println("setOnMouseClicked " + name + " " + event);
});
addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventHandler " + name + " " + event);
});
addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventFilter " + name + " " + event);
});
}
}
#Override
public void start(Stage primaryStage) throws Exception {
DebugPane red = new DebugPane(Color.RED, "red");
DebugPane green = new DebugPane(Color.GREEN, "green");
DebugPane blue = new DebugPane(Color.BLUE, "blue");
setBounds(red, 0, 0, 400, 400);
setBounds(green, 25, 25, 350, 350);
setBounds(blue, 50, 50, 300, 300);
StackPane parent = new StackPane(red, green, blue);
eventHandling(parent);
primaryStage.setScene(new Scene(parent));
primaryStage.show();
}
private void eventHandling(StackPane parent) {
parent.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (!event.isConsumed()) {
for (Node each : parent.getChildren()) {
Event copy = event.copyFor(event.getSource(), each);
parent.fireEvent(copy);
if (copy.isConsumed()) {
break;
}
}
}
event.consume();
});
}
private void setBounds(DebugPane panel, int x, int y, int width, int height) {
panel.setLayoutX(x);
panel.setLayoutY(y);
panel.setPrefWidth(width);
panel.setPrefHeight(height);
}
}
Using the hint from #jewelsea I was able to use a custom chain. I've done this from a "catcher" Pane which is added to the front of the StackPane. This then builds a chain using all the children, in reverse order, excluding itself.
private void eventHandling(StackPane parent) {
Pane catcher = new Pane() {
#Override
public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
EventDispatchChain chain = super.buildEventDispatchChain(tail);
for (int i = parent.getChildren().size() - 1; i >= 0; i--) {
Node child = parent.getChildren().get(i);
if (child != this) {
chain = chain.prepend(child.getEventDispatcher());
}
}
return chain;
}
};
parent.getChildren().add(catcher);
}

Restarting and pausing and resuming clip hangs the gui of music player, while pressing pause and play resumes playing from stopping point

This program is a music player that allows user to pick a .wav file, play, pause, resume, and restart a the music file from a clip object and audioinput stream. The audio input stream loads a file that is determined by user via FileChooser. The program can play, pause, and resume by selecting a file, pressing play, pause, then play again, but does not play using the restart method or the resume method invoked via the respective buttons. Instead, the program hangs until the X button is clicked. I think it has something to do with the resetaudiostream method, but I am unsure what. Maybe something to do with ending the old clip and creating a new clip instance. Please review the logic and let me know what is making it hang and how that could be remedied.
package sample;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javax.sound.sampled.*;
import java.io.File;
import java.io.IOException;
public class Main extends Application {
static File musicfile;
static Long currentFrame;
static Clip clip;
static String status = "play";
static AudioInputStream audioInputStream;
static String filePath;
public void SimpleAudioPlayer()
throws UnsupportedAudioFileException,
IOException, LineUnavailableException
{
// create AudioInputStream object
audioInputStream =
AudioSystem.getAudioInputStream(new File(filePath).getAbsoluteFile());
// create clip reference
clip = AudioSystem.getClip();
// open audioInputStream to the clip
clip.open(audioInputStream);
clip.loop(Clip.LOOP_CONTINUOUSLY);
}
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Music Player");
GridPane gp = new GridPane();
Button selectFile = new Button("Select File");
GridPane.setConstraints(selectFile, 0,0);
selectFile.setOnAction(event->{
FileChooser filechooser = new FileChooser();
// create AudioInputStream object
try {
musicfile = filechooser.showOpenDialog(null);
audioInputStream = AudioSystem.getAudioInputStream(musicfile);
clip = AudioSystem.getClip();
// open audioInputStream to the clip
clip.open(audioInputStream);
}catch(IOException | UnsupportedAudioFileException | LineUnavailableException e){
e.printStackTrace();
}
});
Button play = new Button("Play");
GridPane.setConstraints(play, 1,0);
play.setOnAction(event->{
if(status == "play") {
clip.loop(Clip.LOOP_CONTINUOUSLY);
}
play();
});
Button pause = new Button("Pause");
GridPane.setConstraints(pause, 2,0);
pause.setOnAction(event -> pause());
Button restart = new Button("Restart");
GridPane.setConstraints(restart, 0,1);
restart.setOnAction(event -> {
try{
restart();
}
catch(IOException | UnsupportedAudioFileException | LineUnavailableException e){
e.printStackTrace();}
});
Button resume = new Button("Resume");
GridPane.setConstraints(resume, 1,1);
resume.setOnAction(event -> {
try {
resumeAudio();
}catch(IOException | LineUnavailableException | UnsupportedAudioFileException e){
e.printStackTrace();
}
});
gp.getChildren().addAll(play,selectFile, pause, restart, resume);
primaryStage.setScene(new Scene(gp, 300, 275));
primaryStage.show();
}
public void play()
{
//start the clip
clip.start();
status = "play";
}
// Method to pause the audio
public void pause()
{
if (status.equals("paused"))
{
System.out.println("audio is already paused");
return;
}
currentFrame =
clip.getMicrosecondPosition();
clip.stop();
status = "paused";
}
// Method to resume the audio
public void resumeAudio() throws UnsupportedAudioFileException,
IOException, LineUnavailableException
{
if (status.equals("play"))
{
System.out.println("Audio is already "+
"being played");
return;
}
clip.close();
resetAudioStream();
clip.setMicrosecondPosition(currentFrame);
status = "play";
play();
}
// Method to restart the audio
public void restart() throws IOException, LineUnavailableException,
UnsupportedAudioFileException
{
clip.stop();
clip.close();
resetAudioStream();
currentFrame = 0L;
clip.setMicrosecondPosition(0);
status = "play";
play();
}
// Method to stop the audio
public void stop() throws UnsupportedAudioFileException,
IOException, LineUnavailableException
{
currentFrame = 0L;
clip.stop();
clip.close();
}
// Method to jump over a specific part
public void jump(long c) throws UnsupportedAudioFileException, IOException,
LineUnavailableException
{
if (c > 0 && c < clip.getMicrosecondLength())
{
clip.stop();
clip.close();
resetAudioStream();
currentFrame = c;
clip.setMicrosecondPosition(c);
this.play();
}
}
// Method to reset audio stream
public void resetAudioStream() throws UnsupportedAudioFileException, IOException,
LineUnavailableException
{
audioInputStream = AudioSystem.getAudioInputStream(musicfile);
clip = AudioSystem.getClip();
clip.open(audioInputStream);
clip.loop(Clip.LOOP_CONTINUOUSLY);
}
public static void main(String[] args) {
launch(args);
}
}
It is quiet simple to get the required functionality with a MediaPlayer:
import java.net.URI;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaPlayer.Status;
import javafx.stage.Stage;
import javafx.util.Duration;
/*
* If you get "cannot access class com.sun.glass.utils.NativeLibLoader" exception you may need to
* add a VM argument: --add-modules javafx.controls,javafx.media as explained here:
* https://stackoverflow.com/questions/53237287/module-error-when-running-javafx-media-application
*/
public class Main extends Application {
private MediaPlayer player;
private static final long JUMP_BY = 5000;//millis
#Override
public void start(Stage primaryStage) throws Exception{
URI uri = new URI("https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3");
Media media = new Media(uri.toString());
//OR Media media = new Media("https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3");
player = new MediaPlayer(media);
player.setOnError(() -> System.out.println(media.getError().toString()));
GridPane gp = new GridPane();
gp.setHgap(10);
Button play = new Button("Play");
GridPane.setConstraints(play, 0,0);
play.setOnAction(event-> playAudio());
Button pause = new Button("Pause");
GridPane.setConstraints(pause, 1,0);
pause.setOnAction(event -> pauseAudio());
Button resume = new Button("Resume");
GridPane.setConstraints(resume, 2,0);
resume.setOnAction(event -> resumeAudio());
Button stop = new Button("Stop");
GridPane.setConstraints(stop, 3,0);
stop.setOnAction(event -> stopAudio());
Button restart = new Button("Restart");
GridPane.setConstraints(restart, 4,0);
restart.setOnAction(event -> restartAudio());
Button jump = new Button("Jump >");
GridPane.setConstraints(jump, 5,0);
jump.setOnAction(event -> jump(JUMP_BY));
Label time = new Label();
GridPane.setConstraints(time, 6,0);
time.textProperty().bind( player.currentTimeProperty().asString("%.4s") );
gp.getChildren().addAll(play, pause, resume, stop, restart, jump, time);
primaryStage.setScene(new Scene(gp, 400, 45));
primaryStage.show();
}
//play audio
public void playAudio()
{
player.play();
}
//pause audio
public void pauseAudio()
{
if (player.getStatus().equals(Status.PAUSED))
{
System.out.println("audio is already paused");
return;
}
player.pause();
}
//resume audio
public void resumeAudio()
{
if (player.getStatus().equals(Status.PLAYING))
{
System.out.println("Audio is already playing");
return;
}
playAudio();
}
//restart audio
public void restartAudio()
{
player.seek(Duration.ZERO);
playAudio();
}
// stop audio
public void stopAudio()
{
player.stop();
}
//jump by c millis
public void jump(long c)
{
player.seek(player.getCurrentTime().add(Duration.millis(c)));
}
public static void main(String[] args) {
launch(args);
}
}

Updating two text objects in JavaFX, one field after another, only seeing final result of both changes

I have three text fields displayed, and I want to change the second one, see the result on the display (so wait a couple of seconds), then change the third one, and see the result on the display. Instead, I only see the result of both changes on the display (with no pause inbetween).
import javafx.application.Application;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.animation.PauseTransition;
import javafx.util.Duration;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
public class TestApp extends Application
{
private Text tone = new Text("one");
private Text ttwo = new Text("two");
private Text tthree = new Text("three");
private void process()
{
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> ttwo.setText("four"));
pauseTransition.play();
pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> tthree.setText("five"));
pauseTransition.play();
} // end of method "process"
#Override
public void start(Stage stage)
{
VBox vboxRoot = new VBox();
vboxRoot.getChildren().add(tone);
vboxRoot.getChildren().add(ttwo);
vboxRoot.getChildren().add(tthree);
Scene myScene = new Scene(vboxRoot,350,350);
stage.setScene(myScene);
stage.setTitle("Test");
stage.show();
process();
} // end of method "start"
} // end of class "TestApp"
So initially
one
two
three
is displayed; followed by
one
four
five
What I want to see is
one
four
three
a pause and then
one
four
five
I'm not sure if its a typo if your What I want to see is but if its not the reason you are getting
one
two
three
to initially display is because thats what you have them set as and in this piece of code below you setup 2 PauseTransitions that both have a 2 second wait before changing the text
private void process()
{
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> ttwo.setText("four"));
pauseTransition.play();
pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> tthree.setText("five"));
pauseTransition.play();
}
To fix this you can do a few things such as
Appropriately set what you want from the start
Run ttwo.setText("four"); at the start of your process() method
After doing that you get the starting result of
one
four
three
and after the pause transition finishes 2 seconds later you will see
one
four
five
After pauseTransition.play(); you assign new value to pauseTransition and you play it again, long before the first one completes.
A better approach would be :
Introduce a counter field : private int counter = 0;
And use it like so:
private void process() {
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event ->{
show(counter++);
if(counter < 5 ) pauseTransition.play();//stop criteria
});
pauseTransition.play();
}
private void show(int counter) {
//respond based on counter
}
The following is mcve the demonstrates the idea (it is not meant to demostrate the exact behavior you want which is not clear to me) :
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class FxMain extends Application {
private int counter = 0;
private final Text tone = new Text("one"),
ttwo = new Text("two"),
tthree = new Text("three");
#Override
public void start(Stage primaryStage) throws Exception{
VBox root = new VBox(10);
root.setPadding(new Insets(10));
root.getChildren().addAll(tone,ttwo,tthree);
primaryStage.setScene(new Scene(root, 100,100));
primaryStage.sizeToScene();
primaryStage.show();
process();
}
private void process() {
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event ->{
show(counter++);
if(counter < 3 ) {
pauseTransition.play();//stop criteria
}
});
pauseTransition.play();
}
private void show(int counter) {
switch(counter){
case 0:
tone.setText("two");
break;
case 1:
ttwo.setText("three");
break;
default :
tthree.setText("four");
break;
}
}
public static void main(final String[] args) {
launch(args);
}
}
As others have mentioned, you play both PauseTransitions virtually in parallel. They are started within milliseconds (if not nanoseconds) of each other and I would not be surprised if they actually completed in the same frame. Because of this you see the result of both animations simultaneously.
One solution is to use a SequentialTransition; it will play a list of animations in the order of said list.
private void process() {
SequentialTransition st = new SequentialTransition();
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> ttwo.setText("four"));
st.getChildren().add(pauseTransition);
pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> tthree.setText("five"));
st.getChildren().add(pauseTransition);
st.play();
}
Another solution is to use a Timeline made up of multiple KeyFrames configured to execute at increasing times.
private void process() {
new Timeline(
new KeyFrame(Duration.seconds(2), event -> ttwo.setText("four")),
new KeyFrame(Duration.seconds(4), event -> tthree.setText("five"))
).play();
}
You mention your real goal may be more complicated than setting the text properties of some Text objects. You can adapt either solution to a more general purpose mechanism. Here's an example for a Timeline that will execute an arbitrary number of actions, with a fixed delay between each action (including after the last action):
private static Timeline createTimeline(Duration period, Runnable... actions) {
var frames = new ArrayList<KeyFrame>(actions.length + 1);
var time = Duration.ZERO;
for (var action : actions) {
frames.add(new KeyFrame(time, event -> action.run()));
time = time.add(period);
}
frames.add(new KeyFrame(time)); // adds a delay after last action
return new Timeline(frames.toArray(KeyFrame[]::new));
}
But what happens if I use your approach for one set of actions; and then move onto something else that generates another set of actions. How can I be sure the first set of actions has completed (i.e. been displayed) before I start the second set?
You can use the Animation#onFinished property, in combination with a Queue<Animation>, to play the next Animation when the previous one completes.
private final Queue<Animation> animationQueue = ...;
private Animation currentAnimation;
private void playAnimation(Animation animation) {
if (animation.getCycleCount() == Animation.INDEFINITE) {
throw new IllegalArgumentException();
}
animation.setOnFinished(event -> {
currentAnimation = animationQueue.poll();
if (currentAnimation != null) {
currentAnimation.playFromStart();
}
});
if (currentAnimation != null) {
animationQueue.add(animation);
} else {
currentAnimation = animation;
animation.playFromStart();
}
}

Resources