forked from j62/ctbrec
1
0
Fork 0

Merge branch 'dev'

This commit is contained in:
0xboobface 2018-11-28 17:53:02 +01:00
commit a45ba8f35e
24 changed files with 641 additions and 244 deletions

View File

@ -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
client/.gitignore vendored
View File

@ -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

View File

@ -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>

View File

@ -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;
}
} }

View File

@ -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);
}
}
}; };
} }
}; };

View File

@ -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)) {

View File

@ -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>

View File

@ -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;
} }

View File

@ -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

View File

@ -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;
}
} }

View File

@ -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;
}
} }

View File

@ -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;
} }

View File

@ -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;
}
} }

View File

@ -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;
} }
} }
} }

View File

@ -0,0 +1,11 @@
package ctbrec.recorder.download;
import java.io.IOException;
public class MissingSegmentException extends IOException {
public MissingSegmentException(String msg) {
super(msg);
}
}

View File

@ -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;

View File

@ -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();

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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);
} }
} }
} }

View File

@ -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>

View File

@ -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>

View File

@ -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;
} }