forked from j62/ctbrec
1
0
Fork 0

Merge branch 'dev' into v4

This commit is contained in:
0xb00bface 2021-01-10 17:31:49 +01:00
commit e709e2d45d
27 changed files with 718 additions and 148 deletions

View File

@ -1,7 +1,16 @@
NEXT 3.12.1
========================
* Fix: "Resume all" started the recordings of models marked for later recording
3.12.0
======================== ========================
* Added "record later" tab to "bookmark" models * Added "record later" tab to "bookmark" models
* Added config option to show the total number of models in the title bar * Added config option to show the total number of models in the title bar
* Added support for hlsdl. Some sites (MV Live, LiveJasmin, Showup) are
excluded, because they need some special behavior while the download is
running. hlsdl can be activated in the settings under "Advanced" or with
the config properties "useHlsdl", "hlsdlExecutable" and "loghlsdlOutput".
The used bandwidth calculation does not work with hlsdl.
* Fixed problem with Cam4 playlist URLs, thanks @gohufrapoc * Fixed problem with Cam4 playlist URLs, thanks @gohufrapoc
3.11.0 3.11.0

View File

@ -106,7 +106,7 @@
<plugin> <plugin>
<groupId>com.akathist.maven.plugins.launch4j</groupId> <groupId>com.akathist.maven.plugins.launch4j</groupId>
<artifactId>launch4j-maven-plugin</artifactId> <artifactId>launch4j-maven-plugin</artifactId>
<version>1.7.22</version> <version>1.7.25</version>
<executions> <executions>
<execution> <execution>
<id>l4j-win</id> <id>l4j-win</id>
@ -132,7 +132,9 @@
<bundledJre64Bit>true</bundledJre64Bit> <bundledJre64Bit>true</bundledJre64Bit>
<minVersion>15</minVersion> <minVersion>15</minVersion>
<maxHeapSize>512</maxHeapSize> <maxHeapSize>512</maxHeapSize>
<opts>
<opt>-Dfile.encoding=utf-8</opt> <opt>-Dfile.encoding=utf-8</opt>
</opts>
</jre> </jre>
<versionInfo> <versionInfo>
<fileVersion>4.0.0.0</fileVersion> <fileVersion>4.0.0.0</fileVersion>

View File

@ -1,34 +0,0 @@
package ctbrec.ui.controls;
import ctbrec.ui.PauseIcon;
import javafx.scene.Cursor;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
public class PausedIndicator extends StackPane {
private PauseIcon pausedIcon;
private Rectangle clickPanel;
public PausedIndicator(int size, Color color) {
setMaxSize(size, size);
pausedIcon = new PauseIcon(color, size);
pausedIcon.setVisible(false);
clickPanel = new Rectangle(size, size);
clickPanel.setCursor(Cursor.HAND);
clickPanel.setFill(Paint.valueOf("#00000000"));
getChildren().add(pausedIcon);
getChildren().add(clickPanel);
pausedIcon.visibleProperty().bindBidirectional(visibleProperty());
clickPanel.onMouseClickedProperty().bindBidirectional(onMouseClickedProperty());
Tooltip tooltip = new Tooltip("Resume Recording");
Tooltip.install(clickPanel, tooltip);
}
}

View File

@ -0,0 +1,37 @@
package ctbrec.ui.controls;
import javafx.scene.Cursor;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
public class RecordingIndicator extends StackPane {
private ImageView icon;
private Rectangle clickPanel;
public RecordingIndicator(int size) {
setMaxSize(size, size);
icon = new ImageView();
icon.setVisible(false);
icon.prefHeight(size);
icon.prefWidth(size);
icon.maxHeight(size);
icon.maxWidth(size);
clickPanel = new Rectangle(size, size);
clickPanel.setCursor(Cursor.HAND);
clickPanel.setFill(Paint.valueOf("#00000000"));
getChildren().add(icon);
getChildren().add(clickPanel);
icon.visibleProperty().bindBidirectional(visibleProperty());
}
public void setImage(Image img) {
icon.setImage(img);
}
}

View File

@ -290,6 +290,7 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
ComboBox<Object> comboBox = new ComboBox(listProp); ComboBox<Object> comboBox = new ComboBox(listProp);
Field field = Settings.class.getField(setting.getKey()); Field field = Settings.class.getField(setting.getKey());
Object value = field.get(settings); Object value = field.get(settings);
LOG.debug("{} {} {}", setting.getName(), value, setting.getProperty().getValue());
if (StringUtil.isNotBlank(value.toString())) { if (StringUtil.isNotBlank(value.toString())) {
if (setting.getConverter() != null) { if (setting.getConverter() != null) {
comboBox.getSelectionModel().select(setting.getConverter().convertTo(value)); comboBox.getSelectionModel().select(setting.getConverter().convertTo(value));

View File

@ -9,6 +9,7 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -117,6 +118,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleLongProperty leaveSpaceOnDevice; private SimpleLongProperty leaveSpaceOnDevice;
private SimpleStringProperty ffmpegParameters; private SimpleStringProperty ffmpegParameters;
private SimpleBooleanProperty logFFmpegOutput; private SimpleBooleanProperty logFFmpegOutput;
private SimpleBooleanProperty loghlsdlOutput;
private SimpleStringProperty fileExtension; private SimpleStringProperty fileExtension;
private SimpleStringProperty server; private SimpleStringProperty server;
private SimpleIntegerProperty port; private SimpleIntegerProperty port;
@ -126,6 +128,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleBooleanProperty totalModelCountInTitle; private SimpleBooleanProperty totalModelCountInTitle;
private SimpleBooleanProperty transportLayerSecurity; private SimpleBooleanProperty transportLayerSecurity;
private SimpleBooleanProperty fastScrollSpeed; private SimpleBooleanProperty fastScrollSpeed;
private SimpleBooleanProperty useHlsdl;
private SimpleFileProperty hlsdlExecutable;
private ExclusiveSelectionProperty recordLocal; private ExclusiveSelectionProperty recordLocal;
private SimpleIntegerProperty postProcessingThreads; private SimpleIntegerProperty postProcessingThreads;
private IgnoreList ignoreList; private IgnoreList ignoreList;
@ -170,6 +174,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
leaveSpaceOnDevice = new SimpleLongProperty(null, "minimumSpaceLeftInBytes", (long) new GigabytesConverter().convertTo(settings.minimumSpaceLeftInBytes)); leaveSpaceOnDevice = new SimpleLongProperty(null, "minimumSpaceLeftInBytes", (long) new GigabytesConverter().convertTo(settings.minimumSpaceLeftInBytes));
ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs); ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs);
logFFmpegOutput = new SimpleBooleanProperty(null, "logFFmpegOutput", settings.logFFmpegOutput); logFFmpegOutput = new SimpleBooleanProperty(null, "logFFmpegOutput", settings.logFFmpegOutput);
loghlsdlOutput = new SimpleBooleanProperty(null, "loghlsdlOutput", settings.loghlsdlOutput);
fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix); fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix);
server = new SimpleStringProperty(null, "httpServer", settings.httpServer); server = new SimpleStringProperty(null, "httpServer", settings.httpServer);
port = new SimpleIntegerProperty(null, "httpPort", settings.httpPort); port = new SimpleIntegerProperty(null, "httpPort", settings.httpPort);
@ -184,6 +189,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
onlineCheckSkipsPausedModels = new SimpleBooleanProperty(null, "onlineCheckSkipsPausedModels", settings.onlineCheckSkipsPausedModels); onlineCheckSkipsPausedModels = new SimpleBooleanProperty(null, "onlineCheckSkipsPausedModels", settings.onlineCheckSkipsPausedModels);
fastScrollSpeed = new SimpleBooleanProperty(null, "fastScrollSpeed", settings.fastScrollSpeed); fastScrollSpeed = new SimpleBooleanProperty(null, "fastScrollSpeed", settings.fastScrollSpeed);
confirmationDialogs = new SimpleBooleanProperty(null, "confirmationForDangerousActions", settings.confirmationForDangerousActions); confirmationDialogs = new SimpleBooleanProperty(null, "confirmationForDangerousActions", settings.confirmationForDangerousActions);
useHlsdl = new SimpleBooleanProperty(null, "useHlsdl", settings.useHlsdl);
hlsdlExecutable = new SimpleFileProperty(null, "hlsdlExecutable", settings.hlsdlExecutable);
} }
private void createGui() { private void createGui() {
@ -269,6 +276,11 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Category.of("Advanced / Devtools", Category.of("Advanced / Devtools",
Group.of("Logging", Group.of("Logging",
Setting.of("Log FFmpeg output", logFFmpegOutput, "Log FFmpeg output to files in the system's temp directory") Setting.of("Log FFmpeg output", logFFmpegOutput, "Log FFmpeg output to files in the system's temp directory")
),
Group.of("hlsdl (experimental)",
Setting.of("Use hlsdl (if possible)", useHlsdl, "Use hlsdl to record the live streams. Some features might not work correctly."),
Setting.of("hlsdl executable", hlsdlExecutable, "Path to the hlsdl executable"),
Setting.of("Log hlsdl output", loghlsdlOutput, "Log hlsdl output to files in the system's temp directory")
) )
) )
); );
@ -320,13 +332,17 @@ public class SettingsTab extends Tab implements TabSelectionListener {
prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not())); prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
prefs.getSetting("downloadFilename").ifPresent(s -> bindEnabledProperty(s, recordLocal)); prefs.getSetting("downloadFilename").ifPresent(s -> bindEnabledProperty(s, recordLocal));
prefs.getSetting("hlsdlExecutable").ifPresent(s -> bindEnabledProperty(s, useHlsdl.not()));
prefs.getSetting("loghlsdlOutput").ifPresent(s -> bindEnabledProperty(s, useHlsdl.not()));
postProcessingStepPanel.disableProperty().bind(recordLocal.not()); postProcessingStepPanel.disableProperty().bind(recordLocal.not());
variablesHelpButton.disableProperty().bind(recordLocal); variablesHelpButton.disableProperty().bind(recordLocal);
} }
private void splitValuesChanged(ObservableValue<?> value, Object oldV, Object newV) { private void splitValuesChanged(ObservableValue<?> value, Object oldV, Object newV) {
boolean splitAfterSet = settings.splitRecordingsAfterSecs > 0; boolean splitAfterSet = settings.splitRecordingsAfterSecs > 0;
LOG.debug("after {}", settings.splitRecordingsAfterSecs);
boolean splitBiggerThanSet = settings.splitRecordingsBiggerThanBytes > 0; boolean splitBiggerThanSet = settings.splitRecordingsBiggerThanBytes > 0;
LOG.debug("bigger {}", settings.splitRecordingsBiggerThanBytes);
if (splitAfterSet && splitBiggerThanSet) { if (splitAfterSet && splitBiggerThanSet) {
settings.splitStrategy = TIME_OR_SIZE; settings.splitStrategy = TIME_OR_SIZE;
} else if (splitAfterSet) { } else if (splitAfterSet) {
@ -336,6 +352,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
} else { } else {
settings.splitStrategy = DONT; settings.splitStrategy = DONT;
} }
LOG.debug("strats {}", settings.splitStrategy);
saveConfig(); saveConfig();
} }
@ -422,11 +439,13 @@ public class SettingsTab extends Tab implements TabSelectionListener {
} }
public void saveConfig() { public void saveConfig() {
CompletableFuture.runAsync(() -> {
try { try {
Config.getInstance().save(); Config.getInstance().save();
} catch (IOException e) { } catch (IOException e) {
LOG.error("Couldn't save config", e); LOG.error("Couldn't save config", e);
} }
});
} }
@Override @Override

View File

@ -1,13 +1,58 @@
package ctbrec.ui.tabs; package ctbrec.ui.tabs;
import static ctbrec.Recording.State.*;
import static ctbrec.ui.UnicodeEmoji.*;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.StringUtil; import ctbrec.StringUtil;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.ui.*; import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.action.*; import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.JavaFxModel;
import ctbrec.ui.PreviewPopupHandler;
import ctbrec.ui.StreamSourceSelectionDialog;
import ctbrec.ui.action.CheckModelAccountAction;
import ctbrec.ui.action.EditNotesAction;
import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.IgnoreModelsAction;
import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.RemoveTimeLimitAction;
import ctbrec.ui.action.ResumeAction;
import ctbrec.ui.action.SetStopDateAction;
import ctbrec.ui.action.StartRecordingAction;
import ctbrec.ui.action.StopRecordingAction;
import ctbrec.ui.action.ToggleRecordingAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu; import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.DateTimeCellFactory; import ctbrec.ui.controls.DateTimeCellFactory;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
@ -26,13 +71,33 @@ import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.*; import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent; import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableColumn.SortType; import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.CheckBoxTableCell; import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell; import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.*; import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
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.HBox;
@ -41,30 +106,6 @@ import javafx.util.Callback;
import javafx.util.Duration; import javafx.util.Duration;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import javafx.util.converter.NumberStringConverter; import javafx.util.converter.NumberStringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static ctbrec.Recording.State.RECORDING;
import static ctbrec.ui.UnicodeEmoji.CLOCK;
import static ctbrec.ui.UnicodeEmoji.HEAVY_CHECK_MARK;
public class RecordedModelsTab extends Tab implements TabSelectionListener { public class RecordedModelsTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class); private static final Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class);
@ -409,14 +450,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private void pauseAll(ActionEvent evt) { private void pauseAll(ActionEvent evt) {
boolean yes = Dialogs.showConfirmDialog("Pause all models", "", "Pause the recording of all models?", getTabPane().getScene()); boolean yes = Dialogs.showConfirmDialog("Pause all models", "", "Pause the recording of all models?", getTabPane().getScene());
if (yes) { if (yes) {
new PauseAction(getTabPane(), recorder.getModels(), recorder).execute(); List<Model> models = recorder.getModels().stream().filter(Predicate.not(Model::isMarkedForLaterRecording)).collect(Collectors.toList());
new PauseAction(getTabPane(), models, recorder).execute();
} }
} }
private void resumeAll(ActionEvent evt) { private void resumeAll(ActionEvent evt) {
boolean yes = Dialogs.showConfirmDialog("Resume all models", "", "Resume the recording of all models?", getTabPane().getScene()); boolean yes = Dialogs.showConfirmDialog("Resume all models", "", "Resume the recording of all models?", getTabPane().getScene());
if (yes) { if (yes) {
new ResumeAction(getTabPane(), recorder.getModels(), recorder).execute(); List<Model> models = recorder.getModels().stream().filter(Predicate.not(Model::isMarkedForLaterRecording)).collect(Collectors.toList());
new ResumeAction(getTabPane(), models, recorder).execute();
} }
} }

View File

@ -31,7 +31,7 @@ import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.StreamSourceSelectionDialog; import ctbrec.ui.StreamSourceSelectionDialog;
import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.PlayAction;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.PausedIndicator; import ctbrec.ui.controls.RecordingIndicator;
import ctbrec.ui.controls.StreamPreview; import ctbrec.ui.controls.StreamPreview;
import javafx.animation.FadeTransition; import javafx.animation.FadeTransition;
import javafx.animation.FillTransition; import javafx.animation.FillTransition;
@ -51,6 +51,7 @@ import javafx.scene.control.ContextMenu;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.paint.Paint; import javafx.scene.paint.Paint;
@ -72,6 +73,11 @@ public class ThumbCell extends StackPane {
private static final Logger LOG = LoggerFactory.getLogger(ThumbCell.class); private static final Logger LOG = LoggerFactory.getLogger(ThumbCell.class);
private static final Duration ANIMATION_DURATION = new Duration(250); private static final Duration ANIMATION_DURATION = new Duration(250);
private static Image imgRecordIndicator = new Image(ThumbCell.class.getResource("/media-record-16.png").toExternalForm());
private static Image imgPauseIndicator = new Image(ThumbCell.class.getResource("/media-playback-pause-16.png").toExternalForm());
private static Image imgBookmarkIndicator = new Image(ThumbCell.class.getResource("/bookmark-new-16.png").toExternalForm());
private ModelRecordingState modelRecordingState = ModelRecordingState.NOT;
private Model model; private Model model;
private StreamPreview streamPreview; private StreamPreview streamPreview;
private ImageView iv; private ImageView iv;
@ -85,8 +91,8 @@ public class ThumbCell extends StackPane {
private Text topic; private Text topic;
private Text resolutionTag; private Text resolutionTag;
private Recorder recorder; private Recorder recorder;
private Circle recordingIndicator; private RecordingIndicator recordingIndicator;
private PausedIndicator pausedIndicator; private Tooltip recordingIndicatorTooltip;
private StackPane previewTrigger; private StackPane previewTrigger;
private int index = 0; private int index = 0;
ContextMenu popup; ContextMenu popup;
@ -178,22 +184,15 @@ public class ThumbCell extends StackPane {
StackPane.setMargin(resolutionTag, new Insets(2, 4, 2, 2)); StackPane.setMargin(resolutionTag, new Insets(2, 4, 2, 2));
getChildren().add(resolutionTag); getChildren().add(resolutionTag);
recordingIndicator = new Circle(8); recordingIndicator = new RecordingIndicator(16);
recordingIndicator.setFill(colorRecording);
recordingIndicator.setCursor(Cursor.HAND); recordingIndicator.setCursor(Cursor.HAND);
recordingIndicator.setOnMouseClicked(e -> pauseResumeAction(true)); recordingIndicator.setOnMouseClicked(this::recordingInidicatorClicked);
Tooltip tooltip = new Tooltip("Pause Recording"); recordingIndicatorTooltip = new Tooltip("Pause Recording");
Tooltip.install(recordingIndicator, tooltip); Tooltip.install(recordingIndicator, recordingIndicatorTooltip);
StackPane.setMargin(recordingIndicator, new Insets(3)); StackPane.setMargin(recordingIndicator, new Insets(3));
StackPane.setAlignment(recordingIndicator, Pos.TOP_LEFT); StackPane.setAlignment(recordingIndicator, Pos.TOP_LEFT);
getChildren().add(recordingIndicator); getChildren().add(recordingIndicator);
pausedIndicator = new PausedIndicator(16, colorRecording);
pausedIndicator.setOnMouseClicked(e -> pauseResumeAction(false));
StackPane.setMargin(pausedIndicator, new Insets(3));
StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT);
getChildren().add(pausedIndicator);
if (Config.getInstance().getSettings().livePreviews) { if (Config.getInstance().getSettings().livePreviews) {
getChildren().add(createPreviewTrigger()); getChildren().add(createPreviewTrigger());
} }
@ -231,6 +230,21 @@ public class ThumbCell extends StackPane {
update(); update();
} }
private void recordingInidicatorClicked(MouseEvent evt) {
switch(modelRecordingState) {
case RECORDING:
pauseResumeAction(true);
break;
case PAUSED:
pauseResumeAction(false);
break;
case BOOKMARKED:
recordLater(false);
break;
default:
}
}
private Node createPreviewTrigger() { private Node createPreviewTrigger() {
int s = 32; int s = 32;
previewTrigger = new StackPane(); previewTrigger = new StackPane();
@ -293,7 +307,6 @@ public class ThumbCell extends StackPane {
streamPreview.stop(); streamPreview.stop();
} }
recordingIndicator.setVisible(!visible); recordingIndicator.setVisible(!visible);
pausedIndicator.setVisible(!visible);
if (!visible) { if (!visible) {
updateRecordingIndicator(); updateRecordingIndicator();
} }
@ -445,17 +458,32 @@ public class ThumbCell extends StackPane {
c = mouseHovering ? colorHighlight : colorNormal; c = mouseHovering ? colorHighlight : colorNormal;
} }
nameBackground.setFill(c); nameBackground.setFill(c);
updateRecordingIndicator(); updateRecordingIndicator();
} }
private void updateRecordingIndicator() { private void updateRecordingIndicator() {
if (recording) { if (recording) {
recordingIndicator.setVisible(!model.isSuspended()); recordingIndicator.setVisible(true);
pausedIndicator.setVisible(model.isSuspended()); if (model.isSuspended()) {
modelRecordingState = ModelRecordingState.PAUSED;
recordingIndicator.setImage(imgPauseIndicator);
recordingIndicatorTooltip.setText("Resume Recording");
} else {
modelRecordingState = ModelRecordingState.RECORDING;
recordingIndicator.setImage(imgRecordIndicator);
recordingIndicatorTooltip.setText("Pause Recording");
}
} else {
if (model.isMarkedForLaterRecording()) {
recordingIndicator.setVisible(true);
modelRecordingState = ModelRecordingState.BOOKMARKED;
recordingIndicator.setImage(imgBookmarkIndicator);
recordingIndicatorTooltip.setText("Forget Model");
} else { } else {
recordingIndicator.setVisible(false); recordingIndicator.setVisible(false);
pausedIndicator.setVisible(false); modelRecordingState = ModelRecordingState.NOT;
recordingIndicator.setImage(null);
}
} }
} }
@ -562,9 +590,9 @@ public class ThumbCell extends StackPane {
}); });
} }
void recordLater() { void recordLater(boolean recordLater) {
model.setMarkedForLaterRecording(true); model.setMarkedForLaterRecording(recordLater);
startStopAction(true); startStopAction(recordLater);
} }
public Model getModel() { public Model getModel() {
@ -577,7 +605,6 @@ public class ThumbCell extends StackPane {
this.model.setPreview(model.getPreview()); this.model.setPreview(model.getPreview());
this.model.setTags(model.getTags()); this.model.setTags(model.getTags());
this.model.setUrl(model.getUrl()); this.model.setUrl(model.getUrl());
this.model.setSuspended(recorder.isSuspended(model));
update(); update();
} }
@ -591,9 +618,11 @@ public class ThumbCell extends StackPane {
private void update() { private void update() {
model.setSuspended(recorder.isSuspended(model)); model.setSuspended(recorder.isSuspended(model));
model.setMarkedForLaterRecording(recorder.isMarkedForLaterRecording(model));
setRecording(recorder.isTracked(model)); setRecording(recorder.isTracked(model));
updateRecordingIndicator();
setImage(model.getPreview()); setImage(model.getPreview());
String txt = recording ? " " : ""; String txt = (modelRecordingState != ModelRecordingState.NOT) ? " " : "";
txt += model.getDescription() != null ? model.getDescription() : ""; txt += model.getDescription() != null ? model.getDescription() : "";
topic.setText(txt); topic.setText(txt);
@ -693,4 +722,11 @@ public class ThumbCell extends StackPane {
model.setMarkedForLaterRecording(false); model.setMarkedForLaterRecording(false);
startStopAction(true); startStopAction(true);
} }
private enum ModelRecordingState {
RECORDING,
PAUSED,
BOOKMARKED,
NOT
}
} }

View File

@ -1,5 +1,31 @@
package ctbrec.ui.tabs; package ctbrec.ui.tabs;
import static ctbrec.ui.controls.Dialogs.*;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.event.EventBusHolder; import ctbrec.event.EventBusHolder;
@ -7,12 +33,24 @@ import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.sites.mfc.MyFreeCamsClient; import ctbrec.sites.mfc.MyFreeCamsClient;
import ctbrec.sites.mfc.MyFreeCamsModel; import ctbrec.sites.mfc.MyFreeCamsModel;
import ctbrec.ui.*; import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.TipDialog;
import ctbrec.ui.TokenLabel;
import ctbrec.ui.action.IgnoreModelsAction; import ctbrec.ui.action.IgnoreModelsAction;
import ctbrec.ui.action.OpenRecordingsDir; import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.SetStopDateAction; import ctbrec.ui.action.SetStopDateAction;
import ctbrec.ui.controls.*; import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import javafx.animation.*; import ctbrec.ui.controls.FasterVerticalScrollPaneSkin;
import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.controls.SearchPopover;
import ctbrec.ui.controls.SearchPopoverTreeList;
import javafx.animation.FadeTransition;
import javafx.animation.Interpolator;
import javafx.animation.ParallelTransition;
import javafx.animation.ScaleTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -28,24 +66,34 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Parent; import javafx.scene.Parent;
import javafx.scene.control.*; import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.*; import javafx.scene.input.Clipboard;
import javafx.scene.layout.*; import javafx.scene.input.ClipboardContent;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.transform.Transform; import javafx.scene.transform.Transform;
import javafx.util.Duration; import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import static ctbrec.ui.controls.Dialogs.showError;
public class ThumbOverviewTab extends Tab implements TabSelectionListener { public class ThumbOverviewTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class); private static final Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class);
@ -441,7 +489,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
MenuItem addPaused = new MenuItem("Add in paused state"); MenuItem addPaused = new MenuItem("Add in paused state");
addPaused.setOnAction(e -> addPaused(getSelectedThumbCells(cell))); addPaused.setOnAction(e -> addPaused(getSelectedThumbCells(cell)));
MenuItem recordLater = new MenuItem("Record Later"); MenuItem recordLater = new MenuItem("Record Later");
recordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell))); recordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), true));
MenuItem removeRecordLater = new MenuItem("Forget Model");
removeRecordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), false));
MenuItem addRemoveBookmark = recorder.isMarkedForLaterRecording(model) ? removeRecordLater : recordLater;
MenuItem pause = new MenuItem("Pause Recording"); MenuItem pause = new MenuItem("Pause Recording");
pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true)); pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true));
@ -476,10 +527,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
if (modelIsTrackedByRecorder) { if (modelIsTrackedByRecorder) {
contextMenu.getItems().addAll(pauseResume, recordLater); contextMenu.getItems().addAll(pauseResume, recordLater);
} else { } else {
contextMenu.getItems().addAll(recordUntil, addPaused); contextMenu.getItems().addAll(recordUntil, addPaused, addRemoveBookmark);
if (!recorder.isMarkedForLaterRecording(model)) {
contextMenu.getItems().add(recordLater);
}
} }
contextMenu.getItems().add(new SeparatorMenuItem()); contextMenu.getItems().add(new SeparatorMenuItem());
if (site.supportsFollow()) { if (site.supportsFollow()) {
@ -500,9 +548,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
return contextMenu; return contextMenu;
} }
private void recordLater(List<ThumbCell> list) { private void recordLater(List<ThumbCell> list, boolean recordLater) {
for (ThumbCell cell : list) { for (ThumbCell cell : list) {
cell.recordLater(); cell.recordLater(recordLater);
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

View File

@ -28,6 +28,8 @@ until a recording is finished. 0 means unlimited.
- **generatePlaylist** (server only) - [`true`,`false`] Generate a playlist once a recording terminates. - **generatePlaylist** (server only) - [`true`,`false`] Generate a playlist once a recording terminates.
- **hlsdlExecutable** - Path to the hlsdl executable, which is used, if `useHlsdl` is set to true
- **httpPort** - [1 - 65536] The TCP port, the server listens on. In the server config, this will tell the server, which port to use. In the application this will set - **httpPort** - [1 - 65536] The TCP port, the server listens on. In the server config, this will tell the server, which port to use. In the application this will set
the port ctbrec tries to connect to, if it is run in remote mode. the port ctbrec tries to connect to, if it is run in remote mode.
@ -43,7 +45,9 @@ the port ctbrec tries to connect to, if it is run in remote mode.
- **livePreviews** (app only) - Enables the live preview feature in the app. - **livePreviews** (app only) - Enables the live preview feature in the app.
- **logFFmpegOutput** - The output from FFmpeg (from recordings or post-processing steps) will be logged in temporary files. - **logFFmpegOutput** - [`true`,`false`] The output from FFmpeg (from recordings or post-processing steps) will be logged in temporary files.
- **loghlsdlOutput** - [`true`,`false`] The output from hlsdl will be logged in temporary files. Only in effect, if `useHlsdl` is set to true
- **minimumResolution** - [1 - 2147483647]. Sets the minimum video height for a recording. ctbrec tries to find a stream quality, which is higher than or equal to this value. If the only provided stream quality is below this threshold, ctbrec won't record the stream. - **minimumResolution** - [1 - 2147483647]. Sets the minimum video height for a recording. ctbrec tries to find a stream quality, which is higher than or equal to this value. If the only provided stream quality is below this threshold, ctbrec won't record the stream.
@ -73,5 +77,7 @@ which have the defined length (roughly). Has to be activated with `splitStrategy
- **splitRecordingsBiggerThanBytes** - [0 - 9223372036854775807] in bytes. Split recordings, if the size on disk exceeds this value. The recordings are split up into several individual recordings, - **splitRecordingsBiggerThanBytes** - [0 - 9223372036854775807] in bytes. Split recordings, if the size on disk exceeds this value. The recordings are split up into several individual recordings,
which have the defined size (roughly). Has to be activated with `splitStrategy`. which have the defined size (roughly). Has to be activated with `splitStrategy`.
- **useHlsdl** - [`true`,`false`] Use hlsdl to record the live streams. You also have to set `hlsdlExecutable`, if hlsdl is not globally available on your system. hlsdl won't be used for MV Live, LiveJasmin and Showup.
- **webinterface** (server only) - [`true`,`false`] Enables the webinterface for the server. You can access it with http://host:port/static/index.html Don't activate this on - **webinterface** (server only) - [`true`,`false`] Enables the webinterface for the server. You can access it with http://host:port/static/index.html Don't activate this on
a machine, which can be accessed from the internet, because this is totally unprotected at the moment. a machine, which can be accessed from the internet, because this is totally unprotected at the moment.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -17,6 +17,7 @@ import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.HttpHeaderFactory; import ctbrec.recorder.download.HttpHeaderFactory;
import ctbrec.recorder.download.HttpHeaderFactoryImpl; import ctbrec.recorder.download.HttpHeaderFactoryImpl;
import ctbrec.recorder.download.hls.HlsDownload; import ctbrec.recorder.download.hls.HlsDownload;
import ctbrec.recorder.download.hls.HlsdlDownload;
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload; import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import okhttp3.Request; import okhttp3.Request;
@ -275,12 +276,16 @@ public abstract class AbstractModel implements Model {
@Override @Override
public Download createDownload() { public Download createDownload() {
if (Config.getInstance().getSettings().useHlsdl) {
return new HlsdlDownload();
} else {
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) { if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
return new HlsDownload(getSite().getHttpClient()); return new HlsDownload(getSite().getHttpClient());
} else { } else {
return new MergedFfmpegHlsDownload(getSite().getHttpClient()); return new MergedFfmpegHlsDownload(getSite().getHttpClient());
} }
} }
}
@Override @Override
public HttpHeaderFactory getHttpHeaderFactory() { public HttpHeaderFactory getHttpHeaderFactory() {

View File

@ -213,7 +213,7 @@ public class Config {
return settings; return settings;
} }
public void save() throws IOException { public synchronized void save() throws IOException {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter()) .add(Model.class, new ModelJsonAdapter())
.add(PostProcessor.class, new PostProcessorJsonAdapter()) .add(PostProcessor.class, new PostProcessorJsonAdapter())

View File

@ -69,6 +69,7 @@ public class Settings {
public String flirt4freePassword; public String flirt4freePassword;
public String flirt4freeUsername; public String flirt4freeUsername;
public boolean generatePlaylist = true; public boolean generatePlaylist = true;
public String hlsdlExecutable = "hlsdl";
public int httpPort = 8080; public int httpPort = 8080;
public int httpSecurePort = 8443; public int httpSecurePort = 8443;
public String httpServer = "localhost"; public String httpServer = "localhost";
@ -84,6 +85,7 @@ public class Settings {
public boolean livePreviews = false; public boolean livePreviews = false;
public boolean localRecording = true; public boolean localRecording = true;
public boolean logFFmpegOutput = false; public boolean logFFmpegOutput = false;
public boolean loghlsdlOutput = false;
public int minimumResolution = 0; public int minimumResolution = 0;
public int maximumResolution = 8640; public int maximumResolution = 8640;
public int maximumResolutionPlayer = 0; public int maximumResolutionPlayer = 0;
@ -156,6 +158,7 @@ public class Settings {
public boolean transportLayerSecurity = true; public boolean transportLayerSecurity = true;
public int thumbWidth = 180; public int thumbWidth = 180;
public boolean updateThumbnails = true; public boolean updateThumbnails = true;
public boolean useHlsdl = false;
@Deprecated @Deprecated
public String username = ""; public String username = "";
public int windowHeight = 800; public int windowHeight = 800;

View File

@ -9,15 +9,15 @@ import java.util.concurrent.ScheduledExecutorService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class FfmpegStreamRedirector implements Runnable { public class ProcessStreamRedirector implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(FfmpegStreamRedirector.class); private static final Logger LOG = LoggerFactory.getLogger(ProcessStreamRedirector.class);
private InputStream in; private InputStream in;
private OutputStream out; private OutputStream out;
private boolean keepGoing = true; private boolean keepGoing = true;
private ScheduledExecutorService executor; private ScheduledExecutorService executor;
public FfmpegStreamRedirector(ScheduledExecutorService executor, InputStream in, OutputStream out) { public ProcessStreamRedirector(ScheduledExecutorService executor, InputStream in, OutputStream out) {
super(); super();
this.executor = executor; this.executor = executor;
this.in = in; this.in = in;
@ -37,7 +37,7 @@ public class FfmpegStreamRedirector implements Runnable {
executor.schedule(this, 100, MILLISECONDS); executor.schedule(this, 100, MILLISECONDS);
} }
} catch (Exception e) { } catch (Exception e) {
LOG.debug("Error while reading from FFmpeg output stream: {}", e.getLocalizedMessage()); LOG.debug("Error while reading from process output stream: {}", e.getLocalizedMessage());
keepGoing = false; keepGoing = false;
} }
} }

View File

@ -15,7 +15,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.io.DevNull; import ctbrec.io.DevNull;
import ctbrec.io.FfmpegStreamRedirector; import ctbrec.io.ProcessStreamRedirector;
import ctbrec.recorder.download.ProcessExitedUncleanException; import ctbrec.recorder.download.ProcessExitedUncleanException;
public class FFmpeg { public class FFmpeg {
@ -30,8 +30,8 @@ public class FFmpeg {
private Consumer<Integer> exitCallback; private Consumer<Integer> exitCallback;
private File ffmpegLog = null; private File ffmpegLog = null;
private OutputStream ffmpegLogStream; private OutputStream ffmpegLogStream;
private FfmpegStreamRedirector stdoutRedirector; private ProcessStreamRedirector stdoutRedirector;
private FfmpegStreamRedirector stderrRedirector; private ProcessStreamRedirector stderrRedirector;
private FFmpeg() {} private FFmpeg() {}
@ -45,7 +45,7 @@ public class FFmpeg {
}; };
} }
public void exec(String[] cmdline, String[] env, File executionDir) throws IOException, InterruptedException { public void exec(String[] cmdline, String[] env, File executionDir) throws IOException {
LOG.debug("FFmpeg command line: {}", Arrays.toString(cmdline)); LOG.debug("FFmpeg command line: {}", Arrays.toString(cmdline));
process = Runtime.getRuntime().exec(cmdline, env, executionDir); process = Runtime.getRuntime().exec(cmdline, env, executionDir);
afterStart(); afterStart();
@ -83,8 +83,8 @@ public class FFmpeg {
} else { } else {
ffmpegLogStream = new DevNull(); ffmpegLogStream = new DevNull();
} }
stdoutRedirector = new FfmpegStreamRedirector(processOutputReader, process.getInputStream(), ffmpegLogStream); stdoutRedirector = new ProcessStreamRedirector(processOutputReader, process.getInputStream(), ffmpegLogStream);
stderrRedirector = new FfmpegStreamRedirector(processOutputReader, process.getErrorStream(), ffmpegLogStream); stderrRedirector = new ProcessStreamRedirector(processOutputReader, process.getErrorStream(), ffmpegLogStream);
processOutputReader.submit(stdoutRedirector); processOutputReader.submit(stdoutRedirector);
processOutputReader.submit(stderrRedirector); processOutputReader.submit(stderrRedirector);
} }

View File

@ -143,9 +143,9 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
segmentDownloadFinished(result.get()); segmentDownloadFinished(result.get());
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
e.printStackTrace(); LOG.error("Error in segmentDownloadFinished", e);
} catch (ExecutionException e) { } catch (ExecutionException e) {
e.printStackTrace(); LOG.error("Error in segmentDownloadFinished", e);
} }
}); });
} }
@ -173,7 +173,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
running = false; running = false;
} }
} catch (ExecutionException e1) { } catch (ExecutionException e1) {
modelState = ctbrec.Model.State.UNKNOWN; modelState = State.UNKNOWN;
} }
LOG.info(errorMsg, model, modelState); LOG.info(errorMsg, model, modelState);
waitSomeTime(TEN_SECONDS); waitSomeTime(TEN_SECONDS);

View File

@ -0,0 +1,154 @@
package ctbrec.recorder.download.hls;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.io.DevNull;
import ctbrec.io.ProcessStreamRedirector;
import ctbrec.recorder.download.ProcessExitedUncleanException;
public class Hlsdl {
private static final Logger LOG = LoggerFactory.getLogger(Hlsdl.class);
private static ScheduledExecutorService processOutputReader = Executors.newScheduledThreadPool(2, createThreadFactory("hlsdl output stream reader"));
private Process process;
private boolean logOutput = false;
private Consumer<Process> startCallback;
private Consumer<Integer> exitCallback;
private File processLog = null;
private OutputStream processLogStream;
private ProcessStreamRedirector stdoutRedirector;
private ProcessStreamRedirector stderrRedirector;
private Hlsdl() {}
private static ThreadFactory createThreadFactory(String name) {
return r -> {
Thread t = new Thread(r);
t.setName(name);
t.setDaemon(true);
t.setPriority(Thread.MIN_PRIORITY);
return t;
};
}
public void exec(String[] cmdline, String[] env, File executionDir) throws IOException {
LOG.debug("hlsdl command line: {}", Arrays.toString(cmdline));
process = Runtime.getRuntime().exec(cmdline, env, executionDir);
afterStart();
}
private void afterStart() throws IOException {
notifyStartCallback(process);
setupLogging();
}
public void shutdown(int exitCode) throws IOException {
LOG.debug("hlsdl exit code was {}", exitCode);
processLogStream.flush();
processLogStream.close();
stdoutRedirector.setKeepGoing(false);
stderrRedirector.setKeepGoing(false);
notifyExitCallback(exitCode);
if (exitCode != 1) {
if (processLog != null && processLog.exists()) {
Files.delete(processLog.toPath());
}
} else {
throw new ProcessExitedUncleanException("hlsdl exit code was " + exitCode);
}
}
private void setupLogging() throws IOException {
if (logOutput) {
if (processLog == null) {
processLog = File.createTempFile("hlsdl_", ".log");
}
LOG.debug("Logging hlsdl output to {}", processLog);
processLog.deleteOnExit();
processLogStream = new FileOutputStream(processLog);
} else {
processLogStream = new DevNull();
}
stdoutRedirector = new ProcessStreamRedirector(processOutputReader, process.getInputStream(), processLogStream);
stderrRedirector = new ProcessStreamRedirector(processOutputReader, process.getErrorStream(), processLogStream);
processOutputReader.submit(stdoutRedirector);
processOutputReader.submit(stderrRedirector);
}
private void notifyStartCallback(Process process) {
try {
startCallback.accept(process);
} catch(Exception e) {
LOG.error("Exception in onStart callback", e);
}
}
private void notifyExitCallback(int exitCode) {
try {
exitCallback.accept(exitCode);
} catch(Exception e) {
LOG.error("Exception in onExit callback", e);
}
}
public int waitFor() throws InterruptedException {
int exitCode = process.waitFor();
try {
shutdown(exitCode);
} catch (IOException e) {
LOG.error("Error while shutting down hlsdl process", e);
}
return exitCode;
}
public static class Builder {
private boolean logOutput = false;
private File logFile;
private Consumer<Process> startCallback;
private Consumer<Integer> exitCallback;
public Builder logOutput(boolean logOutput) {
this.logOutput = logOutput;
return this;
}
public Builder logFile(File logFile) {
this.logFile = logFile;
return this;
}
public Builder onStarted(Consumer<Process> callback) {
this.startCallback = callback;
return this;
}
public Builder onExit(Consumer<Integer> callback) {
this.exitCallback = callback;
return this;
}
public Hlsdl build() {
Hlsdl instance = new Hlsdl();
instance.logOutput = logOutput;
instance.startCallback = startCallback != null ? startCallback : p -> {};
instance.exitCallback = exitCallback != null ? exitCallback : exitCode -> {};
instance.processLog = logFile;
return instance;
}
}
}

View File

@ -0,0 +1,205 @@
package ctbrec.recorder.download.hls;
import static ctbrec.recorder.download.StreamSource.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.OS;
import ctbrec.Recording;
import ctbrec.recorder.download.AbstractDownload;
import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.ProcessExitedUncleanException;
import ctbrec.recorder.download.StreamSource;
public class HlsdlDownload extends AbstractDownload {
private static final transient Logger LOG = LoggerFactory.getLogger(HlsdlDownload.class);
protected File targetFile;
private transient Hlsdl hlsdl;
protected transient Process hlsdlProcess;
protected transient boolean running = true;
@Override
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
super.init(config, model, startTime, executorService);
String fileSuffix = config.getSettings().ffmpegFileSuffix;
targetFile = config.getFileForRecording(model, fileSuffix, startTime);
createTargetDirectory();
startHlsdlProcess();
if (hlsdlProcess == null) {
throw new ProcessExitedUncleanException("Couldn't spawn FFmpeg");
}
}
protected void createTargetDirectory() throws IOException {
Files.createDirectories(targetFile.getParentFile().toPath());
}
@Override
public Download call() throws Exception {
try {
if (running && !hlsdlProcess.isAlive()) {
running = false;
int exitValue = hlsdlProcess.exitValue();
hlsdl.shutdown(exitValue);
}
rescheduleTime = Instant.now().plusSeconds(1);
if (splittingStrategy.splitNecessary(this)) {
stop();
}
} catch (ProcessExitedUncleanException e) {
LOG.error("hlsdl exited unclean", e);
}
return this;
}
private void startHlsdlProcess() {
try {
String[] cmdline = prepareCommandLine();
hlsdl = new Hlsdl.Builder()
.logOutput(config.getSettings().logFFmpegOutput)
.onStarted(p -> hlsdlProcess = p)
.build();
hlsdl.exec(cmdline, OS.getEnvironment(), targetFile.getParentFile());
} catch (Exception e) {
LOG.error("Error starting hlsdl", e);
}
}
private String[] prepareCommandLine() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
String playlistUrl = getSegmentPlaylistUrl(model);
Map<String, String> headers = model.getHttpHeaderFactory().createSegmentPlaylistHeaders();
String[] cmdline = new String[9 + headers.size() * 2];
int idx = 0;
cmdline[idx++] = config.getSettings().hlsdlExecutable;
cmdline[idx++] = "-c";
cmdline[idx++] = "-r";
cmdline[idx++] = "3";
cmdline[idx++] = "-w";
cmdline[idx++] = "3";
for (Entry<String, String> header : headers.entrySet()) {
cmdline[idx++] = "-h";
cmdline[idx++] = header.getKey() + ": " + header.getValue();
}
cmdline[idx++] = "-o";
cmdline[idx++] = targetFile.getCanonicalPath();
cmdline[idx] = playlistUrl;
return cmdline;
}
protected String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
LOG.debug("{} stream idx: {}", model.getName(), model.getStreamUrlIndex());
List<StreamSource> streamSources = model.getStreamSources();
Collections.sort(streamSources);
for (StreamSource streamSource : streamSources) {
LOG.debug("{} src {}", model.getName(), streamSource);
}
String url = null;
if (model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) {
// TODO don't use the index, but the bandwidth. if the bandwidth does not match, take the closest one
LOG.debug("{} selected {}", model.getName(), streamSources.get(model.getStreamUrlIndex()));
url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl();
} else {
// filter out stream resolutions, which are out of range of the configured min and max
int minRes = Config.getInstance().getSettings().minimumResolution;
int maxRes = Config.getInstance().getSettings().maximumResolution;
List<StreamSource> filteredStreamSources = streamSources.stream()
.filter(src -> src.height == 0 || src.height == UNKNOWN || minRes <= src.height)
.filter(src -> src.height == 0 || src.height == UNKNOWN || maxRes >= src.height)
.collect(Collectors.toList());
if (filteredStreamSources.isEmpty()) {
throw new ExecutionException(new RuntimeException("No stream left in playlist"));
} else {
LOG.debug("{} selected {}", model.getName(), filteredStreamSources.get(filteredStreamSources.size() - 1));
url = filteredStreamSources.get(filteredStreamSources.size() - 1).getMediaPlaylistUrl();
}
}
LOG.debug("Segment playlist url {}", url);
return url;
}
@Override
public void stop() {
if (running) {
running = false;
if (hlsdlProcess != null) {
hlsdlProcess.destroy();
if (hlsdlProcess.isAlive()) {
LOG.info("hlsdl didn't terminate. Destroying the process with force!");
hlsdlProcess.destroyForcibly();
hlsdlProcess = null;
}
}
}
}
@Override
public void postprocess(Recording recording) {
// nothing to do
}
@Override
public File getTarget() {
return targetFile;
}
@Override
public String getPath(Model model) {
String absolutePath = targetFile.getAbsolutePath();
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
String relativePath = absolutePath.replaceFirst(Pattern.quote(recordingsDir), "");
return relativePath;
}
@Override
public boolean isSingleFile() {
return true;
}
@Override
public long getSizeInByte() {
return getTarget().length();
}
@Override
public Model getModel() {
return model;
}
@Override
public void finalizeDownload() {
// nothing to do
}
@Override
public boolean isRunning() {
return running;
}
}

View File

@ -127,11 +127,6 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
ffmpeg.exec(cmdline, new String[0], target.getParentFile()); ffmpeg.exec(cmdline, new String[0], target.getParentFile());
} catch (IOException | ProcessExitedUncleanException e) { } catch (IOException | ProcessExitedUncleanException e) {
LOG.error("Error in FFmpeg thread", e); LOG.error("Error in FFmpeg thread", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (running) {
LOG.info("Interrupted while waiting for ffmpeg", e);
}
} }
} }

View File

@ -0,0 +1,37 @@
package ctbrec.sites.fc2live;
import java.io.IOException;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.recorder.download.hls.HlsdlDownload;
public class Fc2HlsdlDownload extends HlsdlDownload {
private static final Logger LOG = LoggerFactory.getLogger(Fc2HlsdlDownload.class);
@Override
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
super.init(config, model, startTime, executorService);
Fc2Model fc2Model = (Fc2Model) model;
try {
fc2Model.openWebsocket();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.error("Couldn't start download for {}", model, e);
}
}
@Override
public void finalizeDownload() {
Fc2Model fc2Model = (Fc2Model) model;
fc2Model.closeWebsocket();
super.finalizeDownload();
}
}

View File

@ -381,12 +381,16 @@ public class Fc2Model extends AbstractModel {
@Override @Override
public Download createDownload() { public Download createDownload() {
if(Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) { if (Config.getInstance().getSettings().useHlsdl) {
return new Fc2HlsdlDownload();
} else {
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
return new Fc2HlsDownload(getSite().getHttpClient()); return new Fc2HlsDownload(getSite().getHttpClient());
} else { } else {
return new Fc2MergedHlsDownload(getSite().getHttpClient()); return new Fc2MergedHlsDownload(getSite().getHttpClient());
} }
} }
}
@Override @Override
public void readSiteSpecificData(JsonReader reader) throws IOException { public void readSiteSpecificData(JsonReader reader) throws IOException {