forked from j62/ctbrec
1
0
Fork 0

Move stream preview to its own control

Move stream preview to its own control, so that it can be used in
the ThumbCell, too
This commit is contained in:
0xboobface 2018-12-15 15:55:17 +01:00
parent 465e417b6c
commit d09aad1bf6
2 changed files with 188 additions and 158 deletions

View File

@ -1,39 +1,23 @@
package ctbrec.ui; package ctbrec.ui;
import java.io.InterruptedIOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.ui.controls.StreamPreview;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Point2D; import javafx.geometry.Point2D;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow; import javafx.scene.control.TableRow;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.stage.Popup; import javafx.stage.Popup;
public class PreviewPopupHandler implements EventHandler<MouseEvent> { public class PreviewPopupHandler implements EventHandler<MouseEvent> {
@ -44,53 +28,24 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
private long timeForPopupClose = 400; private long timeForPopupClose = 400;
private Popup popup = new Popup(); private Popup popup = new Popup();
private Node parent; private Node parent;
private ImageView preview = new ImageView(); private StreamPreview streamPreview;
private MediaView videoPreview;
private MediaPlayer videoPlayer;
private Media video;
private JavaFxModel model; private JavaFxModel model;
private volatile long openCountdown = -1; private volatile long openCountdown = -1;
private volatile long closeCountdown = -1; private volatile long closeCountdown = -1;
private volatile long lastModelChange = -1; private volatile long lastModelChange = -1;
private volatile boolean changeModel = false; private volatile boolean changeModel = false;
private ExecutorService executor = Executors.newSingleThreadExecutor();
private Future<?> future;
private ProgressIndicator progressIndicator;
private StackPane pane;
public PreviewPopupHandler(Node parent) { public PreviewPopupHandler(Node parent) {
this.parent = parent; this.parent = parent;
videoPreview = new MediaView(); streamPreview = new StreamPreview();
videoPreview.setFitWidth(Config.getInstance().getSettings().thumbWidth); streamPreview.setStyle("-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;"+
videoPreview.setFitHeight(videoPreview.getFitWidth() * 9 / 16);
videoPreview.setPreserveRatio(true);
StackPane.setMargin(videoPreview, new Insets(5));
preview.setFitWidth(Config.getInstance().getSettings().thumbWidth);
preview.setPreserveRatio(true);
preview.setSmooth(true);
preview.setStyle("-fx-background-radius: 10px, 10px, 10px, 10px;");
preview.visibleProperty().bind(videoPreview.visibleProperty().not());
StackPane.setMargin(preview, new Insets(5));
progressIndicator = new ProgressIndicator();
progressIndicator.setVisible(false);
progressIndicator.prefWidthProperty().bind(videoPreview.fitWidthProperty());
Region veil = new Region();
veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.8)");
veil.visibleProperty().bind(progressIndicator.visibleProperty());
StackPane.setMargin(veil, new Insets(5));
pane = new StackPane();
pane.getChildren().addAll(preview, videoPreview, veil, progressIndicator);
pane.setStyle("-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;"+
"-fx-background-insets: 0 0 -1 0, 0, 1, 2;" + "-fx-background-insets: 0 0 -1 0, 0, 1, 2;" +
"-fx-background-radius: 10px, 10px, 10px, 10px;" + "-fx-background-radius: 10px, 10px, 10px, 10px;" +
"-fx-padding: 1;" + "-fx-padding: 1;" +
"-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0);"); "-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0);");
popup.getContent().add(pane); popup.getContent().add(streamPreview);
StackPane.setMargin(streamPreview, new Insets(5));
createTimerThread(); createTimerThread();
} }
@ -121,8 +76,7 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
if(modelChanged) { if(modelChanged) {
lastModelChange = System.currentTimeMillis(); lastModelChange = System.currentTimeMillis();
changeModel = true; changeModel = true;
future.cancel(true); streamPreview.stop();
progressIndicator.setVisible(true);
} }
} else { } else {
openCountdown = timeForPopupOpen; openCountdown = timeForPopupOpen;
@ -173,121 +127,19 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
} }
private void startStream(JavaFxModel model) { private void startStream(JavaFxModel model) {
if(future != null && !future.isDone()) {
future.cancel(true);
}
future = executor.submit(() -> {
try {
Platform.runLater(() -> {
progressIndicator.setVisible(true);
popup.show(parent.getScene().getWindow());
});
List<StreamSource> sources = model.getStreamSources();
Collections.sort(sources);
StreamSource best = sources.get(0);
checkInterrupt();
LOG.debug("Preview url for {} is {}", model.getName(), best.getMediaPlaylistUrl());
video = new Media(best.getMediaPlaylistUrl());
if(videoPlayer != null) {
videoPlayer.dispose();
}
videoPlayer = new MediaPlayer(video);
videoPlayer.setMute(true);
checkInterrupt();
videoPlayer.setOnReady(() -> {
if(!future.isCancelled()) {
Platform.runLater(() -> {
double aspect = (double)video.getWidth() / video.getHeight();
double w = Config.getInstance().getSettings().thumbWidth;
double h = w / aspect;
resize(w, h);
progressIndicator.setVisible(false);
videoPreview.setVisible(true);
videoPreview.setMediaPlayer(videoPlayer);
videoPlayer.play();
});
}
});
videoPlayer.setOnError(() -> onError(videoPlayer));
} catch (IllegalStateException e) {
if(e.getMessage().equals("Stream url unknown")) {
// fine hls url for mfc not known yet
} else {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
}
showTestImage();
} catch (HttpException e) {
if(e.getResponseCode() != 404) {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
}
showTestImage();
} catch (InterruptedException | InterruptedIOException e) {
// future has been canceled, that's fine
} catch (ExecutionException e) {
if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) {
// future has been canceled, that's fine
} else {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
showTestImage();
}
} catch (Exception e) {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
showTestImage();
}
});
}
private void onError(MediaPlayer videoPlayer) {
LOG.error("Error while starting preview stream", videoPlayer.getError());
if(videoPlayer.getError().getCause() != null) {
LOG.error("Error while starting preview stream root cause:", videoPlayer.getError().getCause());
}
videoPlayer.dispose();
Platform.runLater(() -> { Platform.runLater(() -> {
showTestImage(); streamPreview.startStream(model);
popup.show(parent.getScene().getWindow());
}); });
}
private void resize(double w, double h) {
preview.setFitWidth(w);
preview.setFitHeight(h);
videoPreview.setFitWidth(w);
videoPreview.setFitHeight(h);
pane.setPrefSize(w, h);
popup.setWidth(w);
popup.setHeight(h);
}
private void checkInterrupt() throws InterruptedException {
if(Thread.interrupted()) {
throw new InterruptedException();
}
}
private void showTestImage() {
Platform.runLater(() -> {
videoPreview.setVisible(false);
Image img = new Image(getClass().getResource("/image_not_found.png").toString(), true);
preview.setImage(img);
double aspect = img.getWidth() / img.getHeight();
double w = Config.getInstance().getSettings().thumbWidth;
double h = w / aspect;
resize(w, h);
progressIndicator.setVisible(false);
});
} }
private void hidePopup() { private void hidePopup() {
if(future != null && !future.isDone()) {
future.cancel(true);
}
Platform.runLater(() -> { Platform.runLater(() -> {
popup.setX(-1000); popup.setX(-1000);
popup.setY(-1000); popup.setY(-1000);
popup.hide(); popup.hide();
if(videoPlayer != null) { streamPreview.stop();
videoPlayer.dispose();
}
}); });
} }

View File

@ -0,0 +1,178 @@
package ctbrec.ui.controls;
import java.io.InterruptedIOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
public class StreamPreview extends StackPane {
private static final transient Logger LOG = LoggerFactory.getLogger(StreamPreview.class);
private ImageView preview = new ImageView();
private MediaView videoPreview;
private MediaPlayer videoPlayer;
private Media video;
private ProgressIndicator progressIndicator;
private ExecutorService executor = Executors.newSingleThreadExecutor();
private Future<?> future;
public StreamPreview() {
videoPreview = new MediaView();
videoPreview.setFitWidth(Config.getInstance().getSettings().thumbWidth);
videoPreview.setFitHeight(videoPreview.getFitWidth() * 9 / 16);
videoPreview.setPreserveRatio(true);
StackPane.setMargin(videoPreview, new Insets(5));
preview.setFitWidth(Config.getInstance().getSettings().thumbWidth);
preview.setPreserveRatio(true);
preview.setSmooth(true);
preview.setStyle("-fx-background-radius: 10px, 10px, 10px, 10px;");
preview.visibleProperty().bind(videoPreview.visibleProperty().not());
StackPane.setMargin(preview, new Insets(5));
progressIndicator = new ProgressIndicator();
progressIndicator.setVisible(false);
progressIndicator.prefWidthProperty().bind(videoPreview.fitWidthProperty());
Region veil = new Region();
veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.8)");
veil.visibleProperty().bind(progressIndicator.visibleProperty());
StackPane.setMargin(veil, new Insets(5));
getChildren().addAll(preview, videoPreview, veil, progressIndicator);
}
public void startStream(Model model) {
if(future != null && !future.isDone()) {
future.cancel(true);
}
future = executor.submit(() -> {
try {
Platform.runLater(() -> {
progressIndicator.setVisible(true);
});
List<StreamSource> sources = model.getStreamSources();
Collections.sort(sources);
StreamSource best = sources.get(0);
checkInterrupt();
LOG.debug("Preview url for {} is {}", model.getName(), best.getMediaPlaylistUrl());
video = new Media(best.getMediaPlaylistUrl());
if(videoPlayer != null) {
videoPlayer.dispose();
}
videoPlayer = new MediaPlayer(video);
videoPlayer.setMute(true);
checkInterrupt();
videoPlayer.setOnReady(() -> {
if(!future.isCancelled()) {
Platform.runLater(() -> {
double aspect = (double)video.getWidth() / video.getHeight();
double w = Config.getInstance().getSettings().thumbWidth;
double h = w / aspect;
resizeToFitContent(w, h);
progressIndicator.setVisible(false);
videoPreview.setVisible(true);
videoPreview.setMediaPlayer(videoPlayer);
videoPlayer.play();
});
}
});
videoPlayer.setOnError(() -> onError(videoPlayer));
} catch (IllegalStateException e) {
if(e.getMessage().equals("Stream url unknown")) {
// fine hls url for mfc not known yet
} else {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
}
showTestImage();
} catch (HttpException e) {
if(e.getResponseCode() != 404) {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
}
showTestImage();
} catch (InterruptedException | InterruptedIOException e) {
// future has been canceled, that's fine
} catch (ExecutionException e) {
if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) {
// future has been canceled, that's fine
} else {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
showTestImage();
}
} catch (Exception e) {
LOG.warn("Couldn't start preview video: {}", e.getMessage());
showTestImage();
}
});
}
private void resizeToFitContent(double w, double h) {
setPrefSize(w, h);
preview.setFitWidth(w);
preview.setFitHeight(h);
videoPreview.setFitWidth(w);
videoPreview.setFitHeight(h);
}
public void stop() {
if(future != null && !future.isDone()) {
future.cancel(true);
}
Platform.runLater(() -> {
if(videoPlayer != null) {
videoPlayer.dispose();
}
});
}
private void onError(MediaPlayer videoPlayer) {
LOG.error("Error while starting preview stream", videoPlayer.getError());
if(videoPlayer.getError().getCause() != null) {
LOG.error("Error while starting preview stream root cause:", videoPlayer.getError().getCause());
}
videoPlayer.dispose();
Platform.runLater(() -> {
showTestImage();
});
}
private void showTestImage() {
Platform.runLater(() -> {
videoPreview.setVisible(false);
Image img = new Image(getClass().getResource("/image_not_found.png").toString(), true);
preview.setImage(img);
double aspect = img.getWidth() / img.getHeight();
double w = Config.getInstance().getSettings().thumbWidth;
double h = w / aspect;
resizeToFitContent(w, h);
progressIndicator.setVisible(false);
});
}
private void checkInterrupt() throws InterruptedException {
if(Thread.interrupted()) {
throw new InterruptedException();
}
}
}