543 lines
16 KiB
Java
543 lines
16 KiB
Java
/**
|
|
* Project-HomeFlix
|
|
*
|
|
* Copyright 2016-2020 <seil0@mosad.xyz>
|
|
*
|
|
* 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 org.mosad.homeflix.player;
|
|
|
|
import java.nio.ByteBuffer;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Timer;
|
|
import java.util.TimerTask;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import org.mosad.homeflix.controller.DBController;
|
|
import org.mosad.homeflix.controller.XMLController;
|
|
import org.mosad.homeflix.datatypes.FilmTabelDataType;
|
|
|
|
import com.jfoenix.controls.JFXButton;
|
|
import com.jfoenix.controls.JFXDialogLayout;
|
|
import com.jfoenix.controls.JFXListView;
|
|
import com.jfoenix.controls.JFXPopup;
|
|
import com.jfoenix.controls.JFXPopup.PopupHPosition;
|
|
import com.jfoenix.controls.JFXPopup.PopupVPosition;
|
|
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.geometry.Insets;
|
|
import javafx.scene.Cursor;
|
|
import javafx.scene.control.Label;
|
|
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.AnchorPane;
|
|
import javafx.scene.layout.HBox;
|
|
import javafx.scene.layout.StackPane;
|
|
import javafx.scene.layout.VBox;
|
|
import javafx.scene.paint.Color;
|
|
import javafx.scene.text.Text;
|
|
import javafx.util.StringConverter;
|
|
import uk.co.caprica.vlcj.factory.MediaPlayerFactory;
|
|
import uk.co.caprica.vlcj.media.Media;
|
|
import uk.co.caprica.vlcj.media.MediaEventAdapter;
|
|
import uk.co.caprica.vlcj.media.MediaParsedStatus;
|
|
import uk.co.caprica.vlcj.player.base.MediaPlayer;
|
|
import uk.co.caprica.vlcj.player.base.TrackDescription;
|
|
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;
|
|
|
|
/**
|
|
* The PlayerController class is the main component of the HomeFlix Player.
|
|
* It uses vlcj to play videos and some modified jfoenix GUI components to
|
|
* create a Netflix like GUI.
|
|
*
|
|
* @author seil0
|
|
*
|
|
* TODO this class needs heavy cleaning
|
|
*/
|
|
public class PlayerController {
|
|
|
|
@FXML private AnchorPane panePlayer;
|
|
@FXML private ImageView videoImageView;
|
|
|
|
@FXML private HBox hBoxTop;
|
|
@FXML private HBox controlsHBox;
|
|
|
|
@FXML private VBox bottomVBox;
|
|
|
|
@FXML private JFXSlider timeSlider;
|
|
|
|
@FXML private JFXButton btnBack;
|
|
@FXML private JFXButton btnPlay;
|
|
@FXML private JFXButton btnReplay;
|
|
@FXML private JFXButton btnForward;
|
|
@FXML private JFXButton btnAudio;
|
|
@FXML private JFXButton btnFullscreen;
|
|
@FXML private JFXButton btnNextEpisode;
|
|
|
|
@FXML private ImageView stopIcon;
|
|
@FXML private ImageView playIcon;
|
|
@FXML private ImageView fullscreenIcon;
|
|
|
|
@FXML private Label lblTitle;
|
|
@FXML private Label lblEndTime;
|
|
|
|
private Player player;
|
|
private MediaPlayerFactory mediaPlayerFactory;
|
|
private EmbeddedMediaPlayer embeddedMediaPlayer;
|
|
private WritableImage videoImage;
|
|
private PixelBuffer<ByteBuffer> videoPixelBuffer;
|
|
|
|
private FilmTabelDataType media;
|
|
private long startTime = 0;
|
|
private long currentTime = 0;
|
|
private long endTime = 0;
|
|
private long skipTime = 0;
|
|
private long duration = 0;
|
|
private List<TrackDescription> tracks = new ArrayList<>();
|
|
private int currentTrack = 0;
|
|
|
|
private int season = 0;
|
|
private int episode = 0;
|
|
|
|
private boolean mousePressed = false;
|
|
private boolean showControls = true;
|
|
private boolean autoplay;
|
|
|
|
private Image playArrow = new Image("icons/baseline_play_arrow_white_48dp.png");
|
|
private Image pause = new Image("icons/baseline_pause_white_48dp.png");
|
|
private Image fullscreen = new Image("icons/baseline_fullscreen_white_48dp.png");
|
|
private Image fullscreenExit = new Image("icons/baseline_fullscreen_exit_white_48dp.png");
|
|
|
|
private JFXPopup audioPopup;
|
|
|
|
// fix wrong buffer resolution
|
|
private int videoWidth = 0;
|
|
private int videoHeigth = 0;
|
|
|
|
/**
|
|
* create a new PlayerWindow object
|
|
* @param player the player object (needed for closing action)
|
|
* @param film the film object
|
|
*/
|
|
public PlayerController(Player player, String mediaURL) {
|
|
this.player = player;
|
|
this.media = DBController.getInstance().getStream(mediaURL);
|
|
|
|
mediaPlayerFactory = new MediaPlayerFactory();
|
|
embeddedMediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer();
|
|
embeddedMediaPlayer.videoSurface().set(new FXCallbackVideoSurface());
|
|
}
|
|
|
|
public void init() {
|
|
// 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(media.getStreamUrl());
|
|
autoplay = XMLController.isAutoplay();
|
|
season = !media.getSeason().isEmpty() ? Integer.parseInt(media.getSeason()) : 0;
|
|
episode = !media.getEpisode().isEmpty() ? Integer.parseInt(media.getEpisode()) : 0;
|
|
|
|
if (episode > 0) {
|
|
// if the media is a TV show, add season + episode to the title
|
|
lblTitle.setText(media.getTitle() + " S" + season + ":E" + episode);
|
|
} else {
|
|
lblTitle.setText(media.getTitle());
|
|
}
|
|
|
|
initPlayerWindow();
|
|
initMediaPlayer();
|
|
initTimeSlider();
|
|
}
|
|
|
|
/**
|
|
* initialize some PlayerWindow GUI-Elements actions
|
|
*/
|
|
private void initPlayerWindow() {
|
|
|
|
player.getScene().addEventFilter(MouseEvent.MOUSE_MOVED, new EventHandler<MouseEvent>() {
|
|
|
|
// hide controls timer initialization
|
|
final Timer timer = new Timer();
|
|
TimerTask controlAnimationTask = null; // task to execute save operation
|
|
final long delayTime = 4000; // hide the controls after 2 seconds
|
|
|
|
@Override
|
|
public void handle(MouseEvent mouseEvent) {
|
|
|
|
// show controls
|
|
if (!showControls) {
|
|
player.getScene().setCursor(Cursor.DEFAULT);
|
|
hBoxTop.setVisible(true);
|
|
bottomVBox.setVisible(true);
|
|
}
|
|
|
|
// hide controls
|
|
if (controlAnimationTask != null)
|
|
controlAnimationTask.cancel();
|
|
|
|
controlAnimationTask = new TimerTask() {
|
|
@Override
|
|
public void run() {
|
|
// TODO a animation would be nice
|
|
hBoxTop.setVisible(false);
|
|
bottomVBox.setVisible(false);
|
|
player.getScene().setCursor(Cursor.NONE);
|
|
showControls = false;
|
|
}
|
|
};
|
|
timer.schedule(controlAnimationTask, delayTime);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* initialize the embedded media player
|
|
*/
|
|
private void initMediaPlayer() {
|
|
embeddedMediaPlayer.events().addMediaPlayerEventListener( new HFMediaPlayerEventListener() {
|
|
|
|
@Override
|
|
public void mediaPlayerReady(MediaPlayer mediaPlayer) {
|
|
tracks = mediaPlayer.audio().trackDescriptions();
|
|
currentTrack = mediaPlayer.audio().track();
|
|
}
|
|
|
|
@Override
|
|
public void timeChanged(MediaPlayer mediaPlayer, long newTime) {
|
|
currentTime = newTime;
|
|
endTime = duration - newTime;
|
|
|
|
Platform.runLater(() -> {
|
|
updateControls();
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void error(MediaPlayer mediaPlayer) {
|
|
// Auto-generated method stub
|
|
}
|
|
|
|
@Override
|
|
public void lengthChanged(MediaPlayer mediaPlayer, long newLength) {
|
|
duration = newLength;
|
|
timeSlider.setMax(duration / 1000);
|
|
}
|
|
|
|
});
|
|
|
|
embeddedMediaPlayer.events().addMediaEventListener(new MediaEventAdapter() {
|
|
@Override
|
|
public void mediaParsedChanged(Media media, MediaParsedStatus newStatus) {
|
|
super.mediaParsedChanged(media, newStatus);
|
|
videoWidth = embeddedMediaPlayer.video().videoDimension().width;
|
|
videoHeigth= embeddedMediaPlayer.video().videoDimension().height;
|
|
|
|
// start the video
|
|
embeddedMediaPlayer.media().play(media.newMediaRef());
|
|
embeddedMediaPlayer.controls().skipTime((long) startTime); // skip to the start time
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* initialize the time slider
|
|
*/
|
|
private void initTimeSlider() {
|
|
// if the mouse on the timeSlider is released, skip to the new position
|
|
timeSlider.setOnMouseReleased(new EventHandler<MouseEvent>() {
|
|
@Override
|
|
public void handle(MouseEvent event) {
|
|
embeddedMediaPlayer.controls().skipTime(skipTime);
|
|
mousePressed = false;
|
|
}
|
|
});
|
|
|
|
timeSlider.setOnMousePressed(new EventHandler<MouseEvent>() {
|
|
@Override
|
|
public void handle(MouseEvent event) {
|
|
mousePressed = true;
|
|
}
|
|
});
|
|
|
|
// on value change, get the new skip time
|
|
timeSlider.valueProperty().addListener(new ChangeListener<Number>() {
|
|
@Override
|
|
public void changed(ObservableValue<? extends Number> ov, Number old_val, Number new_val) {
|
|
skipTime = ((new_val.longValue() * 1000) - currentTime);
|
|
//System.out.println(timeSlider.getChildrenUnmodifiable());
|
|
}
|
|
});
|
|
|
|
timeSlider.setOnMouseMoved(new EventHandler<MouseEvent>() {
|
|
@Override
|
|
public void handle(MouseEvent event) {
|
|
//System.out.println("TEST");
|
|
}
|
|
});
|
|
|
|
// show h:mm:ss in the animated thumb
|
|
StringConverter<Double> convert = new StringConverter<Double>() {
|
|
@Override
|
|
public String toString(Double object) {
|
|
long time = object.longValue();
|
|
|
|
return String.format("%d:%02d:%02d", TimeUnit.SECONDS.toHours(time) % 24,
|
|
TimeUnit.SECONDS.toMinutes(time) % 60, TimeUnit.SECONDS.toSeconds(time) % 60);
|
|
|
|
}
|
|
|
|
@Override
|
|
public Double fromString(String string) {
|
|
return null;
|
|
}
|
|
};
|
|
timeSlider.setLabelFormatter(convert);
|
|
|
|
// get the animated thumb as StackPane
|
|
StackPane animatedThumb = (StackPane) timeSlider.lookup(".animated-thumb");
|
|
// System.out.println(animatedThumb);
|
|
// System.out.println(animatedThumb.getChildren());
|
|
|
|
// modify the animated thumb
|
|
Text thumbText = (Text) animatedThumb.getChildren().get(0);
|
|
thumbText.setStyle("-fx-font-size: 15px; -fx-fill: white;");
|
|
|
|
// TODO add a preview to the animated thumb, if that's possible
|
|
// ImageView iv = new ImageView(fullscreenExit);
|
|
// animatedThumb.getChildren().add(iv);
|
|
}
|
|
|
|
// Start the parsing the media meta data
|
|
public void start() {
|
|
// parse meta data
|
|
embeddedMediaPlayer.media().prepare(media.getStreamUrl());
|
|
embeddedMediaPlayer.media().parsing().parse();
|
|
}
|
|
|
|
/**
|
|
* Stop and release the media player.
|
|
* Always call this method to stop the media player.
|
|
*/
|
|
public void stop() {
|
|
DBController.getInstance().setCurrentTime(media.getStreamUrl(), embeddedMediaPlayer.status().time());
|
|
embeddedMediaPlayer.controls().stop();
|
|
embeddedMediaPlayer.release();
|
|
mediaPlayerFactory.release();
|
|
}
|
|
|
|
/**
|
|
* call this every second to update all timers
|
|
*/
|
|
private void updateControls() {
|
|
// update slider position, if the mouse does not press on the time
|
|
if (!mousePressed) {
|
|
timeSlider.setValue(currentTime / 1000);
|
|
}
|
|
|
|
// update endTime label
|
|
lblEndTime.setText(String.format("%d:%02d:%02d",
|
|
TimeUnit.MILLISECONDS.toHours(endTime) % 24,
|
|
TimeUnit.MILLISECONDS.toMinutes(endTime) % 60,
|
|
TimeUnit.MILLISECONDS.toSeconds(endTime) % 60));
|
|
|
|
|
|
// show the next episode button 30 seconds before the end of a episode
|
|
if (endTime < 31000 && episode != 0 && autoplay) {
|
|
int countdown = (int) ((endTime / 1000) - 20); // a 10 seconds countdown
|
|
|
|
if (!btnNextEpisode.isVisible()) {
|
|
btnNextEpisode.setVisible(true);
|
|
}
|
|
|
|
if (endTime > 20000) {
|
|
btnNextEpisode.setText(XMLController.getLocalBundle().getString("nextEpisode")
|
|
+ countdown + XMLController.getLocalBundle().getString("seconds"));
|
|
bottomVBox.setVisible(true);
|
|
} else {
|
|
btnNextEpisode.setVisible(false);
|
|
playNextMedia();
|
|
}
|
|
}
|
|
}
|
|
|
|
@FXML
|
|
void btnBackAction(ActionEvent event) {
|
|
stop();
|
|
player.getStage().close();
|
|
}
|
|
|
|
@FXML
|
|
void btnPlayAction(ActionEvent event) {
|
|
if (embeddedMediaPlayer.status().isPlaying()) {
|
|
embeddedMediaPlayer.controls().pause();
|
|
playIcon.setImage(playArrow);
|
|
} else {
|
|
embeddedMediaPlayer.controls().play();
|
|
playIcon.setImage(pause);
|
|
}
|
|
}
|
|
|
|
@FXML
|
|
void btnReplayAction(ActionEvent event) {
|
|
embeddedMediaPlayer.controls().skipTime(-10000);
|
|
}
|
|
|
|
@FXML
|
|
void btnForwardAction(ActionEvent event) {
|
|
embeddedMediaPlayer.controls().skipTime(10000);
|
|
}
|
|
|
|
@FXML
|
|
void btnAudioAction(ActionEvent event) {
|
|
if (audioPopup == null) {
|
|
audioPopup = new JFXPopup();
|
|
|
|
JFXListView<String> list = new JFXListView<String>();
|
|
tracks.forEach(track -> {
|
|
list.getItems().add(track.description());
|
|
});
|
|
|
|
list.getSelectionModel().select(currentTrack);
|
|
list.setOnMouseClicked(ev -> {
|
|
setAudioTrack(list.getSelectionModel().getSelectedIndex());
|
|
audioPopup.hide();
|
|
});
|
|
// TODO style the JFXListView
|
|
|
|
Text heading = new Text("Audio");
|
|
heading.setFill(Color.WHITE);
|
|
|
|
JFXDialogLayout content = new JFXDialogLayout();
|
|
content.setPrefSize(150, 200);
|
|
content.setHeading(heading);
|
|
content.setBody(list);
|
|
content.setPadding(new Insets(-20, -20, -20, -20)); // fix JFXDialogLayout padding
|
|
content.setSpacing(-10); // fix JFXDialogLayout spacing
|
|
|
|
audioPopup.setPopupContent(content);
|
|
}
|
|
|
|
if (!audioPopup.isShowing()) {
|
|
// TODO this does not work properly
|
|
audioPopup.show(btnAudio, PopupVPosition.BOTTOM, PopupHPosition.RIGHT,
|
|
0, -1 * bottomVBox.getHeight());
|
|
} else {
|
|
audioPopup.hide();
|
|
}
|
|
}
|
|
|
|
@FXML
|
|
void btnFullscreenAction(ActionEvent event) {
|
|
player.getStage().setFullScreen(!player.getStage().isFullScreen());
|
|
fullscreenIcon.setImage(player.getStage().isFullScreen() ? fullscreenExit : fullscreen);
|
|
}
|
|
|
|
@FXML
|
|
void btnNextEpisodeAction(ActionEvent event) {
|
|
btnNextEpisode.setVisible(false);
|
|
playNextMedia();
|
|
}
|
|
|
|
/**
|
|
* play the next media
|
|
*/
|
|
private void playNextMedia() {
|
|
autoplay = false;
|
|
DBController.getInstance().setCurrentTime(media.getStreamUrl(), 0); // reset old video start time
|
|
FilmTabelDataType nextMedia = DBController.getInstance().getNextEpisode(media.getTitle(), episode, season);
|
|
if (nextMedia != null) {
|
|
embeddedMediaPlayer.media().play(nextMedia.getStreamUrl());
|
|
media = nextMedia;
|
|
autoplay = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* change the audio track
|
|
* @param track the index of the audio track
|
|
*/
|
|
private void setAudioTrack(int track) {
|
|
embeddedMediaPlayer.audio().setTrack(track);
|
|
currentTrack = track;
|
|
}
|
|
|
|
public double getCurrentTime() {
|
|
return currentTime;
|
|
}
|
|
|
|
private class FXCallbackVideoSurface extends CallbackVideoSurface {
|
|
FXCallbackVideoSurface() {
|
|
super(new FXBufferFormatCallback(), new FXRenderCallback(), true,
|
|
VideoSurfaceAdapters.getVideoSurfaceAdapter());
|
|
}
|
|
}
|
|
|
|
private class FXBufferFormatCallback implements BufferFormatCallback {
|
|
int sourceWidth;
|
|
int sourceHeight;
|
|
|
|
@Override
|
|
public BufferFormat getBufferFormat(int sourceWidth, int sourceHeight) {
|
|
return new RV32BufferFormat(sourceWidth, sourceHeight);
|
|
}
|
|
|
|
@Override
|
|
public void allocatedBuffers(ByteBuffer[] buffers) {
|
|
PixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteBgraPreInstance();
|
|
|
|
// fixes buffer resolution, video resolution mismatch
|
|
videoPixelBuffer = new PixelBuffer<>(videoWidth, videoHeigth, buffers[0], pixelFormat);
|
|
//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));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
}
|