From d09aad1bf630ed8acf7f1e5a18863c0c110fb2af Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 15 Dec 2018 15:55:17 +0100 Subject: [PATCH] Move stream preview to its own control Move stream preview to its own control, so that it can be used in the ThumbCell, too --- .../java/ctbrec/ui/PreviewPopupHandler.java | 168 +---------------- .../ctbrec/ui/controls/StreamPreview.java | 178 ++++++++++++++++++ 2 files changed, 188 insertions(+), 158 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/controls/StreamPreview.java diff --git a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java index e6ffc72a..16de2224 100644 --- a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java +++ b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java @@ -1,39 +1,23 @@ package ctbrec.ui; -import java.io.InterruptedIOException; -import java.util.Collections; -import java.util.List; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ctbrec.Config; -import ctbrec.io.HttpException; -import ctbrec.recorder.download.StreamSource; +import ctbrec.ui.controls.StreamPreview; import javafx.application.Platform; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.scene.Node; -import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; -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; import javafx.stage.Popup; public class PreviewPopupHandler implements EventHandler { @@ -44,53 +28,24 @@ public class PreviewPopupHandler implements EventHandler { private long timeForPopupClose = 400; private Popup popup = new Popup(); private Node parent; - private ImageView preview = new ImageView(); - private MediaView videoPreview; - private MediaPlayer videoPlayer; - private Media video; + private StreamPreview streamPreview; private JavaFxModel model; private volatile long openCountdown = -1; private volatile long closeCountdown = -1; private volatile long lastModelChange = -1; private volatile boolean changeModel = false; - private ExecutorService executor = Executors.newSingleThreadExecutor(); - private Future future; - private ProgressIndicator progressIndicator; - private StackPane pane; public PreviewPopupHandler(Node parent) { this.parent = parent; - 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)); - - pane = new StackPane(); - pane.getChildren().addAll(preview, videoPreview, veil, progressIndicator); - pane.setStyle("-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;"+ + streamPreview = new StreamPreview(); + streamPreview.setStyle("-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;"+ "-fx-background-insets: 0 0 -1 0, 0, 1, 2;" + "-fx-background-radius: 10px, 10px, 10px, 10px;" + "-fx-padding: 1;" + "-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(); } @@ -121,8 +76,7 @@ public class PreviewPopupHandler implements EventHandler { if(modelChanged) { lastModelChange = System.currentTimeMillis(); changeModel = true; - future.cancel(true); - progressIndicator.setVisible(true); + streamPreview.stop(); } } else { openCountdown = timeForPopupOpen; @@ -173,121 +127,19 @@ public class PreviewPopupHandler implements EventHandler { } 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 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(() -> { - 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() { - if(future != null && !future.isDone()) { - future.cancel(true); - } Platform.runLater(() -> { popup.setX(-1000); popup.setY(-1000); popup.hide(); - if(videoPlayer != null) { - videoPlayer.dispose(); - } + streamPreview.stop(); }); } diff --git a/client/src/main/java/ctbrec/ui/controls/StreamPreview.java b/client/src/main/java/ctbrec/ui/controls/StreamPreview.java new file mode 100644 index 00000000..258ec42b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/StreamPreview.java @@ -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 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(); + } + } +}