diff --git a/pom.xml b/pom.xml index f85e989..1e91303 100644 --- a/pom.xml +++ b/pom.xml @@ -27,19 +27,25 @@ org.openjfx javafx-controls - 12.0.2 + 14 org.openjfx javafx-fxml - 12.0.2 + 14 org.openjfx javafx-media - 12.0.2 + 14 + + + + uk.co.caprica + vlcj + 4.4.0 @@ -69,13 +75,13 @@ org.apache.logging.log4j log4j-api - 2.13.0 + 2.13.1 org.apache.logging.log4j log4j-core - 2.13.0 + 2.13.1 @@ -114,7 +120,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.1 + 3.2.2 Project-HomeFlix true diff --git a/src/main/java/kellerkinder/HomeFlix/application/Main.java b/src/main/java/kellerkinder/HomeFlix/application/Main.java index da229a7..5e1a79a 100644 --- a/src/main/java/kellerkinder/HomeFlix/application/Main.java +++ b/src/main/java/kellerkinder/HomeFlix/application/Main.java @@ -46,6 +46,8 @@ public class Main extends Application { public static final String version = "0.8.0"; public static final String buildNumber = "173"; public static final String versionName = "toothless dragon"; + + // TODO rename streamURL to mediaURL @Override public void start(Stage primaryStage) throws IOException { diff --git a/src/main/java/kellerkinder/HomeFlix/player/HFMediaPlayerEventListener.java b/src/main/java/kellerkinder/HomeFlix/player/HFMediaPlayerEventListener.java new file mode 100644 index 0000000..0bd74fb --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/player/HFMediaPlayerEventListener.java @@ -0,0 +1,172 @@ +/** + * Project-HomeFlix + * + * Copyright 2016-2020 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + */ + +package kellerkinder.HomeFlix.player; + +import uk.co.caprica.vlcj.media.MediaRef; +import uk.co.caprica.vlcj.media.TrackType; +import uk.co.caprica.vlcj.player.base.MediaPlayer; +import uk.co.caprica.vlcj.player.base.MediaPlayerEventListener; + +public class HFMediaPlayerEventListener implements MediaPlayerEventListener { + + @Override + public void mediaChanged(MediaPlayer mediaPlayer, MediaRef media) { + // Auto-generated method stub + } + + @Override + public void opening(MediaPlayer mediaPlayer) { + // Auto-generated method stub + } + + @Override + public void buffering(MediaPlayer mediaPlayer, float newCache) { + // Auto-generated method stub + } + + @Override + public void playing(MediaPlayer mediaPlayer) { + // Auto-generated method stub + } + + @Override + public void paused(MediaPlayer mediaPlayer) { + // Auto-generated method stub + } + + @Override + public void stopped(MediaPlayer mediaPlayer) { + // Auto-generated method stub + } + + @Override + public void forward(MediaPlayer mediaPlayer) { + // Auto-generated method stub + } + + @Override + public void backward(MediaPlayer mediaPlayer) { + // Auto-generated method stub + } + + @Override + public void finished(MediaPlayer mediaPlayer) { + // Auto-generated method stub + } + + @Override + public void timeChanged(MediaPlayer mediaPlayer, long newTime) { + // Auto-generated method stub + } + + @Override + public void positionChanged(MediaPlayer mediaPlayer, float newPosition) { + // Auto-generated method stub + } + + @Override + public void seekableChanged(MediaPlayer mediaPlayer, int newSeekable) { + // Auto-generated method stub + } + + @Override + public void pausableChanged(MediaPlayer mediaPlayer, int newPausable) { + // Auto-generated method stub + } + + @Override + public void titleChanged(MediaPlayer mediaPlayer, int newTitle) { + // Auto-generated method stub + } + + @Override + public void snapshotTaken(MediaPlayer mediaPlayer, String filename) { + // Auto-generated method stub + } + + @Override + public void lengthChanged(MediaPlayer mediaPlayer, long newLength) { + // Auto-generated method stub + } + + @Override + public void videoOutput(MediaPlayer mediaPlayer, int newCount) { + // Auto-generated method stub + } + + @Override + public void scrambledChanged(MediaPlayer mediaPlayer, int newScrambled) { + // Auto-generated method stub + } + + @Override + public void elementaryStreamAdded(MediaPlayer mediaPlayer, TrackType type, int id) { + // Auto-generated method stub + } + + @Override + public void elementaryStreamDeleted(MediaPlayer mediaPlayer, TrackType type, int id) { + // Auto-generated method stub + } + + @Override + public void elementaryStreamSelected(MediaPlayer mediaPlayer, TrackType type, int id) { + // Auto-generated method stub + } + + @Override + public void corked(MediaPlayer mediaPlayer, boolean corked) { + // Auto-generated method stub + } + + @Override + public void muted(MediaPlayer mediaPlayer, boolean muted) { + // Auto-generated method stub + } + + @Override + public void volumeChanged(MediaPlayer mediaPlayer, float volume) { + // Auto-generated method stub + } + + @Override + public void audioDeviceChanged(MediaPlayer mediaPlayer, String audioDevice) { + // Auto-generated method stub + } + + @Override + public void chapterChanged(MediaPlayer mediaPlayer, int newChapter) { + // Auto-generated method stub + } + + @Override + public void error(MediaPlayer mediaPlayer) { + // Auto-generated method stub + } + + @Override + public void mediaPlayerReady(MediaPlayer mediaPlayer) { + // Auto-generated method stub + } + +} diff --git a/src/main/java/kellerkinder/HomeFlix/player/NewMediaPlayer.java b/src/main/java/kellerkinder/HomeFlix/player/NewMediaPlayer.java new file mode 100644 index 0000000..c6952a0 --- /dev/null +++ b/src/main/java/kellerkinder/HomeFlix/player/NewMediaPlayer.java @@ -0,0 +1,115 @@ +package kellerkinder.HomeFlix.player; + +import java.nio.ByteBuffer; + +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.image.PixelBuffer; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.WritableImage; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import kellerkinder.HomeFlix.application.Main; +import uk.co.caprica.vlcj.factory.MediaPlayerFactory; +import uk.co.caprica.vlcj.player.base.MediaPlayer; +import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer; +import uk.co.caprica.vlcj.player.embedded.videosurface.CallbackVideoSurface; +import uk.co.caprica.vlcj.player.embedded.videosurface.VideoSurfaceAdapters; +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormat; +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormatCallback; +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.RenderCallback; +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.format.RV32BufferFormat; + +public class NewMediaPlayer { + + private MediaPlayerFactory mediaPlayerFactory; + private EmbeddedMediaPlayer embeddedMediaPlayer; + private WritableImage videoImage; + private PixelBuffer videoPixelBuffer; + private ImageView videoImageView; + + private Stage stage; + private StackPane pane; + private Scene scene; + + public NewMediaPlayer() { + mediaPlayerFactory = new MediaPlayerFactory(); + embeddedMediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer(); + embeddedMediaPlayer.videoSurface().set(new FXCallbackVideoSurface()); + } + + public void init() { + // Auto-generated method stub + stage = new Stage(); + pane = new StackPane(); + scene = new Scene(pane); + + videoImageView = new ImageView(); + videoImageView.setPreserveRatio(true); + videoImageView.fitWidthProperty().bind(pane.widthProperty()); + videoImageView.fitHeightProperty().bind(pane.heightProperty()); + pane.getChildren().add(videoImageView); + + stage.setScene(scene); + stage.setTitle("HomeFlix"); + stage.getIcons().add(new Image(Main.class.getResourceAsStream("/icons/Homeflix_Icon_64x64.png"))); + stage.setOnCloseRequest(event -> { + //DBController.getInstance().setCurrentTime(streamURL, playerController.getCurrentTime()); + //playerController.getMediaPlayer().stop(); + stop(); + stage.close(); + }); + stage.show(); + } + + public void play(String streamURL) { + embeddedMediaPlayer.media().play(streamURL); + } + + public void stop() { + embeddedMediaPlayer.controls().stop(); + embeddedMediaPlayer.release(); + mediaPlayerFactory.release(); + System.out.println("released"); + } + + private class FXCallbackVideoSurface extends CallbackVideoSurface { + FXCallbackVideoSurface() { + super(new FXBufferFormatCallback(), new FXRenderCallback(), true, + VideoSurfaceAdapters.getVideoSurfaceAdapter()); + } + } + + private class FXBufferFormatCallback implements BufferFormatCallback { + private int sourceWidth; + private int sourceHeight; + + @Override + public BufferFormat getBufferFormat(int sourceWidth, int sourceHeight) { + this.sourceWidth = sourceWidth; + this.sourceHeight = sourceHeight; + return new RV32BufferFormat(sourceWidth, sourceHeight); + } + + @Override + public void allocatedBuffers(ByteBuffer[] buffers) { + assert buffers[0].capacity() == sourceWidth * sourceHeight * 4; + PixelFormat pixelFormat = PixelFormat.getByteBgraPreInstance(); + videoPixelBuffer = new PixelBuffer<>(sourceWidth, sourceHeight, buffers[0], pixelFormat); + videoImage = new WritableImage(videoPixelBuffer); + videoImageView.setImage(videoImage); + } + } + + private class FXRenderCallback implements RenderCallback { + @Override + public void display(MediaPlayer mediaPlayer, ByteBuffer[] nativeBuffers, BufferFormat bufferFormat) { + Platform.runLater(() -> { + videoPixelBuffer.updateBuffer(pb -> null); + }); + } + } + +} diff --git a/src/main/java/kellerkinder/HomeFlix/player/Player.java b/src/main/java/kellerkinder/HomeFlix/player/Player.java index 4d3c297..6544a51 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/Player.java +++ b/src/main/java/kellerkinder/HomeFlix/player/Player.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2016-2019 <@Seil0> + * Copyright 2016-2020 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,9 +36,10 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Pane; import javafx.stage.Stage; import kellerkinder.HomeFlix.application.Main; -import kellerkinder.HomeFlix.controller.DBController; + public class Player { private PlayerController playerController; @@ -54,21 +55,20 @@ public class Player { * @param currentTableFilm the currently selected film */ public Player(String streamURL) { - - if (isSupportedFormat(streamURL)) { - hfPlayer(streamURL); - } else { + try { + newHFPlayer(streamURL); + } catch (Exception e) { + LOGGER.error("Error while playing media", e); legacyPlayer(streamURL); } - } /** * start the integrated player * @param streamURL */ - private void hfPlayer(String streamURL) { - playerController = new PlayerController(this, streamURL); + private void newHFPlayer(String mediaURL) { + playerController = new PlayerController(this, mediaURL); try { FXMLLoader fxmlLoader = new FXMLLoader(); @@ -81,15 +81,14 @@ public class Player { stage.setTitle("HomeFlix"); stage.getIcons().add(new Image(Main.class.getResourceAsStream("/icons/Homeflix_Icon_64x64.png"))); stage.setOnCloseRequest(event -> { - DBController.getInstance().setCurrentTime(streamURL, playerController.getCurrentTime()); - playerController.getMediaPlayer().stop(); + playerController.stop2(); stage.close(); }); - - playerController.init(); - - stage.setFullScreen(true); + //stage.setFullScreen(true); stage.show(); + + playerController.init2(); + playerController.start2(); } catch (Exception e) { e.printStackTrace(); } @@ -158,4 +157,8 @@ public class Player { return scene; } + public Pane getPane() { + return pane; + } + } diff --git a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java index 39eed19..bc131a4 100644 --- a/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java +++ b/src/main/java/kellerkinder/HomeFlix/player/PlayerController.java @@ -1,7 +1,7 @@ /** * Project-HomeFlix * - * Copyright 2016-2019 <@Seil0> + * Copyright 2016-2020 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,15 +22,14 @@ package kellerkinder.HomeFlix.player; -import java.io.File; +import java.nio.ByteBuffer; import java.util.Timer; import java.util.TimerTask; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXSlider; -import javafx.beans.binding.Bindings; -import javafx.beans.property.DoubleProperty; +import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; @@ -39,21 +38,34 @@ import javafx.fxml.FXML; import javafx.scene.Cursor; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.image.PixelBuffer; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.WritableImage; import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.media.Media; import javafx.scene.media.MediaPlayer; -import javafx.scene.media.MediaPlayer.Status; import javafx.scene.media.MediaView; import javafx.util.Duration; + import kellerkinder.HomeFlix.controller.DBController; import kellerkinder.HomeFlix.controller.XMLController; import kellerkinder.HomeFlix.datatypes.FilmTabelDataType; +import uk.co.caprica.vlcj.factory.MediaPlayerFactory; +import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer; +import uk.co.caprica.vlcj.player.embedded.videosurface.CallbackVideoSurface; +import uk.co.caprica.vlcj.player.embedded.videosurface.VideoSurfaceAdapters; +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormat; +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormatCallback; +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.RenderCallback; +import uk.co.caprica.vlcj.player.embedded.videosurface.callback.format.RV32BufferFormat; + public class PlayerController { @FXML private MediaView mediaView; + @FXML private ImageView videoImageView; @FXML private VBox bottomVBox; @@ -72,13 +84,19 @@ public class PlayerController { private Player player; private Media media; - private MediaPlayer mediaPlayer; + private MediaPlayer mediaPlayer2; + + private MediaPlayerFactory mediaPlayerFactory; + private EmbeddedMediaPlayer embeddedMediaPlayer; + private WritableImage videoImage; + private PixelBuffer videoPixelBuffer; private FilmTabelDataType film; - private double currentTime = 0; - private double seekTime = 0; - private double startTime = 0; - private double duration = 0; + private long currentTime = 0; + private long seekTime = 0; + private long startTime = 0; + private long duration = 0; + private int season = 0; private int episode = 0; private int countdown = 0; @@ -100,7 +118,62 @@ public class PlayerController { public PlayerController(Player player, String streamURL) { this.player = player; this.film = DBController.getInstance().getStream(streamURL); + + mediaPlayerFactory = new MediaPlayerFactory(); + embeddedMediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer(); + embeddedMediaPlayer.videoSurface().set(new FXCallbackVideoSurface()); } + + public void init2() { + // initialize the image view + videoImageView.setPreserveRatio(true); + videoImageView.fitWidthProperty().bind(player.getStage().widthProperty()); + videoImageView.fitHeightProperty().bind(player.getStage().heightProperty()); + + // set needed variables + startTime = (long) DBController.getInstance().getCurrentTime(film.getStreamUrl()); + autoplay = XMLController.isAutoplay(); + season = !film.getSeason().isEmpty() ? Integer.parseInt(film.getSeason()) : 0; + episode = !film.getEpisode().isEmpty() ? Integer.parseInt(film.getEpisode()) : 0; + + initActions2(); + } + + private void initActions2() { + embeddedMediaPlayer.events().addMediaPlayerEventListener( new HFMediaPlayerEventListener() { + + @Override + public void timeChanged(uk.co.caprica.vlcj.player.base.MediaPlayer mediaPlayer, long newTime) { + timeSlider.setValue((newTime / 1000) / 60); + } + + @Override + public void error(uk.co.caprica.vlcj.player.base.MediaPlayer mediaPlayer) { + // Auto-generated method stub + + } + + @Override + public void lengthChanged(uk.co.caprica.vlcj.player.base.MediaPlayer mediaPlayer, long newLength) { + duration = newLength; + timeSlider.setMax((duration / 1000) / 60); + } + + }); + } + + public void start2() { + embeddedMediaPlayer.media().play(film.getStreamUrl()); + embeddedMediaPlayer.controls().skipTime((long) startTime); + } + + public void stop2() { + DBController.getInstance().setCurrentTime(film.getStreamUrl(), embeddedMediaPlayer.status().time()); + embeddedMediaPlayer.controls().stop(); + embeddedMediaPlayer.release(); + mediaPlayerFactory.release(); + } + /** * initialize the PlayerWindow @@ -108,54 +181,27 @@ public class PlayerController { public void init() { initActions(); - if (film.getStreamUrl().startsWith("http")) { - media = new Media(film.getStreamUrl()); - } else { - media = new Media(new File(film.getStreamUrl()).toURI().toString()); - } - - // create the MediaPlayer object - mediaPlayer = new MediaPlayer(media); - mediaView.setPreserveRatio(true); - mediaView.setMediaPlayer(mediaPlayer); - - final DoubleProperty width = mediaView.fitWidthProperty(); - final DoubleProperty height = mediaView.fitHeightProperty(); - - width.bind(Bindings.selectDouble(mediaView.sceneProperty(), "width")); - height.bind(Bindings.selectDouble(mediaView.sceneProperty(), "height")); - - startTime = DBController.getInstance().getCurrentTime(film.getStreamUrl()); - autoplay = XMLController.isAutoplay(); - season = !film.getSeason().isEmpty() ? Integer.parseInt(film.getSeason()) : 0; - episode = !film.getEpisode().isEmpty() ? Integer.parseInt(film.getEpisode()) : 0; - initMediaPlayer(); - - // set the control elements to the correct value - playIcon.setImage(pause); - fullscreenIcon.setImage(fullscreenExit); - timeSlider.setValue(0); } private void initMediaPlayer() { // start the media if the player is ready - mediaPlayer.setOnReady(new Runnable() { + mediaPlayer2.setOnReady(new Runnable() { @Override public void run() { - duration = media.getDuration().toMillis(); + duration = (long) media.getDuration().toMillis(); timeSlider.setMax((duration / 1000) / 60); - mediaPlayer.play(); - mediaPlayer.seek(Duration.millis(startTime)); + mediaPlayer2.play(); + mediaPlayer2.seek(Duration.millis(startTime)); } }); // every time the play time changes execute this - mediaPlayer.currentTimeProperty().addListener(new ChangeListener() { + mediaPlayer2.currentTimeProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, Duration oldValue, Duration newValue) { - currentTime = newValue.toMillis(); // set the current time + currentTime = (long) newValue.toMillis(); // set the current time double timeToEnd = (duration - currentTime); if (timeToEnd < 20000 && episode != 0 && autoplay) { @@ -176,7 +222,7 @@ public class PlayerController { } } else if (timeToEnd < 120) { // if we are 120ms to the end stop the media - mediaPlayer.stop(); + mediaPlayer2.stop(); DBController.getInstance().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time playIcon.setImage(playArrow); } else { @@ -232,7 +278,7 @@ public class PlayerController { timeSlider.setOnMouseReleased(new EventHandler() { @Override public void handle(MouseEvent event) { - mediaPlayer.seek(new Duration(seekTime)); + mediaPlayer2.seek(new Duration(seekTime)); mousePressed = false; } }); @@ -248,15 +294,14 @@ public class PlayerController { timeSlider.valueProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue ov, Number old_val, Number new_val) { - seekTime = (double) new_val * 1000 * 60; + seekTime = (long) ((double) new_val * 1000 * 60); } }); } @FXML void stopBtnAction(ActionEvent event) { - DBController.getInstance().setCurrentTime(film.getStreamUrl(), currentTime); - mediaPlayer.stop(); + stop2(); player.getStage().close(); } @@ -273,11 +318,11 @@ public class PlayerController { @FXML void playBtnAction(ActionEvent event) { - if (mediaPlayer.getStatus().equals(Status.PLAYING)) { - mediaPlayer.pause(); + if (embeddedMediaPlayer.status().isPlaying()) { + embeddedMediaPlayer.controls().pause(); playIcon.setImage(playArrow); } else { - mediaPlayer.play(); + embeddedMediaPlayer.controls().play(); playIcon.setImage(pause); } } @@ -292,7 +337,7 @@ public class PlayerController { DBController.getInstance().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time FilmTabelDataType nextFilm = DBController.getInstance().getNextEpisode(film.getTitle(), episode, season); if (nextFilm != null) { - mediaPlayer.stop(); + mediaPlayer2.stop(); film = nextFilm; init(); autoplay = true; @@ -300,11 +345,52 @@ public class PlayerController { } public MediaPlayer getMediaPlayer() { - return mediaPlayer; + return mediaPlayer2; } public double getCurrentTime() { return currentTime; } + + private class FXCallbackVideoSurface extends CallbackVideoSurface { + FXCallbackVideoSurface() { + super(new FXBufferFormatCallback(), new FXRenderCallback(), true, + VideoSurfaceAdapters.getVideoSurfaceAdapter()); + } + } + + private class FXBufferFormatCallback implements BufferFormatCallback { + private int sourceWidth; + private int sourceHeight; + + @Override + public BufferFormat getBufferFormat(int sourceWidth, int sourceHeight) { + this.sourceWidth = sourceWidth; + this.sourceHeight = sourceHeight; + return new RV32BufferFormat(sourceWidth, sourceHeight); + } + + @Override + public void allocatedBuffers(ByteBuffer[] buffers) { + assert buffers[0].capacity() == sourceWidth * sourceHeight * 4; + PixelFormat pixelFormat = PixelFormat.getByteBgraPreInstance(); + videoPixelBuffer = new PixelBuffer<>(sourceWidth, sourceHeight, buffers[0], pixelFormat); + videoImage = new WritableImage(videoPixelBuffer); + videoImageView.setImage(videoImage); + } + } + + private class FXRenderCallback implements RenderCallback { + + @Override + public void display(uk.co.caprica.vlcj.player.base.MediaPlayer mediaPlayer, ByteBuffer[] nativeBuffers, + BufferFormat bufferFormat) { + Platform.runLater(() -> { + videoPixelBuffer.updateBuffer(pb -> null); + }); + } + } + + } diff --git a/src/main/resources/fxml/PlayerWindow.fxml b/src/main/resources/fxml/PlayerWindow.fxml index 40a58b4..2e89abe 100644 --- a/src/main/resources/fxml/PlayerWindow.fxml +++ b/src/main/resources/fxml/PlayerWindow.fxml @@ -16,6 +16,7 @@ + @@ -39,7 +40,7 @@ - +