/** * 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 java.nio.ByteBuffer; import java.util.Timer; import java.util.TimerTask; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXSlider; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.event.EventHandler; 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.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; @FXML private HBox controlsHBox; @FXML private JFXSlider timeSlider; @FXML private JFXButton stopBtn; @FXML private JFXButton playBtn; @FXML private JFXButton fullscreenBtn; @FXML private JFXButton nextEpBtn; @FXML private ImageView stopIcon; @FXML private ImageView playIcon; @FXML private ImageView fullscreenIcon; private Player player; private Media media; private MediaPlayer mediaPlayer2; private MediaPlayerFactory mediaPlayerFactory; private EmbeddedMediaPlayer embeddedMediaPlayer; private WritableImage videoImage; private PixelBuffer videoPixelBuffer; private FilmTabelDataType film; 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; private boolean mousePressed = false; private boolean showControls = true; private boolean autoplay; private Image playArrow = new Image("icons/baseline_play_arrow_black_48dp.png"); private Image pause = new Image("icons/baseline_pause_black_48dp.png"); private Image fullscreen = new Image("icons/baseline_fullscreen_black_48dp.png"); private Image fullscreenExit = new Image("icons/baseline_fullscreen_exit_black_48dp.png"); /** * create a new PlayerWindow object * @param mainWCon the MainWindowController TODO do we need this? * @param player the player object (needed for closing action) * @param film the film object */ 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 */ public void init() { initActions(); initMediaPlayer(); } private void initMediaPlayer() { // start the media if the player is ready mediaPlayer2.setOnReady(new Runnable() { @Override public void run() { duration = (long) media.getDuration().toMillis(); timeSlider.setMax((duration / 1000) / 60); mediaPlayer2.play(); mediaPlayer2.seek(Duration.millis(startTime)); } }); // every time the play time changes execute this mediaPlayer2.currentTimeProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, Duration oldValue, Duration newValue) { currentTime = (long) newValue.toMillis(); // set the current time double timeToEnd = (duration - currentTime); if (timeToEnd < 20000 && episode != 0 && autoplay) { // show 20 seconds before the end a button (next episode in 10 seconds) if (!nextEpBtn.isVisible()) nextEpBtn.setVisible(true); if (countdown != (int) ((timeToEnd - 10000) / 1000)) { countdown = (int) ((timeToEnd - 10000) / 1000); nextEpBtn.setText("next episode in " + countdown + " seconds"); // TODO translate bottomVBox.setVisible(true); } // if we are end time -10 seconds, do autoplay, if activated if (timeToEnd < 10000) { nextEpBtn.setVisible(false); autoPlayNewFilm(); } } else if (timeToEnd < 120) { // if we are 120ms to the end stop the media mediaPlayer2.stop(); DBController.getInstance().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time playIcon.setImage(playArrow); } else { if (nextEpBtn.isVisible()) nextEpBtn.setVisible(false); } if (!mousePressed) { timeSlider.setValue((currentTime / 1000) / 60); } } }); } /** * initialize some PlayerWindow GUI-Elements actions */ private void initActions() { player.getScene().addEventFilter(MouseEvent.MOUSE_MOVED, new EventHandler() { // hide controls timer initialization final Timer timer = new Timer(); TimerTask controlAnimationTask = null; // task to execute save operation final long delayTime = 3000; // hide the controls after 2 seconds @Override public void handle(MouseEvent mouseEvent) { // show controls if (!showControls) { player.getScene().setCursor(Cursor.DEFAULT); bottomVBox.setVisible(true); } // hide controls if (controlAnimationTask != null) controlAnimationTask.cancel(); controlAnimationTask = new TimerTask() { @Override public void run() { bottomVBox.setVisible(false); player.getScene().setCursor(Cursor.NONE); showControls = false; } }; timer.schedule(controlAnimationTask, delayTime); } }); // if the mouse on the timeSlider is released seek to the new position timeSlider.setOnMouseReleased(new EventHandler() { @Override public void handle(MouseEvent event) { mediaPlayer2.seek(new Duration(seekTime)); mousePressed = false; } }); timeSlider.setOnMousePressed(new EventHandler() { @Override public void handle(MouseEvent event) { mousePressed = true; } }); // get the new seek time timeSlider.valueProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue ov, Number old_val, Number new_val) { seekTime = (long) ((double) new_val * 1000 * 60); } }); } @FXML void stopBtnAction(ActionEvent event) { stop2(); player.getStage().close(); } @FXML void fullscreenBtnAction(ActionEvent event) { if (player.getStage().isFullScreen()) { player.getStage().setFullScreen(false); fullscreenIcon.setImage(fullscreen); } else { player.getStage().setFullScreen(true); fullscreenIcon.setImage(fullscreenExit); } } @FXML void playBtnAction(ActionEvent event) { if (embeddedMediaPlayer.status().isPlaying()) { embeddedMediaPlayer.controls().pause(); playIcon.setImage(playArrow); } else { embeddedMediaPlayer.controls().play(); playIcon.setImage(pause); } } @FXML void nextEpBtnAction(ActionEvent event) { autoPlayNewFilm(); } private void autoPlayNewFilm() { autoplay = false; DBController.getInstance().setCurrentTime(film.getStreamUrl(), 0); // reset old video start time FilmTabelDataType nextFilm = DBController.getInstance().getNextEpisode(film.getTitle(), episode, season); if (nextFilm != null) { mediaPlayer2.stop(); film = nextFilm; init(); autoplay = true; } } public MediaPlayer getMediaPlayer() { 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); }); } } }