forked from j62/ctbrec
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:
parent
465e417b6c
commit
d09aad1bf6
|
@ -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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue