From 6e25f98b2b65e7eae6b9c44a2eb7baf50e4e2dae Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 13 Jun 2020 20:08:25 +0200 Subject: [PATCH] Add range slider for the recording resolution --- .../ui/controls/range/DiscreteRange.java | 46 +++ .../ui/controls/range/LabeledNumberAxis.java | 49 +++ .../java/ctbrec/ui/controls/range/Range.java | 10 + .../ctbrec/ui/controls/range/RangeSlider.java | 88 ++++++ .../controls/range/RangeSliderBehavior.java | 52 ++++ .../ui/controls/range/RangeSliderSkin.java | 283 ++++++++++++++++++ .../ctbrec/ui/controls/range/rangeslider.css | 78 +++++ .../java/ctbrec/ui/settings/SettingsTab.java | 54 ++-- common/src/main/java/ctbrec/Settings.java | 1 + .../recorder/download/dash/DashDownload.java | 3 +- .../download/hls/AbstractHlsDownload.java | 23 +- .../ctbrec/recorder/server/ConfigServlet.java | 1 + 12 files changed, 646 insertions(+), 42 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/controls/range/DiscreteRange.java create mode 100644 client/src/main/java/ctbrec/ui/controls/range/LabeledNumberAxis.java create mode 100644 client/src/main/java/ctbrec/ui/controls/range/Range.java create mode 100644 client/src/main/java/ctbrec/ui/controls/range/RangeSlider.java create mode 100644 client/src/main/java/ctbrec/ui/controls/range/RangeSliderBehavior.java create mode 100644 client/src/main/java/ctbrec/ui/controls/range/RangeSliderSkin.java create mode 100644 client/src/main/java/ctbrec/ui/controls/range/rangeslider.css diff --git a/client/src/main/java/ctbrec/ui/controls/range/DiscreteRange.java b/client/src/main/java/ctbrec/ui/controls/range/DiscreteRange.java new file mode 100644 index 00000000..cc80a0c3 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/range/DiscreteRange.java @@ -0,0 +1,46 @@ +package ctbrec.ui.controls.range; + +import java.util.List; + +public class DiscreteRange implements Range { + + private List values; + private List labels; + + + public DiscreteRange(List values, List labels) { + this.values = values; + this.labels = labels; + if (values == null) { + throw new IllegalArgumentException("Values cannot be null"); + } + if (labels == null) { + throw new IllegalArgumentException("Labels cannot be null"); + } + + } + + @Override + public T getMinimum() { + return values.get(0); + } + + @Override + public T getMaximum() { + return values.get(values.size()-1); + } + + @Override + public List getTicks() { + return values; + } + + public List getLabels() { + return labels; + } + + @Override + public String toString() { + return labels.toString(); + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/range/LabeledNumberAxis.java b/client/src/main/java/ctbrec/ui/controls/range/LabeledNumberAxis.java new file mode 100644 index 00000000..fe61590b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/range/LabeledNumberAxis.java @@ -0,0 +1,49 @@ +package ctbrec.ui.controls.range; + +import java.util.Collections; +import java.util.List; + +import javafx.scene.chart.ValueAxis; + +public class LabeledNumberAxis extends ValueAxis { + + private DiscreteRange range; + + public LabeledNumberAxis(DiscreteRange range) { + this.range = range; + } + + @Override + protected List calculateMinorTickMarks() { + return Collections.emptyList(); + } + + @SuppressWarnings("unchecked") + @Override + protected void setRange(Object range, boolean animate) { + if (!(range instanceof DiscreteRange)) { + throw new IllegalArgumentException("Range has to be of type DiscreteRange"); + } + this.range = (DiscreteRange) range; + } + + @Override + protected Object getRange() { + return range; + } + + @Override + protected List calculateTickValues(double length, Object range) { + if (!(range instanceof Range)) { + throw new IllegalArgumentException("Range has to be of type ctbrec.ui.controls.range.Range"); + } + @SuppressWarnings("unchecked") + Range discreteRange = (Range) range; + return discreteRange.getTicks(); + } + + @Override + protected String getTickMarkLabel(Number value) { + return range.getLabels().get(value.intValue()).toString(); + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/range/Range.java b/client/src/main/java/ctbrec/ui/controls/range/Range.java new file mode 100644 index 00000000..9ba36ec9 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/range/Range.java @@ -0,0 +1,10 @@ +package ctbrec.ui.controls.range; + +import java.util.List; + +public interface Range { + + public T getMinimum(); + public T getMaximum(); + public List getTicks(); +} diff --git a/client/src/main/java/ctbrec/ui/controls/range/RangeSlider.java b/client/src/main/java/ctbrec/ui/controls/range/RangeSlider.java new file mode 100644 index 00000000..78d98b21 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/range/RangeSlider.java @@ -0,0 +1,88 @@ +package ctbrec.ui.controls.range; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.geometry.Orientation; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; + +public class RangeSlider extends Control { + + private static final String DEFAULT_STYLE_CLASS = "rangeslider"; + + private Range range; + private DoubleProperty low; + private DoubleProperty high; + private boolean showTickMarks = false; + private boolean showTickLabels = false; + private Orientation orientation = Orientation.HORIZONTAL; + + public RangeSlider(Range range) { + this.range = range; + low = new SimpleDoubleProperty(getMinimum().doubleValue()); + high = new SimpleDoubleProperty(getMaximum().doubleValue()); + getStyleClass().setAll(DEFAULT_STYLE_CLASS); + } + + @Override + protected Skin createDefaultSkin() { + return new RangeSliderSkin(this, new RangeSliderBehavior<>(this)); + } + + @Override + public String getUserAgentStylesheet() { + return RangeSlider.class.getResource("rangeslider.css").toExternalForm(); + } + + public DoubleProperty getLow() { + return low; + } + + public void setLow(T newPosition) { + low.set(newPosition.doubleValue()); + } + + public DoubleProperty getHigh() { + return high; + } + + public void setHigh(T newPosition) { + this.high.set(newPosition.doubleValue()); + } + + public T getMinimum() { + return range.getMinimum(); + } + + public T getMaximum() { + return range.getMaximum(); + } + + public boolean isShowTickMarks() { + return showTickMarks; + } + + public void setShowTickMarks(boolean showTickMarks) { + this.showTickMarks = showTickMarks; + } + + public boolean isShowTickLabels() { + return showTickLabels; + } + + public void setShowTickLabels(boolean showTickLabels) { + this.showTickLabels = showTickLabels; + } + + public Orientation getOrientation() { + return orientation; + } + + public void setOrientation(Orientation orientation) { + this.orientation = orientation; + } + + public Range getRange() { + return range; + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/range/RangeSliderBehavior.java b/client/src/main/java/ctbrec/ui/controls/range/RangeSliderBehavior.java new file mode 100644 index 00000000..9a15b9f1 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/range/RangeSliderBehavior.java @@ -0,0 +1,52 @@ +package ctbrec.ui.controls.range; + +import java.util.List; + +import com.sun.javafx.scene.control.behavior.BehaviorBase; // NOSONAR +import com.sun.javafx.scene.control.inputmap.InputMap; // NOSONAR + +public class RangeSliderBehavior extends BehaviorBase> { + + private RangeSlider rangeSlider; + + public RangeSliderBehavior(RangeSlider rangeSlider) { + super(rangeSlider); + this.rangeSlider = rangeSlider; + } + + @Override + public InputMap> getInputMap() { + InputMap> inputMap = new InputMap<>(rangeSlider); + return inputMap; + } + + /** + * @param position + * The mouse position on track with 0.0 being beginning of track and 1.0 being the end + */ + public void lowThumbDragged(double position) { + rangeSlider.setLow(getNewPosition(position)); + } + + /** + * @param position + * The mouse position on track with 0.0 being beginning of track and 1.0 being the end + */ + public void highThumbDragged(double position) { + rangeSlider.setHigh(getNewPosition(position)); + } + + /** + * Calculate the new position of the thumb given the clicked/dragged position + * + * @param position + * clicked position + * @return new position + */ + private T getNewPosition(double position) { + List ticks = rangeSlider.getRange().getTicks(); + double percentPerTick = 1d / (ticks.size() - 1); + int index = (int) Math.round(position / percentPerTick); + return ticks.get(index); + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/range/RangeSliderSkin.java b/client/src/main/java/ctbrec/ui/controls/range/RangeSliderSkin.java new file mode 100644 index 00000000..42d1715d --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/range/RangeSliderSkin.java @@ -0,0 +1,283 @@ +package ctbrec.ui.controls.range; + +import javafx.geometry.Orientation; +import javafx.geometry.Point2D; +import javafx.geometry.Side; +import javafx.scene.chart.Axis; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.ValueAxis; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.StackPane; + +public class RangeSliderSkin extends SkinBase> { + + private static final double TRACK_TO_TICK_GAP = 2; + private Axis tickLine = null; + private StackPane track; + private ThumbRange thumbRange = new ThumbRange(); + private RangeSliderBehavior behavior; + private double thumbWidth; + private double thumbHeight; + private boolean showTickMarks; + private double trackStart; + private double trackLength; + private double lowThumbPos; + + private double preDragPos; // used as a temp value for low and high thumbsRange + private Point2D preDragThumbPoint; // in skin coordinates + + protected RangeSliderSkin(RangeSlider control, RangeSliderBehavior behavior) { + super(control); + this.behavior = behavior; + initTrack(); + initThumbs(thumbRange); + + ; + registerChangeListener(control.getLow(), (obsVal) -> getSkinnable().requestLayout()); + registerChangeListener(control.getHigh(), (obsVal) -> getSkinnable().requestLayout()); + } + + private void initThumbs(ThumbRange t) { + setShowTickMarks(getSkinnable().isShowTickMarks(), getSkinnable().isShowTickLabels()); + + getChildren().addAll(t.low, t.high, t.rangeBar); + + t.low.setOnMousePressed(me -> { + preDragThumbPoint = t.low.localToParent(me.getX(), me.getY()); + preDragPos = (getSkinnable().getLow().doubleValue() - getSkinnable().getMinimum().doubleValue()) / (getMaxMinusMinNoZero()); + }); + + + t.low.setOnMouseDragged(me -> { + Point2D cur = t.low.localToParent(me.getX(), me.getY()); + double dragPos = (isHorizontal()) ? cur.getX() - preDragThumbPoint.getX() : -(cur.getY() - preDragThumbPoint.getY()); + behavior.lowThumbDragged(preDragPos + dragPos / trackLength); + }); + + t.high.setOnMousePressed(me -> { + preDragThumbPoint = t.high.localToParent(me.getX(), me.getY()); + preDragPos = (getSkinnable().getHigh().doubleValue() - getSkinnable().getMinimum().doubleValue()) / (getMaxMinusMinNoZero()); + }); + + + t.high.setOnMouseDragged(me -> { + boolean orientation = getSkinnable().getOrientation() == Orientation.HORIZONTAL; + double trackLen = orientation ? track.getWidth() : track.getHeight(); + Point2D cur = t.high.localToParent(me.getX(), me.getY()); + double dragPos = getSkinnable().getOrientation() != Orientation.HORIZONTAL ? -(cur.getY() - preDragThumbPoint.getY()) : cur.getX() - preDragThumbPoint.getX(); + behavior.highThumbDragged(preDragPos + dragPos / trackLen); + + }); + } + + private boolean isHorizontal() { + return getSkinnable().getOrientation() == Orientation.HORIZONTAL; + } + + private void initTrack() { + track = new StackPane(); + track.getStyleClass().setAll("track"); + + getChildren().clear(); + getChildren().add(track); + } + + private void setShowTickMarks(boolean ticksVisible, boolean labelsVisible) { + showTickMarks = (ticksVisible || labelsVisible); + var rangeSlider = getSkinnable(); + if (showTickMarks) { + if (tickLine == null) { + var range = rangeSlider.getRange(); + tickLine = createAxis(range, ticksVisible, labelsVisible); + getChildren().addAll(tickLine); + } else { + tickLine.setTickLabelsVisible(labelsVisible); + tickLine.setTickMarkVisible(ticksVisible); + } + } + + getSkinnable().requestLayout(); + } + + @SuppressWarnings("unchecked") + private Axis createAxis(Range range, boolean ticksVisible, boolean labelsVisible) { + ValueAxis axis; + if (range instanceof DiscreteRange) { + axis = new LabeledNumberAxis((DiscreteRange) range); + } else { + axis = new NumberAxis(); + } + + axis.setUpperBound(range.getMaximum().doubleValue()); + axis.setLowerBound(range.getMinimum().doubleValue()); + axis.setMinorTickVisible(false); + axis.setMinorTickCount(0); + axis.setAutoRanging(false); + axis.setAnimated(false); + axis.setSide(isHorizontal() ? Side.BOTTOM : Side.RIGHT); + axis.setTickMarkVisible(ticksVisible); + axis.setTickLabelsVisible(labelsVisible); + return axis; + } + + @Override + protected void layoutChildren(final double x, final double y, final double w, final double h) { + if (thumbRange != null) { + thumbWidth = thumbRange.low.prefWidth(-1); + thumbHeight = thumbRange.low.prefHeight(-1); + thumbRange.low.resize(thumbWidth, thumbHeight); + thumbRange.high.resize(thumbWidth, thumbHeight); + } + + // we are assuming the is common radius's for all corners on the track + double trackRadius = track.getBackground() == null ? 0 + : !track.getBackground().getFills().isEmpty() ? track.getBackground().getFills().get(0).getRadii().getTopLeftHorizontalRadius() : 0; + + double tickLineHeight = (showTickMarks) ? tickLine.prefHeight(-1) : 0; + double trackHeight = 5;// track.prefHeight(-1); + double trackAreaHeight = Math.max(trackHeight, thumbHeight); + double totalHeightNeeded = trackAreaHeight + ((showTickMarks) ? TRACK_TO_TICK_GAP + tickLineHeight : 0); + double startY = y + ((h - totalHeightNeeded) / 2); // center slider in available height vertically + + trackLength = w - thumbWidth; + trackStart = x + (thumbWidth / 2); + + double trackTop = (int) (startY + ((trackAreaHeight - trackHeight) / 2)); + lowThumbPos = (int) (startY + ((trackAreaHeight - thumbHeight) / 2)); + + // layout track + track.resizeRelocate(trackStart - trackRadius, trackTop, trackLength + trackRadius + trackRadius, trackHeight); + + positionThumbs(); + + if (showTickMarks) { + tickLine.setLayoutX(trackStart); + tickLine.setLayoutY(trackTop + trackHeight + TRACK_TO_TICK_GAP); + tickLine.resize(trackLength, tickLineHeight); + tickLine.requestAxisLayout(); + } else { + if (tickLine != null) { + tickLine.resize(0, 0); + tickLine.requestAxisLayout(); + } + tickLine = null; + } + } + + private void positionThumbs() { + RangeSlider s = getSkinnable(); + + double lxl = trackStart + (trackLength * ((s.getLow().doubleValue() - s.getMinimum().doubleValue()) / (getMaxMinusMinNoZero())) - thumbWidth / 2D); + double lxh = trackStart + (trackLength * ((s.getHigh().doubleValue() - s.getMinimum().doubleValue()) / (getMaxMinusMinNoZero())) - thumbWidth / 2D); + double ly = lowThumbPos; + + if (thumbRange != null) { + thumbRange.low.setLayoutX(lxl); + thumbRange.low.setLayoutY(ly); + + thumbRange.high.setLayoutX(lxh); + thumbRange.high.setLayoutY(ly); + + thumbRange.rangeBar.resizeRelocate(thumbRange.low.getLayoutX() + thumbRange.low.getWidth(), track.getLayoutY(), + thumbRange.high.getLayoutX() - thumbRange.low.getLayoutX() - thumbRange.low.getWidth(), track.getHeight()); + } + } + + private double minTrackLength() { + return 2 * ((thumbRange != null) ? thumbRange.low.prefWidth(-1) : 1); + } + + @Override + protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + if (isHorizontal()) { + return (leftInset + minTrackLength() + ((thumbRange != null) ? thumbRange.low.prefWidth(-1) : 1) + rightInset); + } else { + return (leftInset + ((thumbRange != null) ? thumbRange.low.prefWidth(-1) : 1) + rightInset); + } + } + + @Override + protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + if (isHorizontal()) { + return (topInset + ((thumbRange != null) ? thumbRange.low.prefHeight(-1) : 1) + bottomInset); + } else { + return (topInset + minTrackLength() + ((thumbRange != null) ? thumbRange.low.prefHeight(-1) : 1) + bottomInset); + } + } + + @Override + protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + if (isHorizontal()) { + if (showTickMarks) { + double w = Math.max(140, tickLine.prefWidth(-1)); + System.err.println("computePrefWidth " + w); + return w; + } else { + return 140; + } + } else { + return leftInset + Math.max(((thumbRange != null) ? thumbRange.low.prefWidth(-1) : 1), track.prefWidth(-1)) + + ((showTickMarks) ? (TRACK_TO_TICK_GAP + tickLine.prefWidth(-1)) : 0) + rightInset; + } + } + + @Override + protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + if (isHorizontal()) { + return getSkinnable().getInsets().getTop() + Math.max(((thumbRange != null) ? thumbRange.low.prefHeight(-1) : 1), track.prefHeight(-1)) + + ((showTickMarks) ? (TRACK_TO_TICK_GAP + tickLine.prefHeight(-1)) : 0) + bottomInset; + } else { + if (showTickMarks) { + return Math.max(140, tickLine.prefHeight(-1)); + } else { + return 140; + } + } + } + + @Override + protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + if (isHorizontal()) { + return Double.MAX_VALUE; + } else { + return getSkinnable().prefWidth(-1); + } + } + + @Override + protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + if (isHorizontal()) { + return getSkinnable().prefHeight(width); + } else { + return Double.MAX_VALUE; + } + } + + private double getMaxMinusMinNoZero() { + RangeSlider s = getSkinnable(); + return s.getMaximum().doubleValue() - s.getMinimum().doubleValue() == 0 ? 1 : s.getMaximum().doubleValue() - s.getMinimum().doubleValue(); + } + + private static class ThumbPane extends StackPane { + } + + private static class ThumbRange { + ThumbPane low; + ThumbPane high; + StackPane rangeBar; + + ThumbRange() { + low = new ThumbPane(); + low.getStyleClass().setAll("low-thumb"); + low.setFocusTraversable(false); + + high = new ThumbPane(); + high.getStyleClass().setAll("high-thumb"); + high.setFocusTraversable(false); + + rangeBar = new StackPane(); + rangeBar.getStyleClass().setAll("range-bar"); + rangeBar.setFocusTraversable(false); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/range/rangeslider.css b/client/src/main/java/ctbrec/ui/controls/range/rangeslider.css new file mode 100644 index 00000000..4f04487a --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/range/rangeslider.css @@ -0,0 +1,78 @@ +.rangeslider .low-thumb, +.rangeslider .high-thumb { + -fx-background-color: + linear-gradient(to bottom, derive(-fx-text-box-border, -20%), derive(-fx-text-box-border, -30%)), + -fx-inner-border, + -fx-body-color; + -fx-background-insets: 0, 1, 2; + -fx-background-radius: 1.0em; /* makes sure this remains circular */ + -fx-padding: 0.583333em; /* 7 */ + -fx-effect: dropshadow(two-pass-box , rgba(0, 0, 0, 0.1), 5, 0.0 , 0, 2); +} + +.rangeslider:focused .low-thumb, +.rangeslider:focused .high-thumb { + -fx-background-radius: 1.0em; /* makes sure this remains circular */ +} + +.rangeslider .low-thumb:focused, +.rangeslider .high-thumb:focused { + -fx-background-color: + -fx-focus-color, + derive(-fx-color,-36%), + derive(-fx-color,73%), + linear-gradient(to bottom, derive(-fx-color,-19%),derive(-fx-color,61%)); + -fx-background-insets: -1.4, 0, 1, 2; + -fx-background-radius: 1.0em; /* makes sure this remains circular */ +} + +.rangeslider .low-thumb:hover, +.rangeslider .high-thumb:hover { + -fx-color: -fx-hover-base; +} + +.rangeslider .range-bar { + -fx-background-color: -fx-accent; +} + +.rangeslider .low-thumb:pressed, +.rangeslider .high-thumb:pressed { + -fx-color: -fx-pressed-base; +} + +.rangeslider .track { + -fx-background-color: + -fx-shadow-highlight-color, + linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border), + linear-gradient(to bottom, + derive(-fx-control-inner-background, -9%), + derive(-fx-control-inner-background, 0%), + derive(-fx-control-inner-background, -5%), + derive(-fx-control-inner-background, -12%) + ); + -fx-background-insets: 0 0 -1 0, 0, 1; + -fx-background-radius: 0.25em, 0.25em, 0.166667em; /* 3 3 2 */ + -fx-padding: 0.25em; /* 3 */ +} + +.rangeslider:vertical .track { + -fx-background-color: + -fx-shadow-highlight-color, + -fx-text-box-border, + linear-gradient(to right, + derive(-fx-control-inner-background, -9%), + -fx-control-inner-background, + derive(-fx-control-inner-background, -9%) + ); +} + +.rangeslider .axis { + -fx-tick-label-fill: derive(-fx-text-background-color, 30%); + -fx-tick-length: 5px; + -fx-minor-tick-length: 3px; + -fx-border-color: null; +} + +.rangeslider:disabled { + -fx-opacity: 0.4; +} diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index f1fb312e..ca691093 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -36,6 +36,8 @@ import ctbrec.ui.SiteUiFactory; import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.DirectorySelectionBox; import ctbrec.ui.controls.ProgramSelectionBox; +import ctbrec.ui.controls.range.DiscreteRange; +import ctbrec.ui.controls.range.RangeSlider; import ctbrec.ui.sites.ConfigUI; import ctbrec.ui.tabs.TabSelectionListener; import javafx.collections.FXCollections; @@ -95,7 +97,6 @@ public class SettingsTab extends Tab implements TabSelectionListener { private CheckBox removeRecordingAfterPp = new CheckBox(); private RadioButton recordLocal; private ProxySettingsPane proxySettingsPane; - private TextField maxResolution; private TextField concurrentRecordings; private ComboBox splitAfter; private ComboBox directoryStructure; @@ -346,29 +347,32 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - l = new Label("Maximum resolution (0 = unlimited)"); - Tooltip tt = new Tooltip("video height, e.g. 720 or 1080\n!Caution: If the resolution is unknown, ctbrec will not record the stream!"); + l = new Label("Restrict Resolution"); + Tooltip tt = new Tooltip("Only record streams with resolution within the given range"); l.setTooltip(tt); layout.add(l, 0, row); - maxResolution = new TextField(Integer.toString(Config.getInstance().getSettings().maximumResolution)); - maxResolution.setTooltip(tt); - maxResolution.textProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue.matches("\\d*")) { - maxResolution.setText(newValue.replaceAll(PATTERN_NOT_A_DIGIT, "")); - } - if (!maxResolution.getText().isEmpty()) { - int newRes = Integer.parseInt(maxResolution.getText()); - if (newRes != Config.getInstance().getSettings().maximumResolution) { - Config.getInstance().getSettings().maximumResolution = newRes; - saveConfig(); - } - } - }); - maxResolution.prefWidthProperty().bind(directoryStructure.widthProperty()); - layout.add(maxResolution, 1, row++); + List labels = Arrays.asList(0, 240, 360, 480, 600, 720, 960, 1080, 1440, 2160, 4320, 8640); + List values = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + DiscreteRange rangeValues = new DiscreteRange<>(values, labels); + RangeSlider resolutionRange = new RangeSlider<>(rangeValues); + resolutionRange.prefWidthProperty().bind(directoryStructure.widthProperty()); + resolutionRange.setShowTickMarks(true); + resolutionRange.setShowTickLabels(true); + resolutionRange.setLow(labels.indexOf(Config.getInstance().getSettings().minimumResolution)); + resolutionRange.setHigh(labels.indexOf(Config.getInstance().getSettings().maximumResolution)); + layout.add(resolutionRange, 1, row++); GridPane.setMargin(l, new Insets(0, 0, 0, 0)); - GridPane.setColumnSpan(maxResolution, 3); - GridPane.setMargin(maxResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + GridPane.setColumnSpan(resolutionRange, 3); + GridPane.setMargin(resolutionRange, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + resolutionRange.getLow().addListener((obs, oldV, newV) -> { + Config.getInstance().getSettings().minimumResolution = labels.get(newV.intValue()); + saveConfig(); + }); + resolutionRange.getHigh().addListener((obs, oldV, newV) -> { + Config.getInstance().getSettings().maximumResolution = labels.get(newV.intValue()); + saveConfig(); + }); + l = new Label("Concurrent Recordings (0 = unlimited)"); layout.add(l, 0, row); @@ -626,12 +630,6 @@ public class SettingsTab extends Tab implements TabSelectionListener { layout.add(mediaPlayer, 1, row); Button mediaPlayerParamsButton = new Button("⚙"); mediaPlayerParamsButton.setOnAction(e -> { - // Optional playerParams = Dialogs.showTextInput(mediaPlayerParamsButton.getScene(), "Media Player Parameters", - // "Media Player Start Parameters", settings.mediaPlayerParams); - // playerParams.ifPresent(p -> { - // settings.mediaPlayerParams = p; - // saveConfig(); - // }); PlayerSettingsDialog dialog = new PlayerSettingsDialog(getTabPane().getScene(), Config.getInstance()); Optional exception = dialog.showAndWait(); if (exception.isPresent()) { @@ -809,7 +807,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { useTLS.setDisable(local); recordingsDirectory.setDisable(!local); splitAfter.setDisable(!local); - maxResolution.setDisable(!local); + //maxResolution.setDisable(!local); directoryStructure.setDisable(!local); onlineCheckIntervalInSecs.setDisable(!local); leaveSpaceOnDevice.setDisable(!local); diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index e8e30442..3bcddf61 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -69,6 +69,7 @@ public class Settings { public String livejasminUsername = ""; public boolean livePreviews = false; public boolean localRecording = true; + public int minimumResolution = 0; public int maximumResolution = 0; public int maximumResolutionPlayer = 0; public String mediaPlayer = "/usr/bin/mpv"; diff --git a/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java b/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java index 63a2c509..a5c21743 100644 --- a/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java @@ -336,10 +336,11 @@ public class DashDownload extends AbstractDownload { private AdaptationSetType chooseBestVideo(List videoStreams) { AdaptationSetType best = null; + int minHeight = config.getSettings().minimumResolution; int maxHeight = config.getSettings().maximumResolution; long bestHeight = 0; for (AdaptationSetType stream : videoStreams) { - if (stream.getHeight() > bestHeight && (maxHeight == 0 || stream.getHeight() <= maxHeight)) { + if (stream.getHeight() > bestHeight && (minHeight == 0 || stream.getHeight() >= minHeight) && (maxHeight == 0 || stream.getHeight() <= maxHeight)) { bestHeight = stream.getHeight(); best = stream; } diff --git a/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java index 0c5e8a64..fee4c801 100644 --- a/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java @@ -10,7 +10,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -22,6 +21,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import javax.xml.bind.JAXBException; @@ -161,21 +161,18 @@ public abstract class AbstractHlsDownload extends AbstractDownload { url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl(); } else { // filter out stream resolutions, which are too high + int minRes = Config.getInstance().getSettings().minimumResolution; int maxRes = Config.getInstance().getSettings().maximumResolution; - if (maxRes > 0) { - for (Iterator iterator = streamSources.iterator(); iterator.hasNext();) { - StreamSource streamSource = iterator.next(); - if (streamSource.height > 0 && maxRes < streamSource.height) { - LOG.trace("Res too high {} > {}", streamSource.height, maxRes); - iterator.remove(); - } - } - } - if (streamSources.isEmpty()) { + List filteredStreamSources = streamSources.stream() + .filter(src -> src.height == 0 || minRes <= src.height) + .filter(src -> src.height == 0 || maxRes >= src.height) + .collect(Collectors.toList()); + + if (filteredStreamSources.isEmpty()) { throw new ExecutionException(new RuntimeException("No stream left in playlist")); } else { - LOG.debug("{} selected {}", model.getName(), streamSources.get(streamSources.size() - 1)); - url = streamSources.get(streamSources.size() - 1).getMediaPlaylistUrl(); + LOG.debug("{} selected {}", model.getName(), filteredStreamSources.get(filteredStreamSources.size() - 1)); + url = filteredStreamSources.get(filteredStreamSources.size() - 1).getMediaPlaylistUrl(); } } LOG.debug("Segment playlist url {}", url); diff --git a/server/src/main/java/ctbrec/recorder/server/ConfigServlet.java b/server/src/main/java/ctbrec/recorder/server/ConfigServlet.java index d3b63cf8..7d0e73e8 100644 --- a/server/src/main/java/ctbrec/recorder/server/ConfigServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/ConfigServlet.java @@ -55,6 +55,7 @@ public class ConfigServlet extends AbstractCtbrecServlet { addParameter("httpUserAgent", "User-Agent", DataType.STRING, settings.httpUserAgent, json); addParameter("httpUserAgentMobile", "Mobile User-Agent", DataType.STRING, settings.httpUserAgentMobile, json); addParameter("generatePlaylist", "Generate Playlist", DataType.BOOLEAN, settings.generatePlaylist, json); + addParameter("minimumResolution", "Minimum Resolution", DataType.INTEGER, settings.minimumResolution, json); addParameter("maximumResolution", "Maximum Resolution", DataType.INTEGER, settings.maximumResolution, json); addParameter("minimumLengthInSeconds", "Minimum Length (secs)", DataType.INTEGER, settings.minimumLengthInSeconds, json); addParameter("minimumSpaceLeftInBytes", "Leave Space On Device (GiB)", DataType.LONG, settings.minimumSpaceLeftInBytes, json);