forked from j62/ctbrec
Merge branch 'dev'
This commit is contained in:
commit
a45ba8f35e
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,3 +1,17 @@
|
|||
1.12.0
|
||||
========================
|
||||
* Added threshold setting to keep free space on the recording device.
|
||||
This is useful, if you don't want to use up all of your storage.
|
||||
The free space is also shown on the recordings tab
|
||||
* Tweaked the download internals a lot. Downloads should not hang
|
||||
in RECORDING state without actually recording. Downloads should
|
||||
be more robust in general.
|
||||
* Fixed and improved split recordings
|
||||
* Improved detection of online state for Cam4 models
|
||||
* Accelerated the initial loading of the "Recording" tab for many
|
||||
Chaturbate models
|
||||
* Recordings tab now shows smaller size units (Bytes, KiB, MiB, GiB)
|
||||
|
||||
1.11.0
|
||||
========================
|
||||
* Added model search function
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/target/
|
||||
*~
|
||||
*.bak
|
||||
/ctbrec.log
|
||||
/*.log
|
||||
/ctbrec-tunnel.sh
|
||||
/jre/
|
||||
/server-local.sh
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>1.11.0</version>
|
||||
<version>1.12.0</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.Instant;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Recording;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
|
@ -12,9 +13,10 @@ public class JavaFxRecording extends Recording {
|
|||
|
||||
private transient StringProperty statusProperty = new SimpleStringProperty();
|
||||
private transient StringProperty progressProperty = new SimpleStringProperty();
|
||||
private transient StringProperty sizeProperty = new SimpleStringProperty();
|
||||
private transient LongProperty sizeProperty = new SimpleLongProperty();
|
||||
|
||||
private Recording delegate;
|
||||
private long lastValue = 0;
|
||||
|
||||
public JavaFxRecording(Recording recording) {
|
||||
this.delegate = recording;
|
||||
|
@ -89,9 +91,7 @@ public class JavaFxRecording extends Recording {
|
|||
@Override
|
||||
public void setSizeInByte(long sizeInByte) {
|
||||
delegate.setSizeInByte(sizeInByte);
|
||||
double sizeInGiB = sizeInByte / 1024.0 / 1024 / 1024;
|
||||
DecimalFormat df = new DecimalFormat("0.00");
|
||||
sizeProperty.setValue(df.format(sizeInGiB) + " GiB");
|
||||
sizeProperty.set(sizeInByte);
|
||||
}
|
||||
|
||||
public StringProperty getProgressProperty() {
|
||||
|
@ -151,8 +151,13 @@ public class JavaFxRecording extends Recording {
|
|||
return delegate.getSizeInByte();
|
||||
}
|
||||
|
||||
public StringProperty getSizeProperty() {
|
||||
public LongProperty getSizeProperty() {
|
||||
return sizeProperty;
|
||||
}
|
||||
|
||||
public boolean valueChanged() {
|
||||
boolean changed = getSizeInByte() != lastValue;
|
||||
lastValue = getSizeInByte();
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import java.io.IOException;
|
|||
import java.net.URL;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
|
@ -47,13 +48,16 @@ import javafx.scene.Cursor;
|
|||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableColumn.SortType;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
|
@ -62,6 +66,9 @@ import javafx.scene.input.MouseButton;
|
|||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
|
@ -74,12 +81,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
private Recorder recorder;
|
||||
@SuppressWarnings("unused")
|
||||
private List<Site> sites;
|
||||
private long spaceTotal = -1;
|
||||
private long spaceFree = -1;
|
||||
|
||||
FlowPane grid = new FlowPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
TableView<JavaFxRecording> table = new TableView<JavaFxRecording>();
|
||||
ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList();
|
||||
ContextMenu popup;
|
||||
ProgressBar spaceLeft;
|
||||
Label spaceLabel;
|
||||
|
||||
public RecordingsTab(String title, Recorder recorder, Config config, List<Site> sites) {
|
||||
super(title);
|
||||
|
@ -136,9 +147,37 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
TableColumn<JavaFxRecording, String> progress = new TableColumn<>("Progress");
|
||||
progress.setCellValueFactory((cdf) -> cdf.getValue().getProgressProperty());
|
||||
progress.setPrefWidth(100);
|
||||
TableColumn<JavaFxRecording, String> size = new TableColumn<>("Size");
|
||||
size.setCellValueFactory((cdf) -> cdf.getValue().getSizeProperty());
|
||||
TableColumn<JavaFxRecording, Number> size = new TableColumn<>("Size");
|
||||
size.setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
size.setPrefWidth(100);
|
||||
size.setCellValueFactory(cdf -> cdf.getValue().getSizeProperty());
|
||||
size.setCellFactory(new Callback<TableColumn<JavaFxRecording, Number>, TableCell<JavaFxRecording, Number>>() {
|
||||
@Override
|
||||
public TableCell<JavaFxRecording, Number> call(TableColumn<JavaFxRecording, Number> param) {
|
||||
TableCell<JavaFxRecording, Number> cell = new TableCell<JavaFxRecording, Number>() {
|
||||
@Override
|
||||
protected void updateItem(Number sizeInByte, boolean empty) {
|
||||
if(empty || sizeInByte == null) {
|
||||
setText(null);
|
||||
setStyle(null);
|
||||
} else {
|
||||
setText(StringUtil.formatSize(sizeInByte));
|
||||
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
int row = this.getTableRow().getIndex();
|
||||
JavaFxRecording rec = tableViewProperty().get().getItems().get(row);
|
||||
if(!rec.valueChanged() && rec.getStatus() == STATUS.RECORDING) {
|
||||
setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red");
|
||||
} else {
|
||||
setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
//setStyle(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return cell;
|
||||
}
|
||||
});
|
||||
|
||||
table.getColumns().addAll(name, date, status, progress, size);
|
||||
table.setItems(observableRecordings);
|
||||
|
@ -179,8 +218,21 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
});
|
||||
scrollPane.setContent(table);
|
||||
|
||||
HBox spaceBox = new HBox(5);
|
||||
Label l = new Label("Space left on device");
|
||||
HBox.setMargin(l, new Insets(2, 0, 0, 0));
|
||||
spaceBox.getChildren().add(l);
|
||||
spaceLeft = new ProgressBar(0);
|
||||
spaceLeft.setPrefSize(200, 22);
|
||||
spaceLabel = new Label();
|
||||
spaceLabel.setFont(Font.font(11));
|
||||
StackPane stack = new StackPane(spaceLeft, spaceLabel);
|
||||
spaceBox.getChildren().add(stack);
|
||||
BorderPane.setMargin(spaceBox, new Insets(5));
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
root.setPadding(new Insets(5));
|
||||
root.setTop(spaceBox);
|
||||
root.setCenter(scrollPane);
|
||||
setContent(root);
|
||||
|
||||
|
@ -191,6 +243,33 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
updateService = createUpdateService();
|
||||
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
||||
updateService.setOnSucceeded((event) -> {
|
||||
updateRecordingsTable();
|
||||
updateFreeSpaceDisplay();
|
||||
});
|
||||
updateService.setOnFailed((event) -> {
|
||||
LOG.info("Couldn't get list of recordings from recorder", event.getSource().getException());
|
||||
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR);
|
||||
autosizeAlert.setTitle("Whoopsie!");
|
||||
autosizeAlert.setHeaderText("Recordings not available");
|
||||
autosizeAlert.setContentText("An error occured while retrieving the list of recordings");
|
||||
autosizeAlert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateFreeSpaceDisplay() {
|
||||
if(spaceTotal != -1 && spaceFree != -1) {
|
||||
double free = ((double)spaceFree) / spaceTotal;
|
||||
spaceLeft.setProgress(free);
|
||||
double totalGiB = ((double) spaceTotal) / 1024 / 1024 / 1024;
|
||||
double freeGiB = ((double) spaceFree) / 1024 / 1024 / 1024;
|
||||
DecimalFormat df = new DecimalFormat("0.00");
|
||||
String tt = df.format(freeGiB) + " / " + df.format(totalGiB) + " GiB";
|
||||
spaceLeft.setTooltip(new Tooltip(tt));
|
||||
spaceLabel.setText(tt);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRecordingsTable() {
|
||||
List<JavaFxRecording> recordings = updateService.getValue();
|
||||
if (recordings == null) {
|
||||
return;
|
||||
|
@ -215,15 +294,6 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
}
|
||||
table.sort();
|
||||
});
|
||||
updateService.setOnFailed((event) -> {
|
||||
LOG.info("Couldn't get list of recordings from recorder", event.getSource().getException());
|
||||
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR);
|
||||
autosizeAlert.setTitle("Whoopsie!");
|
||||
autosizeAlert.setHeaderText("Recordings not available");
|
||||
autosizeAlert.setContentText("An error occured while retrieving the list of recordings");
|
||||
autosizeAlert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
private ScheduledService<List<JavaFxRecording>> createUpdateService() {
|
||||
|
@ -233,12 +303,23 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
return new Task<List<JavaFxRecording>>() {
|
||||
@Override
|
||||
public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
||||
updateSpace();
|
||||
|
||||
List<JavaFxRecording> recordings = new ArrayList<>();
|
||||
for (Recording rec : recorder.getRecordings()) {
|
||||
recordings.add(new JavaFxRecording(rec));
|
||||
}
|
||||
return recordings;
|
||||
}
|
||||
|
||||
private void updateSpace() {
|
||||
try {
|
||||
spaceTotal = recorder.getTotalSpaceBytes();
|
||||
spaceFree = recorder.getFreeSpaceBytes();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Couldn't update free space", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -55,6 +55,7 @@ import javafx.stage.FileChooser;;
|
|||
public class SettingsTab extends Tab implements TabSelectionListener {
|
||||
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(SettingsTab.class);
|
||||
private static final int ONE_GiB_IN_BYTES = 1024 * 1024 * 1024;
|
||||
|
||||
public static final int CHECKBOX_MARGIN = 6;
|
||||
private TextField recordingsDirectory;
|
||||
|
@ -65,6 +66,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private TextField server;
|
||||
private TextField port;
|
||||
private TextField onlineCheckIntervalInSecs;
|
||||
private TextField leaveSpaceOnDevice;
|
||||
private CheckBox loadResolution;
|
||||
private CheckBox secureCommunication = new CheckBox();
|
||||
private CheckBox chooseStreamQuality = new CheckBox();
|
||||
|
@ -120,7 +122,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
// left side
|
||||
leftSide.getChildren().add(createGeneralPanel());
|
||||
leftSide.getChildren().add(createLocationsPanel());
|
||||
leftSide.getChildren().add(createRecorderPanel());
|
||||
leftSide.getChildren().add(createRecordLocationPanel());
|
||||
|
||||
//right side
|
||||
|
@ -253,9 +255,20 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
return recordLocation;
|
||||
}
|
||||
|
||||
private Node createLocationsPanel() {
|
||||
private Node createRecorderPanel() {
|
||||
int row = 0;
|
||||
GridPane layout = createGridLayout();
|
||||
layout.add(new Label("Post-Processing"), 0, row);
|
||||
postProcessing = new TextField(Config.getInstance().getSettings().postProcessing);
|
||||
postProcessing.focusedProperty().addListener(createPostProcessingFocusListener());
|
||||
GridPane.setFillWidth(postProcessing, true);
|
||||
GridPane.setHgrow(postProcessing, Priority.ALWAYS);
|
||||
GridPane.setColumnSpan(postProcessing, 2);
|
||||
GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(postProcessing, 1, row);
|
||||
postProcessingDirectoryButton = createPostProcessingBrowseButton();
|
||||
layout.add(postProcessingDirectoryButton, 3, row++);
|
||||
|
||||
layout.add(new Label("Recordings Directory"), 0, row);
|
||||
recordingsDirectory = new TextField(Config.getInstance().getSettings().recordingsDir);
|
||||
recordingsDirectory.focusedProperty().addListener(createRecordingsDirectoryFocusListener());
|
||||
|
@ -283,16 +296,97 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
GridPane.setMargin(directoryStructure, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(directoryStructure, 1, row++);
|
||||
|
||||
layout.add(new Label("Post-Processing"), 0, row);
|
||||
postProcessing = new TextField(Config.getInstance().getSettings().postProcessing);
|
||||
postProcessing.focusedProperty().addListener(createPostProcessingFocusListener());
|
||||
GridPane.setFillWidth(postProcessing, true);
|
||||
GridPane.setHgrow(postProcessing, Priority.ALWAYS);
|
||||
GridPane.setColumnSpan(postProcessing, 2);
|
||||
GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(postProcessing, 1, row);
|
||||
postProcessingDirectoryButton = createPostProcessingBrowseButton();
|
||||
layout.add(postProcessingDirectoryButton, 3, row++);
|
||||
Label l = new Label("Maximum resolution (0 = unlimited)");
|
||||
layout.add(l, 0, row);
|
||||
List<Integer> resolutionOptions = new ArrayList<>();
|
||||
resolutionOptions.add(1080);
|
||||
resolutionOptions.add(720);
|
||||
resolutionOptions.add(600);
|
||||
resolutionOptions.add(480);
|
||||
resolutionOptions.add(0);
|
||||
maxResolution = new ComboBox<>(FXCollections.observableList(resolutionOptions));
|
||||
setMaxResolutionValue();
|
||||
maxResolution.setOnAction((e) -> {
|
||||
Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem();
|
||||
saveConfig();
|
||||
});
|
||||
maxResolution.prefWidthProperty().bind(directoryStructure.widthProperty());
|
||||
layout.add(maxResolution, 1, row++);
|
||||
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
|
||||
GridPane.setMargin(maxResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
|
||||
l = new Label("Split recordings after (minutes)");
|
||||
layout.add(l, 0, row);
|
||||
List<SplitAfterOption> splitOptions = new ArrayList<>();
|
||||
splitOptions.add(new SplitAfterOption("disabled", 0));
|
||||
if(Config.isDevMode()) {
|
||||
splitOptions.add(new SplitAfterOption( "1 min", 1 * 60));
|
||||
splitOptions.add(new SplitAfterOption( "3 min", 3 * 60));
|
||||
splitOptions.add(new SplitAfterOption( "5 min", 5 * 60));
|
||||
}
|
||||
splitOptions.add(new SplitAfterOption("10 min", 10 * 60));
|
||||
splitOptions.add(new SplitAfterOption("15 min", 15 * 60));
|
||||
splitOptions.add(new SplitAfterOption("20 min", 20 * 60));
|
||||
splitOptions.add(new SplitAfterOption("30 min", 30 * 60));
|
||||
splitOptions.add(new SplitAfterOption("60 min", 60 * 60));
|
||||
splitAfter = new ComboBox<>(FXCollections.observableList(splitOptions));
|
||||
layout.add(splitAfter, 1, row++);
|
||||
setSplitAfterValue();
|
||||
splitAfter.setOnAction((e) -> {
|
||||
Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue();
|
||||
saveConfig();
|
||||
});
|
||||
splitAfter.prefWidthProperty().bind(directoryStructure.widthProperty());
|
||||
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
|
||||
GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
|
||||
Tooltip tt = new Tooltip("Check every x seconds, if a model came online");
|
||||
l = new Label("Check online state every (seconds)");
|
||||
l.setTooltip(tt);
|
||||
layout.add(l, 0, row);
|
||||
onlineCheckIntervalInSecs = new TextField(Integer.toString(Config.getInstance().getSettings().onlineCheckIntervalInSecs));
|
||||
onlineCheckIntervalInSecs.setTooltip(tt);
|
||||
onlineCheckIntervalInSecs.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue.matches("\\d*")) {
|
||||
onlineCheckIntervalInSecs.setText(newValue.replaceAll("[^\\d]", ""));
|
||||
}
|
||||
if(!onlineCheckIntervalInSecs.getText().isEmpty()) {
|
||||
Config.getInstance().getSettings().onlineCheckIntervalInSecs = Integer.parseInt(onlineCheckIntervalInSecs.getText());
|
||||
saveConfig();
|
||||
}
|
||||
});
|
||||
GridPane.setMargin(onlineCheckIntervalInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(onlineCheckIntervalInSecs, 1, row++);
|
||||
|
||||
tt = new Tooltip("Stop recording, if the free space on the device gets below this threshold");
|
||||
l = new Label("Leave space on device (GiB)");
|
||||
l.setTooltip(tt);
|
||||
layout.add(l, 0, row);
|
||||
long minimumSpaceLeftInBytes = Config.getInstance().getSettings().minimumSpaceLeftInBytes;
|
||||
int minimumSpaceLeftInGiB = (int) (minimumSpaceLeftInBytes / ONE_GiB_IN_BYTES);
|
||||
leaveSpaceOnDevice = new TextField(Integer.toString(minimumSpaceLeftInGiB));
|
||||
leaveSpaceOnDevice.setTooltip(tt);
|
||||
leaveSpaceOnDevice.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue.matches("\\d*")) {
|
||||
leaveSpaceOnDevice.setText(newValue.replaceAll("[^\\d]", ""));
|
||||
}
|
||||
if(!leaveSpaceOnDevice.getText().isEmpty()) {
|
||||
long spaceLeftInGiB = Long.parseLong(leaveSpaceOnDevice.getText());
|
||||
Config.getInstance().getSettings().minimumSpaceLeftInBytes = spaceLeftInGiB * ONE_GiB_IN_BYTES;
|
||||
saveConfig();
|
||||
}
|
||||
});
|
||||
GridPane.setMargin(leaveSpaceOnDevice, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(leaveSpaceOnDevice, 1, row++);
|
||||
|
||||
TitledPane locations = new TitledPane("Recorder", layout);
|
||||
locations.setCollapsible(false);
|
||||
return locations;
|
||||
}
|
||||
|
||||
private Node createGeneralPanel() {
|
||||
GridPane layout = createGridLayout();
|
||||
int row = 0;
|
||||
|
||||
layout.add(new Label("Player"), 0, row);
|
||||
mediaPlayer = new TextField(Config.getInstance().getSettings().mediaPlayer);
|
||||
|
@ -304,15 +398,18 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
layout.add(mediaPlayer, 1, row);
|
||||
layout.add(createMpvBrowseButton(), 3, row++);
|
||||
|
||||
TitledPane locations = new TitledPane("Locations", layout);
|
||||
locations.setCollapsible(false);
|
||||
return locations;
|
||||
}
|
||||
Label l = new Label("Allow multiple players");
|
||||
layout.add(l, 0, row);
|
||||
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
|
||||
multiplePlayers.setOnAction((e) -> {
|
||||
Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected();
|
||||
saveConfig();
|
||||
});
|
||||
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
||||
GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(multiplePlayers, 1, row++);
|
||||
|
||||
private Node createGeneralPanel() {
|
||||
GridPane layout = createGridLayout();
|
||||
int row = 0;
|
||||
Label l = new Label("Display stream resolution in overview");
|
||||
l = new Label("Display stream resolution in overview");
|
||||
layout.add(l, 0, row);
|
||||
loadResolution = new CheckBox();
|
||||
loadResolution.setSelected(Config.getInstance().getSettings().determineResolution);
|
||||
|
@ -323,20 +420,10 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
ThumbOverviewTab.queue.clear();
|
||||
}
|
||||
});
|
||||
//GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||
GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||
GridPane.setMargin(loadResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(loadResolution, 1, row++);
|
||||
|
||||
l = new Label("Allow multiple players");
|
||||
layout.add(l, 0, row);
|
||||
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
|
||||
multiplePlayers.setOnAction((e) -> {
|
||||
Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected();
|
||||
saveConfig();
|
||||
});
|
||||
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
||||
GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(multiplePlayers, 1, row++);
|
||||
|
||||
l = new Label("Manually select stream quality");
|
||||
layout.add(l, 0, row);
|
||||
|
@ -357,60 +444,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
saveConfig();
|
||||
});
|
||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||
GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, CHECKBOX_MARGIN, CHECKBOX_MARGIN));
|
||||
layout.add(updateThumbnails, 1, row++);
|
||||
|
||||
l = new Label("Maximum resolution (0 = unlimited)");
|
||||
layout.add(l, 0, row);
|
||||
List<Integer> resolutionOptions = new ArrayList<>();
|
||||
resolutionOptions.add(1080);
|
||||
resolutionOptions.add(720);
|
||||
resolutionOptions.add(600);
|
||||
resolutionOptions.add(480);
|
||||
resolutionOptions.add(0);
|
||||
maxResolution = new ComboBox<>(FXCollections.observableList(resolutionOptions));
|
||||
setMaxResolutionValue();
|
||||
maxResolution.setOnAction((e) -> {
|
||||
Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem();
|
||||
saveConfig();
|
||||
});
|
||||
layout.add(maxResolution, 1, row++);
|
||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||
GridPane.setMargin(maxResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
|
||||
l = new Label("Split recordings after (minutes)");
|
||||
layout.add(l, 0, row);
|
||||
List<SplitAfterOption> options = new ArrayList<>();
|
||||
options.add(new SplitAfterOption("disabled", 0));
|
||||
options.add(new SplitAfterOption("10 min", 10 * 60));
|
||||
options.add(new SplitAfterOption("15 min", 15 * 60));
|
||||
options.add(new SplitAfterOption("20 min", 20 * 60));
|
||||
options.add(new SplitAfterOption("30 min", 30 * 60));
|
||||
options.add(new SplitAfterOption("60 min", 60 * 60));
|
||||
splitAfter = new ComboBox<>(FXCollections.observableList(options));
|
||||
layout.add(splitAfter, 1, row++);
|
||||
setSplitAfterValue();
|
||||
splitAfter.setOnAction((e) -> {
|
||||
Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue();
|
||||
saveConfig();
|
||||
});
|
||||
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
|
||||
GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
|
||||
layout.add(new Label("Check online state every (seconds)"), 0, row);
|
||||
onlineCheckIntervalInSecs = new TextField(Integer.toString(Config.getInstance().getSettings().onlineCheckIntervalInSecs));
|
||||
onlineCheckIntervalInSecs.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue.matches("\\d*")) {
|
||||
onlineCheckIntervalInSecs.setText(newValue.replaceAll("[^\\d]", ""));
|
||||
}
|
||||
if(!onlineCheckIntervalInSecs.getText().isEmpty()) {
|
||||
Config.getInstance().getSettings().onlineCheckIntervalInSecs = Integer.parseInt(onlineCheckIntervalInSecs.getText());
|
||||
saveConfig();
|
||||
}
|
||||
});
|
||||
GridPane.setMargin(onlineCheckIntervalInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(onlineCheckIntervalInSecs, 1, row++);
|
||||
|
||||
l = new Label("Start Tab");
|
||||
layout.add(l, 0, row);
|
||||
startTab = new ComboBox<>();
|
||||
|
@ -429,12 +465,6 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
|
||||
GridPane.setMargin(colorSettingsPane, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
|
||||
|
||||
splitAfter.prefWidthProperty().bind(startTab.widthProperty());
|
||||
maxResolution.prefWidthProperty().bind(startTab.widthProperty());
|
||||
onlineCheckIntervalInSecs.prefWidthProperty().bind(startTab.widthProperty());
|
||||
onlineCheckIntervalInSecs.maxWidthProperty().bind(startTab.widthProperty());
|
||||
|
||||
TitledPane general = new TitledPane("General", layout);
|
||||
general.setCollapsible(false);
|
||||
return general;
|
||||
|
@ -481,6 +511,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
postProcessing.setDisable(!local);
|
||||
postProcessingDirectoryButton.setDisable(!local);
|
||||
directoryStructure.setDisable(!local);
|
||||
onlineCheckIntervalInSecs.setDisable(!local);
|
||||
leaveSpaceOnDevice.setDisable(!local);
|
||||
}
|
||||
|
||||
private ChangeListener<? super Boolean> createRecordingsDirectoryFocusListener() {
|
||||
|
@ -653,10 +685,11 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
@Override
|
||||
public void selected() {
|
||||
startTab.getItems().clear();
|
||||
if(startTab.getItems().isEmpty()) {
|
||||
for(Tab tab : getTabPane().getTabs()) {
|
||||
startTab.getItems().add(tab.getText());
|
||||
}
|
||||
}
|
||||
String startTabName = Config.getInstance().getSettings().startTab;
|
||||
if(StringUtil.isNotBlank(startTabName)) {
|
||||
startTab.getSelectionModel().select(startTabName);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>1.11.0</version>
|
||||
<version>1.12.0</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -100,10 +100,14 @@ public class Config {
|
|||
Files.write(configFile.toPath(), json.getBytes("utf-8"), CREATE, WRITE, TRUNCATE_EXISTING);
|
||||
}
|
||||
|
||||
public boolean isServerMode() {
|
||||
public static boolean isServerMode() {
|
||||
return Objects.equals(System.getProperty("ctbrec.server.mode"), "1");
|
||||
}
|
||||
|
||||
public static boolean isDevMode() {
|
||||
return Objects.equals(System.getenv("CTBREC_DEV"), "1");
|
||||
}
|
||||
|
||||
public File getConfigDir() {
|
||||
return configDir;
|
||||
}
|
||||
|
@ -113,10 +117,6 @@ public class Config {
|
|||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
|
||||
String startTime = sdf.format(new Date());
|
||||
File targetFile = new File(dirForRecording, model.getName() + '_' + startTime + ".ts");
|
||||
if(getSettings().splitRecordings > 0) {
|
||||
LOG.debug("Splitting recordings every {} seconds", getSettings().splitRecordings);
|
||||
targetFile = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-00000.ts"));
|
||||
}
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ public class Settings {
|
|||
public String httpServer = "localhost";
|
||||
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
|
||||
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
|
||||
public long minimumSpaceLeftInBytes = 0;
|
||||
public String mediaPlayer = "/usr/bin/mpv";
|
||||
public String postProcessing = "";
|
||||
public String username = ""; // chaturbate username TODO maybe rename this onetime
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package ctbrec;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
public class StringUtil {
|
||||
public static boolean isBlank(String s) {
|
||||
return s == null || s.trim().isEmpty();
|
||||
|
@ -8,4 +10,21 @@ public class StringUtil {
|
|||
public static boolean isNotBlank(String s) {
|
||||
return !isBlank(s);
|
||||
}
|
||||
|
||||
public static String formatSize(Number sizeInByte) {
|
||||
DecimalFormat df = new DecimalFormat("0.00");
|
||||
String unit = "Bytes";
|
||||
double size = sizeInByte.doubleValue();
|
||||
if(size > 1024.0 * 1024 * 1024) {
|
||||
size = size / 1024.0 / 1024 / 1024;
|
||||
unit = "GiB";
|
||||
} else if(size > 1024.0 * 1024) {
|
||||
size = size / 1024.0 / 1024;
|
||||
unit = "MiB";
|
||||
} else if(size > 1024.0) {
|
||||
size = size / 1024.0;
|
||||
unit = "KiB";
|
||||
}
|
||||
return df.format(size) + ' ' + unit;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import static ctbrec.Recording.STATUS.*;
|
|||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileStore;
|
||||
import java.nio.file.Files;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
@ -63,6 +64,7 @@ public class LocalRecorder implements Recorder {
|
|||
private List<File> deleteInProgress = Collections.synchronizedList(new ArrayList<>());
|
||||
private RecorderHttpClient client = new RecorderHttpClient();
|
||||
private ReentrantLock lock = new ReentrantLock();
|
||||
private long lastSpaceMessage = 0;
|
||||
|
||||
public LocalRecorder(Config config) {
|
||||
this.config = config;
|
||||
|
@ -81,7 +83,7 @@ public class LocalRecorder implements Recorder {
|
|||
onlineMonitor.start();
|
||||
|
||||
postProcessingTrigger = new PostProcessingTrigger();
|
||||
if(Config.getInstance().isServerMode()) {
|
||||
if(Config.isServerMode()) {
|
||||
postProcessingTrigger.start();
|
||||
}
|
||||
|
||||
|
@ -133,7 +135,6 @@ public class LocalRecorder implements Recorder {
|
|||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Starting recording for model {}", model.getName());
|
||||
if (recordingProcesses.containsKey(model)) {
|
||||
LOG.error("A recording for model {} is already running", model);
|
||||
return;
|
||||
|
@ -149,8 +150,18 @@ public class LocalRecorder implements Recorder {
|
|||
lock.unlock();
|
||||
}
|
||||
|
||||
if(!enoughSpaceForRecording()) {
|
||||
long now = System.currentTimeMillis();
|
||||
if( (now - lastSpaceMessage) > TimeUnit.MINUTES.toMillis(1)) {
|
||||
LOG.info("Not enough space for recording, not starting recording for {}", model);
|
||||
lastSpaceMessage = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Starting recording for model {}", model.getName());
|
||||
Download download;
|
||||
if (Config.getInstance().isServerMode()) {
|
||||
if (Config.isServerMode()) {
|
||||
download = new HlsDownload(client);
|
||||
} else {
|
||||
download = new MergedHlsDownload(client);
|
||||
|
@ -173,7 +184,7 @@ public class LocalRecorder implements Recorder {
|
|||
Download download = recordingProcesses.get(model);
|
||||
download.stop();
|
||||
recordingProcesses.remove(model);
|
||||
if(!Config.getInstance().isServerMode()) {
|
||||
if(!Config.isServerMode()) {
|
||||
postprocess(download);
|
||||
}
|
||||
}
|
||||
|
@ -329,6 +340,15 @@ public class LocalRecorder implements Recorder {
|
|||
public void run() {
|
||||
running = true;
|
||||
while (running) {
|
||||
try {
|
||||
if(!enoughSpaceForRecording() && !recordingProcesses.isEmpty()) {
|
||||
LOG.info("No space left -> Stopping all recordings");
|
||||
stopRecordingProcesses();
|
||||
}
|
||||
} catch (IOException e1) {
|
||||
LOG.warn("Couldn't check free space left", e1);
|
||||
}
|
||||
|
||||
List<Model> restart = new ArrayList<>();
|
||||
for (Iterator<Entry<Model, Download>> iterator = recordingProcesses.entrySet().iterator(); iterator.hasNext();) {
|
||||
Entry<Model, Download> entry = iterator.next();
|
||||
|
@ -338,7 +358,7 @@ public class LocalRecorder implements Recorder {
|
|||
LOG.debug("Recording terminated for model {}", m.getName());
|
||||
iterator.remove();
|
||||
restart.add(m);
|
||||
if(config.isServerMode()) {
|
||||
if(Config.isServerMode()) {
|
||||
try {
|
||||
finishRecording(d.getTarget());
|
||||
} catch(Exception e) {
|
||||
|
@ -365,7 +385,7 @@ public class LocalRecorder implements Recorder {
|
|||
}
|
||||
|
||||
private void finishRecording(File directory) {
|
||||
if(Config.getInstance().isServerMode()) {
|
||||
if(Config.isServerMode()) {
|
||||
Thread t = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
@ -415,7 +435,7 @@ public class LocalRecorder implements Recorder {
|
|||
boolean isOnline = model.isOnline(IGNORE_CACHE);
|
||||
LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline"));
|
||||
if (isOnline && !isSuspended(model) && !recordingProcesses.containsKey(model)) {
|
||||
LOG.info("Model {}'s room back to public. Starting recording", model);
|
||||
LOG.info("Model {}'s room back to public", model);
|
||||
startRecordingProcess(model);
|
||||
}
|
||||
} catch (HttpException e) {
|
||||
|
@ -493,7 +513,7 @@ public class LocalRecorder implements Recorder {
|
|||
|
||||
@Override
|
||||
public List<Recording> getRecordings() {
|
||||
if(Config.getInstance().isServerMode()) {
|
||||
if(Config.isServerMode()) {
|
||||
return listSegmentedRecordings();
|
||||
} else {
|
||||
return listMergedRecordings();
|
||||
|
@ -538,7 +558,7 @@ public class LocalRecorder implements Recorder {
|
|||
return GENERATING_PLAYLIST;
|
||||
}
|
||||
|
||||
if (config.isServerMode()) {
|
||||
if (Config.isServerMode()) {
|
||||
if (recording.hasPlaylist()) {
|
||||
return FINISHED;
|
||||
} else {
|
||||
|
@ -745,4 +765,25 @@ public class LocalRecorder implements Recorder {
|
|||
public HttpClient getHttpClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalSpaceBytes() throws IOException {
|
||||
return getRecordingsFileStore().getTotalSpace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFreeSpaceBytes() throws IOException {
|
||||
return getRecordingsFileStore().getUsableSpace();
|
||||
}
|
||||
|
||||
private FileStore getRecordingsFileStore() throws IOException {
|
||||
File recordingsDir = new File(config.getSettings().recordingsDir);
|
||||
FileStore store = Files.getFileStore(recordingsDir.toPath());
|
||||
return store;
|
||||
}
|
||||
|
||||
private boolean enoughSpaceForRecording() throws IOException {
|
||||
long minimum = config.getSettings().minimumSpaceLeftInBytes;
|
||||
return getFreeSpaceBytes() > minimum;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,4 +42,18 @@ public interface Recorder {
|
|||
public List<Model> getOnlineModels();
|
||||
|
||||
public HttpClient getHttpClient();
|
||||
|
||||
/**
|
||||
* Get the total size of the filesystem we are recording to
|
||||
* @return the total size in bytes
|
||||
* @throws IOException
|
||||
*/
|
||||
public long getTotalSpaceBytes() throws IOException;
|
||||
|
||||
/**
|
||||
* Get the free space left on the filesystem we are recording to
|
||||
* @return the free space in bytes
|
||||
* @throws IOException
|
||||
*/
|
||||
public long getFreeSpaceBytes() throws IOException;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import java.time.Instant;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -45,6 +46,8 @@ public class RemoteRecorder implements Recorder {
|
|||
private List<Model> models = Collections.emptyList();
|
||||
private List<Model> onlineModels = Collections.emptyList();
|
||||
private List<Site> sites;
|
||||
private long spaceTotal = -1;
|
||||
private long spaceFree = -1;
|
||||
|
||||
private Config config;
|
||||
private HttpClient client;
|
||||
|
@ -150,10 +153,35 @@ public class RemoteRecorder implements Recorder {
|
|||
while(running) {
|
||||
syncModels();
|
||||
syncOnlineModels();
|
||||
syncSpace();
|
||||
sleep();
|
||||
}
|
||||
}
|
||||
|
||||
private void syncSpace() {
|
||||
try {
|
||||
String msg = "{\"action\": \"space\"}";
|
||||
RequestBody body = RequestBody.create(JSON, msg);
|
||||
Request.Builder builder = new Request.Builder()
|
||||
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec")
|
||||
.post(body);
|
||||
addHmacIfNeeded(msg, builder);
|
||||
Request request = builder.build();
|
||||
try(Response response = client.execute(request)) {
|
||||
String json = response.body().string();
|
||||
if(response.isSuccessful()) {
|
||||
JSONObject resp = new JSONObject(json);
|
||||
spaceTotal = resp.getLong("spaceTotal");
|
||||
spaceFree = resp.getLong("spaceFree");
|
||||
} else {
|
||||
LOG.error("Couldn't synchronize with server. HTTP status: {} - {}", response.code(), json);
|
||||
}
|
||||
}
|
||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
|
||||
LOG.error("Couldn't synchronize with server", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void syncModels() {
|
||||
try {
|
||||
String msg = "{\"action\": \"list\"}";
|
||||
|
@ -362,4 +390,14 @@ public class RemoteRecorder implements Recorder {
|
|||
public HttpClient getHttpClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalSpaceBytes() throws IOException {
|
||||
return spaceTotal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFreeSpaceBytes() {
|
||||
return spaceFree;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,11 +15,19 @@ import java.nio.file.LinkOption;
|
|||
import java.nio.file.Path;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -48,11 +56,12 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
private BlockingMultiMTSSource multiSource;
|
||||
private Thread mergeThread;
|
||||
private Streamer streamer;
|
||||
private ZonedDateTime startTime;
|
||||
private ZonedDateTime splitRecStartTime;
|
||||
private Config config;
|
||||
private File targetFile;
|
||||
private DecimalFormat df = new DecimalFormat("00000");
|
||||
private int splitCounter = 0;
|
||||
private BlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
|
||||
private ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue);
|
||||
private FileChannel fileChannel = null;
|
||||
|
||||
public MergedHlsDownload(HttpClient client) {
|
||||
super(client);
|
||||
|
@ -67,6 +76,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
try {
|
||||
running = true;
|
||||
super.startTime = Instant.now();
|
||||
splitRecStartTime = ZonedDateTime.now();
|
||||
mergeThread = createMergeThread(targetFile, progressListener, false);
|
||||
LOG.debug("Merge thread started");
|
||||
mergeThread.start();
|
||||
|
@ -81,7 +91,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
downloadSegments(segmentPlaylistUri, false);
|
||||
LOG.debug("Waiting for merge thread to finish");
|
||||
mergeThread.join();
|
||||
LOG.debug("Merge thread to finished");
|
||||
LOG.debug("Merge thread finished");
|
||||
} catch(ParseException e) {
|
||||
throw new IOException("Couldn't parse stream information", e);
|
||||
} catch(PlaylistException e) {
|
||||
|
@ -92,7 +102,12 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
throw new IOException("Couldn't add HMAC to playlist url", e);
|
||||
} finally {
|
||||
alive = false;
|
||||
try {
|
||||
streamer.stop();
|
||||
} catch(Exception e) {
|
||||
LOG.error("Couldn't stop streamer", e);
|
||||
}
|
||||
downloadThreadPool.shutdown();
|
||||
LOG.debug("Download terminated for {}", segmentPlaylistUri);
|
||||
}
|
||||
}
|
||||
|
@ -107,6 +122,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
|
||||
running = true;
|
||||
super.startTime = Instant.now();
|
||||
splitRecStartTime = ZonedDateTime.now();
|
||||
super.model = model;
|
||||
targetFile = Config.getInstance().getFileForRecording(model);
|
||||
String segments = getSegmentPlaylistUrl(model);
|
||||
|
@ -114,6 +130,9 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
mergeThread.start();
|
||||
if(segments != null) {
|
||||
downloadSegments(segments, true);
|
||||
if(config.getSettings().splitRecordings > 0) {
|
||||
LOG.debug("Splitting recordings every {} seconds", config.getSettings().splitRecordings);
|
||||
}
|
||||
} else {
|
||||
throw new IOException("Couldn't determine segments uri");
|
||||
}
|
||||
|
@ -129,7 +148,11 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
} finally {
|
||||
alive = false;
|
||||
if(streamer != null) {
|
||||
try {
|
||||
streamer.stop();
|
||||
} catch(Exception e) {
|
||||
LOG.error("Couldn't stop streamer", e);
|
||||
}
|
||||
}
|
||||
LOG.debug("Download for {} terminated", model);
|
||||
}
|
||||
|
@ -138,75 +161,112 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
private void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
||||
int lastSegment = 0;
|
||||
int nextSegment = 0;
|
||||
long playlistNotFoundFirstEncounter = -1;
|
||||
while(running) {
|
||||
try {
|
||||
if(playlistNotFoundFirstEncounter != -1) {
|
||||
LOG.debug("Downloading playlist {}", segmentPlaylistUri);
|
||||
}
|
||||
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
|
||||
playlistNotFoundFirstEncounter = -1;
|
||||
if(!livestreamDownload) {
|
||||
multiSource.setTotalSegments(lsp.segments.size());
|
||||
}
|
||||
|
||||
// download segments, which might have been skipped
|
||||
downloadMissedSegments(lsp, nextSegment);
|
||||
|
||||
// download new segments
|
||||
long downloadStart = System.currentTimeMillis();
|
||||
downloadNewSegments(lsp, nextSegment);
|
||||
long downloadTookMillis = System.currentTimeMillis() - downloadStart;
|
||||
|
||||
// download segments, which might have been skipped
|
||||
//downloadMissedSegments(lsp, nextSegment);
|
||||
if(nextSegment > 0 && lsp.seq > nextSegment) {
|
||||
LOG.warn("Missed segments {} < {} in download for {}. Download took {}ms. Playlist is {}sec", nextSegment, lsp.seq, lsp.url, downloadTookMillis, lsp.totalDuration);
|
||||
}
|
||||
|
||||
if(livestreamDownload) {
|
||||
// split up the recording, if configured
|
||||
splitRecording();
|
||||
|
||||
// wait some time until requesting the segment playlist again to not hammer the server
|
||||
waitForNewSegments(lsp, lastSegment);
|
||||
waitForNewSegments(lsp, lastSegment, downloadTookMillis);
|
||||
|
||||
lastSegment = lsp.seq;
|
||||
nextSegment = lastSegment + lsp.segments.size();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} catch(HttpException e) {
|
||||
if(e.getResponseCode() == 404) {
|
||||
// playlist is gone -> model probably logged out
|
||||
LOG.debug("Playlist not found. Assuming model went offline");
|
||||
running = false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadMissedSegments(SegmentPlaylist lsp, int nextSegment) throws MalformedURLException {
|
||||
if(nextSegment > 0 && lsp.seq > nextSegment) {
|
||||
LOG.warn("Missed segments {} < {} in download for {}", nextSegment, lsp.seq, lsp.url);
|
||||
String first = lsp.segments.get(0);
|
||||
int seq = lsp.seq;
|
||||
for (int i = nextSegment; i < lsp.seq; i++) {
|
||||
URL segmentUrl = new URL(first.replaceAll(Integer.toString(seq), Integer.toString(i)));
|
||||
LOG.debug("Loading missed segment {} for model {}", i, lsp.url);
|
||||
byte[] segmentData;
|
||||
try {
|
||||
segmentData = new SegmentDownload(segmentUrl, client).call();
|
||||
writeSegment(segmentData);
|
||||
} catch(Exception e) {
|
||||
LOG.error("Error while downloading segment {}", segmentUrl, e);
|
||||
LOG.info("Unexpected error while downloading {}", model.getName(), e);
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
// TODO switch to a lower bitrate/resolution ?!?
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadNewSegments(SegmentPlaylist lsp, int nextSegment) throws MalformedURLException {
|
||||
private void downloadNewSegments(SegmentPlaylist lsp, int nextSegment) throws MalformedURLException, MissingSegmentException, ExecutionException, HttpException {
|
||||
int skip = nextSegment - lsp.seq;
|
||||
if(lsp.segments.isEmpty()) {
|
||||
LOG.debug("Empty playlist: {}", lsp.url);
|
||||
}
|
||||
|
||||
// add segments to download threadpool
|
||||
Queue<Future<byte[]>> downloads = new LinkedList<>();
|
||||
if(downloadQueue.remainingCapacity() == 0) {
|
||||
LOG.warn("Download to slow for this stream. Download queue is full. Skipping segment");
|
||||
} else {
|
||||
for (String segment : lsp.segments) {
|
||||
if(!running) {
|
||||
break;
|
||||
}
|
||||
if(skip > 0) {
|
||||
skip--;
|
||||
} else {
|
||||
URL segmentUrl = new URL(segment);
|
||||
Future<byte[]> download = downloadThreadPool.submit(new SegmentDownload(segmentUrl, client));
|
||||
downloads.add(download);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get completed downloads and write them to the file
|
||||
// TODO it might be a good idea to do this in a separate thread, so that the main download loop isn't blocked
|
||||
writeFinishedSegments(downloads);
|
||||
}
|
||||
|
||||
private void writeFinishedSegments(Queue<Future<byte[]>> downloads) throws ExecutionException, HttpException {
|
||||
for (Future<byte[]> downloadFuture : downloads) {
|
||||
try {
|
||||
byte[] segmentData = new SegmentDownload(segmentUrl, client).call();
|
||||
byte[] segmentData = downloadFuture.get();
|
||||
writeSegment(segmentData);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error while downloading segment {}", segmentUrl, e);
|
||||
} catch (InterruptedException e) {
|
||||
LOG.error("Error while downloading segment", e);
|
||||
} catch (ExecutionException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if(cause instanceof MissingSegmentException) {
|
||||
if(model != null && !isModelOnline()) {
|
||||
LOG.debug("Error while downloading segment, because model {} is offline. Stopping now", model.getName());
|
||||
running = false;
|
||||
} else {
|
||||
LOG.debug("Segment not available, but model {} still online. Going on", model.getName());
|
||||
}
|
||||
} else if(cause instanceof HttpException) {
|
||||
HttpException he = (HttpException) cause;
|
||||
if(model != null && !isModelOnline()) {
|
||||
LOG.debug("Error {} while downloading segment, because model {} is offline. Stopping now", he.getResponseCode(), model.getName());
|
||||
running = false;
|
||||
} else {
|
||||
if(he.getResponseCode() == 404) {
|
||||
LOG.info("Playlist for {} not found [HTTP 404]. Stopping now", model.getName());
|
||||
running = false;
|
||||
} else if(he.getResponseCode() == 403) {
|
||||
LOG.info("Playlist for {} not accessible [HTTP 403]. Stopping now", model.getName());
|
||||
running = false;
|
||||
} else {
|
||||
throw he;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -220,24 +280,36 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
|
||||
private void splitRecording() {
|
||||
if(config.getSettings().splitRecordings > 0) {
|
||||
Duration recordingDuration = Duration.between(startTime, ZonedDateTime.now());
|
||||
Duration recordingDuration = Duration.between(splitRecStartTime, ZonedDateTime.now());
|
||||
long seconds = recordingDuration.getSeconds();
|
||||
if(seconds >= config.getSettings().splitRecordings) {
|
||||
streamer.stop();
|
||||
File target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-"+df.format(++splitCounter)+".ts"));
|
||||
mergeThread = createMergeThread(target, null, true);
|
||||
mergeThread.start();
|
||||
startTime = ZonedDateTime.now();
|
||||
try {
|
||||
targetFile = Config.getInstance().getFileForRecording(model);
|
||||
LOG.debug("Switching to file {}", targetFile.getAbsolutePath());
|
||||
fileChannel = FileChannel.open(targetFile.toPath(), CREATE, WRITE);
|
||||
MTSSink sink = ByteChannelSink.builder().setByteChannel(fileChannel).build();
|
||||
streamer.switchSink(sink);
|
||||
splitRecStartTime = ZonedDateTime.now();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error while splitting recording", e);
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForNewSegments(SegmentPlaylist lsp, int lastSegment) {
|
||||
private void waitForNewSegments(SegmentPlaylist lsp, int lastSegment, long downloadTookMillis) {
|
||||
try {
|
||||
long wait = 0;
|
||||
if (lastSegment == lsp.seq) {
|
||||
// playlist didn't change -> wait for at least half the target duration
|
||||
wait = (long) lsp.targetDuration * 1000 / 2;
|
||||
int timeLeftMillis = (int)(lsp.totalDuration * 1000 - downloadTookMillis);
|
||||
if(timeLeftMillis < 3000) { // we have less than 3 seconds to get the new playlist and start downloading it
|
||||
wait = 1;
|
||||
} else {
|
||||
// wait a second to be nice to the server (don't hammer it with requests)
|
||||
// 1 second seems to be a good compromise. every other calculation resulted in more missing segments
|
||||
wait = 1000;
|
||||
}
|
||||
LOG.trace("Playlist didn't change... waiting for {}ms", wait);
|
||||
} else {
|
||||
// playlist did change -> wait for at least last segment duration
|
||||
|
@ -256,7 +328,9 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
public void stop() {
|
||||
running = false;
|
||||
alive = false;
|
||||
if(streamer != null) {
|
||||
streamer.stop();
|
||||
}
|
||||
LOG.debug("Download stopped");
|
||||
}
|
||||
|
||||
|
@ -267,20 +341,20 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
.setProgressListener(listener)
|
||||
.build();
|
||||
|
||||
FileChannel channel = null;
|
||||
try {
|
||||
Path downloadDir = targetFile.getParentFile().toPath();
|
||||
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Files.createDirectories(downloadDir);
|
||||
}
|
||||
channel = FileChannel.open(targetFile.toPath(), CREATE, WRITE);
|
||||
MTSSink sink = ByteChannelSink.builder().setByteChannel(channel).build();
|
||||
fileChannel = FileChannel.open(targetFile.toPath(), CREATE, WRITE);
|
||||
MTSSink sink = ByteChannelSink.builder().setByteChannel(fileChannel).build();
|
||||
|
||||
streamer = Streamer.builder()
|
||||
.setSource(multiSource)
|
||||
.setSink(sink)
|
||||
.setSleepingEnabled(liveStream)
|
||||
.setBufferSize(10)
|
||||
.setName(model.getName())
|
||||
.build();
|
||||
|
||||
// Start streaming
|
||||
|
@ -293,11 +367,12 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
} catch(Exception e) {
|
||||
LOG.error("Error while saving stream to file", e);
|
||||
} finally {
|
||||
closeFile(channel);
|
||||
deleteEmptyRecording(targetFile);
|
||||
running = false;
|
||||
closeFile(fileChannel);
|
||||
}
|
||||
});
|
||||
t.setName("Segment Merger Thread");
|
||||
t.setName("Segment Merger Thread [" + model.getName() + "]");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
}
|
||||
|
@ -308,22 +383,22 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
Files.delete(targetFile.toPath());
|
||||
Files.delete(targetFile.getParentFile().toPath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error while deleting empty recording {}", targetFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeFile(FileChannel channel) {
|
||||
try {
|
||||
if (channel != null) {
|
||||
if (channel != null && channel.isOpen()) {
|
||||
channel.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error while closing file channel", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SegmentDownload implements Callable<byte[]> {
|
||||
private class SegmentDownload implements Callable<byte[]> {
|
||||
private URL url;
|
||||
private HttpClient client;
|
||||
|
||||
|
@ -333,24 +408,38 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
}
|
||||
|
||||
@Override
|
||||
public byte[] call() throws Exception {
|
||||
public byte[] call() throws IOException {
|
||||
LOG.trace("Downloading segment " + url.getFile());
|
||||
int maxTries = 3;
|
||||
for (int i = 1; i <= maxTries; i++) {
|
||||
try {
|
||||
for (int i = 1; i <= maxTries && running; i++) {
|
||||
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
|
||||
Response response = client.execute(request);
|
||||
try (Response response = client.execute(request)) {
|
||||
if(response.isSuccessful()) {
|
||||
byte[] segment = response.body().bytes();
|
||||
return segment;
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
if (i == maxTries) {
|
||||
LOG.warn("Error while downloading segment. Segment {} finally failed", url.getFile());
|
||||
} else {
|
||||
LOG.warn("Error while downloading segment {} on try {}", url.getFile(), i);
|
||||
LOG.warn("Error while downloading segment {} on try {}", url.getFile(), i, e);
|
||||
}
|
||||
if(model != null && !isModelOnline()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IOException("Unable to download segment " + url.getFile() + " after " + maxTries + " tries");
|
||||
throw new MissingSegmentException("Unable to download segment " + url.getFile() + " after " + maxTries + " tries");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isModelOnline() {
|
||||
try {
|
||||
return model.isOnline(IGNORE_CACHE);
|
||||
} catch (IOException | ExecutionException | InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package ctbrec.recorder.download;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class MissingSegmentException extends IOException {
|
||||
|
||||
public MissingSegmentException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
|
@ -54,7 +54,7 @@ public class StreamSource implements Comparable<StreamSource> {
|
|||
@Override
|
||||
public int compareTo(StreamSource o) {
|
||||
int heightDiff = height - o.height;
|
||||
if(heightDiff != 0) {
|
||||
if(heightDiff != 0 && height != Integer.MAX_VALUE && o.height != Integer.MAX_VALUE) {
|
||||
return heightDiff;
|
||||
} else {
|
||||
return bandwidth - o.bandwidth;
|
||||
|
|
|
@ -5,6 +5,7 @@ import java.io.InputStream;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
|
@ -24,6 +25,7 @@ import com.iheartradio.m3u8.data.PlaylistData;
|
|||
|
||||
import ctbrec.AbstractModel;
|
||||
import ctbrec.Config;
|
||||
import ctbrec.StringUtil;
|
||||
import ctbrec.io.HtmlParser;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
|
@ -38,6 +40,7 @@ public class Cam4Model extends AbstractModel {
|
|||
private String playlistUrl;
|
||||
private String onlineState = "offline";
|
||||
private int[] resolution = null;
|
||||
private boolean privateRoom = false;
|
||||
|
||||
@Override
|
||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
||||
|
@ -53,7 +56,9 @@ public class Cam4Model extends AbstractModel {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
return Objects.equals("NORMAL", onlineState);
|
||||
return (Objects.equals("NORMAL", onlineState) || Objects.equals("GROUP_SHOW_SELLING_TICKETS", onlineState))
|
||||
&& StringUtil.isNotBlank(playlistUrl)
|
||||
&& !privateRoom;
|
||||
}
|
||||
|
||||
private void loadModelDetails() throws IOException, ModelDetailsEmptyException {
|
||||
|
@ -64,11 +69,13 @@ public class Cam4Model extends AbstractModel {
|
|||
if(response.isSuccessful()) {
|
||||
JSONArray json = new JSONArray(response.body().string());
|
||||
if(json.length() == 0) {
|
||||
onlineState = "offline";
|
||||
throw new ModelDetailsEmptyException("Model details are empty");
|
||||
}
|
||||
JSONObject details = json.getJSONObject(0);
|
||||
onlineState = details.getString("showType");
|
||||
playlistUrl = details.getString("hlsPreviewUrl");
|
||||
privateRoom = details.getBoolean("privateRoom");
|
||||
if(details.has("resolution")) {
|
||||
String res = details.getString("resolution");
|
||||
String[] tokens = res.split(":");
|
||||
|
@ -104,7 +111,7 @@ public class Cam4Model extends AbstractModel {
|
|||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
src.height = playlist.getStreamInfo().getResolution().height;
|
||||
src.height = Optional.ofNullable(playlist.getStreamInfo()).map(si -> si.getResolution()).map(res -> res.height).orElse(0);
|
||||
String masterUrl = getPlaylistUrl();
|
||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUrl + playlist.getUri();
|
||||
|
|
|
@ -219,8 +219,16 @@ public class Chaturbate extends AbstractSite {
|
|||
}
|
||||
|
||||
StreamInfo getStreamInfo(String modelName) throws IOException, ExecutionException {
|
||||
return getStreamInfo(modelName, false);
|
||||
}
|
||||
|
||||
StreamInfo getStreamInfo(String modelName, boolean failFast) throws IOException, ExecutionException {
|
||||
if(failFast) {
|
||||
return streamInfoCache.getIfPresent(modelName);
|
||||
} else {
|
||||
return streamInfoCache.get(modelName);
|
||||
}
|
||||
}
|
||||
|
||||
StreamInfo loadStreamInfo(String modelName) throws HttpException, IOException, InterruptedException {
|
||||
throttleRequests();
|
||||
|
|
|
@ -6,6 +6,7 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
@ -39,14 +40,16 @@ public class ChaturbateModel extends AbstractModel {
|
|||
|
||||
@Override
|
||||
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||
StreamInfo info;
|
||||
String roomStatus;
|
||||
if(ignoreCache) {
|
||||
info = getChaturbate().loadStreamInfo(getName());
|
||||
StreamInfo info = getChaturbate().loadStreamInfo(getName());
|
||||
roomStatus = Optional.ofNullable(info).map(i -> i.room_status).orElse("");
|
||||
LOG.trace("Model {} room status: {}", getName(), info.room_status);
|
||||
} else {
|
||||
info = getChaturbate().getStreamInfo(getName());
|
||||
StreamInfo info = getChaturbate().getStreamInfo(getName(), true);
|
||||
roomStatus = Optional.ofNullable(info).map(i -> i.room_status).orElse("");
|
||||
}
|
||||
return Objects.equals("public", info.room_status);
|
||||
return Objects.equals("public", roomStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -221,7 +221,7 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
setName(state.getNm());
|
||||
setState(State.of(state.getVs()));
|
||||
setStreamUrl(streamUrl);
|
||||
Optional<Double> camScore = Optional.of(state.getM()).map(m -> m.getCamscore());
|
||||
Optional<Double> camScore = Optional.ofNullable(state.getM()).map(m -> m.getCamscore());
|
||||
setCamScore(camScore.orElse(0.0));
|
||||
|
||||
// preview
|
||||
|
|
|
@ -30,12 +30,14 @@ public class Streamer {
|
|||
private Thread streamingThread;
|
||||
|
||||
private boolean sleepingEnabled;
|
||||
private String name;
|
||||
|
||||
private Streamer(MTSSource source, MTSSink sink, int bufferSize, boolean sleepingEnabled) {
|
||||
private Streamer(MTSSource source, MTSSink sink, int bufferSize, boolean sleepingEnabled, String name) {
|
||||
this.source = source;
|
||||
this.sink = sink;
|
||||
this.bufferSize = bufferSize;
|
||||
this.sleepingEnabled = sleepingEnabled;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public void stream() throws InterruptedException {
|
||||
|
@ -48,20 +50,26 @@ public class Streamer {
|
|||
try {
|
||||
preBuffer();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Error while bufering", e);
|
||||
throw new IllegalStateException("Error while buffering", e);
|
||||
}
|
||||
log.info("Done PreBuffering");
|
||||
|
||||
bufferingThread = new Thread(this::fillBuffer, "buffering");
|
||||
bufferingThread = new Thread(this::fillBuffer, "Buffering ["+name+"]");
|
||||
bufferingThread.setDaemon(true);
|
||||
bufferingThread.start();
|
||||
|
||||
streamingThread = new Thread(this::internalStream, "streaming");
|
||||
streamingThread = new Thread(this::internalStream, "Streaming ["+name+"]");
|
||||
streamingThread.setDaemon(true);
|
||||
streamingThread.start();
|
||||
|
||||
bufferingThread.join();
|
||||
streamingThread.join();
|
||||
|
||||
try {
|
||||
sink.close();
|
||||
} catch(Exception e) {
|
||||
log.error("Couldn't close sink", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
|
@ -85,6 +93,16 @@ public class Streamer {
|
|||
}
|
||||
}
|
||||
|
||||
public void switchSink(MTSSink sink) {
|
||||
MTSSink old = this.sink;
|
||||
this.sink = sink;
|
||||
try {
|
||||
old.close();
|
||||
} catch (Exception e) {
|
||||
log.error("Couldn't close old sink while switching sinks", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void internalStream() {
|
||||
boolean resetState = false;
|
||||
MTSPacket packet = null;
|
||||
|
@ -123,7 +141,7 @@ public class Streamer {
|
|||
}
|
||||
}
|
||||
} catch (InterruptedException e1) {
|
||||
if(!endOfSourceReached) {
|
||||
if(!endOfSourceReached && !streamingShouldStop) {
|
||||
log.error("Interrupted while waiting for packet");
|
||||
continue;
|
||||
} else {
|
||||
|
@ -240,7 +258,7 @@ public class Streamer {
|
|||
// Stream packet
|
||||
// System.out.println("Streaming packet #" + packetCount + ", PID=" + mtsPacket.getPid() + ", pcrCount=" + pcrCount + ", continuityCounter=" + mtsPacket.getContinuityCounter());
|
||||
|
||||
if(!streamingShouldStop) {
|
||||
if(!streamingShouldStop && !Thread.interrupted()) {
|
||||
try {
|
||||
sink.send(packet);
|
||||
} catch (Exception e) {
|
||||
|
@ -275,7 +293,7 @@ public class Streamer {
|
|||
buffer.put(packet);
|
||||
put = true;
|
||||
} catch (InterruptedException ignored) {
|
||||
|
||||
log.error("Error adding packet to buffer", ignored);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -287,7 +305,11 @@ public class Streamer {
|
|||
log.error("Error reading from source", e);
|
||||
} finally {
|
||||
endOfSourceReached = true;
|
||||
try {
|
||||
streamingThread.interrupt();
|
||||
} catch(Exception e) {
|
||||
log.error("Couldn't interrupt streaming thread", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -308,6 +330,7 @@ public class Streamer {
|
|||
private MTSSource source;
|
||||
private int bufferSize = 1000;
|
||||
private boolean sleepingEnabled = false;
|
||||
private String name;
|
||||
|
||||
public StreamerBuilder setSink(MTSSink sink) {
|
||||
this.sink = sink;
|
||||
|
@ -329,10 +352,16 @@ public class Streamer {
|
|||
return this;
|
||||
}
|
||||
|
||||
public StreamerBuilder setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Streamer build() {
|
||||
Preconditions.checkNotNull(sink);
|
||||
Preconditions.checkNotNull(source);
|
||||
return new Streamer(source, sink, bufferSize, sleepingEnabled);
|
||||
}
|
||||
return new Streamer(source, sink, bufferSize, sleepingEnabled, name);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>1.11.0</version>
|
||||
<version>1.12.0</version>
|
||||
|
||||
<modules>
|
||||
<module>../common</module>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>1.11.0</version>
|
||||
<version>1.12.0</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -137,9 +137,13 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
|||
response = "{\"status\": \"success\", \"msg\": \"Recording resumed\"}";
|
||||
resp.getWriter().write(response);
|
||||
break;
|
||||
case "space":
|
||||
response = "{\"status\": \"success\", \"spaceTotal\": "+recorder.getTotalSpaceBytes()+", \"spaceFree\": "+recorder.getFreeSpaceBytes()+"}";
|
||||
resp.getWriter().write(response);
|
||||
break;
|
||||
default:
|
||||
resp.setStatus(SC_BAD_REQUEST);
|
||||
response = "{\"status\": \"error\", \"msg\": \"Unknown action\"}";
|
||||
response = "{\"status\": \"error\", \"msg\": \"Unknown action ["+request.action+"]\"}";
|
||||
resp.getWriter().write(response);
|
||||
break;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue