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
|
1.11.0
|
||||||
========================
|
========================
|
||||||
* Added model search function
|
* Added model search function
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
/target/
|
/target/
|
||||||
*~
|
*~
|
||||||
*.bak
|
*.bak
|
||||||
/ctbrec.log
|
/*.log
|
||||||
/ctbrec-tunnel.sh
|
/ctbrec-tunnel.sh
|
||||||
/jre/
|
/jre/
|
||||||
/server-local.sh
|
/server-local.sh
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>1.11.0</version>
|
<version>1.12.0</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
package ctbrec.ui;
|
package ctbrec.ui;
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
|
|
||||||
|
@ -12,9 +13,10 @@ public class JavaFxRecording extends Recording {
|
||||||
|
|
||||||
private transient StringProperty statusProperty = new SimpleStringProperty();
|
private transient StringProperty statusProperty = new SimpleStringProperty();
|
||||||
private transient StringProperty progressProperty = new SimpleStringProperty();
|
private transient StringProperty progressProperty = new SimpleStringProperty();
|
||||||
private transient StringProperty sizeProperty = new SimpleStringProperty();
|
private transient LongProperty sizeProperty = new SimpleLongProperty();
|
||||||
|
|
||||||
private Recording delegate;
|
private Recording delegate;
|
||||||
|
private long lastValue = 0;
|
||||||
|
|
||||||
public JavaFxRecording(Recording recording) {
|
public JavaFxRecording(Recording recording) {
|
||||||
this.delegate = recording;
|
this.delegate = recording;
|
||||||
|
@ -89,9 +91,7 @@ public class JavaFxRecording extends Recording {
|
||||||
@Override
|
@Override
|
||||||
public void setSizeInByte(long sizeInByte) {
|
public void setSizeInByte(long sizeInByte) {
|
||||||
delegate.setSizeInByte(sizeInByte);
|
delegate.setSizeInByte(sizeInByte);
|
||||||
double sizeInGiB = sizeInByte / 1024.0 / 1024 / 1024;
|
sizeProperty.set(sizeInByte);
|
||||||
DecimalFormat df = new DecimalFormat("0.00");
|
|
||||||
sizeProperty.setValue(df.format(sizeInGiB) + " GiB");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty getProgressProperty() {
|
public StringProperty getProgressProperty() {
|
||||||
|
@ -151,8 +151,13 @@ public class JavaFxRecording extends Recording {
|
||||||
return delegate.getSizeInByte();
|
return delegate.getSizeInByte();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty getSizeProperty() {
|
public LongProperty getSizeProperty() {
|
||||||
return sizeProperty;
|
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.net.URL;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
@ -47,13 +48,16 @@ import javafx.scene.Cursor;
|
||||||
import javafx.scene.control.Alert.AlertType;
|
import javafx.scene.control.Alert.AlertType;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.scene.control.ButtonType;
|
||||||
import javafx.scene.control.ContextMenu;
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.MenuItem;
|
import javafx.scene.control.MenuItem;
|
||||||
|
import javafx.scene.control.ProgressBar;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.control.TableCell;
|
import javafx.scene.control.TableCell;
|
||||||
import javafx.scene.control.TableColumn;
|
import javafx.scene.control.TableColumn;
|
||||||
import javafx.scene.control.TableColumn.SortType;
|
import javafx.scene.control.TableColumn.SortType;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
import javafx.scene.input.ContextMenuEvent;
|
import javafx.scene.input.ContextMenuEvent;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
|
@ -62,6 +66,9 @@ import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.FlowPane;
|
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.stage.FileChooser;
|
||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
@ -74,12 +81,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
private Recorder recorder;
|
private Recorder recorder;
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private List<Site> sites;
|
private List<Site> sites;
|
||||||
|
private long spaceTotal = -1;
|
||||||
|
private long spaceFree = -1;
|
||||||
|
|
||||||
FlowPane grid = new FlowPane();
|
FlowPane grid = new FlowPane();
|
||||||
ScrollPane scrollPane = new ScrollPane();
|
ScrollPane scrollPane = new ScrollPane();
|
||||||
TableView<JavaFxRecording> table = new TableView<JavaFxRecording>();
|
TableView<JavaFxRecording> table = new TableView<JavaFxRecording>();
|
||||||
ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList();
|
ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList();
|
||||||
ContextMenu popup;
|
ContextMenu popup;
|
||||||
|
ProgressBar spaceLeft;
|
||||||
|
Label spaceLabel;
|
||||||
|
|
||||||
public RecordingsTab(String title, Recorder recorder, Config config, List<Site> sites) {
|
public RecordingsTab(String title, Recorder recorder, Config config, List<Site> sites) {
|
||||||
super(title);
|
super(title);
|
||||||
|
@ -136,9 +147,37 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
TableColumn<JavaFxRecording, String> progress = new TableColumn<>("Progress");
|
TableColumn<JavaFxRecording, String> progress = new TableColumn<>("Progress");
|
||||||
progress.setCellValueFactory((cdf) -> cdf.getValue().getProgressProperty());
|
progress.setCellValueFactory((cdf) -> cdf.getValue().getProgressProperty());
|
||||||
progress.setPrefWidth(100);
|
progress.setPrefWidth(100);
|
||||||
TableColumn<JavaFxRecording, String> size = new TableColumn<>("Size");
|
TableColumn<JavaFxRecording, Number> size = new TableColumn<>("Size");
|
||||||
size.setCellValueFactory((cdf) -> cdf.getValue().getSizeProperty());
|
size.setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||||
size.setPrefWidth(100);
|
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.getColumns().addAll(name, date, status, progress, size);
|
||||||
table.setItems(observableRecordings);
|
table.setItems(observableRecordings);
|
||||||
|
@ -179,8 +218,21 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
});
|
});
|
||||||
scrollPane.setContent(table);
|
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();
|
BorderPane root = new BorderPane();
|
||||||
root.setPadding(new Insets(5));
|
root.setPadding(new Insets(5));
|
||||||
|
root.setTop(spaceBox);
|
||||||
root.setCenter(scrollPane);
|
root.setCenter(scrollPane);
|
||||||
setContent(root);
|
setContent(root);
|
||||||
|
|
||||||
|
@ -191,30 +243,8 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
updateService = createUpdateService();
|
updateService = createUpdateService();
|
||||||
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
|
||||||
updateService.setOnSucceeded((event) -> {
|
updateService.setOnSucceeded((event) -> {
|
||||||
List<JavaFxRecording> recordings = updateService.getValue();
|
updateRecordingsTable();
|
||||||
if (recordings == null) {
|
updateFreeSpaceDisplay();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Iterator<JavaFxRecording> iterator = observableRecordings.iterator(); iterator.hasNext();) {
|
|
||||||
JavaFxRecording old = iterator.next();
|
|
||||||
if (!recordings.contains(old)) {
|
|
||||||
// remove deleted recordings
|
|
||||||
iterator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (JavaFxRecording recording : recordings) {
|
|
||||||
if (!observableRecordings.contains(recording)) {
|
|
||||||
// add new recordings
|
|
||||||
observableRecordings.add(recording);
|
|
||||||
} else {
|
|
||||||
// update existing ones
|
|
||||||
int index = observableRecordings.indexOf(recording);
|
|
||||||
JavaFxRecording old = observableRecordings.get(index);
|
|
||||||
old.update(recording);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
table.sort();
|
|
||||||
});
|
});
|
||||||
updateService.setOnFailed((event) -> {
|
updateService.setOnFailed((event) -> {
|
||||||
LOG.info("Couldn't get list of recordings from recorder", event.getSource().getException());
|
LOG.info("Couldn't get list of recordings from recorder", event.getSource().getException());
|
||||||
|
@ -226,6 +256,46 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Iterator<JavaFxRecording> iterator = observableRecordings.iterator(); iterator.hasNext();) {
|
||||||
|
JavaFxRecording old = iterator.next();
|
||||||
|
if (!recordings.contains(old)) {
|
||||||
|
// remove deleted recordings
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (JavaFxRecording recording : recordings) {
|
||||||
|
if (!observableRecordings.contains(recording)) {
|
||||||
|
// add new recordings
|
||||||
|
observableRecordings.add(recording);
|
||||||
|
} else {
|
||||||
|
// update existing ones
|
||||||
|
int index = observableRecordings.indexOf(recording);
|
||||||
|
JavaFxRecording old = observableRecordings.get(index);
|
||||||
|
old.update(recording);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table.sort();
|
||||||
|
}
|
||||||
|
|
||||||
private ScheduledService<List<JavaFxRecording>> createUpdateService() {
|
private ScheduledService<List<JavaFxRecording>> createUpdateService() {
|
||||||
ScheduledService<List<JavaFxRecording>> updateService = new ScheduledService<List<JavaFxRecording>>() {
|
ScheduledService<List<JavaFxRecording>> updateService = new ScheduledService<List<JavaFxRecording>>() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -233,12 +303,23 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
return new Task<List<JavaFxRecording>>() {
|
return new Task<List<JavaFxRecording>>() {
|
||||||
@Override
|
@Override
|
||||||
public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
||||||
|
updateSpace();
|
||||||
|
|
||||||
List<JavaFxRecording> recordings = new ArrayList<>();
|
List<JavaFxRecording> recordings = new ArrayList<>();
|
||||||
for (Recording rec : recorder.getRecordings()) {
|
for (Recording rec : recorder.getRecordings()) {
|
||||||
recordings.add(new JavaFxRecording(rec));
|
recordings.add(new JavaFxRecording(rec));
|
||||||
}
|
}
|
||||||
return recordings;
|
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 {
|
public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(SettingsTab.class);
|
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;
|
public static final int CHECKBOX_MARGIN = 6;
|
||||||
private TextField recordingsDirectory;
|
private TextField recordingsDirectory;
|
||||||
|
@ -65,6 +66,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
private TextField server;
|
private TextField server;
|
||||||
private TextField port;
|
private TextField port;
|
||||||
private TextField onlineCheckIntervalInSecs;
|
private TextField onlineCheckIntervalInSecs;
|
||||||
|
private TextField leaveSpaceOnDevice;
|
||||||
private CheckBox loadResolution;
|
private CheckBox loadResolution;
|
||||||
private CheckBox secureCommunication = new CheckBox();
|
private CheckBox secureCommunication = new CheckBox();
|
||||||
private CheckBox chooseStreamQuality = new CheckBox();
|
private CheckBox chooseStreamQuality = new CheckBox();
|
||||||
|
@ -120,7 +122,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
// left side
|
// left side
|
||||||
leftSide.getChildren().add(createGeneralPanel());
|
leftSide.getChildren().add(createGeneralPanel());
|
||||||
leftSide.getChildren().add(createLocationsPanel());
|
leftSide.getChildren().add(createRecorderPanel());
|
||||||
leftSide.getChildren().add(createRecordLocationPanel());
|
leftSide.getChildren().add(createRecordLocationPanel());
|
||||||
|
|
||||||
//right side
|
//right side
|
||||||
|
@ -253,9 +255,20 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
return recordLocation;
|
return recordLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Node createLocationsPanel() {
|
private Node createRecorderPanel() {
|
||||||
int row = 0;
|
int row = 0;
|
||||||
GridPane layout = createGridLayout();
|
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);
|
layout.add(new Label("Recordings Directory"), 0, row);
|
||||||
recordingsDirectory = new TextField(Config.getInstance().getSettings().recordingsDir);
|
recordingsDirectory = new TextField(Config.getInstance().getSettings().recordingsDir);
|
||||||
recordingsDirectory.focusedProperty().addListener(createRecordingsDirectoryFocusListener());
|
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));
|
GridPane.setMargin(directoryStructure, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||||
layout.add(directoryStructure, 1, row++);
|
layout.add(directoryStructure, 1, row++);
|
||||||
|
|
||||||
layout.add(new Label("Post-Processing"), 0, row);
|
Label l = new Label("Maximum resolution (0 = unlimited)");
|
||||||
postProcessing = new TextField(Config.getInstance().getSettings().postProcessing);
|
layout.add(l, 0, row);
|
||||||
postProcessing.focusedProperty().addListener(createPostProcessingFocusListener());
|
List<Integer> resolutionOptions = new ArrayList<>();
|
||||||
GridPane.setFillWidth(postProcessing, true);
|
resolutionOptions.add(1080);
|
||||||
GridPane.setHgrow(postProcessing, Priority.ALWAYS);
|
resolutionOptions.add(720);
|
||||||
GridPane.setColumnSpan(postProcessing, 2);
|
resolutionOptions.add(600);
|
||||||
GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
resolutionOptions.add(480);
|
||||||
layout.add(postProcessing, 1, row);
|
resolutionOptions.add(0);
|
||||||
postProcessingDirectoryButton = createPostProcessingBrowseButton();
|
maxResolution = new ComboBox<>(FXCollections.observableList(resolutionOptions));
|
||||||
layout.add(postProcessingDirectoryButton, 3, row++);
|
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);
|
layout.add(new Label("Player"), 0, row);
|
||||||
mediaPlayer = new TextField(Config.getInstance().getSettings().mediaPlayer);
|
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(mediaPlayer, 1, row);
|
||||||
layout.add(createMpvBrowseButton(), 3, row++);
|
layout.add(createMpvBrowseButton(), 3, row++);
|
||||||
|
|
||||||
TitledPane locations = new TitledPane("Locations", layout);
|
Label l = new Label("Allow multiple players");
|
||||||
locations.setCollapsible(false);
|
layout.add(l, 0, row);
|
||||||
return locations;
|
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() {
|
l = new Label("Display stream resolution in overview");
|
||||||
GridPane layout = createGridLayout();
|
|
||||||
int row = 0;
|
|
||||||
Label l = new Label("Display stream resolution in overview");
|
|
||||||
layout.add(l, 0, row);
|
layout.add(l, 0, row);
|
||||||
loadResolution = new CheckBox();
|
loadResolution = new CheckBox();
|
||||||
loadResolution.setSelected(Config.getInstance().getSettings().determineResolution);
|
loadResolution.setSelected(Config.getInstance().getSettings().determineResolution);
|
||||||
|
@ -323,20 +420,10 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
ThumbOverviewTab.queue.clear();
|
ThumbOverviewTab.queue.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||||
GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
GridPane.setMargin(loadResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||||
layout.add(loadResolution, 1, row++);
|
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");
|
l = new Label("Manually select stream quality");
|
||||||
layout.add(l, 0, row);
|
layout.add(l, 0, row);
|
||||||
|
@ -357,60 +444,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
saveConfig();
|
saveConfig();
|
||||||
});
|
});
|
||||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
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++);
|
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");
|
l = new Label("Start Tab");
|
||||||
layout.add(l, 0, row);
|
layout.add(l, 0, row);
|
||||||
startTab = new ComboBox<>();
|
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(l, new Insets(0, 0, 0, 0));
|
||||||
GridPane.setMargin(colorSettingsPane, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
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);
|
TitledPane general = new TitledPane("General", layout);
|
||||||
general.setCollapsible(false);
|
general.setCollapsible(false);
|
||||||
return general;
|
return general;
|
||||||
|
@ -481,6 +511,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
postProcessing.setDisable(!local);
|
postProcessing.setDisable(!local);
|
||||||
postProcessingDirectoryButton.setDisable(!local);
|
postProcessingDirectoryButton.setDisable(!local);
|
||||||
directoryStructure.setDisable(!local);
|
directoryStructure.setDisable(!local);
|
||||||
|
onlineCheckIntervalInSecs.setDisable(!local);
|
||||||
|
leaveSpaceOnDevice.setDisable(!local);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChangeListener<? super Boolean> createRecordingsDirectoryFocusListener() {
|
private ChangeListener<? super Boolean> createRecordingsDirectoryFocusListener() {
|
||||||
|
@ -653,9 +685,10 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void selected() {
|
public void selected() {
|
||||||
startTab.getItems().clear();
|
if(startTab.getItems().isEmpty()) {
|
||||||
for(Tab tab : getTabPane().getTabs()) {
|
for(Tab tab : getTabPane().getTabs()) {
|
||||||
startTab.getItems().add(tab.getText());
|
startTab.getItems().add(tab.getText());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
String startTabName = Config.getInstance().getSettings().startTab;
|
String startTabName = Config.getInstance().getSettings().startTab;
|
||||||
if(StringUtil.isNotBlank(startTabName)) {
|
if(StringUtil.isNotBlank(startTabName)) {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>1.11.0</version>
|
<version>1.12.0</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
|
@ -100,10 +100,14 @@ public class Config {
|
||||||
Files.write(configFile.toPath(), json.getBytes("utf-8"), CREATE, WRITE, TRUNCATE_EXISTING);
|
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");
|
return Objects.equals(System.getProperty("ctbrec.server.mode"), "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isDevMode() {
|
||||||
|
return Objects.equals(System.getenv("CTBREC_DEV"), "1");
|
||||||
|
}
|
||||||
|
|
||||||
public File getConfigDir() {
|
public File getConfigDir() {
|
||||||
return configDir;
|
return configDir;
|
||||||
}
|
}
|
||||||
|
@ -113,10 +117,6 @@ public class Config {
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
|
||||||
String startTime = sdf.format(new Date());
|
String startTime = sdf.format(new Date());
|
||||||
File targetFile = new File(dirForRecording, model.getName() + '_' + startTime + ".ts");
|
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;
|
return targetFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ public class Settings {
|
||||||
public String httpServer = "localhost";
|
public String httpServer = "localhost";
|
||||||
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
|
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
|
||||||
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
|
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
|
||||||
|
public long minimumSpaceLeftInBytes = 0;
|
||||||
public String mediaPlayer = "/usr/bin/mpv";
|
public String mediaPlayer = "/usr/bin/mpv";
|
||||||
public String postProcessing = "";
|
public String postProcessing = "";
|
||||||
public String username = ""; // chaturbate username TODO maybe rename this onetime
|
public String username = ""; // chaturbate username TODO maybe rename this onetime
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package ctbrec;
|
package ctbrec;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
|
||||||
public class StringUtil {
|
public class StringUtil {
|
||||||
public static boolean isBlank(String s) {
|
public static boolean isBlank(String s) {
|
||||||
return s == null || s.trim().isEmpty();
|
return s == null || s.trim().isEmpty();
|
||||||
|
@ -8,4 +10,21 @@ public class StringUtil {
|
||||||
public static boolean isNotBlank(String s) {
|
public static boolean isNotBlank(String s) {
|
||||||
return !isBlank(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.File;
|
||||||
import java.io.FilenameFilter;
|
import java.io.FilenameFilter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.FileStore;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
@ -63,6 +64,7 @@ public class LocalRecorder implements Recorder {
|
||||||
private List<File> deleteInProgress = Collections.synchronizedList(new ArrayList<>());
|
private List<File> deleteInProgress = Collections.synchronizedList(new ArrayList<>());
|
||||||
private RecorderHttpClient client = new RecorderHttpClient();
|
private RecorderHttpClient client = new RecorderHttpClient();
|
||||||
private ReentrantLock lock = new ReentrantLock();
|
private ReentrantLock lock = new ReentrantLock();
|
||||||
|
private long lastSpaceMessage = 0;
|
||||||
|
|
||||||
public LocalRecorder(Config config) {
|
public LocalRecorder(Config config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
@ -81,7 +83,7 @@ public class LocalRecorder implements Recorder {
|
||||||
onlineMonitor.start();
|
onlineMonitor.start();
|
||||||
|
|
||||||
postProcessingTrigger = new PostProcessingTrigger();
|
postProcessingTrigger = new PostProcessingTrigger();
|
||||||
if(Config.getInstance().isServerMode()) {
|
if(Config.isServerMode()) {
|
||||||
postProcessingTrigger.start();
|
postProcessingTrigger.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +135,6 @@ public class LocalRecorder implements Recorder {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.debug("Starting recording for model {}", model.getName());
|
|
||||||
if (recordingProcesses.containsKey(model)) {
|
if (recordingProcesses.containsKey(model)) {
|
||||||
LOG.error("A recording for model {} is already running", model);
|
LOG.error("A recording for model {} is already running", model);
|
||||||
return;
|
return;
|
||||||
|
@ -149,8 +150,18 @@ public class LocalRecorder implements Recorder {
|
||||||
lock.unlock();
|
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;
|
Download download;
|
||||||
if (Config.getInstance().isServerMode()) {
|
if (Config.isServerMode()) {
|
||||||
download = new HlsDownload(client);
|
download = new HlsDownload(client);
|
||||||
} else {
|
} else {
|
||||||
download = new MergedHlsDownload(client);
|
download = new MergedHlsDownload(client);
|
||||||
|
@ -173,7 +184,7 @@ public class LocalRecorder implements Recorder {
|
||||||
Download download = recordingProcesses.get(model);
|
Download download = recordingProcesses.get(model);
|
||||||
download.stop();
|
download.stop();
|
||||||
recordingProcesses.remove(model);
|
recordingProcesses.remove(model);
|
||||||
if(!Config.getInstance().isServerMode()) {
|
if(!Config.isServerMode()) {
|
||||||
postprocess(download);
|
postprocess(download);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -329,6 +340,15 @@ public class LocalRecorder implements Recorder {
|
||||||
public void run() {
|
public void run() {
|
||||||
running = true;
|
running = true;
|
||||||
while (running) {
|
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<>();
|
List<Model> restart = new ArrayList<>();
|
||||||
for (Iterator<Entry<Model, Download>> iterator = recordingProcesses.entrySet().iterator(); iterator.hasNext();) {
|
for (Iterator<Entry<Model, Download>> iterator = recordingProcesses.entrySet().iterator(); iterator.hasNext();) {
|
||||||
Entry<Model, Download> entry = iterator.next();
|
Entry<Model, Download> entry = iterator.next();
|
||||||
|
@ -338,7 +358,7 @@ public class LocalRecorder implements Recorder {
|
||||||
LOG.debug("Recording terminated for model {}", m.getName());
|
LOG.debug("Recording terminated for model {}", m.getName());
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
restart.add(m);
|
restart.add(m);
|
||||||
if(config.isServerMode()) {
|
if(Config.isServerMode()) {
|
||||||
try {
|
try {
|
||||||
finishRecording(d.getTarget());
|
finishRecording(d.getTarget());
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
@ -365,7 +385,7 @@ public class LocalRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void finishRecording(File directory) {
|
private void finishRecording(File directory) {
|
||||||
if(Config.getInstance().isServerMode()) {
|
if(Config.isServerMode()) {
|
||||||
Thread t = new Thread() {
|
Thread t = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
@ -415,7 +435,7 @@ public class LocalRecorder implements Recorder {
|
||||||
boolean isOnline = model.isOnline(IGNORE_CACHE);
|
boolean isOnline = model.isOnline(IGNORE_CACHE);
|
||||||
LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline"));
|
LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline"));
|
||||||
if (isOnline && !isSuspended(model) && !recordingProcesses.containsKey(model)) {
|
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);
|
startRecordingProcess(model);
|
||||||
}
|
}
|
||||||
} catch (HttpException e) {
|
} catch (HttpException e) {
|
||||||
|
@ -493,7 +513,7 @@ public class LocalRecorder implements Recorder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Recording> getRecordings() {
|
public List<Recording> getRecordings() {
|
||||||
if(Config.getInstance().isServerMode()) {
|
if(Config.isServerMode()) {
|
||||||
return listSegmentedRecordings();
|
return listSegmentedRecordings();
|
||||||
} else {
|
} else {
|
||||||
return listMergedRecordings();
|
return listMergedRecordings();
|
||||||
|
@ -538,7 +558,7 @@ public class LocalRecorder implements Recorder {
|
||||||
return GENERATING_PLAYLIST;
|
return GENERATING_PLAYLIST;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.isServerMode()) {
|
if (Config.isServerMode()) {
|
||||||
if (recording.hasPlaylist()) {
|
if (recording.hasPlaylist()) {
|
||||||
return FINISHED;
|
return FINISHED;
|
||||||
} else {
|
} else {
|
||||||
|
@ -745,4 +765,25 @@ public class LocalRecorder implements Recorder {
|
||||||
public HttpClient getHttpClient() {
|
public HttpClient getHttpClient() {
|
||||||
return client;
|
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 List<Model> getOnlineModels();
|
||||||
|
|
||||||
public HttpClient getHttpClient();
|
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.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -45,6 +46,8 @@ public class RemoteRecorder implements Recorder {
|
||||||
private List<Model> models = Collections.emptyList();
|
private List<Model> models = Collections.emptyList();
|
||||||
private List<Model> onlineModels = Collections.emptyList();
|
private List<Model> onlineModels = Collections.emptyList();
|
||||||
private List<Site> sites;
|
private List<Site> sites;
|
||||||
|
private long spaceTotal = -1;
|
||||||
|
private long spaceFree = -1;
|
||||||
|
|
||||||
private Config config;
|
private Config config;
|
||||||
private HttpClient client;
|
private HttpClient client;
|
||||||
|
@ -150,10 +153,35 @@ public class RemoteRecorder implements Recorder {
|
||||||
while(running) {
|
while(running) {
|
||||||
syncModels();
|
syncModels();
|
||||||
syncOnlineModels();
|
syncOnlineModels();
|
||||||
|
syncSpace();
|
||||||
sleep();
|
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() {
|
private void syncModels() {
|
||||||
try {
|
try {
|
||||||
String msg = "{\"action\": \"list\"}";
|
String msg = "{\"action\": \"list\"}";
|
||||||
|
@ -362,4 +390,14 @@ public class RemoteRecorder implements Recorder {
|
||||||
public HttpClient getHttpClient() {
|
public HttpClient getHttpClient() {
|
||||||
return client;
|
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.nio.file.Path;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.text.DecimalFormat;
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZonedDateTime;
|
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.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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -48,11 +56,12 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
private BlockingMultiMTSSource multiSource;
|
private BlockingMultiMTSSource multiSource;
|
||||||
private Thread mergeThread;
|
private Thread mergeThread;
|
||||||
private Streamer streamer;
|
private Streamer streamer;
|
||||||
private ZonedDateTime startTime;
|
private ZonedDateTime splitRecStartTime;
|
||||||
private Config config;
|
private Config config;
|
||||||
private File targetFile;
|
private File targetFile;
|
||||||
private DecimalFormat df = new DecimalFormat("00000");
|
private BlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
|
||||||
private int splitCounter = 0;
|
private ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue);
|
||||||
|
private FileChannel fileChannel = null;
|
||||||
|
|
||||||
public MergedHlsDownload(HttpClient client) {
|
public MergedHlsDownload(HttpClient client) {
|
||||||
super(client);
|
super(client);
|
||||||
|
@ -67,6 +76,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
try {
|
try {
|
||||||
running = true;
|
running = true;
|
||||||
super.startTime = Instant.now();
|
super.startTime = Instant.now();
|
||||||
|
splitRecStartTime = ZonedDateTime.now();
|
||||||
mergeThread = createMergeThread(targetFile, progressListener, false);
|
mergeThread = createMergeThread(targetFile, progressListener, false);
|
||||||
LOG.debug("Merge thread started");
|
LOG.debug("Merge thread started");
|
||||||
mergeThread.start();
|
mergeThread.start();
|
||||||
|
@ -81,7 +91,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
downloadSegments(segmentPlaylistUri, false);
|
downloadSegments(segmentPlaylistUri, false);
|
||||||
LOG.debug("Waiting for merge thread to finish");
|
LOG.debug("Waiting for merge thread to finish");
|
||||||
mergeThread.join();
|
mergeThread.join();
|
||||||
LOG.debug("Merge thread to finished");
|
LOG.debug("Merge thread finished");
|
||||||
} catch(ParseException e) {
|
} catch(ParseException e) {
|
||||||
throw new IOException("Couldn't parse stream information", e);
|
throw new IOException("Couldn't parse stream information", e);
|
||||||
} catch(PlaylistException e) {
|
} catch(PlaylistException e) {
|
||||||
|
@ -92,7 +102,12 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
throw new IOException("Couldn't add HMAC to playlist url", e);
|
throw new IOException("Couldn't add HMAC to playlist url", e);
|
||||||
} finally {
|
} finally {
|
||||||
alive = false;
|
alive = false;
|
||||||
streamer.stop();
|
try {
|
||||||
|
streamer.stop();
|
||||||
|
} catch(Exception e) {
|
||||||
|
LOG.error("Couldn't stop streamer", e);
|
||||||
|
}
|
||||||
|
downloadThreadPool.shutdown();
|
||||||
LOG.debug("Download terminated for {}", segmentPlaylistUri);
|
LOG.debug("Download terminated for {}", segmentPlaylistUri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,6 +122,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
running = true;
|
running = true;
|
||||||
super.startTime = Instant.now();
|
super.startTime = Instant.now();
|
||||||
|
splitRecStartTime = ZonedDateTime.now();
|
||||||
super.model = model;
|
super.model = model;
|
||||||
targetFile = Config.getInstance().getFileForRecording(model);
|
targetFile = Config.getInstance().getFileForRecording(model);
|
||||||
String segments = getSegmentPlaylistUrl(model);
|
String segments = getSegmentPlaylistUrl(model);
|
||||||
|
@ -114,6 +130,9 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
mergeThread.start();
|
mergeThread.start();
|
||||||
if(segments != null) {
|
if(segments != null) {
|
||||||
downloadSegments(segments, true);
|
downloadSegments(segments, true);
|
||||||
|
if(config.getSettings().splitRecordings > 0) {
|
||||||
|
LOG.debug("Splitting recordings every {} seconds", config.getSettings().splitRecordings);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("Couldn't determine segments uri");
|
throw new IOException("Couldn't determine segments uri");
|
||||||
}
|
}
|
||||||
|
@ -129,7 +148,11 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
} finally {
|
} finally {
|
||||||
alive = false;
|
alive = false;
|
||||||
if(streamer != null) {
|
if(streamer != null) {
|
||||||
streamer.stop();
|
try {
|
||||||
|
streamer.stop();
|
||||||
|
} catch(Exception e) {
|
||||||
|
LOG.error("Couldn't stop streamer", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
LOG.debug("Download for {} terminated", model);
|
LOG.debug("Download for {} terminated", model);
|
||||||
}
|
}
|
||||||
|
@ -138,36 +161,110 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
private void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
private void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
||||||
int lastSegment = 0;
|
int lastSegment = 0;
|
||||||
int nextSegment = 0;
|
int nextSegment = 0;
|
||||||
|
long playlistNotFoundFirstEncounter = -1;
|
||||||
while(running) {
|
while(running) {
|
||||||
try {
|
try {
|
||||||
|
if(playlistNotFoundFirstEncounter != -1) {
|
||||||
|
LOG.debug("Downloading playlist {}", segmentPlaylistUri);
|
||||||
|
}
|
||||||
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
|
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
|
||||||
|
playlistNotFoundFirstEncounter = -1;
|
||||||
if(!livestreamDownload) {
|
if(!livestreamDownload) {
|
||||||
multiSource.setTotalSegments(lsp.segments.size());
|
multiSource.setTotalSegments(lsp.segments.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
// download segments, which might have been skipped
|
|
||||||
downloadMissedSegments(lsp, nextSegment);
|
|
||||||
|
|
||||||
// download new segments
|
// download new segments
|
||||||
|
long downloadStart = System.currentTimeMillis();
|
||||||
downloadNewSegments(lsp, nextSegment);
|
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) {
|
if(livestreamDownload) {
|
||||||
// split up the recording, if configured
|
// split up the recording, if configured
|
||||||
splitRecording();
|
splitRecording();
|
||||||
|
|
||||||
// wait some time until requesting the segment playlist again to not hammer the server
|
// wait some time until requesting the segment playlist again to not hammer the server
|
||||||
waitForNewSegments(lsp, lastSegment);
|
waitForNewSegments(lsp, lastSegment, downloadTookMillis);
|
||||||
|
|
||||||
lastSegment = lsp.seq;
|
lastSegment = lsp.seq;
|
||||||
nextSegment = lastSegment + lsp.segments.size();
|
nextSegment = lastSegment + lsp.segments.size();
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch(HttpException e) {
|
} catch(Exception e) {
|
||||||
if(e.getResponseCode() == 404) {
|
LOG.info("Unexpected error while downloading {}", model.getName(), e);
|
||||||
// playlist is gone -> model probably logged out
|
running = false;
|
||||||
LOG.debug("Playlist not found. Assuming model went offline");
|
}
|
||||||
running = false;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = downloadFuture.get();
|
||||||
|
writeSegment(segmentData);
|
||||||
|
} 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 {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -175,43 +272,6 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO switch to a lower bitrate/resolution ?!?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadNewSegments(SegmentPlaylist lsp, int nextSegment) throws MalformedURLException {
|
|
||||||
int skip = nextSegment - lsp.seq;
|
|
||||||
for (String segment : lsp.segments) {
|
|
||||||
if(skip > 0) {
|
|
||||||
skip--;
|
|
||||||
} else {
|
|
||||||
URL segmentUrl = new URL(segment);
|
|
||||||
try {
|
|
||||||
byte[] segmentData = new SegmentDownload(segmentUrl, client).call();
|
|
||||||
writeSegment(segmentData);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("Error while downloading segment {}", segmentUrl, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeSegment(byte[] segmentData) throws InterruptedException {
|
private void writeSegment(byte[] segmentData) throws InterruptedException {
|
||||||
InputStream in = new ByteArrayInputStream(segmentData);
|
InputStream in = new ByteArrayInputStream(segmentData);
|
||||||
InputStreamMTSSource source = InputStreamMTSSource.builder().setInputStream(in).build();
|
InputStreamMTSSource source = InputStreamMTSSource.builder().setInputStream(in).build();
|
||||||
|
@ -220,24 +280,36 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
private void splitRecording() {
|
private void splitRecording() {
|
||||||
if(config.getSettings().splitRecordings > 0) {
|
if(config.getSettings().splitRecordings > 0) {
|
||||||
Duration recordingDuration = Duration.between(startTime, ZonedDateTime.now());
|
Duration recordingDuration = Duration.between(splitRecStartTime, ZonedDateTime.now());
|
||||||
long seconds = recordingDuration.getSeconds();
|
long seconds = recordingDuration.getSeconds();
|
||||||
if(seconds >= config.getSettings().splitRecordings) {
|
if(seconds >= config.getSettings().splitRecordings) {
|
||||||
streamer.stop();
|
try {
|
||||||
File target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-"+df.format(++splitCounter)+".ts"));
|
targetFile = Config.getInstance().getFileForRecording(model);
|
||||||
mergeThread = createMergeThread(target, null, true);
|
LOG.debug("Switching to file {}", targetFile.getAbsolutePath());
|
||||||
mergeThread.start();
|
fileChannel = FileChannel.open(targetFile.toPath(), CREATE, WRITE);
|
||||||
startTime = ZonedDateTime.now();
|
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 {
|
try {
|
||||||
long wait = 0;
|
long wait = 0;
|
||||||
if (lastSegment == lsp.seq) {
|
if (lastSegment == lsp.seq) {
|
||||||
// playlist didn't change -> wait for at least half the target duration
|
int timeLeftMillis = (int)(lsp.totalDuration * 1000 - downloadTookMillis);
|
||||||
wait = (long) lsp.targetDuration * 1000 / 2;
|
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);
|
LOG.trace("Playlist didn't change... waiting for {}ms", wait);
|
||||||
} else {
|
} else {
|
||||||
// playlist did change -> wait for at least last segment duration
|
// playlist did change -> wait for at least last segment duration
|
||||||
|
@ -256,7 +328,9 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
public void stop() {
|
public void stop() {
|
||||||
running = false;
|
running = false;
|
||||||
alive = false;
|
alive = false;
|
||||||
streamer.stop();
|
if(streamer != null) {
|
||||||
|
streamer.stop();
|
||||||
|
}
|
||||||
LOG.debug("Download stopped");
|
LOG.debug("Download stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,20 +341,20 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
.setProgressListener(listener)
|
.setProgressListener(listener)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
FileChannel channel = null;
|
|
||||||
try {
|
try {
|
||||||
Path downloadDir = targetFile.getParentFile().toPath();
|
Path downloadDir = targetFile.getParentFile().toPath();
|
||||||
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
|
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
Files.createDirectories(downloadDir);
|
Files.createDirectories(downloadDir);
|
||||||
}
|
}
|
||||||
channel = FileChannel.open(targetFile.toPath(), CREATE, WRITE);
|
fileChannel = FileChannel.open(targetFile.toPath(), CREATE, WRITE);
|
||||||
MTSSink sink = ByteChannelSink.builder().setByteChannel(channel).build();
|
MTSSink sink = ByteChannelSink.builder().setByteChannel(fileChannel).build();
|
||||||
|
|
||||||
streamer = Streamer.builder()
|
streamer = Streamer.builder()
|
||||||
.setSource(multiSource)
|
.setSource(multiSource)
|
||||||
.setSink(sink)
|
.setSink(sink)
|
||||||
.setSleepingEnabled(liveStream)
|
.setSleepingEnabled(liveStream)
|
||||||
.setBufferSize(10)
|
.setBufferSize(10)
|
||||||
|
.setName(model.getName())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Start streaming
|
// Start streaming
|
||||||
|
@ -293,11 +367,12 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
LOG.error("Error while saving stream to file", e);
|
LOG.error("Error while saving stream to file", e);
|
||||||
} finally {
|
} finally {
|
||||||
closeFile(channel);
|
|
||||||
deleteEmptyRecording(targetFile);
|
deleteEmptyRecording(targetFile);
|
||||||
|
running = false;
|
||||||
|
closeFile(fileChannel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
t.setName("Segment Merger Thread");
|
t.setName("Segment Merger Thread [" + model.getName() + "]");
|
||||||
t.setDaemon(true);
|
t.setDaemon(true);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
@ -308,22 +383,22 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
Files.delete(targetFile.toPath());
|
Files.delete(targetFile.toPath());
|
||||||
Files.delete(targetFile.getParentFile().toPath());
|
Files.delete(targetFile.getParentFile().toPath());
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
LOG.error("Error while deleting empty recording {}", targetFile);
|
LOG.error("Error while deleting empty recording {}", targetFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closeFile(FileChannel channel) {
|
private void closeFile(FileChannel channel) {
|
||||||
try {
|
try {
|
||||||
if (channel != null) {
|
if (channel != null && channel.isOpen()) {
|
||||||
channel.close();
|
channel.close();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
LOG.error("Error while closing file channel", 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 URL url;
|
||||||
private HttpClient client;
|
private HttpClient client;
|
||||||
|
|
||||||
|
@ -333,24 +408,38 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] call() throws Exception {
|
public byte[] call() throws IOException {
|
||||||
LOG.trace("Downloading segment " + url.getFile());
|
LOG.trace("Downloading segment " + url.getFile());
|
||||||
int maxTries = 3;
|
int maxTries = 3;
|
||||||
for (int i = 1; i <= maxTries; i++) {
|
for (int i = 1; i <= maxTries && running; i++) {
|
||||||
try {
|
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
|
||||||
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
|
try (Response response = client.execute(request)) {
|
||||||
Response response = client.execute(request);
|
if(response.isSuccessful()) {
|
||||||
byte[] segment = response.body().bytes();
|
byte[] segment = response.body().bytes();
|
||||||
return segment;
|
return segment;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
if (i == maxTries) {
|
if (i == maxTries) {
|
||||||
LOG.warn("Error while downloading segment. Segment {} finally failed", url.getFile());
|
LOG.warn("Error while downloading segment. Segment {} finally failed", url.getFile());
|
||||||
} else {
|
} 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
|
@Override
|
||||||
public int compareTo(StreamSource o) {
|
public int compareTo(StreamSource o) {
|
||||||
int heightDiff = height - o.height;
|
int heightDiff = height - o.height;
|
||||||
if(heightDiff != 0) {
|
if(heightDiff != 0 && height != Integer.MAX_VALUE && o.height != Integer.MAX_VALUE) {
|
||||||
return heightDiff;
|
return heightDiff;
|
||||||
} else {
|
} else {
|
||||||
return bandwidth - o.bandwidth;
|
return bandwidth - o.bandwidth;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
|
@ -24,6 +25,7 @@ import com.iheartradio.m3u8.data.PlaylistData;
|
||||||
|
|
||||||
import ctbrec.AbstractModel;
|
import ctbrec.AbstractModel;
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
import ctbrec.io.HtmlParser;
|
import ctbrec.io.HtmlParser;
|
||||||
import ctbrec.io.HttpException;
|
import ctbrec.io.HttpException;
|
||||||
import ctbrec.recorder.download.StreamSource;
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
@ -38,6 +40,7 @@ public class Cam4Model extends AbstractModel {
|
||||||
private String playlistUrl;
|
private String playlistUrl;
|
||||||
private String onlineState = "offline";
|
private String onlineState = "offline";
|
||||||
private int[] resolution = null;
|
private int[] resolution = null;
|
||||||
|
private boolean privateRoom = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
||||||
|
@ -53,7 +56,9 @@ public class Cam4Model extends AbstractModel {
|
||||||
return false;
|
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 {
|
private void loadModelDetails() throws IOException, ModelDetailsEmptyException {
|
||||||
|
@ -64,11 +69,13 @@ public class Cam4Model extends AbstractModel {
|
||||||
if(response.isSuccessful()) {
|
if(response.isSuccessful()) {
|
||||||
JSONArray json = new JSONArray(response.body().string());
|
JSONArray json = new JSONArray(response.body().string());
|
||||||
if(json.length() == 0) {
|
if(json.length() == 0) {
|
||||||
|
onlineState = "offline";
|
||||||
throw new ModelDetailsEmptyException("Model details are empty");
|
throw new ModelDetailsEmptyException("Model details are empty");
|
||||||
}
|
}
|
||||||
JSONObject details = json.getJSONObject(0);
|
JSONObject details = json.getJSONObject(0);
|
||||||
onlineState = details.getString("showType");
|
onlineState = details.getString("showType");
|
||||||
playlistUrl = details.getString("hlsPreviewUrl");
|
playlistUrl = details.getString("hlsPreviewUrl");
|
||||||
|
privateRoom = details.getBoolean("privateRoom");
|
||||||
if(details.has("resolution")) {
|
if(details.has("resolution")) {
|
||||||
String res = details.getString("resolution");
|
String res = details.getString("resolution");
|
||||||
String[] tokens = res.split(":");
|
String[] tokens = res.split(":");
|
||||||
|
@ -104,7 +111,7 @@ public class Cam4Model extends AbstractModel {
|
||||||
if (playlist.hasStreamInfo()) {
|
if (playlist.hasStreamInfo()) {
|
||||||
StreamSource src = new StreamSource();
|
StreamSource src = new StreamSource();
|
||||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
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 masterUrl = getPlaylistUrl();
|
||||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||||
String segmentUri = baseUrl + playlist.getUri();
|
String segmentUri = baseUrl + playlist.getUri();
|
||||||
|
|
|
@ -219,7 +219,15 @@ public class Chaturbate extends AbstractSite {
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamInfo getStreamInfo(String modelName) throws IOException, ExecutionException {
|
StreamInfo getStreamInfo(String modelName) throws IOException, ExecutionException {
|
||||||
return streamInfoCache.get(modelName);
|
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 {
|
StreamInfo loadStreamInfo(String modelName) throws HttpException, IOException, InterruptedException {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -39,14 +40,16 @@ public class ChaturbateModel extends AbstractModel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||||
StreamInfo info;
|
String roomStatus;
|
||||||
if(ignoreCache) {
|
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);
|
LOG.trace("Model {} room status: {}", getName(), info.room_status);
|
||||||
} else {
|
} 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
|
@Override
|
||||||
|
|
|
@ -221,7 +221,7 @@ public class MyFreeCamsModel extends AbstractModel {
|
||||||
setName(state.getNm());
|
setName(state.getNm());
|
||||||
setState(State.of(state.getVs()));
|
setState(State.of(state.getVs()));
|
||||||
setStreamUrl(streamUrl);
|
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));
|
setCamScore(camScore.orElse(0.0));
|
||||||
|
|
||||||
// preview
|
// preview
|
||||||
|
|
|
@ -30,12 +30,14 @@ public class Streamer {
|
||||||
private Thread streamingThread;
|
private Thread streamingThread;
|
||||||
|
|
||||||
private boolean sleepingEnabled;
|
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.source = source;
|
||||||
this.sink = sink;
|
this.sink = sink;
|
||||||
this.bufferSize = bufferSize;
|
this.bufferSize = bufferSize;
|
||||||
this.sleepingEnabled = sleepingEnabled;
|
this.sleepingEnabled = sleepingEnabled;
|
||||||
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stream() throws InterruptedException {
|
public void stream() throws InterruptedException {
|
||||||
|
@ -48,20 +50,26 @@ public class Streamer {
|
||||||
try {
|
try {
|
||||||
preBuffer();
|
preBuffer();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalStateException("Error while bufering", e);
|
throw new IllegalStateException("Error while buffering", e);
|
||||||
}
|
}
|
||||||
log.info("Done PreBuffering");
|
log.info("Done PreBuffering");
|
||||||
|
|
||||||
bufferingThread = new Thread(this::fillBuffer, "buffering");
|
bufferingThread = new Thread(this::fillBuffer, "Buffering ["+name+"]");
|
||||||
bufferingThread.setDaemon(true);
|
bufferingThread.setDaemon(true);
|
||||||
bufferingThread.start();
|
bufferingThread.start();
|
||||||
|
|
||||||
streamingThread = new Thread(this::internalStream, "streaming");
|
streamingThread = new Thread(this::internalStream, "Streaming ["+name+"]");
|
||||||
streamingThread.setDaemon(true);
|
streamingThread.setDaemon(true);
|
||||||
streamingThread.start();
|
streamingThread.start();
|
||||||
|
|
||||||
bufferingThread.join();
|
bufferingThread.join();
|
||||||
streamingThread.join();
|
streamingThread.join();
|
||||||
|
|
||||||
|
try {
|
||||||
|
sink.close();
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Couldn't close sink", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
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() {
|
private void internalStream() {
|
||||||
boolean resetState = false;
|
boolean resetState = false;
|
||||||
MTSPacket packet = null;
|
MTSPacket packet = null;
|
||||||
|
@ -123,7 +141,7 @@ public class Streamer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e1) {
|
} catch (InterruptedException e1) {
|
||||||
if(!endOfSourceReached) {
|
if(!endOfSourceReached && !streamingShouldStop) {
|
||||||
log.error("Interrupted while waiting for packet");
|
log.error("Interrupted while waiting for packet");
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
|
@ -240,7 +258,7 @@ public class Streamer {
|
||||||
// Stream packet
|
// Stream packet
|
||||||
// System.out.println("Streaming packet #" + packetCount + ", PID=" + mtsPacket.getPid() + ", pcrCount=" + pcrCount + ", continuityCounter=" + mtsPacket.getContinuityCounter());
|
// System.out.println("Streaming packet #" + packetCount + ", PID=" + mtsPacket.getPid() + ", pcrCount=" + pcrCount + ", continuityCounter=" + mtsPacket.getContinuityCounter());
|
||||||
|
|
||||||
if(!streamingShouldStop) {
|
if(!streamingShouldStop && !Thread.interrupted()) {
|
||||||
try {
|
try {
|
||||||
sink.send(packet);
|
sink.send(packet);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -275,7 +293,7 @@ public class Streamer {
|
||||||
buffer.put(packet);
|
buffer.put(packet);
|
||||||
put = true;
|
put = true;
|
||||||
} catch (InterruptedException ignored) {
|
} 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);
|
log.error("Error reading from source", e);
|
||||||
} finally {
|
} finally {
|
||||||
endOfSourceReached = true;
|
endOfSourceReached = true;
|
||||||
streamingThread.interrupt();
|
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 MTSSource source;
|
||||||
private int bufferSize = 1000;
|
private int bufferSize = 1000;
|
||||||
private boolean sleepingEnabled = false;
|
private boolean sleepingEnabled = false;
|
||||||
|
private String name;
|
||||||
|
|
||||||
public StreamerBuilder setSink(MTSSink sink) {
|
public StreamerBuilder setSink(MTSSink sink) {
|
||||||
this.sink = sink;
|
this.sink = sink;
|
||||||
|
@ -329,10 +352,16 @@ public class Streamer {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public StreamerBuilder setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Streamer build() {
|
public Streamer build() {
|
||||||
Preconditions.checkNotNull(sink);
|
Preconditions.checkNotNull(sink);
|
||||||
Preconditions.checkNotNull(source);
|
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>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<version>1.11.0</version>
|
<version>1.12.0</version>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>../common</module>
|
<module>../common</module>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>1.11.0</version>
|
<version>1.12.0</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
|
@ -137,9 +137,13 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
||||||
response = "{\"status\": \"success\", \"msg\": \"Recording resumed\"}";
|
response = "{\"status\": \"success\", \"msg\": \"Recording resumed\"}";
|
||||||
resp.getWriter().write(response);
|
resp.getWriter().write(response);
|
||||||
break;
|
break;
|
||||||
|
case "space":
|
||||||
|
response = "{\"status\": \"success\", \"spaceTotal\": "+recorder.getTotalSpaceBytes()+", \"spaceFree\": "+recorder.getFreeSpaceBytes()+"}";
|
||||||
|
resp.getWriter().write(response);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
resp.setStatus(SC_BAD_REQUEST);
|
resp.setStatus(SC_BAD_REQUEST);
|
||||||
response = "{\"status\": \"error\", \"msg\": \"Unknown action\"}";
|
response = "{\"status\": \"error\", \"msg\": \"Unknown action ["+request.action+"]\"}";
|
||||||
resp.getWriter().write(response);
|
resp.getWriter().write(response);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue